From 230296c13700bf076d5297a76587b37985944e67 Mon Sep 17 00:00:00 2001 From: lorentz Date: Sat, 11 Jul 2026 09:46:01 -0400 Subject: [PATCH 1/5] feat(13-01): seed pax8-daily sync schedule via migration 096 - Idempotent INSERT ... WHERE NOT EXISTS seed of the pax8-daily row (cron 0 4 * * *, is_enabled false), mirroring migration 089's style - Covers existing installs since createDefaultSchedules() only seeds a virgin sync_schedules table - Applied to running dev DB and verified idempotent (second run = 0 rows) --- migrations/096_pax8_daily_schedule.sql | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 migrations/096_pax8_daily_schedule.sql diff --git a/migrations/096_pax8_daily_schedule.sql b/migrations/096_pax8_daily_schedule.sql new file mode 100644 index 0000000..b5f2dd5 --- /dev/null +++ b/migrations/096_pax8_daily_schedule.sql @@ -0,0 +1,13 @@ +-- Migration 096: Seed the pax8-daily sync schedule. +-- +-- The sync_scheduler.createDefaultSchedules() path only seeds defaults on a +-- virgin sync_schedules table; this migration covers existing installs. +-- sync_schedules has no unique constraint on name, so guard with NOT EXISTS +-- (same pattern as migration 089's appgate-sessions/appgate-daily seeds). + +INSERT INTO sync_schedules (id, name, description, cron_expression, sync_type, is_enabled) +SELECT 'pax8-daily', + 'PAX8 Daily Sync', + 'Full PAX8 sync — companies, subscriptions, products, orders, and company matching, daily at 4 AM.', + '0 4 * * *', 'pax8-daily', false + WHERE NOT EXISTS (SELECT 1 FROM sync_schedules WHERE name = 'PAX8 Daily Sync'); From d07c0b782697cf9d648937b7bb4c7f5ce9d3973f Mon Sep 17 00:00:00 2001 From: lorentz Date: Sat, 11 Jul 2026 09:46:48 -0400 Subject: [PATCH 2/5] feat(13-01): dispatch pax8-daily to fullSync with dual guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extend ScheduleConfig.sync_type union with 'pax8-daily' - Add dual-guarded branch in executeScheduledSync: skips with a distinct log when PAX8 is not configured (isPax8Configured()) or when integration_settings.key='pax8' is disabled, otherwise calls getPax8SyncService().fullSync('scheduled') - PAX8-only inline check per D-01 — no shared helper, no changes to getDbDisabledKeys()/applyDisableOverlay() or other switch branches --- lib/services/sync-scheduler.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/services/sync-scheduler.ts b/lib/services/sync-scheduler.ts index 09b9382..cd0dac2 100644 --- a/lib/services/sync-scheduler.ts +++ b/lib/services/sync-scheduler.ts @@ -22,7 +22,7 @@ export interface ScheduleConfig { name: string; description: string; cron_expression: string; - sync_type: 'incremental' | 'full' | 'veeam-incremental' | 'veeam-full' | 'veeam-rpo-check' | 'contract-services' | 'engagement-daily' | 'zoom-daily' | 'morning-summary' | 'ticket-digest-daily' | 'ticket-digest-weekly' | 'ticket-digest-monthly' | 'device-link-reconcile' | 'integration-health' | 'qbo' | 'appgate-sessions' | 'appgate-daily' | 'tickets-reconcile'; + sync_type: 'incremental' | 'full' | 'veeam-incremental' | 'veeam-full' | 'veeam-rpo-check' | 'contract-services' | 'engagement-daily' | 'zoom-daily' | 'morning-summary' | 'ticket-digest-daily' | 'ticket-digest-weekly' | 'ticket-digest-monthly' | 'device-link-reconcile' | 'integration-health' | 'qbo' | 'appgate-sessions' | 'appgate-daily' | 'tickets-reconcile' | 'pax8-daily'; years_back?: number; is_enabled: boolean; last_run?: Date; @@ -461,6 +461,22 @@ class SyncScheduler { console.log( `[SCHEDULER] tickets-reconcile: scanned=${result.scanned} updated=${result.updated} flippedComplete=${result.statusFlippedToComplete} softDeleted=${result.softDeleted} errors=${result.errors}` ); + } else if (config.sync_type === 'pax8-daily') { + const { isPax8Configured } = await import('@/lib/services/pax8-factory'); + if (!isPax8Configured()) { + console.log('[SCHEDULER] Skipping pax8-daily — PAX8 not configured'); + } else { + const disabledRes = await postgresClient.query<{ disabled: boolean }>( + `SELECT disabled FROM integration_settings WHERE key = 'pax8'` + ); + const isDisabled = disabledRes.rows[0]?.disabled === true; + if (isDisabled) { + console.log('[SCHEDULER] Skipping pax8-daily — PAX8 disabled via /admin/integrations'); + } else { + const { getPax8SyncService } = await import('@/lib/services/pax8-sync-service'); + await getPax8SyncService().fullSync('scheduled'); + } + } } else if (config.sync_type === 'incremental') { await this.syncService.incrementalSync('scheduled'); } else { From d5456bb5c1cee835efc5aae5a1a4d17365d94871 Mon Sep 17 00:00:00 2001 From: lorentz Date: Sat, 11 Jul 2026 09:47:17 -0400 Subject: [PATCH 3/5] docs(13-01): record PAX8 as first DB-toggle-gates-action precedent Note in Operator config / Integration disable that the pax8 toggle is the first integration where the DB toggle stops an action (scheduler skip + 403 on manual sync route) rather than only suppressing health-check display. --- CLAUDE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index f494629..7700b81 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -137,6 +137,10 @@ Examples: `getAutotaskClient()`, `getMsgraphClient()`, `getDattoRmmClient()`, `disabled_at`, and an optional `disabled_reason`. In both cases live auth checks still run (logs surface the underlying state); the UI ignores the result for disabled integrations. + - **PAX8 is the first exception**: disabling `key='pax8'` doesn't just + suppress health-check display — the `pax8-daily` scheduler branch skips + `fullSync()` and `POST /api/pax8/sync` returns 403. Every other + integration's toggle today is display-only. ## Watch out for - A `.env` file is committed to the repo. Treat secrets as potentially real; don't From 73b33f70f5cb0784f494029ccb3d2555a3253e2e Mon Sep 17 00:00:00 2001 From: lorentz Date: Sat, 11 Jul 2026 09:47:50 -0400 Subject: [PATCH 4/5] docs(13-01): add plan summary Co-Authored-By: Claude Sonnet 5 --- .../13-01-SUMMARY.md | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 .planning/phases/13-scheduler-admin-toggle/13-01-SUMMARY.md diff --git a/.planning/phases/13-scheduler-admin-toggle/13-01-SUMMARY.md b/.planning/phases/13-scheduler-admin-toggle/13-01-SUMMARY.md new file mode 100644 index 0000000..1386ac9 --- /dev/null +++ b/.planning/phases/13-scheduler-admin-toggle/13-01-SUMMARY.md @@ -0,0 +1,97 @@ +--- +phase: 13-scheduler-admin-toggle +plan: 01 +subsystem: infra +tags: [sync-scheduler, cron, postgres, pax8, integration-toggle] + +# Dependency graph +requires: + - phase: 11-company-catalog-subscription-sync + provides: Pax8SyncService.fullSync() companies/subscriptions/products orchestration + - phase: 12-orders-invoices-company-matching + provides: Pax8SyncService.fullSync() extended with orders/invoices + company matching +provides: + - Idempotent pax8-daily sync_schedules seed row (migration 096) + - executeScheduledSync dual-guarded pax8-daily dispatch branch + - CLAUDE.md precedent note for DB-toggle-gates-action behavior +affects: [13-02-admin-toggle-route, 13-03-live-verification] + +# Tech tracking +tech-stack: + added: [] + patterns: [dual-guard scheduler branch (config + DB toggle), idempotent sync_schedules seed migration] + +key-files: + created: [migrations/096_pax8_daily_schedule.sql] + modified: [lib/services/sync-scheduler.ts, CLAUDE.md] + +key-decisions: + - "pax8-daily seeded only via migration 096, never added to the in-code defaultSchedules array, per existing precedent for existing installs" + - "PAX8 disabled-check is inline in the scheduler branch only (D-01) — no shared helper extracted, no changes to other integrations' branches" + - "cron 0 4 * * * groups pax8-daily with the backend-reconciliation cluster (contract-services, tickets-reconcile) per D-04" + +patterns-established: + - "Dual-guard scheduler branch: isXConfigured() env check first, then integration_settings.disabled DB check, each with a distinct skip log, before calling the sync service" + +requirements-completed: [PAX8-07, PAX8-09] + +# Metrics +duration: ~15min +completed: 2026-07-11 +--- + +# Phase 13 Plan 01: Scheduler pax8-daily Wiring Summary + +**Wired the existing `Pax8SyncService.fullSync()` into the daily cron scheduler via a new idempotent migration seed and a dual-guarded `pax8-daily` dispatch branch that respects both the env-config check and the `integration_settings` DB disable toggle.** + +## Performance + +- **Duration:** ~15 min +- **Tasks:** 3 +- **Files modified:** 3 (1 created, 2 modified) + +## Accomplishments +- `migrations/096_pax8_daily_schedule.sql` idempotently seeds a `pax8-daily` row (cron `0 4 * * *`, `is_enabled=false`) using the `WHERE NOT EXISTS` style from migration 089; applied to the running dev DB and verified idempotent on a second run. +- `executeScheduledSync` in `lib/services/sync-scheduler.ts` now dispatches `pax8-daily` to `getPax8SyncService().fullSync('scheduled')`, gated by two independent checks: `isPax8Configured()` (env vars) and `integration_settings.key='pax8'` disabled flag (DB toggle), each with a distinct skip log line. +- `CLAUDE.md`'s "Operator config" section now documents PAX8 as the first integration where the DB toggle gates an action (scheduler skip + 403 on the manual route), not just health-check display. + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Seed the pax8-daily schedule via migration 096** - `230296c` (feat) +2. **Task 2: Add dual-guarded pax8-daily dispatch branch to executeScheduledSync** - `d07c0b7` (feat) +3. **Task 3: Record the DB-toggle-gates-action precedent in CLAUDE.md** - `d5456bb` (docs) + +_No plan metadata commit yet — orchestrator handles that after wave completion (worktree mode)._ + +## Files Created/Modified +- `migrations/096_pax8_daily_schedule.sql` - Idempotent seed of the pax8-daily sync_schedules row +- `lib/services/sync-scheduler.ts` - Extended `sync_type` union with `'pax8-daily'`; added dual-guarded dispatch branch after the appgate branch in `executeScheduledSync` +- `CLAUDE.md` - Added a note in Operator config / Integration disable documenting PAX8 as the first DB-toggle-gates-action precedent + +## Decisions Made +- Followed the exact 089-style `WHERE NOT EXISTS` seed pattern (not 090's `ON CONFLICT`), per explicit CONTEXT.md/PATTERNS.md direction. +- Kept the disabled-check PAX8-only and inline in the scheduler branch (D-01) rather than extracting a shared helper, since generalizing to all integrations was explicitly out of scope for this phase. +- Placed the new branch immediately after the appgate branch to keep integration-toggle blocks together in the switch, per PATTERNS.md guidance. + +## Deviations from Plan + +None - plan executed exactly as written. + +## Issues Encountered + +None. `npx tsc --noEmit --pretty` passed with no output after the sync-scheduler.ts edit. The migration was applied to the running `pulse-postgres` container using the default `pulse_user`/`pulse_autotask` credentials confirmed from `docker-compose.yml` (no `.env` overrides present). + +## User Setup Required + +None - no external service configuration required. The `pax8-daily` schedule ships `is_enabled=false`; an admin must opt in via the schedule editor separately, as documented in the plan. + +## Next Phase Readiness + +- The `pax8-daily` schedule row exists and the scheduler branch is wired and type-checked; ready for the manual-trigger route toggle (D-02, likely 13-02) and live cron/disable-skip verification (13-03). +- No changes were made to `app/api/pax8/sync/route.ts` or `lib/services/integration-health.ts` in this plan — those remain for subsequent plans in this phase per the plan's `files_modified` scope. + +--- +*Phase: 13-scheduler-admin-toggle* +*Completed: 2026-07-11* From 6490a36894067a3e40aad121bb31d773e050ae6b Mon Sep 17 00:00:00 2001 From: lorentz Date: Sat, 11 Jul 2026 09:48:03 -0400 Subject: [PATCH 5/5] docs(13-01): record self-check results in summary --- .../phases/13-scheduler-admin-toggle/13-01-SUMMARY.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.planning/phases/13-scheduler-admin-toggle/13-01-SUMMARY.md b/.planning/phases/13-scheduler-admin-toggle/13-01-SUMMARY.md index 1386ac9..6498007 100644 --- a/.planning/phases/13-scheduler-admin-toggle/13-01-SUMMARY.md +++ b/.planning/phases/13-scheduler-admin-toggle/13-01-SUMMARY.md @@ -92,6 +92,15 @@ None - no external service configuration required. The `pax8-daily` schedule shi - The `pax8-daily` schedule row exists and the scheduler branch is wired and type-checked; ready for the manual-trigger route toggle (D-02, likely 13-02) and live cron/disable-skip verification (13-03). - No changes were made to `app/api/pax8/sync/route.ts` or `lib/services/integration-health.ts` in this plan — those remain for subsequent plans in this phase per the plan's `files_modified` scope. +## Self-Check: PASSED + +- FOUND: migrations/096_pax8_daily_schedule.sql +- FOUND: .planning/phases/13-scheduler-admin-toggle/13-01-SUMMARY.md +- FOUND commit: 230296c (Task 1) +- FOUND commit: d07c0b7 (Task 2) +- FOUND commit: d5456bb (Task 3) +- FOUND commit: 73b33f7 (SUMMARY.md) + --- *Phase: 13-scheduler-admin-toggle* *Completed: 2026-07-11*