Implements the full E2E flow against the deployed docker stack and
documents it as a repeatable test playbook.
Surfaced and fixed three real issues while building the E2E:
1. JWT secret mismatch — docker-compose.override.yml backend was using
a NoteLett-only JWT_SECRET that platform-service did not share, so
every Authorization: Bearer call returned 'Invalid or expired token'.
Aligned the override to use platform-service's actual secret
(dev-ecosystem-secret-do-not-use-in-production).
2. CORS preflight missing PATCH/DELETE — @bytelyst/fastify-core registers
@fastify/cors with only { origin }, which leaves Access-Control-Allow-
Methods at the @fastify/cors default of 'GET,HEAD,POST'. Real browser
PATCH/DELETE preflights would fail. Added an onSend hook in
backend/src/server.ts that rewrites the header to
'GET,HEAD,POST,PATCH,PUT,DELETE,OPTIONS' on CORS preflight responses.
3. Product 'notelett' wasn't registered with platform-service — auth
register/login both error with 'Unknown or disabled product: notelett'.
The seed script now POSTs to /api/products idempotently.
Deliverables:
- scripts/e2e-docker-seed.sh — idempotent: registers the notelett product
and creates two test users (admin@notelett.app with role=admin who can
write, user@notelett.app with role=user who is read-only). Re-runs are
no-ops once seeded.
- scripts/e2e-docker-test.sh — 9-step E2E that drives the deployed stack
via HTTP only (no browser): login → CORS preflight for PATCH →
workspace create → note create → note read → note PATCH (status:
draft→active) → note list → note delete → workspace delete.
- docs/testing/E2E_DOCKER_TESTING.md — full playbook covering prereqs,
seed, automated E2E, manual UI smoke, stack architecture diagram,
troubleshooting (JWT mismatch, unknown product, role rejection,
CORS, port conflict, data loss), tear-down, CI wiring guidance.
- package.json — pnpm e2e:docker:seed and pnpm e2e:docker:test
shortcuts.
Verified live on this host's deployed stack:
$ bash scripts/e2e-docker-seed.sh
↷ product 'notelett' already exists
↷ admin user already registered + login works
✓ user created
🟢 Seed complete.
$ bash scripts/e2e-docker-test.sh
✓ user=usr_e094e0c2-... role=admin
✓ CORS allows PATCH
✓ workspace created
✓ note created
✓ note read matches
✓ note patched (status: draft → active)
✓ note list returned (1 item)
✓ note deleted (HTTP 204)
✓ workspace deleted (HTTP 204)
🟢 All 9 E2E steps passed.
Backend regression suite still green: 380/380.
Previously P10.5 was marked complete with a deferral note because the
sibling services (platform-service 4003, extraction-service 4005,
mcp-server 4007) were not running on the audit host. Today they are
all running, so I executed the smoke and confirmed it passes.
Command:
JWT_SECRET="dev-secret-change-me-at-least-32-characters-long" \
bash scripts/local-smoke.sh
Output (exit 0, 11 ok lines):
info: starting NoteLett backend in memory mode
ok: NoteLett backend started at http://localhost:4016
ok: NoteLett health
ok: NoteLett bootstrap
ok: platform-service health
ok: extraction-service health
ok: mcp-server health
ok: authenticated workspace create
ok: authenticated note create
ok: authenticated note read
ok: smoke cleanup attempted
ok: local production-readiness smoke passed
Updates:
- §Post-Sprint-A Re-verification: replaces the blanket deferral note
with the actual verification details for live shared-service smoke
and a separate, narrower deferral note for Docker compose smoke
(which still fails on corp-network hosts due to TLS interception in
the backend/Dockerfile npm install step but succeeds on CI).
- §P10.5: replaces the historical deferral text with today's
end-to-end verification result.
UI8 deferred deleting the legacy global classes (.surface-card,
.surface-muted, .input-shell, .badge) because 69+ call sites in UI6/UI7
territory (dashboard, search, workspaces, notes detail, chat, palace)
still depend on them. Removing the globals before those screens migrate
would visually break the app.
Instead, ship a one-way ratchet that solves the actually-important
problem: prevent NEW legacy usage from creeping in while existing
sites get migrated.
- scripts/ui-drift-ratchet.sh — reads scripts/ui-drift-baseline.json
and FAILS if any of the four UI drift categories regress above the
tracked baseline. Pure bash, no jq required, works with grep or
ripgrep. Uses the same patterns as scripts/ui-drift-audit.sh.
- scripts/ui-drift-baseline.json — checked-in baseline captured today:
raw controls 38, legacy classes 92, hardcoded colors 0, direct imports 0.
- package.json — adds pnpm run audit:ui:ratchet and
audit:ui:ratchet:update scripts.
- .github/workflows/ci.yml release-guards job — runs the ratchet as a
required step plus the existing audit in report mode.
- docs/UI_UX_PLATFORM_CORE_ROADMAP.md — marks the CI-guard checklist
item complete, documents the path to fully strict mode (drive
baseline to zero, then delete globals.css legacy classes, then flip
audit:ui:strict from advisory to required).
Verified:
- Ratchet at baseline: exits 0
- Synthetic regression (added a file with surface-card + raw <input>):
ratchet correctly exits 1, reporting +1 in each affected category
- pnpm run verify: backend 380/380, web 96/96, mobile 97/97 (no
behavior change)
Sprint B — closes audit item B7 (doc consolidation).
- docs/AGENT_TASK_ROADMAP.md, docs/ARCHITECTURE_REVIEW_AND_REUSE_ROADMAP.md,
docs/GAP_ANALYSIS.md were each self-marked as historical snapshots
but kept polluting the top of docs/. Moved them under docs/archive/
in the previous commit; this commit:
- Adds docs/archive/README.md explaining what's archived vs active
- Repoints cross-doc links in docs/IMPLEMENTATION_TRACKER.md,
docs/WEB_AI_FAST_ROADMAP.md, and docs/roadmaps/*.md to the new
archive paths
- Fixes relative links inside the archived files themselves so
historical readers can still navigate back to active docs
- AGENTS.md §1.1 refreshed: reflects the May 22 re-verified state
(382/96/97 tests), links the two new runbooks, and points readers
away from docs/archive/ as a work source.
Sprint B — closes audit items B4 and B5.
- docs/runbooks/MEK_ROTATION.md: step-by-step procedure for rotating
the field-encrypt master key in Azure Key Vault, including pre-flight
checks, rewrapAllDeks usage, verification queries, rollback, and lost-MEK
recovery. Replaces the previous gap where MEK rotation had no
documented operator path.
- docs/runbooks/SECRET_MANAGEMENT.md: inventory of every secret consumed
by NoteLett with its production source (AKV), two production-grade
patterns (workload identity vs K8s CSI), the compose-host pattern,
rotation flow per secret type, verification commands, and red-flag
triage.
Both docs cross-link each other and call out concrete open items
(automation, dual-JWT support, audit-log emission) for later sprints
rather than overstating current capabilities.
Sprint B — closes audit items B6 (event-bus completeness) and B3
(public-share revocation regression).
Event bus:
- note-tasks/repository.ts createNoteTask now emits task.created with
taskId, noteId, workspaceId, userId, title
- workspaces/repository.ts createWorkspace now emits workspace.created
with workspaceId, userId, name
The event-bus already declared these event types (event-bus.ts) and
webhook subscribers can target them, but they were never emitted —
making the contract dead. Emissions follow the same .catch(() => {})
pattern used by note.created/updated/deleted in notes/repository.ts so
a subscriber failure cannot break the create flow.
Regression tests:
- note-tasks/repository.test.ts and workspaces/repository.test.ts
exercise the emission paths end-to-end through the in-memory
datastore.
- note-shares/repository.integration.test.ts adds a 5-test integration
suite for the public-share revocation path: token resolves before
revocation; token returns null after deleteShare (hard delete);
expired token returns null; cross-product token rejected;
listSharesForNote does not include revoked shares.
Verified:
- pnpm --filter @notelett/backend run test: 380/380 (was 373, +7 new)
- pnpm run verify end-to-end green
- Commit previously untracked docs/NEXT_SPRINT_ROADMAP.md with refreshed
May 22 status; mark Sprint 1 (backend build) and Sprint 2 (lint) as
resolved by Sprint A workspace-path fix
- Add post-Sprint-A re-verification section to
docs/PRODUCTION_READINESS_HANDOFF_ROADMAP.md documenting the
workspace-path regression and the re-verified gates
- Update README quick-start to reference the canonical common-platform
checkout path with BYTELYST_COMMON_PLAT_ROOT override note