Skip to content

feat(dashboard): per-profile cron toggle UI - #26614

Closed
cypres0099 wants to merge 2 commits into
NousResearch:mainfrom
cypres0099:feat/dashboard-per-profile-cron
Closed

feat(dashboard): per-profile cron toggle UI#26614
cypres0099 wants to merge 2 commits into
NousResearch:mainfrom
cypres0099:feat/dashboard-per-profile-cron

Conversation

@cypres0099

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds a Profile dropdown to the dashboard's Cron screen so jobs in every installed profile can be managed from the same dashboard daemon. Mirrors the precedent set by #25116 for the Skills page.

Until now, /api/cron/jobs only knew the active profile (whichever HERMES_HOME the dashboard process was launched under), so operators with multi-profile installs (orchestrator + worker sub-profiles, copywriter / SEO / creative bots, etc.) had to SSH to the host and run hermes --profile <name> cron list to see what was scheduled — easy to miss when a sub-profile's cron quietly stops firing.

Two intentional divergences from #25116:

  • Default view is "All", not the active profile. Skills defaults to active because toggling skills is per-profile config work; cron is operations work that benefits from cross-profile visibility.
  • Trigger is gated to running profiles only. Writing next_run_at = now is a silent no-op without a live dispatcher tick. Pause / resume / edit / delete / create are filesystem-only and work regardless of gateway state.

Related Issue

Depends on #25116 (per-profile skills toggle UI). This PR is a sibling: it imports the is_active/_active_profile_path_str and per-profile-route patterns introduced there. The diff against main currently includes #25116's commits because this branch sits on top of feat/dashboard-per-profile-skills. Once #25116 merges, this PR's diff narrows to the cron-specific changes only. Order of merge: #25116 first, then this.

(Author-attribution check tracked by #26608.)

Type of Change

  • ✨ Feature

Changes Made

Backend (hermes_cli/web_server.py):

  • Add 8 routes under /api/profiles/{name}/cron/jobs* mirroring the legacy /api/cron/jobs* shape: list, get-single, create, update, pause, resume, trigger, delete.
  • Add web-layer _load_profile_jobs / _save_profile_jobs helpers that read/write <profile_dir>/cron/jobs.json atomically (tempfile + replace). cron/jobs.py's module-global JOBS_FILE and _jobs_file_lock are left untouched — same constraint feat(dashboard): per-profile skills toggle UI #25116 worked around for load_config / save_config.
  • Trigger route additionally calls _check_gateway_running(profile_dir) and returns 409 Conflict when False (defense in depth; the UI disables the button as primary UX).
  • Add gateway_running: bool to ProfileInfo response via _profile_to_dict and _fallback_profile_dicts. Reuses the existing _check_gateway_running helper from hermes_cli/profiles.py (PID-file plus process-identity verification).
  • Per-profile in-process write lock (threading.Lock dict-of-locks keyed by profile name) serialises concurrent dashboard writes to the same profile's jobs.json.

Frontend (web/src/lib/api.ts):

  • Add 8 profile-scoped cron client functions (getProfileCronJobs, createProfileCronJob, etc.).
  • Extend ProfileInfo interface with gateway_running?: boolean (optional for backward-compat with older gateways, matching the is_active? posture from feat(dashboard): per-profile skills toggle UI #25116).

Frontend (web/src/pages/CronPage.tsx):

  • Profile dropdown (hidden when only one profile is installed) with All as the first/default option.
  • "All" view aggregates jobs from every installed profile via Promise.allSettled; per-row owning-profile badge with the active profile annotated (active).
  • Single-profile filter hides the profile badge and fetches only that profile's jobs.
  • Trigger button is disabled when row.gateway_running === false, with tooltip Gateway not running for profile <name>.
  • Create form gains a Profile selector when in "All" view (defaults to the active profile); the selector is hidden when filtered to a specific profile.
  • Schedule cell renders Repeat: ∞ for recurring jobs and Repeat: 1/1 for one-shots — verbatim what hermes cron list shows — so the every-prefix foot-gun ('4320m' = one-shot, 'every 4320m' = recurring) is visible at a glance.

Tests (tests/hermes_cli/test_web_server.py):

  • 13 new tests in TestNewEndpoints covering: list/create/get roundtrip; pause/resume/trigger/delete lifecycle; PUT update; multiline-prompt roundtrip (closes the hermes cron edit --prompt foot-gun for the dashboard write path); recurring-vs-one-shot schedule kind; empty-list for new profile; unknown-profile 404; invalid-name 400; trigger-when-dormant 409; job-id-not-found 404 across all routes; gateway_running consistency between /api/profiles and the trigger gate.

The legacy /api/cron/jobs* routes are untouched and remain the canonical path for the active profile.

How to Test

  1. scripts/run_tests.sh tests/hermes_cli/test_web_server.py::TestNewEndpoints -k cron → 12 new tests pass; -k gateway_running covers the 13th.
  2. Manual against a multi-profile install:
    • Dropdown visible when profiles.length > 1; first option is All.
    • "All" shows jobs from every profile with the per-row profile badge; active profile annotated (active).
    • Filter to a profile whose gateway is running → trigger buttons enabled.
    • Filter to a profile whose gateway is dormant → trigger buttons disabled with the tooltip.
    • Create from "All" requires picking a profile; create when filtered targets that profile automatically.
  3. Single-profile install: dropdown and per-row profile badge both hidden; behavior identical to today's CronPage.
  4. SPA rebuild required after applying: cd web && npm install && npm run build.

Checklist

Code

  • I've read the Contributing Guide
  • Commit messages follow Conventional Commits
  • Searched existing PRs for duplicates
  • PR contains only changes related to this feature (the upstream-of-main feat/dashboard-per-profile-skills commit appears in the diff only because feat(dashboard): per-profile skills toggle UI #25116 is not yet merged)
  • scripts/run_tests.sh tests/hermes_cli/test_web_server.py::TestNewEndpoints passes
  • Added tests for my changes (13 new)
  • Tested on: macOS 26 (Sequoia)

Documentation & Housekeeping

  • No new config keys / cli-config.yaml.example change — N/A
  • No .env.example changes — N/A
  • No CONTRIBUTING.md / AGENTS.md changes — N/A
  • Considered cross-platform impact — _save_profile_jobs uses tempfile.mkstemp + utils.atomic_replace (same helper cron.jobs.save_jobs uses on every platform); _check_gateway_running already cross-platform via gateway.status.get_running_pid; scripts/check-windows-footguns.py clean
  • No tool descriptions/schemas changed — N/A

cypres0099 and others added 2 commits May 13, 2026 11:53
Adds a profile selector to the dashboard's Skills page so each installed
profile's skills.disabled list can be managed from the same dashboard
daemon. Until now, /api/skills only knew the active profile (whichever
HERMES_HOME the dashboard process was launched under), so toggling skills
for a non-active profile required spinning up a second dashboard daemon
bound to that profile's HERMES_HOME — operationally awkward for users
running multiple profiles (e.g. a default + a worker/specialist profile).

Backend
-------
- Add GET  /api/profiles/{name}/skills        — list a profile's skills
- Add PUT  /api/profiles/{name}/skills/toggle — toggle for one profile
- Add is_active to ProfileInfo so the UI can identify the daemon's
  resident profile (the one served by the legacy /api/skills routes).
- Reads/writes go directly against the profile's config.yaml
  (skills.disabled). The load_config/save_config helpers are bound to
  the process-level HERMES_HOME via get_config_path(), so they can't
  be reused for cross-profile mutation without invasive global state
  changes.
- v1 omits skills.external_dirs scanning for non-active profiles. The
  dropdown targets profile-installed skills; external dirs are still
  respected by the gateway at runtime.

Frontend
--------
- SkillsPage gains a Select dropdown next to the enabled-of count
  (hidden when there's only one installed profile, so default-only
  installs are unchanged).
- Default selection is the dashboard's own profile (is_active, or
  is_default for older gateways that don't emit the field).
- Switching profile refetches the skills list from the profile-scoped
  endpoint; the active-profile selection still uses the legacy
  /api/skills route to stay in sync with the gateway's skill index.
- Toggles route through the appropriate endpoint based on selection.

The legacy /api/skills and /api/skills/toggle routes are untouched and
remain the canonical path for the active profile.
Adds a profile selector to the dashboard's Cron screen so cron jobs in
every installed profile can be managed from the same dashboard daemon.
Until now, /api/cron/jobs only knew the active profile (whichever
HERMES_HOME the dashboard process was launched under), forcing users to
SSH and run `hermes --profile <name> cron list` to see what was
scheduled on sub-profiles. Mirrors the precedent set by the per-profile
skills toggle UI (NousResearch#25116).

Backend
-------
- Add 8 routes under /api/profiles/{name}/cron/jobs* mirroring the
  legacy /api/cron/jobs* shape: list, get-single, create, update,
  pause, resume, trigger, delete. Reads and writes go directly against
  the profile's cron/jobs.json via new web-layer helpers
  (_load_profile_jobs / _save_profile_jobs); cron/jobs.py's
  module-global JOBS_FILE is left untouched.
- Trigger requests against a profile whose gateway is not running
  return 409 Conflict — writing next_run_at=now without a live
  dispatcher tick is a silent no-op. This is defense in depth; the UI
  disables the button as the primary UX.
- Add gateway_running to ProfileInfo so the frontend can gate the
  trigger button per row. Reuses the existing _check_gateway_running
  helper (PID-file plus process-identity verification).
- Atomic tempfile+replace writes preserve cron/jobs.py's anti-partial-
  write semantics so a concurrent dispatcher read never sees a torn
  file. Per-profile in-process write locks serialise concurrent
  dashboard requests for the same profile.

Frontend
--------
- CronPage gains a Profile dropdown (hidden when only one profile is
  installed). The default "All" view aggregates jobs across every
  profile via Promise.allSettled; the active profile is rendered with
  an "(active)" suffix matching the skills-patch convention.
- Each row carries the owning profile as a badge in the "All" view;
  the badge is hidden when filtered to a specific profile.
- Trigger button is disabled on rows where the owning profile's
  gateway is down, with tooltip explaining why.
- Create form gains a Profile selector when in "All" view (defaults to
  the active profile); the selector is hidden when filtered.
- Schedule cell renders "Repeat: ∞" (recurring) vs "Repeat: 1/1"
  (one-shot) so the 'every 4320m' vs '4320m' foot-gun is visible at a
  glance, matching what `hermes cron list` shows.

The legacy /api/cron/jobs* routes are untouched and remain the
canonical path for the active profile. Default-to-"All" diverges from
the per-profile skills page (which defaults to the active profile)
because cron is operations work that benefits from cross-profile
visibility, where skills is per-profile config work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/cron Cron scheduler and job management labels May 15, 2026
@cypres0099

Copy link
Copy Markdown
Contributor Author

Closing this stale broad branch rather than trying to revive it in-place. The current PR is very large, failing attribution/supply-chain/test checks, and predates the profile-scoped dashboard work that later landed on main. If per-profile cron controls are still worth pursuing, the better path is a fresh, small PR from current main that targets only the cron UI/API behavior and references this PR for context.

@cypres0099 cypres0099 closed this Jun 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cron Cron scheduler and job management P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants