wulf-pulse/.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-07-PLAN.md

24 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud 07 execute 4
24-04
24-05
24-06
app/admin/sync/route53/page.tsx
components/admin/route53/record-editor-dialog.tsx
false
SC-2
SC-4
SC-6
truths artifacts key_links
SC-6: /admin/sync/route53 renders zones, records, and change history using the existing DataTable / DetailModal / SyncScheduler vocabulary, matching /admin/sync/veeam's structure
SC-2: An admin can create, edit, and delete a DNS record from the page, and the change reaches AWS Route 53 through the plan 24-05 write routes
SC-4: Record-level change history is visible per record, showing before/after values, the actor, the timestamp, and whether the change came from Pulse (pulse_crud) or was detected externally (sync_detected_drift)
D-01: The record-type selector offers only A, AAAA, CNAME, MX, TXT, SRV — and NS/SOA rows render as read-only with no edit or delete control (the server-side 400 remains the real gate; this is UI consistency, not the enforcement point)
D-03: The delete control executes immediately after a single confirmation dialog — the confirmation is a misclick guard, not an approval workflow, and there is no pending/approval state
T-24-20: The save/delete control is disabled while a request for that zone is in flight, avoiding PriorRequestNotComplete from double-submits
path provides min_lines
app/admin/sync/route53/page.tsx Zones / Records / History / Sync tabs for Route 53 200
path provides
components/admin/route53/record-editor-dialog.tsx Create/edit record form and delete confirmation
from to via pattern
app/admin/sync/route53/page.tsx /api/route53/zones fetch in useEffect fetch('/api/route53/zones
from to via pattern
components/admin/route53/record-editor-dialog.tsx /api/route53/zones/[zoneId]/records fetch POST/PATCH/DELETE method: '(POST|PATCH|DELETE)'
from to via pattern
app/admin/sync/route53/page.tsx /api/route53/zones/[zoneId]/records/[recordId]/history fetch on history drill-down /history
Build the `/admin/sync/route53` detail page (D-09): zones, records, per-record change history, and the record editor that drives the plan 24-05 CRUD routes — then run the end-to-end human verification for the whole phase.

Purpose: SC-6 (the integration is usable from the existing admin surface), SC-2 (CRUD is reachable by an operator, not only by curl), SC-4 (history is visible, not just queryable). Output: app/admin/sync/route53/page.tsx, components/admin/route53/record-editor-dialog.tsx, and a completed phase verification checkpoint.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @DESIGN.md @.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-PATTERNS.md @.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-CONTEXT.md @.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-04-SUMMARY.md @.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-05-SUMMARY.md @.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-06-SUMMARY.md

GET /api/route53/sync -> { inProgress: boolean, counts: { zones, records, historyRows }, history: SyncHistoryRow[] } POST /api/route53/sync body { syncType?: 'full' | 'incremental' } -> { ok: true, message } | 409 { error: 'Sync already in progress' } | 503

GET /api/route53/zones -> Route53Zone[] { id, name, comment, privateZone, recordCount, authoritativeNameServers, syncedAt }

GET /api/route53/zones/{zoneId}/records?type=&search= -> Route53Record[] { recordKey, zoneId, name, type, setIdentifier, ttl, resourceRecords, aliasTarget, syncedAt } POST /api/route53/zones/{zoneId}/records body { name, type, ttl, resourceRecords: [{ value }] } -> 201 { auditId, status: 'committed', propagationStatus: 'INSYNC'|'PENDING', record } | 400 { error, message } (NS/SOA or invalid shape) | 409 { error: 'Record already exists' } | 502 { auditId, status: 'failed', error, message }

PATCH /api/route53/zones/{zoneId}/records/{recordId} body { name, type, ttl, resourceRecords } DELETE /api/route53/zones/{zoneId}/records/{recordId} -> same success/failure envelope as POST recordId is the URL-encoded record_key: ${zoneId}:${name}:${type}:${setIdentifier ?? ''}

GET /api/route53/zones/{zoneId}/records/{recordId}/history?limit= -> Route53RecordHistory[] { id, recordName, recordType, changeAction, beforeValue, afterValue, source, changedByEmail, changedAt }

components/admin/DataTable.tsx — @tanstack/react-table wrapper: components/admin/DetailModal.tsx — formatted/raw tab detail modal components/admin/SyncScheduler.tsx — schedule editor used by every /admin/sync/ page components/ui/* — shadcn primitives (Button, Tabs, Dialog, Input, Select, Badge) lib/hooks/use-user-timezone.ts — useUserTimezone() for timestamp rendering

Task 1: Zones, records, and history page shell app/admin/sync/route53/page.tsx - app/admin/sync/veeam/page.tsx (read in full — the canonical multi-tab detail page: imports at lines 3-22, data-fetch tab pattern at lines 197-243, manual-sync-trigger at lines 567-593) - app/admin/sync/pax8/page.tsx (a more recent, smaller example of the same page shape) - components/admin/DataTable.tsx (column definition contract) - components/admin/DetailModal.tsx (formatted/raw tab props) - components/admin/SyncScheduler.tsx (props — how other pages embed the schedule editor) - lib/types/route53.ts (response shapes) - DESIGN.md (page header, spacing, and component vocabulary rules) Create `app/admin/sync/route53/page.tsx` as a `'use client'` page following `app/admin/sync/veeam/page.tsx`'s structure. Use `useState` / `useEffect` / `fetch` only — no SWR, no react-query, no server actions (CLAUDE.md).

Page header: product name AWS Route 53, a back link to /admin/sync, a "Sync Now" button, and a status line showing counts.zones / counts.records / counts.historyRows plus the most recent sync_history entry's status and completion time (rendered through useUserTimezone()).

Four tabs using components/ui/tabs:

  1. ZonesDataTable over GET /api/route53/zones. Columns: name, zone id, private (badge), record count, synced at. Clicking a row selects that zone and switches to the Records tab with the zone pre-filtered. Show authoritativeNameServers in a DetailModal drill-down so an operator can compare against the health check's finding from plan 24-04.

  2. Records — a zone selector (shadcn Select populated from the zones response) plus a DataTable over GET /api/route53/zones/{zoneId}/records. Columns: name, type (badge), TTL, values (join resourceRecords values with a comma, truncated with a title attribute), and an actions cell. Render record values as plain text through React's default escaping — no dangerouslySetInnerHTML anywhere on this page (T-24-25). Wire the ?type= and ?search= query params to a type filter and a search input. Actions cell: for a record whose type is one of A/AAAA/CNAME/MX/TXT/SRV, render Edit and Delete buttons; for any other type (notably NS and SOA) render a muted "read-only" label with no controls, and add a title explaining that NS and SOA are zone-delegation records excluded from the Pulse write path (D-01). The server-side 400 in plan 24-05 remains the actual gate — this is UI consistency, not enforcement. Also render a "New record" button that opens the editor dialog in create mode.

  3. HistoryDataTable over the currently-selected record's GET /api/route53/zones/{zoneId}/records/{recordId}/history, plus a zone-wide view when no record is selected. Columns: changed at, record name, type, change action (badge), source (badge — visually distinguish pulse_crud from sync_detected_drift, since answering "did someone change this outside Pulse?" is D-06's whole point), and actor (changedByEmail, or an em dash for drift rows, which have no Pulse actor). Clicking a row opens DetailModal with the before/after JSONB rendered side by side using the existing formatted/raw tabs — do not add a JSON-diff library (24-RESEARCH.md "Don't Hand-Roll").

  4. Schedule — embed components/admin/SyncScheduler.tsx the same way app/admin/sync/veeam/page.tsx does, so the route53-incremental and route53-full schedules seeded in plan 24-06 are editable here.

"Sync Now" posts to /api/route53/sync, disables itself while inProgress is true, and polls GET /api/route53/sync every 3 seconds until inProgress flips false (bounded at 20 polls), then refetches the active tab. Surface failures with toast.error from sonner; surface a 409 as an informational toast rather than an error.

Handle the unconfigured case explicitly: if GET /api/route53/sync returns 503, render an empty-state card explaining that AWS credentials are not configured and that they are injected via BWS at the container entrypoint — do not render broken tables. npx tsc --noEmit --pretty && npm run build <acceptance_criteria> - app/admin/sync/route53/page.tsx exists, starts with 'use client', and default-exports a component - grep -c "dangerouslySetInnerHTML" app/admin/sync/route53/page.tsx returns 0 (T-24-25) - grep -c "swr\|react-query\|useSWR" app/admin/sync/route53/page.tsx returns 0 (CLAUDE.md) - The page fetches all four endpoints: grep -c "/api/route53/" app/admin/sync/route53/page.tsx >= 4 - The history table renders the source field: grep -q "sync_detected_drift" app/admin/sync/route53/page.tsx - The actions cell gates on the writable type list: grep -q "'SRV'" app/admin/sync/route53/page.tsx - npm run build succeeds - npx tsc --noEmit --pretty exits 0 - Visiting /admin/sync/route53 as an admin returns HTTP 200 and renders four tab triggers </acceptance_criteria> The detail page renders zones, records, history with source badges, and the schedule editor, degrading cleanly when unconfigured.

Task 2: Record editor dialog with create, edit, and immediate delete components/admin/route53/record-editor-dialog.tsx - app/admin/sync/route53/page.tsx (as written in Task 1 — the props the dialog receives) - components/ui/dialog.tsx, components/ui/select.tsx, components/ui/input.tsx (shadcn primitive APIs available in this project) - lib/services/route53-record-validation.ts (WRITABLE_RECORD_TYPES — the UI selector must offer exactly this list, and the same TTL/value constraints so client and server agree) - app/api/route53/zones/[zoneId]/records/[recordId]/route.ts (plan 24-05 — the exact success/failure response envelope to handle) - components/admin/DetailModal.tsx (existing dialog styling conventions to match) Create `components/admin/route53/record-editor-dialog.tsx` exporting a `RecordEditorDialog` PascalCase component from the kebab-case file (CLAUDE.md naming rule).

Props: { open, onOpenChange, zoneId, mode: 'create' | 'edit', record?: Route53Record, onSaved: () => void }.

Form fields (plain useState, no react-hook-form — CLAUDE.md scopes react-hook-form to admin/auth forms and this matches the surrounding /admin/sync/* pages' plain-state style):

  • name — text input; in edit mode it is read-only, because changing the name of a Route 53 recordset is a delete-plus-create, not an update, and the phase does not implement that.
  • type — shadcn Select whose options are exactly A, AAAA, CNAME, MX, TXT, SRV (D-01). NS and SOA must not appear as options. Read-only in edit mode for the same reason as name.
  • ttl — number input, default 300, constrained to 0..2147483647.
  • resourceRecords — a repeatable list of text inputs with add/remove controls, minimum one non-empty entry, capped at 100 entries to match the server-side validator.

Submit behavior:

  • create mode: POST /api/route53/zones/{zoneId}/records
  • edit mode: PATCH /api/route53/zones/{zoneId}/records/{encodeURIComponent(recordKey)} Disable the submit button while the request is in flight and until the response settles (T-24-20 — prevents PriorRequestNotComplete from a double-click against the same hosted zone). On a 2xx response call toast.success including the returned propagationStatus (INSYNC → "Propagated", PENDING → "Submitted — propagating"), then onSaved() and close. On 400 / 409 / 502 render the response body's message field inline in the dialog AND as a toast.error, and keep the dialog open with the user's input intact so it can be corrected.

Delete: export a RecordDeleteConfirm component (or a mode: 'delete' branch of the same dialog) that shows the record's current name, type, TTL, and full value list, plus a single "Delete record" confirm button issuing DELETE /api/route53/zones/{zoneId}/records/{encodeURIComponent(recordKey)}. Per D-03 this executes immediately on confirm — do NOT add a typed-name confirmation, a second approval step, an approver field, or any pending state. The dialog exists as a misclick guard only; state that in a code comment so a future reader does not mistake it for an approval workflow and does not "strengthen" it into one.

Wire both into the Records tab's actions cell from Task 1, refetching the records list and the history list via onSaved(). npx tsc --noEmit --pretty && npm run build && test $(grep -c "'NS'|'SOA'" components/admin/route53/record-editor-dialog.tsx) -eq 0 && echo PASS <acceptance_criteria> - components/admin/route53/record-editor-dialog.tsx exports RecordEditorDialog (PascalCase from a kebab-case file) - The type selector's option list contains exactly the six writable types; grep -c "'NS'\|'SOA'" components/admin/route53/record-editor-dialog.tsx returns 0 - The submit button's disabled prop is bound to an in-flight state variable (T-24-20) - grep -c "confirmText\|typeToConfirm\|approval\|pendingApproval" components/admin/route53/record-editor-dialog.tsx returns 0 (D-03 — misclick guard only, no approval workflow) - Delete issues method: 'DELETE' against the encoded record key: grep -q "encodeURIComponent" components/admin/route53/record-editor-dialog.tsx - Error responses render the server message inline; a 400 leaves the dialog open (verified in the checkpoint below) - npm run build succeeds; npx tsc --noEmit --pretty exits 0 </acceptance_criteria> An admin can create, edit, and delete records from the UI; double-submits are blocked; no approval workflow was introduced.

Task 3: End-to-end phase verification Pause execution and present the nine verification steps below to the developer verbatim. Confirm the dev server is running on port 3100 first. Run any command the developer asks you to run on their behalf. Do not mark the phase complete until they respond. Record the per-step outcomes in the SUMMARY and update 24-VALIDATION.md's Manual-Only Verifications rows with the observed results. The full Phase 24 stack: `@aws-sdk/client-route-53` + migration 102 + factory (24-01); zone/record sync with drift history (24-02); record validation and the audit lifecycle (24-03); the health check with the D-12 NS-delegation comparison (24-04); the `/api/route53/*` read and CRUD routes (24-05); scheduler entries and the `/admin/sync` tile (24-06); and the `/admin/sync/route53` detail page with the record editor (this plan). Everything below is already automated — this checkpoint confirms the live round-trip against a real AWS account, which cannot be safely automated (see 24-VALIDATION.md's Manual-Only Verifications table). 1. **Sync (SC-1).** Visit `http://localhost:3100/admin/sync` — confirm an "AWS Route 53" tile appears with a working logo. Click it, then click "Sync Now". Expect the zone and record counts to become non-zero and a `sync_history` row with `entity_type='route53'` and `status='completed'` to appear in the Schedule/status area.
  1. Create (SC-2, SC-3, SC-4). On the Records tab pick a zone, click "New record", and create a disposable TXT record (for example pulse-phase24-test.<yourzone> with value phase24-verification, TTL 300). Expect a success toast naming the propagation status. Confirm the record now exists in the AWS console. Then check the History tab: one row with change_action='create', source='pulse_crud', and your email as the actor.

  2. Update. Edit that record's value to phase24-verification-updated. Confirm the change in the AWS console and a second history row with change_action='update' whose before_value holds the original value.

  3. Delete (D-03). Delete the record. Confirm exactly one confirmation dialog appears and that confirming executes immediately with no approval step. Confirm it disappears from the AWS console and a third history row with change_action='delete' exists.

  4. Audit completeness (SC-3, D-07). Run: docker exec pulse-postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "SELECT operation, status, record_name, performed_by_email, aws_change_status, left(coalesce(error_message,''),80) FROM route53_audit_log ORDER BY performed_at DESC LIMIT 10;" Expect 3 rows with status='committed' from steps 2-4, each with your email.

  5. Failure logging (D-07, T-24-01). As an admin, attempt an NS write: curl -s -o /dev/null -w '%{http_code}\n' -X POST 'http://localhost:3100/api/route53/zones/<ZONEID>/records' -H 'Content-Type: application/json' -H "Cookie: <your session cookie>" -d '{"name":"ns-test.<yourzone>","type":"NS","ttl":300,"resourceRecords":[{"value":"ns1.example.com"}]}' Expect 400. Then force a genuine AWS-side failure (for example a CNAME at the zone apex, which Route 53 rejects) and confirm the response is 502 and a route53_audit_log row with status='failed' and a non-empty, sanitized error_message exists.

  6. Auth gating (D-04, T-24-02). Sign in as a user-role account (or reuse its session cookie) and repeat the create curl. Expect 403. Confirm GET /api/route53/zones still returns 200 for that same non-admin session (reads are requireAuth(), writes are requireAdmin()).

  7. Drift detection (D-06). Change a record directly in the AWS console (edit any TXT value in a synced zone), then click "Sync Now" in Pulse. Expect a history row for that record with source='sync_detected_drift' and a null actor.

  8. Health check (D-12, SC-6). Visit /admin/integrations (or whichever page renders checkIntegrationHealth) and confirm an "AWS Route 53" row with a live status. If a zone's registrar-level NS records genuinely differ from Route 53's delegation set, confirm the row reports the mismatch. Then toggle Route 53 off at /admin/integrations, wait for the 5-minute health cache (or trigger the PATCH which clears it), and confirm the health row shows disabled while a manual POST /api/route53/sync still succeeds — that is D-10's display-only behavior, and its failure mode (sync silently stopping) is the specific thing this step exists to catch. Reply "approved" if all nine steps behave as described, or list the step numbers that failed with what you observed instead.

<threat_model>

Trust Boundaries

Boundary Description
AWS-sourced record values → rendered admin page Data controlled outside Pulse is displayed to an authenticated admin
Admin browser → /api/route53/* write routes Operator-initiated live DNS mutation

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-24-25 Tampering (stored XSS) record values, history before/after JSONB, and AWS error messages rendered on the page mitigate All values render as text through React's default escaping. dangerouslySetInnerHTML is absent from both files (grep-asserted). Record values are never rendered as anchors or src/href attributes, so an attacker-controlled CNAME/TXT value cannot become a navigable link.
T-24-02 Elevation of Privilege edit/delete controls visible to a non-admin session mitigate UI gating is cosmetic; the real control is requireAdmin() on every write route (plan 24-05). Checkpoint step 7 verifies a user-role session receives 403 when calling the API directly, independent of what the UI shows.
T-24-20 Denial of Service rapid double-submit producing PriorRequestNotComplete on the same hosted zone mitigate Submit and delete controls bind disabled to an in-flight state variable; the server side additionally classifies PriorRequestNotComplete as retryable with bounded backoff (plan 24-05).
T-24-01 Tampering NS/SOA edit reachable from the UI mitigate The type selector offers only the six D-01 types and NS/SOA rows render read-only. Enforcement remains the server-side 400 (plan 24-03/24-05); checkpoint step 6 verifies the API rejects NS directly, not merely that the button is hidden.
T-24-26 Repudiation operator mistaking the delete confirmation for an approval gate mitigate The confirmation is documented in code as a misclick guard only, and D-03's no-approval-gate constraint is asserted by grep in Task 2's acceptance criteria, so the behavior cannot silently drift into a partial approval workflow.
T-24-05 Tampering / Spoofing semantically malicious record values submitted by an authorized admin accept Final carry-forward of the D-03 acceptance recorded in plan 24-01's must_haves. No pre-write semantic analysis is performed at any layer. Post-hoc controls verified live in checkpoint steps 5, 6, and 8.
</threat_model>
- `npm run build` succeeds; `npx tsc --noEmit --pretty` exits 0; `npm test` full suite green - `/admin/sync` shows the AWS Route 53 tile; `/admin/sync/route53` renders four tabs - Checkpoint steps 1-9 all pass, with results recorded in the SUMMARY - 24-VALIDATION.md's three Manual-Only rows (write-route auth gating, live AWS round-trip, DNS-egress assumption) are all exercised by checkpoint steps 7, 2-4, and 9 respectively

<success_criteria>

  • /admin/sync/route53 renders zones, records, per-record history with source badges, and the schedule editor
  • An admin can create, update, and delete a record end-to-end and see it reflected in AWS
  • History distinguishes pulse_crud from sync_detected_drift
  • NS/SOA are absent from the UI type selector and rejected with 400 by the API
  • Delete executes immediately behind a single misclick guard (D-03), with no approval state
  • Disabling Route 53 in /admin/integrations suppresses the health row without stopping sync (D-10) </success_criteria>
Create `.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-07-SUMMARY.md` when done. Record the checkpoint results per step, and update `.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-VALIDATION.md`'s Manual-Only Verifications rows with their observed outcomes.