diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 8c0f82d6..045e1358 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -1330,10 +1330,23 @@ Plans: 4. The homepage is a guided tour (journal, presentations, quizzes, payments, FAQ, accounts) in the summercms.io look, in English and Polish. The full bm presentation system (public, speaker, phone remote, own decks) runs on its own Centrifugo, and a roadmap deck generated from ROADMAP.md shows the features with links and the OIDC provider as screenshots. Quizzes keep logged-in attempts. 5. The new code has unit tests, delivered in the phase's last plan. -**Plans:** 0 plans +**Plans:** 5 plans Plans: -- [ ] TBD (run /msd-plan-phase 19.4.1 to break down) +**Wave 1** +- [ ] 19.4.1-01-PLAN.md — Roadmap and sales deck for the 2026-10-21 meeting: demo:seed generates EN/PL roadmap decks from ROADMAP.md (client names filtered) plus features, PHP migration with OIDC screenshots, the pxpx slide and the MCP future; presented locally in bm's viewer with a PDF fallback (wave 1) + +**Wave 2** *(blocked on Wave 1 completion)* +- [ ] 19.4.1-02-PLAN.md — Release and live tracer: module-mode go.mod of plugin tags, build/pins/smoke/deploy checks, private repos, banner and backend call to action, first deploy of demo.summercms.io on rome (wave 2) + +**Wave 3** *(blocked on Wave 2 completion)* +- [ ] 19.4.1-03-PLAN.md — Accounts behind Turnstile (sm-user-plugin opt-in), guided tour in the summercms.io look, EN/PL, journal author fields and translations, contact form via feedback, disabled reset job, reload-on-deploy banner, tags and redeploy (wave 3) + +**Wave 4** *(blocked on Wave 3 completion)* +- [ ] 19.4.1-04-PLAN.md — bm presentation system ported to Nuxt (public, speaker, phone remote, own decks), the demo's own Centrifugo, quizzes for anonymous and logged-in visitors, redeploy (wave 4) + +**Wave 5** *(blocked on Wave 4 completion)* +- [ ] 19.4.1-05-PLAN.md — Unit tests (Go harness, bm lib tests under vitest, plugin tests), check-all gate and live verification, docs, todos and g15office (wave 5) ### Phase 19.5: Social login in sm-user-plugin (INSERTED) @@ -1500,7 +1513,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → | 19.1. Chat satellites | 6/6 | Complete | 2026-10-18 | | 19.3. Framework transport and infrastructure for WavePath | 6/6 | In Progress| | | 19.4. OIDC provider on wristband | 4/4 | Complete | 2026-10-20 | -| 19.4.1. Early public demo on demo.summercms.io | 0/TBD | Not started | - | +| 19.4.1. Early public demo on demo.summercms.io | 0/5 | Planned | - | | 19.5. Social login in sm-user-plugin | 0/4 | Planned | - | | 19.6. Two-factor auth and device flow in sm-user-plugin | 0/TBD | Not started | - | | 19.7. Generic MCP module | 0/TBD | Not started | - | diff --git a/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-01-PLAN.md b/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-01-PLAN.md new file mode 100644 index 00000000..0f0c1b54 --- /dev/null +++ b/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-01-PLAN.md @@ -0,0 +1,343 @@ +--- +phase: 19.4.1-early-public-demo-on-demo-summercms-io +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - ../sm-demo-app/plugins/demo/plugin.go + - ../sm-demo-app/plugins/demo/config/config.yaml + - ../sm-demo-app/plugins/demo/console/seed.go + - ../sm-demo-app/plugins/demo/console/roadmap_deck.go + - ../sm-demo-app/plugins/demo/content/content.go + - ../sm-demo-app/plugins/demo/content/ROADMAP.md + - ../sm-demo-app/plugins/demo/content/oidc/oidc-consent.png + - ../sm-demo-app/plugins/demo/content/oidc/oidc-error.png + - ../sm-demo-app/plugins/demo/content/oidc/oidc-clients.png + - ../sm-demo-app/plugins/demo/content/screens/admin-posts-list.webp + - ../sm-demo-app/plugins/demo/content/screens/admin-post-edit.webp + - ../sm-demo-app/plugins/demo/roadmap/roadmap.go + - ../sm-demo-app/plugins/demo/roadmap/names.go + - ../sm-demo-app/plugins/demo/roadmap/deck.go + - ../sm-demo-app/plugins/demo/roadmap/curated.go + - ../sm-demo-app/plugins/demo/roadmap/roadmap_smoke_test.go + - ../sm-demo-app/scripts/sync-roadmap.sh + - ../sm-demo-app/scripts/dev.sh + - ../sm-demo-app/scripts/deck-snapshot.cjs + - ../sm-demo-app/scripts/capture-oidc.cjs + - ../sm-demo-app/deploy/env.example + - ../sm-demo-app/README.md +autonomous: false +requirements: [SC-4, D-06, D-13, D-16, D-19, D-20, D-23] +estimate: + tokens: 190000 + raw_tokens: 190000 + tasks: 4 + confidence: low +must_haves: + truths: + - "D-20, D-16: demo:seed creates or updates two roadmap decks, slug roadmap (English) and slug roadmap-pl (Polish), owned by the visitor account and published, whose sections come in this order: (1) the roadmap, phases done and next, parsed from the embedded ROADMAP.md snapshot; (2) features of the framework and the shared plugins; (3) migration from PHP: WinterCMS/OctoberCMS applications ported plugin by plugin with the frontend unchanged, tide record and replay, the OIDC provider as screenshots; (4) cooperation with pxpx; (5) the future of the framework, an MCP-controlled CMF." + - "D-16: a second demo:seed run replaces the slides of both decks, keeps each deck's public_hash (a shared /prezentacje/{hash} link keeps working) and leaves no duplicate image attachment on either deck." + - "D-19: the pxpx slide names (1) GraphQL compatible with Lighthouse PHP, which most pxpx applications use, and (2) migrating and supporting legacy WordPress applications through tide (record the old site, replay against the port), as work Golem15 does not plan to do itself; it carries no link and exists in both languages." + - "D-20 (client names filtered, RESEARCH Pitfall 7): no slide title, body or speaker note of either deck contains a denylisted client name; phases whose ROADMAP text names a client appear under a curated neutral title; the smoke test pins zero leaks and zero left-out phases for the committed snapshot." + - "D-06: demo:seed reads the visitor's email and password only from golem15.demo.visitor.email and golem15.demo.visitor.password (SUMMER_GOLEM15__DEMO__VISITOR__EMAIL and __PASSWORD); the hard-coded customer account is gone; with the email unset the visitor and roadmap steps report skipped; the password is never printed." + - "D-20 (presentable on 2026-10-21): scripts/dev.sh present starts the demo binary on 127.0.0.1:8096 and bm's unmodified presentation viewer on 127.0.0.1:5173 and prints the public URL /prezentacje/{hash} of both decks and the speaker URL /moje-prezentacje/{slug}; scripts/dev.sh snapshot renders every slide of both decks to PNG and into one PDF per deck as an offline fallback." + - "D-13: both decks carry the same sequence of slides, each in its own language, with speaker notes (say_html) in that language." + artifacts: + - path: "../sm-demo-app/plugins/demo/roadmap/roadmap.go" + provides: "Parse(md []byte) ([]Phase, error) over the ROADMAP.md phase list" + contains: "func Parse" + - path: "../sm-demo-app/plugins/demo/roadmap/deck.go" + provides: "Build(phases []Phase, locale string) (Deck, []string, error) composing the five sections" + contains: "func Build" + - path: "../sm-demo-app/plugins/demo/roadmap/curated.go" + provides: "curated EN/PL feature, migration, pxpx and MCP slides" + contains: "Lighthouse" + - path: "../sm-demo-app/plugins/demo/roadmap/names.go" + provides: "client-name denylist, per-phase EN/PL titles and groups" + contains: "denylist" + - path: "../sm-demo-app/plugins/demo/console/roadmap_deck.go" + provides: "the roadmap decks seed step (presentations classes.Import with Replace, publish, theme, image de-duplication)" + contains: "classes.Import" + - path: "../sm-demo-app/plugins/demo/content/content.go" + provides: "embedded ROADMAP.md snapshot, OIDC screenshots and admin screenshots" + contains: "go:embed" + - path: "../sm-demo-app/scripts/deck-snapshot.cjs" + provides: "per-slide PNG and PDF render of a deck through a running viewer" + contains: "snapshot:" + key_links: + - from: "../sm-demo-app/plugins/demo/console/roadmap_deck.go" + to: "../sm-demo-app/plugins/golem15/presentations/classes/import.go" + via: "classes.Import(ctx, db, deck, classes.ImportOptions{Slug, OwnerEmail, Replace: true, DeckDir, Bucket})" + pattern: "Replace:\\s*true" + - from: "../sm-demo-app/plugins/demo/console/roadmap_deck.go" + to: "../sm-demo-app/plugins/demo/content/content.go" + via: "roadmap.Parse(content.Roadmap) then roadmap.Build per locale" + pattern: "content\\.Roadmap" + - from: "../sm-demo-app/scripts/dev.sh" + to: "../sm-bm-app/frontend/vite.config.ts" + via: "present starts the demo binary on 127.0.0.1:8096, the backend bm's Vite proxy targets" + pattern: "8096" + prohibitions: + - statement: "No visitor or admin credential value appears in a committed file, a slide, the seed output or the present output (the present output names the .env key that holds the password)." + status: unresolved + verification: judgment + - statement: "No client name reaches either deck (checked on the built decks, not on source files)." + status: unresolved + verification: automated + - statement: "No file under ../sm-bm-app is modified; bm's frontend is only run." + status: unresolved + verification: judgment + - statement: "Nothing is pushed, tagged or deployed in this plan." + status: unresolved + verification: judgment +--- + +## Phase Goal + +ROADMAP Goal (not in user-story format; no story is invented here): Clients can be shown a live demo at `demo.summercms.io` now, before the chat family lands: `sm-demo-app` with today's 16 shared plugins runs on rome, its public Nuxt site shows the plugins working through prepared demo accounts, and the admin backend stays private. + +This plan's slice (plan 1 of 5, wave 1, executed first per D-20 and D-23): the roadmap and sales deck. The user presents it at an orientation meeting on 2026-10-21, before the Nuxt port and the deploy exist, so it has to be complete and presentable from a local run. Its generator lives in `demo:seed`, so plans 02 to 04 and every later deploy reuse and refresh it. + +## Decisions taken in this plan (Claude's discretion and recorded assumptions) + +- **Presenting path for 2026-10-21 (D-20, the cheapest reliable one):** a local run. `scripts/dev.sh present` builds the demo, seeds it and runs the binary on `127.0.0.1:8096`, the backend that bm's Vite dev server already proxies `/_user`, `/_presentations` and `/storage` to (`../sm-bm-app/frontend/vite.config.ts`). It then starts bm's frontend, unmodified, on `127.0.0.1:5173`. Both use the same `sm-presentations-plugin` API at the version the demo pins, so the deck renders in the viewer bm already ships: public full screen at `/prezentacje/{hash}` and the speaker view at `/moje-prezentacje/{slug}` after logging in as the visitor at `/logowanie`. Rejected options: pushing the deck into bm's production through its PAT API (that writes sales content into another live application, needs a token and a go-ahead, and the PAT API cannot attach the screenshots), and waiting for the Nuxt port (plan 04). Fallback for the meeting: `scripts/dev.sh snapshot` writes one PNG per slide and a PDF per deck under `logs/deck/`. +- **ROADMAP source (discretion):** a committed snapshot `plugins/demo/content/ROADMAP.md`, refreshed by `scripts/sync-roadmap.sh` from the framework's committed `master` (`git show`, never the working tree) and embedded with `go:embed`. Plan 02's release build fails when the snapshot is stale (D-16, "stays current with each deploy"). +- **Owner:** the roadmap decks belong to the visitor account, so the user can present them from the speaker view now and with the phone remote after plan 04. `presentations` `classes.Import` needs an existing owner, so the seed half of D-06 (visitor credentials from config only) moves into this plan. Plan 03 keeps the site half of D-05/D-06 (nothing shown on the site). +- **Links (D-16):** the presentations sanitizer (`classes.Clean`, `classes/sanitize.go`) allows no anchor element, so feature slides show URLs as visible text (`demo.summercms.io/blog`, `summercms.io/docs/...`). That also keeps user decks free of links on the public origin. +- **Client names (Pitfall 7, assumption A6 confirmed by D-20 "client names filtered"):** curated neutral titles per phase number plus a case-insensitive denylist. A phase that still matches after the overrides is left out of the deck and reported in the seed output, so a future roadmap line cannot leak a name or break a deploy. The denylist and the overrides live in `sm-demo-app` (a private application repository), never in framework code. +- **OIDC screenshots (discretion):** captured automatically with the globally installed Playwright 1.56.1 (`/usr/lib/node_modules/playwright`, Chromium 1223 cached) against the local demo, stored as embedded PNGs and attached to the decks as presentation images. The scripts are CommonJS (`.cjs`) because ESM imports ignore `NODE_PATH`. The one-time client secret screen is never captured. +- **Done phases** are grouped into curated themes (kernel and data; HTTP, accounts and OAuth; admin; jobs, realtime and search; first production port; content plugins; payments; chat; identity), one timeline per slide. Phase titles exist in English and Polish; speaker notes are written per group. +- **Theme:** `atlas` or `monografia` (both registered in bm's `src/lib/deck/themes.ts` and offered by the plugin), whichever reads better full screen on a projector. The executor records the choice. + +## Artifacts this phase produces (this plan) + +- Packages in `sm-demo-app`: `plugins/demo/roadmap` (`Parse`, `Build`, types `Phase`, `Deck`, `Slide`), `plugins/demo/content` (`Roadmap []byte`, `Images embed.FS`). +- `demo:seed` steps: `visitor` (from config), `roadmap decks roadmap, roadmap-pl` (upsert). Plugin config keys `golem15.demo.visitor.email` and `golem15.demo.visitor.password`. +- Decks: `roadmap` (EN) and `roadmap-pl` (PL), published, owned by the visitor. +- Scripts: `scripts/sync-roadmap.sh [--check]`, `scripts/dev.sh present | snapshot [slug...]`, `scripts/deck-snapshot.cjs`, `scripts/capture-oidc.cjs`. +- Assets: `plugins/demo/content/ROADMAP.md`, `content/oidc/oidc-{consent,error,clients}.png`, `content/screens/admin-{posts-list,post-edit}.webp`. +- Smoke tests: `TestRoadmapTracerSmoke`, `TestRoadmapDeckSmoke` (`plugins/demo/roadmap/roadmap_smoke_test.go`). Full unit tests come in plan 05. + + +Give the user a complete roadmap and sales deck, in English and Polish, that they can present on 2026-10-21 from a local run, generated by demo:seed from the framework's ROADMAP.md plus curated content, in the D-20 section order, with the pxpx cooperation slide (D-19) and the OIDC provider as screenshots (D-16). + +Purpose: D-20 (the deck is the user's main presentation tomorrow), D-16, D-19, D-13 for the deck, the seed half of D-06; SC-4's roadmap deck. D-23: this is plan 01 of the five agreed plans and executes first. +Output: commits in ../sm-demo-app only (code and assets; no planning files there). Nothing pushed. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-CONTEXT.md +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-RESEARCH.md +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-PATTERNS.md + +Repos: ../sm-demo-app (all edits). Read-only: ../sm-bm-app/frontend (run with npm, never edited), ../sm-demo-app/plugins/golem15/presentations (the submodule at the version the demo compiles; read, never edit), ../sm-oauthserver-plugin (config loader analog), summercms.go `.planning/ROADMAP.md`, `README.md` (module table) and https://summercms.io/docs/ (the published docs index; URLs on slides must exist there), ../sm-summercmsio-app/vue-summercmsio-app/public/screens/*.webp (admin screenshots to reuse). + +Rules: no co-author tags; stage files by explicit path; code and planning docs in separate commits; sm-demo-app keeps building with go.work in this plan (plan 02 folds plugins/demo into the root module). Verify commands run from the framework root; sm-demo-app is addressed with `go -C ../sm-demo-app` and its scripts by path (they cd to their own root). `plugins/demo` is a nested module today, so Go commands name `. ./plugins/demo/...` explicitly; `./...` would pull in every plugin submodule of the workspace. + + +presentations plugin (../sm-demo-app/plugins/golem15/presentations, read-only): +- `classes.Import(ctx context.Context, db *gorm.DB, deck *classes.Deck, opts classes.ImportOptions) (classes.ImportSummary, error)`; `classes.ImportOptions{Slug, OwnerEmail string; TargetSeconds int; DryRun, Replace bool; DeckDir string; Bucket *blob.Bucket}`. With Replace an existing deck keeps its row and so its `public_hash`; its slides are deleted and rewritten; its owner must match OwnerEmail. Images listed in `Deck.Images` are read from DeckDir and attached on every run (old attachments are NOT removed). +- `classes.Deck{Slides []classes.Slide; Images []string; TotalSeconds int; ...}`, `classes.Slide{Key string; Number int; Special bool; ParentKey string; Position int; Title, CSSClass, SectionLabel string; Seconds int; BodyHTML, SayHTML, NotesHTML, SourcesHTML, PublicCommentHTML, PublicSourcesHTML, Todo string; Images []string}` (classes/deck_parse.go:22-49). +- `models.Presentation` columns `owner_user_id`, `is_published`, `public_hash` (BeforeCreate assigns it once), `theme`, `css`; `models.PresentationImagesField`; `(Presentation).MorphName()`. +- `classes.Clean` allowlist (classes/sanitize.go:21-41): div, span, p, br, b, i, em, strong, small, sub, sup, h1-h4, ul, ol, li, header, footer, figure, figcaption, table elements, blockquote, cite, q, abbr, code, hr, img; class/title/lang everywhere; img src must be a bare attached filename. No anchor element. +- Public API `GET /_presentations/api/v1/public/{slugHash}`, images `GET /_presentations/api/v1/public/{slugHash}/images/{name}`; speaker `GET /_presentations/api/v1/mine/{slug}` behind jwt.auth. +framework v0.3.0: `attach.OpenBucket(ctx, cfg *compass.Config) (*blob.Bucket, error)`, `attach.BlobKeys(f attach.File) []string`, `attach.DeleteKeys(ctx, bucket, keys) error`; `compass.Config.String/Bool/Int`; `bouncer.HashPassword(cost int, password string)`. +bm viewer (../sm-bm-app/frontend, read-only): routes `/prezentacje/:slugHash` (public, bare), `/moje-prezentacje/:slug` (speaker), `/logowanie` (login) in src/router.ts; Vite proxy target `http://127.0.0.1:8096`; DeckViewer follows `window.location.hash` (src/components/deck/DeckViewer.vue:136, 417; src/lib/deck/engine.ts); deck CSS vocabulary in src/deck/deck.css: slide classes `title`, `sec`, `special`; helpers `.k`, `.cols`, `.cols.w`, `.tl` with `.y`, `.tl.compact`, `.tl.big`, `.pts`, `.theses`, `.note`, `.quote`, `.sum`, `.people`/`.p`, `.small`, `.muted`, `.red`; themes `atlas`, `monografia`. +seed.go today: `lookupDB(ctx, app)`, `exists`, `insertID`, step table with a per-step transaction (lines 36-64), hard-coded customer constants at lines 17-22, `seedPresentation` (getting-started, unowned). +Config loader analog: ../sm-oauthserver-plugin/plugin.go:40-41 (`//go:embed config`) and :110-121 (`config()`, `ConfigFS() fs.FS`). +ROADMAP.md phase list (before `## Phase Details`): 22 done lines and 8 open lines today, format `- [x] **Phase N: Title** - summary (completed YYYY-MM-DD)`. + + + + + + + Task 1: Tracer: the user opens the English roadmap deck, generated by demo:seed from ROADMAP.md, full screen in bm's viewer against the local demo + `docker info` answers; 127.0.0.1:8096 and 127.0.0.1:5173 are free (`ss -ltnH` shows neither); ../sm-bm-app/frontend/package.json exists. + ../sm-demo-app/scripts/sync-roadmap.sh, ../sm-demo-app/plugins/demo/content/content.go, ../sm-demo-app/plugins/demo/content/ROADMAP.md, ../sm-demo-app/plugins/demo/roadmap/roadmap.go, ../sm-demo-app/plugins/demo/roadmap/names.go, ../sm-demo-app/plugins/demo/roadmap/deck.go, ../sm-demo-app/plugins/demo/roadmap/roadmap_smoke_test.go, ../sm-demo-app/plugins/demo/config/config.yaml, ../sm-demo-app/plugins/demo/plugin.go, ../sm-demo-app/plugins/demo/console/seed.go, ../sm-demo-app/plugins/demo/console/roadmap_deck.go, ../sm-demo-app/scripts/dev.sh, ../sm-demo-app/scripts/deck-snapshot.cjs, ../sm-demo-app/deploy/env.example + ../sm-demo-app/plugins/demo/console/seed.go, ../sm-demo-app/plugins/demo/plugin.go, ../sm-demo-app/scripts/dev.sh, ../sm-demo-app/deploy/env.example, ../sm-demo-app/plugins/golem15/presentations/classes/import.go (Import, loadOrCreate, attachImages, findOwner), ../sm-demo-app/plugins/golem15/presentations/classes/deck_parse.go (Slide, Deck), ../sm-demo-app/plugins/golem15/presentations/models/presentation.go, ../sm-oauthserver-plugin/plugin.go (config embed and loader), ../sm-bm-app/frontend/vite.config.ts, ../sm-bm-app/frontend/src/router.ts, ../sm-bm-app/frontend/src/views/PresentationPublicView.vue, ../sm-bm-app/frontend/src/components/deck/DeckViewer.vue, ../sm-bm-app/frontend/src/lib/deck/engine.ts, ../sm-bm-app/frontend/src/deck/deck.css, .planning/ROADMAP.md (phase list, lines 17-50), 19.4.1-RESEARCH.md (Roadmap deck, Pitfalls 7 and 10), 19.4.1-PATTERNS.md (seed.go, roadmap.go, embedded content) + Per D-20, D-16, D-06 (seed half), SC-4. Wire one path end to end: ROADMAP.md, parser, deck builder, seed upsert, presentations API, bm's viewer, a rendered PNG. English only in this task; Task 2 adds the remaining sections and Polish. + +1. Snapshot. scripts/sync-roadmap.sh (bash, set -euo pipefail, ROOT from the script path, FW="${SUMMERCMS_FRAMEWORK:-$ROOT/../summercms.go}", REF="${ROADMAP_REF:-master}") writes the output of `git -C "$FW" show "$REF:.planning/ROADMAP.md"` to plugins/demo/content/ROADMAP.md (committed content only, never the working tree). With `--check` it compares instead and exits 1 with "sync-roadmap: plugins/demo/content/ROADMAP.md differs from $REF; run scripts/sync-roadmap.sh" on a difference, and prints "sync-roadmap: ok" otherwise. Run it and commit the snapshot. plugins/demo/content/content.go (package content) embeds ROADMAP.md into the exported `Roadmap []byte` (Task 3 adds the image FS). + +2. Parser. plugins/demo/roadmap/roadmap.go (package roadmap, standard library only): type `Phase{Number, Title, Summary string; Done bool; Completed string}`; `Parse(md []byte) ([]Phase, error)` reads lines until the first `## Phase Details` heading and matches the RESEARCH regex for phase list lines (checked or open box, bold "Phase N: Title", " - " summary). It moves a trailing "(completed YYYY-MM-DD)" into Completed. An input with no phase line is an error. Keep the order of the file. + +3. Names. plugins/demo/roadmap/names.go: an unexported `denylist` of lower-case substrings (the five client names of RESEARCH Pitfall 7, plus the ASCII spelling of the first one), matched case-insensitively. A `phaseText` map keyed by phase number gives curated EN titles (and, for open phases, an EN summary) for every phase whose ROADMAP title or summary names a client: phases 3, 12, 13, 14.2, 16, 18, 19.3, 30, 31 today (neutral wording such as "First production port: core content API", "First blog on reusable views", "Framework transport for a chat client", "First production cutover"). A `groups` table assigns each done phase number to a curated theme (see Decisions) with an EN label; a number missing from the table falls into a last "More" group. Expose `leaks(text string) bool`. + +4. Deck. plugins/demo/roadmap/deck.go: types `Slide{Title, CSSClass, SectionLabel string; Seconds int; BodyHTML, SayHTML string; Images []string}` and `Deck{Slug, Title, Theme string; Slides []Slide; Images []string}`; `Build(phases []Phase, locale string) (Deck, []string, error)` returns the deck plus the numbers of phases it left out. Locale "en" gives slug roadmap; any locale other than en or pl is an error. A phase whose curated or original text still leaks is left out. In this task the deck holds: a title slide (CSSClass title: "SummerCMS", a subtitle, "Golem15 · summercms.io"); a "Roadmap" section divider (CSSClass sec, SectionLabel set on every slide of the section); one Done slide per group (`.tl` timeline: phase number in `.y`, title, completion date); one or two Next slides with the open phases (number, title, one-line summary). Escape every text value with html.EscapeString. Emit no anchor element. Use the bm deck CSS vocabulary listed in the interfaces block. + +5. Seed. plugins/demo/config/config.yaml holds `visitor: {email: "", password: ""}`. plugin.go embeds the config directory, exposes `ConfigFS() fs.FS` and asserts the same config capability interface the oauthserver plugin asserts. In seed.go, replace the hard-coded customer constants and seedCustomer with `seedVisitor`, which reads golem15.demo.visitor.email and golem15.demo.visitor.password from app.Config: + - an empty email gives state "skipped (golem15.demo.visitor.email is not set)"; + - an empty password, or one shorter than 8 characters, is an error naming the key; + - a missing row is created (activated, has_self_set_password true, name "Demo", surname "Visitor"); + - an existing row whose bcrypt hash no longer verifies the configured password gets a new hash (state "updated"); otherwise the state is "exists". + Change the step table so each step returns its state string ("created", "exists", "updated", "skipped …"); keep the per-step transaction for the existing steps. + console/roadmap_deck.go adds the step "roadmap decks". It runs outside the per-step transaction because classes.Import opens its own: parse content.Roadmap, Build("en"), map the slides to classes.Slide (Key and Number the 1-based index as text and int, Position the index, Title, CSSClass, SectionLabel, Seconds, BodyHTML, SayHTML, Images), set TotalSeconds, and call classes.Import with Slug, OwnerEmail set to the visitor email, Replace true, TargetSeconds the total, DeckDir and Bucket (attach.OpenBucket(ctx, app.Config)). Then publish the deck and set its theme in one UPDATE. The state is "created" when the slug did not exist before, else "updated". Print the left-out phase numbers, if any, on their own line. With no visitor email the step is skipped with the same reason. Never print the password. + +6. Present. scripts/dev.sh: + - GO_ADDR becomes overridable. + - ensure_env appends SUMMER_GOLEM15__DEMO__VISITOR__EMAIL=visitor@example.test and a random 24-character SUMMER_GOLEM15__DEMO__VISITOR__PASSWORD to an existing .env that lacks them (umask 077, values never echoed). + - New subcommand `present`. It refuses, naming the port and the process, when 127.0.0.1:8096 or 127.0.0.1:5173 is taken by a process it did not start. It runs build, Postgres and seed, then starts the binary on 127.0.0.1:8096 under the pid name present-go. It starts bm's frontend with `npm --prefix "${BM_FRONTEND:-../sm-bm-app/frontend}" run dev -- --host 127.0.0.1 --port 5173 --strictPort` under the pid name present-bm, running `npm ci` there only when node_modules is missing. It waits for both, then prints for each roadmap deck the line `deck {slug}: http://127.0.0.1:5173/prezentacje/{public_hash}` (hash read with psql in the sm-demo-pg container), the speaker URL /moje-prezentacje/{slug}, the login page /logowanie, and "visitor: {email} (password: SUMMER_GOLEM15__DEMO__VISITOR__PASSWORD in .env)". + - New subcommand `snapshot [slug...]` (default roadmap roadmap-pl; a slug without a deck is reported and fails the command): runs `node scripts/deck-snapshot.cjs` per slug with NODE_PATH="$(npm root -g)". + - `down` also stops present-go and present-bm. + deploy/env.example documents the two visitor keys with empty values. + +7. scripts/deck-snapshot.cjs (CommonJS, `require('playwright')`): + - arguments --base, --path, --hash, --slug, --out (default logs/deck); + - fetches {base}/_presentations/api/v1/public/{hash} through the viewer's proxy; a non-200 answer exits 1; + - opens {base}{path}{hash} in headless Chromium at 1920x1080 and walks every slide with the viewer's own navigation (the hash format in engine.ts, or ArrowRight); + - on each slide waits for the active slide and fails when its text is empty or an image in it has naturalWidth 0; writes {out}/{slug}/NN.png; + - prints the PNGs into {out}/{slug}.pdf, one 1920x1080 page per slide with printBackground; + - prints `snapshot: {slug} {N} slides {pdf path}`; + - exits 1 when the rendered slide count differs from the API's. + +8. roadmap_smoke_test.go `TestRoadmapTracerSmoke`: Parse(content.Roadmap) finds at least 20 done and 6 open phases; Build(phases, "en") returns slug roadmap, a first slide with CSSClass title, at least one sec divider, no left-out phase, and no slide field for which leaks reports true. + +Do not edit anything under ../sm-bm-app. Commit in sm-demo-app: the snapshot and sync script; the roadmap package, seed and config; the dev.sh and snapshot script (separate logical commits). + + go -C ../sm-demo-app vet . ./plugins/demo/... && go -C ../sm-demo-app test -count=1 -short . ./plugins/demo/... && go -C ../sm-demo-app test -count=1 -short -run '^TestRoadmapTracerSmoke$' -v ./plugins/demo/roadmap 2>&1 | grep -q -- '^--- PASS: TestRoadmapTracerSmoke' && ../sm-demo-app/scripts/dev.sh present && ../sm-demo-app/scripts/dev.sh snapshot roadmap 2>&1 | grep -q '^snapshot: roadmap ' + Non-zero exit: vet or the short suite red; no "--- PASS: TestRoadmapTracerSmoke" line; present stops on a busy port, a build error or a seed error; or no "snapshot: roadmap " line (the public deck API is not 200, a slide renders empty, or the rendered slide count differs from the API). + + + - `../sm-demo-app/scripts/sync-roadmap.sh --check` prints `sync-roadmap: ok`. + - `grep -rn 'Customer123' ../sm-demo-app/plugins/demo ../sm-demo-app/scripts` prints nothing. + - `grep -c 'golem15.demo.visitor.email' ../sm-demo-app/plugins/demo/console/seed.go` prints at least 1. + - `grep -c 'Replace: *true' ../sm-demo-app/plugins/demo/console/roadmap_deck.go` prints 1. + - Running `../sm-demo-app/scripts/dev.sh present` twice prints the same `deck roadmap: http://127.0.0.1:5173/prezentacje/` URL both times (public_hash kept). + - `git -C ../sm-bm-app status --porcelain` prints nothing the executor caused (compare with the state recorded before the task). + + The English roadmap deck opens full screen in bm's viewer from a local run; demo:seed regenerates it from ROADMAP.md without changing its link; the visitor comes from config only. + + + + Task 2: The whole deck in English and Polish: roadmap, framework and plugin features with URLs, migration from PHP, the pxpx cooperation slide and the MCP future, with speaker notes + ../sm-demo-app/plugins/demo/roadmap/curated.go, ../sm-demo-app/plugins/demo/roadmap/names.go, ../sm-demo-app/plugins/demo/roadmap/deck.go, ../sm-demo-app/plugins/demo/roadmap/roadmap_smoke_test.go, ../sm-demo-app/plugins/demo/console/roadmap_deck.go, ../sm-demo-app/plugins/demo/content/content.go, ../sm-demo-app/plugins/demo/content/screens/admin-posts-list.webp, ../sm-demo-app/plugins/demo/content/screens/admin-post-edit.webp, ../sm-demo-app/README.md + ../sm-demo-app/plugins/demo/roadmap/deck.go and names.go (Task 1), README.md of summercms.go (module table and summary sentences), the README.md first paragraph of each shared plugin the demo installs (../sm-journal-plugin, ../sm-faq-plugin, ../sm-menu-plugin, ../sm-sitemap-plugin, ../sm-translate-plugin, ../sm-paymentgateway-plugin, ../sm-paymentgatewaysubscriptions-plugin, ../sm-pgstripe-plugin, ../sm-oauthserver-plugin, ../fonoteka.go/plugins/golem15/user, ../fonoteka.go/plugins/golem15/feedback, ../sm-demo-app/plugins/golem15/presentations, ../sm-demo-app/plugins/golem15/quizzes, ../sm-demo-app/plugins/golem15/golem), modules/tide/README.md, modules/wristband/README.md, .planning/ROADMAP.md (Phase 19.7 and Phase 20 details), https://summercms.io/docs/ (index of published pages), ../sm-summercmsio-app/vue-summercmsio-app/public/screens/, 19.4.1-CONTEXT.md (D-16, D-19, D-20) + Per D-20, D-19, D-16, D-13. Complete the deck in the D-20 order and add Polish. Content is curated Go data in plugins/demo/roadmap/curated.go: a table of slides per section, each with EN and PL title, body HTML and speaker notes (say_html). Facts come only from the READMEs, ROADMAP.md and docs listed in read_first, never invented. + +- Title slide (both languages), then: +- (1) Roadmap: as Task 1, plus PL. Add a PL title in phaseText for every phase number in the snapshot (curated, not machine-literal), a PL label per group and per-group speaker notes in both languages that summarize what the group delivered. +- (2) Features. A section divider, then 3 to 5 slides: + - the framework: one binary with plugins, admin and APIs; WinterCMS-style plugins with capability interfaces; lists and forms from columns.yaml and fields.yaml compiled into a JSON admin API with the embedded Vue admin (attach the two admin screenshots copied from ../sm-summercmsio-app/vue-summercmsio-app/public/screens/posts-list.webp and post-edit.webp as content/screens/admin-posts-list.webp and admin-post-edit.webp, embedded and listed in the slide Images); jobs on River, realtime on Centrifugo, OAuth 2.1 and an OpenID Connect provider, Turnstile, translations and mail, console scaffolding; + - the shared plugins with one line each: user, translate, journal, sitemap, menu, FAQ, payment gateway with Stripe and subscriptions, presentations, quizzes, AI models, feedback, OIDC provider; the chat family arrives with Phase 20. + - Each feature that has a demo page or a docs page shows that URL as visible text in a `.k` span (demo.summercms.io/blog, /faq, /subscriptions, /quizzes, /presentations; summercms.io/docs/... pages that exist on the published index). No anchor element anywhere. +- (3) Migration from PHP. A divider, then: + - "Port plugin by plugin, frontend unchanged": the PHP API contract is the acceptance test; the existing Nuxt site and MCP server run unchanged against the Go backend; byte-compatible JSON; + - "tide: record the PHP application, replay against Go, diff": the parity harness; + - the OIDC provider slides (filled in Task 3). +- (4) "Cooperation with pxpx" (PL "Współpraca z pxpx"), one slide per D-19: work Golem15 does not plan to do itself but that would make migrating pxpx's OctoberCMS applications straightforward: (1) GraphQL compatible with Lighthouse PHP, which most pxpx applications use; (2) migrating and supporting legacy WordPress applications through tide, by recording the old site and replaying against the port. No links. Use CSSClass special or a distinct layout so it stands out. +- (5) The future. A divider and 1 or 2 slides: an MCP-controlled CMF where content, the admin and scaffolding are driven by AI agents through MCP; Phase 19.7 (generic MCP module: plugin-declared tools filtered by permissions, one connector per person through OAuth) is the first step. +- Closing slide: demo.summercms.io, summercms.io/docs, Golem15. + +Build(phases, "pl") returns slug roadmap-pl with the same slide sequence in Polish. Seconds per slide fit the content (20 to 120); the deck target is the sum. The seed step now imports both decks and reports them as "roadmap decks roadmap, roadmap-pl". README.md gets a "Roadmap deck" section (the generator, sync-roadmap.sh, present, snapshot, the visitor keys) and drops the hard-coded customer from its seeded-content list. + +Extend roadmap_smoke_test.go with `TestRoadmapDeckSmoke`: +- for en and pl, the section dividers appear in the order Roadmap, Features, Migration, pxpx, Future; +- the EN and PL decks have equal slide counts; +- every slide has non-empty BodyHTML and SayHTML; +- no field of any slide leaks, and no field contains an anchor tag; +- the pxpx slide mentions Lighthouse, WordPress and tide; +- the Future section mentions MCP; +- every phase number in the snapshot has a PL title in phaseText. + + go -C ../sm-demo-app vet . ./plugins/demo/... && go -C ../sm-demo-app test -count=1 -short -run '^(TestRoadmapTracerSmoke|TestRoadmapDeckSmoke)$' -v ./plugins/demo/roadmap 2>&1 | grep -c -- '^--- PASS: Test' | grep -qx 2 && ../sm-demo-app/scripts/dev.sh present && ../sm-demo-app/scripts/dev.sh snapshot roadmap roadmap-pl 2>&1 | grep -c '^snapshot: roadmap' | grep -qx 2 + Non-zero exit: vet red; fewer than two "--- PASS" lines (wrong section order, a leaked client name, an anchor, a missing PL title, unequal decks); present fails; or fewer than two "snapshot: roadmap" lines (a deck missing or a slide rendering empty in bm's viewer). + + + - `grep -c 'Lighthouse' ../sm-demo-app/plugins/demo/roadmap/curated.go` prints at least 2 (EN and PL). + - `grep -c 'MCP' ../sm-demo-app/plugins/demo/roadmap/curated.go` prints at least 2. + - `ls ../sm-demo-app/logs/deck/roadmap-pl.pdf ../sm-demo-app/logs/deck/roadmap.pdf` lists both files. + - The seed output line for the decks reads "roadmap decks roadmap, roadmap-pl" followed by "created" or "updated", and no left-out phase line is printed. + - Every summercms.io/docs URL in curated.go answers 200 (`curl -s -o /dev/null -w '%{http_code}'` per URL, results recorded in the SUMMARY). + + Both decks carry all five D-20 sections, in their own language, with notes and visible URLs; the snapshot renders both to PNG and PDF. + + + + Task 3: The migration section shows the Go OIDC provider: consent page, branded error page and the Clients admin, captured from the local demo and attached to both decks + ../sm-demo-app/scripts/capture-oidc.cjs, ../sm-demo-app/plugins/demo/content/oidc/oidc-consent.png, ../sm-demo-app/plugins/demo/content/oidc/oidc-error.png, ../sm-demo-app/plugins/demo/content/oidc/oidc-clients.png, ../sm-demo-app/plugins/demo/content/content.go, ../sm-demo-app/plugins/demo/roadmap/curated.go, ../sm-demo-app/plugins/demo/console/roadmap_deck.go, ../sm-demo-app/scripts/dev.sh + ../sm-oauthserver-plugin/README.md (routes, consent, branding, Clients admin), ../sm-oauthserver-plugin/routes.go, ../sm-oauthserver-plugin/oidc_flow_smoke_test.go and pages_smoke_test.go (how a client and a consent request are set up), ../sm-demo-app/scripts/dev.sh (admin credentials constants), ../sm-demo-app/plugins/golem15/presentations/classes/import.go (attachImages), ../sm-demo-app/plugins/golem15/presentations/classes/sanitize.go (bare image names), ../sm-demo-app/plugins/demo/console/roadmap_deck.go (Task 1) + Per D-16 (OIDC provider as screenshots, no links) and D-20 section 3. + +scripts/capture-oidc.cjs (CommonJS, require('playwright'), run by `scripts/dev.sh capture-oidc` against the running present stack on 127.0.0.1:8096, NODE_PATH="$(npm root -g)") writes three PNGs, each at most 1600 px wide, into plugins/demo/content/oidc/: +- oidc-error.png: the plugin's branded error page for an authorize request with an unknown client id; +- oidc-clients.png: the admin Clients list after the script, logged in to the admin with the dev.sh admin credentials, has created a client named "Acme portal" with redirect URI https://portal.example.test/callback and scopes openid, profile and email; +- oidc-consent.png: the consent page for that client, reached by an authorize request (PKCE S256, state, nonce) made as the visitor (the JWT from /_user/api/v1/login, sent the way the plugin's consent route expects; read routes.go and the smoke tests). +Never navigate to or capture the one-time client secret screen. Use only local throwaway data (the visitor, the example client). Read the oauthserver README for any config the local demo needs (issuer and so on) and set it through the local .env written by dev.sh, never in committed config that would reach production. + +content.go embeds the images (`Images embed.FS` over oidc/*.png and screens/*.webp). curated.go adds two OIDC slides per language: (a) consent and branded error side by side (`.cols`, img with bare file names and alt text), (b) the Clients admin. Their captions describe the standard flows (discovery, JWKS with rotation, id_token, userinfo, consent, pairwise subjects, import of an existing PHP issuer with its keys and secrets) without links. + +roadmap_deck.go writes every image a deck lists from content.Images into a temporary DeckDir (removed afterwards) and passes the names in Deck.Images. Because Import attaches images again on every run, the step reads the ids and blob keys of the deck's existing image attachments (attachment_id = deck id, field = models.PresentationImagesField) before the import. After a successful import it deletes those rows in one transaction and only then deletes their blobs (attach.DeleteKeys). A failed import leaves the old images in place. + + for f in consent error clients; do file -b ../sm-demo-app/plugins/demo/content/oidc/oidc-$f.png | grep -q '^PNG image data' || exit 1; done && go -C ../sm-demo-app vet . ./plugins/demo/... && go -C ../sm-demo-app test -count=1 -short ./plugins/demo/... && ../sm-demo-app/scripts/dev.sh present && ../sm-demo-app/scripts/dev.sh present && ../sm-demo-app/scripts/dev.sh snapshot roadmap roadmap-pl 2>&1 | grep -c '^snapshot: roadmap' | grep -qx 2 + Non-zero exit: a screenshot is missing or not a PNG; vet or the short suite red; present fails on either of the two runs; or fewer than two "snapshot: roadmap" lines (an OIDC or admin image with naturalWidth 0 fails the snapshot of its slide). + + + - After the two present runs, `docker exec sm-demo-pg psql -U summercms_demo summercms_demo -tAc "SELECT count(*) - count(DISTINCT (attachment_id, file_name)) FROM system_files WHERE attachment_id IN (SELECT id::text FROM golem15_presentations_presentations WHERE slug IN ('roadmap','roadmap-pl'))"` prints 0. + - `grep -c 'oidc-consent.png' ../sm-demo-app/plugins/demo/roadmap/curated.go` prints at least 2. + - `python3 -c "import struct,sys;d=open(sys.argv[1],'rb').read(24);print(struct.unpack('>I',d[16:20])[0])" ../sm-demo-app/plugins/demo/content/oidc/oidc-clients.png` prints a width of at most 1600. + + The migration section shows the Go OIDC provider as three real screenshots in both decks; re-seeding does not pile up image attachments. + + + + Task 4: The user checks the deck they present on 2026-10-21 + ../sm-demo-app/logs/deck/roadmap.pdf, ../sm-demo-app/logs/deck/roadmap-pl.pdf, the output of `scripts/dev.sh present` + + - The user replied "approved", or every change they listed has been applied, `scripts/dev.sh present` and `scripts/dev.sh snapshot` re-run green, and the user approved the result. + + The roadmap and sales deck in English (slug roadmap) and Polish (slug roadmap-pl), generated by demo:seed: roadmap done and next with client names filtered, framework and plugin features with URLs, migration from PHP with the OIDC screenshots, cooperation with pxpx, the MCP-controlled CMF. The local demo runs on 127.0.0.1:8096 and bm's viewer on 127.0.0.1:5173 (started by `scripts/dev.sh present`), and PDF fallbacks sit in ../sm-demo-app/logs/deck/. + + 1. Open the "deck roadmap" URL that `scripts/dev.sh present` printed (http://127.0.0.1:5173/prezentacje/{hash}), press F11 and step through with the arrow keys. Do the same with the "deck roadmap-pl" URL. + 2. Check the order: roadmap, features, migration from PHP, pxpx, future. Check that no client is named, the pxpx slide says what you want to say, the OIDC screenshots are legible, and the URLs on the feature slides are the ones you want to show. + 3. Optional speaker view: log in at http://127.0.0.1:5173/logowanie with the visitor email that present printed and the password from SUMMER_GOLEM15__DEMO__VISITOR__PASSWORD in ../sm-demo-app/.env, then open /moje-prezentacje/roadmap for the notes and the clock. Without Centrifugo the phone remote stays off until plan 04. + 4. Open logs/deck/roadmap.pdf and roadmap-pl.pdf as the offline fallback. + 5. For tomorrow: `scripts/dev.sh present` starts everything again, and `scripts/dev.sh down` stops it. + + Reply "approved", or list slide changes (slide number, language, new text). Claude edits curated.go or names.go, re-runs present and snapshot, and asks again. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| ROADMAP.md (internal planning text) to a deck shown to prospective clients | internal phase names include client applications | +| local .env to the seed | visitor credentials enter the database | +| local demo pages to committed screenshots | captured pages could show secrets or personal data | +| seeded deck HTML to the public deck API | rendered on a public origin after plan 02 | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-19.4.1-01 | Information disclosure | roadmap deck builder | high | mitigate | curated neutral titles per phase number, case-insensitive denylist applied after the overrides, leaking phases left out and reported; TestRoadmapTracerSmoke and TestRoadmapDeckSmoke assert zero leaks on the built decks; the user reviews the deck at Task 4 | +| T-19.4.1-02 | Information disclosure | visitor credentials | high | mitigate | email and password only from golem15.demo.visitor.* (env), hard-coded constants removed, .env written with umask 077, the password is never printed by seed or present (present names the .env key) | +| T-19.4.1-03 | Information disclosure | OIDC screenshots | medium | mitigate | the capture script never opens the one-time secret screen, uses only the local visitor and an example client, and the user checks the images at Task 4 | +| T-19.4.1-04 | Tampering | deck HTML | low | accept | seeded content is curated in the repository and every field passes classes.Clean at render; text values are HTML-escaped by the builder | +| T-19.4.1-05 | Denial of service | repeated seeding | low | mitigate | prior image attachments are deleted after a successful import and their blobs after commit, so uploads do not grow with each deploy | +| T-19.4.1-SC | Tampering | npm/pip/cargo installs | high | mitigate | no package is installed in this plan: Playwright 1.56.1 and bm's node_modules already exist; `npm ci` in bm reinstalls only its own lockfile when node_modules is missing | + + + +- Task 1: TestRoadmapTracerSmoke, a stable public_hash across two present runs, the English deck rendered by deck-snapshot.cjs in bm's viewer. +- Task 2: TestRoadmapDeckSmoke (section order, EN/PL parity, no leaks, no anchors, pxpx and MCP content), both decks rendered to PNG and PDF. +- Task 3: three PNG screenshots attached, no duplicate attachments after two seeds, images load in the viewer. +- Task 4: the user approves the deck. + + + +The user can present the roadmap deck in English or Polish on 2026-10-21 from `scripts/dev.sh present`, with a PDF fallback; demo:seed regenerates it from ROADMAP.md without breaking its link; no client name and no credential appears in it. + + + +Create `.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-01-SUMMARY.md` when done (record the theme chosen, the docs URLs checked, the left-out phases if any, and how the speaker view behaved without Centrifugo). + diff --git a/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-02-PLAN.md b/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-02-PLAN.md new file mode 100644 index 00000000..35d99442 --- /dev/null +++ b/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-02-PLAN.md @@ -0,0 +1,497 @@ +--- +phase: 19.4.1-early-public-demo-on-demo-summercms-io +plan: 02 +type: execute +wave: 2 +depends_on: ["19.4.1-01"] +files_modified: + - ../sm-paymentgatewaysubscriptions-plugin/go.mod + - ../sm-paymentgatewaysubscriptions-plugin/go.sum + - ../sm-pgstripe-plugin/go.mod + - ../sm-pgstripe-plugin/go.sum + - ../sm-demo-app/go.mod + - ../sm-demo-app/go.sum + - ../sm-demo-app/go.work + - ../sm-demo-app/plugins/demo/go.mod + - ../sm-demo-app/plugins/golem15/journal + - ../sm-demo-app/plugins/golem15/sitemap + - ../sm-demo-app/plugins/golem15/menu + - ../sm-demo-app/plugins/golem15/faq + - ../sm-demo-app/plugins/golem15/paymentgateway + - ../sm-demo-app/plugins/golem15/paymentgatewaysubscriptions + - ../sm-demo-app/plugins/golem15/pgstripe + - ../sm-demo-app/plugins/golem15/presentations + - ../sm-demo-app/plugins/golem15/translate + - ../sm-demo-app/plugins/golem15/oauthserver + - ../sm-demo-app/vue-demo-app + - ../sm-demo-app/plugins/demo/console/seed.go + - ../sm-demo-app/scripts/build.sh + - ../sm-demo-app/scripts/check-pins.sh + - ../sm-demo-app/scripts/smoke.sh + - ../sm-demo-app/scripts/check-deploy.sh + - ../sm-demo-app/scripts/check-live.sh + - ../sm-demo-app/scripts/dev.sh + - ../sm-demo-app/config/env/production/app.yaml + - ../sm-demo-app/config/env/production/http.yaml + - ../sm-demo-app/config/env/production/golem15.paymentgatewaysubscriptions.yaml + - ../sm-demo-app/deploy/env.example + - ../sm-demo-app/deploy/site.env.example + - ../sm-demo-app/deploy/nginx/demo.summercms.io.conf + - ../sm-demo-app/deploy/supervisor/summercms-demo.conf + - ../sm-demo-app/deploy/supervisor/summercms-demo-site.conf + - ../sm-demo-app/DEPLOY.md + - ../sm-demo-app/README.md + - ../sm-demo-app/vue-demo-app/nuxt.config.ts + - ../sm-demo-app/vue-demo-app/app/layouts/default.vue + - ../sm-demo-app/vue-demo-app/app/pages/index.vue + - ../sm-demo-app/vue-demo-app/app/pages/login.vue + - ../sm-demo-app/vue-demo-app/app/components/DemoBanner.vue + - ../sm-demo-app/vue-demo-app/app/components/BackendCta.vue + - ../sm-demo-app/vue-demo-app/app/assets/css/site.css +autonomous: false +requirements: [SC-1, SC-2, SC-3, D-02, D-03, D-04, D-05, D-06, D-08, D-10, D-16, D-21, D-23] +user_setup: + - service: dns + why: "demo.summercms.io must resolve to rome before certbot can issue its certificate (D-02)" + dashboard_config: + - task: "Create an A record (or a CNAME to summercms.io) for demo.summercms.io pointing at the address summercms.io resolves to" + location: "the DNS provider of summercms.io" + - service: stripe + why: "payments run in Stripe test mode on the demo (D-08)" + env_vars: + - name: SUMMER_GOLEM15__PGSTRIPE__TEST_SECRET_KEY + source: "the Stripe CLI test login on the build machine (stripe config --list, test_mode_api_key), used only after the go-ahead at Task 4" + - name: SUMMER_GOLEM15__PGSTRIPE__TEST_SUBSCRIPTION_WEBHOOK_SECRET + source: "the signing secret of the webhook endpoint Claude creates at Task 6 with the Stripe CLI" + - service: secrets-file + why: "the admin and visitor emails for rome, never pasted into the chat (D-06)" + env_vars: + - name: DEMO_ADMIN_EMAIL + source: "the user, in ~/.config/summercms/demo-secrets.env (mode 600)" + - name: DEMO_VISITOR_EMAIL + source: "the user, in the same file" +estimate: + tokens: 300000 + raw_tokens: 300000 + tasks: 7 + confidence: low +must_haves: + truths: + - "SC-1, D-03: ../sm-demo-app/go.mod requires git.golem15.com/golem15/summercms v0.3.0 and a tag of every shared plugin module in summer.yaml, carries no replace directive, and `GOWORK=off GOPRIVATE='git.golem15.com/*' go build` produces bin/demo for linux/amd64; plugins/demo is a package of the root module (its own go.mod is gone)." + - "D-03: sm-paymentgateway-plugin has its first tag, and sm-paymentgatewaysubscriptions-plugin and sm-pgstripe-plugin are re-tagged requiring that tag instead of the v0.0.0 placeholder; sitemap, menu, faq and oauthserver have first tags; translate and presentations have patch tags on their pushed heads. Every tag number is the next free one read from the remote at execution, every tag sits on a pushed commit, and no tag is reused." + - "SC-1 (RESEARCH Pitfall 2): scripts/check-pins.sh confirms that every plugin submodule's HEAD is the commit of the version go.mod requires." + - "D-21, SC-1: golem15/sm-demo-app and golem15/vue-demo-app exist as private repositories on git.golem15.com, and their remote master equals the local HEAD after the push (vue-demo-app pushed before the sm-demo-app pointer)." + - "SC-2, D-02: https://demo.summercms.io serves the Nuxt site (SSR under Node/supervisor) and the Go binary (supervisor) from rome, with its own PostgreSQL 15 database and role, a certbot certificate of its own and HSTS; DEPLOY.md covers one-time setup, build, upload, release and rollback." + - "D-04: nginx answers 404 for /backend and everything under it; /oauth, /.well-known and /api/ are not routed to Go; the site carries a 'Want to see the backend? Contact Golem15' call to action (linking to golem15.com until plan 03 turns it into the contact form)." + - "D-10: every page shows a banner saying this is a demo, data may be reset at any time, and payments use Stripe test mode with card 4242 4242 4242 4242." + - "D-05: no page pre-fills or shows a credential and no page links to the admin; the seeded FAQ and posts no longer name the admin location or a demo customer login." + - "D-08: the demo runs Stripe in test mode and Stripe's webhook endpoint points at https://demo.summercms.io/webhook/stripe/subscriptions." + - "D-16: scripts/build.sh release refuses a ROADMAP snapshot that differs from the framework's master, and every release step runs demo:seed, which refreshes the roadmap decks." + - "D-06: on rome the visitor and admin credentials exist only in /srv/summercms-demo/.env and a root-only file; DEPLOY.md and the repositories name the keys, never the values." + artifacts: + - path: "../sm-demo-app/scripts/build.sh" + provides: "release (GOWORK=off, linux/amd64, Nuxt .output) and dev builds" + contains: "GOWORK=off" + - path: "../sm-demo-app/scripts/check-pins.sh" + provides: "submodule HEAD equals required version check" + contains: "check-pins: ok" + - path: "../sm-demo-app/scripts/smoke.sh" + provides: "release binary and Nuxt output booted on postgres:15 with HTTP checks" + contains: "smoke: ok" + - path: "../sm-demo-app/scripts/check-deploy.sh" + provides: "nginx -t, supervisor parse and port consistency of deploy/" + contains: "check-deploy: ok" + - path: "../sm-demo-app/scripts/check-live.sh" + provides: "live HTTP checks of a deployed origin" + contains: "check-live: ok" + - path: "../sm-demo-app/deploy/nginx/demo.summercms.io.conf" + provides: "TLS vhost, /backend refusal, Go allow-list, Nuxt default" + contains: "location ^~ /backend" + - path: "../sm-demo-app/DEPLOY.md" + provides: "rome runbook: setup, build, upload, release, rollback, secrets by key name" + contains: "Rollback" + - path: "../sm-demo-app/vue-demo-app/app/components/DemoBanner.vue" + provides: "the D-10 banner" + contains: "4242 4242 4242 4242" + key_links: + - from: "../sm-demo-app/deploy/nginx/demo.summercms.io.conf" + to: "../sm-demo-app/deploy/supervisor/summercms-demo.conf" + via: "every Go proxy_pass port equals the serve --addr port; check-deploy.sh compares them" + pattern: "127\\.0\\.0\\.1:8098" + - from: "../sm-demo-app/scripts/build.sh" + to: "../sm-demo-app/scripts/sync-roadmap.sh" + via: "release runs sync-roadmap.sh --check before compiling (D-16)" + pattern: "sync-roadmap\\.sh --check" + - from: "../sm-demo-app/vue-demo-app/app/layouts/default.vue" + to: "../sm-demo-app/vue-demo-app/app/components/DemoBanner.vue" + via: "the default layout renders the banner above the header on every page" + pattern: "DemoBanner" + prohibitions: + - statement: "No secret value (keys, passwords, DSN password, Stripe keys, visitor or admin password) is committed in any repository or printed in a log the executor keeps." + status: unresolved + verification: judgment + - statement: "/backend is never reachable through nginx on demo.summercms.io." + status: unresolved + verification: automated + - statement: "No tag, push, repository creation or server change happens before the Task 4 go-ahead." + status: unresolved + verification: judgment + - statement: "go.mod of sm-demo-app holds no replace directive after Task 5." + status: unresolved + verification: automated + - statement: "The framework (summercms.go) is not changed or tagged by this plan." + status: unresolved + verification: judgment +--- + +## Phase Goal + +ROADMAP Goal (not in user-story format; no story is invented here): Clients can be shown a live demo at `demo.summercms.io` now, before the chat family lands: `sm-demo-app` with today's 16 shared plugins runs on rome, its public Nuxt site shows the plugins working through prepared demo accounts, and the admin backend stays private. + +This plan's slice (plan 2 of 5, wave 2): the release pipeline and the first live deploy. Plugins get real tags. The app builds from those tags without `go.work`. The demo's two repositories go to git.golem15.com as private repositories, and `https://demo.summercms.io` goes live on rome with the minimum public-site safety: the banner, the backend call to action, no admin link and no credentials. + +## Decisions taken in this plan (Claude's discretion and recorded assumptions) + +- **Module layout (RESEARCH Pattern 1):** `plugins/demo` folds into the root module (its `go.mod` is deleted). `go.mod` requires tags only. `go.work` stays committed for development (submodule `use` lines and the framework replace) and is ignored by release builds (`GOWORK=off`). +- **Tracer before tags:** Task 1 proves the module-mode release with the versions that already exist remotely: tags where a tag sits on the pushed head, pseudo-versions of pushed heads otherwise, plus the version-pinned paymentgateway placeholder fallback the research proved. That way the one-way tags of Task 5 are cut only after the build, the boot and the site are known to work. +- **Serving (discretion):** Nuxt runs as SSR under Node/supervisor (`summercms-demo-site`), like the summercms.io changelog. Every page is server-rendered; plan 04 turns the deck routes client-only. +- **nginx split (discretion):** an allow-list of Go prefixes. Everything else goes to Nuxt. `/backend` gets 404. `/oauth`, `/.well-known` and `/api/` are not routed to Go (plan 04 adds exactly `/api/realtime/token`, `/_demo/` and `/ws`). +- **Ports (assumption A2):** Go 127.0.0.1:8098, Nuxt 127.0.0.1:3098 (Centrifugo 8010 in plan 04), checked read-only on rome in Task 3; a taken port moves to the next free one and is recorded in DEPLOY.md. +- **Backend call to action now:** it links to https://golem15.com, the Golem15 link summercms.io already uses. Plan 03 points it at the feedback contact form (D-04). This is a working link, not a placeholder. +- **Server layout:** install root `/srv/summercms-demo` (root-owned; only `storage/` belongs to the service), system user `summercms-demo`, database and role `summercms_demo` on rome's PostgreSQL 15, Node from the path the summercms.io changelog program uses (assumption A8, checked in Task 3), certbot webroot lineage `demo.summercms.io` (bm precedent). Secrets are generated on rome and never leave it. The admin password goes to a root-only file. +- **Plugin tags:** additive changes get patch tags and new repositories `v0.1.0` (D-03). Each number is read from `git ls-remote --tags` at execution, because Phase 19.5 runs in parallel. No framework tag is cut (the framework is unchanged), so no changelog post. + +## Artifacts this phase produces (this plan) + +- Scripts in sm-demo-app: `scripts/build.sh [release|dev]`, `scripts/check-pins.sh`, `scripts/smoke.sh [binary]`, `scripts/check-deploy.sh`, `scripts/check-live.sh URL`. +- Deploy files: `deploy/nginx/demo.summercms.io.conf`, `deploy/supervisor/summercms-demo.conf`, `deploy/supervisor/summercms-demo-site.conf`, `deploy/env.example`, `deploy/site.env.example`; production overlay `config/env/production/{app,http,golem15.paymentgatewaysubscriptions}.yaml`. +- Runbook: `DEPLOY.md` (rewritten). +- Site components: `DemoBanner.vue`, `BackendCta.vue`. +- Tags (numbers read at execution): sm-paymentgateway-plugin first tag; sm-paymentgatewaysubscriptions-plugin and sm-pgstripe-plugin patch re-tags; first tags of sm-sitemap-plugin, sm-menu-plugin, sm-faq-plugin, sm-oauthserver-plugin; patch tags of sm-translate-plugin and sm-presentations-plugin. +- Remote repositories: git.golem15.com/golem15/sm-demo-app and golem15/vue-demo-app (private). +- Live: https://demo.summercms.io; rome supervisor programs `summercms-demo`, `summercms-demo-site`; nginx site `demo.summercms.io`; certbot lineage `demo.summercms.io`; database `summercms_demo`. + + +Ship sm-demo-app the way D-03 requires (tagged plugins, a go.mod of tags, a release build without go.work), put both demo repositories on git.golem15.com as private repositories (D-21), and bring https://demo.summercms.io live on rome (D-02) with the banner (D-10), the backend refusal and call to action (D-04), no credentials (D-05) and Stripe test mode (D-08). + +Purpose: SC-1, SC-2, the banner part of SC-3; the live origin later plans redeploy to. D-23: plan 02 of five (release and live tracer). +Output: commits in ../sm-paymentgatewaysubscriptions-plugin and ../sm-pgstripe-plugin (go.mod), ../sm-demo-app/vue-demo-app and ../sm-demo-app; tags and pushes after the go-ahead; a live deploy. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-CONTEXT.md +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-RESEARCH.md +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-PATTERNS.md +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-01-SUMMARY.md + +Repos: ../sm-demo-app (app), ../sm-demo-app/vue-demo-app (the app's Nuxt site; the submodule checkout on branch master with origin set is the one to edit; the sibling ../vue-demo-app is a stale duplicate at the same commit with no remote and is not touched), ../sm-paymentgatewaysubscriptions-plugin and ../sm-pgstripe-plugin (go.mod only), and the plugin repositories that only get tags. Plugin code is never edited under ../sm-demo-app/plugins/golem15/* (submodule mirrors); those paths appear here only as gitlink pointers in sm-demo-app. Repositories without a sibling checkout (sm-presentations-plugin) are tagged from a fresh clone in the session scratchpad. + +Rules: no co-author tags; stage by explicit path; plugin push first, then the pointer bump in the app as a separate commit; no push, tag, repository creation or server change before the Task 4 go-ahead (CLAUDE.md); rome is `ssh -p 2222 debian@10.8.3.118` with passwordless sudo; secrets are generated on rome and never echoed into logs or the chat; `GOPRIVATE='git.golem15.com/*'` for every module-mode command (anonymous HTTPS go-get works, research). + + +Analogs (copy structure, not names): ../sm-summercmsio-app/scripts/build.sh (release/dev modes, tool checks, refusal of uncommitted changes, DEPS checks), ../sm-summercmsio-app/scripts/check-deploy.sh (nginx -t in a temp prefix with certbot paths, python supervisor parse, port match, prints "check-deploy: ok"), ../sm-summercmsio-app/scripts/smoke.sh (postgres:15 container, migrate, serve, curl checks, the Nuxt output against the binary), ../sm-summercmsio-app/deploy/nginx/summercms.io.conf (lines 18-30 ACME and redirect, 50-67 TLS/HSTS/gzip, 72-74 /backend 404, 81-90 per-prefix Go proxy block, 124-135 location / to Nuxt), ../sm-summercmsio-app/deploy/supervisor/summercms-io.conf and summercms-io-changelog.conf (Go and Node programs), ../sm-summercmsio-app/deploy/env.example and changelog.env.example, ../sm-summercmsio-app/DEPLOY.md (sections Overview, Launch checklist, One-time server setup, Build, Upload, Release, Cutover, Verify after cutover, Rollback). +Current state: ../sm-demo-app/go.mod has no require and replaces the framework with ../summercms.go; go.work uses ., ./plugins/demo and 11 plugin submodules and replaces summercms, paymentgateway, pgsub and user; plugins/demo/go.mod is a nested module; config/app.yaml debug true and url 127.0.0.1:8097; config/http.yaml allows every origin; vue-demo-app layouts/default.vue and pages/index.vue link to the admin through a runtime config key and pages/login.vue pre-fills the old customer account; seed.go FAQ "home" names the admin path and post "subscriptions-with-stripe" tells visitors to log in as the demo customer. +RESEARCH tag matrix (re-read with git ls-remote at execution): paymentgateway none (head 16484ba), pgsub v0.1.1 (requires paymentgateway v0.0.0), pgstripe v0.1.1 (requires paymentgateway v0.0.0 and pgsub v0.1.1), sitemap none (46898c2), menu none (f7eddd5), faq none (2efe351), oauthserver none (0deb9a4), translate v0.1.1 with one untagged commit (54cf93b), presentations v0.1.0 with six untagged commits (b445da7), user v0.1.2 (4c5d3b8), journal v0.2.0 (05e38ae), quizzes, golem and feedback v0.1.0 at their heads. +Stripe events the webhook needs (scripts/dev.sh stripe_setup): customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, invoice.paid, invoice.payment_failed. + + + + + + + Task 1: Tracer: a release build compiled without go.work boots on a fresh PostgreSQL 15, seeds the demo and serves its APIs and the Nuxt home + ../sm-demo-app/plugins/demo/go.mod, ../sm-demo-app/go.mod, ../sm-demo-app/go.sum, ../sm-demo-app/go.work, ../sm-demo-app/plugins/golem15/journal, ../sm-demo-app/plugins/golem15/sitemap, ../sm-demo-app/plugins/golem15/oauthserver, ../sm-demo-app/plugins/golem15/paymentgatewaysubscriptions, ../sm-demo-app/plugins/golem15/pgstripe, ../sm-demo-app/scripts/build.sh, ../sm-demo-app/scripts/check-pins.sh, ../sm-demo-app/scripts/smoke.sh, ../sm-demo-app/README.md + ../sm-demo-app/go.mod, ../sm-demo-app/go.work, ../sm-demo-app/plugins/demo/go.mod, ../sm-demo-app/summer.yaml, ../sm-demo-app/main.go, ../sm-demo-app/boot_test.go, ../sm-demo-app/scripts/dev.sh, ../sm-demo-app/scripts/sync-roadmap.sh (plan 01), ../sm-summercmsio-app/scripts/build.sh, ../sm-summercmsio-app/scripts/smoke.sh, 19.4.1-RESEARCH.md (Module-mode build probe, Pattern 1, Pitfalls 1 and 2) + Per D-03, SC-1, D-16. No tag, push or server change in this task. + +- Fold plugins/demo into the root module: delete plugins/demo/go.mod and drop ./plugins/demo from the go.work use list. Import paths stay git.golem15.com/golem15/sm-demo-app/plugins/demo/...; summer.yaml keeps its entry unless `summer build` rejects it (then adjust it and record why). +- go.mod requires git.golem15.com/golem15/summercms v0.3.0 and every shared plugin module named in summer.yaml (one require per module; pgstripe covers both of its plugin ids) at the version its pushed remote head resolves to: the latest tag when that tag's commit is the remote head, else the pseudo-version of the remote head (`GOWORK=off GOPRIVATE='git.golem15.com/*' go list -m -json module@commit`). The only replace allowed in this task is the version-pinned paymentgateway placeholder fallback from the research probe, with a comment that Task 5 removes it. No local-path replace: go.work keeps the framework replace for development. Run `GOWORK=off GOPRIVATE='git.golem15.com/*' go mod tidy` and commit go.sum. +- Move each plugin submodule whose HEAD differs from the required commit to that commit (journal, sitemap, oauthserver, pgsub and pgstripe lag today). These are pushed commits; check out the commit inside the submodule and commit the gitlinks in sm-demo-app. +- scripts/check-pins.sh: for each submodule under plugins/golem15/, read its module path from its go.mod and the version the app requires (`GOWORK=off go list -m -f '{{.Version}}' module` in the app). Resolve the commit: for a tag, rev-parse the tag's commit after fetching tags; for a pseudo-version, take its 12-character hash. Compare with the submodule HEAD. Print one line per mismatch and exit 1, or print `check-pins: ok ({N} modules)`. +- scripts/build.sh, structured like the summercms.io one (set -euo pipefail, ROOT, die, usage, tool checks for go, pnpm, git, node, summer from $HOME/go/bin as fallback): + - `release` (the default) refuses uncommitted changes in the app and in vue-demo-app; + - it runs scripts/sync-roadmap.sh --check (D-16) and scripts/check-pins.sh; + - it runs `summer build` only to regenerate main.go and plugins.gen.go and refuses when they drift from the committed files; + - it builds `GOWORK=off GOPRIVATE='git.golem15.com/*' CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -o bin/demo .`; + - it builds the site with `pnpm --dir vue-demo-app install --frozen-lockfile` and `pnpm --dir vue-demo-app build` (vue-demo-app/.output); + - it prints `build: ok release {short sha256 of bin/demo}`; + - `dev` builds for the host with go.work and skips the refusals. +- scripts/smoke.sh [binary] (summercms.io pattern): + - starts postgres:15 in Docker (container sm-demo-smoke, a free port, removed on exit) and writes a temporary env file (generated app key, admin and user JWT secrets, a smoke visitor email and password); + - runs `migrate`, `admin:create` (password on stdin) and `demo:seed`, then serves on 127.0.0.1:${SMOKE_PORT:-18098}; + - checks 200 and JSON for /_journal/api/v1/posts and /_faq/api/v1/faqs/home, 200 for /sitemap.xml, and 200 for /_presentations/api/v1/public/{hash} of the roadmap deck (hash read with psql inside the container); + - starts vue-demo-app/.output/server/index.mjs with NITRO_HOST 127.0.0.1, NITRO_PORT ${SMOKE_SITE_PORT:-13098} and the NUXT_* API bases at the binary, and checks GET / for 200 and text/html; + - prints `smoke: ok`; each failed check prints its URL and status and exits 1. +- README.md: a Release section (build.sh release, check-pins.sh, smoke.sh) and the go.mod/go.work split. +Commit: module fold and go.mod; submodule pointers; scripts and README. + + GOWORK=off GOPRIVATE='git.golem15.com/*' go -C ../sm-demo-app vet . ./plugins/demo/... && go -C ../sm-demo-app test -count=1 -short . ./plugins/demo/... && ../sm-demo-app/scripts/check-pins.sh | tail -1 | grep -q '^check-pins: ok' && ../sm-demo-app/scripts/build.sh release | tail -1 | grep -q '^build: ok release' && ../sm-demo-app/scripts/smoke.sh 2>&1 | tail -1 | grep -qx 'smoke: ok' + Non-zero exit: module-mode vet fails (an unresolved or placeholder require), the workspace short suite is red, a submodule is off its required commit, build.sh refuses (uncommitted files, stale ROADMAP snapshot, generated-file drift) or does not print "build: ok release", or smoke.sh does not end with "smoke: ok" (a migration, the seed, an API route or the Nuxt home failed). + + + - `test -e ../sm-demo-app/plugins/demo/go.mod` fails (the nested module is gone). + - `grep -c 'plugins/demo' ../sm-demo-app/go.work` prints 0. + - `grep -c 'git.golem15.com/golem15/' ../sm-demo-app/go.mod` prints at least 16 (the module line plus the framework and 14 plugin requires). + - `file -b ../sm-demo-app/bin/demo | grep -c 'ELF 64-bit LSB executable, x86-64'` prints 1. + + A release build made from remote module versions, without go.work, boots, migrates, seeds and serves the APIs and the Nuxt home on a fresh database. + + + + Task 2: Public pages are safe for a public origin: a demo banner on every page, a backend call to action, no admin link and no credentials + ../sm-demo-app/vue-demo-app/nuxt.config.ts, ../sm-demo-app/vue-demo-app/app/layouts/default.vue, ../sm-demo-app/vue-demo-app/app/pages/index.vue, ../sm-demo-app/vue-demo-app/app/pages/login.vue, ../sm-demo-app/vue-demo-app/app/components/DemoBanner.vue, ../sm-demo-app/vue-demo-app/app/components/BackendCta.vue, ../sm-demo-app/vue-demo-app/app/assets/css/site.css, ../sm-demo-app/vue-demo-app, ../sm-demo-app/plugins/demo/console/seed.go, ../sm-demo-app/scripts/smoke.sh + ../sm-demo-app/vue-demo-app/nuxt.config.ts, ../sm-demo-app/vue-demo-app/app/layouts/default.vue, ../sm-demo-app/vue-demo-app/app/pages/index.vue, ../sm-demo-app/vue-demo-app/app/pages/login.vue, ../sm-demo-app/vue-demo-app/app/assets/css/site.css, ../sm-demo-app/plugins/demo/console/seed.go (seedFAQ, demoPosts), ../sm-demo-app/scripts/smoke.sh (Task 1) + Per D-10, D-04, D-05. + +- DemoBanner.vue, rendered by the default layout above the header on every page: "This is a demo. Data may be reset at any time. Payments use Stripe test mode: pay with card 4242 4242 4242 4242, any future date and any CVC." The text is English in this plan; plan 03 moves it to i18n keys and adds Polish. It is a non-dismissable strip with role="note", styled in site.css. +- BackendCta.vue: "Want to see the backend? Contact Golem15", linking to https://golem15.com (rel noopener, new tab), rendered on the homepage and in the footer. Plan 03 points it at the contact form. +- Remove the admin-URL runtime config key from nuxt.config.ts and its footer and homepage links. Keep the sitemap link and robots' disallow list. +- login.vue starts with empty fields and autocomplete="username" and "current-password". +- seed.go: rewrite the FAQ "home" answer about the admin to "The admin is private on this demo. Use the 'Want to see the backend?' link to ask Golem15 for a tour." Rewrite the subscriptions post so it tells readers to create an account and subscribe with the test card, without naming any account. Rewrite the last slide of the getting-started deck (it tells readers to open the admin) to point at the journal and the subscriptions page instead. Existing rows on old local databases keep their text (steps skip existing rows); fresh databases, including rome's, get the new text. +- smoke.sh, after the site boots: assert that the HTML of / and /login contains the banner sentence "Data may be reset at any time", the test card number and the call-to-action text, and that neither page nor /faq contains the admin path or the smoke visitor email. +Commit in vue-demo-app (local, unpushed until Task 5), then the vue-demo-app pointer and seed.go and smoke.sh in sm-demo-app. + + pnpm --dir ../sm-demo-app/vue-demo-app build && ../sm-demo-app/scripts/build.sh release | tail -1 | grep -q '^build: ok release' && ../sm-demo-app/scripts/smoke.sh 2>&1 | tail -1 | grep -qx 'smoke: ok' + Non-zero exit: the Nuxt build fails; build.sh refuses (an uncommitted change in vue-demo-app or a pointer not committed); or smoke.sh does not end with "smoke: ok" (banner, card number or call to action missing, or the admin path or the visitor email found in a page). + + + - `grep -rc 'adminUrl' ../sm-demo-app/vue-demo-app/nuxt.config.ts ../sm-demo-app/vue-demo-app/app | grep -v ':0$'` prints nothing. + - `grep -c 'DemoBanner' ../sm-demo-app/vue-demo-app/app/layouts/default.vue` prints at least 1. + - `grep -c '4242 4242 4242 4242' ../sm-demo-app/vue-demo-app/app/components/DemoBanner.vue` prints 1. + - `grep -c 'Contact Golem15' ../sm-demo-app/vue-demo-app/app/components/BackendCta.vue` prints 1. + + Every page tells visitors it is a demo with test payments, offers the backend tour contact, and shows no admin link or credential. + + + + Task 3: The rome deployment is written down and checked: nginx, supervisor, env templates, the production config overlay and the runbook + ../sm-demo-app/deploy/nginx/demo.summercms.io.conf, ../sm-demo-app/deploy/supervisor/summercms-demo.conf, ../sm-demo-app/deploy/supervisor/summercms-demo-site.conf, ../sm-demo-app/deploy/env.example, ../sm-demo-app/deploy/site.env.example, ../sm-demo-app/config/env/production/app.yaml, ../sm-demo-app/config/env/production/http.yaml, ../sm-demo-app/config/env/production/golem15.paymentgatewaysubscriptions.yaml, ../sm-demo-app/scripts/check-deploy.sh, ../sm-demo-app/scripts/check-live.sh, ../sm-demo-app/scripts/dev.sh, ../sm-demo-app/DEPLOY.md, ../sm-demo-app/README.md + ../sm-summercmsio-app/deploy/nginx/summercms.io.conf, ../sm-summercmsio-app/deploy/supervisor/summercms-io.conf, ../sm-summercmsio-app/deploy/supervisor/summercms-io-changelog.conf, ../sm-summercmsio-app/deploy/env.example, ../sm-summercmsio-app/deploy/changelog.env.example, ../sm-summercmsio-app/scripts/check-deploy.sh, ../sm-summercmsio-app/DEPLOY.md, ../sm-bm-app/deploy/nginx/bm.jakubzych.com.conf (certbot lineage of its own), ../sm-demo-app/DEPLOY.md, ../sm-demo-app/deploy/env.example, ../sm-demo-app/config/*.yaml, modules/compass/README.md (env overlay, SUMMER_ENV default production), 19.4.1-RESEARCH.md (rome deployment, Pitfalls 3, 4 and 6) + Per D-02, D-04, D-06, D-08, D-16. + +- Read-only probe of rome (`ssh -p 2222 debian@10.8.3.118`), changing nothing: + - `ss -ltnH` for 8098, 3098 and 8010 (a taken port moves to the next free one, used in every file below); + - the Node path in /etc/supervisor/conf.d/summercms-io-changelog.conf; + - `psql --version`, `certbot --version` and `ls /var/www/letsencrypt`; + - `id summercms-demo` (expected absent). + Record the results in DEPLOY.md. +- deploy/nginx/demo.summercms.io.conf, copied from the summercms.io file, with a header comment naming the install path, the upstream ports and the secrets source: + - port 80 serves the ACME webroot /var/www/letsencrypt and redirects everything else; + - 443 uses the certbot lineage demo.summercms.io, HSTS and gzip; + - `location ^~ /backend { return 404; }`; + - one `location ^~` Go block per prefix, proxied to 127.0.0.1:8098 with the four proxy_set_header lines: /_user/, /_journal/, /_menu/, /_faq/, /_paymentgateway/, /_paymentgatewaysubscriptions/, /_presentations/, /_quizzes/, /_feedback/, /webhook/stripe/ and /storage/app/uploads/public/, plus exact locations for /sitemap.xml and /sitemap.xsl; + - `location /` goes to Nuxt on 127.0.0.1:3098; + - no location for /oauth, /.well-known/ or /api/; client_max_body_size 2m. +- deploy/supervisor/summercms-demo.conf runs /srv/summercms-demo/bin/demo serve --addr 127.0.0.1:8098 as user summercms-demo with SUMMER_ENV="production" (summercms.io program settings). summercms-demo-site.conf runs node with --env-file=/srv/summercms-demo/site.env on /srv/summercms-demo/site/.output/server/index.mjs, with NODE_ENV production, NITRO_HOST 127.0.0.1 and NITRO_PORT 3098. +- deploy/env.example lists every key with an empty value and a generation hint: SUMMER_DATABASE__DSN, SUMMER_APP__KEY, SUMMER_ADMIN__JWT__SECRET, SUMMER_GOLEM15__USER__JWT__SECRET, the two PGSTRIPE test keys, SUMMER_GOLEM15__PAYMENTGATEWAY__HASHIDS_SALT, SUMMER_GOLEM15__DEMO__VISITOR__EMAIL and __PASSWORD (D-06), SUMMER_SITE_CACHE__PURGE_URL (http://127.0.0.1:3098/api/__site__/cache/purge) and its token key. deploy/site.env.example holds the NUXT_*_API_BASE values at 127.0.0.1:8098, NUXT_SITE_CACHE_PURGE_TOKEN and the public site URL key. +- Production overlay (selected by SUMMER_ENV): + - config/env/production/app.yaml sets debug false and url https://demo.summercms.io; + - http.yaml limits CORS to https://demo.summercms.io and keeps trusted_proxies 127.0.0.1/32; + - golem15.paymentgatewaysubscriptions.yaml sets the success, cancel and portal return URLs on https://demo.summercms.io/subscriptions. +- scripts/dev.sh exports SUMMER_ENV=local for every binary it runs, so production overlays never apply locally (Pitfall 4). +- scripts/check-deploy.sh (copied from summercms.io): + - nginx -t in a temporary prefix with self-signed files at the certbot paths; + - both supervisor files parse; + - every Go proxy_pass port equals the serve --addr port, and the Nuxt proxy_pass port equals NITRO_PORT; + - the /backend location returns 404, and no location routes /oauth or /api/; + - prints `check-deploy: ok`. +- scripts/check-live.sh URL: + - GET / answers 200 text/html containing "Data may be reset at any time" and a Strict-Transport-Security header; + - GET /backend and /backend/x answer 404; + - GET /_journal/api/v1/posts answers 200 JSON; + - GET /oauth/authorize does not reach Go (404); + - prints `check-live: ok`, else one line per failed check and exit 1. +- DEPLOY.md, rewritten in the summercms.io structure: + - Overview with layout, ports, programs and the Go/Nuxt split; + - one-time server setup: system user, database role and database, directories, generating .env and site.env on rome with openssl rand without echoing values, the port-80-only vhost first, then `certbot certonly --webroot -w /var/www/letsencrypt -d demo.summercms.io`, then the full vhost, the supervisor programs, admin:create with a generated password written to a root-only file; + - Stripe test mode: the webhook endpoint created with the Stripe CLI for the five events, the pgstripe settings and "Sync All to Stripe"; + - Build (build.sh release), Upload (rsync of bin/demo.new, config/ and site/.output.new); + - Release: migrate, then demo:seed, which refreshes the roadmap decks (D-16), then swap with .prev, restart, check-live; + - Rollback (.prev binary and .output; migrations are not reversed); + - Secrets: key names only, values never in git, visitor credentials only in .env (D-06). + README links to DEPLOY.md. + + ../sm-demo-app/scripts/check-deploy.sh 2>&1 | tail -1 | grep -qx 'check-deploy: ok' && bash -n ../sm-demo-app/scripts/check-live.sh && go -C ../sm-demo-app test -count=1 -short . ./plugins/demo/... + Non-zero exit: nginx -t fails, a supervisor file does not parse, a proxy_pass port differs from its program, /backend is not refused, /oauth or /api/ is routed (check-deploy does not print "check-deploy: ok"); check-live.sh has a syntax error; or the short suite is red after the overlay (the boot test loads committed config). + + + - `grep -c 'location \^~ /backend' ../sm-demo-app/deploy/nginx/demo.summercms.io.conf` prints 1. + - `grep -c 'proxy_pass http://127.0.0.1:3098;' ../sm-demo-app/deploy/nginx/demo.summercms.io.conf` prints 1 (or the probed Nuxt port). + - `grep -c 'SUMMER_ENV=local' ../sm-demo-app/scripts/dev.sh` prints at least 1. + - `grep -E '^SUMMER_[A-Z_]+=.+' ../sm-demo-app/deploy/env.example | grep -v '^SUMMER_SITE_CACHE__PURGE_URL='` prints nothing (every secret key has an empty value; the loopback purge URL is not a secret). + - DEPLOY.md contains the headings "One-time server setup", "Build", "Upload", "Release", "Rollback" and "Secrets" (`grep -c '^## ' ../sm-demo-app/DEPLOY.md` prints at least 6). + + The deploy files, the production overlay and the runbook describe exactly what Task 6 will do on rome, and check-deploy proves they are consistent. + + + + Task 4: Go-ahead for the one-way release: plugin tags, private repositories, pushes and the first deploy + The release table Claude prints before asking: per repository the current remote tags (`git ls-remote --tags`), the next free tag, the commit it will sit on, and why (new, go.mod fix, untagged commits); the two go.mod changes (pgsub, pgstripe); the probed rome ports; ../sm-demo-app/DEPLOY.md + + - The user chose one option. With "release-all", ~/.config/summercms/demo-secrets.env exists with mode 600 (`stat -c %a` prints 600), holds DEMO_ADMIN_EMAIL and DEMO_VISITOR_EMAIL, and `dig +short demo.summercms.io` answers with the address `dig +short summercms.io` answers. + + Cut the plugin tags, create the two private repositories, push, and deploy to rome now? + D-03 is rated one-way: a pushed Go module tag is cached by proxies and can never point anywhere else. Pushing tags and repositories, creating the Stripe webhook and changing rome all need your go-ahead (CLAUDE.md). Task 1 already proved the release build from the same commits, so the tags only freeze what was tested. The tag numbers come from the remote at this moment, because Phase 19.5 may tag sm-user-plugin in parallel (this plan does not tag the user plugin; plan 03 does). + + + + + + + For "release-all": (1) create the DNS record for demo.summercms.io (an A record to the address summercms.io uses, or a CNAME to summercms.io); (2) write ~/.config/summercms/demo-secrets.env (chmod 600) with DEMO_ADMIN_EMAIL=… and DEMO_VISITOR_EMAIL=…; (3) confirm that the demo may use the Stripe test account of your local Stripe CLI login. Do not paste secrets into the chat; passwords are generated on rome. + + Reply "release-all", "tags-only" or "hold", and for release-all confirm "dns ready" and "stripe ok" (or name another Stripe test account to use). + + + + Task 5: Tagged releases: every shared plugin the demo requires has a tag, go.mod requires only tags, submodules sit on the tag commits, and both demo repositories are private on git.golem15.com + D-03: pushed Go module tags are cached by module proxies and cannot be reused or moved. + ../sm-paymentgatewaysubscriptions-plugin/go.mod, ../sm-paymentgatewaysubscriptions-plugin/go.sum, ../sm-pgstripe-plugin/go.mod, ../sm-pgstripe-plugin/go.sum, ../sm-demo-app/go.mod, ../sm-demo-app/go.sum, ../sm-demo-app/plugins/golem15/sitemap, ../sm-demo-app/plugins/golem15/menu, ../sm-demo-app/plugins/golem15/faq, ../sm-demo-app/plugins/golem15/paymentgateway, ../sm-demo-app/plugins/golem15/paymentgatewaysubscriptions, ../sm-demo-app/plugins/golem15/pgstripe, ../sm-demo-app/plugins/golem15/presentations, ../sm-demo-app/plugins/golem15/translate, ../sm-demo-app/plugins/golem15/oauthserver, ../sm-demo-app/README.md + The Task 4 reply, ../sm-paymentgatewaysubscriptions-plugin/go.mod, ../sm-pgstripe-plugin/go.mod, ../sm-demo-app/go.mod (Task 1), ../sm-demo-app/scripts/check-pins.sh, 19.4.1-RESEARCH.md (Tag and release matrix, Tag order, Cross-phase collision) + Only with "release-all" or "tags-only". Per D-03, D-21, SC-1. + +Before each tag: +- `git fetch --tags` in the checkout; +- the working tree is clean apart from untracked go.work.sum; +- the commit to tag is the remote head (`git ls-remote origin refs/heads/{branch}`); +- the tag number is the next free one from `git ls-remote --tags origin`: first tags are v0.1.0, additive changes are the next patch. +Each tag is annotated with a one-line message, and after the push `git ls-remote origin "refs/tags/{tag}^{}"` must equal the commit. Never reuse or move a tag. On any failed push stop and report. + +Order: +1. sm-paymentgateway-plugin: tag its head. +2. sm-paymentgatewaysubscriptions-plugin: require the paymentgateway tag instead of the v0.0.0 placeholder (go get, `GOWORK=off GOPRIVATE='git.golem15.com/*' go mod tidy`, vet and `go test -short ./...` green), commit "build: require sm-paymentgateway-plugin {tag}", push, tag the next patch. +3. sm-pgstripe-plugin: the same, requiring both new tags. +4. sm-sitemap-plugin, sm-menu-plugin, sm-faq-plugin, sm-oauthserver-plugin: first tags on their heads. +5. sm-translate-plugin: the next patch on its head. +6. sm-presentations-plugin: the next patch on its head, from a fresh clone in the scratchpad. + +Then sm-demo-app: +- require every new tag, remove the paymentgateway placeholder fallback (go.mod ends with no replace directive), tidy, move every submodule to its tag commit, run check-pins, build release and smoke, and commit (go.mod and go.sum in one commit, the pointers in the next). +- Create the repositories with `tea repos create --owner golem15 --name vue-demo-app --private` and the same for sm-demo-app (D-21). Push vue-demo-app master first (its origin is already set). Then add origin git@git.golem15.com:golem15/sm-demo-app.git to sm-demo-app and push master. +- README.md names the remote and the tag rule. +Plugin tags need no changelog post (the framework is not tagged). + + ../sm-demo-app/scripts/check-pins.sh | tail -1 | grep -q '^check-pins: ok' && ! grep -q '=>' ../sm-demo-app/go.mod && ../sm-demo-app/scripts/build.sh release | tail -1 | grep -q '^build: ok release' && ../sm-demo-app/scripts/smoke.sh 2>&1 | tail -1 | grep -qx 'smoke: ok' && test "$(git -C ../sm-demo-app ls-remote origin refs/heads/master | cut -f1)" = "$(git -C ../sm-demo-app rev-parse HEAD)" && test "$(git -C ../sm-demo-app/vue-demo-app ls-remote origin refs/heads/master | cut -f1)" = "$(git -C ../sm-demo-app/vue-demo-app rev-parse HEAD)" + Non-zero exit: a submodule is not on its tag commit; go.mod still contains a replace arrow; the release build or the smoke run fails against the tags (for example a placeholder require left in pgsub or pgstripe); or either remote master differs from the local HEAD (a push failed or the repository was not created). + + + - `GOWORK=off GOPRIVATE='git.golem15.com/*' go -C ../sm-demo-app list -m -f '{{.Path}} {{.Version}}' all | grep '^git.golem15.com/golem15/' | grep -c -- '-0\.\|v0\.0\.0'` prints 0 (no pseudo-version or placeholder left). + - `grep -c 'v0.0.0' ../sm-paymentgatewaysubscriptions-plugin/go.mod ../sm-pgstripe-plugin/go.mod` prints 0 for both files. + - `tea repos ls --owner golem15 2>/dev/null | grep -E 'sm-demo-app|vue-demo-app'` lists both, and the web UI or `tea` shows them as private. + - The SUMMARY lists every tag created with its repository and commit. + + sm-demo-app builds from tags alone, its submodules match those tags, and both demo repositories are private and pushed. + + + + Task 6: demo.summercms.io is live on rome: TLS, nginx, supervisor, its own database, Stripe test mode, the seeded content and the roadmap decks + The Task 4 reply was "release-all" with "dns ready"; `dig +short demo.summercms.io` matches `dig +short summercms.io`; ~/.config/summercms/demo-secrets.env has mode 600. + ../sm-demo-app/DEPLOY.md, ../sm-demo-app/deploy/nginx/demo.summercms.io.conf + ../sm-demo-app/DEPLOY.md (Task 3), ../sm-demo-app/deploy/*, ../sm-demo-app/scripts/check-live.sh, ../sm-summercmsio-app/DEPLOY.md (One-time server setup, Release, Verify after cutover), ../sm-demo-app/scripts/dev.sh (stripe_setup: Stripe CLI config lookup, pgstripe settings update) + Per D-02, D-08, D-06, D-16. Follow DEPLOY.md step by step and correct it wherever reality differs (commit the corrections). + +- One-time setup on rome: + - system user summercms-demo; database role and database summercms_demo with a generated password; + - /srv/summercms-demo with bin/, config/, site/ (root) and storage/ (service); + - .env and site.env generated on rome from the examples (mode 0640 root:summercms-demo), with values from openssl rand and from the secrets file: visitor email from DEMO_VISITOR_EMAIL, visitor password generated (D-06); + - the Stripe test secret key read from the local Stripe CLI config and piped over ssh without echo; the site-cache purge token shared by both files. +- Stripe (D-08): + - create the webhook endpoint https://demo.summercms.io/webhook/stripe/subscriptions with the five events using the local Stripe CLI and write its signing secret into .env without printing it; + - after the first migrate, set the pgstripe settings row (enabled, test mode, test publishable key) with psql as dev.sh does. +- TLS: the port-80 vhost only, nginx -t, reload, `certbot certonly --webroot -w /var/www/letsencrypt -d demo.summercms.io`, the full vhost, nginx -t, reload. Install the supervisor programs. +- First release, from build.sh release output: upload bin/demo, config/ and vue-demo-app/.output; `migrate`; `admin:create` with the email from DEMO_ADMIN_EMAIL and a generated password written to /root/summercms-demo-admin.txt (0600, never printed); `demo:seed` (visitor, content, roadmap decks); start both programs. +- Trigger Subscription Plans "Sync All to Stripe" through an SSH tunnel to the admin (`ssh -p 2222 -N -L 18098:127.0.0.1:8098 debian@10.8.3.118`), using the admin API action the button calls (read the pgsub plugin's admin action). If that action cannot be scripted, leave it as step 1 of Task 7. +- Run check-live.sh against https://demo.summercms.io. No value from .env, site.env or the root-only file appears in the SUMMARY. + + ../sm-demo-app/scripts/check-live.sh https://demo.summercms.io 2>&1 | tail -1 | grep -qx 'check-live: ok' + Non-zero exit or no "check-live: ok" line: TLS not served, the banner missing, /backend reachable, the journal API not answering 200, HSTS absent, or /oauth/authorize reaching Go. + + + - `ssh -p 2222 debian@10.8.3.118 sudo supervisorctl status summercms-demo summercms-demo-site` shows both RUNNING. + - `ssh -p 2222 debian@10.8.3.118 sudo stat -c '%a %U:%G' /srv/summercms-demo/.env` prints `640 root:summercms-demo`. + - `ssh -p 2222 debian@10.8.3.118 sudo certbot certificates --cert-name demo.summercms.io` lists the lineage with a future expiry. + - `curl -s https://demo.summercms.io/_presentations/api/v1/public/{roadmap hash}` answers 200 (the hash read on rome with psql). + + The demo answers at https://demo.summercms.io from rome with its own database, certificate and programs, Stripe test mode wired, and fresh seeded content. + + + + Task 7: The user checks the first live release + ../sm-demo-app/DEPLOY.md, the check-live output of Task 6 + + - The user replied "approved", or the issues they listed are fixed, redeployed through the DEPLOY.md release steps, check-live is green again, and the user approved. + + https://demo.summercms.io on rome: Nuxt SSR and the Go binary under supervisor, its own PostgreSQL database, a certbot certificate, /backend refused, the demo banner and the backend call to action on every page, Stripe test mode with the webhook at demo.summercms.io, the seeded content and the two roadmap decks (viewable in a browser from plan 04 on; their JSON is live now). + + 1. Open https://demo.summercms.io: the banner is at the top, the "Want to see the backend? Contact Golem15" link is on the homepage and in the footer, and there is no admin link. Open /blog and /faq. + 2. Open https://demo.summercms.io/backend: you get 404. + 3. If Task 6 could not sync the plans: open an SSH tunnel (`ssh -p 2222 -N -L 18098:127.0.0.1:8098 debian@10.8.3.118`), log in at http://127.0.0.1:18098/backend with the admin email and the password in /root/summercms-demo-admin.txt on rome, and click Subscription Plans, then "Sync All to Stripe". + 4. Log in at /login as the visitor (DEMO_VISITOR_EMAIL; the password is SUMMER_GOLEM15__DEMO__VISITOR__PASSWORD in /srv/summercms-demo/.env on rome) and subscribe on /subscriptions with 4242 4242 4242 4242. After checkout the page shows the subscription (D-08). + + Reply "approved", or describe what is wrong. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| internet to nginx on demo.summercms.io | every request is untrusted; only listed prefixes reach Go | +| nginx to Go and Nuxt on loopback | trusted proxy (127.0.0.1/32) sets client IP headers | +| build machine to module proxies and git.golem15.com | tags become immutable inputs of every consumer | +| build machine to rome over SSH | secrets generated or piped, never logged | +| Stripe to the webhook | signed callbacks from the internet | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-19.4.1-06 | Elevation of privilege | admin exposure | high | mitigate | nginx `location ^~ /backend { return 404; }`, admin only through an SSH tunnel; check-deploy and check-live assert the 404 | +| T-19.4.1-07 | Information disclosure | secrets in repositories | high | mitigate | env.example and site.env.example hold key names only, values generated on rome (0640 root:service), admin password in a root-only file, repositories private (D-21); the SUMMARY records no value | +| T-19.4.1-08 | Tampering | one-way plugin tags | medium | mitigate | tags only on pushed heads that Task 1 built and booted; next free number read from the remote; the go-ahead at Task 4; ls-remote check after each push | +| T-19.4.1-09 | Spoofing | Stripe webhook | high | mitigate | pgstripe verifies the Stripe signature with the endpoint's signing secret, stored only in rome's .env | +| T-19.4.1-10 | Information disclosure | debug pages and permissive CORS | medium | mitigate | production overlay sets debug false and CORS to the demo origin; SUMMER_ENV production in supervisor, local runs export SUMMER_ENV=local | +| T-19.4.1-11 | Elevation of privilege | unintended Go routes (OAuth, PAT APIs, subscribe proxy) | medium | mitigate | nginx allow-list; /oauth, /.well-known and /api/ not routed; check-deploy fails if they are | +| T-19.4.1-12 | Denial of service | Nuxt site-cache purge route | medium | mitigate | purge URL on loopback only, token shared through the env files, /api/ not routed by nginx to Go and the layer refuses proxied purge requests | +| T-19.4.1-13 | Information disclosure | credentials on public pages | high | mitigate | login without pre-fill, no admin link, seeded FAQ and post rewritten; smoke.sh greps the rendered pages for the admin path and the visitor email | +| T-19.4.1-14 | Spoofing | TLS | medium | mitigate | own certbot lineage, HTTP redirected to HTTPS, HSTS asserted by check-live | +| T-19.4.1-15 | Denial of service | port collision with bm or summercms.io | low | mitigate | ports probed on rome before the files are final; check-deploy compares proxy_pass with the programs | +| T-19.4.1-SC | Tampering | npm/pip/cargo installs | high | mitigate | no new package: Go modules are the project's own tagged plugins (GOPRIVATE, go.sum committed); pnpm installs vue-demo-app's frozen lockfile only | + + + +- Task 1: module-mode vet, build release and smoke against remote versions. +- Task 2: Nuxt build; smoke asserts the banner, the call to action and the absence of the admin path and credentials. +- Task 3: check-deploy.sh green; DEPLOY.md complete. +- Task 4: go-ahead recorded. +- Task 5: check-pins, no replace, release build and smoke against tags, both remotes equal HEAD. +- Task 6: check-live against https://demo.summercms.io. +- Task 7: the user approves the live site and the test subscription. + + + +sm-demo-app is on git.golem15.com and builds from tags without go.work (SC-1); https://demo.summercms.io runs on rome with DEPLOY.md covering build, upload, release and rollback, /backend refused and the backend call to action shown (SC-2); every page carries the demo banner (SC-3, banner part). + + + +Create `.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-02-SUMMARY.md` when done (tags with commits, ports used, Node path, anything DEPLOY.md had to correct; no secret values). + diff --git a/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-03-PLAN.md b/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-03-PLAN.md new file mode 100644 index 00000000..6cf2f9b1 --- /dev/null +++ b/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-03-PLAN.md @@ -0,0 +1,534 @@ +--- +phase: 19.4.1-early-public-demo-on-demo-summercms-io +plan: 03 +type: execute +wave: 3 +depends_on: ["19.4.1-02"] +files_modified: + - ../fonoteka.go/plugins/golem15/user/routes.go + - ../fonoteka.go/plugins/golem15/user/config/config.yaml + - ../fonoteka.go/plugins/golem15/user/README.md + - ../fonoteka.go/plugins/golem15/user/turnstile_routes_smoke_test.go + - ../sm-journal-plugin/controllers/api/authors.go + - ../sm-journal-plugin/journal_api_authors_test.go + - ../sm-journal-plugin/README.md + - ../sm-demo-app/go.mod + - ../sm-demo-app/go.sum + - ../sm-demo-app/plugins/golem15/user + - ../sm-demo-app/plugins/golem15/journal + - ../sm-demo-app/vue-demo-app + - ../sm-demo-app/config/golem15.user.yaml + - ../sm-demo-app/plugins/demo/plugin.go + - ../sm-demo-app/plugins/demo/schedule.go + - ../sm-demo-app/plugins/demo/config/config.yaml + - ../sm-demo-app/plugins/demo/console/seed.go + - ../sm-demo-app/plugins/demo/console/seed_locales.go + - ../sm-demo-app/plugins/demo/console/reset.go + - ../sm-demo-app/plugins/demo/schedule_smoke_test.go + - ../sm-demo-app/plugins/demo/content/content.go + - ../sm-demo-app/plugins/demo/content/author/summercms-team.webp + - ../sm-demo-app/scripts/dev.sh + - ../sm-demo-app/scripts/check-local.sh + - ../sm-demo-app/scripts/check-live.sh + - ../sm-demo-app/deploy/env.example + - ../sm-demo-app/deploy/site.env.example + - ../sm-demo-app/DEPLOY.md + - ../sm-demo-app/README.md + - ../sm-demo-app/vue-demo-app/nuxt.config.ts + - ../sm-demo-app/vue-demo-app/i18n/locales/en.json + - ../sm-demo-app/vue-demo-app/i18n/locales/pl.json + - ../sm-demo-app/vue-demo-app/public/sun-logo.webp + - ../sm-demo-app/vue-demo-app/app/assets/css/tokens.css + - ../sm-demo-app/vue-demo-app/app/assets/css/base.css + - ../sm-demo-app/vue-demo-app/app/assets/css/site.css + - ../sm-demo-app/vue-demo-app/app/layouts/default.vue + - ../sm-demo-app/vue-demo-app/app/components/SiteHeader.vue + - ../sm-demo-app/vue-demo-app/app/components/SiteFooter.vue + - ../sm-demo-app/vue-demo-app/app/components/HeroSection.vue + - ../sm-demo-app/vue-demo-app/app/components/TourCard.vue + - ../sm-demo-app/vue-demo-app/app/components/LanguageSwitcher.vue + - ../sm-demo-app/vue-demo-app/app/components/DemoBanner.vue + - ../sm-demo-app/vue-demo-app/app/components/BackendCta.vue + - ../sm-demo-app/vue-demo-app/app/components/TurnstileWidget.vue + - ../sm-demo-app/vue-demo-app/app/components/UpdateBanner.vue + - ../sm-demo-app/vue-demo-app/app/components/Journal/AuthorCard.vue + - ../sm-demo-app/vue-demo-app/app/composables/useJournalApi.ts + - ../sm-demo-app/vue-demo-app/app/composables/useDemoAuth.ts + - ../sm-demo-app/vue-demo-app/app/utils/journalLocale.ts + - ../sm-demo-app/vue-demo-app/app/utils/buildUpdate.ts + - ../sm-demo-app/vue-demo-app/app/plugins/manifest-update.client.ts + - ../sm-demo-app/vue-demo-app/app/pages/index.vue + - ../sm-demo-app/vue-demo-app/app/pages/register.vue + - ../sm-demo-app/vue-demo-app/app/pages/login.vue + - ../sm-demo-app/vue-demo-app/app/pages/faq.vue + - ../sm-demo-app/vue-demo-app/app/pages/contact.vue +autonomous: false +requirements: [SC-2, SC-3, SC-4, D-03, D-04, D-05, D-07, D-09, D-11, D-12, D-13, D-22, D-23] +user_setup: + - service: cloudflare-turnstile + why: "register and login are protected by Turnstile on demo.summercms.io (D-07)" + env_vars: + - name: SUMMER_TURNSTILE__SECRET + source: "Cloudflare dashboard, Turnstile, a widget for demo.summercms.io (secret key), written by the user into ~/.config/summercms/demo-secrets.env" + - name: NUXT_PUBLIC_TURNSTILE_SITE_KEY + source: "the same widget's site key, same file" + dashboard_config: + - task: "Add a Turnstile widget (managed mode) for the hostname demo.summercms.io" + location: "Cloudflare dashboard, Turnstile" + - service: g15office + why: "contact submissions become G15Office tasks (D-04; no mail is sent)" + env_vars: + - name: SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__BASE_URL + source: "the G15Office instance URL, in ~/.config/summercms/demo-secrets.env" + - name: SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__TOKEN + source: "a G15Office API token for the project that receives demo contact requests, same file" + - name: SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__PROJECT + source: "that project's id, same file" +estimate: + tokens: 360000 + raw_tokens: 360000 + tasks: 8 + confidence: low +must_haves: + truths: + - "D-07: sm-user-plugin has an opt-in key golem15.user.turnstile (empty by default, open or closed; any other value fails boot naming the key) that adds turnstile:{mode} to POST /_user/api/v1/register and /login only; the demo sets it to closed, so a register or login request without a valid turnstile_token is refused (422 with a secret, 503 in production without one) on top of the existing throttle:user-api and login attempt limits; every other application keeps its behaviour because its config does not set the key." + - "D-05: visitors register themselves on /register and log in on /login, both rendering the Cloudflare Turnstile widget; neither page nor any other page shows or pre-fills a credential; there is no email verification step in the demo flow." + - "D-11: the homepage is a guided tour: a hero, then six cards (journal, presentations, quizzes, payments, FAQ, accounts), each with one sentence and a link to try it, then the backend call to action; the card is labelled Payments (PL Płatności), never Subscriptions; a card links only to a page that exists in the build (plan 04 adds the presentations and quizzes pages)." + - "D-12: header, footer, palette and type match summercms.io: tokens.css and base.css copied from vue-summercmsio-app, the sun logo, the header and footer structure." + - "D-13: a language switcher sets English or Polish; the UI strings, the banner, the call to action, the tour, the FAQ, the menu and the journal posts render in the chosen language on the server (SSR) and in the browser; demo:seed writes the Polish FAQ row, Polish menu titles and Polish post translations through the translate plugin." + - "Todo esrp4qtx: GET /_journal/api/v1/authors/{author} returns subtitle, description and avatar (a URL or null) besides the existing keys; the demo's posts have the SummerCMS Team author, whose card shows these fields on the post and on /blog/author/{login}." + - "D-04 (revised 2026-10-20): the backend call to action opens /contact, which posts to the golem15.feedback API; each submission appears in Admin, Feedback and is filed as a G15Office task; no mail is sent." + - "D-09: demo:reset --force wipes and reseeds the demo data (frontend users with their tokens, subscriptions, orders and quiz attempts, journal posts and categories, FAQ, menu, quizzes, presentations, feedback submissions and their uploads) and keeps admins, subscription plans, settings, locales, the sitemap and the roadmap decks' public_hash; its schedule is golem15.demo.reset.enabled (false by default) and golem15.demo.reset.at (04:00)." + - "D-22: an open tab of the site shows a non-blocking 'A new version is available, reload' banner once a newer build is deployed (Nuxt app manifest), and reloads only when the user clicks." + - "D-03: sm-user-plugin and sm-journal-plugin get the next free patch tag after their additive changes; the demo requires those tags and its submodules sit on them; other applications' pins are not touched." + artifacts: + - path: "../fonoteka.go/plugins/golem15/user/routes.go" + provides: "opt-in turnstile middleware on register and login" + contains: "turnstile:" + - path: "../sm-journal-plugin/controllers/api/authors.go" + provides: "subtitle, description and avatar on the author payload" + contains: "subtitle" + - path: "../sm-demo-app/plugins/demo/console/reset.go" + provides: "demo:reset --force" + contains: "demo:reset" + - path: "../sm-demo-app/plugins/demo/schedule.go" + provides: "the disabled-by-default reset schedule" + contains: "DailyAt" + - path: "../sm-demo-app/vue-demo-app/app/components/TurnstileWidget.vue" + provides: "client-only Turnstile widget emitting the token" + contains: "turnstile.render" + - path: "../sm-demo-app/vue-demo-app/app/pages/contact.vue" + provides: "the contact form posting to the feedback API" + contains: "_feedback/api/v1" + - path: "../sm-demo-app/vue-demo-app/app/plugins/manifest-update.client.ts" + provides: "the reload-on-deploy banner trigger" + contains: "app:manifest:update" + - path: "../sm-demo-app/scripts/check-local.sh" + provides: "named end-to-end checks against the local stack" + contains: "check-local: ok" + key_links: + - from: "../sm-demo-app/config/golem15.user.yaml" + to: "../fonoteka.go/plugins/golem15/user/routes.go" + via: "golem15.user.turnstile: closed selects the middleware the plugin appends" + pattern: "turnstile: *closed" + - from: "../sm-demo-app/vue-demo-app/app/composables/useJournalApi.ts" + to: "../sm-demo-app/vue-demo-app/app/utils/journalLocale.ts" + via: "the app override maps translations[locale] over the post fields before the layer's views drop them" + pattern: "translations" + - from: "../sm-demo-app/plugins/demo/plugin.go" + to: "../sm-demo-app/plugins/demo/schedule.go" + via: "Schedule() returns the demo:reset entry only when golem15.demo.reset.enabled" + pattern: "reset\\.enabled" + prohibitions: + - statement: "No mail is sent for a contact submission (no listener, job or mailer added for it)." + status: unresolved + verification: judgment + - statement: "No page shows or pre-fills a credential; no page links to the admin." + status: unresolved + verification: automated + - statement: "Other applications' sm-user-plugin and sm-journal-plugin pins are not changed by this plan." + status: unresolved + verification: judgment + - statement: "No tag, push or deploy happens before the Task 6 go-ahead." + status: unresolved + verification: judgment +--- + +## Phase Goal + +ROADMAP Goal (not in user-story format; no story is invented here): Clients can be shown a live demo at `demo.summercms.io` now, before the chat family lands: `sm-demo-app` with today's 16 shared plugins runs on rome, its public Nuxt site shows the plugins working through prepared demo accounts, and the admin backend stays private. + +This plan's slice (plan 3 of 5, wave 3): accounts, the guided tour, English and Polish, and the journal. Visitors register and log in behind Turnstile. The homepage tours the features in the summercms.io look, in both languages. The journal shows translated posts with author cards. The backend call to action becomes a contact form. A disabled reset job and the reload-on-deploy banner close the plan, followed by tags and a redeploy. + +## Decisions taken in this plan (Claude's discretion and recorded assumptions) + +- **Turnstile wiring (RESEARCH Pattern 5):** an opt-in key in sm-user-plugin, edited in its canonical checkout `../fonoteka.go/plugins/golem15/user` (the same path the Phase 19.5 plans use). The plugin requires summercms v0.1.9, whose surf does not know the `turnstile:*` names, so its smoke test records the routes with a recording `pact.Router` instead of building a router; the end-to-end refusal is checked in the demo (framework v0.3.0) by `check-local.sh turnstile` and, in plan 05, by a unit test in sm-demo-app. The plugin's framework requirement stays as it is. +- **Demo setting:** `config/golem15.user.yaml` sets `turnstile: closed` for every environment. Local runs use Cloudflare's always-pass test keys (secret `1x0000000000000000000000000000000AA`, site key `1x00000000000000000000AA`, written to the local .env by dev.sh); rome uses the real widget keys from the secrets file. +- **Local testing of unpushed plugin commits:** in the demo's submodule mirror, `git fetch {canonical checkout} {branch}` and `git checkout --detach FETCH_HEAD`. The gitlink is committed only after the plugin commit is pushed and tagged (Task 7). Plugin code is edited only in the canonical checkouts. +- **i18n (discretion within D-13):** `strategy: 'no_prefix'` with the locale in a cookie (`detectBrowserLanguage.useCookie`, the module's default cookie name), so SSR renders the chosen language and the layer routes keep their paths. FAQ and menu get `locale` from the active locale. Journal posts go through an app-level `useJournalApi` override that copies `translations[locale]` (title, excerpt, content_html) over the defaults before the layer's views drop the map (RESEARCH, assumption A4). + - **Fallback if the override does not take precedence:** the same mapping is added opt-in (default off) to `../vue-journal-layer` as its next minor tag, and vue-demo-app's pin moves to it. That tag goes through the Task 6 go-ahead, and the executor records it as a deviation. grzybyfunkcjonalne and summercms.io keep v0.5.0. +- **Seeded translations are non-destructive:** the seed writes a Polish translation, an author or a Polish FAQ/menu row only where none exists, so deploys never overwrite admin edits (RESEARCH Pitfall 10). +- **Author:** the seed creates a backend user "SummerCMS Team" (login `summercms-team`, not a superuser, no role, a random unusable password that is never stored or printed). Its metadata has subtitle "Golem15" and a short description, and its avatar is the summercms.io sun badge. The demo posts get it as `user_id`. +- **Contact (D-04 revised):** a custom page, not the feedback widget script. The form fields are name, email (required in the form), message and a hidden type "other"; page_url is the current URL. The feedback settings (enabled, allowed origins, widget key) are written by demo:seed from `golem15.demo.feedback.*` config, so DEPLOY.md stays scriptable. The widget key is public by design (Origin allow-list plus per-IP throttles). +- **Tour links:** a card renders its "try it" link only when the target path resolves to a page in the current build, so the presentations and quizzes cards show their links once plan 04 adds those pages, and the live site never links to a 404. +- **Reset (discretion):** a console command `demo:reset --force` run in-process by conga's leader-elected scheduler through `pact.HasSchedule` when enabled. `schedule:run --once` under cron is the documented alternative. An invalid `reset.at` fails boot naming the key. +- **Reload on deploy (D-22):** Nuxt's app manifest (`experimental.checkOutdatedBuildInterval` 5 minutes, hook `app:manifest:update`) plus a re-check of `builds/latest.json` on `visibilitychange`. No service worker and no Pinia. + + +Detector signal: "chosen" (a configurable reset job; also the visitor credentials and the Turnstile mode move from constants to config). +Primary noun: the demo data set that `demo:seed` defines; the reset schedule, the visitor credentials and the Turnstile mode are operator configuration of that one instance, not new identities. +Decision: no-change. Rationale: there is still exactly one demo instance and one seed definition; making a schedule and a mode configurable does not add a second variant of any modelled entity. + + +## Artifacts this phase produces (this plan) + +- sm-user-plugin: config key `golem15.user.turnstile` (`""` | `open` | `closed`), smoke test `TestTurnstileRoutesSmoke`, README Configuration row; tag (next free patch). +- sm-journal-plugin: author payload keys `subtitle`, `description`, `avatar`; README; `TestJournalAuthorProfile` key set updated; tag (next free patch). +- golem15.demo: command `demo:reset` (flag `force`), schedule entry `demo:reset --force` (daily at `golem15.demo.reset.at` when `golem15.demo.reset.enabled`), config keys `golem15.demo.reset.enabled`, `golem15.demo.reset.at`, `golem15.demo.feedback.widget_key`, `golem15.demo.feedback.allowed_origins`; seed steps for the team author, Polish FAQ, menu and post translations, feedback settings; smoke test `TestResetScheduleSmoke`. +- vue-demo-app: pages `/register`, `/contact`, the tour on `/`; components `SiteHeader`, `SiteFooter`, `HeroSection`, `TourCard`, `LanguageSwitcher`, `TurnstileWidget`, `UpdateBanner`, `Journal/AuthorCard` (override); composable override `useJournalApi`; utils `journalLocale.ts` (`applyJournalLocale`), `buildUpdate.ts` (`isNewerBuild`); plugin `manifest-update.client.ts`; locale files `i18n/locales/{en,pl}.json`; runtime keys `public.turnstileSiteKey`, `public.feedbackKey`. +- Scripts: `scripts/check-local.sh [turnstile|tour|journal|contact|reset]...`; check-live.sh gains the register, contact and Polish checks. + + +Let visitors register and log in behind Turnstile (D-07, D-05), tour every feature from a summercms.io-styled homepage (D-11, D-12) in English or Polish (D-13), read translated journal posts with author cards (todo esrp4qtx), ask Golem15 for a backend tour through a contact form that files G15Office tasks (D-04), give the operator a disabled reset job (D-09), prompt open tabs to reload after a deploy (D-22), then tag the two changed plugins and redeploy. + +Purpose: SC-3 (registration, Turnstile, reset job), SC-4 (tour, look, EN/PL), the D-04 contact form of SC-2; folded todos esrp4qtx and ltcvqext (site half). D-23: plan 03 of five. +Output: commits in ../fonoteka.go/plugins/golem15/user, ../sm-journal-plugin, ../sm-demo-app/vue-demo-app and ../sm-demo-app; two plugin tags and pushes after the go-ahead; a redeploy. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-CONTEXT.md +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-RESEARCH.md +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-PATTERNS.md +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-02-SUMMARY.md +@.planning/todos/pending/journal-author-profile-fields.md +@.planning/todos/pending/2026-10-11-reload-open-apps-when-a-new-version-is-deployed.md + +Repos: ../fonoteka.go/plugins/golem15/user (canonical sm-user-plugin checkout: switch to branch master and `pull --ff-only` first; Phase 19.5 also plans changes here, so finish and tag this change first and let 19.5 read the next free tag), ../sm-journal-plugin, ../sm-demo-app/vue-demo-app, ../sm-demo-app. Read-only: ../sm-summercmsio-app/vue-summercmsio-app (look), ../vue-journal-layer, ../vue-menu-layer, ../vue-faq-layer (layer APIs), ../fonoteka.go/plugins/golem15/feedback (feedback API and settings), ../sm-translate-plugin (SetTranslated). + +Rules: no co-author tags; stage by path; a change to a plugin's config keys or API updates its README in the same commit; plugin push first, then the pointer bump in the app as a separate commit; no push, tag or deploy before the Task 6 go-ahead; sm-user-plugin changes are additive and opt-in (fonoteka, bm, grzybyfunkcjonalne and summercms.io consume it). + + +sm-user-plugin routes.go:9-30: `r.Group("/_user/api/v1", surf.Use("throttle:user-api"), func(g pact.Router) { g.Post("/login", controllers.Login(p.app)); ... g.Post("/register", controllers.Register(p.app)); ... })`; per-route middleware is the trailing variadic argument (`g.Post("/tokens", controllers.APITokenStore(p.app), "jwt.auth")`). Registration reads a fields map (controllers/registration.go RegisterUser), so an extra turnstile_token field is ignored by the controller. +framework v0.3.0 surf: middleware `turnstile:open` / `turnstile:closed`, secret `turnstile.secret` (SUMMER_TURNSTILE__SECRET), token field `turnstile.field` default `turnstile_token` (query, JSON or form); closed answers 503 in production without a secret and 422 for a bad token (surf README lines 30, 165-166). +sm-journal-plugin controllers/api/authors.go: `authorColumns` (line 23), `authorRow` (28), `findAuthorRow` (67), `authorProfile(row) map[string]any` (82); test `TestJournalAuthorProfile` (journal_api_authors_test.go:91) pins the exact key set. Framework: `cabana.BackendUser.Subtitle()`, `cabana.BackendUserAvatar(ctx, db, userID)` (in every tag since v0.1.15; the plugin requires v0.2.0); `backend_users.metadata` JSON holds subtitle (and description for this todo). +translate: `translate.SetTranslated(ctx, db *gorm.DB, m Translatable, field, locale string, value any) error` (sm-translate-plugin/classes/translatable.go:177); journal `models.Post` Translatable fields title, content, content_html, excerpt, metadata, slug (keep slug untranslated); usage analog ../sm-journal-plugin/console/import_posts.go:315-335. +feedback: `POST /_feedback/api/v1/{key}/submit` multipart (message required up to 5000, type bug|feature|other, email optional, page_url required, screenshot optional), Origin allow-list (empty list refuses everything), throttled 10 a minute per IP; Settings row fields enabled, widget_key, allowed_origins (models/settings.go); G15Office env SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__{BASE_URL,TOKEN,PROJECT}. +Layers: `useFaq(slug, { locale })`, `useMenu(code, { locale })`, `MenuNav` prop `locale`; vue-journal-layer `useJournalApi()` (app/composables/useJournalApi.ts:10), `Journal/AuthorCard.vue` (an app may override it or fill its slot), `app/utils/view.ts:11` and `postView.ts:17` drop `translations`; journal routes in nuxt.config `routes.author: false` today. +pact: `ScheduledCommand{Command, Args, Cadence}`, `DailyAt(hour, minute)`, `HasSchedule`; analog ../sm-oauthserver-plugin/schedule.go and plugin.go:34,105. attach: `BlobKeys`, `DeleteKeys` (two-phase delete after commit, modules/lagoon/attach/file.go:113-188). +summercms.io look: ../sm-summercmsio-app/vue-summercmsio-app/app/assets/css/{tokens,base}.css, app/components/{SiteHeader,SiteFooter,HeroSection,FeaturesSection}.vue, public/{sun-logo,sun-badge}.webp, nuxt.config.ts fonts. + + + + + + + Task 1: Tracer: a visitor registers on the demo through the Turnstile widget and is logged in; without a token the API refuses + ../fonoteka.go/plugins/golem15/user/routes.go, ../fonoteka.go/plugins/golem15/user/config/config.yaml, ../fonoteka.go/plugins/golem15/user/README.md, ../fonoteka.go/plugins/golem15/user/turnstile_routes_smoke_test.go, ../sm-demo-app/config/golem15.user.yaml, ../sm-demo-app/scripts/dev.sh, ../sm-demo-app/scripts/check-local.sh, ../sm-demo-app/vue-demo-app/nuxt.config.ts, ../sm-demo-app/vue-demo-app/app/components/TurnstileWidget.vue, ../sm-demo-app/vue-demo-app/app/pages/register.vue, ../sm-demo-app/vue-demo-app/app/pages/login.vue, ../sm-demo-app/vue-demo-app/app/composables/useDemoAuth.ts + ../fonoteka.go/plugins/golem15/user/routes.go, ../fonoteka.go/plugins/golem15/user/config/config.yaml, ../fonoteka.go/plugins/golem15/user/README.md (Configuration), ../fonoteka.go/plugins/golem15/user/controllers/registration.go, modules/surf/README.md (lines 25-35, 160-170), modules/bouncer/README.md (Turnstile), ../sm-bm-app/frontend/src/views/RegisterView.vue and LoginView.vue, ../sm-demo-app/vue-demo-app/app/pages/login.vue, ../sm-demo-app/vue-demo-app/app/composables/useDemoAuth.ts, ../sm-demo-app/vue-demo-app/app/utils/authCookie.ts, ../sm-demo-app/scripts/dev.sh, 19.4.1-RESEARCH.md (Pattern 5, Code Examples: Turnstile, Pitfall 4) + Per D-07, D-05. + +- sm-user-plugin (canonical checkout, branch master): + - config/config.yaml gains `turnstile: ""` with a comment: empty is off, `open` lets requests pass when Cloudflare is unreachable, `closed` refuses them; the middleware comes from the framework, from v0.3.0 on. + - routes.go appends "turnstile:" plus the mode to the /login and /register routes only, when the key is open or closed. An empty value adds nothing. Any other value makes Routes return an error naming golem15.user.turnstile. + - README Configuration gets the row in the same commit. + - turnstile_routes_smoke_test.go `TestTurnstileRoutesSmoke` uses a recording pact.Router (it records method, path and middleware of each registration). Empty: no route carries a turnstile middleware. Closed: exactly /login and /register carry turnstile:closed and the other routes are unchanged. "bogus": Routes returns an error mentioning the key. + - Commit locally; do not push (Task 7). +- Demo: + - config/golem15.user.yaml sets `turnstile: closed` (D-07); + - dev.sh writes the Cloudflare always-pass test secret into SUMMER_TURNSTILE__SECRET, plus NUXT_PUBLIC_TURNSTILE_SITE_KEY with the test site key, into .env when missing, and passes the site key to `nuxt dev`; + - move the demo's user submodule mirror to the new local commit (fetch from the canonical checkout, detached checkout, no gitlink commit yet). +- vue-demo-app: + - runtimeConfig.public.turnstileSiteKey (from NUXT_PUBLIC_TURNSTILE_SITE_KEY); + - TurnstileWidget.vue loads https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit client-only, calls turnstile.render on mount with the site key and the widget language of the active locale (en or pl), emits the token from callback and an empty token from error-callback and expired-callback, exposes reset (tokens are single-use and expire after 300 s, so reset after each submit), and calls turnstile.remove on unmount; + - register.vue is ported from bm's RegisterView, with the fields the plugin's register endpoint takes, turnstile_token in the body, and field errors shown; + - login.vue gets the widget, with no pre-fill; + - useDemoAuth stores the returned token in the existing demo cookie and logs the user in after registration. +- scripts/check-local.sh [name...]: + - runs against the local dev stack (Go 127.0.0.1:8097, Nuxt 127.0.0.1:3020); + - check `turnstile`: POST /_user/api/v1/register without turnstile_token answers 422; the same request with the Cloudflare test dummy token XXXX.DUMMY.TOKEN.XXXX and a fresh email answers 200 or 201 with a token; GET /register on Nuxt contains the widget container; + - prints `check-local: ok {names}`, or the failing check with the status and exits 1. + + go -C ../fonoteka.go/plugins/golem15/user vet ./... && go -C ../fonoteka.go/plugins/golem15/user test -count=1 -short ./... && go -C ../fonoteka.go/plugins/golem15/user test -count=1 -short -run '^TestTurnstileRoutesSmoke$' -v . 2>&1 | grep -q -- '^--- PASS: TestTurnstileRoutesSmoke' && go -C ../sm-demo-app test -count=1 -short . ./plugins/demo/... && pnpm --dir ../sm-demo-app/vue-demo-app build && ../sm-demo-app/scripts/dev.sh up && ../sm-demo-app/scripts/check-local.sh turnstile | tail -1 | grep -q '^check-local: ok' + Non-zero exit: the user plugin's vet or suite red; no "--- PASS: TestTurnstileRoutesSmoke"; the demo boot test fails with turnstile:closed in config; the Nuxt build fails; the stack does not start; or check-local does not print "check-local: ok" (a token-less register is accepted, or the test token is refused). + + + - `grep -c 'turnstile' ../fonoteka.go/plugins/golem15/user/README.md` prints at least 1. + - `grep -c 'turnstile: *closed' ../sm-demo-app/config/golem15.user.yaml` prints 1. + - `grep -c 'render=explicit' ../sm-demo-app/vue-demo-app/app/components/TurnstileWidget.vue` prints 1. + - `git -C ../fonoteka.go/plugins/golem15/user log -1 --format=%s` names the Turnstile change, and `git -C ../fonoteka.go/plugins/golem15/user status --porcelain` is empty apart from untracked files. + + Registration and login on the demo require a Turnstile token end to end; the shared user plugin is unchanged for every app that does not set the key. + + + + Task 2: The homepage is a guided tour in the summercms.io look, and the whole site switches between English and Polish, including the FAQ and the menu + ../sm-demo-app/vue-demo-app/nuxt.config.ts, ../sm-demo-app/vue-demo-app/i18n/locales/en.json, ../sm-demo-app/vue-demo-app/i18n/locales/pl.json, ../sm-demo-app/vue-demo-app/public/sun-logo.webp, ../sm-demo-app/vue-demo-app/app/assets/css/tokens.css, ../sm-demo-app/vue-demo-app/app/assets/css/base.css, ../sm-demo-app/vue-demo-app/app/assets/css/site.css, ../sm-demo-app/vue-demo-app/app/layouts/default.vue, ../sm-demo-app/vue-demo-app/app/components/SiteHeader.vue, ../sm-demo-app/vue-demo-app/app/components/SiteFooter.vue, ../sm-demo-app/vue-demo-app/app/components/HeroSection.vue, ../sm-demo-app/vue-demo-app/app/components/TourCard.vue, ../sm-demo-app/vue-demo-app/app/components/LanguageSwitcher.vue, ../sm-demo-app/vue-demo-app/app/components/DemoBanner.vue, ../sm-demo-app/vue-demo-app/app/components/BackendCta.vue, ../sm-demo-app/vue-demo-app/app/pages/index.vue, ../sm-demo-app/vue-demo-app/app/pages/faq.vue, ../sm-demo-app/plugins/demo/console/seed.go, ../sm-demo-app/plugins/demo/console/seed_locales.go, ../sm-demo-app/scripts/check-local.sh + ../sm-summercmsio-app/vue-summercmsio-app/app/assets/css/tokens.css, base.css, app/components/SiteHeader.vue, SiteFooter.vue, HeroSection.vue, FeaturesSection.vue, nuxt.config.ts (fonts, head), ../sm-demo-app/vue-demo-app/app/layouts/default.vue, app/pages/index.vue, app/pages/faq.vue, app/components/DemoBanner.vue and BackendCta.vue (plan 02), ../vue-menu-layer/app/components/Menu/Nav.vue, ../vue-faq-layer/app/composables/useFaq.ts, ../vue-journal-layer/i18n/locales/pl.json (layer keys already translated), ../sm-demo-app/plugins/demo/console/seed.go (seedFAQ, seedMenu), ../sm-translate-plugin/classes/translatable.go, ../sm-faq-plugin (FAQ locale_id), 19.4.1-RESEARCH.md (i18n table) + Per D-11, D-12, D-13. + +- Look (D-12): copy tokens.css and base.css from vue-summercmsio-app (at its current commit; record the hash in the SUMMARY) and sun-logo.webp. SiteHeader and SiteFooter follow the summercms.io markup and classes, with the demo's MenuNav (locale bound to the active locale), the language switcher and the log in or log out control. site.css keeps only the demo-specific rules (banner, cards, forms) on the copied tokens. Use the summercms.io font families through @nuxt/fonts. +- i18n (D-13): nuxt.config sets locales en (en-US) and pl (pl-PL) with files i18n/locales/en.json and pl.json, strategy no_prefix and detectBrowserLanguage with useCookie true and redirectOn root, so SSR reads the cookie. LanguageSwitcher sets the locale (setLocale), which writes the cookie. Every app string, including the plan 02 banner and call to action, comes from the locale files. Polish copy is written for Polish readers, not word for word. +- Tour (D-11): index.vue renders HeroSection (headline, one sentence, "one Go binary, WinterCMS-shaped") and then six TourCard items in this order: Journal (/blog), Presentations (/roadmap, which plan 04 adds and which opens the roadmap deck of the active locale), Quizzes (/quizzes; plan 04), Payments (/subscriptions; the card says "Payments" in English and "Płatności" in Polish, and its sentence says the gateway handles all kinds of payments with subscriptions as the flow shown here), FAQ (/faq), Accounts (/register). Each card has an icon, a title, one sentence and a "Try it" link. TourCard renders the link only when the target resolves to a page of the build (router.resolve(path).matched is non-empty); otherwise it shows "Coming with the next update". The BackendCta closes the page. +- FAQ and menu: faq.vue passes the active locale to useFaq. Add seed_locales.go with steps that create the Polish FAQ "home" row (locale_id from golem15_translate_locales where code is pl, the same three questions in Polish without any admin path) when it is missing, and that write Polish menu entry titles with SetTranslated when they have none. The English rows stay as they are. +- check-local.sh check `tour`: + - GET / with no cookie contains the six English card titles including "Payments" and not "Subscriptions"; + - GET / with the locale cookie set to pl contains "Płatności" and the Polish banner; + - GET /faq with the pl cookie contains a Polish question. + + go -C ../sm-demo-app vet . ./plugins/demo/... && go -C ../sm-demo-app test -count=1 -short . ./plugins/demo/... && pnpm --dir ../sm-demo-app/vue-demo-app build && ../sm-demo-app/scripts/dev.sh up && ../sm-demo-app/scripts/dev.sh seed && ../sm-demo-app/scripts/check-local.sh tour | tail -1 | grep -q '^check-local: ok' + Non-zero exit: vet or tests red; the Nuxt build fails; the seed fails on the Polish rows; or check-local does not print "check-local: ok" (a card missing, "Subscriptions" used as the card title, Polish not rendered by SSR from the cookie, or the Polish FAQ missing). + + + - `diff -q ../sm-summercmsio-app/vue-summercmsio-app/app/assets/css/tokens.css ../sm-demo-app/vue-demo-app/app/assets/css/tokens.css` reports no difference (or the SUMMARY lists each intentional change). + - `grep -c '"Płatności"\|Płatności' ../sm-demo-app/vue-demo-app/i18n/locales/pl.json` prints at least 1. + - `grep -c 'TourCard' ../sm-demo-app/vue-demo-app/app/pages/index.vue` prints at least 1. + - `python3 -c "import json,sys;a=json.load(open(sys.argv[1]));b=json.load(open(sys.argv[2]));f=lambda d,p='':[p+k for k,v in d.items() for k in ([k] if not isinstance(v,dict) else [k+'.'+x for x in f(v)])];print(sorted(f(a))==sorted(f(b)))" ../sm-demo-app/vue-demo-app/i18n/locales/en.json ../sm-demo-app/vue-demo-app/i18n/locales/pl.json` prints True (same keys in both languages). + + The homepage tours six features in the summercms.io look, and the site, the FAQ and the menu switch between English and Polish on the server and in the browser. + + + + Task 3: Journal posts read in Polish or English and show an author card with subtitle, description and avatar + ../sm-journal-plugin/controllers/api/authors.go, ../sm-journal-plugin/journal_api_authors_test.go, ../sm-journal-plugin/README.md, ../sm-demo-app/plugins/demo/console/seed_locales.go, ../sm-demo-app/plugins/demo/content/content.go, ../sm-demo-app/plugins/demo/content/author/summercms-team.webp, ../sm-demo-app/vue-demo-app/nuxt.config.ts, ../sm-demo-app/vue-demo-app/app/composables/useJournalApi.ts, ../sm-demo-app/vue-demo-app/app/utils/journalLocale.ts, ../sm-demo-app/vue-demo-app/app/components/Journal/AuthorCard.vue, ../sm-demo-app/vue-demo-app/i18n/locales/en.json, ../sm-demo-app/vue-demo-app/i18n/locales/pl.json, ../sm-demo-app/scripts/check-local.sh + ../sm-journal-plugin/controllers/api/authors.go, ../sm-journal-plugin/journal_api_authors_test.go (TestJournalAuthorProfile, line 91), ../sm-journal-plugin/README.md (API reference), ../sm-journal-plugin/console/import_posts.go:315-335, ../sm-translate-plugin/classes/translatable.go, modules/cabana/README.md (BackendUser.Subtitle, BackendUserAvatar), ../vue-journal-layer/app/composables/useJournalApi.ts, ../vue-journal-layer/app/utils/view.ts, ../vue-journal-layer/app/utils/postView.ts, ../vue-journal-layer/app/components/Journal/AuthorCard.vue, ../vue-journal-layer/app/journal-pages/PostPage.vue and AuthorPage.vue, ../sm-demo-app/plugins/demo/console/seed.go (demoPosts), .planning/todos/pending/journal-author-profile-fields.md + Per todo esrp4qtx and D-13. + +- sm-journal-plugin (additive): + - authorColumns and authorRow gain backend_users.metadata; + - authorProfile adds `subtitle` (metadata.subtitle, as cabana.BackendUser.Subtitle reads it), `description` (metadata.description) and `avatar` (cabana.BackendUserAvatar URL, or null). A key with no stored value is an empty string, or null for avatar; + - update TestJournalAuthorProfile's exact key set and assert the three values for a backend user with metadata and an avatar; + - README API reference documents the keys in the same commit. + Commit locally and move the demo's journal submodule mirror to it as in Task 1. +- Seed (seed_locales.go): + - step "journal author": create the backend user SummerCMS Team (login summercms-team, first name "SummerCMS", last name "Team", not a superuser, no role, a random password hash from 32 random bytes that is never stored elsewhere or printed). Its metadata has subtitle "Golem15" and a two-sentence description. Its avatar is the embedded content/author/summercms-team.webp, copied from vue-summercmsio-app/public/sun-badge.webp and attached the way cabana stores backend avatars. Set user_id on the demo posts whose user_id is null. Skip anything that exists. + - step "journal translations": for each demo post with no Polish translation yet, SetTranslated title, excerpt, content and content_html in pl (Polish texts written in seed_locales.go next to the English ones; the slug stays untranslated). Do the same for the two categories' translatable fields. +- vue-demo-app: + - journal routes enable `author: '/blog/author/:author'`; + - app/utils/journalLocale.ts exports applyJournalLocale(post, locale), which returns the post with title, excerpt and content_html taken from translations[locale] when present, and works on list payloads too; + - app/composables/useJournalApi.ts overrides the layer's composable: it wraps the layer client so every post and list response passes through applyJournalLocale with the active locale before the layer's view mappers run; + - verify the override takes precedence (a Polish post title rendered with the pl cookie). If it does not, apply the vue-journal-layer fallback from the decisions and record it as a deviation; + - Journal/AuthorCard.vue overrides the layer component and shows the avatar, the name, the subtitle and the description, with labels from the locale files. +- check-local.sh check `journal`: + - GET /_journal/api/v1/authors/summercms-team returns subtitle, description and avatar; + - GET /blog/{first demo slug} with the pl cookie contains the Polish title; + - GET /blog/author/summercms-team contains "Golem15". + + go -C ../sm-journal-plugin vet ./... && go -C ../sm-journal-plugin test -count=1 -short ./... && go -C ../sm-journal-plugin test -count=1 -run '^TestJournalAuthorProfile$' -v . 2>&1 | grep -q -- '^--- PASS: TestJournalAuthorProfile' && go -C ../sm-demo-app vet . ./plugins/demo/... && pnpm --dir ../sm-demo-app/vue-demo-app build && ../sm-demo-app/scripts/dev.sh up && ../sm-demo-app/scripts/dev.sh seed && ../sm-demo-app/scripts/check-local.sh journal | tail -1 | grep -q '^check-local: ok' + Non-zero exit: the journal plugin's vet or suite red; no "--- PASS: TestJournalAuthorProfile" (Docker unavailable, key set wrong); the Nuxt build fails; the seed fails; or check-local does not print "check-local: ok" (author keys missing, the Polish title not rendered, the author page missing the subtitle). + + + - `grep -c '"subtitle"\|"description"\|"avatar"' ../sm-journal-plugin/controllers/api/authors.go` prints at least 3. + - `grep -c 'subtitle' ../sm-journal-plugin/README.md` prints at least 1. + - `grep -c 'translations' ../sm-demo-app/vue-demo-app/app/utils/journalLocale.ts` prints at least 1. + - `grep -c "author: '/blog/author/:author'" ../sm-demo-app/vue-demo-app/nuxt.config.ts` prints 1. + + The journal reads in both languages and every demo post shows the SummerCMS Team author with subtitle, description and avatar from the extended author API. + + + + Task 4: "Want to see the backend?" opens a contact form whose submissions land in the admin and as G15Office tasks + ../sm-demo-app/vue-demo-app/app/pages/contact.vue, ../sm-demo-app/vue-demo-app/app/components/BackendCta.vue, ../sm-demo-app/vue-demo-app/nuxt.config.ts, ../sm-demo-app/vue-demo-app/i18n/locales/en.json, ../sm-demo-app/vue-demo-app/i18n/locales/pl.json, ../sm-demo-app/plugins/demo/plugin.go, ../sm-demo-app/plugins/demo/config/config.yaml, ../sm-demo-app/plugins/demo/console/seed_locales.go, ../sm-demo-app/scripts/dev.sh, ../sm-demo-app/scripts/check-local.sh, ../sm-demo-app/deploy/env.example, ../sm-demo-app/deploy/site.env.example + ../fonoteka.go/plugins/golem15/feedback/README.md, ../fonoteka.go/plugins/golem15/feedback/routes.go, ../fonoteka.go/plugins/golem15/feedback/models/settings.go, ../fonoteka.go/plugins/golem15/feedback/controllers/api/feedback_api_controller.go (field rules, Origin check), ../fonoteka.go/plugins/golem15/feedback/config/config.yaml (g15_office keys), ../sm-demo-app/vue-demo-app/app/components/BackendCta.vue, 19.4.1-RESEARCH.md (Feedback contact form) + Per D-04 (revised: no mail). + +- golem15.demo config gains `feedback: {widget_key: "", allowed_origins: ""}` (comma-separated origins). Add golem15.feedback to Requires(). A seed step "feedback settings" upserts the feedback settings row: enabled true, allowed_origins from config, and widget_key from config when set. With no widget key configured it reports "skipped (golem15.demo.feedback.widget_key is not set)". +- dev.sh writes a local key (wk_ plus 32 random hex) and allowed origin http://127.0.0.1:3020 into .env when missing, and passes NUXT_PUBLIC_FEEDBACK_KEY to nuxt dev. env.example and site.env.example document the keys: SUMMER_GOLEM15__DEMO__FEEDBACK__WIDGET_KEY with SUMMER_GOLEM15__DEMO__FEEDBACK__ALLOWED_ORIGINS, NUXT_PUBLIC_FEEDBACK_KEY with the same value, and the three SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__* keys. +- vue-demo-app: + - runtimeConfig.public.feedbackKey; + - contact.vue (EN/PL): name, email (required), message (required, up to 4800 characters so the name prefix fits the API's 5000), a privacy line saying the message goes to Golem15 only. It posts FormData to /_feedback/api/v1/{key}/submit with type "other", message "{name} ({email}): {message}", email and page_url set to location.href. It shows a thank-you state, the API's field errors and a 429 hint; + - BackendCta links to /contact instead of golem15.com. +- No mail: add no listener, job or mailer. The feedback plugin stores the submission and files the G15Office task when the G15Office keys are set. +- check-local.sh check `contact`: a multipart POST with Origin http://127.0.0.1:3020 to the submit URL answers 2xx and the submissions count in the database grows by one; a POST with Origin https://evil.example is refused; GET /contact answers 200. + + go -C ../sm-demo-app vet . ./plugins/demo/... && go -C ../sm-demo-app test -count=1 -short . ./plugins/demo/... && pnpm --dir ../sm-demo-app/vue-demo-app build && ../sm-demo-app/scripts/dev.sh up && ../sm-demo-app/scripts/dev.sh seed && ../sm-demo-app/scripts/check-local.sh contact | tail -1 | grep -q '^check-local: ok' + Non-zero exit: vet or tests red (golem15.feedback missing from Requires breaks activation order); the Nuxt build fails; or check-local does not print "check-local: ok" (the allowed origin refused, a foreign origin accepted, or no submission row written). + + + - `grep -c '/contact' ../sm-demo-app/vue-demo-app/app/components/BackendCta.vue` prints at least 1. + - `grep -rlE 'mail\.(Send|Queue)|postcard\.' ../sm-demo-app/plugins/demo` prints nothing (no mail sending added). + - `grep -c 'golem15.feedback' ../sm-demo-app/plugins/demo/plugin.go` prints at least 1. + + Visitors who want a backend tour fill in a contact form; the submission is stored for the admin and filed in G15Office, and no mail is sent. + + + + Task 5: The operator can reset the demo to its seeded state (schedule shipped disabled), and open tabs offer a reload after a deploy + ../sm-demo-app/plugins/demo/console/reset.go, ../sm-demo-app/plugins/demo/schedule_smoke_test.go, ../sm-demo-app/plugins/demo/console/seed.go, ../sm-demo-app/plugins/demo/schedule.go, ../sm-demo-app/plugins/demo/plugin.go, ../sm-demo-app/plugins/demo/config/config.yaml, ../sm-demo-app/scripts/check-local.sh, ../sm-demo-app/vue-demo-app/nuxt.config.ts, ../sm-demo-app/vue-demo-app/app/utils/buildUpdate.ts, ../sm-demo-app/vue-demo-app/app/plugins/manifest-update.client.ts, ../sm-demo-app/vue-demo-app/app/components/UpdateBanner.vue, ../sm-demo-app/vue-demo-app/app/layouts/default.vue, ../sm-demo-app/vue-demo-app/i18n/locales/en.json, ../sm-demo-app/vue-demo-app/i18n/locales/pl.json, ../sm-demo-app/README.md + ../sm-oauthserver-plugin/schedule.go, ../sm-oauthserver-plugin/plugin.go (HasSchedule assertion and Schedule), ../sm-faq-plugin/console/import_winter.go:46-69 and console/helpers.go:30-36 (--force flag), modules/lagoon/attach/file.go:113-188 (BlobKeys, DeleteKeys), modules/pact/capabilities.go:120-176, modules/conga/README.md (scheduler, schedule:run --once), ../sm-demo-app/plugins/demo/console/seed.go, ../sm-demo-app/plugins/demo/console/roadmap_deck.go (plan 01), the migrations of the user, journal, faq, menu, quizzes, presentations, paymentgateway, pgsub and feedback plugins (table names and foreign keys), ../sm-bm-app/frontend/src/stores/pwa.ts (updateReady and applyUpdate behaviour), 19.4.1-RESEARCH.md (Pattern 4, Pattern 6, Pitfall 9) + Per D-09 and D-22. + +Reset: +- console/reset.go registers `demo:reset` with a bare flag `force`. Without --force it prints what it would wipe and exits 1. +- With --force: + 1. read the public_hash of the roadmap decks; + 2. in one transaction, delete in foreign-key order: + - frontend users and everything they own (API tokens, sessions, subscriptions, orders and payments, quiz attempts and answers, owned presentations and their slides); + - journal posts with their category links and translations, and journal categories; + - the FAQ rows and the menu with its entries; + - quizzes with their questions and answers; + - presentations and slides; + - feedback submissions; + - every system_files row except backend-user attachments; + collect attach.BlobKeys of the deleted file rows; + 3. after commit, delete those blobs (attach.DeleteKeys); + 4. run every seed step; + 5. write the saved public_hash values back onto the reseeded roadmap decks. +- It keeps admins, subscription plans (their Stripe price ids, Pitfall 9), settings, locales, the sitemap definition and migrations. Print one count line per wiped table and the seed output. +- schedule.go: Schedule() returns nil unless golem15.demo.reset.enabled. When enabled it returns the entry demo:reset with Args --force and Cadence DailyAt(hour, minute) parsed from golem15.demo.reset.at. +- plugin.go asserts pact.HasSchedule and validates reset.at in Register: anything but HH:MM fails boot naming the key. +- config.yaml: `reset: {enabled: false, at: "04:00"}` with a comment that the schedule ships disabled (D-09). +- plugins/demo/schedule_smoke_test.go `TestResetScheduleSmoke` (package demo, no database): disabled gives nil; enabled with "05:30" gives one entry with --force; an invalid value is refused. +- README documents demo:reset, the keys and the `schedule:run --once` cron alternative. +- check-local.sh check `reset`: + - it records the plan count, the admin count and the roadmap hash; + - registers a throwaway user; + - runs `bin/demo demo:reset --force` with the local env; + - asserts the throwaway user is gone, the visitor exists, the plan and admin counts are unchanged and the roadmap hash is unchanged. + +Reload (D-22, site half): +- app/utils/buildUpdate.ts exports isNewerBuild(currentId, latest), true when latest.id differs from currentId and both are non-empty. +- app/plugins/manifest-update.client.ts: + - sets a useState flag on the app:manifest:update hook; + - on visibilitychange to visible, fetches builds/latest.json from the build assets URL with a cache-busting query and compares it with useRuntimeConfig().app.buildId through isNewerBuild. +- UpdateBanner.vue, in the default layout, says "A new version of this site is available" with a Reload button that calls location.reload(). It never reloads by itself. +- nuxt.config sets experimental.checkOutdatedBuildInterval to 300000. + + go -C ../sm-demo-app vet . ./plugins/demo/... && go -C ../sm-demo-app test -count=1 -short -run '^TestResetScheduleSmoke$' -v ./plugins/demo/... 2>&1 | grep -q -- '^--- PASS: TestResetScheduleSmoke' && pnpm --dir ../sm-demo-app/vue-demo-app build && ../sm-demo-app/scripts/dev.sh up && ../sm-demo-app/scripts/check-local.sh reset | tail -1 | grep -q '^check-local: ok' + Non-zero exit: vet red; no "--- PASS: TestResetScheduleSmoke" (schedule present by default, missing when enabled, or a bad time accepted); the Nuxt build fails; or check-local does not print "check-local: ok" (the reset leaves a frontend user, loses a plan or an admin, or changes the roadmap deck hash). + + + - `grep -c 'enabled: false' ../sm-demo-app/plugins/demo/config/config.yaml` prints at least 1. + - `grep -c 'app:manifest:update' ../sm-demo-app/vue-demo-app/app/plugins/manifest-update.client.ts` prints 1. + - `grep -rl 'location.reload' ../sm-demo-app/vue-demo-app/app` prints exactly one path, app/components/UpdateBanner.vue. + - `ls ../sm-demo-app/vue-demo-app/public/sw.js ../sm-demo-app/vue-demo-app/app/sw.js 2>/dev/null` lists nothing (no service worker). + + demo:reset --force restores the seeded demo without touching admins, plans or the roadmap links; its schedule is off until configured; open tabs learn about a new deploy and reload on request. + + + + Task 6: Go-ahead to tag sm-user-plugin and sm-journal-plugin, push, and redeploy with the Turnstile and G15Office keys + The table Claude prints before asking: the next free patch tags of sm-user-plugin and sm-journal-plugin from `git ls-remote --tags` (and of vue-journal-layer if the fallback was used), the commits, and whether Phase 19.5 has tagged sm-user-plugin since plan 02; the list of new rome env keys + + - The user chose one option. With "release", ~/.config/summercms/demo-secrets.env (mode 600) also holds SUMMER_TURNSTILE__SECRET, NUXT_PUBLIC_TURNSTILE_SITE_KEY and the three SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__* keys (names checked with grep -c, values never printed). + + Tag the two plugin changes, push them and the demo, and redeploy demo.summercms.io? + D-03 is one-way: the tags are permanent. sm-user-plugin also has Phase 19.5 work planned; tagging here first lets 19.5 take the following number. Production needs a Turnstile widget for demo.summercms.io: without its secret, `turnstile:closed` answers 503 to every registration and login in production. Contact submissions need a G15Office token to become tasks; without it they are only stored in the admin. + + + + + + For "release": (1) create a Turnstile widget for demo.summercms.io in the Cloudflare dashboard and add its secret and site key to ~/.config/summercms/demo-secrets.env as SUMMER_TURNSTILE__SECRET and NUXT_PUBLIC_TURNSTILE_SITE_KEY; (2) add SUMMER_GOLEM15__FEEDBACK__G15_OFFICE__BASE_URL, __TOKEN and __PROJECT for the project that should receive demo contact requests. Do not paste them into the chat. + + Reply "release" (after updating the secrets file) or "hold". + + + + Task 7: Registration, the bilingual tour, journal authors and the contact form are live on demo.summercms.io from tagged plugins + D-03: pushed tags of sm-user-plugin and sm-journal-plugin are cached by module proxies and cannot be reused. + The Task 6 reply was "release"; the secrets file has mode 600 and holds the Turnstile and G15Office key names. + ../sm-demo-app/go.mod, ../sm-demo-app/go.sum, ../sm-demo-app/plugins/golem15/user, ../sm-demo-app/plugins/golem15/journal, ../sm-demo-app/vue-demo-app, ../sm-demo-app/scripts/check-live.sh, ../sm-demo-app/DEPLOY.md, ../sm-demo-app/README.md + The Task 6 reply, ../sm-demo-app/DEPLOY.md (Release), ../sm-demo-app/scripts/check-pins.sh, ../sm-demo-app/scripts/check-live.sh, 19.4.1-02-SUMMARY.md (tag procedure and ports) + Per D-03, D-07, D-04, SC-3, SC-4. + +- Push the sm-user-plugin commit (branch master) and tag the next free patch on it. Push the sm-journal-plugin commit (branch main) and tag its next free patch. Use the plan 02 tag procedure: fetch tags, check the remote head, annotated tag, ls-remote check. If Phase 19.5 has pushed commits to sm-user-plugin since Task 1, rebase this commit on them, re-run its suite, then tag. +- In sm-demo-app: require both tags, tidy, move the two submodules to the tag commits (separate pointer commit), check-pins, build release, smoke. Push vue-demo-app master first, then sm-demo-app master. +- On rome, append the new keys to /srv/summercms-demo/.env and site.env from the secrets file without printing values: + - .env: SUMMER_TURNSTILE__SECRET, the three G15Office keys, a generated feedback widget key with allowed origin https://demo.summercms.io; + - site.env: NUXT_PUBLIC_TURNSTILE_SITE_KEY and NUXT_PUBLIC_FEEDBACK_KEY with the same widget key. + Then run the DEPLOY.md release steps (upload, migrate, demo:seed, swap, restart). +- check-live.sh gains: + - /register answers 200 and contains the Turnstile widget container; + - /contact answers 200; + - / with the pl locale cookie contains "Płatności"; + - POST /_user/api/v1/register without a token answers 422. +- DEPLOY.md documents the new keys (names only) and the reset command, and states that the reset schedule stays disabled unless SUMMER_GOLEM15__DEMO__RESET__ENABLED is set. + + ../sm-demo-app/scripts/check-pins.sh | tail -1 | grep -q '^check-pins: ok' && test "$(git -C ../sm-demo-app ls-remote origin refs/heads/master | cut -f1)" = "$(git -C ../sm-demo-app rev-parse HEAD)" && ../sm-demo-app/scripts/check-live.sh https://demo.summercms.io 2>&1 | tail -1 | grep -qx 'check-live: ok' + Non-zero exit: a submodule is off its tag; the sm-demo-app push is missing; or check-live does not print "check-live: ok" (register or contact page missing, Polish not served, a token-less register accepted, or any plan 02 check regressed). + + + - `git -C ../fonoteka.go/plugins/golem15/user ls-remote --tags origin | grep -c "$(git -C ../fonoteka.go/plugins/golem15/user rev-parse HEAD)"` prints at least 1 (the change is tagged). + - `git -C ../sm-journal-plugin ls-remote --tags origin | grep -c "$(git -C ../sm-journal-plugin rev-parse HEAD)"` prints at least 1. + - `curl -s https://demo.summercms.io/_journal/api/v1/authors/summercms-team | python3 -c "import json,sys;d=json.load(sys.stdin);d=d.get('data',d);print(all(k in d for k in ('subtitle','description','avatar')))"` prints True. + - `ssh -p 2222 debian@10.8.3.118 sudo grep -c '^SUMMER_GOLEM15__DEMO__RESET__ENABLED=true' /srv/summercms-demo/.env` prints 0 (the schedule stays off in production). + + The demo runs from the tagged user and journal plugins with Turnstile, the bilingual tour, journal authors and the contact form live. + + + + Task 8: The user checks registration, the tour in both languages, the journal and the contact form on the live demo + The check-live output of Task 7 + + - The user replied "approved", or the issues they listed are fixed, redeployed, check-live is green again, and the user approved. + + On https://demo.summercms.io: registration and login behind Turnstile, the guided tour in the summercms.io look with six cards (Payments, not Subscriptions), the English/Polish switcher across UI, FAQ, menu and journal, journal author cards, the contact form that files G15Office tasks, the reload banner after deploys, and demo:reset (schedule off). + + 1. Open https://demo.summercms.io and compare the header, colours and type with https://summercms.io. Switch to Polish and back; reload the page in Polish and it stays Polish. + 2. Register a new account at /register (the Turnstile widget appears and passes), then log out and log in again. + 3. Open a journal post in both languages and the author page /blog/author/summercms-team. + 4. Click "Want to see the backend? Contact Golem15", send a message, and check that a task arrives in G15Office (and that the submission is in Admin, Feedback through the SSH tunnel if you want). No mail should arrive. + 5. Keep a tab open, ask Claude for a trivial redeploy (or wait for the next one) and see the reload banner within five minutes or when you return to the tab. + + Reply "approved", or describe what is wrong. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| browser to /_user/api/v1/register and /login | anonymous, automatable sign-ups and credential attempts | +| browser to /_feedback/api/v1/{key}/submit | anonymous free text with a public key | +| Go to Cloudflare siteverify and to G15Office | outbound calls carrying secrets | +| operator to demo:reset | destructive command on production data | +| browser locale cookie to SSR | user-controlled input selecting content | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-19.4.1-16 | Spoofing | bot sign-ups and credential stuffing | high | mitigate | turnstile:closed on register and login (fails closed with 503 in production without a secret), existing throttle:user-api and login attempt limits; check-local and check-live assert a token-less register gets 422 | +| T-19.4.1-17 | Tampering | shared sm-user-plugin behaviour | high | mitigate | the key defaults to empty (no change for other apps), invalid values fail boot, TestTurnstileRoutesSmoke pins that only register and login change; other apps' pins untouched | +| T-19.4.1-18 | Denial of service | contact form spam | medium | mitigate | the feedback plugin's per-IP throttle (10 a minute), the Origin allow-list set to the demo origin, message length limits; submissions only create G15Office tasks, no mail | +| T-19.4.1-19 | Information disclosure | Turnstile secret and G15Office token | high | mitigate | values only in the 0600 secrets file and rome's 0640 .env, piped over ssh without echo; repositories and DEPLOY.md hold key names only | +| T-19.4.1-20 | Tampering | demo:reset deleting kept data | high | mitigate | refuses without --force, explicit keep-list (admins, plans, settings, locales, sitemap), one transaction with blob deletion after commit, roadmap hashes restored; schedule disabled by default and asserted off on rome; check-local reset verifies counts | +| T-19.4.1-21 | Tampering | stored XSS through seeded translations or the author description | low | mitigate | seeded content is curated in the repository; the journal layer sanitizes post HTML; the author card renders text, not HTML | +| T-19.4.1-22 | Information disclosure | author payload | low | accept | only subtitle, description and avatar are added, the fields the WinterCMS author component already publishes; email and login data stay out of authorColumns | +| T-19.4.1-23 | Tampering | locale cookie | low | accept | only en and pl are accepted by the i18n module; any other value falls back to the default | +| T-19.4.1-24 | Information disclosure | feedback widget key in the page | low | accept | public by design; the Origin allow-list and throttles gate it | +| T-19.4.1-SC | Tampering | npm/pip/cargo installs | high | mitigate | no new npm or Go package: the Turnstile script loads from Cloudflare at runtime (no npm package), Go dependencies stay as tagged; a vue-journal-layer fallback tag would be the project's own layer | + + + +- Task 1: TestTurnstileRoutesSmoke; check-local turnstile (422 without token, success with the test token). +- Task 2: Nuxt build; check-local tour (six cards, Payments, Polish SSR, Polish FAQ). +- Task 3: TestJournalAuthorProfile; check-local journal. +- Task 4: check-local contact (origin allow-list, submission row). +- Task 5: TestResetScheduleSmoke; check-local reset (keeps admins, plans and deck hashes). +- Task 7: check-pins, pushes, check-live with the new checks. +- Task 8: the user approves on the live site. + + + +Visitors self-register behind Turnstile and rate limits with no credential shown; the reset job exists and ships disabled (SC-3); the homepage is a guided tour in the summercms.io look in English and Polish (SC-4, tour part); the backend call to action leads to a working contact form (SC-2); the journal author API carries subtitle, description and avatar (todo esrp4qtx); open tabs offer a reload after a deploy (todo ltcvqext, site half). + + + +Create `.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-03-SUMMARY.md` when done (tags and commits, the summercms.io commit the CSS came from, whether the journal composable override held or the layer fallback was used, no secret values). + diff --git a/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-04-PLAN.md b/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-04-PLAN.md new file mode 100644 index 00000000..171802f0 --- /dev/null +++ b/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-04-PLAN.md @@ -0,0 +1,505 @@ +--- +phase: 19.4.1-early-public-demo-on-demo-summercms-io +plan: 04 +type: execute +wave: 4 +depends_on: ["19.4.1-03"] +files_modified: + - ../sm-demo-app/plugins/demo/plugin.go + - ../sm-demo-app/plugins/demo/realtime.go + - ../sm-demo-app/plugins/demo/routes.go + - ../sm-demo-app/plugins/demo/decks_api.go + - ../sm-demo-app/plugins/demo/quizzes_api.go + - ../sm-demo-app/plugins/demo/console/seed.go + - ../sm-demo-app/plugins/demo/console/seed_locales.go + - ../sm-demo-app/boot_test.go + - ../sm-demo-app/config/realtime.yaml + - ../sm-demo-app/config/golem15.presentations.yaml + - ../sm-demo-app/deploy/centrifugo.json.example + - ../sm-demo-app/deploy/supervisor/summercms-demo-centrifugo.conf + - ../sm-demo-app/deploy/nginx/demo.summercms.io.conf + - ../sm-demo-app/deploy/env.example + - ../sm-demo-app/scripts/dev.sh + - ../sm-demo-app/scripts/check-local.sh + - ../sm-demo-app/scripts/check-deploy.sh + - ../sm-demo-app/scripts/check-live.sh + - ../sm-demo-app/DEPLOY.md + - ../sm-demo-app/README.md + - ../sm-demo-app/vue-demo-app + - ../sm-demo-app/vue-demo-app/package.json + - ../sm-demo-app/vue-demo-app/pnpm-lock.yaml + - ../sm-demo-app/vue-demo-app/nuxt.config.ts + - ../sm-demo-app/vue-demo-app/i18n/locales/en.json + - ../sm-demo-app/vue-demo-app/i18n/locales/pl.json + - ../sm-demo-app/vue-demo-app/app/lib/deck/clock.ts + - ../sm-demo-app/vue-demo-app/app/lib/deck/compose.ts + - ../sm-demo-app/vue-demo-app/app/lib/deck/engine.ts + - ../sm-demo-app/vue-demo-app/app/lib/deck/reading.ts + - ../sm-demo-app/vue-demo-app/app/lib/deck/remote.ts + - ../sm-demo-app/vue-demo-app/app/lib/deck/styles.ts + - ../sm-demo-app/vue-demo-app/app/lib/deck/themes.ts + - ../sm-demo-app/vue-demo-app/app/lib/realtime.ts + - ../sm-demo-app/vue-demo-app/app/lib/pilot.ts + - ../sm-demo-app/vue-demo-app/app/lib/quizSession.ts + - ../sm-demo-app/vue-demo-app/app/lib/markdown.ts + - ../sm-demo-app/vue-demo-app/app/lib/forms.ts + - ../sm-demo-app/vue-demo-app/app/lib/api/http.ts + - ../sm-demo-app/vue-demo-app/app/lib/api/presentations.ts + - ../sm-demo-app/vue-demo-app/app/lib/api/quizzes.ts + - ../sm-demo-app/vue-demo-app/app/lib/api/demo.ts + - ../sm-demo-app/vue-demo-app/app/lib/api/types.ts + - ../sm-demo-app/vue-demo-app/app/assets/deck/deck.css + - ../sm-demo-app/vue-demo-app/app/assets/deck/deck-speaker.css + - ../sm-demo-app/vue-demo-app/app/assets/deck/themes/atlas.css + - ../sm-demo-app/vue-demo-app/app/assets/deck/themes/monografia.css + - ../sm-demo-app/vue-demo-app/app/components/deck/DeckViewer.vue + - ../sm-demo-app/vue-demo-app/app/components/deck/SpeakerTools.vue + - ../sm-demo-app/vue-demo-app/app/components/remote/RemoteDrawer.vue + - ../sm-demo-app/vue-demo-app/app/components/remote/RemoteKeys.vue + - ../sm-demo-app/vue-demo-app/app/components/remote/RemoteTopBar.vue + - ../sm-demo-app/vue-demo-app/app/components/AttemptList.vue + - ../sm-demo-app/vue-demo-app/app/components/DemoBanner.vue + - ../sm-demo-app/vue-demo-app/app/composables/useToast.ts + - ../sm-demo-app/vue-demo-app/app/pages/roadmap.vue + - ../sm-demo-app/vue-demo-app/app/pages/p/[slugHash].vue + - ../sm-demo-app/vue-demo-app/app/pages/presentations/index.vue + - ../sm-demo-app/vue-demo-app/app/pages/presentations/new.vue + - ../sm-demo-app/vue-demo-app/app/pages/presentations/[slug]/index.vue + - ../sm-demo-app/vue-demo-app/app/pages/presentations/[slug]/pilot.vue + - ../sm-demo-app/vue-demo-app/app/pages/presentations/[slug]/edit.vue + - ../sm-demo-app/vue-demo-app/app/pages/quizzes/index.vue + - ../sm-demo-app/vue-demo-app/app/pages/quizzes/[slug].vue +autonomous: false +requirements: [SC-4, D-13, D-14, D-15, D-16, D-17, D-18, D-23] +estimate: + tokens: 380000 + raw_tokens: 380000 + tasks: 7 + confidence: low +must_haves: + truths: + - "D-14: bm's presentation system runs in vue-demo-app as Nuxt pages: the public deck at /p/{hash}, the speaker view at /presentations/{slug}, the phone remote at /presentations/{slug}/pilot and My Presentations at /presentations; the deck routes are client-only (routeRules ssr false) and the bm views, components and libraries are ported with their behaviour unchanged." + - "D-16: /roadmap opens the roadmap deck of the active locale (roadmap or roadmap-pl) in the ported viewer, including the OIDC screenshots; the tour's Presentations card links to it." + - "D-15: any registered user creates a deck (title, theme from the fixed list, slides written in markdown), edits it, and presents it from the speaker view with the phone remote; the demo endpoint drops custom CSS and context, refuses unknown themes, keeps user decks unpublished, and every HTML field still passes classes.Clean." + - "D-17: the demo runs its own Centrifugo on rome (supervisor program summercms-demo-centrifugo, its own HMAC secret, API key and proxy secret, allowed origin https://demo.summercms.io, namespace presentations with the subscribe proxy) and nginx sends /ws to it; the speaker view and the phone remote sync through lighthouse." + - "D-17: GET /api/realtime/token is mounted behind jwt.auth, presentations.realtime-owner and the bucket golem15.demo.realtime (120 a minute per user, else per IP); POST /api/realtime/subscribe is mounted for Centrifugo only and nginx never routes it." + - "D-18: anonymous visitors see the quiz list of their language at /quizzes (from GET /_demo/api/v1/quizzes?context=demo:{locale}, no answers exposed) with a prompt to register; a logged-in user takes a quiz, sees the score and the list of past attempts." + - "D-13: demo:seed writes an English quiz (summercms-basics, context demo:en) and a Polish one (summercms-podstawy, context demo:pl) and a Polish twin of the getting-started deck; the deck and quiz pages pick the locale's content." + artifacts: + - path: "../sm-demo-app/plugins/demo/realtime.go" + provides: "lighthouse service wiring with the user lookup (port of bm's app-plugin realtime.go)" + contains: "lighthouse" + - path: "../sm-demo-app/plugins/demo/routes.go" + provides: "/_demo/api/v1 routes, the realtime bucket and lighthouse.Mount" + contains: "lighthouse.Mount" + - path: "../sm-demo-app/plugins/demo/decks_api.go" + provides: "JWT deck create/show/update wrapping the presentations manage handlers" + contains: "ManageCreate" + - path: "../sm-demo-app/plugins/demo/quizzes_api.go" + provides: "anonymous published quiz list by context" + contains: "context" + - path: "../sm-demo-app/deploy/centrifugo.json.example" + provides: "Centrifugo v6 config template for the demo's own instance" + contains: "subscribe_proxy_enabled" + - path: "../sm-demo-app/vue-demo-app/app/components/deck/DeckViewer.vue" + provides: "the ported deck viewer" + contains: "followHash" + - path: "../sm-demo-app/vue-demo-app/app/pages/presentations/[slug]/pilot.vue" + provides: "the ported phone remote" + contains: "remote" + key_links: + - from: "../sm-demo-app/vue-demo-app/app/lib/realtime.ts" + to: "../sm-demo-app/plugins/demo/routes.go" + via: "GET /api/realtime/token with the demo cookie's JWT, then a WebSocket to /ws" + pattern: "/api/realtime/token" + - from: "../sm-demo-app/deploy/nginx/demo.summercms.io.conf" + to: "../sm-demo-app/deploy/centrifugo.json.example" + via: "location = /ws proxies to the demo Centrifugo's /connection/websocket port" + pattern: "connection/websocket" + - from: "../sm-demo-app/deploy/centrifugo.json.example" + to: "../sm-demo-app/plugins/demo/routes.go" + via: "the subscribe proxy calls 127.0.0.1:{go port}/api/realtime/subscribe with X-Centrifugo-Secret" + pattern: "api/realtime/subscribe" + - from: "../sm-demo-app/vue-demo-app/app/pages/roadmap.vue" + to: "../sm-demo-app/plugins/demo/routes.go" + via: "GET /_demo/api/v1/roadmap?locale= returns the deck's public hash" + pattern: "_demo/api/v1/roadmap" + prohibitions: + - statement: "nginx never routes /api/realtime/subscribe, and the Centrifugo API and proxy secrets never leave rome's env and config files." + status: unresolved + verification: automated + - statement: "No user-created deck is published or carries custom CSS." + status: unresolved + verification: automated + - statement: "No new npm package except centrifuge@5.7.3 (the audited pin); no Pinia, no service worker, no @fontsource package." + status: unresolved + verification: judgment + - statement: "No file under ../sm-bm-app and no shared plugin is modified." + status: unresolved + verification: judgment + - statement: "No deploy before the Task 5 go-ahead." + status: unresolved + verification: judgment +--- + +## Phase Goal + +ROADMAP Goal (not in user-story format; no story is invented here): Clients can be shown a live demo at `demo.summercms.io` now, before the chat family lands: `sm-demo-app` with today's 16 shared plugins runs on rome, its public Nuxt site shows the plugins working through prepared demo accounts, and the admin backend stays private. + +This plan's slice (plan 4 of 5, wave 4): presentations and quizzes. bm's presentation system (public deck, speaker view, phone remote, My Presentations) is ported into vue-demo-app and syncs through the demo's own Centrifugo on rome. Registered users write and present their own decks. The roadmap deck from plan 01 is shown in the ported viewer. Quizzes list for everyone and keep attempts for logged-in users. + +## Decisions taken in this plan (Claude's discretion and recorded assumptions) + +- **Port by copy, then edit:** bm's files are copied with `cp` into vue-demo-app and then edited only where Nuxt differs: + - `@/` imports become `~/`, `useRouter().push` becomes `navigateTo`; + - Pinia stores become composables (`useToast`, the existing `useDemoAuth`), bm's localStorage token store becomes the demo cookie; + - Polish UI strings become i18n keys with English and Polish values. + This keeps the port faithful (bm's lib tests will run unchanged in plan 05) and cheap in context. No file under ../sm-bm-app is edited. +- **Fonts:** bm's themes and the remote import `@fontsource` packages. The demo declares the same families through `@nuxt/fonts` (already used by vue-demo-app), and the theme `loadFonts` hooks resolve without imports. No `@fontsource` package is added, so the only new npm package is `centrifuge@5.7.3`, which the research audit approved. +- **SSR boundary:** `routeRules` set `ssr: false` for `/p/**` and `/presentations/**` (about 94 window, document and localStorage touches in the bm views). `/roadmap` and `/quizzes` render on the server. +- **Demo endpoints in golem15.demo (RESEARCH Pattern 3):** no shared plugin changes. + - `GET /_demo/api/v1/roadmap?locale=` gives the slug and public hash of the locale's roadmap deck. + - `GET /_demo/api/v1/quizzes?context=demo:{locale}` gives published quizzes (slug, title, description, question count) to anonymous visitors. + - `GET|POST /_demo/api/v1/presentations` and `GET|PUT /_demo/api/v1/presentations/{slug}`, behind jwt.auth, reuse the presentations plugin's `ManageIndex`, `ManageCreate`, `ManageShow` and `ManageUpdate` handlers. A wrapper rewrites the body first: it clears `css` and `context`, forces `is_published` to false, accepts theme `""`, `atlas` or `monografia` only, and caps the slide count at 60 and the body at 256 KiB. +- **User decks stay unpublished (RESEARCH Pitfall 8):** the owner presents them from the speaker view with the remote (D-15 needs nothing more), so no registrant can host arbitrary pages under demo.summercms.io. The reset job wipes them. +- **Realtime (D-17):** a port of bm's app-plugin wiring (`realtime.go`, `mountRealtime`, the bucket). The committed `config/realtime.yaml` keeps `driver: "null"`, so tests and fresh checkouts mount nothing; the local .env (written by dev.sh, with a local Centrifugo from the installed v6.5.1 binary) and rome's .env switch it on. Centrifugo v6 config keys follow the research; the `static_headers` placement of the subscribe proxy (assumption A3) is validated by `centrifugo checkconfig` and by a live subscribe. +- **Ports:** Centrifugo listens on 127.0.0.1:8010 on rome (or the port plan 02's probe recorded) and on 127.0.0.1:8011 locally. + +## Artifacts this phase produces (this plan) + +- golem15.demo routes: `GET /_demo/api/v1/roadmap`, `GET /_demo/api/v1/quizzes`, `GET|POST /_demo/api/v1/presentations`, `GET|PUT /_demo/api/v1/presentations/{slug}`, `GET /api/realtime/token`, `POST /api/realtime/subscribe` (Centrifugo driver only); bucket `golem15.demo.realtime`; command `websockets:health` (centrifugo.Commands). +- Config: `config/realtime.yaml` (`driver: "null"`), `config/golem15.presentations.yaml` (`public_path: /p`); env keys `SUMMER_REALTIME__DRIVER`, `SUMMER_REALTIME__CENTRIFUGO__{API_URL,API_KEY,TOKEN_SECRET,PROXY_SECRET,WS_URL}`, `SUMMER_GOLEM15__PRESENTATIONS__REMOTE_KEY`. +- Deploy: `deploy/centrifugo.json.example`, `deploy/supervisor/summercms-demo-centrifugo.conf`, nginx locations `= /ws`, `= /api/realtime/token`, `^~ /_demo/`; rome program `summercms-demo-centrifugo` and `/srv/summercms-demo/centrifugo.json`. +- vue-demo-app pages `/roadmap`, `/p/[slugHash]`, `/presentations`, `/presentations/new`, `/presentations/[slug]`, `/presentations/[slug]/pilot`, `/presentations/[slug]/edit`, `/quizzes`, `/quizzes/[slug]`; ported libs under `app/lib/` and `app/lib/deck/`, components under `app/components/deck/` and `app/components/remote/`, `AttemptList.vue`, composable `useToast`; dependency `centrifuge@5.7.3`. +- Seed: quiz `summercms-podstawy` (context `demo:pl`), context `demo:en` on `summercms-basics`, deck `pierwsze-kroki`. +- check-local checks `roadmap`, `realtime`, `decks`, `quizzes`; `scripts/dev.sh snapshot --site`. + + +Bring bm's full presentation system (public, speaker, phone remote, My Presentations) to the demo site on the demo's own Centrifugo (D-14, D-17), let registered users create and present decks (D-15), show the roadmap deck in the ported viewer (D-16), and offer quizzes that anonymous visitors can browse and logged-in users take with saved attempts (D-18), in English and Polish (D-13). + +Purpose: the presentations, realtime and quizzes part of SC-4. D-23: plan 04 of five. +Output: commits in ../sm-demo-app/vue-demo-app and ../sm-demo-app; after the go-ahead, the demo's Centrifugo and a redeploy on rome. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-CONTEXT.md +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-RESEARCH.md +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-PATTERNS.md +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-01-SUMMARY.md +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-03-SUMMARY.md + +Repos: ../sm-demo-app/vue-demo-app and ../sm-demo-app. Read-only sources: ../sm-bm-app/frontend/src (views, components, lib, api, deck CSS, stores), ../sm-bm-app/plugins/jz/bm/{realtime.go,routes.go,plugin.go} (bm's app plugin), ../sm-bm-app/realtime_test.go, ../sm-bm-app/config/realtime.yaml, ../sm-bm-app/deploy/nginx/bm.jakubzych.com.conf (/ws block, lines 60-73), ../sm-bm-app/DEPLOY.md (Realtime section), ../sm-demo-app/plugins/golem15/presentations and ../sm-demo-app/plugins/golem15/quizzes (pinned versions), modules/lighthouse/README.md. + +Rules: no co-author tags; stage by path; the vue-demo-app commits first, then the sm-demo-app pointer; no deploy before the Task 5 go-ahead; Centrifugo secrets are generated on rome and never printed. + + +bm app plugin (read-only): realtime.go (49 lines) builds the lighthouse service in Boot (`lighthouse.From(app)`, `SetUserLookup` reading users.id and name through `app.Lookup[*gorm.DB]()` per call) with the blank import of `.../summercms/modules/lighthouse/centrifugo`; routes.go:43-47 and 69-78 define `Buckets()` (`surf.BucketProvider`, key `{bucket}|u:{id}` or `{bucket}|ip:{client ip}` with `surf.TrustedProxies(app.Config)` and `surf.ClientIP`); routes.go:194-204 `mountRealtime` calls `lighthouse.Mount(r, p.realtime.Driver(), lighthouse.Surfaces{UserAuth: surf.Use("jwt.auth", "presentations.realtime-owner", bucketRealtime), ServerToServer: surf.Use(), Middleware: surf.Use()})` only when `app.HasPlugin("golem15.presentations")`; plugin.go:70-103 Boot and `Commands()` appending `centrifugo.Commands(p.app)`. +bm realtime_test.go: `tokenRoute = "GET /api/realtime/token"`, `subscribeRoute = "POST /api/realtime/subscribe"`, booting through `compass.Open(compass.Options{Dir, Environ})` with SUMMER_ENV=testing. +presentations plugin: `api.New(app)` handlers `PublicDeck`, `Mine`, `Speaker`, `Remote`, `RemoteState`, `ManageIndex`, `ManageCreate`, `ManageShow`, `ManageUpdate` (owner from `bouncer.User`, `mineCaller`); body types `classes.ManagedCreate` (title, slug, css, theme, is_published, todo, target_seconds, context, slides) and `classes.ManagedPatch` (pointer fields); routes `/_presentations/api/v1/public/{slugHash}`, `/mine`, `/mine/{slug}`, `/mine/{slug}/remote`, `/mine/{slug}/remote/state`, images under `/public/{slugHash}/images/{name}` and `/mine/{slug}/images/{name}`; config `public_path` (default /prezentacje) and `remote_key`. +quizzes plugin: `/_quizzes/api/v1` behind jwt.auth (`/quizzes`, `/quizzes/{slug}`, `/quizzes/{slug}/attempts`, `/attempts`, `/attempts/{id}`, `/attempts/{id}/answers`, `/attempts/{id}/finish`); model `Quiz.Context` (`context` column), `is_published`. +bm frontend constants: `lib/realtime.ts` `TOKEN_PATH = '/api/realtime/token'`, `REALTIME_WS_PATH = '/ws'`, dynamic `import('centrifuge')`; api bases `'/_presentations/api/v1'`, `'/_quizzes/api/v1'`, `'/_user/api/v1'`; router names `presentation-public`, `presentation-speaker`, `presentation-remote`, `my-presentations`, `quizzes`, `quiz`. +vue-demo-app today: cookie JWT through `app/utils/authCookie.ts` and `useDemoAuth`; nitro devProxy for the Go paths; @nuxt/fonts families Fraunces and Inter; i18n en/pl from plan 03; TourCard links appear once the target route exists. +Centrifugo v6 config (RESEARCH, rome deployment): client.token.hmac_secret_key, client.allowed_origins, http_api.key, http_server.port, channel.proxy.subscribe.endpoint plus http.static_headers X-Centrifugo-Secret, namespaces [{name: presentations, subscribe_proxy_enabled: true}]. + + + + + + + + Task 1: Tracer: a visitor opens /roadmap and reads the roadmap deck of their language in the ported deck viewer on the demo site + ../sm-demo-app/plugins/demo/plugin.go, ../sm-demo-app/plugins/demo/routes.go, ../sm-demo-app/config/golem15.presentations.yaml, ../sm-demo-app/boot_test.go, ../sm-demo-app/scripts/dev.sh, ../sm-demo-app/scripts/check-local.sh, ../sm-demo-app/vue-demo-app/nuxt.config.ts, ../sm-demo-app/vue-demo-app/app/lib/deck/clock.ts, ../sm-demo-app/vue-demo-app/app/lib/deck/compose.ts, ../sm-demo-app/vue-demo-app/app/lib/deck/engine.ts, ../sm-demo-app/vue-demo-app/app/lib/deck/reading.ts, ../sm-demo-app/vue-demo-app/app/lib/deck/styles.ts, ../sm-demo-app/vue-demo-app/app/lib/deck/themes.ts, ../sm-demo-app/vue-demo-app/app/lib/api/http.ts, ../sm-demo-app/vue-demo-app/app/lib/api/presentations.ts, ../sm-demo-app/vue-demo-app/app/lib/api/demo.ts, ../sm-demo-app/vue-demo-app/app/lib/api/types.ts, ../sm-demo-app/vue-demo-app/app/assets/deck/deck.css, ../sm-demo-app/vue-demo-app/app/assets/deck/themes/atlas.css, ../sm-demo-app/vue-demo-app/app/assets/deck/themes/monografia.css, ../sm-demo-app/vue-demo-app/app/components/deck/DeckViewer.vue, ../sm-demo-app/vue-demo-app/app/components/DemoBanner.vue, ../sm-demo-app/vue-demo-app/app/pages/p/[slugHash].vue, ../sm-demo-app/vue-demo-app/app/pages/roadmap.vue, ../sm-demo-app/vue-demo-app/i18n/locales/en.json, ../sm-demo-app/vue-demo-app/i18n/locales/pl.json + ../sm-bm-app/frontend/src/views/PresentationPublicView.vue, ../sm-bm-app/frontend/src/components/deck/DeckViewer.vue, ../sm-bm-app/frontend/src/lib/deck/*.ts (non-test), ../sm-bm-app/frontend/src/api/http.ts, presentations.ts, types.ts, ../sm-bm-app/frontend/src/deck/deck.css and themes/*.css, ../sm-bm-app/frontend/vite.config.ts (proxy list), ../sm-bm-app/plugins/jz/bm/routes.go (route group and interface asserts), ../sm-demo-app/vue-demo-app/nuxt.config.ts, ../sm-demo-app/vue-demo-app/app/utils/authCookie.ts, ../sm-demo-app/vue-demo-app/app/components/TourCard.vue (plan 03), ../sm-demo-app/boot_test.go, ../sm-demo-app/scripts/deck-snapshot.cjs (plan 01), ../sm-demo-app/plugins/golem15/presentations/config/config.yaml + Per D-14, D-16, D-13. + +Go: +- routes.go makes golem15.demo a route provider (assert pact.HasRoutes) with the group `/_demo/api/v1` and `surf.Use("throttle:120,1")`, and the route `GET /roadmap?locale=en|pl`. It answers `{"slug": ..., "public_hash": ...}` for roadmap or roadmap-pl when that deck is published, and 404 otherwise. Any other locale value falls back to en. +- config/golem15.presentations.yaml sets `public_path: /p`. +- boot_test.go's wanted prefixes gain /_demo/api/v1. + +Nuxt: +- Copy bm's lib/deck/{clock,compose,engine,reading,styles,themes}.ts (not the tests; plan 05 ports them), DeckViewer.vue, deck.css and the two theme CSS files into the listed paths. Fix the import paths. +- In themes.ts, replace the font-package dynamic imports with a resolved promise. nuxt.config's fonts.families gain the theme and viewer families (Source Serif 4, IBM Plex Mono, Libre Caslon Display, IBM Plex Sans, Courier Prime, keeping Fraunces and Inter), so the same font names resolve. +- Port bm's api/http.ts with a token store backed by the demo cookie (utils/authCookie.ts), plus api/presentations.ts and types.ts. api/demo.ts calls /_demo/api/v1/roadmap. +- pages/p/[slugHash].vue is the PresentationPublicView port: bare layout (definePageMeta layout false) plus DemoBanner in a compact variant (one line) that hides while the document is in browser fullscreen, so D-10 holds on every page and a presenter still gets a clean full screen; document title "SummerCMS demo". +- pages/roadmap.vue reads the active locale, fetches the roadmap hash (server-side through runtimeConfig demo.apiBase, NUXT_DEMO_API_BASE) and navigates to /p/{hash} with a 302. +- nuxt.config: + - routeRules `'/p/**': { ssr: false }`; + - nitro devProxy adds /_presentations, /_quizzes, /_demo and /api/realtime; + - runtimeConfig.demo.apiBase. +- The TourCard Presentations link (target /roadmap) now resolves and shows. + +Scripts: +- dev.sh `snapshot --site [slug...]` runs deck-snapshot.cjs against http://127.0.0.1:3020 with path /p/. +- check-local.sh check `roadmap`: GET /roadmap with the pl cookie answers a redirect to /p/{the roadmap-pl hash}, and without the cookie to the roadmap hash; GET /_demo/api/v1/roadmap?locale=xx falls back to en. + + go -C ../sm-demo-app vet . ./plugins/demo/... && go -C ../sm-demo-app test -count=1 -short -run '^TestBootAllPlugins$' -v . 2>&1 | grep -q -- '^--- PASS: TestBootAllPlugins' && pnpm --dir ../sm-demo-app/vue-demo-app build && ../sm-demo-app/scripts/dev.sh up && ../sm-demo-app/scripts/check-local.sh roadmap | tail -1 | grep -q '^check-local: ok' && ../sm-demo-app/scripts/dev.sh snapshot --site roadmap roadmap-pl 2>&1 | grep -c '^snapshot: roadmap' | grep -qx 2 + Non-zero exit: vet red; TestBootAllPlugins fails (the /_demo/api/v1 prefix is not routed or a plugin no longer activates); the Nuxt build fails (an unported import); check-local roadmap fails (wrong redirect per locale); or fewer than two "snapshot: roadmap" lines (a slide renders empty or an OIDC image fails to load in the ported viewer). + + + - `grep -c "'/p/\*\*'" ../sm-demo-app/vue-demo-app/nuxt.config.ts` prints at least 1. + - `grep -rc '@fontsource' ../sm-demo-app/vue-demo-app/app ../sm-demo-app/vue-demo-app/package.json | grep -v ':0$'` prints nothing. + - `grep -c 'public_path: /p' ../sm-demo-app/config/golem15.presentations.yaml` prints 1. + - `diff -q ../sm-bm-app/frontend/src/lib/deck/engine.ts ../sm-demo-app/vue-demo-app/app/lib/deck/engine.ts` reports no difference, or the SUMMARY lists every change and why. + + The roadmap deck opens from the tour in the visitor's language in the ported viewer on the demo's own site. + + + + Task 2: The deck owner presents from the speaker view and drives it from the phone remote through the demo's own Centrifugo; My Presentations lists their decks + ../sm-demo-app/plugins/demo/realtime.go, ../sm-demo-app/plugins/demo/routes.go, ../sm-demo-app/plugins/demo/plugin.go, ../sm-demo-app/config/realtime.yaml, ../sm-demo-app/boot_test.go, ../sm-demo-app/deploy/centrifugo.json.example, ../sm-demo-app/deploy/env.example, ../sm-demo-app/scripts/dev.sh, ../sm-demo-app/scripts/check-local.sh, ../sm-demo-app/vue-demo-app/package.json, ../sm-demo-app/vue-demo-app/pnpm-lock.yaml, ../sm-demo-app/vue-demo-app/nuxt.config.ts, ../sm-demo-app/vue-demo-app/app/lib/realtime.ts, ../sm-demo-app/vue-demo-app/app/lib/pilot.ts, ../sm-demo-app/vue-demo-app/app/lib/deck/remote.ts, ../sm-demo-app/vue-demo-app/app/assets/deck/deck-speaker.css, ../sm-demo-app/vue-demo-app/app/components/deck/SpeakerTools.vue, ../sm-demo-app/vue-demo-app/app/components/remote/RemoteDrawer.vue, ../sm-demo-app/vue-demo-app/app/components/remote/RemoteKeys.vue, ../sm-demo-app/vue-demo-app/app/components/remote/RemoteTopBar.vue, ../sm-demo-app/vue-demo-app/app/composables/useToast.ts, ../sm-demo-app/vue-demo-app/app/pages/presentations/index.vue, ../sm-demo-app/vue-demo-app/app/pages/presentations/[slug]/index.vue, ../sm-demo-app/vue-demo-app/app/pages/presentations/[slug]/pilot.vue, ../sm-demo-app/vue-demo-app/i18n/locales/en.json, ../sm-demo-app/vue-demo-app/i18n/locales/pl.json + ../sm-bm-app/plugins/jz/bm/realtime.go, ../sm-bm-app/plugins/jz/bm/routes.go (Buckets, mountRealtime), ../sm-bm-app/plugins/jz/bm/plugin.go (Boot, Commands), ../sm-bm-app/realtime_test.go, ../sm-bm-app/config/realtime.yaml, ../sm-bm-app/DEPLOY.md (Realtime section), modules/lighthouse/README.md, ../sm-bm-app/frontend/src/lib/realtime.ts, lib/pilot.ts, lib/deck/remote.ts, components/deck/SpeakerTools.vue, components/remote/*.vue, views/PresentationSpeakerView.vue, views/PresentationRemoteView.vue, views/MyPresentationsView.vue, stores/toast.ts, stores/auth.ts, deck/deck-speaker.css, ../sm-demo-app/plugins/golem15/presentations/middleware.go and realtime.go (presentations.realtime-owner, remote channel), 19.4.1-RESEARCH.md (Pattern 2, rome deployment: Centrifugo config, Go realtime env) + Per D-14, D-17, D-15. + +Go: +- realtime.go is ported verbatim from bm's app plugin (package demo, error prefix "golem15.demo:"). +- plugin.go gains a realtime field, wires it in Boot and appends centrifugo.Commands(p.app) to Commands(). +- routes.go adds Buckets() with "golem15.demo.realtime" (120 a minute, keyed per user, else per IP, as bm) and mountRealtime with the bm surfaces, using bucket "throttle:golem15.demo.realtime". +- config/realtime.yaml commits `driver: "null"` (quoted) with empty secrets. +- boot_test.go gets a sub-test that sets realtime.driver centrifugo plus test secrets and asserts the token route and the subscribe route are routed. + +Local Centrifugo: +- dev.sh writes, when missing, SUMMER_REALTIME__DRIVER=centrifugo, API_URL http://127.0.0.1:8011/api, generated API_KEY, TOKEN_SECRET and PROXY_SECRET, WS_URL /ws, and SUMMER_GOLEM15__PRESENTATIONS__REMOTE_KEY to .env. +- It generates logs/centrifugo.json from deploy/centrifugo.json.example with those values, port 8011 and allowed origin http://127.0.0.1:3020, and starts `centrifugo -c logs/centrifugo.json` (pid name centrifugo) in `up` when the binary is installed. It warns and runs without realtime otherwise. +- The Nuxt devProxy sends /ws (ws: true) to 127.0.0.1:8011/connection/websocket. +- deploy/centrifugo.json.example holds the v6 keys from the interfaces block with placeholder values and a header comment (install path /srv/summercms-demo/centrifugo.json, mode 0640, the Go port of the subscribe proxy). env.example documents the realtime keys. + +Nuxt: +- `pnpm --dir vue-demo-app add centrifuge@5.7.3` (exact; the audited pin). +- Copy lib/realtime.ts (the token client reads the demo cookie's JWT), lib/pilot.ts, lib/deck/remote.ts, deck-speaker.css, SpeakerTools.vue and the three remote components. useToast replaces bm's toast store. +- Pages: + - presentations/index.vue (My Presentations: login required, else a link to /login with a return path; lists /_presentations/api/v1/mine with links to speaker, remote and, for published decks, public); + - presentations/[slug]/index.vue (PresentationSpeakerView); + - presentations/[slug]/pilot.vue (PresentationRemoteView). + Polish strings become i18n keys. The bare speaker and remote pages carry the compact DemoBanner as the public deck does (D-10). routeRules add `'/presentations/**': { ssr: false }`. + +check-local.sh check `realtime`: +- GET /api/realtime/token without a JWT answers 401; +- with the visitor's JWT (logged in through /_user/api/v1/login, credentials from the local .env) it answers 200 with a token; +- POST /_presentations/api/v1/mine/roadmap/remote with a "next" action answers 2xx (Go published through Centrifugo's HTTP API); +- `go run` or bin/demo `websockets:health` prints its OK line; +- `centrifugo checkconfig -c logs/centrifugo.json` exits 0. + + go -C ../sm-demo-app vet . ./plugins/demo/... && go -C ../sm-demo-app test -count=1 -short -run '^TestBootAllPlugins' -v . 2>&1 | grep -q -- '^--- PASS: TestBootAllPlugins' && centrifugo checkconfig -c ../sm-demo-app/deploy/centrifugo.json.example && pnpm --dir ../sm-demo-app/vue-demo-app build && ../sm-demo-app/scripts/dev.sh up && ../sm-demo-app/scripts/check-local.sh realtime | tail -1 | grep -q '^check-local: ok' + Non-zero exit: vet red; TestBootAllPlugins (or its realtime sub-test) fails because the realtime routes are not mounted with the driver set; centrifugo checkconfig rejects the template (assumption A3 wrong); the Nuxt build fails; or check-local realtime does not print "check-local: ok" (the token route open without a JWT, no token for the owner, the remote publish refused, or websockets:health failing). + + + - `grep -c '"centrifuge": "5.7.3"' ../sm-demo-app/vue-demo-app/package.json` prints 1. + - `grep -c 'driver: "null"' ../sm-demo-app/config/realtime.yaml` prints 1. + - `grep -c 'golem15.demo.realtime' ../sm-demo-app/plugins/demo/routes.go` prints at least 1. + - `grep -c "'/presentations/\*\*'" ../sm-demo-app/vue-demo-app/nuxt.config.ts` prints at least 1. + + A deck owner opens the speaker view, pairs the phone remote and moves slides through the demo's own Centrifugo locally; My Presentations lists their decks. + + + + Task 3: Any registered user writes a deck in markdown, edits it and presents it; the demo never publishes it or keeps custom CSS + ../sm-demo-app/plugins/demo/decks_api.go, ../sm-demo-app/plugins/demo/routes.go, ../sm-demo-app/boot_test.go, ../sm-demo-app/scripts/check-local.sh, ../sm-demo-app/vue-demo-app/app/lib/markdown.ts, ../sm-demo-app/vue-demo-app/app/lib/forms.ts, ../sm-demo-app/vue-demo-app/app/lib/api/demo.ts, ../sm-demo-app/vue-demo-app/app/pages/presentations/new.vue, ../sm-demo-app/vue-demo-app/app/pages/presentations/[slug]/edit.vue, ../sm-demo-app/vue-demo-app/app/pages/presentations/index.vue, ../sm-demo-app/vue-demo-app/i18n/locales/en.json, ../sm-demo-app/vue-demo-app/i18n/locales/pl.json + ../sm-demo-app/plugins/golem15/presentations/classes/manage.go (ManagedCreate, ManagedPatch, validation, Clean on every HTML field), ../sm-demo-app/plugins/golem15/presentations/controllers/api/manage.go (handlers, body decoding, mineCaller), ../sm-demo-app/plugins/golem15/presentations/routes.go, ../sm-bm-app/frontend/src/lib/markdown.ts and lib/forms.ts, 19.4.1-RESEARCH.md (Pattern 3, Pitfall 8) + Per D-15 (any registered user creates and presents decks) and RESEARCH Pitfall 8. + +Go, decks_api.go: +- A group `/_demo/api/v1/presentations` with surf.Use("jwt.auth", "throttle:20,1") and a 256 KiB body limit: + - GET "" maps to h.ManageIndex; + - POST "" maps to h.ManageCreate behind a wrapper; + - GET /{slug} maps to h.ManageShow; + - PUT /{slug} maps to h.ManageUpdate behind a wrapper (slug pattern as the plugin's). +- The wrappers decode the body into classes.ManagedCreate or classes.ManagedPatch: + - empty CSS and Context, IsPublished false (create) or unset/false (patch); + - a theme outside "", "atlas" and "monografia" gives 422 with a field error; + - more than 60 slides gives 422; + - then re-encode the body and call the plugin handler, which keeps classes.Clean on every HTML field and the owner check. +- boot_test.go asserts the routes are mounted. + +Nuxt: +- Copy lib/markdown.ts and lib/forms.ts. +- presentations/new.vue and [slug]/edit.vue are a form: title, slug (generated from the title, editable), theme select (default, atlas, monografia), and an ordered list of slides, each with a title, a markdown body and optional speaker notes, rendered through lib/markdown.ts into body_html and say_html. Save posts or puts to the demo endpoint, shows field errors and returns to My Presentations. +- My Presentations gains "New deck" and "Edit" actions, and a note that personal decks stay private and are removed when the demo resets. + +check-local.sh check `decks`, with the visitor's JWT: +- POST without a JWT answers 401; +- POST with title, theme atlas, css "body{display:none}", is_published true, context "x" and two slides, one containing a script element, answers 201; +- GET shows css empty, context empty, is_published false and no script element; +- theme "evil" answers 422; +- PUT with three slides answers 200 and GET shows three slides. + + go -C ../sm-demo-app vet . ./plugins/demo/... && go -C ../sm-demo-app test -count=1 -short -run '^TestBootAllPlugins' . && pnpm --dir ../sm-demo-app/vue-demo-app build && ../sm-demo-app/scripts/dev.sh up && ../sm-demo-app/scripts/check-local.sh decks | tail -1 | grep -q '^check-local: ok' + Non-zero exit: vet or the boot test red (routes missing); the Nuxt build fails; or check-local decks does not print "check-local: ok" (an anonymous create accepted, CSS or context kept, a deck published, a script element kept, an unknown theme accepted, or the update not applied). + + + - `grep -c 'ManageCreate\|ManageUpdate' ../sm-demo-app/plugins/demo/decks_api.go` prints at least 2. + - `grep -c '"atlas"' ../sm-demo-app/plugins/demo/decks_api.go` prints at least 1. + - `grep -c 'markdown' ../sm-demo-app/vue-demo-app/app/pages/presentations/new.vue` prints at least 1. + + Registered users write, edit and present their own decks; nothing they write is published or styled beyond the fixed themes. + + + + Task 4: Anonymous visitors browse the quizzes of their language; logged-in users take one, see the score and their past attempts + ../sm-demo-app/plugins/demo/quizzes_api.go, ../sm-demo-app/plugins/demo/routes.go, ../sm-demo-app/plugins/demo/console/seed.go, ../sm-demo-app/plugins/demo/console/seed_locales.go, ../sm-demo-app/boot_test.go, ../sm-demo-app/scripts/check-local.sh, ../sm-demo-app/vue-demo-app/app/lib/quizSession.ts, ../sm-demo-app/vue-demo-app/app/lib/api/quizzes.ts, ../sm-demo-app/vue-demo-app/app/components/AttemptList.vue, ../sm-demo-app/vue-demo-app/app/pages/quizzes/index.vue, ../sm-demo-app/vue-demo-app/app/pages/quizzes/[slug].vue, ../sm-demo-app/vue-demo-app/i18n/locales/en.json, ../sm-demo-app/vue-demo-app/i18n/locales/pl.json + ../sm-demo-app/plugins/golem15/quizzes/routes.go, ../sm-demo-app/plugins/golem15/quizzes/models/quiz.go (Context, is_published), the quizzes plugin's list handler (the fields it returns), ../sm-bm-app/frontend/src/views/QuizzesView.vue, QuizView.vue, components/AttemptList.vue, lib/quizSession.ts, api/quizzes.ts, ../sm-demo-app/plugins/demo/console/seed.go (seedQuiz, seedPresentation) + Per D-18 and D-13. + +Go: +- quizzes_api.go: `GET /_demo/api/v1/quizzes?context=demo:en|demo:pl` (throttle:60,1, no auth) returns the published quizzes whose context matches, each with slug, title, description and question_count. It never returns questions, answers or correctness. A context outside the two values answers 422. +- Seed: + - set context demo:en on summercms-basics when it is null; + - add summercms-podstawy (context demo:pl, the same three questions and answers in Polish); + - add the Polish twin of the getting-started deck, pierwsze-kroki (unowned, published, Polish slides). + Each step skips what exists. +- boot_test.go asserts the route. + +Nuxt: +- Copy lib/quizSession.ts, api/quizzes.ts and AttemptList.vue. +- quizzes/index.vue (SSR) lists the quizzes of the active locale's context from the demo endpoint. Anonymous visitors see a "Register to take this quiz" prompt linking to /register with a return path. Logged-in users see a Start link and, below, their past attempts (AttemptList over /_quizzes/api/v1/attempts). +- quizzes/[slug].vue ports QuizView (login required, else a redirect to /login with a return path): it starts an attempt, answers, finishes, shows the score and the pass state, and links back to the list with the attempts. +- The tour's Quizzes card link now resolves. + +check-local.sh check `quizzes`: +- anonymous GET /_demo/api/v1/quizzes?context=demo:pl contains summercms-podstawy and neither a correctness flag nor any answer text; +- GET /_quizzes/api/v1/quizzes without a JWT answers 401; +- with the visitor's JWT, starting an attempt on summercms-basics, answering and finishing answers 2xx; +- GET /_quizzes/api/v1/attempts lists it. + + go -C ../sm-demo-app vet . ./plugins/demo/... && go -C ../sm-demo-app test -count=1 -short -run '^TestBootAllPlugins' . && pnpm --dir ../sm-demo-app/vue-demo-app build && ../sm-demo-app/scripts/dev.sh up && ../sm-demo-app/scripts/dev.sh seed && ../sm-demo-app/scripts/check-local.sh quizzes | tail -1 | grep -q '^check-local: ok' + Non-zero exit: vet or the boot test red; the Nuxt build fails; the seed fails; or check-local quizzes does not print "check-local: ok" (the Polish quiz missing, answers leaked to anonymous visitors, the quiz API open without a JWT, or the attempt not recorded). + + + - `grep -c 'summercms-podstawy' ../sm-demo-app/plugins/demo/console/seed_locales.go ../sm-demo-app/plugins/demo/console/seed.go | grep -v ':0$' | wc -l` prints at least 1. + - `grep -c 'is_correct' ../sm-demo-app/plugins/demo/quizzes_api.go` prints 0. + - `grep -c 'AttemptList' ../sm-demo-app/vue-demo-app/app/pages/quizzes/index.vue` prints at least 1. + + Everyone sees the quizzes of their language; only logged-in users take them, and their scores and attempts are kept. + + + + Task 5: Go-ahead to add the demo's own Centrifugo on rome and redeploy + ../sm-demo-app/DEPLOY.md, ../sm-demo-app/deploy/centrifugo.json.example, the check-local outputs of Tasks 1-4 + + - The user chose one option, and for "deploy" Claude has re-read rome's free ports (the Centrifugo port is still free). + + Deploy the presentations port, quizzes and the demo's own Centrifugo to demo.summercms.io now? + Deploying changes rome: a third supervisor program (the demo's Centrifugo on its own loopback port with its own secrets, D-17), three nginx locations (/ws, /api/realtime/token, /_demo/) and new .env keys, then the usual release. Nothing here is one-way: the DEPLOY.md rollback restores the previous binary and site, and the Centrifugo program can be stopped. Optional before deciding: review locally with `scripts/dev.sh up` (Nuxt on 127.0.0.1:3020; /roadmap, /presentations, /quizzes). + + + + + Reply "deploy" or "hold". + + + + Task 6: The presentations, the phone remote and the quizzes are live on demo.summercms.io with the demo's own Centrifugo + The Task 5 reply was "deploy"; `ssh -p 2222 debian@10.8.3.118 command -v centrifugo` or bm's supervisor command names a Centrifugo v6 binary. + ../sm-demo-app/deploy/supervisor/summercms-demo-centrifugo.conf, ../sm-demo-app/deploy/nginx/demo.summercms.io.conf, ../sm-demo-app/scripts/check-deploy.sh, ../sm-demo-app/scripts/check-live.sh, ../sm-demo-app/DEPLOY.md, ../sm-demo-app/README.md, ../sm-demo-app/vue-demo-app + ../sm-demo-app/DEPLOY.md, ../sm-bm-app/deploy/supervisor/bm.conf and ../sm-bm-app/DEPLOY.md (Centrifugo program, websockets:health), ../sm-bm-app/deploy/nginx/bm.jakubzych.com.conf (lines 60-73), ../sm-demo-app/deploy/centrifugo.json.example, ../sm-demo-app/scripts/check-deploy.sh, ../sm-demo-app/scripts/check-live.sh + Per D-17, D-14, D-18. + +- deploy/supervisor/summercms-demo-centrifugo.conf runs the rome Centrifugo binary with -c /srv/summercms-demo/centrifugo.json as user summercms-demo (summercms.io program settings). +- The nginx vhost adds: + - `location = /ws` to 127.0.0.1:8010/connection/websocket with the upgrade headers and 86400 s timeouts (bm block); + - `location = /api/realtime/token` and `location ^~ /_demo/` to Go. + It still has no location for /api/realtime/subscribe. +- check-deploy.sh adds: + - `centrifugo checkconfig -c deploy/centrifugo.json.example`; + - the /ws port equals the template's http_server port; + - the subscribe proxy endpoint's port equals the Go program's port; + - no nginx location names /api/realtime/subscribe. +- On rome: + - generate centrifugo.json from the template with fresh secrets (openssl rand), allowed origin https://demo.summercms.io (mode 0640 root:summercms-demo, values never printed); + - append the matching SUMMER_REALTIME__* keys and SUMMER_GOLEM15__PRESENTATIONS__REMOTE_KEY to .env; + - install the program and the vhost (nginx -t, reload); + - run the DEPLOY.md release (upload, migrate, demo:seed, swap, restart); + - run `bin/demo websockets:health`. +- check-live.sh adds: + - /roadmap answers a redirect to /p/; + - /p/{hash} answers 200; + - /_demo/api/v1/quizzes?context=demo:en answers 200 JSON; + - /api/realtime/token without a JWT answers 401; + - /api/realtime/subscribe does not reach Go (404 from Nuxt); + - a WebSocket upgrade request to /ws answers 101. +- DEPLOY.md gets a Realtime section (the demo's own instance, keys by name, websockets:health after each deploy, no shared secret with bm). Push vue-demo-app, then the sm-demo-app pointer and the deploy changes. + + ../sm-demo-app/scripts/check-deploy.sh 2>&1 | tail -1 | grep -qx 'check-deploy: ok' && ../sm-demo-app/scripts/check-live.sh https://demo.summercms.io 2>&1 | tail -1 | grep -qx 'check-live: ok' && test "$(git -C ../sm-demo-app ls-remote origin refs/heads/master | cut -f1)" = "$(git -C ../sm-demo-app rev-parse HEAD)" + Non-zero exit: check-deploy fails (Centrifugo template invalid, ports inconsistent, the subscribe route exposed); check-live fails (roadmap redirect, public deck, quiz list, token route or the /ws upgrade broken, or any earlier check regressed); or the sm-demo-app push is missing. + + + - `ssh -p 2222 debian@10.8.3.118 sudo supervisorctl status summercms-demo-centrifugo` shows RUNNING. + - `grep -c 'api/realtime/subscribe' ../sm-demo-app/deploy/nginx/demo.summercms.io.conf` prints 0. + - `ssh -p 2222 debian@10.8.3.118 sudo stat -c '%a %U:%G' /srv/summercms-demo/centrifugo.json` prints `640 root:summercms-demo`. + + The demo serves the ported presentation system, the phone remote over its own Centrifugo, user decks and quizzes from rome. + + + + Task 7: The user checks the presentations, the remote, the OIDC screenshots and the quizzes on the live demo + The check-live output of Task 6 + + - The user replied "approved", or the issues they listed (including replaced screenshots, re-captured with `scripts/dev.sh capture-oidc` and committed) are fixed, redeployed, check-live is green, and the user approved. + + On https://demo.summercms.io: /roadmap opens the roadmap deck of your language in the ported viewer; the speaker view, the phone remote (over the demo's own Centrifugo) and My Presentations; your own decks written in markdown; quizzes with a register prompt for anonymous visitors and saved attempts for logged-in users. + + 1. Open https://demo.summercms.io/roadmap in English and in Polish, and step through to the migration section: are the OIDC screenshots (consent, branded error, Clients admin) the ones you want to show? If not, say which to replace. + 2. Log in as the visitor (credentials in /srv/summercms-demo/.env on rome) and open /presentations, then the roadmap deck's speaker view. Open the remote on your phone (the pilot link or QR code in the speaker tools) and change slides from the phone. The speaker view follows. + 3. Create a deck at /presentations/new with two markdown slides, present it, and drive it from the phone. + 4. Log out and open /quizzes: you see the list and the register prompt. Log in, take the quiz, see the score, then the attempt in the list. + + Reply "approved", or describe what is wrong. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| registered user to /_demo/api/v1/presentations | untrusted HTML, markdown and settings stored on the demo origin | +| browser to /api/realtime/token and /ws | authenticated users obtain realtime credentials | +| Centrifugo to /api/realtime/subscribe | server-to-server authorization over loopback | +| Go to the Centrifugo HTTP API | publishes slide positions with an API key | +| anonymous visitor to /_demo/api/v1/quizzes | public read of quiz metadata | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-19.4.1-25 | Tampering | stored XSS and phishing through user decks | high | mitigate | classes.Clean (bluemonday, no anchors, bare image names) on every HTML field by the plugin handlers; the demo wrapper clears CSS and context, forces unpublished, allows two fixed themes; the reset wipes user decks; check-local decks asserts each rule | +| T-19.4.1-26 | Elevation of privilege | cross-app realtime token reuse | high | mitigate | the demo's own Centrifugo with its own HMAC secret (D-17), tokens only for deck owners (presentations.realtime-owner), HMAC-tagged channel names sent only to the owner | +| T-19.4.1-27 | Spoofing | the subscribe proxy | high | mitigate | not routed by nginx (check-deploy asserts it), constant-time X-Centrifugo-Secret check in lighthouse, ownership read from the database on every subscribe | +| T-19.4.1-28 | Information disclosure | Centrifugo API key, HMAC secret, proxy secret, remote key | high | mitigate | generated on rome into centrifugo.json and .env (0640 root:service), never printed; the template holds placeholders; websockets:health never prints the API key | +| T-19.4.1-29 | Spoofing | WebSocket connections from other sites | medium | mitigate | client.allowed_origins limited to https://demo.summercms.io; tokens required for every connection | +| T-19.4.1-30 | Denial of service | deck writes and realtime token route | medium | mitigate | throttle:20,1 and a 256 KiB body limit on deck writes, at most 60 slides, the golem15.demo.realtime bucket (120 a minute per user) on the token route, the plugin's remote buckets on remote actions | +| T-19.4.1-31 | Information disclosure | quiz answers to anonymous visitors | medium | mitigate | the demo list returns slug, title, description and question count only; the quiz API itself stays behind jwt.auth; check-local quizzes greps for correctness flags | +| T-19.4.1-32 | Elevation of privilege | another user's decks | medium | mitigate | the plugin's mineCaller scopes every manage and speaker call to the JWT's user; the demo wrapper adds no owner parameter | +| T-19.4.1-SC | Tampering | npm/pip/cargo installs | high | mitigate | one package, centrifuge@5.7.3, exact pin from the RESEARCH legitimacy audit (approved, already in bm); fonts through @nuxt/fonts instead of new @fontsource packages; no Go dependency added (lighthouse/centrifugo is part of the framework) | + + + +- Task 1: boot test, Nuxt build, check-local roadmap, both decks rendered by the ported viewer (snapshot --site). +- Task 2: boot test with the realtime sub-test, centrifugo checkconfig, check-local realtime (token, remote publish, websockets:health). +- Task 3: check-local decks (auth, CSS, context, publish, sanitizing, theme, update). +- Task 4: check-local quizzes (locale list, no answers, attempt saved). +- Task 6: check-deploy and check-live on demo.summercms.io. +- Task 7: the user approves the presentation system, the remote, the screenshots and the quizzes live. + + + +The full bm presentation system (public, speaker, phone remote, own decks) runs on the demo's own Centrifugo; the roadmap deck generated from ROADMAP.md shows the features with URLs and the OIDC provider as screenshots; quizzes keep logged-in attempts (SC-4). + + + +Create `.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-04-SUMMARY.md` when done (each bm file ported and its changes, the Centrifugo binary path and ports on rome, assumption A3 outcome, no secret values). + diff --git a/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-05-PLAN.md b/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-05-PLAN.md new file mode 100644 index 00000000..119d9505 --- /dev/null +++ b/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-05-PLAN.md @@ -0,0 +1,413 @@ +--- +phase: 19.4.1-early-public-demo-on-demo-summercms-io +plan: 05 +type: execute +wave: 5 +depends_on: ["19.4.1-01", "19.4.1-02", "19.4.1-03", "19.4.1-04"] +files_modified: + - ../sm-demo-app/go.mod + - ../sm-demo-app/go.sum + - ../sm-demo-app/internal/demotest/postgres.go + - ../sm-demo-app/testmain_test.go + - ../sm-demo-app/go_mod_test.go + - ../sm-demo-app/realtime_test.go + - ../sm-demo-app/demo_api_test.go + - ../sm-demo-app/turnstile_test.go + - ../sm-demo-app/boot_test.go + - ../sm-demo-app/plugins/demo/roadmap/roadmap_test.go + - ../sm-demo-app/seed_test.go + - ../sm-demo-app/reset_test.go + - ../sm-demo-app/plugins/demo/console/visitor_test.go + - ../sm-demo-app/plugins/demo/schedule_test.go + - ../sm-demo-app/scripts/smoke.sh + - ../sm-demo-app/scripts/check-all.sh + - ../sm-demo-app/scripts/check-live.sh + - ../sm-demo-app/scripts/deck-snapshot.cjs + - ../sm-demo-app/README.md + - ../sm-demo-app/DEPLOY.md + - ../sm-demo-app/vue-demo-app + - ../sm-demo-app/vue-demo-app/package.json + - ../sm-demo-app/vue-demo-app/pnpm-lock.yaml + - ../sm-demo-app/vue-demo-app/vitest.config.ts + - ../sm-demo-app/vue-demo-app/README.md + - ../sm-demo-app/vue-demo-app/app/lib/deck/clock.test.ts + - ../sm-demo-app/vue-demo-app/app/lib/deck/compose.test.ts + - ../sm-demo-app/vue-demo-app/app/lib/deck/engine.test.ts + - ../sm-demo-app/vue-demo-app/app/lib/deck/reading.test.ts + - ../sm-demo-app/vue-demo-app/app/lib/deck/remote.test.ts + - ../sm-demo-app/vue-demo-app/app/lib/deck/styles.test.ts + - ../sm-demo-app/vue-demo-app/app/lib/deck/themes.test.ts + - ../sm-demo-app/vue-demo-app/app/lib/realtime.test.ts + - ../sm-demo-app/vue-demo-app/app/lib/pilot.test.ts + - ../sm-demo-app/vue-demo-app/app/lib/quizSession.test.ts + - ../sm-demo-app/vue-demo-app/app/lib/markdown.test.ts + - ../sm-demo-app/vue-demo-app/app/lib/api/http.test.ts + - ../sm-demo-app/vue-demo-app/app/utils/journalLocale.test.ts + - ../sm-demo-app/vue-demo-app/app/utils/buildUpdate.test.ts + - ../fonoteka.go/plugins/golem15/user/turnstile_routes_test.go + - ../sm-journal-plugin/journal_api_authors_test.go + - .planning/todos/pending/journal-author-profile-fields.md + - .planning/todos/done/journal-author-profile-fields.md + - .planning/todos/pending/2026-10-11-reload-open-apps-when-a-new-version-is-deployed.md +autonomous: false +requirements: [SC-1, SC-2, SC-3, SC-4, SC-5, D-01, D-03, D-05, D-06, D-07, D-09, D-10, D-13, D-15, D-16, D-17, D-18, D-19, D-20, D-22, D-23] +estimate: + tokens: 300000 + raw_tokens: 300000 + tasks: 7 + confidence: low +must_haves: + truths: + - "SC-5: every behaviour row of 19.4.1-VALIDATION.md has a passing automated test under the name it lists: TestGoModRequiresTags, TestRealtime*, TestDemoQuizList and TestBootAllPlugins (sm-demo-app root); TestSeedRoadmapUpsert, TestSeedLocales and TestResetKeepsPlans (sm-demo-app root, which compiles every plugin in); TestRoadmap* (plugins/demo/roadmap), TestSeedVisitorFromConfig (plugins/demo/console) and TestResetSchedule (plugins/demo); TestTurnstileRoutes (sm-user-plugin); TestJournalAuthorProfile (sm-journal-plugin); the vitest suite of vue-demo-app; and the scripts build.sh release, check-pins.sh, check-deploy.sh and check-live.sh." + - "SC-1, D-03: TestGoModRequiresTags parses go.mod with golang.org/x/mod/modfile and fails on any replace directive, any golem15 requirement that is not a semver tag, and any shared plugin of summer.yaml without a requirement." + - "D-16, D-19, D-20: TestRoadmap* pin the parser (done, open, completion dates, the Phase Details stop), the section order, the pxpx and MCP content, EN/PL parity, no client name and no anchor in either deck, and a Polish title for every phase; TestSeedRoadmapUpsert pins that a second seed keeps public_hash, replaces slides and leaves no duplicate image." + - "D-06, D-09: TestSeedVisitorFromConfig pins config-only credentials (skipped without an email, refused with a short password); TestResetSchedule pins the disabled default and the enabled entry; TestResetKeepsPlans pins that a reset removes frontend users and demo content and keeps admins, plans, settings, locales, the sitemap and the roadmap hashes." + - "D-07, D-15, D-17, D-18: TestDemoTurnstile refuses a token-less register with 422 and accepts a verified one (fake siteverify); TestRealtime* (port of bm's realtime_test.go) pin the token and subscribe routes, their middleware and the bucket; TestDemoDeckWrites pins CSS and context stripping, unpublished decks, the theme list and the 401 without a JWT; TestDemoQuizList pins the anonymous list without answers." + - "D-14, D-13, D-22: bm's deck, remote, clock, realtime, pilot, quizSession, markdown and http tests run unchanged under vitest 3.2.7 in vue-demo-app, next to tests for applyJournalLocale and isNewerBuild." + - "D-10, D-05: smoke.sh asserts the banner on every server-rendered page and the absence of the admin path and credentials; deck-snapshot.cjs --expect-banner asserts the compact banner on the client-only deck pages." + - "Live (SC-2): scripts/check-live.sh https://demo.summercms.io and scripts/check-all.sh pass at the end of the phase." + - "Todos: journal-author-profile-fields (esrp4qtx) moves to todos/done with its g15office task set to Live; the reload-on-deploy todo (ltcvqext) stays pending with a progress note that the site half shipped in vue-demo-app and the admin half (boardwalk, a framework tag) remains (D-22)." + - "D-01: ROADMAP.md keeps Phase 19.4.1 between 19.4 and 19.5 in the phase list, and Phase 20's goal names this live demo as its base." + artifacts: + - path: "../sm-demo-app/internal/demotest/postgres.go" + provides: "shared Postgres harness: testcontainers postgres:15, every plugin migrated, a booted app" + contains: "testcontainers" + - path: "../sm-demo-app/go_mod_test.go" + provides: "TestGoModRequiresTags" + contains: "modfile" + - path: "../sm-demo-app/realtime_test.go" + provides: "TestRealtime* ported from bm" + contains: "GET /api/realtime/token" + - path: "../sm-demo-app/reset_test.go" + provides: "TestResetKeepsPlans (TestResetSchedule lives in plugins/demo/schedule_test.go)" + contains: "TestResetKeepsPlans" + - path: "../sm-demo-app/vue-demo-app/vitest.config.ts" + provides: "vitest node environment over app/**/*.test.ts" + contains: "app/**/*.test.ts" + - path: "../sm-demo-app/scripts/check-all.sh" + provides: "the phase gate: every suite and check in one run" + contains: "check-all: ok" + key_links: + - from: "../sm-demo-app/testmain_test.go" + to: "../sm-demo-app/internal/demotest/postgres.go" + via: "TestMain calls flag.Parse, then starts the shared harness only when !testing.Short()" + pattern: "testing\\.Short\\(\\)" + - from: "../sm-demo-app/scripts/check-all.sh" + to: "../sm-demo-app/scripts/check-live.sh" + via: "the gate ends with the live check of https://demo.summercms.io" + pattern: "check-live\\.sh" + - from: "../sm-demo-app/vue-demo-app/vitest.config.ts" + to: "../sm-demo-app/vue-demo-app/app/lib/deck/engine.test.ts" + via: "aliases @ and ~ to app/ so bm's tests run unchanged" + pattern: "alias" + prohibitions: + - statement: "Tests use no real secret, no production database and no network service except the Docker Postgres and local fakes (the Turnstile verifier is a fake endpoint)." + status: unresolved + verification: judgment + - statement: "No production code changes in this plan except fixes for bugs the tests expose, each in its own commit and named in the SUMMARY." + status: unresolved + verification: judgment + - statement: "Nothing is pushed or tagged before the Task 6 go-ahead; test-only plugin commits get no tag." + status: unresolved + verification: judgment +--- + +## Phase Goal + +ROADMAP Goal (not in user-story format; no story is invented here): Clients can be shown a live demo at `demo.summercms.io` now, before the chat family lands: `sm-demo-app` with today's 16 shared plugins runs on rome, its public Nuxt site shows the plugins working through prepared demo accounts, and the admin backend stays private. + +This plan's slice (plan 5 of 5, wave 5; project rule: unit tests are the last plan of every phase): the unit tests for everything plans 01 to 04 built, bm's frontend library tests ported unchanged, one gate script, the final live verification, the documentation pass and the todo and g15office bookkeeping. + +## Decisions taken in this plan (Claude's discretion and recorded assumptions) + +- **One Postgres harness, database tests in the root package:** only package main compiles every plugin in (plugins.gen.go blank-imports them), so database tests that boot the whole demo live in the sm-demo-app root and drive `demo:seed` and `demo:reset` through the activated app's commands. Pure tests stay next to their code (roadmap, visitor config, schedule). `internal/demotest` (a normal package imported only by tests) starts one postgres:15 container per test binary, migrates every plugin of the plugin list its caller passes and publishes the database, after the oauthserver plugin's TestMain pattern (`flag.Parse()`, then `testing.Short()`; CLAUDE.md). `testcontainers-go` is already in the module graph through the plugins, so the app adds a direct requirement and no new dependency to the project. +- **Test names follow 19.4.1-VALIDATION.md** so `/msd-validate-phase` can map each row. Tests that need Docker skip under `-short`, and the verify commands run without `-short`. +- **Turnstile end to end lives in sm-demo-app** (framework v0.3.0), because sm-user-plugin requires a framework without the `turnstile:*` middleware. The plugin's `TestTurnstileRoutes` pins the route wiring with a recording router. +- **bm's tests are copied unchanged:** vitest aliases `@` and `~` to `app/`. A test that needs an edit gets the smallest one, and the SUMMARY lists it. +- **vitest 3.2.7 exactly** (the RESEARCH audit flags the newest vitest as too new; 3.2.7 is the pin summercms.go/admin and bm use). No @nuxt/test-utils and no happy-dom (also flagged); the node environment covers the pure libraries, and pages are covered by smoke.sh, deck-snapshot and check-live. + +## Artifacts this phase produces (this plan) + +- Go tests in sm-demo-app: `TestGoModRequiresTags`, `TestRealtimeDefaultDriver`, `TestRealtimeRoutes`, `TestRealtimeConfigDefaults`, `TestRemoteKeyOverride`, `TestDemoQuizList`, `TestDemoDeckWrites`, `TestDemoTurnstile`, `TestBootAllPlugins` (extended), `TestRoadmapParse`, `TestRoadmapDeckOrder`, `TestRoadmapNoClientNames`, `TestRoadmapPolishTitles`, `TestSeedVisitorFromConfig`, `TestSeedRoadmapUpsert`, `TestSeedLocales`, `TestResetSchedule`, `TestResetKeepsPlans`; package `internal/demotest` (`Start`, `App`, `DB`). +- Plugin tests: `TestTurnstileRoutes` (sm-user-plugin), `TestJournalAuthorProfile` extended with the empty and null cases (sm-journal-plugin). +- vitest in vue-demo-app: `vitest.config.ts`, script `test`, devDependency `vitest@3.2.7`, the bm library tests and `journalLocale.test.ts`, `buildUpdate.test.ts`. +- Scripts: `scripts/check-all.sh` (prints `check-all: ok`), smoke.sh route loop, `deck-snapshot.cjs --expect-banner`. +- Planning: journal-author-profile-fields todo moved to done; reload-on-deploy todo progress note; g15office comments on g02hcroj, esrp4qtx and ltcvqext. + + +Prove with unit tests everything the phase built (SC-5), port bm's frontend library tests unchanged, finish with a green phase gate and live check against https://demo.summercms.io, and close the phase's bookkeeping (README and DEPLOY.md pass, todos, g15office). + +Purpose: SC-5 and the final verification of SC-1 to SC-4. D-23: plan 05 of five, the unit-test plan the project rule puts last. +Output: test commits in ../sm-demo-app, ../sm-demo-app/vue-demo-app, ../fonoteka.go/plugins/golem15/user and ../sm-journal-plugin; planning doc commits in summercms.go; pushes after the go-ahead. + + + +@~/.claude/msd-core/workflows/execute-plan.md +@~/.claude/msd-core/templates/summary.md + + + +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-VALIDATION.md +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-RESEARCH.md +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-01-SUMMARY.md +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-02-SUMMARY.md +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-03-SUMMARY.md +@.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-04-SUMMARY.md +@.planning/G15OFFICE.md + +Repos: ../sm-demo-app, ../sm-demo-app/vue-demo-app, ../fonoteka.go/plugins/golem15/user, ../sm-journal-plugin (tests); summercms.go (planning docs only). Docker-backed tests skip under -short; run them on a Docker host (through `sudo -u golem` if the socket is denied although the user is in the docker group; CLAUDE.md) and record NOT RUN when no daemon answers. + +Rules: no co-author tags; stage by path; tests and planning docs in separate commits; state updates only through `msd_run query state.*` (the execute workflow does them); leave other sessions' uncommitted STATE.md and state.json edits alone. + + +Harness analog: ../sm-oauthserver-plugin/testmain_test.go (TestMain with flag.Parse, testing.Short, testcontainers postgres module, startPostgres, stopPostgres) and harness_test.go (boot an app on the database). bm tests: ../sm-bm-app/realtime_test.go (TestRealtimeDefaultDriver, TestRealtimeRoutes, TestRealtimeConfigDefaults, TestRemoteKeyOverride; constants tokenRoute, subscribeRoute; realtimeApp(t, ids, env...) through compass.Open with SUMMER_ENV=testing), ../sm-bm-app/frontend/src/lib/**/*.test.ts and src/api/http.test.ts, ../sm-bm-app/frontend/vite.config.ts (test: environment node, include src/**/*.test.ts, css true). +Code under test (plans 01-04): plugins/demo/roadmap (Parse, Build, leaks, phaseText, groups), plugins/demo/console (seedVisitor, roadmap decks step, seed_locales steps, demo:reset), plugins/demo/schedule.go, plugins/demo/{routes,decks_api,quizzes_api,realtime}.go, vue-demo-app app/utils/{journalLocale,buildUpdate}.ts and app/lib/**. +framework: `bouncer.Turnstile` (Endpoint field for a fake siteverify; a published *bouncer.Turnstile replaces the one built from turnstile.secret, surf README line 165), `surf.BuildRouter(app, plugins)`, `party.Activate`, `compass.Open(compass.Options{Dir, Environ})`. +VALIDATION.md rows and commands: the Per-Task Verification Map table (names above). + + + + + + + Task 1: Tracer: one shared Postgres harness boots the whole demo, and the first database test proves the roadmap deck upsert keeps its link + ../sm-demo-app/go.mod, ../sm-demo-app/go.sum, ../sm-demo-app/internal/demotest/postgres.go, ../sm-demo-app/testmain_test.go, ../sm-demo-app/seed_test.go, ../sm-demo-app/go_mod_test.go + ../sm-oauthserver-plugin/testmain_test.go, ../sm-oauthserver-plugin/harness_test.go, ../sm-demo-app/main.go and plugins.gen.go (boot path, PluginIDs), ../sm-demo-app/boot_test.go (loadCommittedConfig), ../sm-demo-app/plugins/demo/console/seed.go and roadmap_deck.go, ../sm-demo-app/go.mod, 19.4.1-VALIDATION.md + Per SC-5, D-16, SC-1. + +- internal/demotest/postgres.go (package demotest): + - `Start(ctx) (dsn string, stop func(), err error)` runs the testcontainers postgres module with image postgres:15; + - `App(t, dsn string, pluginIDs []string, env map[string]string) *backpack.App` loads the committed config with test secrets (as boot_test.go does) plus env overrides, activates the given plugin ids (the root tests pass PluginIDs), opens and publishes the database (lagoon), runs every plugin's migrations, and returns the app; + - `DB(app) *gorm.DB` returns the published database; + - `Reset(t, app)` truncates every non-migration table between tests. +- testmain_test.go (root package): TestMain calls flag.Parse(), starts the harness only when !testing.Short(), records a start error so DB tests t.Skip with it, and stops the container after m.Run(). +- seed_test.go (root package) `TestSeedRoadmapUpsert` (skips under -short), running demo:seed through the app's command list: + - boot with a visitor email and password in env and run the seed command twice; + - both decks exist, are published and owned by the visitor; + - public_hash is equal across runs; + - the slide count equals Build's; + - no duplicate (attachment_id, file_name) among the decks' images; + - the attached image blobs exist in the bucket (a temp file:// bucket from env). +- go_mod_test.go `TestGoModRequiresTags` (root package, no database): parse go.mod with golang.org/x/mod/modfile (add the requirement; it is already in the module graph). Fail on: + - any Replace; + - any git.golem15.com/golem15 requirement whose version is not a canonical semver tag (`semver.IsValid` and no pseudo-version, `module.IsPseudoVersion`); + - any plugin module named in summer.yaml (read with goccy/go-yaml) that has no requirement, except the app's own module. + + go -C ../sm-demo-app vet . ./plugins/demo/... ./internal/... && go -C ../sm-demo-app test -count=1 -run '^(TestGoModRequiresTags|TestSeedRoadmapUpsert)$' -v . 2>&1 | grep -c -- '^--- PASS: Test' | grep -qx 2 + Non-zero exit: vet red, or fewer than two "--- PASS" lines (go.mod holds a replace, a pseudo-version or misses a plugin; Docker unavailable so the upsert test skips instead of passing; the hash changes; duplicate images). + + + - `grep -c 'flag.Parse()' ../sm-demo-app/testmain_test.go` prints 1 and `grep -c 'testing.Short()' ../sm-demo-app/testmain_test.go` prints at least 1. + - `grep -c 'postgres:15' ../sm-demo-app/internal/demotest/postgres.go` prints at least 1. + - `go -C ../sm-demo-app test -count=1 -short .` passes without Docker (the DB tests skip). + + The demo has a reusable database test harness and the first two VALIDATION rows (go.mod tags, roadmap upsert) are green. + + + + Task 2: Unit tests cover the roadmap generator, the seed, the reset job, the demo endpoints, Turnstile and realtime + ../sm-demo-app/plugins/demo/roadmap/roadmap_test.go, ../sm-demo-app/plugins/demo/console/visitor_test.go, ../sm-demo-app/plugins/demo/schedule_test.go, ../sm-demo-app/seed_test.go, ../sm-demo-app/reset_test.go, ../sm-demo-app/realtime_test.go, ../sm-demo-app/demo_api_test.go, ../sm-demo-app/turnstile_test.go, ../sm-demo-app/boot_test.go + ../sm-bm-app/realtime_test.go, ../sm-demo-app/plugins/demo/roadmap/*.go and roadmap_smoke_test.go, ../sm-demo-app/plugins/demo/console/*.go, ../sm-demo-app/plugins/demo/schedule.go, ../sm-demo-app/plugins/demo/{routes,decks_api,quizzes_api,realtime}.go, ../sm-demo-app/boot_test.go, modules/bouncer/README.md (Turnstile verifier and Endpoint), modules/surf/README.md (turnstile middleware, BuildRouter) + Per SC-5 and the VALIDATION map. Use table-driven tests; each test asserts behaviour, not implementation details. + +- roadmap_test.go: + - TestRoadmapParse: done and open lines, the completion date moved out of the summary, stopping at Phase Details, an error on an input with no phase line, decimals such as 19.4.1; + - TestRoadmapDeckOrder: the five sections in the D-20 order for en and pl, equal slide counts; + - TestRoadmapNoClientNames: every denylisted name in every case variant is caught by leaks; a synthetic phase that leaks is left out and reported; the committed snapshot builds with zero left out and zero leaks in any field of either deck; + - TestRoadmapPolishTitles: every phase number of the snapshot has a Polish title; the pxpx slide carries Lighthouse, WordPress and tide in both languages and no anchor; the future section mentions MCP. +- plugins/demo/console/visitor_test.go TestSeedVisitorFromConfig (pure, -short, package console): an empty email gives skipped; a password under 8 characters gives an error naming the key; a valid pair gives the values; the seed source holds no credential literal. +- seed_test.go (root) TestSeedLocales (DB): after the seed, FAQ home exists for en and pl, the menu has Polish titles, every demo post has a Polish title translation and the team author, both quizzes have their contexts, and the second run changes no row. +- schedule_test.go TestResetSchedule (pure): disabled gives nil; enabled with 05:30 gives the entry with --force and the right cadence; a bad time is refused at Register. +- reset_test.go (root) TestResetKeepsPlans (DB), through the app's demo:reset command: + - seed; add a frontend user with a token, a subscription and order, a quiz attempt, a user deck, a feedback submission and an uploaded file; + - run demo:reset without --force: nothing changes; + - with --force: those rows and the file's blob are gone, the admins, plans (with their Stripe price ids), settings, locales and sitemap are unchanged, the visitor exists again, and the roadmap hashes are unchanged. +- realtime_test.go: port bm's four tests (TestRealtimeDefaultDriver, TestRealtimeRoutes, TestRealtimeConfigDefaults, TestRemoteKeyOverride), with golem15.demo, the bucket throttle:golem15.demo.realtime and demo- secrets. +- demo_api_test.go (DB, through httptest on the built router): + - TestDemoQuizList: anonymous 200 for demo:en and demo:pl with only slug, title, description and question_count; 422 for another context; unpublished quizzes absent; + - TestDemoDeckWrites: 401 without a JWT; create strips CSS and context and forces unpublished; an unknown theme gives 422; 61 slides give 422; a script element is removed; an update replaces slides; another user's slug gives 404. +- turnstile_test.go TestDemoTurnstile: boot with golem15.user.turnstile closed and a published *bouncer.Turnstile whose Endpoint is an httptest fake answering success for one token and failure otherwise. A register without a token gives 422; with the bad token 422; with the good token success. With the key emptied, register succeeds without a token. +- boot_test.go's TestBootAllPlugins asserts the final route set: /_demo/api/v1, and the realtime routes only with the driver set. +Fix any production bug a test exposes in its own commit (named in the SUMMARY). + + go -C ../sm-demo-app vet . ./plugins/demo/... ./internal/... && go -C ../sm-demo-app test -count=1 -short . ./plugins/demo/... ./internal/... && go -C ../sm-demo-app test -count=1 -run '^(TestRoadmapParse|TestRoadmapDeckOrder|TestRoadmapNoClientNames|TestRoadmapPolishTitles|TestSeedVisitorFromConfig|TestSeedLocales|TestResetSchedule|TestResetKeepsPlans|TestRealtimeDefaultDriver|TestRealtimeRoutes|TestRealtimeConfigDefaults|TestRemoteKeyOverride|TestDemoQuizList|TestDemoDeckWrites|TestDemoTurnstile|TestBootAllPlugins)$' -v . ./plugins/demo/... 2>&1 | grep -c -- '^--- PASS: Test' | grep -qx 16 + Non-zero exit: vet red; the -short suite red; or fewer than 16 top-level "--- PASS" lines (a behaviour regressed, a test name is missing, or Docker is unavailable so the database tests skip). + + + - `grep -c 'golem15.demo.realtime' ../sm-demo-app/realtime_test.go` prints at least 1. + - `grep -c 'Endpoint' ../sm-demo-app/turnstile_test.go` prints at least 1. + - `go -C ../sm-demo-app test -count=1 -run '^TestResetKeepsPlans$' -v . 2>&1 | grep -c 'SKIP'` prints 0 on the Docker host. + + Every Go behaviour of the phase that VALIDATION lists is pinned by a named, passing unit test. + + + + Task 3: The shared plugins' changes are pinned by their own unit tests: Turnstile route wiring in sm-user-plugin, author profile fields in sm-journal-plugin + ../fonoteka.go/plugins/golem15/user/turnstile_routes_test.go, ../sm-journal-plugin/journal_api_authors_test.go + ../fonoteka.go/plugins/golem15/user/routes.go, ../fonoteka.go/plugins/golem15/user/turnstile_routes_smoke_test.go (plan 03), ../sm-journal-plugin/controllers/api/authors.go, ../sm-journal-plugin/journal_api_authors_test.go + Per SC-5, D-07, todo esrp4qtx. + +- sm-user-plugin turnstile_routes_test.go `TestTurnstileRoutes` (recording pact.Router, no database), table over "", "open", "closed", " closed " (trimmed) and "bogus": + - only POST /_user/api/v1/login and /register carry the middleware; + - every other route's middleware list equals its list with the key empty; + - "bogus" makes Routes fail with the key in the message. + It may absorb the plan 03 smoke test (delete the smoke file then). +- sm-journal-plugin TestJournalAuthorProfile gains cases for a backend user with no metadata and no avatar (subtitle and description empty strings, avatar null) and with only a subtitle. The exact key set stays pinned. +- Test-only commits; no tag (the shipped code is unchanged). + + go -C ../fonoteka.go/plugins/golem15/user vet ./... && go -C ../fonoteka.go/plugins/golem15/user test -count=1 -short -run '^TestTurnstileRoutes$' -v . 2>&1 | grep -q -- '^--- PASS: TestTurnstileRoutes' && go -C ../fonoteka.go/plugins/golem15/user test -count=1 -short ./... && go -C ../sm-journal-plugin test -count=1 -run '^TestJournalAuthorProfile$' -v . 2>&1 | grep -q -- '^--- PASS: TestJournalAuthorProfile' + Non-zero exit: vet or the user plugin suite red; no "--- PASS: TestTurnstileRoutes" (a mode wired wrong or another route changed); or no "--- PASS: TestJournalAuthorProfile" (key set changed, a null case wrong, or Docker unavailable). + + + - `git -C ../fonoteka.go/plugins/golem15/user diff --stat HEAD~1 -- routes.go` shows no change in the test commit (tests only). + - `git -C ../sm-journal-plugin log -1 --format=%s` names a test change and touches only *_test.go files (`git -C ../sm-journal-plugin show --stat HEAD`). + + Both shared plugin changes of the phase are pinned by tests in their own repositories. + + + + Task 4: The frontend libraries are tested: bm's tests run unchanged in vue-demo-app under vitest, next to tests for the journal locale mapper and the build-update check + ../sm-demo-app/vue-demo-app/package.json, ../sm-demo-app/vue-demo-app/pnpm-lock.yaml, ../sm-demo-app/vue-demo-app/vitest.config.ts, ../sm-demo-app/vue-demo-app/README.md, ../sm-demo-app/vue-demo-app/app/lib/deck/clock.test.ts, ../sm-demo-app/vue-demo-app/app/lib/deck/compose.test.ts, ../sm-demo-app/vue-demo-app/app/lib/deck/engine.test.ts, ../sm-demo-app/vue-demo-app/app/lib/deck/reading.test.ts, ../sm-demo-app/vue-demo-app/app/lib/deck/remote.test.ts, ../sm-demo-app/vue-demo-app/app/lib/deck/styles.test.ts, ../sm-demo-app/vue-demo-app/app/lib/deck/themes.test.ts, ../sm-demo-app/vue-demo-app/app/lib/realtime.test.ts, ../sm-demo-app/vue-demo-app/app/lib/pilot.test.ts, ../sm-demo-app/vue-demo-app/app/lib/quizSession.test.ts, ../sm-demo-app/vue-demo-app/app/lib/markdown.test.ts, ../sm-demo-app/vue-demo-app/app/lib/api/http.test.ts, ../sm-demo-app/vue-demo-app/app/utils/journalLocale.test.ts, ../sm-demo-app/vue-demo-app/app/utils/buildUpdate.test.ts, ../sm-demo-app/scripts/smoke.sh, ../sm-demo-app/scripts/deck-snapshot.cjs, ../sm-demo-app/vue-demo-app + ../sm-bm-app/frontend/vite.config.ts (test block), ../sm-bm-app/frontend/src/lib/**/*.test.ts, ../sm-bm-app/frontend/src/api/http.test.ts, ../sm-demo-app/vue-demo-app/app/lib/** (ported sources and their edits listed in 19.4.1-04-SUMMARY.md), ../sm-demo-app/vue-demo-app/app/utils/journalLocale.ts, buildUpdate.ts, ../sm-demo-app/scripts/smoke.sh, ../sm-demo-app/scripts/deck-snapshot.cjs, 19.4.1-RESEARCH.md (Package Legitimacy Audit: vitest pin) + Per SC-5, D-14, D-13, D-22, D-10. + +- vitest: + - `pnpm --dir vue-demo-app add -D vitest@3.2.7` (exact); + - the package.json script "test": "vitest run"; + - vitest.config.ts: environment node, include app/**/*.test.ts, css true, aliases @ and ~ to ./app. +- Copy bm's tests next to the ported sources: lib/deck/*.test.ts, realtime.test.ts, pilot.test.ts, quizSession.test.ts, markdown.test.ts, api/http.test.ts. Where a ported source changed on purpose (the cookie token store, the font loader, navigateTo), adapt only the affected assertions and list them in the SUMMARY. +- New tests: + - journalLocale.test.ts: a post with translations.pl gets the Polish title, excerpt and content_html; a missing locale keeps the defaults; list payloads map every item; the slug never changes; + - buildUpdate.test.ts: isNewerBuild is false for equal ids, an empty current id or an empty latest id, and true for differing non-empty ids. +- smoke.sh loops the banner, card-number and no-admin-path assertions over every server-rendered route: /, /blog, a post, /faq, /subscriptions, /register, /login, /contact, /quizzes. +- deck-snapshot.cjs gets --expect-banner, which fails a slide whose page has no compact demo banner element. dev.sh `snapshot --site` passes it. +- vue-demo-app README documents test and build. + + out=$(pnpm --dir ../sm-demo-app/vue-demo-app test 2>&1) && printf '%s\n' "$out" | grep -E 'Tests +[0-9]+ passed' | grep -vq 'skipped' && ../sm-demo-app/scripts/build.sh release | tail -1 | grep -q '^build: ok release' && ../sm-demo-app/scripts/smoke.sh 2>&1 | tail -1 | grep -qx 'smoke: ok' + Non-zero exit: vitest exits non-zero (a failed test), prints no "Tests N passed" line, or reports skipped tests; the release build fails (lockfile drift); or smoke.sh does not end with "smoke: ok" (a server-rendered page without the banner, or with the admin path). + + + - `grep -c '"vitest": "3.2.7"' ../sm-demo-app/vue-demo-app/package.json` prints 1. + - `ls ../sm-demo-app/vue-demo-app/app/lib/deck/*.test.ts | wc -l` prints 7. + - `diff -q ../sm-bm-app/frontend/src/lib/deck/engine.test.ts ../sm-demo-app/vue-demo-app/app/lib/deck/engine.test.ts` reports no difference, or the SUMMARY explains the change. + + The ported frontend libraries carry bm's tests, the demo's own helpers are tested, and every server-rendered page is checked for the banner. + + + + Task 5: One command proves the whole phase, the live demo passes it, and the docs, todos and g15office reflect the result + ../sm-demo-app/scripts/check-all.sh, ../sm-demo-app/scripts/check-live.sh, ../sm-demo-app/README.md, ../sm-demo-app/DEPLOY.md, .planning/todos/pending/journal-author-profile-fields.md, .planning/todos/done/journal-author-profile-fields.md, .planning/todos/pending/2026-10-11-reload-open-apps-when-a-new-version-is-deployed.md + ../sm-demo-app/scripts/*.sh, ../sm-demo-app/README.md, ../sm-demo-app/DEPLOY.md, .planning/G15OFFICE.md, .planning/todos/pending/journal-author-profile-fields.md, .planning/todos/pending/2026-10-11-reload-open-apps-when-a-new-version-is-deployed.md, .planning/todos/done/ (format of a closed todo), .planning/ROADMAP.md (phase list and Phase 20 section), 19.4.1-VALIDATION.md (Manual-Only Verifications) + Per SC-5, SC-2, D-22, D-01. + +- scripts/check-all.sh runs in order and stops at the first failure: + 1. go vet and go test without -short for the app; + 2. the user plugin and journal plugin suites (canonical checkouts); + 3. pnpm test and the Nuxt build; + 4. sync-roadmap.sh --check, check-pins.sh, build.sh release, smoke.sh and check-deploy.sh; + 5. check-live.sh https://demo.summercms.io. + It prints `check-all: ok`. +- check-live.sh gets a final review: every SC-2 and D-10 item, plus a check that no response of the routes it visits contains the admin path as a link. Run check-all. +- README.md and DEPLOY.md final pass: every script, config key, route and port named in them exists. Each runbook step was executed in plans 02 to 04 or is marked as not yet run. Secrets appear by key name only. +- Todos (planning docs in summercms.go, their own commit): + - `git mv` journal-author-profile-fields.md into todos/done/ with a closing note (done in Phase 19.4.1 plan 03, the plugin tag, the demo's author card; the description editor stays open as a follow-up line); + - add a "Progress (Phase 19.4.1)" section to the reload todo: the site half shipped in vue-demo-app through the Nuxt app manifest; the admin half (boardwalk build header, SPA banner, a framework tag and changelog post) remains, so the todo stays pending. +- g15office (G15OFFICE.md protocol): + - set esrp4qtx to Live with a one-line comment; + - comment progress on ltcvqext; + - comment on the phase task g02hcroj that plans 01 to 05 are executed, and set it to Testing. +- Confirm D-01: grep shows Phase 19.4.1 between 19.4 and 19.5 in the phase list, and Phase 20's goal names the 19.4.1 demo. + + ../sm-demo-app/scripts/check-all.sh 2>&1 | tail -1 | grep -qx 'check-all: ok' && test -f .planning/todos/done/journal-author-profile-fields.md && ! test -e .planning/todos/pending/journal-author-profile-fields.md && grep -q 'Phase 19.4.1' .planning/todos/pending/2026-10-11-reload-open-apps-when-a-new-version-is-deployed.md + Non-zero exit: check-all stops before "check-all: ok" (any suite, build, pin, deploy or live check failing), the author todo is not moved to done, or the reload todo has no Phase 19.4.1 progress note. + + + - `awk '/^## Phase Details/{exit} {print}' .planning/ROADMAP.md | grep -n 'Phase 19.4:\|Phase 19.4.1:\|Phase 19.5:' | cut -d: -f1 | paste -sd' '` prints three increasing line numbers. + - `grep -c '19.4.1' .planning/ROADMAP.md` prints at least 3 (phase list, details, Phase 20's goal). + - The SUMMARY records the g15office updates (task ids and new statuses) and each manual-only row of VALIDATION.md with its outcome from the plan 02 to 04 human checks. + + One gate script proves the phase and passes against the live demo; the documentation is accurate; the todos and g15office show what shipped and what remains. + + + + Task 6: Go-ahead to push the test commits and close the phase's execution + The check-all output of Task 5; the list of unpushed commits per repository (`git log origin/{branch}..HEAD --oneline` in sm-demo-app, vue-demo-app, the user plugin and the journal plugin) + + - The user chose one option; with "push" they also confirmed the manual-only checks (the subscribe flow with the visitor and the phone remote) were seen working in plans 02 and 04, or named what still fails. + + Push the test-only commits of this plan to the four repositories? + Pushing needs your go-ahead (CLAUDE.md). The commits add tests, the vitest setup, check-all.sh and documentation. They change no shipped behaviour, so no tag and no redeploy follow. The plugin test commits land after the plugins' tags; the demo keeps requiring those tags. + + + + + Reply "push" or "hold", plus anything from the manual checks that still fails. + + + + Task 7: The test commits are on git.golem15.com and every repository is in step with its remote + The Task 6 reply was "push". + ../sm-demo-app/vue-demo-app + The Task 6 reply + Per the go-ahead. Push in dependency order: +1. the sm-user-plugin test commit (branch master); +2. the sm-journal-plugin test commit (branch main); +3. vue-demo-app master; +4. sm-demo-app master, after committing the vue-demo-app pointer if it moved. +Push no tag. Then confirm that each remote branch equals the local HEAD. If the user reported a failing manual check, record it in the SUMMARY as an open item for /msd-verify-work. Do not fix it here unless it is a test-only problem. + + for r in ../sm-demo-app ../sm-demo-app/vue-demo-app; do test "$(git -C $r ls-remote origin refs/heads/master | cut -f1)" = "$(git -C $r rev-parse HEAD)" || exit 1; done && test "$(git -C ../fonoteka.go/plugins/golem15/user ls-remote origin refs/heads/master | cut -f1)" = "$(git -C ../fonoteka.go/plugins/golem15/user rev-parse HEAD)" && test "$(git -C ../sm-journal-plugin ls-remote origin refs/heads/main | cut -f1)" = "$(git -C ../sm-journal-plugin rev-parse HEAD)" + Non-zero exit: a remote branch differs from the local HEAD (a push failed or was skipped). + + + - `git -C ../sm-demo-app status --porcelain` prints nothing but untracked ignored-by-design files. + - The SUMMARY lists the pushed commit range per repository and confirms no tag was pushed. + + All of the phase's code and tests are on git.golem15.com. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| test harness to Docker | containers started by tests on the developer machine | +| check-live.sh to the production origin | read-only requests from the build machine | +| test fixtures to the repositories | data committed with the tests | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-19.4.1-33 | Information disclosure | test fixtures and logs | medium | mitigate | tests generate their own secrets and fake verifier, use no production value, and log no token; the SUMMARY records names and counts only | +| T-19.4.1-34 | Tampering | regression of a security mitigation from plans 01-04 | high | mitigate | the threats of plans 01-04 each have a pinning test: client-name denylist (T-01), config-only visitor (T-02), image de-duplication (T-05), Turnstile (T-16, T-17), reset keep-list (T-20), deck stripping and unpublished decks (T-25), realtime route middleware (T-26, T-27), anonymous quiz list without answers (T-31), plus smoke and check-live for the admin path and the banner (T-06, T-13) | +| T-19.4.1-35 | Denial of service | check-live against production | low | accept | a handful of GET requests and one refused POST per run, inside the existing throttles | +| T-19.4.1-SC | Tampering | npm/pip/cargo installs | high | mitigate | vitest@3.2.7 only, the exact pin the RESEARCH audit approved (the newest release is flagged too-new); golang.org/x/mod and testcontainers-go are already in the module graph; no other package | + + + +- Task 1: TestGoModRequiresTags and TestSeedRoadmapUpsert pass on the shared harness. +- Task 2: 16 named demo tests pass with Docker; the -short suite passes without it. +- Task 3: TestTurnstileRoutes and TestJournalAuthorProfile pass. +- Task 4: vitest green with bm's tests unchanged; smoke covers every server-rendered page. +- Task 5: check-all.sh ends with "check-all: ok" against the live demo; todos and g15office updated. +- Tasks 6-7: the test commits are pushed after the go-ahead. + + + +The new code of Phase 19.4.1 has unit tests, delivered in this last plan (SC-5), and the full gate, including the live check of https://demo.summercms.io, passes. + + + +Create `.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-05-SUMMARY.md` when done (tests per VALIDATION row with status, any bug fixed, NOT RUN items with reasons, g15office updates, open manual items). + diff --git a/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-PATTERNS.md b/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-PATTERNS.md new file mode 100644 index 00000000..4180b88a --- /dev/null +++ b/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-PATTERNS.md @@ -0,0 +1,290 @@ +# Phase 19.4.1: Early public demo on demo.summercms.io - Pattern Map + +**Mapped:** 2026-10-20 +**Files analyzed:** 24 (groups; the Nuxt port is one row per group) +**Analogs found:** 22 / 24 + +All paths are relative to `/media/nvme/dev/golem15/summercms.io/summercms/`. Analogs are tracked files in their own repos. Do not cite `sm-demo-app/plugins/golem15/*` (submodule mirrors). Cite the plugin's own repo (`sm-oauthserver-plugin/...`, `sm-faq-plugin/...`) or the canonical user checkout `fonoteka.go/plugins/golem15/user`. + +## File Classification + +| New/Modified File | Role | Data Flow | Closest Analog | Match | +|---|---|---|---|---| +| `sm-demo-app/plugins/demo/console/seed.go` (EN/PL, visitor from config, author, upserted roadmap decks) | command | batch / CRUD | itself (`seed.go`) + `sm-journal-plugin/console/import_posts.go:315-335` (SetTranslated) | exact | +| `sm-demo-app/plugins/demo/roadmap/roadmap.go` (ROADMAP.md parser, curated sections D-16/D-19/D-20) | utility | transform | none (line regex, RESEARCH §Standard Stack) | no analog | +| `sm-demo-app/plugins/demo/content/ROADMAP.md`, `content/oidc/*.png` (go:embed) | config/asset | file-I/O | `sm-bm-app/plugins/jz/bm/plugin.go:35-41` (`//go:embed`) | role-match | +| `sm-demo-app/plugins/demo/console/reset.go` (`demo:reset --force`) | command | batch | `sm-faq-plugin/console/import_winter.go:46-69` (`--force` flag) | role-match | +| `sm-demo-app/plugins/demo/schedule.go` + `Schedule()` in plugin.go | config | event-driven (scheduled) | `sm-oauthserver-plugin/schedule.go` + `plugin.go:34,103-119` | exact | +| `sm-demo-app/plugins/demo/config/config.yaml` + config loader | config | — | `sm-oauthserver-plugin/plugin.go:110-121` (`config()`, `ConfigFS`) | exact | +| `sm-demo-app/plugins/demo/plugin.go` (Boot wiring, Commands, Buckets, Schedule) | provider | request-response | `sm-bm-app/plugins/jz/bm/plugin.go:60-103` | exact | +| `sm-demo-app/plugins/demo/realtime.go` | service | pub-sub | `sm-bm-app/plugins/jz/bm/realtime.go` (49 lines, port verbatim) | exact | +| `sm-demo-app/plugins/demo/routes.go` (`/_demo/api/v1/...`, `lighthouse.Mount`, bucket) | route | request-response | `sm-bm-app/plugins/jz/bm/routes.go:22-80,181-204` | exact | +| `sm-demo-app/realtime_test.go` | test | request-response | `sm-bm-app/realtime_test.go` | exact | +| `fonoteka.go/plugins/golem15/user/routes.go` (opt-in `turnstile:` on /login, /register) | route | request-response | itself, lines 9-30; surf README line 30, 165-166 | exact | +| `fonoteka.go/plugins/golem15/user/config/config.yaml` (`turnstile: ""`) + README | config | — | same file | exact | +| `sm-journal-plugin` author API fields (todo esrp4qtx) | controller | request-response | existing author serializer in the same plugin | exact | +| `sm-demo-app/go.mod`, `go.work`, delete `plugins/demo/go.mod` | config | — | RESEARCH Pattern 1; `sm-summercmsio-app/go.mod` (tag requires) | role-match | +| `sm-demo-app/scripts/build.sh` | utility | batch | `sm-summercmsio-app/scripts/build.sh` (151 lines) | exact | +| `sm-demo-app/scripts/check-deploy.sh`, `smoke.sh` | utility | batch | `sm-summercmsio-app/scripts/check-deploy.sh`, `smoke.sh` | exact | +| `sm-demo-app/deploy/nginx/demo.summercms.io.conf` | config | request-response | `sm-summercmsio-app/deploy/nginx/summercms.io.conf` + `/ws` block from `sm-bm-app/deploy/nginx/bm.jakubzych.com.conf:60-73` | exact | +| `sm-demo-app/deploy/supervisor/{summercms-demo,summercms-demo-site,summercms-demo-centrifugo}.conf` | config | — | `sm-summercmsio-app/deploy/supervisor/summercms-io.conf`, `summercms-io-changelog.conf`; `sm-bm-app/deploy/supervisor/bm.conf` | exact | +| `sm-demo-app/deploy/{env.example,site.env.example,centrifugo.json.example}` | config | — | `sm-summercmsio-app/deploy/env.example`, `changelog.env.example`; `sm-bm-app/deploy/env.example` | exact | +| `sm-demo-app/DEPLOY.md` | docs | — | `sm-summercmsio-app/DEPLOY.md`, `sm-bm-app/DEPLOY.md` (Centrifugo section) | exact | +| `sm-demo-app/config/env/production/*.yaml`, `config/realtime.yaml` | config | — | `sm-bm-app/config/realtime.yaml` (`driver: "null"`) | exact | +| `vue-demo-app` layout, header/footer, hero, tour cards (D-11/D-12) | component | request-response | `sm-summercmsio-app/vue-summercmsio-app/app/components/{SiteHeader,SiteFooter,HeroSection,FeaturesSection}.vue`; current `sm-demo-app/vue-demo-app/app/layouts/default.vue` | exact | +| `vue-demo-app` presentations/quizzes/register/login pages, `lib/*`, `components/deck/*`, `remote/*` | component | streaming (WS) / request-response | `sm-bm-app/frontend/src/views/*`, `src/lib/realtime.ts`, `src/components/deck/*` | exact (port) | +| `vue-demo-app/app/plugins/manifest-update.client.ts`, `UpdateBanner.vue` | provider | event-driven | `sm-bm-app/frontend/src/stores/pwa.ts` (`updateReady`/`applyUpdate` behaviour only) | partial | +| `vue-demo-app/app/components/TurnstileWidget.vue`, `pages/contact.vue` (feedback POST) | component | request-response | none in repo (Cloudflare explicit render; feedback API in RESEARCH §Feedback) | no analog | + +## Pattern Assignments + +### `sm-demo-app/plugins/demo/console/seed.go` (command, batch) — Plan 01 first + +**Analog:** itself. Keep the step table and per-step transaction (lines 36-64): +```go +steps := []struct { + name string + run func(*gorm.DB) (bool, error) +}{ + {"frontend user " + CustomerEmail, seedCustomer}, + ... +} +for _, s := range steps { + var created bool + err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + var err error + created, err = s.run(tx) + return err + }) + if err != nil { + return fmt.Errorf("demo:seed: %s: %w", s.name, err) + } + state := "exists" + if created { state = "created" } + out.Printf("%-40s %s\n", s.name, state) +} +``` +- DB access: `lookupDB(ctx, app)` (lines 73-89), helpers `exists` / `insertID` (lines 91-103). Reuse them from reset.go. +- **Presentation insert to copy for the roadmap decks** (lines 202-226): `INSERT INTO golem15_presentations_presentations (title, slug, is_published, target_seconds, public_hash, ...)`, then a loop of `INSERT INTO golem15_presentations_slides (presentation_id, position, title, css_class, seconds, body_html, ...)`. `publicHash()` (lines 228-234). +- **Change for D-16 ("stays current"):** the roadmap step must not use the `exists → skip` guard. Upsert instead: find the deck id by slug (`roadmap`, `roadmap-pl`), keep `public_hash`, `DELETE FROM golem15_presentations_slides WHERE presentation_id = ?`, then insert the slides again. State label `"updated"`. +- Lines 19-22 hard-code `CustomerEmail`/`CustomerPassword`. Replace them with `golem15.demo.visitor.*` config (D-06). Skip the step when the email is empty. +- Lines 132 and 175 contain "log in as the demo customer" and "At `/backend`". Remove both (D-04/D-05). +- Quizzes: copy `seedQuiz` (lines 236-267) and parameterize it per locale (slug `summercms-podstawy`, `context` column `demo:pl`). +- FAQ: `seedFAQ` (lines 169-181) selects `locale_id` from `golem15_translate_locales WHERE is_default`. For PL, use `WHERE code = 'pl'` with a second row. + +**Translations (journal posts, menu titles)** — `sm-journal-plugin/console/import_posts.go:320-332`: +```go +for _, field := range fields { + val, ok := attrs[field] + if !ok || val == nil { continue } + if err := translate.SetTranslated(ctx, db, post, field, locale, val); err != nil { + return err + } +} +``` +Signature (`sm-translate-plugin/classes/translatable.go:177`): `SetTranslated(ctx, db *gorm.DB, m Translatable, field, locale string, value any) error`. It needs a loaded model (journal `models.Post`), not raw SQL ids. Load it with `tx.First(&post, id)` after `insertID`. Keep `slug` untranslated. + +### `sm-demo-app/plugins/demo/roadmap/roadmap.go` (utility, transform) +No analog. Use RESEARCH's regex `^- \[( |x)\] \*\*Phase ([0-9.]+): (.+?)\*\* - (.+)$` over an embedded `content/ROADMAP.md`. Section order from D-20: roadmap, features, PHP migration (OIDC screenshots), pxpx (D-19), MCP future. Use stdlib only. Run a client-name filter (Pitfall 7) and unit-test it in plan 05. + +### Embedded content (`content/ROADMAP.md`, `content/oidc/*.png`) +**Analog:** `sm-bm-app/plugins/jz/bm/plugin.go:35-41`: +```go +//go:embed config +var configFS embed.FS + +//go:embed lang +var langFS embed.FS +``` +Screenshots have to be reachable by slides. Either copy them into the uploads public bucket at seed time, or inline them as data URIs. Check whether `classes.Clean` keeps `data:` URIs before choosing that route. + +### `sm-demo-app/plugins/demo/console/reset.go` (command, batch) +**Analog:** `sm-faq-plugin/console/import_winter.go:46-69` (flag declaration) and `console/helpers.go:30-36` (`flagOn`): +```go +Flags: []bonfire.Flag{ + {Name: "force", Description: "Replace the FAQs the table already holds", Bare: true}, +}, +Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error { + opts := WinterOptions{ Force: flagOn(in, "force"), ... } + db, done, err := lookupDB(ctx, app) + if err != nil { return err } + defer done() +``` +Wipe in one transaction, collect `attach.BlobKeys`, then `attach.DeleteKeys` after commit (`summercms.go/modules/lagoon/attach/file.go:113-188`), then call the seed steps. + +### `sm-demo-app/plugins/demo/schedule.go` (config, scheduled) +**Analog:** `sm-oauthserver-plugin/schedule.go:13-19`: +```go +func schedule(p *Plugin) []pact.ScheduledCommand { + out := []pact.ScheduledCommand{{Command: "oauthserver:prune", Cadence: pact.Daily()}} + if interval := p.config().RotationInterval; interval > 0 { + out = append(out, pact.ScheduledCommand{Command: "oauthserver:rotate-keys", Cadence: pact.Every(interval)}) + } + return out +} +``` +Plus `plugin.go:34` `_ pact.HasSchedule = (*Plugin)(nil)` and `:105` `func (p *Plugin) Schedule() []pact.ScheduledCommand { return schedule(p) }`. For the demo, return `nil` unless `reset.enabled` (D-09), and use `pact.DailyAt(h, m)` with `Args: []string{"--force"}`. + +**Config loader** (`sm-oauthserver-plugin/plugin.go:110-121`): +```go +func (p *Plugin) config() Config { + if p.cfg.KeyRing.Algorithm != "" || p.app == nil { return p.cfg } + cfg, err := LoadConfig(p.app) + if err != nil { return Config{} } + return cfg +} +func (p *Plugin) ConfigFS() fs.FS { return configFS } +``` + +### `sm-demo-app/plugins/demo/plugin.go` (provider) +**Analog:** `sm-bm-app/plugins/jz/bm/plugin.go:70-103`: +```go +func (p *Plugin) Boot(app *backpack.App) error { + p.app = app + if app != nil { + if err := p.wireRealtime(app); err != nil { return err } + } + return nil +} +func (p *Plugin) Commands() []bonfire.Command { + cmds := append(generatedCommands(), console.SeedCommand(p.withDB)) + return append(cmds, centrifugo.Commands(p.app)...) +} +``` +The struct gains `realtime *lighthouse.Service` (bm `plugin.go:54-56`). Keep the existing `Subscribables()` (demo `plugin.go:48-51`). Add `golem15.feedback` to `Requires()` if the seed sets the feedback settings. + +### `sm-demo-app/plugins/demo/realtime.go` (service, pub-sub) +**Analog:** `sm-bm-app/plugins/jz/bm/realtime.go:1-49`. Port it verbatim: rename the package to `demo` and the error text `jz.bm:` to `golem15.demo:`. It keeps the blank import `_ ".../lighthouse/centrifugo"` and the user lookup `Select("id","name")` through `app.Lookup[*gorm.DB]()` per call. + +### `sm-demo-app/plugins/demo/routes.go` (route) +**Analog:** `sm-bm-app/plugins/jz/bm/routes.go`. +Bucket (lines 43-47, 69-78): +```go +func (p *Plugin) Buckets() map[string]surf.Bucket { + trusted := surf.TrustedProxies(nil) + if p.app != nil { trusted = surf.TrustedProxies(p.app.Config) } + return map[string]surf.Bucket{ + "jz.bm.realtime": { + Max: 120, Decay: time.Minute, + Key: func(r *http.Request) string { + if u, ok := bouncer.User(r.Context()); ok && u.ID != 0 { + return "jz.bm.realtime|u:" + strconv.FormatUint(uint64(u.ID), 10) + } + return "jz.bm.realtime|ip:" + surf.ClientIP(r, trusted) + }, + }, + } +} +``` +Mount (lines 194-204): +```go +func (p *Plugin) mountRealtime(r pact.Router) error { + if p.realtime == nil || p.app == nil || !p.app.HasPlugin("golem15.presentations") { return nil } + return lighthouse.Mount(r, p.realtime.Driver(), lighthouse.Surfaces{ + UserAuth: surf.Use("jwt.auth", "presentations.realtime-owner", bucketRealtime), + ServerToServer: surf.Use(), + Middleware: surf.Use(), + }) +} +``` +Interface asserts (lines 22-25): `_ pact.HasRoutes`, `_ surf.BucketProvider`. The group pattern for the `/_demo/api/v1` routes follows the user plugin `r.Group(prefix, surf.Use("throttle:..."), func(g pact.Router){...})`. + +### `sm-demo-app/realtime_test.go` (test) +**Analog:** `sm-bm-app/realtime_test.go:1-45`: constants `tokenRoute = "GET /api/realtime/token"` and `subscribeRoute = "POST /api/realtime/subscribe"`, and `realtimeApp(t, ids, env...)` booting through `compass.Open(compass.Options{Dir, Environ})` with `SUMMER_ENV=testing`. Change the secrets prefix `bm-` to `demo-` and the bucket to `throttle:golem15.demo.realtime`. + +### `fonoteka.go/plugins/golem15/user/routes.go` (route, Turnstile opt-in) +**Analog:** itself, lines 9-15: +```go +r.Group("/_user/api/v1", surf.Use("throttle:user-api"), func(g pact.Router) { + g.Post("/login", controllers.Login(p.app)) + ... + g.Post("/register", controllers.Register(p.app)) +``` +Per-route middleware is the trailing variadic argument, as in `g.Post("/tokens", controllers.APITokenStore(p.app), "jwt.auth")` (line 26). Append `"turnstile:"+mode` only when `golem15.user.turnstile` is non-empty. Valid names are `turnstile:open|closed` (`summercms.go/modules/surf/router.go:691`). Token field `turnstile_token`; secret `SUMMER_TURNSTILE__SECRET` (`surf/README.md:165-166`). Update the user README Configuration section in the same commit. + +### `sm-demo-app/scripts/build.sh` (utility, batch) +**Analog:** `sm-summercmsio-app/scripts/build.sh:1-60+`. Copy the structure: `set -euo pipefail`, `ROOT=...`, `die()`, `usage()`, `release|dev` mode, the tool check loop `for tool in pnpm go rsync git tar`, `FW="${SUMMERCMS_FRAMEWORK:-$ROOT/../summercms.go}"`, the refusal of uncommitted changes in release mode, and the submodule `go.mod` presence checks (`DEPS=(...)`). The release step is `GOWORK=off GOPRIVATE='git.golem15.com/*' CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -o bin/demo .`, plus the submodule-HEAD-equals-tag pin check (RESEARCH Pattern 1). Copy check-deploy.sh (127 lines) and smoke.sh (314 lines) from the same repo. check-deploy.sh compares NITRO_PORT with the nginx proxy_pass. Add `centrifugo checkconfig`. + +### `sm-demo-app/deploy/nginx/demo.summercms.io.conf` (config) +**Analog:** `sm-summercmsio-app/deploy/nginx/summercms.io.conf`. Copy: +- lines 18-30: the port-80 ACME webroot block plus redirect; +- lines 50-67: the 443 server, the cert lines (lineage `demo.summercms.io`), HSTS and gzip; +- lines 72-74: `location ^~ /backend { return 404; }`; +- lines 81-90: a per-prefix Go proxy block (`proxy_pass http://127.0.0.1:;` plus `proxy_http_version 1.1` plus the four `proxy_set_header` lines). Repeat it per Go prefix from RESEARCH §Architecture; +- lines 124-135: `location /` goes to Nuxt (`3098`). Drop `limit_except` there, because Nuxt pages only GET but the layers' POST routes (purge) must stay unreachable from outside anyway. + +The WebSocket block comes from `sm-bm-app/deploy/nginx/bm.jakubzych.com.conf:62-73`: +```nginx +location = /ws { + proxy_pass http://127.0.0.1:8000/connection/websocket; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + ... + proxy_read_timeout 86400; + proxy_send_timeout 86400; +} +``` +Change the port to the demo's own Centrifugo (8010). Never route `/api/realtime/subscribe`. + +### `sm-demo-app/deploy/supervisor/*.conf` (config) +**Analogs:** `sm-summercmsio-app/deploy/supervisor/summercms-io.conf` (Go): +```ini +[program:summercms-io] +command=/srv/summercms-io/bin/summercms-io serve --addr 127.0.0.1:8095 +directory=/srv/summercms-io +user=summercms +environment=SUMMER_ENV="production" +autostart=true +autorestart=true +startsecs=3 +stopsignal=TERM +stopwaitsecs=15 +redirect_stderr=true +stdout_logfile=/var/log/supervisor/summercms-io.log +stdout_logfile_maxbytes=10MB +stdout_logfile_backups=5 +``` +and `summercms-io-changelog.conf` (Nuxt) line 11 and 14: +```ini +command=/home/deploy/.nvm/versions/node/v22.22.0/bin/node --env-file=/srv/summercms-io/changelog.env /srv/summercms-io/changelog/.output/server/index.mjs +environment=NODE_ENV="production",NITRO_HOST="127.0.0.1",NITRO_PORT="3095",NUXT_JOURNAL_API_BASE="http://127.0.0.1:8095/_journal/api/v1" +``` +Centrifugo program: `sm-bm-app/deploy/supervisor/bm.conf` and `sm-bm-app/DEPLOY.md` (Centrifugo section, shared-secret caveat lines 327-331). + +### `vue-demo-app` look (layout, SiteHeader, SiteFooter, HeroSection, TourCard, DemoBanner, BackendCta) +**Analog:** `sm-summercmsio-app/vue-summercmsio-app/app/components/SiteHeader.vue`, `SiteFooter.vue`, `HeroSection.vue`, `FeaturesSection.vue` plus that app's CSS tokens. The current `sm-demo-app/vue-demo-app/app/layouts/default.vue:1-43` holds the structure to keep (`useDemoAuth`, `MenuNav code="main"`). Remove `adminUrl` (lines 3-4, 37) and the admin link in `pages/index.vue:31`. + +### `vue-demo-app` presentations, quizzes, register and login (port) +**Analog:** `sm-bm-app/frontend/src/views/*` and the per-file table in RESEARCH §bm frontend port inventory. Rules: `routeRules` `ssr:false` for `/presentations/**` and `/p/**`; Pinia stores become `useState` composables; `useRouter().push` becomes `navigateTo`; the dynamic `import('centrifuge')` in `src/lib/realtime.ts` stays client-only. Port `lib/**/*.test.ts` with vitest 3.2.7. + +### `vue-demo-app/app/plugins/manifest-update.client.ts` (provider, event-driven) +**Analog (behaviour only):** `sm-bm-app/frontend/src/stores/pwa.ts:15-22` exposes `updateReady` as a computed flag and reloads only on the user's action (`applyUpdate`). Implement it with `nuxtApp.hook('app:manifest:update', ...)` plus `experimental.checkOutdatedBuildInterval`. No service worker, no Pinia. + +## Shared Patterns + +### Plugin command DB access +**Source:** `sm-demo-app/plugins/demo/console/seed.go:73-89` (`lookupDB`, same shape in `sm-faq-plugin/console`). **Apply to:** seed, reset and any new demo command. + +### Rate-limit buckets and middleware names +**Source:** `sm-bm-app/plugins/jz/bm/routes.go:43-80` (`surf.BucketProvider`, `surf.TrustedProxies`, `surf.ClientIP`). Inline form: `"throttle:20,1"`. **Apply to:** `/_demo/api/v1/*`, realtime token route. + +### Config with defaults off +**Source:** `sm-oauthserver-plugin/plugin.go:110-121` plus embedded `config/config.yaml`; env override `SUMMER_GOLEM15__DEMO__RESET__ENABLED`. **Apply to:** reset, visitor, feedback seed keys; user plugin `turnstile`. + +### Deploy file headers +Every summercms.io deploy file opens with a comment block naming its install path on rome, its upstream ports and its secrets source (see `summercms-io-changelog.conf:1-9`, `summercms.io.conf:1-12`). Copy that convention. + +## No Analog Found + +| File | Role | Data Flow | Reason | +|---|---|---|---| +| `plugins/demo/roadmap/roadmap.go` | utility | transform | No markdown-to-slides generator exists; use the RESEARCH regex | +| `vue-demo-app/app/components/TurnstileWidget.vue` | component | request-response | No Turnstile client anywhere; Cloudflare explicit render, client-only | +| `vue-demo-app/app/pages/contact.vue` | component | request-response | No frontend uses feedback with a form; follow RESEARCH §Feedback (multipart, `type=other`, `page_url`) | + +## Metadata + +**Analog search scope:** sm-demo-app, sm-bm-app (plugins/jz/bm, deploy, frontend/src), sm-summercmsio-app (deploy, scripts, vue-summercmsio-app), sm-oauthserver-plugin, sm-faq-plugin, sm-journal-plugin, sm-translate-plugin, fonoteka.go/plugins/golem15/user, summercms.go/modules/surf +**Files scanned:** ~30 +**Pattern extraction date:** 2026-10-20 diff --git a/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/COVERAGE.md b/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/COVERAGE.md new file mode 100644 index 00000000..80eefa8e --- /dev/null +++ b/.planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/COVERAGE.md @@ -0,0 +1,56 @@ +# API Coverage — Phase 19.4.1 (Cloudflare Turnstile, Centrifugo, Stripe, G15Office) + +> Full coverage by default. Opt-outs are explicit, reasoned decisions. The phase builds no new server-side client for any of these services: Turnstile verification (`bouncer.Turnstile`), Centrifugo (`lighthouse`), Stripe (`sm-pgstripe-plugin`) and G15Office (`sm-feedback-plugin`) are existing integrations. This phase adds the Turnstile browser widget and the centrifuge-js client port, and configures the rest. + +## Cloudflare Turnstile (plan 03 widget; server side through the framework) + +| capability | decision | reason | +|---|---|---| +| turnstile.render (explicit rendering) | INTEGRATE | | +| callback (token) | INTEGRATE | | +| error-callback | INTEGRATE | | +| expired-callback | INTEGRATE | | +| turnstile.reset | INTEGRATE | | +| turnstile.remove | INTEGRATE | | +| language option | INTEGRATE | | +| siteverify (server) | INTEGRATE | | +| turnstile.getResponse | OPT-OUT | not needed: the token arrives through callback | +| turnstile.isExpired | OPT-OUT | not needed: expired-callback clears the token | +| turnstile.execute (deferred or invisible execution) | OPT-OUT | not needed: managed widgets run on render | +| action and cData parameters | OPT-OUT | not needed: one widget per form, no analytics split | +| siteverify idempotency_key | OPT-OUT | not needed: each token is verified once per request by bouncer.Turnstile | + +## Centrifugo v6 (plan 04; own instance on rome) + +| capability | decision | reason | +|---|---|---| +| connection JWT (HS256, client.token.hmac_secret_key) | INTEGRATE | | +| subscribe proxy (channel.proxy.subscribe) | INTEGRATE | | +| server HTTP API publish | INTEGRATE | | +| client allowed_origins | INTEGRATE | | +| namespaces (presentations) | INTEGRATE | | +| health check (websockets:health) | INTEGRATE | | +| client-side publish | OPT-OUT | explicitly out of scope: browsers never publish; Go publishes after authorizing the remote action | +| client RPC | OPT-OUT | not needed: the remote uses HTTP actions | +| presence and join/leave | OPT-OUT | not needed: the speaker view does not show who is connected | +| history and recovery | OPT-OUT | not needed: the remote fetches state over HTTP after reconnecting | +| connect proxy | OPT-OUT | not needed: connection tokens are minted by Go | +| admin web UI | OPT-OUT | explicitly out of scope: never exposed on the public demo | + +## Stripe (plan 02 configuration; test mode only, D-08) + +| capability | decision | reason | +|---|---|---| +| test-mode API keys | INTEGRATE | | +| webhook endpoint for the subscription events | INTEGRATE | | +| price sync of subscription plans (existing admin action) | INTEGRATE | | +| Checkout and billing portal (existing pgstripe flows) | INTEGRATE | | +| live mode | OPT-OUT | explicitly out of scope: D-08 keeps the demo in test mode | +| other Stripe products (Connect, invoicing UI, refunds UI) | OPT-OUT | explicitly out of scope: payment features belong to the payment plugins (Phases 18 and 18.1) | + +## G15Office (plan 03 configuration through golem15.feedback) + +| capability | decision | reason | +|---|---|---| +| task creation for a feedback submission | INTEGRATE | | +| other G15Office endpoints | OPT-OUT | not needed: the demo only files contact requests as tasks |