Skip to content

Web: Move watcher release config from env vars to admin-only settings page - #74

Merged
wasimxyz merged 3 commits into
stagingfrom
cursor/watcher-release-settings
May 18, 2026
Merged

Web: Move watcher release config from env vars to admin-only settings page#74
wasimxyz merged 3 commits into
stagingfrom
cursor/watcher-release-settings

Conversation

@wasimxyz

@wasimxyz wasimxyz commented May 16, 2026

Copy link
Copy Markdown
Member

Summary

  • Replaces the WATCHER_LATEST_VERSION / WATCHER_MIN_SUPPORTED_VERSION / WATCHER_RELEASE_CHANNEL / WATCHER_MANDATORY_UPDATE env vars with a singleton watcher_release_config Postgres row (migration 0024), so release rollouts no longer require a Vercel env-var bump.
  • New admin-only Settings → Watcher Release page (web/app/settings/watchers/page.tsx) and PUT /api/v1/settings/watcher-release route (session-only via requireAdmin(), matching the members PATCH precedent). The update-check endpoint reads from the DB and falls back to latest_version: null when no row exists, preserving the existing "unset means skip self-update" contract for the watcher fleet.
  • Docs updated to point at the new settings page; both test harnesses (TS and Python) seed the singleton row directly, and resetDb() re-seeds after every cycle because TRUNCATE … CASCADE on `"user"` wipes the row via the updated_by FK regardless of ON DELETE SET NULL.

Design notes

  • Singleton schema. id boolean PRIMARY KEY DEFAULT true + CHECK (id = true) — exactly 0 or 1 rows enforced at the DB layer, so routes can skip LIMIT 1 / ordering discipline.
  • Wire format unchanged. GET /api/v1/watchers/:id/update-check returns the same four-field shape; the watcher fleet doesn't need to know the source moved from env to DB.
  • Per-environment rollouts still work. Each Vercel environment has its own DB, so the staging-only-then-production cadence in docs/guides/upgrading-the-watcher.md still applies — just edited through the UI instead of vercel env commands.
  • Admin gate is session-only. PATs never edit the release config (consistent with /api/v1/users/[userId] PATCH); 401, not 403, for PAT callers so the failure mode reads as "session required".
  • React/Next.js best practices. Page is an async Server Component reading via Drizzle (no extra API hop); only the form's primitive values + small "last updated" metadata cross the server/client boundary. The client form uses useTransition + router.refresh() instead of a manual useState loading flag; the Mandatory checkbox derives its disabled state during render from latest_version's blank-ness (no useEffect sync).

Verification

  • make check-all — clean (ruff, ruff-format, pyright, prettier, eslint, tsc).
  • make fe-test-integration — 181 tests across 13 files pass, including the new `watcher-release.test.ts` (8 tests: admin-gate negatives + end-to-end upsert → update-check + singleton CHECK constraint).
  • make fe-test-mcp — 55 tests pass.
  • make py-test-unit — 547 tests pass.
  • watcher/tests/integration/test_update_check.py — 2 tests pass against the new DB-seeded path (verifies the shared Python test harness fix).

Post-merge

Once each Vercel environment's admin has saved a release on the settings page, the four WATCHER_* env vars can be unset in the Vercel project. Out of scope for this PR.

Test plan

  • Apply migration 0024_add_watcher_release_config.sql on staging.
  • Sign in to staging as an admin, open Settings → Watcher Release, save the current production version, and verify /api/v1/watchers/<id>/update-check returns the new value.
  • Sign in as a non-admin and confirm the page renders the "Admins only" block and the sidebar entry is hidden.
  • Confirm the watcher fleet on staging picks up the saved release on the next hourly tick.

Made with Cursor

… page

Replaces the WATCHER_LATEST_VERSION / WATCHER_MIN_SUPPORTED_VERSION /
WATCHER_RELEASE_CHANNEL / WATCHER_MANDATORY_UPDATE env vars with a
singleton `watcher_release_config` row, edited via a new admin-only
/settings/watcher-release page (PUT /api/v1/watcher-release, gated by
requireAdmin). The update-check endpoint reads from the DB and falls
back to latest_version: null when no row exists, preserving the existing
"unset means skip self-update" contract for the watcher fleet.

Both test harnesses now seed the singleton row directly; resetDb()
re-seeds after each cycle because TRUNCATE CASCADE on "user" wipes the
row via the updated_by FK regardless of ON DELETE SET NULL.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
data-hub Ready Ready Preview, Comment May 18, 2026 6:24pm

Request Review

@wasimxyz
wasimxyz marked this pull request as ready for review May 17, 2026 17:21
@wasimxyz wasimxyz self-assigned this May 17, 2026
…d layout

Three follow-ups to the previous watcher release commit:

* Replace the hand-rolled per-field validation in the PUT route with a
  Zod `strictObject` schema. Drops `normalizeOptionalString` and the
  bespoke `unknown_fields` detail in favor of zod issues, and unifies
  PUT semantics so all four fields take their defaults uniformly when
  omitted rather than silently preserving existing values for the two
  version columns.

* Move the API surface from `/api/v1/watcher-release` to
  `/api/v1/settings/watcher-release` and the admin page from
  `/settings/watcher-release` to `/settings/watchers`. Reorder the
  settings sidebar to Access Tokens → Watchers → Members. Comment and
  doc references throughout (schema, env example, integration tests,
  shared testing helper, upgrade guide) updated to match.

* UI polish: drop the duplicate "Settings" H1 from the settings layout
  (the sidebar already labels the section), rename the on-page heading
  to "Watcher Version", and wrap the entire form (heading, description,
  fields, footer) in a full-width Card. CardHeader, CardContent and the
  inner footer row are capped at max-w-2xl so the input rows stay at a
  readable measure while the card's top border and full-width footer
  separator span the page.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replaces hand-rolled useState/useTransition wiring in the watcher release
settings form with @tanstack/react-form. Field state, isSubmitting, and
validation are now owned by the form library, and the same Zod schema is
checked onBlur (live feedback after a field is touched) and onSubmit
(final guard) — mirroring VERSION_REGEX from the server route so typos
are caught locally instead of round-tripping to a 400.

- Adds the shadcn Field primitive (Field, FieldGroup, FieldLabel,
  FieldDescription, FieldError, FieldContent) used by the docs' TanStack
  Form examples.
- Scopes re-renders to small subtrees via form.Subscribe selectors for
  the mandatory-switch row and the Save button.
- Preserves the existing UX (visually-off-when-disabled mandatory
  switch, "" → null wire normalisation, last-updated-by footer,
  router.refresh on success).

Co-authored-by: Cursor <cursoragent@cursor.com>
@wasimxyz
wasimxyz merged commit 5ebfea0 into staging May 18, 2026
6 checks passed
@wasimxyz
wasimxyz deleted the cursor/watcher-release-settings branch May 18, 2026 19:45
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.

1 participant