---
phase: 24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud
plan: 07
type: execute
wave: 4
depends_on: ["24-04", "24-05", "24-06"]
files_modified:
  - app/admin/sync/route53/page.tsx
  - components/admin/route53/record-editor-dialog.tsx
autonomous: false
requirements: [SC-2, SC-4, SC-6]

must_haves:
  truths:
    - "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"
  artifacts:
    - path: "app/admin/sync/route53/page.tsx"
      provides: "Zones / Records / History / Sync tabs for Route 53"
      min_lines: 200
    - path: "components/admin/route53/record-editor-dialog.tsx"
      provides: "Create/edit record form and delete confirmation"
  key_links:
    - from: "app/admin/sync/route53/page.tsx"
      to: "/api/route53/zones"
      via: "fetch in useEffect"
      pattern: "fetch\\('/api/route53/zones"
    - from: "components/admin/route53/record-editor-dialog.tsx"
      to: "/api/route53/zones/[zoneId]/records"
      via: "fetch POST/PATCH/DELETE"
      pattern: "method: '(POST|PATCH|DELETE)'"
    - from: "app/admin/sync/route53/page.tsx"
      to: "/api/route53/zones/[zoneId]/records/[recordId]/history"
      via: "fetch on history drill-down"
      pattern: "/history"
---

<objective>
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.
</objective>

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

<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

<interfaces>
<!-- API contract from plan 24-05. Use directly. All responses are camelCase. -->

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 }

<!-- Existing shared components (do not rebuild) -->
components/admin/DataTable.tsx      — @tanstack/react-table wrapper: <DataTable columns={} data={} />
components/admin/DetailModal.tsx    — formatted/raw tab detail modal
components/admin/SyncScheduler.tsx  — schedule editor used by every /admin/sync/<x> page
components/ui/*                     — shadcn primitives (Button, Tabs, Dialog, Input, Select, Badge)
lib/hooks/use-user-timezone.ts      — useUserTimezone() for timestamp rendering
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Zones, records, and history page shell</name>
  <files>app/admin/sync/route53/page.tsx</files>
  <read_first>
    - 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)
  </read_first>
  <action>
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. **Zones** — `DataTable` 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. **History** — `DataTable` 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.
  </action>
  <verify>
    <automated>npx tsc --noEmit --pretty && npm run build</automated>
  </verify>
  <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>
  <done>The detail page renders zones, records, history with source badges, and the schedule editor, degrading cleanly when unconfigured.</done>
</task>

<task type="auto">
  <name>Task 2: Record editor dialog with create, edit, and immediate delete</name>
  <files>components/admin/route53/record-editor-dialog.tsx</files>
  <read_first>
    - 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)
  </read_first>
  <action>
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()`.
  </action>
  <verify>
    <automated>npx tsc --noEmit --pretty && npm run build && test $(grep -c "'NS'\|'SOA'" components/admin/route53/record-editor-dialog.tsx) -eq 0 && echo PASS</automated>
  </verify>
  <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>
  <done>An admin can create, edit, and delete records from the UI; double-submits are blocked; no approval workflow was introduced.</done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 3: End-to-end phase verification</name>
  <action>
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.
  </action>
  <what-built>
    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).
  </what-built>
  <how-to-verify>
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.

2. **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.

3. **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.

4. **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.

5. **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.

6. **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.

7. **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()`).

8. **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.

9. **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.
  </how-to-verify>
  <resume-signal>
Reply "approved" if all nine steps behave as described, or list the step numbers that failed
with what you observed instead.
  </resume-signal>
</task>

</tasks>

<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>

<verification>
- `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
</verification>

<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>

<output>
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.
</output>
