Skip to content

Enforce RBAC, run scheduled jobs, and deliver notifications - #17

Merged
jomardyan merged 4 commits into
mainfrom
claude/app-bugs-gaps-ui-ucqbsl
Sep 7, 2026
Merged

Enforce RBAC, run scheduled jobs, and deliver notifications#17
jomardyan merged 4 commits into
mainfrom
claude/app-bugs-gaps-ui-ucqbsl

Conversation

@jomardyan

Copy link
Copy Markdown
Owner

The backend shipped a full permission model, a cron field on every job and six
notification channel types, none of which did anything. This wires them up and
fixes the data-layer bugs found alongside them.

Security and access control

  • Add app/routes/deps.py with require_permission/require_admin and apply it
    across every router. The REST API was reachable anonymously, including the
    endpoint that stores a shell command and the one that executes it.
  • Resolve SECRET_KEY from the environment, else generate and persist a random
    key. The previous fallback was a published constant, so anyone could forge an
    admin token against a default deployment.
  • Redact secrets in notification channel responses and preserve them when a
    client sends the redaction placeholder back on update.
  • Move change-password credentials from query parameters into a request body.
  • Gate self-registration behind ALLOW_SELF_REGISTRATION (default off) and stop
    an admin from demoting, deactivating or deleting the last administrator.
  • Escape HTML in the plain-text note render/preview path.
  • Escape LIKE metacharacters so a search for "%" no longer matches everything.
  • Keep monitor ping keys out of list responses; expose them on their own route.

Data layer

  • Apply PRAGMA foreign_keys on every connection. It was set only during init,
    so every ON DELETE CASCADE was dead and deleting a folder root orphaned all
    of its scripts. Adds a one-shot repair for databases already affected.
  • Add an additive migration step; CREATE TABLE IF NOT EXISTS never delivered a
    new column to an existing database.
  • Enable WAL and a busy timeout, and index the columns the hot queries filter on.
  • Serialize every timestamp as UTC-aware so the UI stops shifting them.
  • Aggregate tags with a unit separator; a tag containing a comma became two.

Scheduling, monitoring and notifications

  • Add app/services/cron.py (validated 5-field parser with timezone support) and
    app/services/scheduler.py, which fires due jobs, evaluates monitors on a timer
    and reaps executions stranded by a restart.
  • Add app/services/notifier.py with working Slack, Discord, webhook, PagerDuty,
    SMTP and Twilio delivery; "test channel" now sends and reports the outcome.
  • Validate cron expressions and timezones on write and compute next_run_at.
  • Kill the whole process group on job timeout; the drain used to hang forever.
  • Raise incidents and alert channels on job failure, and resolve them on recovery.

Scanning, search and attachments

  • Run directory walks off the event loop and guard against symlink loops.
  • Populate the folders table so the folder tree and folder notes work at all.
  • Maintain the FTS index on scan, watch and note writes, and honour the
    search_content/search_notes flags that were computed and then ignored.
  • Rebuild watch mode around one worker per root instead of a thread per event,
    and apply the root's include/exclude patterns.
  • Stream attachment uploads with an early size abort, sanitise filenames, and
    remove files from disk when their parent is deleted.
  • Turn off difflib's autojunk heuristic, which zeroed similarity scores for any
    file over ~2 KB, and make the sweep load each file once with a bounded cap.
  • Add PUT /api/folder-roots/{id} so content indexing and watch mode can be
    enabled without deleting and recreating the root.

Deployment

  • Proxy /api to the backend in nginx-frontend.conf. Without it the composed
    frontend answered every API call with index.html.
  • Stop shipping a default SECRET_KEY in docker-compose.yml.

Tests: 110 passing (was 57). Adds coverage for cron parsing, RBAC enforcement,
cascade deletes, migrations and secret redaction.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01JybLja9BWK1Q6yfejNYmwr

The backend shipped a full permission model, a cron field on every job and six
notification channel types, none of which did anything. This wires them up and
fixes the data-layer bugs found alongside them.

Security and access control
- Add app/routes/deps.py with require_permission/require_admin and apply it
  across every router. The REST API was reachable anonymously, including the
  endpoint that stores a shell command and the one that executes it.
- Resolve SECRET_KEY from the environment, else generate and persist a random
  key. The previous fallback was a published constant, so anyone could forge an
  admin token against a default deployment.
- Redact secrets in notification channel responses and preserve them when a
  client sends the redaction placeholder back on update.
- Move change-password credentials from query parameters into a request body.
- Gate self-registration behind ALLOW_SELF_REGISTRATION (default off) and stop
  an admin from demoting, deactivating or deleting the last administrator.
- Escape HTML in the plain-text note render/preview path.
- Escape LIKE metacharacters so a search for "%" no longer matches everything.
- Keep monitor ping keys out of list responses; expose them on their own route.

Data layer
- Apply PRAGMA foreign_keys on every connection. It was set only during init,
  so every ON DELETE CASCADE was dead and deleting a folder root orphaned all
  of its scripts. Adds a one-shot repair for databases already affected.
- Add an additive migration step; CREATE TABLE IF NOT EXISTS never delivered a
  new column to an existing database.
- Enable WAL and a busy timeout, and index the columns the hot queries filter on.
- Serialize every timestamp as UTC-aware so the UI stops shifting them.
- Aggregate tags with a unit separator; a tag containing a comma became two.

Scheduling, monitoring and notifications
- Add app/services/cron.py (validated 5-field parser with timezone support) and
  app/services/scheduler.py, which fires due jobs, evaluates monitors on a timer
  and reaps executions stranded by a restart.
- Add app/services/notifier.py with working Slack, Discord, webhook, PagerDuty,
  SMTP and Twilio delivery; "test channel" now sends and reports the outcome.
- Validate cron expressions and timezones on write and compute next_run_at.
- Kill the whole process group on job timeout; the drain used to hang forever.
- Raise incidents and alert channels on job failure, and resolve them on recovery.

Scanning, search and attachments
- Run directory walks off the event loop and guard against symlink loops.
- Populate the folders table so the folder tree and folder notes work at all.
- Maintain the FTS index on scan, watch and note writes, and honour the
  search_content/search_notes flags that were computed and then ignored.
- Rebuild watch mode around one worker per root instead of a thread per event,
  and apply the root's include/exclude patterns.
- Stream attachment uploads with an early size abort, sanitise filenames, and
  remove files from disk when their parent is deleted.
- Turn off difflib's autojunk heuristic, which zeroed similarity scores for any
  file over ~2 KB, and make the sweep load each file once with a bounded cap.
- Add PUT /api/folder-roots/{id} so content indexing and watch mode can be
  enabled without deleting and recreating the root.

Deployment
- Proxy /api to the backend in nginx-frontend.conf. Without it the composed
  frontend answered every API call with index.html.
- Stop shipping a default SECRET_KEY in docker-compose.yml.

Tests: 110 passing (was 57). Adds coverage for cron parsing, RBAC enforcement,
cascade deletes, migrations and secret redaction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JybLja9BWK1Q6yfejNYmwr
The React app had no sign-in screen and never sent a bearer token, so every
authenticated endpoint was unreachable from the browser and the Team page could
only ever show "Not authenticated". Several whole backend feature areas had no
UI at all. This wires the client to the API and rebuilds the interface on a
shared design system.

Authentication
- Add a sign-in screen, an auth context, and axios interceptors that attach the
  token and drop back to sign-in on 401 instead of leaving a dead error page.
- Show who is signed in, offer sign-out, and add a Settings page for changing
  your own password.
- Hide navigation entries and actions the account lacks permission for, and
  block direct URLs to pages it cannot reach, mirroring the server's rules.

Features that had no interface
- Duplicates page: exact (hash) duplicates and near-duplicate similarity groups.
- Full-text content search and saved searches on the Search page, plus the eight
  filters SearchRequest supported but the form never exposed.
- Script detail: note editing and deletion with Markdown rendering, attachments,
  custom fields, similar scripts, and a change history that shows old to new
  values and who made the change.
- Folder roots: an edit dialog, so content indexing and watch mode can finally be
  turned on, plus scan history and per-root script counts.
- Bulk tagging and status changes, and metadata export, from the Scripts page.
- Notification channel pickers on monitors and jobs, so alerts reach someone.
- A typed notification channel form (the old one was a JSON textarea seeded with
  a template the backend rejected), a live cron preview, and index maintenance
  and watch-mode controls in Settings.

Interface
- Replace two conflicting palettes and 100+ inline style objects with CSS
  design tokens, and add a dark theme that follows the OS with a manual override.
- Replace 43 blocking alert() calls with a toast layer, and window.confirm with
  a dialog that states the impact of a destructive action.
- Add an accessible Modal (dialog role, focus trap, Escape, restored focus),
  associate every label with its control, make table headers sortable with
  aria-sort, add a skip link, and fix text colours that failed AA contrast.
- Collapse the sidebar into a drawer below 900px; it used to push ~540px of
  navigation above every page on a phone.
- Add an error boundary, a 404 route, per-page document titles, skeleton
  loaders, empty states, and retryable error banners.

Bugs
- Surface the server's error detail; users saw "Request failed with status code
  400" and, for validation errors, "[object Object]".
- Parse offset-less timestamps as UTC. Every time in the UI was shifted by the
  viewer's timezone offset.
- Clear the scan-status polling interval on unmount and time it out; it leaked
  one interval per scan and polled forever.
- Discard stale list responses so a slow request cannot overwrite newer results,
  and clamp the page number when a result set shrinks.
- Guard every submit against double-clicks, and send only changed status fields
  so saving no longer writes four no-op audit entries.
- Re-arm the mounted ref on mount: under StrictMode it was only ever cleared,
  which left the app stuck on "Checking your session".

Backend and deployment
- Keep monitor ping keys out of listings; they are the credential a ping needs.
- Serve demo mode a real administrator account and show its one-time password,
  instead of finishing setup with no way to sign in.

Docs: README, docs/API.md and both .env.example files now describe the
scheduler, real notification delivery, RBAC enforcement and the new settings.

Verified end to end in a browser: sign-in, all ten pages, script detail,
tag creation, modal Escape, impact-stating confirmations, full-text search,
live cron validation, dark mode, the 404 route, the mobile drawer, sign-out,
and a viewer account seeing a correctly reduced interface. 110 backend tests
pass and the frontend builds clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JybLja9BWK1Q6yfejNYmwr
CI could never pass: the frontend job pinned Node 18, which Vite 7 refuses to
install on (it requires ^20.19 || >=22.12). Split the workflow into a frontend
and a backend job, move to Node 22, run pytest from backend/ so pytest.ini
applies, and install from the new requirements-dev.txt.

Overlap prevention was a check-then-act race: two concurrent triggers could each
see no running execution and each insert one. A partial unique index on
job_executions(job_id) WHERE status='running' makes the database refuse the
second, and both the manual trigger and the scheduler translate that into the
409 (or a skip) it means.

Bulk tagging now rejects unknown tag ids with a 404 instead of letting the
foreign-key failure be counted as "skipped", and it looks each tag name up once
rather than per script.

Adds tests/test_scheduling.py (17 tests) covering cron and timezone validation,
next_run_at computation, the cron preview, overlap prevention at both the API
and database level, output capture, incident creation on failure, timeout
killing the process group, reaping executions stranded by a restart, background
monitor evaluation including never-pinged monitors, ping recovery resolving
incidents, and ping keys staying out of listings.

127 backend tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JybLja9BWK1Q6yfejNYmwr
Eleven issues found reviewing the diff, all fixed:

Backend
- Retain references to scheduled job tasks. asyncio holds only a weak reference,
  so a running job could be garbage-collected mid-execution, stranding its row
  at 'running' and blocking the job until the next startup reap.
- Key the overlap guard on a new job_executions.overlap_key column instead of
  job_id. The unconditional unique index made prevent_overlap = false
  unimplementable: such jobs were still refused a concurrent run.
- Commit monitor evaluation before dispatching alerts. Sending inside the open
  write transaction held SQLite's single write lock for up to the notifier's
  10s HTTP timeout, so unrelated writers failed with "database is locked" -
  reachable from a plain GET /api/monitors.
- Stop a watcher off the event loop, and drain the queue before signalling the
  worker. Blocking joins and an unbounded put could deadlock the whole API when
  a worker thread had already died with a full queue.
- Allow user creation when REQUIRE_AUTH=false. There is no user object in that
  mode, so the Team page's "New user" action could never succeed.
- Create an administrator when setup completes in demo mode through
  /api/setup/complete. Only /api/setup/demo had been fixed, so that path still
  marked setup complete with no account to sign in with.
- Skip files younger than an hour when pruning orphaned attachments. An upload
  writes its file before inserting the row that names it, so a concurrent prune
  could delete an in-flight upload.
- Move GET /monitors/{id}/ping-url behind write access. Read access is held by
  the viewer role, which undid removing ping keys from the monitor response.

Frontend
- Do not require a command when a job is linked to a script, and carry
  script_id through the edit form; such a job could not be saved at all.
- Take the open-incident count from the stats endpoint. It was derived from a
  list fetched with limit 5, so the dashboard tile plateaued at 5.
- Read onClose through a ref in Modal. Every caller passes an inline arrow, so
  any parent re-render (a toast, for instance) re-ran the focus-trap effect and
  pulled focus out of the field being typed in.

Adds tests for the corrected overlap semantics in both directions. 129 backend
tests pass; the browser walkthrough passes unchanged, and a job with
prevent_overlap disabled now accepts two concurrent triggers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JybLja9BWK1Q6yfejNYmwr
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-07T06:33:42.378467Z dc4cdea PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@jomardyan
jomardyan merged commit 7a3dd4e into main Sep 7, 2026
4 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.

2 participants