---
phase: 24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud
plan: 06
type: execute
wave: 3
depends_on: ["24-02"]
files_modified:
  - lib/services/sync-scheduler.ts
  - app/admin/sync/page.tsx
  - public/logos/route53.svg
autonomous: true
requirements: [SC-1, SC-6]

must_haves:
  truths:
    - "SC-1/D-11: Two cron schedules exist — route53-incremental (every 15 minutes) and route53-full (daily) — both seeded into sync_schedules and editable from /admin, matching how every other integration's schedules are managed"
    - "SC-6/D-09: A Route 53 tile appears in the /admin/sync integration list with the same shape as the Veeam / Datto RMM / PAX8 tiles, linking to the dedicated /admin/sync/route53 detail page built in plan 24-07 — the existing per-integration pattern, not folded into an existing page"
    - "D-10: The scheduler dispatch branch for Route 53 checks isRoute53Configured() only — it does NOT consult integration_settings.disabled, so disabling Route 53 in /admin/integrations suppresses health display without stopping sync (PAX8 remains the sole blocking exception)"
    - "New schedules are seeded is_enabled: false, matching every other newly-introduced integration in this file, so nothing starts hitting AWS before an operator enables it"
  artifacts:
    - path: "lib/services/sync-scheduler.ts"
      provides: "route53-incremental and route53-full in the sync_type union, defaultSchedules, and the dispatch chain"
      contains: "route53-incremental"
    - path: "app/admin/sync/page.tsx"
      provides: "route53 entry in the INTEGRATIONS tile array"
      contains: "id: 'route53'"
    - path: "public/logos/route53.svg"
      provides: "Tile logo asset"
  key_links:
    - from: "lib/services/sync-scheduler.ts"
      to: "lib/services/route53-sync-service.ts"
      via: "dynamic import of getRoute53SyncService inside the dispatch branch"
      pattern: "getRoute53SyncService"
    - from: "app/admin/sync/page.tsx"
      to: "/admin/sync/route53"
      via: "tile href"
      pattern: "/admin/sync/route53"
---

<objective>
Wire Route 53 into the two existing operator surfaces it must appear in: the node-cron sync
scheduler (D-11's incremental + daily-full cadence) and the `/admin/sync` integration tile
list.

Purpose: SC-1 (sync runs on a schedule) and SC-6 (integration appears in the existing sync
admin UI/scheduler alongside the others).
Output: modified `lib/services/sync-scheduler.ts` and `app/admin/sync/page.tsx`, plus a
`public/logos/route53.svg` tile asset.
</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
@.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-02-SUMMARY.md

<interfaces>
<!-- Existing contracts in the files being modified. Read the live files; these are the anchors. -->

lib/services/sync-scheduler.ts:
  line 25:  sync_type: 'incremental' | 'full' | 'veeam-incremental' | ... | 'phishing-sweep';
  line 147: sync_schedules table DDL — sync_type VARCHAR(30) NOT NULL (no CHECK constraint;
            'route53-incremental' is 19 chars and fits)
  line 180: const defaultSchedules = [ ... ]   // entries: { id, name, description, cron_expression, sync_type, is_enabled }
  line 312: for (const schedule of defaultSchedules) { INSERT INTO sync_schedules ... ON CONFLICT DO NOTHING }
  line 413+: dispatch chain — if (config.sync_type === 'veeam-incremental') { ... } else if (...)
  line 478-493: pax8-daily branch — the ONE branch that also checks integration_settings.disabled.
                Route 53 must NOT copy that gate (D-10).
  line 494-504: mimecast-sync branch — config-check-only pattern; this is the shape to copy.

app/admin/sync/page.tsx:
  lines 9-16:  interface IntegrationCard { id, category, product, description, href, logo, color }
  lines 20-29: const INTEGRATIONS: IntegrationCard[] = [ ... 'pax8' entry is last ]
  lines 32-39: COLOR_MAP — available keys: red, green, blue, orange, purple, gray (no others)
  line 266:    const colors = COLOR_MAP[intg.color];   // an unmapped color yields undefined

<!-- From plan 24-02 -->
lib/services/route53-sync-service.ts:
  getRoute53SyncService(): Route53SyncService
  #fullSync(triggeredBy?): Promise<Route53SyncResult>
  #incrementalSync(triggeredBy?): Promise<Route53SyncResult>

<!-- From plan 24-01 -->
lib/services/route53-factory.ts:
  isRoute53Configured(): boolean
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add route53 sync types, schedules, and dispatch branches to the scheduler</name>
  <files>lib/services/sync-scheduler.ts</files>
  <read_first>
    - lib/services/sync-scheduler.ts (read the full file — the sync_type union at line 25, the sync_schedules DDL at line 147, defaultSchedules at lines 180-311, the seed loop at line 312, and the whole dispatch chain from line 413 onward)
    - lib/services/route53-sync-service.ts (getRoute53SyncService — plan 24-02)
    - lib/services/route53-factory.ts (isRoute53Configured — plan 24-01)
    - .planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-PATTERNS.md (sync-scheduler section — the exact branch shape, and the explicit instruction NOT to copy PAX8's disable gate)
    - .planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-CONTEXT.md (D-10, D-11)
  </read_first>
  <action>
Modify `lib/services/sync-scheduler.ts` in three places.

1. Line 25 `sync_type` union: append `| 'route53-incremental' | 'route53-full'` to the
   existing pipe-delimited string union. Do not reformat the rest of the line.

2. `defaultSchedules` array (starts line 180): append two entries matching the exact object
   shape of the surrounding `veeam-incremental` / `veeam-full` entries at lines 198-221:
   - id `route53-incremental`, name `Route 53 Incremental Sync`, description
     `Syncs AWS Route 53 hosted zones and records every 15 minutes`, cron_expression
     `*/15 * * * *`, sync_type `route53-incremental`, is_enabled `false`.
   - id `route53-full`, name `Route 53 Full Sync`, description
     `Full AWS Route 53 zone and record reconciliation daily at 4:00 AM`, cron_expression
     `0 4 * * *`, sync_type `route53-full`, is_enabled `false`.
   `is_enabled: false` matches every newly-introduced integration in this file — an operator
   enables them from `/admin` after confirming credentials. The `0 4 * * *` slot avoids the
   known collisions at 2:00 AM (`veeam-full`, `qbo-sync-2am`, `mimecast-sync`) and 3:00 AM
   (`weekly-full`'s `0 3 * * 0`). Enumerate every `cron_expression` in the live array before
   committing and pick the next free hour if 4:00 AM is now occupied.
   The seed loop at line 312 uses `ON CONFLICT DO NOTHING`, so existing deployments pick
   these up without overwriting operator-modified rows.

3. Dispatch chain (line 413 onward): add two `else if` branches following the
   `mimecast-sync` config-check-only shape at lines 494-504, NOT the `pax8-daily` shape at
   lines 478-493:
   - `else if (config.sync_type === 'route53-incremental')` — dynamically
     `await import('@/lib/services/route53-factory')` for `isRoute53Configured`; if false,
     log `[SCHEDULER] Skipping route53-incremental — Route 53 not configured` and return;
     otherwise dynamically `await import('@/lib/services/route53-sync-service')` and call
     `getRoute53SyncService().incrementalSync('scheduled')`.
   - `else if (config.sync_type === 'route53-full')` — same shape, calling
     `fullSync('scheduled')`.
   Use dynamic `import()` in both branches (as every other branch does) so the sync service
   module is not eager-loaded at scheduler-import time. The scheduler self-initializes as a
   side effect of its first server-side import, and a top-level import here would pull the
   AWS SDK into every server module graph.

CRITICAL (D-10): do NOT add an `integration_settings.disabled` query to either branch. PAX8
is the codebase's sole exception where disabling also blocks sync; CONTEXT.md D-10 states
Route 53 explicitly does not join that list. Add a short comment above the first Route 53
branch recording this, so a future reader does not "fix" the apparent inconsistency with the
PAX8 branch sitting a few lines above.

Confirm `sync_schedules.sync_type` at line 147 is `VARCHAR(30)` with no CHECK constraint
before relying on the new values fitting — `route53-incremental` is 19 characters.
  </action>
  <verify>
    <automated>npx tsc --noEmit --pretty && test $(grep -c "route53-incremental" lib/services/sync-scheduler.ts) -ge 3 && test $(grep -c "route53-full" lib/services/sync-scheduler.ts) -ge 3 && test $(grep -A12 "config.sync_type === 'route53" lib/services/sync-scheduler.ts | grep -c integration_settings) -eq 0 && echo PASS</automated>
  </verify>
  <acceptance_criteria>
    - `route53-incremental` and `route53-full` each appear at least 3 times in `lib/services/sync-scheduler.ts` (union, defaultSchedules, dispatch)
    - Both new `defaultSchedules` entries have `is_enabled: false`
    - Neither Route 53 dispatch branch body contains `integration_settings` (D-10) — grep over the 12 lines following each branch head returns 0
    - Both dispatch branches use dynamic `await import(...)`; no top-level route53 import exists: `grep -c "^import.*route53" lib/services/sync-scheduler.ts` returns 0
    - The `route53-full` cron hour differs from every other daily `cron_expression` hour in `defaultSchedules` — verified by enumerating the array
    - A comment above the Route 53 branches records why the PAX8 disable gate is deliberately absent
    - `npx tsc --noEmit --pretty` exits 0
  </acceptance_criteria>
  <done>Scheduler knows both Route 53 sync types, seeds them disabled, dispatches via dynamic import, and never consults the disable toggle.</done>
</task>

<task type="auto">
  <name>Task 2: Add the Route 53 tile to /admin/sync</name>
  <files>app/admin/sync/page.tsx, public/logos/route53.svg</files>
  <read_first>
    - app/admin/sync/page.tsx (read the full file — the IntegrationCard interface at lines 9-16, the INTEGRATIONS array at lines 20-29, COLOR_MAP at lines 32-39, and the tile render at line ~266 to confirm how `logo` is consumed)
    - public/logos/ directory listing (confirm the existing asset naming convention — every current asset is `.ico`)
    - .planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-PATTERNS.md (admin sync page section)
  </read_first>
  <action>
Create `public/logos/route53.svg` — a small hand-authored SVG with `viewBox="0 0 32 32"`,
`xmlns="http://www.w3.org/2000/svg"`, using the AWS orange `#FF9900`, containing a simple
globe-or-DNS-node glyph built from primitive shapes only. It must contain no `<script>`
element, no external `href`/`xlink:href` reference, and no embedded raster data. Every
existing asset in `public/logos/` is an `.ico`; an SVG is used here because the file must be
authored offline rather than downloaded. Read the tile render near line 266 first: if `logo`
is consumed as a plain image `src`, `.svg` works unchanged; if anything assumes an `.ico`
extension, change the asset format rather than the render code.

Append one entry to the `INTEGRATIONS` array in `app/admin/sync/page.tsx`, after the `pax8`
entry, matching the existing single-line object formatting and column alignment:
`id` `route53`, `category` `DNS`, `product` `AWS Route 53`, `description`
`Hosted zones, DNS records, change history, NS-delegation health`, `href`
`/admin/sync/route53`, `logo` `/logos/route53.svg`, `color` `orange`.

`color` must be a key present in `COLOR_MAP` (lines 32-39: `red`, `green`, `blue`, `orange`,
`purple`, `gray`) — `COLOR_MAP[intg.color]` is dereferenced at line ~266 and an unmapped key
yields `undefined`. `orange` is already used by `datto-rmm`, and reuse is already the norm in
this array (`blue` three times, `green` twice, `purple` twice).

Do not modify `IntegrationCard`, `COLOR_MAP`, or the render logic. This task is one array
append plus one static asset.
  </action>
  <verify>
    <automated>npx tsc --noEmit --pretty && grep -q "id: 'route53'" app/admin/sync/page.tsx && grep -q "/admin/sync/route53" app/admin/sync/page.tsx && test -f public/logos/route53.svg && test $(grep -ci "script\|xlink:href" public/logos/route53.svg) -eq 0 && echo PASS</automated>
  </verify>
  <acceptance_criteria>
    - `public/logos/route53.svg` exists, opens with an `<svg` root carrying a `viewBox` attribute, and contains zero occurrences of `script` or `xlink:href`
    - `app/admin/sync/page.tsx` `INTEGRATIONS` array contains an entry with `id: 'route53'` and `href: '/admin/sync/route53'`
    - The entry's `color` value is one of `red|green|blue|orange|purple|gray` (a key present in `COLOR_MAP`)
    - `git diff app/admin/sync/page.tsx` shows only added lines inside the `INTEGRATIONS` array — no change to `IntegrationCard`, `COLOR_MAP`, or the JSX below
    - `npx tsc --noEmit --pretty` exits 0
    - `curl -s -o /dev/null -w '%{http_code}' localhost:3100/logos/route53.svg` returns 200 when the dev server is running
  </acceptance_criteria>
  <done>The Route 53 tile renders on /admin/sync with a valid color key and a working logo asset, linking to the detail page built in plan 24-07.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| node-cron scheduler → AWS Route 53 API | Unattended, recurring outbound calls with production credentials |
| Static asset → browser | SVG served from `public/` renders inline in an authenticated admin page |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-24-21 | Denial of Service | unattended sync hitting Route 53 API rate limits | mitigate | 15-minute incremental plus one daily full is well inside Route 53's current token-bucket limits (50-burst / 10-per-second default action bucket per 24-RESEARCH.md's verified throttling numbers). Both schedules seed `is_enabled: false` so nothing runs until an operator enables it, and cadence stays operator-editable from `/admin`. |
| T-24-22 | Elevation of Privilege | AWS SDK eager-loaded into every server module graph via a top-level scheduler import | mitigate | Both dispatch branches use dynamic `await import()`, matching every other branch, keeping the AWS client out of unrelated module graphs and out of any hot path. Grep-asserted. |
| T-24-23 | Tampering | active-content injection through the tile logo asset | mitigate | `public/logos/route53.svg` is hand-authored from primitive shapes with no `<script>` element, no external reference, and no embedded raster data. Grep-asserted. |
| T-24-24 | Repudiation | operator confusion causing sync to be silently stopped or silently left running | mitigate | The deliberate absence of the PAX8 disable gate is recorded in an inline comment above the branches and in this plan's `must_haves`, so the D-10 behavior is discoverable at the code site rather than only in planning artifacts. |
| T-24-05 | Tampering / Spoofing | live DNS record content | accept | Carried forward from plan 24-01 — D-03 accepts immediate execution with post-hoc audit only. This plan adds no write path. |
</threat_model>

<verification>
- `npx tsc --noEmit --pretty` exits 0
- `npm test` full suite green
- `npm run build` succeeds (the scheduler is imported at server startup; a bad dynamic import surfaces here)
- `/admin/sync` renders 11 tiles including AWS Route 53, and the tile links to `/admin/sync/route53`
- `SELECT id, cron_expression, is_enabled FROM sync_schedules WHERE sync_type LIKE 'route53%'` returns two rows, both `is_enabled = false`
</verification>

<success_criteria>
- `route53-incremental` and `route53-full` present in the sync_type union, defaultSchedules, and the dispatch chain
- Both schedules seeded disabled with non-colliding cron expressions
- Dispatch branches gate on `isRoute53Configured()` only, never on `integration_settings` (D-10)
- Route 53 tile visible on `/admin/sync` with a valid COLOR_MAP key and a working logo asset
</success_criteria>

<output>
Create `.planning/phases/24-aws-route-53-dns-sync-track-changes-crud-operations-full-aud/24-06-SUMMARY.md` when done.
Record the final cron expressions chosen for both schedules.
</output>
