- Import classifyValarms from vevent.ts
- Derive reminderLeadMinutesValue: preset/offlist → leadMinutes; custom/none → null
- Add reminderLeadMinutes to .values() and .onDuplicateKeyUpdate({ set: {} })
- Scheduler now has ground truth for native-client VALARMs (T-11-07 mitigated)
- CAL-14 preserve: UPDATE with no reminderLeadMinutes preserves VALARM from rawVevent
- CAL-13 timed: CREATE with reminderLeadMinutes=15 emits TRIGGER:-PT15M
- CAL-13 clear: UPDATE with reminderLeadMinutes=null emits no VALARM (passes trivially)
- CAL-13 all-day: CREATE with allDay=true and reminderLeadMinutes=1440 emits VALUE=DATE-TIME
CREATE DATABASE IF NOT EXISTS reuses the prior run's data, so the test DB
would itself accumulate users run-over-run (the same flaky list_shares
fan-out the dev DB suffered). Truncate every table (except the drizzle
migration ledger) at globalSetup start, FK-safe, so each run is a clean
deterministic slate. Verified: familysync_test users 186 -> 93 across a run
(reset, not doubled); dev familysync untouched at 3.
- Update apps/api/test/setup.ts header: clarify tests run against familysync_test
(provisioned by global-setup.ts), document users-cleanup decision (intact across
tests), and note CI-vs-local env difference
- Add apps/api/README.md "Running API tests locally" section: documents the test
DB isolation, run command, DB_ROOT_PASSWORD requirement, and CI no-op behaviour
- Fix apps/api/test/global-setup.ts: switch from drizzle({ client, mode }) to
drizzle(pool, { mode }) — drizzle-orm@0.45.2 isConfig() has a tautological OR
in the `mode` branch that always returns false, causing the combined-config form
to pass the config object as the client (client.query is not a function); two-arg
form routes correctly; 244/244 tests pass against familysync_test
PUT /api/admin/calendars/:id/shared cleared the current shared calendar then
set the target in two non-transactional UPDATEs without checking the target
exists — a bad/stale id wiped the family shared lane and still returned ok.
Verify the target inside a transaction; return 404 when absent. Adds a
regression test (RED→GREEN).
RED phase: all admin.test.ts tests fail (404 — routes/mounts not yet created).
Tests cover:
- T-10-08 Pitfall 9: 403 for non-admin on every /api/admin/* route
- T-10-09 Pitfall 7: 400 with no echoed password for all credential failure modes
(PROPFIND/auth failure, createFastmailClient throw, network error, schema mismatch)
- T-10-11: valid credential stores encrypted (AES-256-GCM), not plaintext
- ADMIN-02: PUT /api/admin/calendars/:id/shared — exclusive is_shared=1
- T-10-12 Pitfall 6: POST /api/me/credential ignores body userId, writes to session user
- D-07: non-admin member can POST /api/me/credential (no requireAdmin on self-service)
- dev-bypass path: DB lookup for users.isAdmin (T-10-05 bypass skips OIDC not DB)
- OIDC path: same resolveAdminAndSetupStatus helper after upsertUser
- needsProviderSetup: true when no member_credentials row, false when one exists
- no /api/me/credential POST added here (Plan 03)
- dev-bypass path: isAdmin from DB (not hardcoded), needsProviderSetup from member_credentials
- needsProviderSetup=true when no member_credentials row exists
- needsProviderSetup=false when member_credentials row exists
- zero-admin COUNT check before INSERT: first user gets is_admin=true
- subsequent users (admin already exists) get is_admin=false
- existing-user early-return path unchanged (is_admin not modified)
- Phase-12 hook comment: tighten to first login after app_config.setup_complete
- adds 'import { sql }' from drizzle-orm
- first user with zero admins → is_admin=true in INSERT values
- subsequent user with admin present → is_admin=false in INSERT values
- existing user re-upsert → is_admin unchanged (early-return path, no insert)
- update existing color tests to accommodate new 4-select flow order
- reads users.isAdmin from DB (never trusts context user's isAdmin claim)
- 403 with { error: 'Forbidden' } for non-admins and missing user
- side-effect import of devBypass.js for ContextVariableMap augmentation
- bypass path skips OIDC only, not the DB check (T-10-04/T-10-05)
- 403 for non-admin user (is_admin=false in DB)
- next() called for admin user (is_admin=true in DB)
- 403 when no user on context (no DB query)
- 403 when context user spoofs isAdmin=true but DB has is_admin=false (T-10-04)
- Generated via drizzle-kit generate from updated schema.ts
- SQL is additive-only: CREATE TABLE app_config + ALTER TABLE ADD COLUMN (3x) + ADD CONSTRAINT UNIQUE
- Applied to live dev MariaDB via direct SQL execution (drizzle-kit migrate journal hash mismatch
with legacy migration tracking; DDL applied + hash recorded in __drizzle_migrations manually)
- Verified: MIGRATION OK via live DB SHOW COLUMNS / SHOW TABLES query
- No DROP/TRUNCATE statements in generated SQL (grep returns 0)
- Disable detect-object-injection globally in eslint.config.js: all hits were
numeric loop array indices (ranks[i]) — not user-controlled keys; zod guards
real API input boundaries; justification comment added (T-16-09)
- Add inline eslint-disable for detect-non-literal-fs-filename at 2 sites:
- apps/api/src/index.ts: realpathSync(process.argv[1]) — runtime entry path, not user input
- apps/api/tests/broker/expand.test.ts: readFileSync of test fixture path — test-controlled
- pnpm lint exits 0 across both apps with --max-warnings 0
- 14 of 15 security rules remain active at error; no blanket file disables
- Add ENV NODE_ENV=production in production stage after WORKDIR /app/apps/api
- Engages the devBypass.ts hard guard so DEV_AUTH_BYPASS can never inject
DEV_USER in the shipped image, even if accidentally set (D-07)
- No other stage (base/builder/dev/pwa-builder) is affected
- Create apps/api/src/lib/bootGuards.ts with assertNotDevBypassInProduction()
- Guard exits non-zero when NODE_ENV=production AND DEV_AUTH_BYPASS=true (D-08)
- Wire import + call as first statement in isMainModule() block in index.ts
- 3/3 unit tests pass, typecheck green
- Three test cases: prod+bypass=exit(1), dev+bypass=no-exit, prod+unset=no-exit
- Fails with Cannot find module (src/lib/bootGuards.ts absent) — RED confirmed
- extend outboxWorker import to include initOutboxTrigger alongside startOutboxWorker
- call initOutboxTrigger() immediately after startOutboxWorker() inside isMainModule() guard
- gated by isMainModule() so tests importing app never register the drain listener (Pitfall 4)
- import signalOutboxDrain from '../lib/outboxTrigger.js'
- Site 1 (POST /create): fire-and-forget signal after outbox insert, before 202
- Site 2 (PATCH edit-as-move): signal after await db.transaction() resolves, not inside callback (D-03)
- Site 3 (PATCH same-calendar update): signal after outbox insert, before 202
- Site 4 (DELETE): signal after outbox insert, before 202
- Import scheduleOutboxDrain (not yet exported — causes RED)
- Import signalOutboxDrain from outboxTrigger.ts
- Add describe block 'scheduleOutboxDrain — trigger wiring (D-09)' with 3 tests:
Test A SC-1: signalOutboxDrain() fires drain promptly without timer advance
Test B D-05: two mid-drain signals collapse to exactly one trailing re-drain
Test C D-07: concurrent scheduleOutboxDrain() calls dispatch exactly once via isDraining guard
- 27 pre-existing tests unmodified and passing; 3 new tests failing (RED)
- Fix MD040 (11 bare fences): add language tags (text/bash) across 7 files
- Fix MD031 (2 violations): add blank lines around fence in GETTING-STARTED.md
- Wire 'Markdown lint' step to fast-checks job (after Format check, before Typecheck)
- Reformat .markdownlint-cli2.jsonc per Prettier (trailing commas in JSONC)
- pnpm md:lint exits 0; pnpm format:check exits 0; gate can fail on bare fence (verified)
- eslint.config.js: disable React Compiler rules (v7 flat.recommended enables
them; codebase does not use the Compiler); add e2e/ to disableTypeChecked
block; promote exhaustive-deps to error
- API broker: remove redundant as-casts (outboxWorker, poller, reminderScheduler,
expand, sync, vevent, spike); add targeted ical.js no-unsafe-assignment/argument
disables with justifying comments inside try blocks
- API routes/sse.ts: fix no-misused-promises on async writeSSE callback with
void+IIFE+catch pattern
- API routes/lists.ts: let → const for updateValues
- API tests: remove unused imports (beforeEach, eq, vi); rename unused vars
with _ prefix; remove unused lastActiveId assignment
- PWA components: void navigate() and void queryClient.invalidateQueries() on
all fire-and-forget call sites; fix CalendarShell explicit-type-casts;
Couldn't → HTML entity
- PWA test files: as unknown as Response for partial mock objects; string | null
type annotation on mockLastSyncedUid; remove async from test callbacks without
await; act(() => {}) not await act(async () => {}) for sync ops
- sw.ts: restructure Notification.data?.url access as let+if so disable
comments land on the exact violation lines; void self.skipWaiting()
0000_easy_slipstream already created lists/list_shares/list_items and the
calendars unique constraint, but 0001_lists_schema re-created those tables and
0001_calendars_user_url_unique was an orphan (not in _journal) — so a cold
`drizzle-kit migrate` against an empty DB failed with ERROR 1050 'Table lists
already exists'. Dev only survived because its DB was built incrementally; CI
is the first cold migrate and exposed it.
Regenerated a single 0000_baseline.sql from schema.ts. Verified on a fresh
mariadb:11: migrate succeeds, schema is structurally identical to the running
dev DB, `drizzle-kit generate` reports no drift, and all 238 API tests pass.
Local dev DBs must be rebuilt (drop + db:migrate); no prod exists.