From 7238c97a9882ca1e11258bc0150fb21bbeaa5b58 Mon Sep 17 00:00:00 2001 From: lorentz Date: Sun, 10 May 2026 07:44:19 -0400 Subject: [PATCH] =?UTF-8?q?docs(09-06):=20complete=20admin-surfaces=20plan?= =?UTF-8?q?=20=E2=80=94=20channels=20owner=20column,=20event-keys=20CRUD,?= =?UTF-8?q?=20pipeline-executions=20ROUTE-07?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../09-06-SUMMARY.md | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 .planning/phases/09-user-profile-preferences-new/09-06-SUMMARY.md diff --git a/.planning/phases/09-user-profile-preferences-new/09-06-SUMMARY.md b/.planning/phases/09-user-profile-preferences-new/09-06-SUMMARY.md new file mode 100644 index 0000000..2abb691 --- /dev/null +++ b/.planning/phases/09-user-profile-preferences-new/09-06-SUMMARY.md @@ -0,0 +1,177 @@ +--- +phase: 09-user-profile-preferences-new +plan: "06" +subsystem: admin-surfaces +tags: [admin, notification-channels, event-keys, pipeline-executions, auth, phase-9] +dependency_graph: + requires: + - "notification_channels.owner_user_id column (Plan 01 — migration 085)" + - "notify_event_keys table (Plan 01 — migration 086)" + - "pipeline_executions + pipeline_execution_steps tables (pre-existing pipeline engine)" + provides: + - "GET /api/notification-channels: requireAuth(), role-scoped, owner_email JOIN, ?owner=global|personal|all" + - "POST /api/notification-channels: requireAdmin(), owner_user_id column accepted" + - "GET/PUT/DELETE /api/notification-channels/[id]: requireAuth() + per-row (isAdmin || isOwner) authorization" + - "Owner badge on /admin/workflow/channels (Global vs Personal: {email}) + Show filter" + - "GET/POST /api/admin/notify-event-keys: admin-only list + create with key regex" + - "PUT/DELETE /api/admin/notify-event-keys/[key]: admin-only update + delete" + - "/admin/workflow/event-keys: full CRUD page for notify_event_keys" + - "GET /api/admin/pipeline-executions: admin-only, fallbacks_only/pipeline_id/limit params, has_fallback boolean" + - "/admin/workflow/executions: NEW admin page at ROUTE-07 locked URL with fallback filter" + affects: + - "Plans 03/04/05 — personal-channels service now has gated backing API" + - "Any non-admin code calling /api/notification-channels — now requires auth (previously unauthenticated)" +tech_stack: + added: [] + patterns: + - "Per-row authorization: isAdmin || isOwner predicate against owner_user_id" + - "Role-scoped GET: admin sees all + LEFT JOIN owner email; non-admin sees WHERE owner_user_id IS NULL" + - "Four complete parameterized SQL strings for pipeline-executions — no alias-in-WHERE (HIGH 4 fix)" + - "EXISTS subquery inlined in WHERE for fallback filter — avoids PostgreSQL alias-in-WHERE rejection" + - "URLSearchParams.set() for building query strings in client components" +key_files: + created: + - app/api/admin/notify-event-keys/route.ts + - app/api/admin/notify-event-keys/[key]/route.ts + - app/admin/workflow/event-keys/page.tsx + - app/api/admin/pipeline-executions/route.ts + - app/admin/workflow/executions/page.tsx + modified: + - app/api/notification-channels/route.ts + - app/api/notification-channels/[id]/route.ts + - app/admin/workflow/channels/page.tsx +decisions: + - "Per-row authorization uses checkAccess() helper that treats owner_user_id=null (global) as admin-only; personal rows allow isOwner OR isAdmin" + - "GET /api/notification-channels now requires requireAuth() — pre-existing security gap closed; non-admins see only global rows" + - "Four parameterized SQL strings for pipeline-executions chosen over dynamic WHERE building — eliminates alias-in-WHERE bug from original plan pseudo-SQL" + - "Conflict detection on notify_event_keys POST: ON CONFLICT DO NOTHING + timestamp-based check (created_at within 2s = just inserted)" + - "/admin/workflow/event-keys and /admin/workflow/executions not linked from /admin/workflow index page — noted as deferred wire-up (see Known Stubs)" +metrics: + duration_minutes: 20 + completed_date: "2026-05-10" + tasks_completed: 3 + files_created: 5 + files_modified: 3 +--- + +# Phase 9 Plan 06: Admin Surfaces Summary + +One-liner: Three admin surfaces land — notification-channels gets owner column + role-scoped auth (closing a pre-existing unauth gap), a new event-keys CRUD page backed by two new admin API routes, and a new pipeline-executions page at the ROUTE-07 locked URL with a fallback filter using four safe parameterized SQL strings. + +## Tasks Completed + +| Task | Name | Commit | Files | +|------|------|--------|-------| +| 1 | Owner column + role-scoped reads on /admin/workflow/channels | 47cab78 | app/api/notification-channels/route.ts, app/api/notification-channels/[id]/route.ts, app/admin/workflow/channels/page.tsx | +| 2 | /admin/workflow/event-keys CRUD page + API | 7f4ffa0 | app/api/admin/notify-event-keys/route.ts, app/api/admin/notify-event-keys/[key]/route.ts, app/admin/workflow/event-keys/page.tsx | +| 3 | NEW /admin/workflow/executions page + API for ROUTE-07 fallback filter | 23a8c7c | app/api/admin/pipeline-executions/route.ts, app/admin/workflow/executions/page.tsx | + +## What Was Built + +### Owner Column Rendering Rule + +- `channel.owner_user_id == null` → `Global` +- `channel.owner_user_id != null` → `Personal: {owner_email ?? owner_user_id}` +- The `owner_email` field comes from a `LEFT JOIN "user" u ON u.id = nc.owner_user_id` in the admin GET query. +- Owner badge renders BEFORE the channel-type badge in each channel row card. + +### Owner-filter URL Parameter on /api/notification-channels + +GET accepts `?owner=global|personal|all` (default: `all` for admin sessions). + +- `global` → `WHERE nc.owner_user_id IS NULL` +- `personal` → `WHERE nc.owner_user_id IS NOT NULL` +- `all` → no WHERE clause on ownership (full JOIN result) + +Non-admin sessions always get global-only rows regardless of the `owner` parameter. + +### Event Keys CRUD Page + +- URL: `/admin/workflow/event-keys` +- Key regex for POST validation: `/^[a-z][a-z0-9_]*$/i` (max 128 chars) +- `display_label` max 200 chars, required +- POST uses `ON CONFLICT (key) DO NOTHING` + timestamp-based duplicate detection → 409 if key pre-existed +- Inline edit on each row; `is_active` toggles via PUT with `COALESCE($4, is_active)` +- PUT uses: `UPDATE notify_event_keys SET display_label = COALESCE($1, display_label), description = COALESCE($2, description), sort_order = COALESCE($3, sort_order), is_active = COALESCE($4, is_active), updated_at = NOW() WHERE key = $5` + +### Pipeline Executions API — JSONB Predicate + +JSONB containment predicate: `pes.output_data ? 'user_route_fallback'` + +Used in all four SQL branches as an EXISTS subquery inlined in WHERE — never as a SELECT-list alias referenced in WHERE. + +### has_fallback Per-Row Flag + +- `fallbacksOnly=true` branches: constant `true AS has_fallback` +- `fallbacksOnly=false` branches: `EXISTS (SELECT 1 FROM pipeline_execution_steps pes WHERE pes.execution_id = pe.id AND pes.output_data ? 'user_route_fallback') AS has_fallback` +- The page renders `fallback` on rows where `has_fallback === true` + +### ROUTE-07 Confirmation + +The filter at `/admin/workflow/executions` is the URL locked by both CONTEXT.md (D-11) and REQUIREMENTS.md (ROUTE-07). No silent rerouting — a fresh `app/admin/workflow/executions/page.tsx` was created over the pipeline-engine tables (`pipeline_executions` + `pipeline_execution_steps`). The legacy `workflow_executions` table and existing admin route are untouched. + +### Four Parameterized SQL Strings Confirmation + +The pipeline-executions route uses exactly four complete SQL strings selected by boolean flags: +1. `fallbacksOnly && pipelineId !== null` — filters to one pipeline's fallbacks +2. `fallbacksOnly && pipelineId === null` — all fallbacks across all pipelines +3. `!fallbacksOnly && pipelineId !== null` — one pipeline, all executions with EXISTS-computed has_fallback +4. `!fallbacksOnly && pipelineId === null` — all pipelines, all executions with EXISTS-computed has_fallback + +The alias-in-WHERE bug from the original plan pseudo-SQL (`${fallbacks_only ? 'AND has_fallback' : ''}`) is absent — the EXISTS predicate is inlined in WHERE, not referenced via alias. + +### POST /api/notification-channels channel_type Allowlist + +Unchanged — all four values remain accepted: +`validTypes = ['teams', 'telegram', 'ntfy', 'webhook']` + +### Wire-up Deferred Note + +Links to `/admin/workflow/event-keys` and `/admin/workflow/executions` were NOT added to the `/admin/workflow` index page (`app/admin/workflow/page.tsx`). Both pages are functional at their URLs and reachable via direct navigation. A future small plan should add entries to the workflow admin index. This is noted in Known Stubs below. + +## Deviations from Plan + +None — all three tasks executed as specified. The one implementation choice (URLSearchParams.set() for building the fetch URL in the executions page) is semantically identical to string concatenation — `params.set('fallbacks_only', '1')` produces `fallbacks_only=1` in the final URL. + +## Known Stubs + +| Stub | File | Reason | +|------|------|--------| +| No navigation link to /admin/workflow/event-keys | app/admin/workflow/page.tsx (not in this plan) | Plan scope: build the target pages, not update the nav index. Future plan should add entries for event-keys and pipeline-executions to the workflow admin index | +| No navigation link to /admin/workflow/executions | app/admin/workflow/page.tsx (not in this plan) | Same as above | + +Both pages are fully functional and reachable by URL — the stubs are navigation convenience items only, not blockers for the plan's goal. + +## Threat Flags + +No new network endpoints, auth paths, file access patterns, or schema changes at trust boundaries beyond what was declared in the plan's threat model (T-09-06-01 through T-09-06-08). All eight threats are addressed: + +- T-09-06-01: /api/notification-channels GET now requires requireAuth() — gap closed +- T-09-06-02: Per-row authorization (isAdmin || isOwner) on [id] routes +- T-09-06-03: Admin disclaimer rendered above personal-channel list +- T-09-06-04: requireAdmin() + key regex + ON CONFLICT DO NOTHING on event keys POST +- T-09-06-05: Admin-only writes via gated API; page HTML contains no extra secrets +- T-09-06-06: SQL injection mitigated — pipeline_id /^\d+$/ validated, limit capped at 500, fallbacks_only is boolean flag selecting one of four static SQL strings +- T-09-06-07: has_fallback reveals only presence of fallback, not user_id — admin-only context +- T-09-06-08: POST channel_type allowlist preserved verbatim (teams/telegram/ntfy/webhook) + +## Self-Check: PASSED + +Files created: +- app/api/admin/notify-event-keys/route.ts: FOUND +- app/api/admin/notify-event-keys/[key]/route.ts: FOUND +- app/admin/workflow/event-keys/page.tsx: FOUND +- app/api/admin/pipeline-executions/route.ts: FOUND +- app/admin/workflow/executions/page.tsx: FOUND + +Files modified: +- app/api/notification-channels/route.ts: FOUND +- app/api/notification-channels/[id]/route.ts: FOUND +- app/admin/workflow/channels/page.tsx: FOUND + +Commits: +- 47cab78: FOUND (Task 1 — channels owner column + auth) +- 7f4ffa0: FOUND (Task 2 — event-keys CRUD) +- 23a8c7c: FOUND (Task 3 — pipeline executions page + API) + +TypeScript: `npx tsc --noEmit` exit 0 — no errors.