docs(13): capture phase context

This commit is contained in:
lorentz 2026-07-11 08:20:30 -04:00
parent 4b34109a65
commit e6802d549a
2 changed files with 288 additions and 0 deletions

View file

@ -0,0 +1,209 @@
# Phase 13: Scheduler & Admin Toggle - Context
**Gathered:** 2026-07-11
**Status:** Ready for planning
<domain>
## Phase Boundary
PAX8 sync runs automatically once a day like every other Pulse integration
(`pax8-daily` cron entry in `sync-scheduler.ts`), and can be turned on/off from
`/admin/integrations` without a container restart. Unlike every existing
integration's toggle today, disabling PAX8 must actually stop future sync
runs (not just suppress health-check display) — this phase introduces that
enforcement, scoped to PAX8 only. No `/pax8` UI, no manual company-match
resolution (Phase 14). `Pax8SyncService.fullSync()` already performs the
complete companies + subscriptions + products + orders + company-matching
sequence (built in Phases 11-12) — this phase only wires it into the
scheduler and the admin toggle, it does not change sync logic itself.
</domain>
<decisions>
## Implementation Decisions
### Toggle Enforcement Scope
- **D-01:** The disabled-check is a PAX8-only inline check inside
`executeScheduledSync`'s `pax8-daily` case — query `integration_settings`
for `key='pax8'` before calling `fullSync()`, skip + log if disabled.
Mirrors the `isAppgateConfigured()`/`isMsgraphConfigured()` guard style
already used for `appgate-daily`/`engagement-daily`, just checking the DB
toggle instead of env vars. Explicitly **not** a general mechanism applied
to all integrations — that would change behavior for ~10 existing
integrations in a phase whose requirements (PAX8-07, PAX8-09) only ask for
PAX8. Do not touch the shared `executeScheduledSync` switch's other cases.
- **D-02:** The manual trigger route (`POST /api/pax8/sync`) must also
refuse to run while PAX8 is disabled — return 403 with a clear message.
"Disabled" means fully off; no side door via the manual route while the
toggle says off.
- **D-03:** If PAX8 gets disabled while a sync is already in progress, let
it finish normally — no mid-flight cancellation. The disabled flag only
prevents the *next* scheduled tick or *next* manual trigger from
starting. `isSyncInProgress()`'s existing 409 guard already treats a
running sync as atomic; don't add cancellation logic to `fullSync()`'s
per-entity loop for this rare edge case.
### Daily Sync Timing
- **D-04:** `pax8-daily` cron fires at **4:00 AM** (`0 4 * * *`), grouping
with the earlier "backend reconciliation" cluster (`contract-services` at
4am, `tickets-reconcile` at 4:30am) rather than the 6-7:30am
"reporting/digest" cluster (`engagement-daily`, `zoom-daily`,
`ticket-digest-daily`, `integration-health`).
### Failure Visibility
- **D-05:** No dedicated alert/notification on sync failure. A failed
`pax8-daily` run sets `sync_schedules.last_status = 'failed'` /
`last_error` — visible on `/admin` only, same silent-failure pattern
already used by Veeam full sync, contract-services, and most other
integrations. Do not add a new Teams webhook alert path for this phase —
that's a bigger cross-cutting concern than PAX8-07/PAX8-09 ask for.
### Claude's Discretion
- **Migration for seeding the `sync_schedules` row** — follow the exact
precedent in `migrations/089_appgate_tables.sql`: seed via
`INSERT INTO sync_schedules (...) SELECT ... WHERE NOT EXISTS (SELECT 1
FROM sync_schedules WHERE name = '...')` inside the phase's own numbered
migration (next number after `095`). `is_enabled` defaults to `false`
every existing precedent except one legacy row (`contract-services`)
ships new schedules disabled by default; an admin must opt in via the
schedule editor (`/api/sync/schedules`) separately from the
`integration_settings` toggle. Not discussed explicitly this session
since it's a clear, unambiguous precedent match — flagging here so the
planner doesn't re-derive it from scratch.
- **`/admin/integrations` row wiring** — add PAX8 to the
`checkConfigOnly()` list in `checkIntegrationHealth()`
(`lib/services/integration-health.ts`), following the `qbo`/`appgate`
entries exactly (key, display name, category, required env vars). This
is what makes PAX8 appear as a toggleable row per SC#2 — mechanical, not
discussed as it has no meaningful alternative given the established
pattern.
- **Env var names for the `checkConfigOnly()` call** — confirm the exact
`PAX8_*` env var names against `lib/services/pax8-factory.ts`'s
`isPax8Configured()` implementation (established in Phase 10) rather than
guessing from convention.
</decisions>
<canonical_refs>
## Canonical References
**Downstream agents MUST read these before planning or implementing.**
### Project scope & requirements
- `.planning/PROJECT.md` — Current Milestone: v2.0 PAX8 Integration section
- `.planning/REQUIREMENTS.md` — PAX8-07, PAX8-09 (this phase's requirement
IDs); traceability table confirms both map to Phase 13
- `.planning/ROADMAP.md` — Phase 13 section (goal, 4 success criteria,
depends on Phase 12)
### Prior phase foundation this phase builds on
- `lib/services/pax8-sync-service.ts``Pax8SyncService.fullSync()`
(line ~74) already runs companies → subscriptions → products → orders →
company-matching in one call (Phases 11-12 built this); this phase's
scheduler entry calls `getPax8SyncService().fullSync('scheduled')`
directly, no new orchestration needed
- `app/api/pax8/sync/route.ts` — existing fire-and-forget manual trigger
(`POST`) and status (`GET`); D-02 requires adding a disabled-check to the
`POST` handler
- `lib/services/pax8-factory.ts``isPax8Configured()` from Phase 10; the
scheduler case should check this too (like `appgate-daily` does with
`isAppgateConfigured()`), in addition to the new `integration_settings`
disabled check from D-01
- `migrations/091_pax8_tables.sql` through `095_...` — existing PAX8
schema; next migration number for this phase's `sync_schedules` seed is
096
### Existing patterns to follow
- `lib/services/sync-scheduler.ts``executeScheduledSync` (line ~385):
the `appgate-sessions`/`appgate-daily` and `engagement-daily` branches are
the direct template for the new `pax8-daily` branch, including the
`isXConfigured()` skip-with-log style; `defaultSchedules` array (line
~180) only seeds on a virgin table (`createDefaultSchedules()` — "already
exist, skipping defaults") so the actual seed for an existing DB must go
through the migration, not this array
- `migrations/089_appgate_tables.sql` (lines 137-150) — exact precedent for
seeding a new `sync_schedules` row via `INSERT ... WHERE NOT EXISTS`
inside a feature migration, `is_enabled: false`
- `lib/services/integration-health.ts``checkConfigOnly()` (line ~239)
and the `checkConfigOnly('qbo', ...)`/`checkConfigOnly('appgate', ...)`
call sites (line ~344-347) in `checkIntegrationHealth()` — template for
adding the PAX8 row; `getDbDisabledKeys()` (line ~295) and
`applyDisableOverlay()` (line ~310) show the exact `integration_settings`
query shape D-01's scheduler check should reuse
- `app/api/admin/integrations/route.ts` — existing `integration_settings`
CRUD (list, toggle with `disabled_by`/`disabled_at`/`disabled_reason`);
no changes needed here, PAX8 just needs a row like any other integration
- `CLAUDE.md` "Operator config" section — documents the current two-source
disable model (env var + DB) and that "live auth checks still run" for
disabled integrations; note D-01 makes PAX8 the **first** integration
where the DB toggle actually gates something beyond display — call this
out explicitly if updating this doc during/after the phase, since it's a
behavior precedent, not just a new integration following an old one
</canonical_refs>
<code_context>
## Existing Code Insights
### Reusable Assets
- `getPax8SyncService().fullSync('scheduled')` — the entire scheduled-run
entry point; no new sync orchestration code needed in this phase
- `getDbDisabledKeys()` / `applyDisableOverlay()` pattern in
`integration-health.ts` — the exact SQL shape (`SELECT key FROM
integration_settings WHERE disabled = true`) to reuse for D-01's
scheduler-side check (querying for `key = 'pax8'` specifically, or
reusing the existing helper if it's exported/exportable)
### Established Patterns
- New integration schedules always ship `is_enabled: false`, seeded via
`INSERT ... WHERE NOT EXISTS` in that feature's own migration (see
`089_appgate_tables.sql`) — never rely on the in-code `defaultSchedules`
array to reach an already-provisioned Postgres volume
- `is<Name>Configured()` env-var guard + `integration_settings.disabled`
DB guard are two independent checks; both should gate `pax8-daily`
(config missing → skip + log distinctly from disabled → skip + log)
- `/admin/integrations` rows come from `checkIntegrationHealth()`'s
`checkConfigOnly()` calls, not a separate registry — adding PAX8 there
is the entire SC#2 requirement
### Integration Points
- `lib/services/sync-scheduler.ts` — new `pax8-daily` branch in
`executeScheduledSync`
- `app/api/pax8/sync/route.ts``POST` handler gains a disabled check
- `lib/services/integration-health.ts` — new `checkConfigOnly('pax8', ...)`
call site in `checkIntegrationHealth()`
- New migration `096_pax8_daily_schedule.sql` (or similar name) — seeds the
`sync_schedules` row per D-04's cron expression
</code_context>
<specifics>
## Specific Ideas
No UI mockups or specific behavioral scripts — this is scheduler + toggle
wiring, no new page. The five numbered decisions (D-01 through D-05) plus
the three Claude's Discretion items are the concrete specifics: PAX8-scoped
(not general) toggle enforcement covering both the cron and the manual
route, let-it-finish semantics on mid-run disable, 4am cron slot, and
silent (dashboard-only) failure visibility matching existing precedent.
</specifics>
<deferred>
## Deferred Ideas
None — discussion stayed within Phase 13's scope. (The `/pax8` UI and
manual company-match resolution are already sequenced into Phase 14 per
ROADMAP.md/REQUIREMENTS.md, not deferred from this discussion. Generalizing
the toggle-gates-scheduler mechanism to other integrations, discussed as
an explicit alternative under Toggle Enforcement Scope, was deliberately
rejected as out of scope for this phase — not deferred to a specific future
phase, just noted as a possible future direction if another integration
needs the same guarantee.)
</deferred>
---
*Phase: 13-scheduler-admin-toggle*
*Context gathered: 2026-07-11*

View file

@ -0,0 +1,79 @@
# Phase 13: Scheduler & Admin Toggle - Discussion Log
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
**Date:** 2026-07-11
**Phase:** 13-scheduler-admin-toggle
**Areas discussed:** Toggle enforcement scope, Daily sync timing, Failure visibility
---
## Toggle Enforcement Scope
| Option | Description | Selected |
|--------|-------------|----------|
| PAX8-only inline check | Check inside executeScheduledSync's pax8-daily case, mirrors isAppgateConfigured()/isMsgraphConfigured() guard style | ✓ |
| General scheduler mechanism | Shared isIntegrationEnabled(key) helper applied to every sync_type | |
**User's choice:** PAX8-only inline check
**Notes:** Rejecting the general mechanism explicitly to avoid changing behavior for ~10 existing integrations in a phase scoped only to PAX8-07/PAX8-09.
| Option | Description | Selected |
|--------|-------------|----------|
| Yes, block it too | POST /api/pax8/sync returns 403 when PAX8 is disabled | ✓ |
| No, manual trigger still works | Only the scheduled cron respects the toggle | |
**User's choice:** Yes, block it too
**Notes:** "Disabled" should mean fully off, no side door via the manual route.
| Option | Description | Selected |
|--------|-------------|----------|
| Let it finish | In-progress fullSync() completes normally; disabled flag only blocks the next tick/trigger | ✓ |
| Abort immediately | Requires threading cancellation into fullSync()'s per-entity loop | |
**User's choice:** Let it finish
**Notes:** Rare edge case, not worth the added complexity.
---
## Daily Sync Timing
| Option | Description | Selected |
|--------|-------------|----------|
| 6:00 AM | Matches engagement-daily/zoom-daily | |
| 4:00 AM | Groups with contract-services (4am) and tickets-reconcile (4:30am) | ✓ |
| You decide | No strong preference | |
**User's choice:** 4:00 AM
**Notes:** None beyond the selection.
---
## Failure Visibility
| Option | Description | Selected |
|--------|-------------|----------|
| Silent, admin-dashboard only | Matches Veeam full sync / contract-services precedent — last_status/last_error only | ✓ |
| Post a Teams alert on failure | Follows integration-health/ticket-digest precedent of pushing an Adaptive Card | |
**User's choice:** Silent, admin-dashboard only
**Notes:** Keeps phase scoped to scheduling + toggle, not a new alerting feature.
---
## Claude's Discretion
- Migration mechanics for seeding the `sync_schedules` row — follow
`migrations/089_appgate_tables.sql`'s `INSERT ... WHERE NOT EXISTS`
precedent exactly, `is_enabled: false` by default. Not asked as a
question — unambiguous precedent match.
- `/admin/integrations` row wiring — add PAX8 to `checkConfigOnly()` in
`integration-health.ts` following the `qbo`/`appgate` call sites.
Mechanical, no real alternative.
- Exact `PAX8_*` env var names for that call — confirm against
`pax8-factory.ts`'s `isPax8Configured()` rather than guessing.
## Deferred Ideas
None — discussion stayed within Phase 13's scope.