Skip to content

feat: add profile-aware cron panel#1352

Closed
NocGeek wants to merge 1 commit intonesquena:masterfrom
NocGeek:profile-aware-cron-panel-upstream
Closed

feat: add profile-aware cron panel#1352
NocGeek wants to merge 1 commit intonesquena:masterfrom
NocGeek:profile-aware-cron-panel-upstream

Conversation

@NocGeek
Copy link
Copy Markdown
Contributor

@NocGeek NocGeek commented Apr 30, 2026

Summary

  • Add profile-aware cron API support for profile= and comma-separated profiles= queries.
  • Return profile on cron jobs and route cron detail/action endpoints through the owning profile.
  • Add a Scheduled Jobs visible-profile selector stored in localStorage, while keeping active profile semantics for new job creation.
  • Fix manual WebUI cron runs so they save output and mark last_run_at / last_status.

Tests

  • python3 -m py_compile api/routes.py tests/test_cron_profile_visibility.py
  • node --check static/panels.js
  • git diff --check origin/master..HEAD
  • /home/nocmadman/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_cron_profile_visibility.py tests/test_sprint3.py::test_crons_list tests/test_sprint3.py::test_crons_list_has_required_fields tests/test_sprint3.py::test_crons_output_requires_job_id tests/test_sprint3.py::test_crons_run_nonexistent tests/test_sprint7.py::test_cron_update_unknown_job_404 tests/test_sprint7.py::test_cron_delete_unknown_404 tests/test_sprint10.py::test_crons_output_limit_param tests/test_cron_refresh_button_835.py tests/test_issue1140_cron_badge_per_job.py

Result: 25 passed.

Manual Verification

  • hermes cron list shows default jobs.
  • cronbot cron list shows cronbot jobs.
  • Authenticated API call to /api/crons?profiles=default,cronbot returns jobs from both profiles with profile fields.
  • WebUI Scheduled Jobs shows [default] and [cronbot] rows when both are selected.
  • Running a cronbot job from WebUI shows running status, creates a new output file, and updates last_run_at / last_status.

Notes

ARCHITECTURE.md and TESTING.md could be updated in a follow-up to document the new profile-aware cron query/body parameters and manual visible-profile checklist.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Thanks @NocGeek, this fills a real gap. The cron panel only ever showing the active profile's jobs has been a usability cliff for anyone running cronbot alongside a default profile — you'd see "no jobs" and have no way to inspect or trigger the cronbot ones from the WebUI. Profile-aware listing with a localStorage selector is the right shape.

What I like

  • profile= and profiles=a,b as additive query params keeps the existing single-profile API contract intact. Good.
  • profile field returned on each job object means the UI doesn't need a second lookup to render the [profile] badge. Right call.
  • Routing detail/action endpoints through the owning profile (not the active one) is the only correct behaviour — running a cronbot job has to use cronbot's binary, env, and skills, not whatever profile the WebUI happens to have selected.
  • Manual run now persists last_run_at / last_status and saves output. That was the silent gap that made manual runs feel "ghost-like" — fix is well-targeted.
  • 25 tests passing including test_cron_profile_visibility.py, test_cron_refresh_button_835.py, test_issue1140_cron_badge_per_job.py — the cross-cutting regression coverage is exactly what I want to see.

Questions

  1. When a user has only the default profile selected but a cronbot job is currently running, does the UI hide it entirely, or show a stale-status row? Hiding is fine; just want to confirm there's no "ghost" flash on profile switch.
  2. The localStorage key for visible profiles — what's the namespace (hermes-webui:cron:visible_profiles or similar)? If it's just cron_visible_profiles it could collide on shared origins.
  3. New job creation still uses the active profile (per your summary). That's the right default, but is there a hint in the UI when the visible-profiles selector includes profiles other than the active one? Otherwise users may try to create a cronbot job from the panel and be surprised it lands in default.

Follow-ups (don't block this PR)

This is a solid, well-tested change. I'll do a closer line-level pass on the API routing changes shortly.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

On hold — needs rebase + scope discussion

Thanks @NocGeek for the substantial work on profile-aware crons. Putting this on hold for now while we sort out a few things.

Why on hold

1. Merge conflict against mastermergeStateStatus: DIRTY. Master has moved through v0.50.247 (PR #1345 — cron sessions auto-assigned to a "Cron Jobs" project) and v0.50.248 (cron-related session metadata changes via #1356). Both touch the cron sidebar paths, so a rebase is required before review.

2. CI hasn't run yet. No status checks present on this PR. After rebase, GH Actions will trigger automatically — please verify all 3 Python versions go green (test (3.11), test (3.12), test (3.13)) before pinging us.

3. Scope — at +447/−103 across 5 files this is a substantial architectural change introducing per-profile cron filtering, a Scheduled Jobs visible-profile selector with localStorage persistence, profile-routing on cron action endpoints, and manual-run output-saving fixes. We want to give this the careful review it deserves rather than rushing into a batch release. Two options:

  • Option A (preferred): Split into two PRs:
    • PR-1: just the manual-run output-saving fix (last_run_at / last_status after a manual run). This is a focused bug fix and probably ships in the next release.
    • PR-2: the profile-aware filtering + UI selector. Bigger architectural conversation; we want time to look at the localStorage key naming, multi-profile semantics, and whether profiles= (comma-separated) should be the canonical query shape vs. multiple profile= params.
  • Option B: Keep as a single PR but expect a longer hold while we review end-to-end, especially the interaction with v0.50.247's new "Cron Jobs" project auto-assignment.

What I'd want to see before un-holding

  • ✅ Rebased on current master (post-v0.50.248)
  • ✅ CI green on all 3 Python versions
  • ✅ Test for the manual-run output-saving fix specifically — there's a regression class here where manual runs were silently dropping output, so we want a regression-pinning test
  • ✅ A note in the description about how profile= interacts with the v0.50.247 "Cron Jobs" project (does it also filter by project? Or is project-scope independent of profile-scope?)
  • ✅ Decision on Option A vs. Option B (no pressure either way — we'll review whichever shape you choose)

What's good

The localStorage-based visible-profile selector pattern is solid. The fact that you preserved active-profile semantics for new job creation while letting users filter the visible set is the right separation.

@NocGeek tagging — happy to discuss any of the above, especially the rebase strategy if you'd like a hand. The cron paths in api/routes.py have churned a bit recently so a clean rebase might be tricky.

This was referenced Apr 30, 2026
@NocGeek
Copy link
Copy Markdown
Contributor Author

NocGeek commented Apr 30, 2026

  • When a user has only the default profile selected but a cronbot job is currently running, does the UI hide it entirely, or show a stale-status row? Hiding is fine; just want to confirm there's no "ghost" flash on profile switch.
  • The localStorage key for visible profiles — what's the namespace (hermes-webui:cron:visible_profiles or similar)? If it's just cron_visible_profiles it could collide on shared origins.
  • New job creation still uses the active profile (per your summary). That's the right default, but is there a hint in the UI when the visible-profiles selector includes profiles other than the active one? Otherwise users may try to create a cronbot job from the panel and be surprised it lands in default.

** Just for reference cronbot is a second profile i use so i just use that in my description. if you prefer in the future i could say altprofile or 2ndprofile if it adds clarity **

  1. If only default is selected while a cronbot job is running, the UI hides the cronbot job entirely. It does not render a stale-status row. On profile selection changes the cron detail view is cleared, the run watcher is stopped, and the list is reloaded from /api/crons?profiles=<selected>. Any in-flight status/output poll is guarded by the current job id + profile, so it should not reattach to a now-hidden cronbot job.

  2. The localStorage key is currently:
    hermes.cron.visibleProfiles

    It is not the very generic cron_visible_profiles, but it also is not fully app-namespaced as hermes-webui:cron:visible_profiles. I agree the latter would be clearer and safer for shared-origin installs. If you need me to make the change and submit a new PR i can tomorrow.

  3. New job creation still uses S.activeProfile, not the visible-profile selection. There is currently no explicit UI hint when multiple visible profiles are selected and the active profile differs from one of them. The profile chips show what is visible, but they do not explain where a new job will be created. That is a fair UX gap; adding a small “New jobs use active profile: ” hint near the selector would reduce surprise.

I can add this tomorrow I did not think to test that as I have never actually created a job in the UI would you want that as a separate additional PR or would you want to decline this one and have me resubmit as a new PR

i just saw the hold notes Ill work on updating and redoing it again tomorrow

@nesquena-hermes nesquena-hermes mentioned this pull request Apr 30, 2026
5 tasks
nesquena-hermes pushed a commit that referenced this pull request Apr 30, 2026
Manual WebUI cron runs previously called cron.scheduler.run_job(job)
and then only cleared the in-memory running flag. That meant output
could be dropped and job metadata like last_run_at / last_status was
not updated after a manual run.

This PR matches the scheduled cron path (cron/scheduler.py:1334-1364)
exactly:
- Save manual-run output via save_job_output
- Mark manual runs complete via mark_job_run
- Treat empty final_response as a soft failure with the same error
  string as the scheduled path
- Record manual-run failures in job metadata via mark_job_run(False)
- Keep _run_cron_tracked self-contained for worker-thread execution

Includes 2 behavioral regression tests using monkeypatch.setitem on
sys.modules to mock cron.scheduler.run_job + cron.jobs helpers — the
right test pattern (exercises the real _run_cron_tracked code path).

Split out from #1352 (the larger profile-aware-cron-panel PR that's
on hold) per pre-release-review feedback. Self-contained, doesn't
touch the held PR's profile-filtering scope.

Co-authored-by: NocGeek <[email protected]>
nesquena-hermes added a commit that referenced this pull request Apr 30, 2026
Opus pre-release findings on #1370 applied:

SHOULD-FIX 1: Tightened parent_session_id exposure to only emit when
the parent's end_reason is in {compression, cli_close}. Without this,
two distinct WebUI sessions sharing a non-continuation parent (e.g.
'user_stop') would get clustered by frontend's _sessionLineageKey
(which falls through to parent_session_id when _lineage_root_id is
missing) and incorrectly collapsed into a single sidebar row.

  Updated assertions in:
  - tests/test_session_lineage_metadata_api.py::
    test_non_compression_state_db_parent_does_not_create_sidebar_lineage
  - tests/test_pr1370_lineage_metadata_perf_and_orphan.py::
    test_non_compression_parent_does_not_extend_lineage

SHOULD-FIX 2: Chunked the IN-clause to 500 vars to stay under
SQLITE_MAX_VARIABLE_NUMBER. Python 3.9 ships sqlite 3.31 with the
default limit of 999. A power user with 2000+ sessions in the
sidebar would hit OperationalError, the silent except-wrapper would
swallow it, and lineage collapse would never work. Added
test_in_clause_chunked_for_large_session_set with SQL interception
to lock the invariant in source.

PR addition (per user directive — Opus + my review, no second
independent review round needed for combined batch):

#1372 from @NocGeek — fix: persist manual cron run results.
Self-contained 89 LOC fix split out from the held #1352. Mirrors the
scheduled-cron path (cron/scheduler.py:1334-1364) exactly: saves
output, marks job complete, treats empty response as soft failure
with matching error string. 2 behavioral tests using sys.modules
monkeypatch to mock cron.scheduler.run_job. CI not yet attached
because branch is brand-new; ran the new tests + adjacent suites
locally — all pass.

Final test count: 3471 passing, 0 failed.

Also adds 2 more regression tests for the perf-fix invariants:
- test_in_clause_chunked_for_large_session_set
- test_two_children_sharing_non_continuation_parent_not_collapsed
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Closed — superseded by #1372 (shipped) + #1374 (architectural piece on hold)

Thanks @NocGeek for the original work on this. Closing #1352 as it has now been superseded by two focused successor PRs:

The Option A split I recommended in this PR's hold comment worked exactly as intended — thanks for the rebase + clean re-write. The manual-run fix shipped fast in v0.50.251 (which is the bug fix) while the architectural piece gets the careful review it deserves in #1374 (which is the feature).

@NocGeek tagging — happy to discuss the architectural coordination questions on #1374 anytime.

GeoffBao pushed a commit to GeoffBao/hermes-webui that referenced this pull request May 1, 2026
Manual WebUI cron runs previously called cron.scheduler.run_job(job)
and then only cleared the in-memory running flag. That meant output
could be dropped and job metadata like last_run_at / last_status was
not updated after a manual run.

This PR matches the scheduled cron path (cron/scheduler.py:1334-1364)
exactly:
- Save manual-run output via save_job_output
- Mark manual runs complete via mark_job_run
- Treat empty final_response as a soft failure with the same error
  string as the scheduled path
- Record manual-run failures in job metadata via mark_job_run(False)
- Keep _run_cron_tracked self-contained for worker-thread execution

Includes 2 behavioral regression tests using monkeypatch.setitem on
sys.modules to mock cron.scheduler.run_job + cron.jobs helpers — the
right test pattern (exercises the real _run_cron_tracked code path).

Split out from nesquena#1352 (the larger profile-aware-cron-panel PR that's
on hold) per pre-release-review feedback. Self-contained, doesn't
touch the held PR's profile-filtering scope.

Co-authored-by: NocGeek <[email protected]>
GeoffBao pushed a commit to GeoffBao/hermes-webui that referenced this pull request May 1, 2026
…rsistence

Opus pre-release findings on nesquena#1370 applied:

SHOULD-FIX 1: Tightened parent_session_id exposure to only emit when
the parent's end_reason is in {compression, cli_close}. Without this,
two distinct WebUI sessions sharing a non-continuation parent (e.g.
'user_stop') would get clustered by frontend's _sessionLineageKey
(which falls through to parent_session_id when _lineage_root_id is
missing) and incorrectly collapsed into a single sidebar row.

  Updated assertions in:
  - tests/test_session_lineage_metadata_api.py::
    test_non_compression_state_db_parent_does_not_create_sidebar_lineage
  - tests/test_pr1370_lineage_metadata_perf_and_orphan.py::
    test_non_compression_parent_does_not_extend_lineage

SHOULD-FIX 2: Chunked the IN-clause to 500 vars to stay under
SQLITE_MAX_VARIABLE_NUMBER. Python 3.9 ships sqlite 3.31 with the
default limit of 999. A power user with 2000+ sessions in the
sidebar would hit OperationalError, the silent except-wrapper would
swallow it, and lineage collapse would never work. Added
test_in_clause_chunked_for_large_session_set with SQL interception
to lock the invariant in source.

PR addition (per user directive — Opus + my review, no second
independent review round needed for combined batch):

nesquena#1372 from @NocGeek — fix: persist manual cron run results.
Self-contained 89 LOC fix split out from the held nesquena#1352. Mirrors the
scheduled-cron path (cron/scheduler.py:1334-1364) exactly: saves
output, marks job complete, treats empty response as soft failure
with matching error string. 2 behavioral tests using sys.modules
monkeypatch to mock cron.scheduler.run_job. CI not yet attached
because branch is brand-new; ran the new tests + adjacent suites
locally — all pass.

Final test count: 3471 passing, 0 failed.

Also adds 2 more regression tests for the perf-fix invariants:
- test_in_clause_chunked_for_large_session_set
- test_two_children_sharing_non_continuation_parent_not_collapsed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants