Skip to content

feat(cron): add the host OS crontab to the /cron page#80

Merged
JayantDevkar merged 6 commits into
JayantDevkar:mainfrom
Mktotoy:feat/cron-linux-crontab
Jun 16, 2026
Merged

feat(cron): add the host OS crontab to the /cron page#80
JayantDevkar merged 6 commits into
JayantDevkar:mainfrom
Mktotoy:feat/cron-linux-crontab

Conversation

@Mktotoy

@Mktotoy Mktotoy commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Summary

The /cron page only surfaced Claude Code's session-scoped CronCreate jobs. 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

  • New API router routers/system_cron.pyGET /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: Linux + macOS/BSD; on Windows returns a clean supported: false payload (no cron daemon there) instead of erroring
    • next-run inferred via croniter where the expression is parseable
  • New self-fetching component SystemCrontabSection.svelte: collapsible groups per origin, colored badges, sorted by next run, system group collapsed by default, graceful empty / unsupported / error states
  • Additive wiring only: one import + one include_router in main.py; one import + one tag in cron/+page.svelte

Component

  • API (FastAPI backend)
  • Frontend (SvelteKit dashboard)
  • Captain Hook (hook models)
  • Hook Scripts
  • Documentation
  • CI / Build

Type

  • Feature (new functionality)

Testing

  • API tests pass (cd api && pytest tests/test_system_cron.py → 24 passed; full suite green except one pre-existing, unrelated test_background_shells env test that also fails on main)
  • Frontend type-checks (npm run check → 0 errors; new files add no warnings)
  • Frontend lints clean (npm run lint → 0 errors in new files)
  • Captain Hook tests pass — N/A (no hook-model changes)
  • Manually tested (endpoint returns parsed crontab; /cron renders HTTP 200)

Screenshots

Omitted — developed in a headless CLI environment. Happy to attach a screenshot of the grouped section on request.

Related Issues

None.

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>
@Mktotoy Mktotoy force-pushed the feat/cron-linux-crontab branch from a9d90e2 to 6aab868 Compare June 10, 2026 10:06
the-non-expert and others added 4 commits June 16, 2026 08:12
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 JayantDevkar left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:true payload 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_source aggregation.
  • 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" with next_run: null (graceful).
  • Probes: POST → 405, bad include_periodic → 422, endpoint present in OpenAPI.
  • Tests: pytest tests/test_system_cron.py39 passed (no CI ran on this fork PR, so I ran it locally).

Frontend (/cron)

  • Source toggle with ?source=system URL 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 JayantDevkar left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@JayantDevkar JayantDevkar merged commit 40b3e29 into JayantDevkar:main Jun 16, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants