---
phase: 13-scheduler-admin-toggle
plan: 03
type: execute
status: partial
---

## What this plan verified

Live verification of Phase 13's four success criteria against the running app. No code
changes — this plan proves behavior, it doesn't implement it.

## Pre-existing blocker found and fixed (out of this plan's scope, but required to test anything)

The running `pulse-app` Docker image was built **2026-05-21** — before any PAX8 code
existed. `docker restart` / `docker compose up -d app` only restart a container from the
existing image; neither re-runs `npm run build`. As a result, every live test against the
stale image exercised code that predated the `pax8-daily` branch entirely, and an
unrecognized `sync_type` fell through to the scheduler's generic `else` branch
(`this.syncService.fullSync('scheduled', ...)`), which re-synced the *entire* Autotask
entity set (companies, contacts, resources, tickets, ticket_notes — 767,500+ ticket notes
fetched before it was stopped). This was **not** a bug in Phase 13's code; the new code was
never running.

Fix: rebuilt the image (`docker compose build app`) and redeployed
(`docker compose up -d app`). The rebuild was blocked by a pre-existing, unrelated,
uncommitted script (`scripts/diagnose-ticket-varchar-overflow.ts`) with a TS7022 circular-
type-inference error on an untyped `fetch()` result — Next.js's build runs a full
repo-wide typecheck. Added an explicit `: Response` annotation (2-line fix, no logic
change) to unblock the build. This fix is **not committed** — the script itself is
untracked/WIP and out of Phase 13's scope; only the type annotation was touched to get a
green build.

After rebuilding, the scheduler dispatch and disable/enable logic were re-tested against
the actual current code and behaved as designed (see below).

## Automated verification (via direct DB/log access — no browser session available)

**SC#1 — pax8-daily is scheduled and fires: CONFIRMED**
- Enabled the schedule with a near-future cron; it fired, logged
  `[Pax8Sync] Starting full sync (...) — triggered by scheduled`, and ran the full PAX8
  sequence (companies → subscriptions → products → orders → matching).
- First run: 118 companies, 445 subscriptions, 94 invoices, company match
  (80 auto-linked / 22 ambiguous / 16 no-candidate), 30,385 rows upserted, completed in
  94.4s, `sync_history` status `completed`.

**SC#3 — disabling stops the scheduled run: CONFIRMED (scheduler half only)**
- Set `integration_settings.pax8.disabled = true`, forced another near-future tick.
- Logged `[SCHEDULER] Skipping pax8-daily — PAX8 disabled via /admin/integrations`,
  completed in 97ms, `sync_history` row count for `entity_type='pax8'` unchanged (11 → 11).
- **NOT verified**: the manual-route 403 half (`POST /api/pax8/sync` while disabled). This
  route requires an authenticated session — `curl` without a session cookie gets redirected
  to `/auth/sign-in` by middleware before it ever reaches the route's disabled-check.
  Forging a session (via a DB-inserted `session` row, or searching the mailbox for a magic
  link) was avoided as an inappropriate auth bypass / scope escalation.

**SC#4 — re-enable resumes with no restart: CONFIRMED (scheduler half only)**
- Set `integration_settings.pax8.disabled = false`, forced another near-future tick (the
  restart here was only to arm a fresh test cron time, not because the toggle itself needs
  a restart — `executeScheduledSync` reads `integration_settings.disabled` fresh from the DB
  on every tick).
- The next tick ran a full sync (not a skip) — confirmed resumption. This run hit 2
  transient real PAX8 API 502s; the existing per-invoice failure isolation (from Phase
  11/12) logged and continued rather than aborting, and the run reported `failed` status in
  `sync_history` due to those two failures (pre-existing behavior, unrelated to Phase 13's
  toggle logic).
- **NOT verified**: the manual-route 200 half (same auth blocker as above).

**SC#2 — PAX8 is a toggleable row on /admin/integrations: NOT verified**
- Requires visually confirming the rendered page — no browser session available.

## Cleanup performed

- `pax8-daily` restored to its safe shipped default: `cron_expression='0 4 * * *'`,
  `is_enabled=false`, `last_run`/`last_status` cleared.
- `integration_settings.pax8.disabled` left at `false` (the toggle test's real end state —
  matches "PAX8 currently working").
- `docker-compose.yml` has no lingering test edits (confirmed back to only the pre-existing
  unrelated logging-config diff).
- `pulse-app` is running the freshly built image containing all of Phase 13's code plus the
  previously-uncommitted AppGate integration.

## What's left for the human

Three checks need a real authenticated browser session (deliberately not bypassed):
1. Open `/admin/integrations`, confirm a "PAX8" row appears (SC#2).
2. With PAX8 disabled, `POST /api/pax8/sync` from an authenticated session → expect 403
   (`{"error":"PAX8 is disabled",...}`) (SC#3 manual-route half).
3. Re-enable, repeat → expect success (SC#4 manual-route half).

## Self-Check: PARTIAL

Automated/DB-level verification passed for all scheduler-side behavior (SC#1, SC#3
scheduler half, SC#4 scheduler half). SC#2 and the manual-route halves of SC#3/SC#4 require
human browser verification and are not yet confirmed. Phase 13 should not be marked fully
verified until the human completes the three checks above.
