feat(cron): add the host OS crontab to the /cron page#80
Conversation
The /cron page only surfaced Claude Code's session-scoped CronCreate jobs.
This adds a second, read-only section showing the machine's real cron
daemon, grouped by origin so a user's Claude skill crons stand out from OS
noise.
API — new router `routers/system_cron.py`, `GET /cron/system`:
- reads the user crontab (`crontab -l`), /etc/crontab, /etc/cron.d/*, and
the run-parts periodic dirs; each source fails independently and never
blocks the others
- classifies every entry by origin: claude-skill (with the skill name
extracted), claude, system, user — command path wins over source
- origin detection honours a custom CLAUDE_KARMA_CLAUDE_BASE, with a
~/.claude fallback, and normalises path separators
- cross-platform: works on Linux and macOS/BSD; on Windows it returns a
clean `supported: false` payload (no cron daemon there) instead of erroring
- next-run inferred via croniter where the expression is parseable
Frontend — new self-fetching component `SystemCrontabSection.svelte`:
- collapsible groups per origin, colored badges, sorted by next run
- the system group is collapsed by default to hide OS noise
- graceful empty / unsupported-platform / error states
Kept additive to minimise merge surface: the only edits to existing files
are one import + one include_router in main.py, and one import + one tag in
cron/+page.svelte.
Tests: routers/system_cron parsing + classification (user vs system rows,
macros, env/comment skipping, custom base, command-wins-over-source).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
a9d90e2 to
6aab868
Compare
Reworks the /cron System crontab view for consistency with the existing Claude cron list and adds job-purpose surfacing. Frontend: - Replace the bottom-of-page section with a "Claude crons / System crontab" segmented toggle; persist the choice in the URL (?source=system) - Swap nested origin accordions for origin filter chips - Render each entry as an expandable card mirroring the Claude cron cards, with a schedule timeline reusing the fire-history styles - Show each job's purpose (crontab comment, or skill SKILL.md description) - Translate all UI strings from French to English - Fix duplicate-key crash (key rows by source + raw line) Backend (routers/system_cron.py): - Make next-run/timeline timestamps timezone-aware - Add recent_runs/upcoming_runs computed from the cron expression - Capture the comment block above an entry as its description; skill crons fall back to their SKILL.md description Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Follow-up fixes to the system crontab rework: - _skill_description: reject path separators and dot segments in the skill name before building a filesystem path (the name derives from the crontab command, so it must not escape the skills dir) - Add tests for the previously-untested helpers: _parse_text (comment → description capture and block-reset rules), _skill_description (SKILL.md read + path-traversal rejection), _run_times (tz-aware timeline) - Key system cron rows by entry + index so two byte-identical crontab lines can't trigger a duplicate-key crash - Fix a stale component header comment (panel no longer shows the raw line) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On the System crontab tab the top stats now reflect the OS crontab (total entries, Claude-related = skills + ~/.claude scripts, user & system) instead of the meaningless Claude-cron counts. SystemCrontabSection reports a small summary up via an onSummary callback after it fetches /cron/system. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The every-Nth-hour shortcut accepted any minute field, so "* */2 * * *" (every minute during even hours) was mislabelled "every 2h". Require the minute to be 0 so only true every-N-hours expressions get that label; others fall back to the raw expression. Adds a regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JayantDevkar
left a comment
There was a problem hiding this comment.
Approved ✅
Verified end-to-end on macOS — worktree off this branch, FastAPI on :8000 + SvelteKit on :5173, driven via Playwright. Works as described.
Backend (GET /cron/system)
- Empty /
supported:truepayload on a host with no crontab; correct cross-platform shape. - With a representative crontab: correct origin classification (
claude-skill/claude/user), skill-name extraction,PATH=env line filtered,by_origin/by_sourceaggregation. - Description enrichment: inline comment wins over SKILL.md; SKILL.md fallback is first-sentence-truncated.
schedule_human+ croniter timeline correct across*/30,0 */2,0 3,30 9,@daily,* * * * *,5 4 * * 0;@reboot→ "at boot" withnext_run: null(graceful).- Probes:
POST→ 405, badinclude_periodic→ 422, endpoint present in OpenAPI. - Tests:
pytest tests/test_system_cron.py→ 39 passed (no CI ran on this fork PR, so I ran it locally).
Frontend (/cron)
- Source toggle with
?source=systemURL persistence; stat strip reacts to the toggle; origin filter chips (only existing origins shown); expandable per-entry fire-timeline (past / now / upcoming). - Self-fetch
GET /cron/system → 200, 0 console errors; renders cleanly in dark mode (design tokens). Original Claude-crons view ({:else}branch) untouched.
One non-blocking nit: next_run is a fetch-time snapshot, so a fast job's relative label drifts stale (an every-minute job went "now" → "2m ago" without a reload). The Reload button covers it; a periodic re-fetch / client recompute would make it self-correcting. Fine to land as-is.
Clean, strictly-additive change. 👍
test_system_cron.py imports routers.system_cron, which requires fastapi. The unit-test job (Python Tests) installs only .[dev] (no fastapi) and runs pytest tests/ --ignore=tests/api/, so collecting this file there raised ModuleNotFoundError: No module named 'fastapi', failing all 3 matrix jobs. Move it to tests/api/ (the API Integration Tests job installs requirements.txt incl. fastapi) and fix the sys.path depth accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JayantDevkar
left a comment
There was a problem hiding this comment.
Re-approving — my prior approval was dismissed by the new push. The only change since is afc35a46 (fix(tests): move system_cron test into tests/api so it runs with fastapi): a pure rename of the test file + a one-line sys.path depth fix, no source/frontend changes. Full CI matrix is green on this head. Verification from my earlier review still stands.
Summary
The
/cronpage only surfaced Claude Code's session-scopedCronCreatejobs. This adds a second, read-only section showing the host machine's real cron daemon, grouped by origin so a user's Claude skill crons stand out from OS noise.Changes
routers/system_cron.py→GET /cron/system:crontab -l),/etc/crontab,/etc/cron.d/*, and the run-parts periodic dirs; each source fails independently and never blocks the othersclaude-skill(with the skill name extracted),claude,system,user— command path wins over sourceCLAUDE_KARMA_CLAUDE_BASE, with a~/.claudefallback, and normalises path separatorssupported: falsepayload (no cron daemon there) instead of erroringcroniterwhere the expression is parseableSystemCrontabSection.svelte: collapsible groups per origin, colored badges, sorted by next run, system group collapsed by default, graceful empty / unsupported / error statesinclude_routerinmain.py; one import + one tag incron/+page.svelteComponent
Type
Testing
cd api && pytest tests/test_system_cron.py→ 24 passed; full suite green except one pre-existing, unrelatedtest_background_shellsenv test that also fails onmain)npm run check→ 0 errors; new files add no warnings)npm run lint→ 0 errors in new files)/cronrenders HTTP 200)Screenshots
Omitted — developed in a headless CLI environment. Happy to attach a screenshot of the grouped section on request.
Related Issues
None.