Unify edit pages on shared hooks

Task 12467 · Done · Coder · created · updated

## Context (BA Task 12447 → Architecture Task 12448 → Plan Task 12449) Original request (verbatim): "Regarding task <https://app.jaah.ai/tasks/12393> This is a repeat situation with the same user apparently behind a broken corporate firewall. He says that previous fix helped and his task list is no longer causing trouble, but he was now struggling with Projects edit. Go over all UI surfaces and ensure that the same code is reused everywhere - one choke point - that works identically and uses the approach and protocols that are now proven working. If there will be other similar issues we would only have to fix it in one place" Root cause (Architecture §0, AC-0): the hang was a WRITE — `PUT /api/projects/54` sat on a dead direct HTTP/2 connection (prod `activity_events` 1661992/3; every page read was relay-rescued, `client_events` 1335-1345). Writes are bare `fetch` with no relay/timeout (`apps/web/src/api.ts:68`); the relay is GET-only (`apps/api/app/routes/read_relay.py:13`). Operator decisions (verbatim, PRD §3 + Architecture §1): D2 "**Relay once + idempotency key.** Every write carries a unique key. When the direct connection looks dead, the write also goes over the WebSocket relay, and the server applies a given key at most once." D3 "**All four:** all `api()` reads + writes; the raw-fetch stragglers (`useVersionCheck`, live `config.ts`, live rewriter, S3 upload PUT); streams + terminal (EventSource `/api/stream`, terminal WebSocket); browser-fetched URLs (`apiHref`: img, iframe, downloads)." D4 "**Yes, also unify edit pages:** load via `useEditRecord`, save via `useSubmit`". A1 one epic, parallel where no dependency. A2 **At most once** (claim before run). A3 **All console writes, capped** (body ≤ 1 MiB, per-session write budget, key required). A4 **Public relay, live paths only** (cookieless socket, `GET /api/public/live/*`, per-IP budget). Full documents (read your section in full before building): the PRD and Architecture are the settled `prd` / `architecture` artifacts of Tasks 12447 / 12448 (read with `task_read` / `artifact_read`). Your section below is copied from them; the pinned seams are binding contracts other children build against — do not change them without updating the sibling that consumes them. ## Your scope: C6 — Edit pages on `useEditRecord` + `useSubmit` (D4) Move the five deviant edit pages (Project, Feature, Playbook, Automation, Account) onto the shared load/save pair, per §3.7. Independent of the network work (no data/API dependency). Capture the AC-9 baseline screenshots on dev BEFORE opening the PR (Architecture §4 baselines, verbatim): **Baselines before merge.** AC-3 needs the median save time and AC-9 needs side-by-side screenshots, both against develop. Once a child's merge deploys, dev runs the new code. So each of these children captures its baseline on dev **before** opening its PR and attaches it to its report: - C3: 20 Project Saves on a healthy connection, timed in Playwright. - C6: desktop and phone-width screenshots of each AS7 page. Every coder child runs the dev oracles of the ACs it owns against `https://app.dev.jaah.ai` after its merge deploys (see §6), not `dev.jaah.ai`. That PRD host does not resolve from node h2. ### Architecture §3.7 (verbatim) ### 3.7 Edit pages (D4) [child C6] The target is the shared pair: `useEditRecord` (`hooks/useEditRecord.ts`) to load and `useSubmit` (`hooks/useSubmit.ts`) to save. No successor hook is needed. Recon (file:line, develop at 33639d841c): | Page | Load today | Save today | Change | |---|---|---|---| | `ProjectEditPage.tsx` | own `useQuery` `byIdQuery` :394 + render-time seed :394-454 | `useSubmit` :485 → `put` :636 | load → `useEditRecord(id, …, onLoad=seed)`. The side queries (governance, members, models, jira :404-474) stay `useQuery`: they are not the record. | | `FeatureEditPage.tsx` | `useQuery` :84,:92 | `useMutation` :113 | both → shared hooks | | `playbooks/PlaybookEditPage.tsx` | `useEditRecord` :235 | `useMutation` :274 (save), :293 (duplicate) | save + duplicate → `useSubmit` (two instances) | | `automations/AutomationEditPage.tsx` | `useEditRecord` :370 | direct `createAutomation` :505 / `updateAutomation` | → `useSubmit.run` | | `credentials/AccountEditPage.tsx` | own `useEffect`s :1942,:1956 + `get('/api/accounts')` :1363 | `useSubmit` ×3 | load → `useEditRecord` | | User, Group, Template, Credential, NodeLink | `useEditRecord` | `useSubmit` | reference, no change | React Query cache invalidation that the `useMutation` sites did in `onSuccess` moves into the `run` write closure (`queryClient.invalidateQueries` after the `await`), with the same keys. There must be no visible change (AC-9). Each page's vitest suite is the regression net. ### PRD assumptions that bind this child (verbatim) - AS7. **Edit pages in scope (D4)** = every route page that loads one record and saves it: `ProjectEditPage`, `UserEditPage`, `GroupEditPage`, `FeatureEditPage`, `pages/playbooks/PlaybookEditPage`, `pages/templates/TemplateEditPage`, `pages/automations/AutomationEditPage`, `pages/credentials/AccountEditPage`, `pages/credentials/CredentialEditPage`, `pages/nodes/nodeLinkStore/NodeLinkEditPage`, plus the other `useEditRecord` users (`CustomerPage`, `ApiKeyDetailPage`, `GmailConnectionPage`) as the reference. List and dialog mutations elsewhere (compliance, billing, playbook list) are not edit pages. They are covered by D3 because they call `api()`, not by D4. - AS8. **No feature flag.** The change is on by default in dev and prod, like #7651. ### Requirement constraints (PRD §7, verbatim) - Writes are never applied twice (D2, AC-2). Idempotency touches every mutating route, so it is a security-surface change (data integrity). Project instructions grant merge permission. The review lane still applies. - Act-as headers (`lib/actAs.ts`) and session headers (`lib/activeOrg.ts`) must ride relayed writes exactly as direct ones. A relayed write must be authorized as the same session. A relayed 401/403 never wins over the direct answer, the same rule as reads. - Single-home: the relay rules live in one module. `docs/standards/coding-standards.md` and the context policy apply. ### Acceptance criteria (PRD §5, verbatim) **AC-9 — Edit pages on one load/save pattern (D4).** Every edit page in AS7 loads its record through `useEditRecord` (or one named successor) and saves through `useSubmit` (or one named successor). No page-local `useQuery` load with render-time draft seeding, and no raw `useMutation` for a page save. - verify: grep: `ProjectEditPage`, `FeatureEditPage`, `PlaybookEditPage` and `AutomationEditPage` use the shared hooks. Each page's existing vitest suite passes. - verify (dev): open and save each AS7 page once on dev → loads, saves, refusal message on a bad value, leave guard on unsaved changes, all as today. - Quality: no visible change to these pages. Layout, fields, tabs, copy and URLs are identical (side-by-side screenshot vs develop on dev at desktop and phone width). Credentials & reach: (Architecture §6, verbatim rows) | Oracle | Credential / identity / environment | Status | |---|---|---| | AC-1/3/5/6/7/9 dev Playwright | `me/jaah/debug/main/operator-login` (operator; its description says it works on dev and main) on `https://app.dev.jaah.ai` | **verified**: `credential_get` succeeded; `GET /version.json` → 200 from h2. `org/console/dev-login` also exists, but its base_url names a host that does not answer from h2, so the operator login is the one to use. | | AC-10 deploy run | `jaah-forge pr checks` / `ci log` (pre-authenticated forge) | **verified**: no credential to fetch | Out of scope: PRD §6 (verbatim) — - Do NOT add a hostname, port, certificate, or change cookies or auth to carry writes. The WebSocket relay carrier stays (decision of Task 11936). - Do NOT change the read relay's existing read semantics (race timing, probe, suspect and dropping memory) beyond what writes, streams and uploads need. - Do NOT change any edit page's visible UI: layout, fields, tabs, copy, URLs, routes (AC-9 quality). - Do NOT change React Query's global defaults (`apps/web/src/queryClient.ts`, `retry: false` for queries). - Do NOT change server route business logic. Server changes are limited to (a) idempotency for console writes and (b) the relay accepting writes under that key. - Do NOT touch `apps/site-widget`, `apps/docs`, `apps/jaahd`, `packages/` (AS9). - Do NOT refactor non-edit-page mutations (compliance, billing, playbook list, template cards) beyond what routing them through the choke point needs. - Do NOT add a feature flag (AS8). - Do NOT treat this as Nick-only: no per-user or per-org switch. - Do NOT touch `apps/web/src/api.ts`, `lib/readRelay.ts` or any network code (C3 owns it). - Do NOT change the side queries on ProjectEditPage (governance, members, models, jira) — they are not the record. Done means: - AC-9 — oracle verbatim above (PRD §5 AC-9 `verify:` lines) - AC-9 quality is strict: side-by-side desktop + phone-width screenshots vs the pre-PR baseline show NO visual difference on every AS7 page; refusal message, leave guard and post-save navigation behave as before. Passing grep alone does not satisfy AC-9. - Dev records: save each AS7 page on a record you created through the UI (scratch user/group/template/credential/account/playbook/automation/NodeLink/feature/project), never on a record in real use; restore anything you change. The bad-value refusal check must not persist anything. - `./dc check quick` green; PR merged via the merge lane; Jaah Deploy to dev green. Dev oracles run on `https://app.dev.jaah.ai` (the PRD's `dev.jaah.ai` does not resolve from the nodes — Architecture §7), after your merge's Jaah Deploy to dev succeeds. Watch that deploy; if it fails, fix and merge again.

Outcome

Project/Feature/Playbook/Automation/Account edit pages now load via useEditRecord and save via useSubmit; PR #7690 merged (db76773), dev deploy green. Dev screenshots match the baselines; saves and refusals checked on scratch records. Filed draft 12500 (leave guards). Memory: none applied.

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Runner ended (done)