docs(15): capture phase context

This commit is contained in:
Jakub Zych
2026-10-04 14:30:25 +02:00
parent 0323aeb6fb
commit 67e4406250
2 changed files with 278 additions and 0 deletions

View File

@@ -0,0 +1,155 @@
# Phase 15: Cutover - Context
**Gathered:** 2026-10-04
**Status:** Ready for planning
<domain>
## Phase Boundary
The Go backend (`fonoteka.go` on SummerCMS) replaces the PHP WinterCMS backend in production at `https://plytarium.com`. `vue-fonoteka-app` and `fonoteka-mcp` keep running unchanged. The phase covers four pieces of work:
- **Data move:** production data moves from **MariaDB** (production runs `DB_CONNECTION=mysql` with `utf8mb4_polish_ci`, per `fonoteka/docs/deploy/plytarium.com.md`) into the Go Postgres schema. The Go stack is Postgres-only and MySQL support stays out of scope (PROJECT.md), so the cutover is the one place a MySQL reader exists.
- **Final parity gate:** every manifest route is green with zero `pending` entries.
- **Production switch:** a freeze-import-swap on rome with a rollback path, the Go runtime under supervisor, and a Postgres backup timer.
- **Daily-use acceptance:** a 7-day soak in production with a scripted Nuxt and MCP session.
Repos: `summercms.go` (the generic import runner, framework mappers, docs) and `fonoteka.go` (app and plugin mappers, CUTOVER.md runbook, scripts, supervisor/nginx/backup configs). The shared plugin repos (`sm-user-plugin`, `sm-golem-plugin`, `sm-feedback-plugin`) receive their own import mappers.
Not in scope: anything on the PHP side after the swap. The user handles the PHP sign-off, stopping its cron and queue worker, and the PHP/MariaDB decommission outside the plans.
</domain>
<decisions>
## Implementation Decisions
### Data move (MariaDB → Postgres)
- **D-01:** The move is a Go import command that reads MariaDB over a DSN and writes into the schema the Go migrations created, table by table. It coerces types along the way: tinyint to bool, JSON text to jsonb, datetime and timezone handling, and a sequence reset at the end. pgloader and SQL-dump rewriting were rejected. A MySQL driver is added as a dependency for this command only. The phase decision names it, as CLAUDE.md requires, and the researcher picks the driver (expected: `github.com/go-sql-driver/mysql`). — **Reversibility:** reversible — the dependency is confined to the import path.
- **D-02:** The import logic is split between the framework and the plugins. `summercms.go` ships a generic `summer import:winter` runner: the MySQL source, table copy with coercion, batching, sequence reset, and a Laravel-decrypt hook built on `lagoon/laravel_decrypt.go`. Each plugin implements an optional capability interface (working name `HasWinterImport`, alongside the `pact` capabilities) that declares its tables and any per-column transforms. Framework-owned tables (`backend_users`, `backend_user_roles`, `system_files`, settings) get framework mappers. The runner is reusable for the next WinterCMS port (the blog project). Framework READMEs and docs must not name the application. — **Reversibility:** costly — the capability interface becomes part of the plugin API that shared plugins implement.
- **D-03:** Encrypted columns are decrypted from Laravel's AES-256-CBC with the PHP `APP_KEY` and re-encrypted to the Go AES-256-GCM format during the import (Phase 5 D-10). The import is the only caller of `DecryptLaravelPayload`, which never enters the live Scan/Value path. Golem models arrive through the existing `golem:import-settings` and `feedback:import-settings` importers (Phase 14 D-18), which the runner calls or the runbook sequences.
- **D-04:** The following data is not carried over:
- queue and job state (Laravel `jobs`, `failed_jobs`, Redis queue contents);
- sessions, cache and event/request logs;
- the JWT blacklist (Phase 7 D-06: live JWTs keep working through the shared `JWT_SECRET`);
- the Typesense index, which is rebuilt with `fonoteka:reindex` after the import;
- notifications older than the prune window.
Everything else is copied, and primary keys are preserved so IDs in URLs, share links and fixtures stay valid. Pending CSV import sessions are drained before the freeze rather than migrated into River.
- **D-05:** The import is rehearsed before the real switch on a restored production `mariadb-dump` (the nightly dump from `backup-restore.md`). A verify step runs after each import:
- row counts per table;
- checksums of key columns;
- a decrypt round-trip on every encrypted value;
- a file-existence check for every `system_files` row against the copied uploads tree.
The import is repeated until it is clean. One timed dress rehearsal then sizes the downtime window. The import must be re-runnable: it truncates its target tables or refuses to start on a non-empty target.
### Production switch and rollback
- **D-06:** Switch: put the PHP site into the existing nginx `@maintenance` page, take a final dump, run the import and verify, then repoint nginx's backend locations at the Go binary. Those locations are the API prefixes, the OAuth endpoints, `/storage/app/uploads`, the realtime/feedback routes and the admin path. The researcher derives the exact list from the manifest and the current vhost. The origin stays `https://plytarium.com`. Nuxt SSR and its build stay untouched. A staging subdomain and nginx mirroring were rejected: the first would need a different Nuxt build, and mirroring only covers GET requests.
- **D-07:** Rollback: PHP and the MariaDB snapshot stay in place on rome, and rollback means swapping the nginx upstream back. There is no reverse sync from Go to MariaDB, so writes made on Go during the window are lost or re-entered by hand. That is acceptable for the household user base. The rollback window ends at the 7-day sign-off (D-13).
- **D-08:** The runtime on rome is supervisor with two programs: `fonoteka serve` and a separate River worker/scheduler program, each restartable on its own. This matches the 11.2 summercms.io deploy and the existing `albumy-queue` program. Postgres runs on rome next to MariaDB. The configs are committed in `fonoteka.go`.
- **D-09:** The user runs every production step, because rome is not reachable from the dev machine. The phase delivers a `CUTOVER.md` runbook in `fonoteka.go`. It covers the preflight, the freeze, the import, the verify, the swap, the smoke checks, the rollback, and the env mapping from the Winter `.env` names to the `SUMMER_*` names, with every secret left as a placeholder. Each step has its own script. The user runs them and pastes the output back as evidence, and verification records that evidence as UAT items.
- **D-10:** Backups: a nightly `pg_dump` plus the uploads tarball replaces the MariaDB timer. It follows `backup-restore.md`: files are 0600, the directory is 0700 and outside the docroot, and credentials live in a 0600 file. It is installed at the swap, so production is never without a backup, and one restore drill runs against a scratch database.
### "All routes green" gate
- **D-11:** The acceptance set is every route in `fonoteka.go/parity/manifest.yaml`: 175 entries today, covering the fonoteka, user, oauth, realtime and feedback routes. Every entry must be `ported`, with passing recorded cases, zero `pending` entries, and `routes.snapshot` matching PHP on path, method and auth group. API-09, QA-05 and the ROADMAP success criteria are reworded at plan time from "154 routes" to "every manifest route".
- **D-12:** Fixtures: the whole corpus is re-recorded from the current PHP backend (`summer parity:record`) shortly before the freeze, and the Go replay must be green on it. A drift report lists every fixture whose PHP response changed since it was committed, and each drift is resolved (a Go fix, or an accepted normalizer change) before the swap. In addition, a **read-only smoke diff runs on the rehearsal import**. A scripted set of authenticated GET requests (as real users, via minted JWTs and personal tokens) is replayed against PHP on the restored MariaDB and against Go on the Postgres imported from the same dump, and both are diffed with the tide normalizer. The smoke diff runs locally on the dump only.
- **D-13 (prerequisites):** Phase 14.1 (the 3 pending routes) and Phase 12.1 (the user admin screens) are hard prerequisites, and 12.1 is added to Phase 15's depends-on in ROADMAP. Phase 14's open UAT items must also be closed: the CR-01 decision record, and the WR-02/WR-05 dispositions, which `fonoteka.go` commits suggest are already fixed. Phase 15's first plan opens with a preflight gate that fails unless all of this holds and the manifest has zero pending routes. Planning may start before the prerequisites close.
### Daily-use acceptance
- **D-14:** The soak is 7 days of normal household use on Go with no rollback-worthy issue. During it, the wishlist digest and the notification prune schedules each run at least once, and at least one CSV import and one Discogs match complete. The end of the soak is the phase sign-off and closes the rollback window.
- **D-15:** The scripted manual session runs right after the swap and again before sign-off. It covers:
- **Nuxt core session:** log in with an existing account and confirm the JWT survives the switch; browse collections and albums; search; edit an album; rate; upload a cover; add and remove a wishlist entry; run a CSV import and export; switch locale.
- **MCP + OAuth:** run the `fonoteka-mcp` install/auth flow from scratch; reconnect an existing MCP client; make a representative set of tool calls (list/search/get/create/update album, wishlist, stats, cover fetch).
Sharing/invitations and admin/ops checks were offered and not selected for the scripted session. They remain covered by the parity gate.
- **D-16:** After sign-off, PHP is left running but unrouted (the user's choice, for a late rollback). Stopping the PHP cron and queue worker, the PHP sign-off and any later removal are handled by the user **outside the plans**, so no plan touches the PHP stack beyond the maintenance-page freeze and the nginx swap.
### Claude's Discretion
- How the runner batches rows, its ordering by foreign key and how the coercion table is laid out, the naming of the `HasWinterImport` interface and its method set, and the verify-report format.
- How the runbook is split into scripts, and the layout of the evidence capture.
- The exact supervisor program names and the log locations.
- Whether the read-only smoke diff reuses `tide` replay against two base URLs or adds a dedicated `parity:*` subcommand.
</decisions>
<canonical_refs>
## Canonical References
**Downstream agents MUST read these before planning or implementing.**
### Production environment (PHP, current)
- `/media/nvme/dev/golem15/fonoteka/docs/deploy/plytarium.com.md`: the production origin, same-origin SPA+API, the MariaDB/Redis/Typesense env, SSR rules, and the nginx include files.
- `/media/nvme/dev/golem15/fonoteka/docs/deploy/backup-restore.md`: the current `mariadb-dump` + uploads backup and the restore drill. It is the model for D-10 and the source of the rehearsal dump (D-05).
- `/media/nvme/dev/golem15/fonoteka/docs/deploy/systemd/`: the current backup units.
- `/media/nvme/dev/golem15/fonoteka/scripts/deploy-frontend.sh`: the Nuxt deploy and maintenance-page behaviour, which stays untouched.
### Prior decisions this phase builds on
- `.planning/PROJECT.md`: Postgres only, the out-of-scope MySQL support, the core value.
- `.planning/REQUIREMENTS.md`: API-09 and QA-05, which are reworded per D-11.
- `.planning/phases/05-data-layer-full-fidelity/05-RESEARCH.md`: D-10 (Laravel decrypt is import-only), D-14/D-17 (`system_files` shape and thumb reuse), and the cutover import deferred to Phase 15.
- `.planning/phases/07-user-plugin-and-authentication/07-CONTEXT.md`: D-06 (wire-compatible JWTs, blacklist not migrated) and D-15 (reset codes).
- `.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-CONTEXT.md`: D-01 (Winter-shaped `backend_users`, copied straight in at cutover) and WR-17 (user-level permissions).
- `.planning/phases/14-domain-jobs-and-external-integrations/14-CONTEXT.md`: D-18 (golem/feedback settings importers), D-20 (`GOLEM15_SSRF_ALLOWED_HOSTS`), and D-09 (the routes now in 14.1).
- `.planning/phases/14-domain-jobs-and-external-integrations/14-04-SUMMARY.md` and `14-05-SUMMARY.md`: the operator steps at cutover (`golem:import-settings`, `feedback:import-settings`, the `SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__*` env vars, and the SSRF allowlist question).
- `.planning/phases/14-domain-jobs-and-external-integrations/14-UAT.md` and `14-VERIFICATION.md`: the open items behind the D-13 preflight.
### Deploy pattern to mirror
- `../sm-summercmsio-app/DEPLOY.md` (and `../sm-summercmsio-app/deploy/`): the 11.2 supervisor + nginx deploy on rome, with its rollback copy and its "Verify after cutover" pattern.
### Parity tooling
- `../fonoteka.go/parity/manifest.yaml`, `../fonoteka.go/parity/routes.snapshot`, `../fonoteka.go/parity/fixtures/`: the acceptance set (D-11).
- `modules/tide/README.md`: the record, replay, proxy, normalize and diff commands.
</canonical_refs>
<code_context>
## Existing Code Insights
### Reusable Assets
- `modules/lagoon/laravel_decrypt.go`: a tested Laravel CBC payload decrypt helper, built for this import.
- `modules/lagoon/encrypted.go`: the GCM `Encrypted` cast that the import re-encrypts into.
- `modules/pact/capabilities.go`: the optional capability interface pattern (`HasCommands`, `HasMigrations`, …), where `HasWinterImport` belongs.
- `modules/tide/` (`record.go`, `replay.go`, `diff.go`, `normalize.go`, `proxy.go`, `report.go`): the re-record, replay and drift report pieces, and the smoke diff normalizer.
- `modules/bonfire`: the CLI command kernel for `summer import:winter` and the fonoteka-side verify commands.
- The existing importers `golem:import-settings` and `feedback:import-settings` (sm-golem-plugin, sm-feedback-plugin).
- `fonoteka:reindex` (Phase 14): rebuilds the Typesense index after the import.
### Established Patterns
- Two repos: the framework code (runner, capability, framework-table mappers) goes in `summercms.go` and must not name the application. App and plugin mappers, the runbook and the deploy configs go in `fonoteka.go` and the shared plugin repos (submodules; push them with `ssu`).
- Tests that need real databases use testcontainers. The import tests run MariaDB → Postgres containers and are gated like the existing integration suite.
- The database is created with the Polish ICU default collation (`fonoteka.go/README.md`). Production Postgres on rome must be created the same way, to keep the sort order of `utf8mb4_polish_ci`.
- Unit tests are the last plan of the phase (CLAUDE.md lean rule).
### Integration Points
- The Go migrations must have run on the target database before the import. The import writes into the migrated schema and never creates tables.
- The uploads tree is copied as-is under `storage/app/uploads`. Blob keys are partition + `disk_name` (Phase 5), so existing thumbs are reused.
- The nginx vhost on rome is operator-owned. The runbook gives the location blocks to change, and changes go through `nginx -t` and are never overwritten blindly.
</code_context>
<specifics>
## Specific Ideas
- The user handles the PHP side after the swap (cron, queue worker, decommission). Plans must not include those steps.
- "Leave PHP running but unrouted" is the user's chosen post-sign-off state.
- The scripted session deliberately covers only the Nuxt core session and MCP + OAuth.
</specifics>
<deferred>
## Deferred Ideas
- The Go → MariaDB reverse sync for rollback was considered and rejected, not deferred (D-07).
- PHP/MariaDB removal: the user handles it outside the plans (D-16).
- A benchmark of WinterCMS against SummerCMS on Płytarium (todo `2026-10-01-benchmark-the-application-on-wintercms-vs-summercms.md`). The rehearsal dump and the dual-backend smoke setup (D-12) make good inputs for it, but it is not in scope.
### Reviewed Todos (not folded)
- `orphan-pending-routes.md`: now owned by Phase 14.1, a prerequisite (D-13).
- `rewrite-summercms-readme.md` and `refresh-fonoteka-readme.md`: README work, not a cutover step. They can be folded into a docs quick task.
- `backend-admin-api-tokens.md`, `readme-go-fences-src.md`, `lagoon-readme-after-commit-callback-order.md`, `sitemap-plugin-port.md`: unrelated to the cutover.
</deferred>
---
*Phase: 15-cutover*
*Context gathered: 2026-10-04*

View File

@@ -0,0 +1,123 @@
# Phase 15: Cutover - 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-10-04
**Phase:** 15-cutover
**Areas discussed:** MariaDB → Postgres data move, production switch and rollback, the "all routes green" gate, daily-use acceptance
Before the discussion started, the scout found that production runs on MariaDB (with a Redis queue), not Postgres. It also found that the parity manifest holds 175 routes (172 ported, 3 pending), not 154, and that Phases 14.1 and 12.1 have not started.
---
## MariaDB → Postgres data move
| Option | Description | Selected |
|--------|-------------|----------|
| Go import command | Reads MariaDB via DSN and writes into the migrated schema. Re-encrypts inline, testable with testcontainers, adds a MySQL driver | ✓ |
| pgloader + Go fix-up pass | Staging schema copy, then a Go transform. Needs an external tool and runs in two steps | |
| mariadb-dump → SQL rewrite | Dump, rewrite the dialect, load. The most fragile option | |
| Option | Description | Selected |
|--------|-------------|----------|
| Framework capability + per-plugin mappers | Generic `summer import:winter` runner, plus `HasWinterImport` per plugin | ✓ |
| App-only command in fonoteka.go | One-off command that knows every table | |
**Not carried over (multi-select):** transient queue/job state ✓, sessions/caches/JWT blacklist ✓, Typesense index (reindex instead) ✓, old notifications ✓
| Option | Description | Selected |
|--------|-------------|----------|
| Rehearse on a prod dump + verify | Counts, checksums, decrypt round-trip and file existence, then a timed dress rehearsal | ✓ |
| Rehearse + replay Nuxt/MCP flows on imported data | Adds a manual click-through before the switch | |
| Unit/integration tests only | The first real-data run is the cutover itself | |
---
## Production switch and rollback
| Option | Description | Selected |
|--------|-------------|----------|
| Freeze, import, swap upstream | Maintenance page, final dump, import, then repoint the nginx API locations on the same origin | ✓ |
| Staging subdomain first, then swap | Needs a second Nuxt build | |
| Shadow/mirror then swap | nginx mirror, which only validates GET requests | |
| Option | Description | Selected |
|--------|-------------|----------|
| Keep PHP+MariaDB frozen; rollback window, no reverse sync | Rollback swaps nginx back; Go-side writes are lost | ✓ |
| Reverse export Go→MariaDB | Doubles the import work | |
| No rollback path | Fix forward only | |
| Option | Description | Selected |
|--------|-------------|----------|
| Supervisor, serve + worker as two programs | Matches the 11.2 deploy and `albumy-queue` | ✓ |
| Supervisor, single process | Simpler, but a deploy restarts jobs mid-flight | |
| systemd units | A new pattern on rome | |
| Option | Description | Selected |
|--------|-------------|----------|
| User runs a runbook; Claude writes it | CUTOVER.md plus per-step scripts, with output pasted back as evidence | ✓ |
| Claude runs it over SSH | Needs SSH access to rome | |
---
## "All routes green" gate
| Option | Description | Selected |
|--------|-------------|----------|
| Every manifest route, zero pending | 175 entries; API-09/QA-05 reworded | ✓ |
| Exactly the 154 fonoteka routes.php routes | The literal requirement | |
| Option | Description | Selected |
|--------|-------------|----------|
| Existing corpus + a fresh PHP re-record | Re-record before the freeze, with a drift report | ✓ |
| Existing committed corpus only | Could miss PHP changes made after recording | |
| Option | Description | Selected |
|--------|-------------|----------|
| Hard prerequisites (14.1, 12.1, Phase 14 UAT) | A preflight gate in the first plan; 12.1 is added to depends-on | ✓ |
| Only 14.1 is a prerequisite | 12.1 can land after cutover | |
| Fold the remaining work into Phase 15 | 14.1 would be removed | |
| Option | Description | Selected |
|--------|-------------|----------|
| Yes, a read-only smoke run on the rehearsal import | PHP on MariaDB against Go on imported Postgres from the same dump, diffed | ✓ |
| No, the seeded corpus is enough | | |
---
## Daily-use acceptance
| Option | Description | Selected |
|--------|-------------|----------|
| 7 days of normal household use | The scheduled jobs each run at least once, plus a CSV import and a Discogs match | ✓ |
| 48 hours | | |
| 14 days | | |
**Scripted session (multi-select):** Nuxt core session ✓, MCP + OAuth ✓, Sharing & invitations (not selected), Admin & ops (not selected)
| Option | Description | Selected |
|--------|-------------|----------|
| Stop & archive, delete later | | |
| Leave PHP running but unrouted | Keep it up for a late rollback | ✓ |
| Full removal in-phase | | |
| Option | Description | Selected |
|--------|-------------|----------|
| Yes, a pg_dump timer replaces the MariaDB one | Installed at the swap, with a restore drill | ✓ |
| No, handle separately | | |
**PHP scheduler/queue worker follow-up:** the user wrote: "I'll manage php sign off / stopping cron / workers, outside the plans, don't mind it."
---
## Claude's Discretion
- The runner's internals (batching, FK ordering, the coercion table, the interface naming) and the verify-report format.
- How the runbook is split into scripts and how evidence is laid out; the supervisor program names.
- Whether the smoke diff reuses tide replay or adds a new `parity:*` subcommand.
## Deferred Ideas
- The WinterCMS vs SummerCMS benchmark (existing todo) can reuse the rehearsal dump and the dual-backend setup.
- PHP/MariaDB decommission: the user handles it outside the plans.