docs(09-06): complete admin-surfaces plan — channels owner column, event-keys CRUD, pipeline-executions ROUTE-07
This commit is contained in:
parent
23a8c7c5d9
commit
7238c97a98
1 changed files with 177 additions and 0 deletions
|
|
@ -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` → `<Badge variant="secondary">Global</Badge>`
|
||||
- `channel.owner_user_id != null` → `<Badge>Personal: {owner_email ?? owner_user_id}</Badge>`
|
||||
- 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 `<Badge variant="outline">fallback</Badge>` 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue