docs(phase-19.4.1): add research and validation strategy

This commit is contained in:
2026-10-20 02:42:14 +02:00
parent 741def2ceb
commit 1200346bdd
2 changed files with 881 additions and 0 deletions

View File

@@ -0,0 +1,782 @@
# Phase 19.4.1: Early public demo on demo.summercms.io - Research
**Researched:** 2026-10-20
**Domain:** Release engineering (Go module tags, module-mode build), rome deployment (nginx, supervisor, Postgres, certbot, Centrifugo), a Nuxt 4 site port (bm Vite SPA to Nuxt pages), seeding, i18n and abuse protection
**Confidence:** HIGH for repository state, tags and build mechanics (probed this session); MEDIUM for the Nuxt port and the Centrifugo config (official docs plus bm precedent); LOW only where marked `[ASSUMED]`
## Summary
`sm-demo-app` is further along than the phase description suggests, but **not one byte of it is on git.golem15.com**. Neither `golem15/sm-demo-app` nor `golem15/vue-demo-app` exists as a remote (`git ls-remote` answers "Cannot find repository" for both). The local repo has 7 commits and no `origin`, and its `go.mod` has **no `require` lines at all**. It builds only through `go.work` today. The 16 plugin ids in `summer.yaml` come from 14 shared plugin repos (pgstripe ships two plugin ids) plus the in-repo `golem15.demo`.
A release build without `go.work` works. This session it was proven by compiling the demo set in pure module mode (`GOWORK=off`) against framework `v0.3.0` and the pushed plugin heads; the build and the boot test both pass. Two mechanical problems were found along the way. First, `sm-paymentgatewaysubscriptions-plugin v0.1.1` and `sm-pgstripe-plugin v0.1.1` require `sm-paymentgateway-plugin v0.0.0`, a placeholder that does not exist. A module-mode build fails on it with `unknown revision v0.0.0` unless paymentgateway gets a real tag and those two plugins are re-tagged, or the app carries a version-pinned `replace`. Second, five shared plugins have **no tag at all** (sitemap, menu, faq, paymentgateway, oauthserver), and three have untagged commits the demo needs (translate, presentations, plus journal and user once this phase changes them).
The product work has three hard spots that CONTEXT does not spell out:
1. **Turnstile on register and login needs a change to `sm-user-plugin`.** Its routes carry no Turnstile middleware, and the framework has no hook that lets an app add middleware to another plugin's routes.
2. **The realtime wiring for the phone remote lives in the bm *app plugin*, not in `sm-presentations-plugin`.** The lighthouse service, the user lookup, `lighthouse.Mount` and the token-route bucket all sit in bm's app plugin, so `golem15.demo` must reproduce `plugins/jz/bm/realtime.go` and `mountRealtime`.
3. **EN/PL content is uneven across plugins.** FAQ and menu take `?locale=`. Journal returns `translations` per post, but `vue-journal-layer` v0.5.0 strips them. Quizzes and presentations are not translatable at all, so they need one seeded copy per language. Quizzes can be selected by their `context` field; for decks, the frontend picks the slug for the active locale.
**Primary recommendation:** Build tracer-first in four plans. (1) Release and deploy skeleton: tags, a module-mode `go.mod`, repo creation and push, `scripts/build.sh`, the `deploy/` configs, `DEPLOY.md` and a first live deploy behind go-ahead checkpoints. (2) Accounts, safety and the tour: Turnstile through `sm-user-plugin`, register and login, the banner, the backend call to action through feedback, the reset job, EN/PL, the summercms.io look and the journal author card. (3) Presentations and quizzes: the bm port, realtime on the demo's own Centrifugo, the roadmap deck and the quiz pages. (4) Unit tests and the final live verification.
## Project Constraints (from CLAUDE.md)
- Commands: `go vet ./...`, `go test -short ./...` (fast), `go test ./...` (needs Docker; tests under `-short` skip containers). Keep vet and tests green at every commit.
- One Go module per repo. A new dependency needs a research doc or decision note naming it. YAML only through `goccy/go-yaml`, never `gopkg.in/yaml.v3`.
- Compiled plugins only. No runtime plugin loading.
- When porting, response shapes are not "improved". Additive changes to shared plugins are allowed, and core plugins must avoid breaking changes (`sm-user-plugin` is consumed by fonoteka, bm, grzyby and summercms.io).
- Framework code never names a consuming application. This applies to framework READMEs and docs, **not** to `sm-demo-app` itself. The roadmap deck generator must still avoid leaking client names (see Pitfall 7).
- Documentation: a change to a plugin's exported API, config keys, CLI or dependencies updates that plugin's `README.md` in the same commit. README structure is fixed (H1, summary, import, **Name**, Overview, Features, Usage, API reference, Configuration, CLI, Dependencies, Testing).
- MSD rules: few, large plans (present the plan count and wait for confirmation before writing PLAN.md files); **unit tests are the last plan**; update STATE.md only through `msd_run query state.*`; mirror phase progress in g15office (`.planning/G15OFFICE.md`).
- Commits: no co-author tags; one logical change per commit; planning docs and code in separate commits; stage by explicit path, never `git add -A`; several sessions run at once, so leave others' uncommitted `STATE.md`/`state.json` alone.
- Sibling repos: commit and push the plugin first, then the pointer bump in the app as a separate commit.
- **Do not push, tag or deploy without the user's go-ahead.** Every pushed framework tag `vX.Y.Z` gets a changelog post the same day (`../sm-summercmsio-app/scripts/changelog-post.py`). Plugin tags do not.
- Auto-memory: prefer maintained libraries over hand-rolled protocol or security code; rome is `ssh debian@10.8.3.118 -p 2222` (the alias `rome` does not resolve).
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
#### Roadmap and hosting
- **D-01:** Inserted as 19.4.1, after 19.4 and before 19.5-19.7. Phase 20 narrows to adding the chat family to this live demo.
- **D-02:** Hosted on rome (`ssh debian@10.8.3.118 -p 2222`, passwordless sudo), next to summercms.io and bm, following `sm-summercmsio-app/DEPLOY.md` (nginx, supervisor, PostgreSQL 15 database and role of its own, TLS via certbot, a `DEPLOY.md` with build, upload, release, rollback). The DNS record for `demo.summercms.io` is created by the user.
- **D-03:** `sm-demo-app` gets a git.golem15.com remote, and its `go.mod` requires tagged versions of the framework and every shared plugin, so a release build compiles without `go.work`. Plugins without a suitable tag get one (additive changes are patch tags, potentially breaking ones minor) — **Reversibility:** one-way — pushed Go module tags are cached by proxies and cannot be reused.
- **D-04:** `/backend` is refused by nginx. The site carries a "want to see the backend? contact Golem15" call to action that opens a contact form on the `golem15.feedback` plugin (submissions land in its admin and by mail).
#### Accounts, registration and reset
- **D-05:** Frontend registration is open, as in bm. No demo credentials are shown anywhere on the site.
- **D-06:** `demo:seed` creates one known visitor account whose credentials live only in `.env`/`DEPLOY.md`, for the user's own walkthroughs (including the subscribe flow).
- **D-07:** Register and login are protected by Cloudflare Turnstile (`bouncer.Turnstile`, `turnstile:closed` in production) plus the existing per-IP rate limits. No email verification.
- **D-08:** Payments run in Stripe test mode; checkout uses the test card. Stripe webhooks point at `demo.summercms.io`.
- **D-09:** A reset job wipes and reseeds all demo data (users, posts, quiz attempts, subscriptions, uploads) via `demo:seed`. Its schedule is configurable and it ships **disabled**.
- **D-10:** A banner on every page says this is a demo, data may be reset at any time, and payments use Stripe test mode with card `4242 4242 4242 4242`.
#### Home page and look
- **D-11:** The homepage is a guided tour: a hero, then one card per feature (journal, presentations, quizzes, **payments**, FAQ, accounts), each with a sentence and a link to try it, then the backend call to action. The card says "Payments", not "Subscriptions": the Payment Gateway supports all kinds of payments, and subscriptions are the showcase flow.
- **D-12:** The look matches summercms.io (palette, type, header style); reuse `sm-summercmsio-app/vue-summercmsio-app` components and CSS tokens where practical.
- **D-13:** English and Polish with a language switcher. `demo:seed` writes both languages of the seeded content (posts, quiz, deck slides, FAQ) through the translate plugin, so switching changes content as well as UI.
#### Presentations and quizzes
- **D-14:** The full presentation system comes over from bm's frontend: public view, speaker view, phone remote (pilot), My Presentations. bm is a Vite + Vue Router SPA, so its views and components are ported into Nuxt pages in `vue-demo-app`.
- **D-15:** Any registered user can create decks and present them with the remote; the seeded roadmap deck is public to everyone.
- **D-16:** The roadmap deck is generated by `demo:seed` from the framework's `.planning/ROADMAP.md` (phases done and next) plus a curated feature list with links to the demo pages and the docs. The OIDC provider is shown on slides with screenshots and no links. The deck stays current with each deploy.
- **D-17:** The demo runs its own Centrifugo instance on rome (own secrets and namespace) for speaker and remote sync through `lighthouse`, separate from bm's.
- **D-18:** Quizzes need a login: a registered user takes a seeded quiz and sees score and past attempts; anonymous visitors see the quiz list and a "register to take it" prompt.
#### Folded Todos
- **Expose author subtitle, description and avatar on the Journal author API** (`.planning/todos/pending/journal-author-profile-fields.md`, g15office `esrp4qtx`): the demo's journal pages show author cards, which need these fields.
- **Prompt open admin and site tabs to reload when a new version is deployed** (`.planning/todos/pending/2026-10-11-reload-open-apps-when-a-new-version-is-deployed.md`, g15office `ltcvqext`): the demo is redeployed often; port bm's implementation (`sm-bm-app/frontend/src/stores/pwa.ts`, `src/pwa/sw.js`, the version check in `src/api/http.ts`).
### Claude's Discretion
- How the Nuxt app is served on rome (SSR under Node/supervisor like the summercms.io changelog, or prerendered where pages allow), and the nginx split between Go and Nuxt routes (`sm-demo-app/DEPLOY.md` sketches the Go paths).
- Where the ROADMAP.md text comes from at seed time (embedded at build, or fetched from the framework tag), and how the OIDC screenshots are captured and stored.
- The reset job's mechanism (conga/River scheduled job or a console command under cron) and its config key names.
- Journal page layout (list, post, author card, categories) within the summercms.io look.
### Deferred Ideas (OUT OF SCOPE)
- A live "Log in with SummerCMS" relying-party sample — replaced by screenshots for now; revisit when a client asks to try it.
- Chat, channels, video calls, chatbots, forum, herald, friends on the demo — Phase 20.
- Reviewed todo not folded: **Purge the Nuxt site cache on menu and FAQ admin saves** (`site-cache-purge-for-menu-and-faq.md`, g15office `zugdxorb`) — already implemented in 19.3-04; closes when Phase 19.3 completes.
</user_constraints>
<phase_requirements>
## Phase Requirements
No REQUIREMENTS.md IDs are mapped (ROADMAP: "Requirements: TBD"). The scope is success criteria SC1 to SC5, D-01 to D-18 and the two folded todos.
| ID | Description | Research Support |
|----|-------------|------------------|
| SC1 | Pushed to git.golem15.com; `go.mod` requires tags; builds without `go.work` | §Tag and release matrix, §Module-mode build (probe), Pitfalls 1-2 |
| SC2 | Live on rome with nginx, supervisor, Postgres and TLS; DEPLOY.md; `/backend` refused; backend call to action | §rome deployment, §nginx route split, §Feedback contact form |
| SC3 | Self-registration with Turnstile and rate limits; banner; reset job shipped disabled | §Turnstile, §Reset job, §Seed changes |
| SC4 | Guided tour, summercms.io look, EN/PL; bm presentation system on its own Centrifugo; roadmap deck; quizzes keep attempts | §i18n, §bm port inventory, §Realtime wiring, §Roadmap deck, §Quizzes |
| SC5 | Unit tests in the last plan | §Validation Architecture |
| Todo esrp4qtx | Author subtitle, description, avatar on the Journal author API | §Journal author fields |
| Todo ltcvqext | Reload on deploy | §Reload on deploy |
</phase_requirements>
## Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| Plugin APIs (journal, faq, menu, payments, presentations, quizzes, user, feedback) | API / Backend (Go binary `demo`) | — | Shared plugins own the contracts; the demo only configures and seeds them |
| Demo-only endpoints (public quiz list, JWT deck create/update, realtime token route mounting) | API / Backend (`golem15.demo` app plugin) | — | App-specific glue belongs to the app plugin (bm precedent `plugins/jz/bm`), not to shared plugins |
| Turnstile verification | API / Backend (`sm-user-plugin` route middleware `turnstile:closed`) | Browser (widget renders the token) | The token is only trustworthy when siteverify runs server-side |
| Seeding, roadmap deck generation, reset | API / Backend (console commands in `golem15.demo`, conga scheduler) | Database | Runs in-process with the plugins' models; scheduled through `pact.HasSchedule` |
| Pages, tour, i18n UI, banner | Frontend Server (Nuxt SSR under Node) | Browser | SSR for crawlable tour and journal pages; layers already SSR |
| Speaker view, phone remote, public deck viewer | Browser (client-only routes, `ssr: false`) | Frontend Server (shell only) | About 94 `window`/`document`/`localStorage` touches in the bm views; Centrifugo is browser-only |
| Realtime fan-out | Centrifugo (own instance on rome) | API (token route plus subscribe proxy in Go) | Browsers never publish; Go mints tokens and authorizes subscribes |
| TLS, `/backend` refusal, Go/Nuxt split, `/ws` upgrade | nginx on rome | — | Same pattern as summercms.io and bm |
| Uploads (public files, deck images) | Go (`storage.uploads.serve: true`) | Disk `/srv/.../storage` | Already configured in `config/storage.yaml` |
## Current State (verified this session)
### sm-demo-app repository
- **Remote:** none. `git remote -v` prints nothing; `git ls-remote git@git.golem15.com:golem15/sm-demo-app.git` answers "Cannot find repository: golem15/sm-demo-app". The same holds for `golem15/vue-demo-app`, although `vue-demo-app` has `origin` set to that URL locally (2 commits). [VERIFIED: git ls-remote]
- **Commits:** 7 (`2033502` … `1439b3a fix: serve public uploads so admin image previews load`). [VERIFIED: git log]
- **go.mod** (`sm-demo-app/go.mod:1-5`), verbatim:
```
module git.golem15.com/golem15/sm-demo-app
go 1.27.0
replace git.golem15.com/golem15/summercms => ../summercms.go
```
It has no `require` block. `GOWORK=off go build .` fails today with "module git.golem15.com/golem15/summercms provides package … and is replaced but not required". [VERIFIED: go build run this session]
- **go.work** uses `.`, `./plugins/demo` and 10 plugin submodules, and replaces summercms (`../summercms.go`) plus paymentgateway, pgsub and user to submodule paths (`sm-demo-app/go.work:5-31`). [VERIFIED: file read]
- **plugins/demo** is a nested module (`module git.golem15.com/golem15/sm-demo-app/plugins/demo`) whose `go.mod` has only a `replace` and no requires. [VERIFIED: file read]
- **summer.yaml** lists 16 plugin ids: `golem15.user`, `golem15.translate`, `golem15.journal`, `golem15.sitemap`, `golem15.menu`, `golem15.faq`, `golem15.paymentgateway`, `golem15.pgstripe`, `golem15.paymentgatewaysubscriptions`, `golem15.pgstripesubscriptions` (module `git.golem15.com/golem15/sm-pgstripe-plugin/subscriptions`), `golem15.presentations`, `golem15.quizzes`, `golem15.golem`, `golem15.feedback`, `golem15.oauthserver` and `golem15.demo`. That is **14 shared repos plus the app plugin.** [VERIFIED: sm-demo-app/summer.yaml]
- **Submodule pointers lag the pushed heads.** Journal is at `8de4928` (the remote tag v0.2.0 is `05e38ae`), sitemap at `1bb89ce` (remote `46898c2`), oauthserver at `1651ba0` (remote `0deb9a4`), pgsub at `d5579cd` (remote v0.1.1 `8b3a5c0`) and pgstripe at `a5b3314` (remote v0.1.1 `b870e8d`). The user submodule is on a detached HEAD at `4c5d3b8`, which is the remote tag v0.1.2. [VERIFIED: git submodule status plus ls-remote]
- **demo:seed** (`plugins/demo/console/seed.go`) runs 8 idempotent steps (customer user, journal 2 categories and 4 posts, FAQ `home`, menu `main`, presentation `getting-started`, quiz `summercms-basics`, plans `starter` and `professional`, sitemap). Gaps against CONTEXT:
- The credentials are **hard-coded constants**, verbatim from `seed.go:20-21`: `CustomerEmail = "customer@example.test"` and `CustomerPassword = "Customer123!"`. This conflicts with D-06.
- All content is English only, with no translate rows (D-13).
- There is no roadmap deck (D-16).
- Posts have no `user_id`, so no author (todo esrp4qtx).
- The FAQ answer says the admin is "At `<code>/backend</code>`", and one post says "log in as the demo customer". Both conflict with D-04 and D-05.
- Every step skips when its first row exists, so nothing is upserted. The roadmap deck must be upserted to "stay current" (D-16).
- **vue-demo-app** (Nuxt 4.4.8, Vue 3.5.35; layers journal v0.5.0, menu v0.3.0, faq v0.3.0, paymentgateway v0.2.1) has pages `index`, `login`, `faq` and `subscriptions`, a cookie JWT (`summercms_demo_token`) and no tests. Gaps:
- `login.vue:5-6` pre-fills `'customer@example.test'` and `'Customer123!'` (D-05).
- `layouts/default.vue:37` and `pages/index.vue:31` link to the admin (D-04).
- i18n is `strategy: 'no_prefix'` with only `en` (`nuxt.config.ts:35-37`) (D-13).
- There is no register page, banner, quizzes or presentations.
- **config:** `config/app.yaml:2` has `debug: true` and `:6` has `url: http://127.0.0.1:8097`; `config/http.yaml:10` has `allowed_origins: ["*"]`; mail driver is `log`; queue `work_in_serve: true`. These need production overlays (see §Production config).
### Tag and release matrix (remote state from `git ls-remote`, peeled tags)
| Repo | Latest tag (commit) | Remote head | Untagged commits | Action for D-03 |
|------|--------------------|-------------|------------------|-----------------|
| `summercms` | v0.3.0 (`dd047eb`) | `dd047eb` | none (local has 3 docs-only commits) | **none**; v0.3.0 has Turnstile (surf), `lighthouse.Mount` and `cabana.BackendUser.Subtitle` (checked with `git show v0.3.0:…`) |
| `sm-user-plugin` | v0.1.2 (`4c5d3b8`) | `4c5d3b8` | none | **v0.1.3 (patch)** after the opt-in Turnstile config (this phase) |
| `sm-translate-plugin` | v0.1.1 (`1134dd1`) | `54cf93b` | 1 (`fix: mark the locale cookie Secure on HTTPS requests`) | **v0.1.2 (patch)** on `54cf93b` |
| `sm-journal-plugin` | v0.2.0 (`05e38ae`) | `05e38ae` | none | **v0.2.1 (patch)** after the author `subtitle`/`description`/`avatar` keys (todo esrp4qtx) |
| `sm-sitemap-plugin` | none | `46898c2` | all | **v0.1.0** |
| `sm-menu-plugin` | none | `f7eddd5` | all | **v0.1.0** |
| `sm-faq-plugin` | none | `2efe351` | all | **v0.1.0** |
| `sm-paymentgateway-plugin` | none | `16484ba` | all | **v0.1.0** (required before re-tagging pgsub and pgstripe) |
| `sm-paymentgatewaysubscriptions-plugin` | v0.1.1 (`8b3a5c0`) | `8b3a5c0` | none | **v0.1.2 (patch)**: `go.mod` requires paymentgateway `v0.1.0` instead of the `v0.0.0` placeholder |
| `sm-pgstripe-plugin` | v0.1.1 (`b870e8d`) | `b870e8d` | none | **v0.1.2 (patch)**: requires paymentgateway `v0.1.0` and pgsub `v0.1.2` |
| `sm-presentations-plugin` | v0.1.0 (`63f3fed`) | `b445da7` | 6 (PAT API, remote channel, HMAC tag, theme field, scheme action) | **v0.1.1 (patch, additive)** on `b445da7` (the remote needs these commits) |
| `sm-quizzes-plugin` | v0.1.0 (`362dc02`) | `362dc02` | none | none |
| `sm-golem-plugin` | v0.1.0 (`544aa28`) | `544aa28` | none | none |
| `sm-feedback-plugin` | v0.1.0 (`e06b140`) | `e06b140` | none | none (unless the "by mail" gap is closed in the plugin; see Open Question 1) |
| `sm-oauthserver-plugin` | none | `0deb9a4` | all | **v0.1.0** (requires summercms v0.3.0 and user v0.1.2) |
[VERIFIED: `git ls-remote` per repo, peeled `^{}` lines]
**Tag order** (dependencies first): paymentgateway v0.1.0 → pgsub v0.1.2 → pgstripe v0.1.2; then sitemap, menu, faq, oauthserver, translate and presentations in any order; user v0.1.3 and journal v0.2.1 after their code changes. Each is an annotated tag on a pushed commit. Plugin tags need no changelog post. No framework tag is needed unless the phase changes the framework.
**Cross-phase collision:** Phase 19.5's plans (`19.5-02-PLAN.md` Task 5) propose tagging `sm-user-plugin` **v0.1.2**, but v0.1.2 already exists (`4c5d3b8`). Whichever phase tags next takes the next free number. Plans must read `git ls-remote --tags` at execution and never hard-code a plugin tag number. [VERIFIED: ls-remote and 19.5-02-PLAN.md:369-393]
### Module-mode build (probe run this session)
Scratch copy: `main.go`, `plugins.gen.go`, `boot_test.go`, `config/`, and `plugins/demo` **folded into the root module** (its `go.mod` deleted). Requires: summercms v0.3.0, user v0.1.2, journal v0.2.0, pgsub v0.1.1, pgstripe v0.1.1, quizzes/golem/feedback v0.1.0, plus pseudo-versions for translate, sitemap, menu, faq, paymentgateway, presentations and oauthserver. Command: `GOWORK=off GOPRIVATE='git.golem15.com/*' go mod tidy && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath . && go test -short .`
- `go mod tidy`: OK, but only with `replace git.golem15.com/golem15/sm-paymentgateway-plugin v0.0.0 => git.golem15.com/golem15/sm-paymentgateway-plugin v0.0.0-20261016183724-16484ba7f51b`.
- Without that replace, the failing output was:
```
go: git.golem15.com/golem15/sm-paymentgateway-plugin@v0.0.0: reading git.golem15.com/golem15/sm-paymentgateway-plugin/go.mod at revision v0.0.0: unknown revision v0.0.0
```
- `go build` (linux/amd64, CGO off): OK. `go test -short .` gave `ok git.golem15.com/golem15/sm-demo-app 0.189s`.
- All repos answer anonymous HTTPS `git ls-remote`, and `?go-get=1` serves `go-import` meta, so the build machine needs no credentials for module downloads.
[VERIFIED: probe output above]
A pseudo-version `v0.0.0-2026…` sorts **below** `v0.0.0`, so requiring the paymentgateway pseudo-version does not override the placeholder. A real `v0.1.0` tag does override it in MVS. Whether Go still reads the `v0.0.0` go.mod once v0.1.0 is selected was not observed. Re-tag pgsub and pgstripe anyway (it fixes every consumer), and keep the version-pinned replace as the fallback. [ASSUMED for the "v0.1.0 alone suffices" case]
## Standard Stack
### Go side (sm-demo-app)
| Library / module | Version | Purpose | Why |
|------------------|---------|---------|-----|
| `git.golem15.com/golem15/summercms` | v0.3.0 | Framework (surf Turnstile, lighthouse, conga, attach, cooler) | Already contains every needed feature [VERIFIED: git show v0.3.0] |
| `…/summercms/modules/lighthouse/centrifugo` | v0.3.0 | Centrifugo driver plus `websockets:health` (`centrifugo.Commands(app)`) | bm precedent (`plugins/jz/bm/realtime.go` blank import) |
| Shared plugins | the tags in the matrix above | Features | D-03 |
| `gorm.io/gorm`, `github.com/go-gormigrate/gormigrate/v2` | as the framework pins | Seed and reset | Already in the graph |
No new third-party Go dependency is needed. ROADMAP.md parsing is a line regex (`^- \[( |x)\] \*\*Phase ([0-9.]+): (.+?)\*\* - (.+)$`), not a markdown library.
### Nuxt side (vue-demo-app)
| Package | Version | Purpose | When |
|---------|---------|---------|------|
| `nuxt` / `vue` | 4.4.8 / 3.5.35 (pinned) | Site | Existing |
| layers journal/menu/faq/paymentgateway | v0.5.0 / v0.3.0 / v0.3.0 / v0.2.1 | Content pages | Existing; they bring `@nuxtjs/i18n` 10.4.0, `@nuxt/fonts` 0.14.0, `@nuxtjs/seo` 5.2.1 |
| `centrifuge` | 5.7.3 (bm's exact pin; registry latest 5.7.4) | Browser Centrifugo client for speaker and remote | Dynamic `import('centrifuge')` client-only, as in bm `lib/realtime.ts` |
| `@fontsource/libre-caslon-display`, `@fontsource/ibm-plex-sans`, `@fontsource/courier-prime` | 5.3.0 | Deck theme fonts imported by `DeckViewer.vue` and `PresentationRemoteView.vue` | Only if the deck themes are ported unchanged; alternative is `@nuxt/fonts` families |
| `vitest` (dev) | **3.2.7** (exact, the version `summercms.go/admin` and bm use) | Port bm's `lib/**/*.test.ts` unchanged | Plan 4 |
Do **not** add `pinia`. bm's `pwa`, `auth` and `toast` stores become Nuxt composables or `useState`, and the speaker and remote views don't need a store. No Turnstile npm package either: load Cloudflare's script (`https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit`) client-only and call `turnstile.render` [CITED: developers.cloudflare.com/turnstile/get-started/client-side-rendering]. Use no service worker. Reload on deploy uses Nuxt's built-in app manifest (see below).
**Installation:**
```bash
pnpm --dir vue-demo-app add centrifuge@5.7.3 @fontsource/libre-caslon-display@5.3.0 @fontsource/ibm-plex-sans@5.3.0 @fontsource/courier-prime@5.3.0
pnpm --dir vue-demo-app add -D vitest@3.2.7
```
## Package Legitimacy Audit
| Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition |
|---------|----------|-----|-----------|-------------|---------|-------------|
| centrifuge | npm | years (latest 5.7.4, 2026-09-09) | — | github.com/centrifugal/centrifuge-js | OK | Approved (already in bm, exact 5.7.3) |
| @fontsource/libre-caslon-display | npm | years | — | github.com/fontsource/font-files | OK | Approved (in bm) |
| @fontsource/ibm-plex-sans | npm | years | — | same | OK | Approved (in bm) |
| @fontsource/courier-prime | npm | years | — | same | OK | Approved (in bm) |
| vitest | npm | years | — | github.com/vitest-dev/vitest | SUS ("too-new": the **latest** 5.0.5 was published 2026-10-19) | Pin **3.2.7**, already used by `summercms.go/admin/package.json:41`; no checkpoint needed for that exact version |
| @nuxt/test-utils, happy-dom | npm | — | — | — | SUS (too-new latest) | **Not recommended.** Test the ported pure `lib/` modules with vitest's node environment instead |
`npm view <pkg> scripts.postinstall` is empty for all of them. **Packages removed due to [SLOP]:** none. **Packages flagged [SUS]:** vitest (latest only; use the in-repo pinned 3.2.7). [VERIFIED: npm view plus `msd-tools query package-legitimacy check`]
## Architecture Patterns
### System architecture
```
browser ──https──> nginx demo.summercms.io (TLS, gzip, HSTS)
│ │
│ ├─ /backend ......................... 404 (refused)
│ ├─ /ws .............................. Centrifugo (own instance, 127.0.0.1:<cport>/connection/websocket)
│ ├─ Go prefixes (allow-list) ......... Go `demo serve` 127.0.0.1:<gport>
│ │ /_user/ /_journal/ /_menu/ /_faq/ /_paymentgateway/ /_paymentgatewaysubscriptions/
│ │ /_presentations/ /_quizzes/ /_feedback/ /_demo/ /api/realtime/token
│ │ /webhook/stripe/ /storage/app/uploads/public /sitemap.xml /sitemap.xsl
│ │ /plugins/golem15/feedback/assets/js/embed.js (only if the widget is used)
│ └─ everything else .................. Nuxt SSR (node .output/server/index.mjs) 127.0.0.1:<nport>
│ │ SSR fetches → Go on loopback (NUXT_*_API_BASE)
│ │ /api/__site__/cache/purge ← Go after admin saves (loopback only)
│
│ speaker/remote pages (client-only) ── GET /api/realtime/token (jwt.auth + presentations.realtime-owner)
│ └─ WS /ws ── Centrifugo ── subscribe proxy POST 127.0.0.1:<gport>/api/realtime/subscribe
│ (X-Centrifugo-Secret, never via nginx)
│ phone remote taps ── POST /_presentations/api/v1/mine/{slug}/remote ── Go publishes via Centrifugo HTTP API
│
Go `demo serve` ── Postgres 15 (db summercms_demo, own role) ── storage/ (uploads)
└─ conga worker in-process: Stripe webhooks jobs, scheduled demo:reset (only when enabled)
Stripe (test mode) ── POST https://demo.summercms.io/webhook/stripe/subscriptions
```
### Recommended structure (changes only)
```
sm-demo-app/
├── go.mod # requires tags; no replace of the framework (go.work keeps it)
├── go.work # dev only: use ./plugins/golem15/*; replace summercms => ../summercms.go
├── plugins/demo/ # package of the ROOT module (delete plugins/demo/go.mod)
│ ├── plugin.go # + realtime wiring, routes, buckets, schedule, config
│ ├── realtime.go # port of sm-bm-app/plugins/jz/bm/realtime.go
│ ├── routes.go # /_demo/api/v1/{quizzes,presentations}, lighthouse.Mount
│ ├── config/config.yaml # golem15.demo.* defaults (reset disabled, visitor empty)
│ ├── console/seed.go # EN/PL, visitor from config, author, upserted roadmap deck
│ ├── console/reset.go # demo:reset --force
│ ├── roadmap/roadmap.go # ROADMAP.md parser plus curated feature list
│ └── content/ROADMAP.md, content/oidc/*.png # go:embed snapshots
├── config/env/production/ # app.yaml (debug false, url), http.yaml (cors), golem15.user.yaml (turnstile)
├── deploy/{env.example, site.env.example, centrifugo.json.example}
├── deploy/nginx/demo.summercms.io.conf
├── deploy/supervisor/{summercms-demo,summercms-demo-site,summercms-demo-centrifugo}.conf
├── scripts/{build.sh, check-deploy.sh, smoke.sh, sync-roadmap.sh, dev.sh}
└── vue-demo-app/
├── app/pages/{index,register,login,faq,subscriptions,quizzes/index,quizzes/[slug],
│ presentations/index (My Presentations),presentations/new,
│ presentations/[slug]/index (speaker),presentations/[slug]/pilot,
│ p/[slugHash] (public deck),contact}.vue
├── app/components/{SiteHeader,SiteFooter,DemoBanner,TourCard,BackendCta,
│ TurnstileWidget,AuthorCard,UpdateBanner,deck/*,remote/*}.vue
├── app/lib/ (ported verbatim: deck/*.ts, realtime.ts, pilot.ts, quizSession.ts, markdown.ts, http.ts)
├── app/plugins/{auth.ts, manifest-update.client.ts}
└── i18n/locales/{en,pl}.json
```
### Pattern 1: Module-mode `go.mod` plus dev `go.work`
**What:** `go.mod` lists real tags and nothing local. `go.work` (committed, as today) keeps `use` lines for the submodules and the framework replace, so local development keeps compiling the checkouts.
**Release build:** `GOWORK=off GOPRIVATE='git.golem15.com/*' CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -o bin/demo .`
**Pin check** (scripts): for each submodule, `git -C plugins/golem15/<x> rev-parse HEAD` must equal the commit of the tag `go list -m -f '{{.Version}}' <module>` returns. Otherwise dev and release compile different code.
**Example** (go.mod head after the change):
```
module git.golem15.com/golem15/sm-demo-app
go 1.27.0
require (
git.golem15.com/golem15/summercms v0.3.0
git.golem15.com/golem15/sm-user-plugin v0.1.3
git.golem15.com/golem15/sm-paymentgateway-plugin v0.1.0
// … every shared plugin at its tag (matrix above)
)
```
The `plugins/demo` folding removes the nested module. `summer build` only writes import paths (`internal/build/build.go`), so `git.golem15.com/golem15/sm-demo-app/plugins/demo` stays valid as a package path. The `summercms.go/internal/build/scaffold.go` convention (`require <mod> v0.0.0` plus `replace => ./plugins/x`) also works in module mode, if the planner prefers to keep the nested module.
### Pattern 2: Realtime wiring in the app plugin (bm precedent)
`golem15.demo` must do what `sm-bm-app/plugins/jz/bm` does:
- In `Boot`, call `lighthouse.From(app)` and `svc.SetUserLookup(...)`, which reads `users.id, name` per call.
- Blank-import `.../lighthouse/centrifugo`.
- In `Routes`, mount realtime only when `app.HasPlugin("golem15.presentations")`:
```go
return lighthouse.Mount(r, p.realtime.Driver(), lighthouse.Surfaces{
UserAuth: surf.Use("jwt.auth", "presentations.realtime-owner", bucketRealtime),
ServerToServer: surf.Use(),
Middleware: surf.Use(),
})
```
(Source: `sm-bm-app/plugins/jz/bm/routes.go:195-203`.)
- Provide a named bucket `golem15.demo.realtime` (120 a minute per user, else per IP) through `surf.BucketProvider`.
- Add `centrifugo.Commands(p.app)` to `Commands()` (`websockets:health`).
The routes it mounts are `GET /api/realtime/token` and `POST /api/realtime/subscribe` (contract test `sm-bm-app/realtime_test.go:20-27`, verbatim: `tokenRoute = "GET /api/realtime/token"`, `subscribeRoute = "POST /api/realtime/subscribe"`). Port that test as `sm-demo-app/realtime_test.go`, with `jz.bm` replaced by `golem15.demo` and `throttle:jz.bm.realtime` by the demo bucket.
### Pattern 3: Demo endpoints in `golem15.demo` (no shared plugin changes)
- **Deck create and update for any registered user (D-15).** The presentations handlers `api.New(app).ManageCreate`, `ManageUpdate`, `ManageIndex` and `ManageShow` read the owner from `bouncer.User` (`mineCaller`). The plugin mounts them only behind `user.api_token` at `/api/v1/presentations` (`presentations/routes.go:44-50`). The demo mounts the same handlers under `r.Group("/_demo/api/v1/presentations", surf.Use("jwt.auth", "throttle:20,1"), …)`. The input is `classes.ManagedCreate` (title, slug, slides `[]ManagedSlide{title, body_html, …}`), and every HTML field passes `classes.Clean` (bluemonday) on write (`classes/manage.go:553-558`). [VERIFIED: files read]
- **Public quiz list (D-18).** `/_quizzes/api/v1/quizzes` sits behind `jwt.auth` (`quizzes/routes.go:23-24`). The demo adds `GET /_demo/api/v1/quizzes?context=demo:<locale>` (throttled, read-only: title, slug, description, question count) for anonymous visitors.
### Pattern 4: Scheduled reset through conga, off by default
- `demo:reset` (console, needs `--force`) wipes the demo data and runs the seed steps. Keep the plans, the admin users, the settings (pgstripe test-mode keys, feedback settings), the locales, the sitemap and migrations. Wipe:
- frontend `users` (cascade: tokens, subscriptions, orders, quiz attempts, owned decks),
- journal posts and categories,
- FAQ and menu (they are reseeded),
- quizzes and attempts,
- presentations,
- pgsub subscriptions, paymentgateway orders and payments,
- feedback submissions,
- `system_files` rows other than backend avatars.
- Uploads: collect `attach.BlobKeys(f)` for the deleted `system_files` rows inside the transaction, then call `attach.DeleteKeys(ctx, bucket, keys)` **after commit** (two-phase delete, `lagoon/attach/file.go:113-188`). Journal editor media uploaded outside `system_files` need a bucket prefix listing; verify the journal media key prefix before deleting.
- `pact.HasSchedule` returns an entry only when `golem15.demo.reset.enabled` is true: `[]pact.ScheduledCommand{{Command: "demo:reset", Args: []string{"--force"}, Cadence: pact.DailyAt(h, m)}}` from `golem15.demo.reset.at` (`"04:00"`). Conga runs it in-process (`queue.work_in_serve: true`), leader-elected, one attempt (`conga/README.md:38`). `schedule:run --once` under cron is the fallback.
- Config keys (plugin config, env `SUMMER_GOLEM15__DEMO__…`):
```yaml
reset:
enabled: false # D-09 ships disabled
at: "04:00" # daily, app.timezone
visitor:
email: "" # D-06: set only in the server .env
password: ""
```
### Pattern 5: Turnstile through `sm-user-plugin` (opt-in, additive)
- Add the key `golem15.user.turnstile` (`""` = off, the default; `open`; `closed`). `Routes` appends `"turnstile:"+mode` to `/login` and `/register` (optionally `/forgot-password`) when set. `turnstile:` must be `open` or `closed`, or boot fails (`surf/router.go:451-452`).
- The token field defaults to `turnstile_token` (`bouncer.DefaultTurnstileField`). The secret is `turnstile.secret` / `SUMMER_TURNSTILE__SECRET` (`surf/README.md:165-166`).
- `turnstile:closed` answers **503 only in the production environment** when the secret is empty and passes elsewhere. With a secret, a bad token gets 422.
- **`SUMMER_ENV` defaults to `production`** (`compass/README.md:107`). A local `scripts/dev.sh` without `SUMMER_ENV` runs in production mode, so set the Cloudflare **test** secret `1x0000000000000000000000000000000AA` and site key `1x00000000000000000000AA` locally, or export `SUMMER_ENV=local`. [CITED: developers.cloudflare.com/turnstile/troubleshooting/testing]
- This change goes in the canonical checkout `../fonoteka.go/plugins/golem15/user` (the 19.5 plans use the same path). Then: README Configuration, tag v0.1.3, and bump the pointers in sm-demo-app only. Other apps keep their pins.
### Pattern 6: Reload on deploy (todo ltcvqext, site part)
Nuxt's app manifest is on by default. `experimental.checkOutdatedBuildInterval` (ms) sets the polling interval, and the client hook `app:manifest:update` (`{ id, timestamp }`) fires when a newer build is detected [CITED: nuxt.com/docs/4.x/guide/going-further/experimental-features; nuxt.com/docs/4.x/api/advanced/hooks]. Port bm's *behaviour*: a non-blocking "New version available — reload" banner (bm `stores/pwa.ts` `updateReady`/`applyUpdate`), never an automatic reload. bm's service worker (`src/pwa/sw.js`) exists for offline and install and is **not** needed. Implementation:
- `app/plugins/manifest-update.client.ts`: `nuxtApp.hook('app:manifest:update', () => updateReady.value = true)`.
- `nuxt.config.ts`: `experimental: { checkOutdatedBuildInterval: 5 * 60 * 1000 }`.
- Also re-check on `visibilitychange`, following bm's `reg.update()` on visible.
The default interval is believed to be one hour [ASSUMED]. The admin SPA (boardwalk) part of the todo is framework work; see Open Question 3.
### Anti-patterns to avoid
- **Putting `replace … => ../summercms.go` in go.mod.** It ties the release to a sibling checkout and breaks SC1. Keep it in `go.work`.
- **Re-using a pushed tag, or tagging an unpushed commit.** Tags are one-way (D-03).
- **Proxying `/api/` wholesale to Go.** Nuxt owns `/api/__site__/cache/purge` and the layers' `/api/__sitemap__/journal`. Route only `/api/realtime/token` to Go, and never route `/api/realtime/subscribe` publicly.
- **SSR-rendering the deck views.** They touch `window`, `document` and `localStorage` about 94 times. Use `routeRules: { '/presentations/**': { ssr: false }, '/p/**': { ssr: false } }`.
- **Showing credentials or the admin link anywhere on the site**, including the seeded FAQ and journal content (D-05, D-04).
- **Adding Turnstile by forking user routes into the demo plugin.** Duplicate routes fail boot, and the framework has no global middleware hook. Change the plugin, opt-in.
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Captcha verification | Own siteverify client | `bouncer.Turnstile` through `turnstile:closed` | Timeouts, the token length cap, open/closed semantics and refusal envelopes already exist |
| Realtime auth | Own JWT minting or WS proxy | `lighthouse` Centrifugo driver plus `lighthouse.Mount` | Token route, subscribe proxy with a constant-time secret, HMAC-tagged channels |
| Scheduling | systemd timer or Go ticker | `pact.HasSchedule` (conga, leader-elected) or `schedule:run --once` | Compiled entry table; no double runs across failover |
| Deleting uploads | `rm -rf storage/…` | `attach.BlobKeys` plus `attach.DeleteKeys` after commit | Keeps rows and blobs consistent; never deletes kept files |
| HTML sanitizing of user decks | Own filter | `classes.Clean` (bluemonday, already on every managed write) | XSS on the demo origin |
| Reload detection | Own `/version` poller | Nuxt app manifest (`app:manifest:update`) | Already emitted per build |
| Captcha widget | `vue-turnstile`-style package | Cloudflare script plus `turnstile.render`, client-only | No extra dependency |
| Release build | Hand-built `go.work` in temp | `GOWORK=off` against tags (probe-proven) | SC1 literally |
## Runtime State Inventory
Not a rename phase. Partly applicable because it **deploys new runtime state**; recorded for rollback planning:
| Category | Items | Action |
|----------|-------|--------|
| Stored data | New Postgres DB `summercms_demo` and role on rome PG15; Stripe test-mode customers, subscriptions and prices (lookup keys `starter_*`, `professional_*`) | Created by migrate, seed and the admin plan sync; reset keeps plans so no Stripe re-sync is needed |
| Live service config | Stripe webhook endpoint (dashboard), Cloudflare Turnstile site (dashboard), DNS `demo.summercms.io` (user), g15office tasks | Human-action checkpoints; record in DEPLOY.md |
| OS-registered state | 3 supervisor programs, 1 nginx site, 1 certbot lineage `demo.summercms.io`, Centrifugo config file | Files in `deploy/`; DEPLOY.md first-release steps |
| Secrets/env vars | `/srv/summercms-demo/.env` (DSN, app key, two JWT secrets, Stripe test keys, Turnstile secret, realtime keys, site-cache token, visitor credentials), `site.env` (Nuxt purge token, Turnstile site key), Centrifugo `hmac_secret_key`, `http_api.key` and proxy secret | Generated on rome, never committed |
| Build artifacts | `bin/demo`, `vue-demo-app/.output`, `plugins.gen.go`/`main.go` (committed, regenerated by `summer build`) | Release build refuses drift (summercms.io `scripts/build.sh` pattern) |
## rome deployment (from repository files only; nothing was run on the server)
- **Ports.** bm's live install already uses **127.0.0.1:8097** (`sm-bm-app/DEPLOY.md:28`), the demo's local Go port. summercms.io uses 8095 (Go) and 3095 (Nuxt), and bm's shared Centrifugo uses 8000. Pick new loopback ports, for example Go **8098**, Nuxt **3098**, Centrifugo **8010**, all *check on rome with `ss -ltnp`*. [VERIFIED: DEPLOY.md files; the free ports are ASSUMED]
- **Layout** (summercms.io pattern):
- install root `/srv/summercms-demo` (root-owned), `bin/`, `config/` (root), `storage/` (owned by the service user),
- system user `summercms-demo` (or the existing `summercms`),
- `.env` with mode 0640 root:group,
- `site/.output` for Nuxt,
- the Node path from the summercms.io supervisor file (`/home/deploy/.nvm/versions/node/v22.22.0/bin/node`; system Node is 18).
- **Supervisor programs:**
- `summercms-demo`: `bin/demo serve --addr 127.0.0.1:8098` with `SUMMER_ENV="production"`.
- `summercms-demo-site`: `node --env-file=site.env .output/server/index.mjs` with `NITRO_HOST/NITRO_PORT` and `NUXT_*_API_BASE=http://127.0.0.1:8098/...`.
- `summercms-demo-centrifugo`: `centrifugo -c /srv/summercms-demo/centrifugo.json`. The binary path is *check on rome*; v6.5.1 runs there for bm, and v6.5.1 is installed locally for `centrifugo checkconfig`.
- **Centrifugo v6 config** (own instance, D-17) [CITED: centrifugal.dev/docs/server/configuration, /docs/server/proxy]:
```json
{
"client": { "token": { "hmac_secret_key": "<secret>" }, "allowed_origins": ["https://demo.summercms.io"] },
"http_api": { "key": "<api key>" },
"http_server": { "port": 8010 },
"channel": {
"proxy": { "subscribe": { "endpoint": "http://127.0.0.1:8098/api/realtime/subscribe",
"http": { "static_headers": { "X-Centrifugo-Secret": "<proxy secret>" } } } },
"namespaces": [ { "name": "presentations", "subscribe_proxy_enabled": true } ]
}
}
```
The `static_headers` placement under `channel.proxy.subscribe.http` is inferred from the connect-proxy example [ASSUMED]; validate with `centrifugo checkconfig -c` in `scripts/check-deploy.sh`. Its own `hmac_secret_key` removes bm's shared-secret caveat (`sm-bm-app/DEPLOY.md:327-331`).
- **Go realtime env:** `SUMMER_REALTIME__DRIVER=centrifugo`, `…__CENTRIFUGO__API_URL=http://127.0.0.1:8010/api`, `…__API_KEY`, `…__TOKEN_SECRET`, `…__PROXY_SECRET`, `…__WS_URL=/ws`, and optionally `SUMMER_GOLEM15__PRESENTATIONS__REMOTE_KEY` (bm `config/realtime.yaml`). Add `config/realtime.yaml` with `driver: "null"` (quoted) to the demo, so local and test runs mount nothing.
- **nginx** (`deploy/nginx/demo.summercms.io.conf`):
- port 80 serves `/.well-known/acme-challenge/` and redirects everything else,
- 443 has `location ^~ /backend { return 404; }`, the `location = /ws` upgrade block (bm `deploy/nginx/bm.jakubzych.com.conf:62-70`) to `127.0.0.1:8010/connection/websocket`, one `location ^~` per Go prefix with `proxy_pass http://127.0.0.1:8098` and the four `proxy_set_header` lines, and `location /` to Nuxt.
- `client_max_body_size` 11m on `/_journal/api/v1/` if editor uploads are exposed. The deck create endpoint is JSON only.
- Do not route `/oauth`, `/.well-known/openid-configuration` or `/api/realtime/subscribe`.
- TLS: a **new** certbot lineage `certbot certonly --webroot -w /var/www/letsencrypt -d demo.summercms.io` (bm precedent, `sm-bm-app/DEPLOY.md:28`); the summercms.io cert covers only the apex and `www`.
- **Release steps** (each deploy): rsync `config/`, `bin/demo.new` and `site/.output.new`; `./bin/demo.new migrate`; `./bin/demo.new demo:seed` (upserts the roadmap deck, D-16); swap with `.prev`; `supervisorctl restart` all three (Centrifugo only when its config changed); `websockets:health`; curl checks. Rollback mirrors summercms.io (binary `.prev`, `.output.prev`; migrations not reversed).
- **First deploy only:**
- create the DB and role;
- `admin:create` (the admin stays reachable only through `ssh -N -L 8098:127.0.0.1:8098`);
- in the admin: pgstripe settings (enabled, test mode, publishable key) and Subscription Plans → **Sync All to Stripe**. There is no CLI for the sync; pgstripe exposes only `pgstripe:import-winter`.
- Stripe dashboard webhook `https://demo.summercms.io/webhook/stripe/subscriptions` with the events that dev.sh forwards (`customer.subscription.created,updated,deleted`, `invoice.paid`, `invoice.payment_failed`).
- Feedback settings: enable, allowed origin `demo.summercms.io`.
- **Site cache purge:** `SUMMER_SITE_CACHE__PURGE_URL=http://127.0.0.1:3098/api/__site__/cache/purge` plus the same token as `NUXT_SITE_CACHE_PURGE_TOKEN` (summercms.io `deploy/env.example`).
- **Production config overlay** (`config/env/production/`, selected by `SUMMER_ENV`):
- `app.yaml`: `debug: false`, `url: https://demo.summercms.io`;
- `golem15.paymentgatewaysubscriptions.yaml`: return URLs on the public origin;
- `http.yaml`: restrict CORS to the origin, or drop the `cors.paths` (same-origin behind nginx);
- `golem15.presentations.yaml`: `public_path: /p` (the default is `/prezentacje`, `presentations/config/config.yaml:1`);
- `golem15.user.yaml`: `turnstile: closed`.
## Feedback contact form (D-04)
`golem15.feedback` is a bug-report widget backend, not a contact form [VERIFIED: README plus controllers]:
- `POST /_feedback/api/v1/{key}/submit`, multipart, fields `message` (required, ≤5000), `type` (`bug`/`feature`/`other`), `email` (optional), `page_url` (required), optional `screenshot`.
- It is gated by the widget key in the URL and an **Origin allow-list** (an empty list refuses every Origin), and throttled 10 a minute per IP.
- Submissions are listed in Admin › Feedback › Submissions and **filed as G15Office tasks** by a job.
- It sends **no mail**. "Lands … by mail" (D-04) is not supported today (see Open Question 1).
- The key is generated on first settings access (`wk_…`). The Nuxt contact page needs it at runtime (`NUXT_PUBLIC_FEEDBACK_KEY`). Seed or first deploy enables the widget and sets the allowed origin; `demo:seed` can set `enabled`, `allowed_origins` and the key from `golem15.demo.feedback.*` config so DEPLOY.md stays scriptable.
- The contact page posts `type=other`, `email`, `message` and `page_url=location.href` with `FormData`.
## i18n (D-13)
| Content | Mechanism today | Seed must write |
|---------|-----------------|-----------------|
| UI strings | `@nuxtjs/i18n` 10.4.0 from the journal layer; layers ship `en`/`pl` (journal, paymentgateway) | `vue-demo-app/i18n/locales/{en,pl}.json`; enable both locales (`strategy: 'no_prefix'` with a cookie, or `prefix_except_default`) |
| FAQ | One row per locale (`locale_id`); `GET /_faq/api/v1/faqs/{slug}?locale=xx`; `useFaq(slug, { locale })` | FAQ `home` for `en` and for `pl` (translate migrations seed `en` default and `pl`) |
| Menu | `MenuEntry` translatable; `?locale=` on the API; `useMenu(code, { locale })` | `SetTranslated` titles for `pl` |
| Journal | `Post.Translatable()` = title, content, content_html, excerpt, metadata, slug; **the public API returns host-column values plus a `translations` map** (`serializePost`); layer v0.5.0 **strips `translations`** (`app/utils/view.ts:11`, `postView.ts:17`) and sends no locale | `translate.SetTranslated(ctx, db, post, field, "pl", …)` per field; keep the slug untranslated so the routes match |
| Quizzes | Not translatable; `context` filter on `GET /quizzes?context=` | Two quizzes, `summercms-basics` (`context: demo:en`) and `summercms-podstawy` (`context: demo:pl`) |
| Presentations | Not translatable | Two roadmap decks, slugs `roadmap` and `roadmap-pl`; the tour links the one for the active locale |
**Journal PL content without a layer release:** override the layer's `useJournalApi` in `vue-demo-app/app/composables/useJournalApi.ts` (Nuxt resolves the app's composable over the layer's of the same name). Wrap the fetcher to copy `translations[locale].{title,excerpt,content_html}` over the default fields **before** the layer's views strip them. Unit-test the mapper. Record a todo to upstream this into vue-journal-layer, because grzyby and summercms.io pin v0.5.0. [ASSUMED that the override precedence holds for this layer composable; verify early in plan 2]
## bm frontend port inventory (D-14)
Source `sm-bm-app/frontend/src` (Vite 7, Vue Router 4, Pinia 3, `centrifuge` 5.7.3, vitest). About 6,000 lines to bring over:
| bm file | Lines | Nuxt target | Port notes |
|---------|-------|-------------|------------|
| `views/PresentationPublicView.vue` | 117 | `pages/p/[slugHash].vue` (`ssr:false`) | `presentationApi.publicDeck` (auth false) |
| `views/PresentationSpeakerView.vue` | 611 | `pages/presentations/[slug]/index.vue` (`ssr:false`) | `useRouter` → `navigateTo`; clock, engine and remote libs |
| `views/PresentationRemoteView.vue` | 714 | `pages/presentations/[slug]/pilot.vue` (`ssr:false`) | fontsource imports |
| `views/MyPresentationsView.vue` | 91 | `pages/presentations/index.vue` | Polish strings → i18n; add "New deck" |
| `views/QuizzesView.vue`, `QuizView.vue` | 50, 298 | `pages/quizzes/index.vue`, `[slug].vue` | Anonymous list via `/_demo/api/v1/quizzes`; the attempt views need login |
| `views/RegisterView.vue`, `LoginView.vue` | 77, 61 | `pages/register.vue`, `login.vue` | Add `TurnstileWidget`; body field `turnstile_token` |
| `components/deck/DeckViewer.vue`, `SpeakerTools.vue` | 772, 129 | `components/deck/*` | `?inline` CSS imports work in Nuxt's Vite |
| `components/remote/*` (3) | 833 | `components/remote/*` | — |
| `components/AttemptList.vue` | 29 | same | — |
| `lib/deck/{clock,compose,engine,reading,remote,styles,themes}.ts` (+ tests) | ~1,900 (+~1,900 tests) | `app/lib/deck/*` | Pure TS; tests run unchanged under vitest |
| `lib/realtime.ts` (+test) | 99 | `app/lib/realtime.ts` | `TOKEN_PATH = '/api/realtime/token'`, `REALTIME_WS_PATH = '/ws'` (`lib/realtime.ts:5,8`); token client must read the demo cookie JWT |
| `lib/pilot.ts`, `quizSession.ts`, `markdown.ts`, `forms.ts` | 64, 156, 119, 22 | `app/lib/*` | `pilot.ts` imports the type `RouteLocationRaw` from vue-router; Nuxt re-exports it |
| `api/http.ts` (+test), `client.ts`, `presentations.ts`, `quizzes.ts`, `user.ts`, `types.ts` | 226, 23, 80, 32, 30, 207 | `app/lib/api/*` | Replace bm's `TokenStore` (localStorage) with a cookie-backed store over `DEMO_TOKEN_COOKIE`, so SSR pages and the payment layer share one session |
| `deck/deck.css`, `deck-speaker.css`, `themes/{atlas,monografia}.css` | 424 | `app/assets/deck/*` | — |
| `stores/pwa.ts`, `pwa/sw.js` | 97, 83 | Not ported as files; see Pattern 6 | — |
**Not ported:** dashboard, courses, schedule, readings, materials, syllabi, electives and calendar views (bm-specific). bm's API paths: presentations `'/_presentations/api/v1'`, quizzes `'/_quizzes/api/v1'`, user `'/_user/api/v1'` (`api/*.ts`, verbatim constants `const base = …`).
## Roadmap deck (D-16)
- **Source:** embed a committed snapshot `plugins/demo/content/ROADMAP.md` (`//go:embed`). `scripts/sync-roadmap.sh` refreshes it with `git -C ../summercms.go show origin/master:.planning/ROADMAP.md` (committed content only). `scripts/build.sh release` warns or fails when the snapshot differs from the framework's `origin/master`. ROADMAP.md is tracked in the framework repo (`git ls-files` confirms), and `git show v0.3.0:.planning/ROADMAP.md` works.
- **Parse:** lines matching `- [x] **Phase N: Title** - summary (completed YYYY-MM-DD)` and `- [ ] **Phase N: Title** - summary`. Today there are 227 `[x]` and 18 `[ ]` lines in the whole file, but only the Phases list (before `## Phase Details`) is the source. Slides: "Done" (grouped), "Next" (unchecked), then a curated feature list (Go map in `roadmap.go`) with links to demo pages and `https://summercms.io/docs/...`, then 2-3 OIDC slides with `<img src="oidc-consent.png">` and no links.
- **Images:** the presentation `images` attachMany (public). Slide HTML refers to bare file names, and the API rewrites them to `/_presentations/api/v1/public/{slugHash}/images/{name}` (`classes/sanitize.go:105-194`). The seed stores the embedded PNGs with `attach.Store` against the bucket (open it with `attach.OpenBucket(ctx, app.Config)` when the command runs outside serve; `presentations:import` does the same).
- **Upsert:** on every `demo:seed`, find the deck by slug, replace its slides (the `classes.ManagedPatch` semantics, "Sending slides replaces the deck's slides"), keep `public_hash` so shared links survive, and set `is_published=true`.
- **Screenshots:** a human-action checkpoint. Run the local demo (`scripts/dev.sh up`), open the oauthserver consent page, the branded error page and Admin › Clients, save PNGs (≤1600px wide) under `plugins/demo/content/oidc/`, and commit them.
## Journal author fields (todo esrp4qtx)
The framework side is done (`cabana.BackendUser.Subtitle()`, `cabana.BackendUserAvatar(ctx, db, userID)`; `backend_users.metadata.subtitle`). In `sm-journal-plugin/controllers/api/authors.go`:
- extend `authorColumns`/`authorRow` with `metadata`;
- `authorProfile` adds `subtitle`, `description` (`metadata.description`) and `avatar` (URL or null);
- update README and `TestJournalAuthorProfile`'s exact key set (`journal_api_authors_test.go:91`).
The description editor in My account stays out of scope. Seed writes it through `metadata` directly. The demo author card fetches `GET /_journal/api/v1/authors/{login}` using `post.author.login` (the post serializer exposes `id, first_name, last_name, login`). Seed sets `golem15_journal_posts.user_id` to the admin created by `admin:create` (or a dedicated "SummerCMS Team" backend user created in the seed) and fills its metadata and avatar.
## Common Pitfalls
### Pitfall 1: `v0.0.0` placeholder requires break module mode
**What goes wrong:** `GOWORK=off` builds fail with `unknown revision v0.0.0` (output above).
**Why:** pgsub v0.1.1 and pgstripe v0.1.1 `go.mod` require `sm-paymentgateway-plugin v0.0.0`, which only ever worked behind a replace.
**Avoid:** tag paymentgateway v0.1.0, then re-tag pgsub and pgstripe with real requires; probe with pseudo-versions of the pushed commits **before** tagging.
**Warning sign:** `go mod tidy` rewrites a require to `v0.0.0`.
### Pitfall 2: Submodule pointers differ from the required tags
**What goes wrong:** dev (go.work) compiles one commit and release (module mode) another, so tests pass locally against code that is not shipped.
**Avoid:** bump every submodule to its tag commit in the same app commit as the go.mod change; `scripts/check-pins.sh` compares them.
### Pitfall 3: Port collision on rome
**What goes wrong:** the demo's local 8097 is bm's production port.
**Avoid:** dedicated ports, checked with `ss -ltnp`; `check-deploy.sh` asserts nginx `proxy_pass` matches supervisor `--addr`/`NITRO_PORT`.
### Pitfall 4: `SUMMER_ENV` defaults to production
**What goes wrong:** local runs hit `turnstile:closed` 503s, or production overlays apply locally.
**Avoid:** dev.sh exports `SUMMER_ENV=local` (no `config/env/local/`) or uses Cloudflare test keys.
### Pitfall 5: Credentials and admin links leak through content, not code
**What goes wrong:** the seeded FAQ and post bodies, `login.vue` defaults, the README (if the repo becomes public) and the footer link.
**Avoid:** a test greps the built `.output/public` and seeded strings for `/backend` and the visitor email; the visitor credentials come only from config.
### Pitfall 6: nginx `/api/` routing
**What goes wrong:** Nuxt's `/api/__site__/cache/purge` becomes reachable from the internet, or `/api/realtime/token` falls through to Nuxt (404).
**Avoid:** exact `location = /api/realtime/token` to Go; the purge route stays loopback (the journal layer answers 404 to proxied requests unless `allowProxied`).
### Pitfall 7: The roadmap deck exposes client names
**What goes wrong:** ROADMAP.md phase titles name Płytarium, fonoteka, WavePath, Horoskopia and grzybyfunkcjonalne.pl (for example "Phase 30: Płytarium cutover", "Phase 16: grzybyfunkcjonalne.pl on reusable blog views").
**Avoid:** a curated title map plus a denylist. A phase whose title or summary matches a denylisted name is shown under a neutral label ("First production port") or skipped, and a test pins that no denylisted word reaches the slides. Confirm with the user (Assumption A6).
### Pitfall 8: User-created decks on a public origin
**What goes wrong:** any registrant can publish HTML and CSS under `demo.summercms.io` (phishing, CSS exfiltration).
**Avoid:** HTML already passes `classes.Clean`. The demo create endpoint does not accept `css` and `theme` freeform (fixed theme list, `css` stripped), user decks default to unpublished, and the reset wipes them.
### Pitfall 9: Stripe plans after reset
**What goes wrong:** reseeding plans creates rows without Stripe price ids, so checkout breaks until someone clicks "Sync All to Stripe".
**Avoid:** reset keeps `golem15_pgsub_subscription_plans` and pgstripe settings and wipes only subscriptions and orders.
### Pitfall 10: Seed idempotence vs "current" content
**What goes wrong:** the current seed skips existing rows, so a deploy never updates the roadmap deck or the PL translations.
**Avoid:** upsert the roadmap deck and translations; keep "exists" skips for user-editable content.
## Code Examples
### Turnstile on user routes (sm-user-plugin, opt-in)
```go
// Source pattern: sm-user-plugin routes.go:10-26 plus surf turnstile:open|closed (surf/README.md:30,165)
func (p *Plugin) authGuard() []string {
switch mode := strings.TrimSpace(p.app.Config.String("golem15.user.turnstile")); mode {
case "open", "closed":
return []string{"turnstile:" + mode}
default:
return nil // off: unchanged behaviour for every other app
}
}
// g.Post("/login", controllers.Login(p.app), p.authGuard()...)
// g.Post("/register", controllers.Register(p.app), p.authGuard()...)
```
### Client-only Turnstile widget (Nuxt)
```ts
// Source: developers.cloudflare.com/turnstile/get-started/client-side-rendering
useHead({ script: [{ src: 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit', defer: true }] })
onMounted(async () => {
await until(() => (window as any).turnstile).toBeTruthy() // or poll
widgetId = (window as any).turnstile.render(el.value, {
sitekey: useRuntimeConfig().public.turnstileSiteKey,
callback: (t: string) => emit('token', t),
'error-callback': () => emit('token', ''),
})
})
// Tokens are single-use and expire after 300 s: call turnstile.reset(widgetId) after every submit.
```
### Scheduled reset (golem15.demo)
```go
// Source: pact/capabilities.go:146-176 (ScheduledCommand, DailyAt), conga/README.md:38
func (p *Plugin) Schedule() []pact.ScheduledCommand {
if p.app == nil || !p.app.Config.Bool("golem15.demo.reset.enabled") {
return nil
}
h, m := parseHHMM(p.app.Config.String("golem15.demo.reset.at"), 4, 0)
return []pact.ScheduledCommand{{Command: "demo:reset", Args: []string{"--force"}, Cadence: pact.DailyAt(h, m)}}
}
```
(`Config.Bool` is assumed to exist on compass; verify the accessor name.)
## State of the Art
| Old approach | Current approach | When | Impact |
|--------------|------------------|------|--------|
| Shared rome Centrifugo with one HMAC secret (bm) | Per-app Centrifugo instance | D-17 | Tokens are no longer valid across apps |
| bm service worker update prompt | Nuxt app manifest plus `app:manifest:update` | Nuxt ≥3.8 | No SW needed for an SSR site |
| `v0.0.0` plus replace requires between plugins | Real tags and module-mode release | this phase | `GOWORK=off` builds |
| bm Pinia stores | Nuxt composables / `useState` | — | One less dependency |
## Assumptions Log
| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | After paymentgateway v0.1.0 exists, Go may not need the `v0.0.0` go.mod; re-tagging pgsub and pgstripe is still recommended | Module-mode build | Low: the fallback replace is proven |
| A2 | Ports 8098, 3098 and 8010 are free on rome | rome deployment | Boot failure; check with `ss -ltnp` |
| A3 | Centrifugo v6 accepts `static_headers` under `channel.proxy.subscribe.http` | Centrifugo config | The subscribe proxy gets no secret and every subscribe is denied; `centrifugo checkconfig` plus a smoke test catches it |
| A4 | The Nuxt app's `useJournalApi` overrides the layer's composable of the same name | i18n | PL journal content needs a layer release (v0.6.0) instead |
| A5 | The default `checkOutdatedBuildInterval` is 1 hour | Reload on deploy | None, since the value is set explicitly |
| A6 | Client app names must not appear in the public roadmap deck | Pitfall 7 | User may prefer showing real ports as social proof |
| A7 | A G15Office task is an acceptable stand-in for "by mail" on contact submissions | Feedback | D-04 unmet unless mail is added |
| A8 | The Node binary path and the Centrifugo binary path on rome match the summercms.io/bm files | rome deployment | Supervisor start failure; *check on rome* |
| A9 | `compass.Config` has a `Bool` accessor | Code example | Use `String` == "true" |
## Open Questions
1. **"By mail" for contact submissions (D-04).**
- Known: feedback stores the submission (admin list) and files a G15Office task; it never mails. The demo mail driver is `log`.
- Unclear: whether a g15office task satisfies "by mail", and SMTP credentials for rome.
- Recommendation: configure the G15Office sync for the demo, plus a small `golem15.demo` listener or job that mails the operator. Mail on submission in the shared plugin would need a v0.1.1 tag and SMTP config. Ask the user in plan 2.
2. **Repository visibility and creation.**
- `tea repos create --owner golem15 --name <repo>` is the precedent (19.5-02-PLAN Task 5; `tea` 0.16.0 is logged in as `jakub`).
- Unclear: are sm-demo-app and vue-demo-app private (19.5 default) or public (showcase)?
- If public, README and DEPLOY.md must hold no credentials (DEPLOY.md documents the key names only, D-06).
3. **Admin-tab half of todo ltcvqext.** The demo admin is private. The boardwalk `X-Summer-Build`/manifest work is framework code with a framework tag and changelog post. Recommendation: do the site half here and leave the todo open with a progress note for the admin half, unless the user wants a framework release in this phase.
4. **Concurrency with Phase 19.5,** which also edits `sm-user-plugin` and plans to mount sociallogin in sm-demo-app (19.5 D-21). Recommendation: finish 19.4.1's user change and tag first, and let 19.5 read the next free tag at execution.
## Environment Availability
| Dependency | Required By | Available | Version | Fallback |
|------------|------------|-----------|---------|----------|
| Go | build | ✓ | go1.27.2 | — |
| `summer` CLI | `summer build` | ✓ (`~/go/bin/summer`, not on PATH) | — | dev.sh already falls back to `$HOME/go/bin/summer` |
| Docker | Postgres tests, smoke | ✓ | 29.9.1 (socket accessible) | — |
| pnpm / Node | Nuxt build | ✓ | 12.14.0 / v22.23.3 | — |
| nginx | `check-deploy.sh` (`nginx -t`) | ✓ | 1.30.6 | — |
| centrifugo | `checkconfig` in check-deploy | ✓ | v6.5.1 (same as rome) | — |
| supervisord | config parse | ✓ | 4.3.0 | python configparser (summercms.io check) |
| openssl, python3 | check-deploy | ✓ | 3.6.5 / 3.14.8 | — |
| stripe CLI | local test mode | ✓ | 1.40.9 | — |
| tea | repo creation | ✓ | 0.16.0, login `git.golem15.com` | web UI |
| psql | DB checks | ✓ | 18.6 | docker exec |
| rome access | deploy | not probed (do not ssh in research) | — | human-action checkpoint |
**Missing dependencies with no fallback:** none locally. The DNS record (user), the Cloudflare Turnstile site and the Stripe webhook endpoint are external human actions.
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework (Go) | `go test` (stdlib), testcontainers Postgres for DB tests (skip under `-short`) |
| Framework (Nuxt) | vitest 3.2.7, node environment, `app/lib/**/*.test.ts` (Wave 0: add) |
| Config files | none yet in vue-demo-app (add `vitest.config.ts` with `include: ['app/**/*.test.ts']`, alias `~`) |
| Quick run | `go test -short ./...` (sm-demo-app) · `pnpm --dir vue-demo-app test` |
| Full suite | `go test ./...` (Docker) · `pnpm --dir vue-demo-app test && pnpm --dir vue-demo-app build` · `scripts/check-deploy.sh` · `scripts/build.sh release` |
| Plugin repos | `go test -short ./...` in `../fonoteka.go/plugins/golem15/user` and `../sm-journal-plugin` |
### Success Criteria → Test Map
| Req | Behavior | Type | Automated Command | Exists? |
|-----|----------|------|-------------------|---------|
| SC1 | go.mod requires only tags; no local replace | unit (Go) | `go test -short -run TestGoModRequiresTags .` (parse go.mod with `golang.org/x/mod/modfile` or line scan; no `=>` with `./`/`../` except allowed) | ❌ Wave 0 |
| SC1 | Release builds without go.work | script | `GOWORK=off GOPRIVATE='git.golem15.com/*' go build -o /dev/null .` inside `scripts/build.sh release` | ❌ |
| SC1 | Submodules equal their tags | script | `scripts/check-pins.sh` | ❌ |
| SC1 | Remote exists and is pushed | script | `git ls-remote origin master` equals `git rev-parse HEAD` | manual gate |
| SC2 | nginx config valid, `/backend` refused, ports consistent | script | `scripts/check-deploy.sh` (nginx -t, supervisor parse, `centrifugo checkconfig`, port match) | ❌ |
| SC2 | Live: 200 `/`, 404 `/backend`, 200 `/_journal/api/v1/posts`, HSTS | smoke | `scripts/check-live.sh https://demo.summercms.io` (curl) | ❌ |
| SC2 | Backend call to action present, no admin link | unit (vitest) or `.output` grep | `pnpm test` (component render string) or `grep -L '/backend' .output/public` | ❌ |
| SC3 | Register and login carry `turnstile:<mode>` only when configured | unit (Go, user plugin) | `go test -short -run TestTurnstileRoutes ./...` (route middleware list via `surf.BuildRouter`) | ❌ |
| SC3 | Missing token → 422/503 on register | unit | same file, httptest with a fake siteverify (`bouncer.Turnstile.Endpoint`) | ❌ |
| SC3 | Visitor credentials only from config; none in site output | unit | `go test -short -run TestSeedVisitorFromConfig ./plugins/demo/...` plus `.output` grep | ❌ |
| SC3 | Reset schedule absent by default, present when enabled | unit | `go test -short -run TestResetSchedule ./plugins/demo/...` | ❌ |
| SC3 | Reset wipes and reseeds, keeps plans and admins | integration (Postgres) | `go test -run TestResetKeepsPlans ./plugins/demo/...` (testcontainers) | ❌ |
| SC3 | Banner on every page | unit (vitest) | layout render test, or `.output` SSR grep via smoke | ❌ |
| SC4 | ROADMAP parse (done/next), denylist enforced | unit | `go test -short -run TestRoadmap ./plugins/demo/roadmap` | ❌ |
| SC4 | Roadmap deck upserts and keeps `public_hash` | integration | `go test -run TestSeedRoadmapUpsert ./plugins/demo/...` | ❌ |
| SC4 | EN/PL seed rows (FAQ per locale, journal translations, two quizzes, two decks) | integration | `go test -run TestSeedLocales ./plugins/demo/...` | ❌ |
| SC4 | Realtime routes and middleware (port of bm realtime_test.go) | unit | `go test -short -run TestRealtime .` | ❌ |
| SC4 | Ported deck/remote/clock/realtime/quizSession libs | unit (vitest) | `pnpm --dir vue-demo-app test` (bm's tests ported unchanged) | ❌ (sources exist in bm) |
| SC4 | Journal translation mapper | unit (vitest) | `pnpm test -- journalLocale` | ❌ |
| SC4 | Public quiz list endpoint anonymous; attempts need JWT | unit | `go test -short -run TestDemoQuizList .` | ❌ |
| SC4 | Author profile keys | unit (journal) | `go test -short -run TestJournalAuthorProfile ./...` in sm-journal-plugin | ✅ (extend) |
| SC4 | All 16 plugins boot plus demo routes mounted | unit | `go test -short -run TestBootAllPlugins .` | ✅ (extend wantRoutes) |
| Todo ltcvqext | Update banner on `app:manifest:update` | unit (vitest) | plugin logic in a pure function plus test | ❌ |
| SC5 | All of the above | — | full suite | — |
### Sampling Rate
- **Per task commit:** `go vet ./... && go test -short ./...` in each touched Go repo; `pnpm --dir vue-demo-app test` once Wave 0 exists.
- **Per plan merge:** plus `go test ./...` (Docker), `pnpm --dir vue-demo-app build`, `scripts/check-deploy.sh`.
- **Phase gate:** `scripts/build.sh release` (GOWORK=off), `scripts/check-pins.sh`, `scripts/check-live.sh https://demo.summercms.io` green before `/msd-verify-work`.
### Wave 0 Gaps
- [ ] `vue-demo-app/vitest.config.ts` and a `"test": "vitest run"` script; `vitest@3.2.7` dev dependency
- [ ] `sm-demo-app/realtime_test.go` (port of `sm-bm-app/realtime_test.go`)
- [ ] `sm-demo-app/plugins/demo/**/_test.go` with a Postgres harness (copy the `TestMain` pattern: `flag.Parse()` then `testing.Short()`, per CLAUDE.md)
- [ ] `scripts/check-deploy.sh`, `scripts/check-pins.sh`, `scripts/check-live.sh`
- [ ] Turnstile route test in sm-user-plugin
## Security Domain
### Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | yes | golem15.user JWT (existing), bcrypt; Turnstile on login and register; `throttle:user-api` (120 a minute) plus login attempt limit (`attempt_limit: 5`, `suspension_minutes: 15`, user `config/config.yaml`) |
| V3 Session Management | yes | JWT in a cookie (`summercms_demo_token`, SameSite=Lax); 1-day TTL (`config/golem15.user.yaml`); the refresh route |
| V4 Access Control | yes | `/backend` refused at nginx; `presentations.realtime-owner`; deck ownership via `mineCaller`; the subscribe proxy owner check plus HMAC channel tag |
| V5 Input Validation | yes | `classes.Clean` (bluemonday) on deck HTML; feedback validation rules; user plugin validators |
| V6 Cryptography | yes | Centrifugo HS256 secret per instance; `app.key`; no hand-rolled crypto |
| V13 API | yes | Rate limits per bucket; Origin allow-list on feedback; CORS restricted in production |
### Known Threat Patterns
| Pattern | STRIDE | Standard Mitigation |
|---------|--------|---------------------|
| Bot sign-ups and credential stuffing | Spoofing / DoS | `turnstile:closed` plus per-IP throttles; reset job available |
| Stored XSS through user decks | Tampering | bluemonday `Clean` on write; no freeform `css` on the demo endpoint; unpublished by default |
| Phishing content on a trusted subdomain | Spoofing | Decks unpublished by default; reset wipes; the banner states the site is a demo |
| Cross-app realtime token reuse | Elevation | Own Centrifugo with its own `hmac_secret_key` (D-17) |
| Admin exposure | Elevation | nginx `location ^~ /backend { return 404; }`; admin over an SSH tunnel only |
| Secret leakage in repos | Information disclosure | `.env`, `site.env` and `centrifugo.json` only on rome; `*.example` templates in git; visitor credentials from env |
| Webhook forgery | Tampering | Stripe signature (pgstripe webhook secret) |
| Public purge route abuse | DoS | Loopback only; layer refuses proxied requests; token ≥32 chars |
## Suggested plan shape (MVP, tracer-first; project rule: few large plans, tests last)
1. **Plan 01: Release and live tracer.** Plugin go.mod fix and tags (paymentgateway, pgsub, pgstripe, sitemap, menu, faq, oauthserver, translate, presentations); module-mode go.mod with `plugins/demo` folded in; submodule bumps; `scripts/build.sh` (release = GOWORK=off) and `check-pins.sh`; `deploy/` (nginx, three supervisor programs, env examples, Centrifugo example); `DEPLOY.md`; `check-deploy.sh`; minimal safety (remove pre-filled credentials and admin links, banner, backend call-to-action stub); repo creation and push; first deploy to rome. **Checkpoints:** go-ahead for tags and pushes, DNS, certbot, first release.
2. **Plan 02: Accounts, tour, EN/PL and journal.** sm-user-plugin Turnstile (tag v0.1.3); journal author fields (tag v0.2.1); register and login pages with the widget; visitor from config; summercms.io look (copy `tokens.css`, `base.css`, `sun-logo.webp`, header and footer markup from `vue-summercmsio-app` at `644f04e`); guided-tour homepage; EN/PL (locale files, FAQ/menu locale props, journal composable override, seed translations); contact form through feedback; reset command plus disabled schedule; reload banner.
3. **Plan 03: Presentations, quizzes and realtime.** bm port (pages, components, libs); demo realtime wiring and endpoints (deck create, public quiz list); own Centrifugo on rome; roadmap deck generator plus embedded snapshot plus OIDC screenshots (human checkpoint); EN/PL quizzes and decks; redeploy.
4. **Plan 04: Unit tests and live verification.** Everything in the Validation map marked ❌; port bm's lib tests; `check-live.sh` against production; README and DEPLOY.md final pass; g15office update; close the todos (or leave a progress note on ltcvqext's admin half).
## Sources
### Primary (HIGH confidence)
- Local repositories, read this session: `sm-demo-app` (go.mod, go.work, summer.yaml, seed.go, config/, vue-demo-app), `sm-bm-app` (frontend/src, plugins/jz/bm/{realtime,routes,plugin}.go, realtime_test.go, config/realtime.yaml, DEPLOY.md), `sm-summercmsio-app` (DEPLOY.md, scripts/build.sh, scripts/check-deploy.sh, deploy/), shared plugin sources (presentations, quizzes, feedback, user, journal, translate), framework READMEs (bouncer, surf, lighthouse, conga, compass, lagoon) and `pact/capabilities.go`.
- `git ls-remote` against git.golem15.com for all 15 module repos and the two missing app repos.
- Module-mode build probe (`GOWORK=off go mod tidy`, `go build`, `go test -short`) in the session scratchpad.
- npm registry (`npm view`) plus `msd-tools query package-legitimacy check`.
### Secondary (MEDIUM confidence)
- centrifugal.dev/docs/server/configuration and /docs/server/proxy (v6 keys)
- nuxt.com/docs/4.x/guide/going-further/experimental-features, /api/advanced/hooks (`app:manifest:update`)
- developers.cloudflare.com/turnstile (client-side rendering, testing keys)
### Tertiary (LOW confidence)
- Default `checkOutdatedBuildInterval` value; `static_headers` placement for the subscribe proxy (see Assumptions)
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH. Versions come from lockfiles, the registry and the build probe.
- Release mechanics: HIGH. Module-mode build and the placeholder failure were reproduced.
- Architecture (realtime, endpoints, reset): HIGH for the framework APIs (read), MEDIUM for the Centrifugo JSON shape.
- Nuxt port and i18n: MEDIUM. The source was inventoried; override precedence and SSR boundaries are to be confirmed early.
- Pitfalls: HIGH. Each one comes from an observed file or command.
**Research date:** 2026-10-20
**Valid until:** 2026-10-27. Tags move quickly while 19.5 runs in parallel, so re-run the ls-remote matrix before tagging.

View File

@@ -0,0 +1,99 @@
---
phase: "19.4.1"
slug: "early-public-demo-on-demo-summercms-io"
# status lifecycle: draft (seeded by plan-phase) → validated (set by validate-phase §6)
# audit-milestone §5.5 distinguishes NOT-VALIDATED (draft) from PARTIAL (validated + nyquist_compliant: false) (#2117)
status: draft
nyquist_compliant: false
wave_0_complete: false
created: "2026-10-20"
---
# Phase 19.4.1 — Validation Strategy
> Per-phase validation contract for feedback sampling during execution. Source: `19.4.1-RESEARCH.md` § Validation Architecture.
---
## Test Infrastructure
| Property | Value |
|----------|-------|
| **Framework** | `go test` (stdlib, testcontainers Postgres skipped under `-short`); vitest 3.2.7 in `vue-demo-app` (Wave 0) |
| **Config file** | none yet in `vue-demo-app` — Wave 0 adds `vitest.config.ts` |
| **Quick run command** | `go vet ./... && go test -short ./...` (sm-demo-app and each touched plugin repo) · `pnpm --dir vue-demo-app test` |
| **Full suite command** | `go test ./...` (Docker) · `pnpm --dir vue-demo-app test && pnpm --dir vue-demo-app build` · `scripts/check-deploy.sh` · `scripts/build.sh release` |
| **Estimated runtime** | ~120 seconds quick, ~6 minutes full |
---
## Sampling Rate
- **After every task commit:** Run the quick command in each touched repo
- **After every plan wave:** Run the full suite command
- **Before `/msd-verify-work`:** `scripts/build.sh release` (GOWORK=off), `scripts/check-pins.sh`, `scripts/check-live.sh https://demo.summercms.io` green
- **Max feedback latency:** 120 seconds
---
## Per-Task Verification Map
Task IDs are assigned by the planner; this map lists the behaviors each plan must cover.
| Criterion | Behavior | Test Type | Automated Command | File Exists | Status |
|-----------|----------|-----------|-------------------|-------------|--------|
| SC1 | go.mod requires only tags, no local replace | unit | `go test -short -run TestGoModRequiresTags .` | ❌ W0 | ⬜ pending |
| SC1 | Release builds without go.work | script | `scripts/build.sh release` | ❌ W0 | ⬜ pending |
| SC1 | Submodules equal their tags | script | `scripts/check-pins.sh` | ❌ W0 | ⬜ pending |
| SC2 | nginx/supervisor/Centrifugo config valid, `/backend` refused | script | `scripts/check-deploy.sh` | ❌ W0 | ⬜ pending |
| SC2 | Live 200 `/`, 404 `/backend`, journal API 200, HSTS | smoke | `scripts/check-live.sh https://demo.summercms.io` | ❌ W0 | ⬜ pending |
| SC3 | Turnstile on register/login only when configured | unit | `go test -short -run TestTurnstileRoutes ./...` (sm-user-plugin) | ❌ W0 | ⬜ pending |
| SC3 | Visitor credentials only from config | unit | `go test -short -run TestSeedVisitorFromConfig ./plugins/demo/...` | ❌ W0 | ⬜ pending |
| SC3 | Reset schedule off by default | unit | `go test -short -run TestResetSchedule ./plugins/demo/...` | ❌ W0 | ⬜ pending |
| SC3 | Reset wipes and reseeds, keeps plans and admins | integration | `go test -run TestResetKeepsPlans ./plugins/demo/...` | ❌ W0 | ⬜ pending |
| SC3 | Demo banner on every page | unit | `pnpm --dir vue-demo-app test` | ❌ W0 | ⬜ pending |
| SC4 | ROADMAP parse and client-name denylist | unit | `go test -short -run TestRoadmap ./plugins/demo/...` | ❌ W0 | ⬜ pending |
| SC4 | Roadmap deck upsert keeps `public_hash`; pxpx slide present (D-19) | integration | `go test -run TestSeedRoadmapUpsert ./plugins/demo/...` | ❌ W0 | ⬜ pending |
| SC4 | EN/PL seed rows | integration | `go test -run TestSeedLocales ./plugins/demo/...` | ❌ W0 | ⬜ pending |
| SC4 | Realtime routes (port of bm `realtime_test.go`) | unit | `go test -short -run TestRealtime .` | ❌ W0 | ⬜ pending |
| SC4 | Ported deck/remote/realtime libs | unit | `pnpm --dir vue-demo-app test` | ❌ W0 | ⬜ pending |
| SC4 | Anonymous quiz list; attempts need JWT | unit | `go test -short -run TestDemoQuizList .` | ❌ W0 | ⬜ pending |
| SC4 | Journal author profile keys | unit | `go test -short -run TestJournalAuthorProfile ./...` (sm-journal-plugin) | ✅ extend | ⬜ pending |
| SC4 | All 16 plugins boot with demo routes | unit | `go test -short -run TestBootAllPlugins .` | ✅ extend | ⬜ pending |
| Todo ltcvqext | Update banner on new deploy | unit | `pnpm --dir vue-demo-app test` | ❌ W0 | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
---
## Wave 0 Requirements
- [ ] `vue-demo-app/vitest.config.ts`, `"test": "vitest run"` script, `vitest@3.2.7` dev dependency
- [ ] `sm-demo-app/realtime_test.go` (port of `sm-bm-app/realtime_test.go`)
- [ ] `sm-demo-app/plugins/demo/**/*_test.go` with a Postgres `TestMain` (`flag.Parse()` then `testing.Short()`)
- [ ] `scripts/check-deploy.sh`, `scripts/check-pins.sh`, `scripts/check-live.sh`, `scripts/build.sh`
- [ ] Turnstile route test in sm-user-plugin
---
## Manual-Only Verifications
| Behavior | Criterion | Why Manual | Test Instructions |
|----------|-----------|------------|-------------------|
| DNS, certbot certificate | SC2 | External (user creates DNS) | `curl -I https://demo.summercms.io` after certbot |
| Stripe test checkout with the seeded visitor | SC3 | Needs a browser and Stripe | Log in as the visitor, subscribe with `4242 4242 4242 4242` |
| Phone remote drives the speaker view | SC4 | Two devices, live Centrifugo | Open speaker view, scan the remote QR, change slides |
| OIDC screenshots captured | SC4 | Visual capture | Check the roadmap deck slides render the images |
---
## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 120s
- [ ] `nyquist_compliant: true` set in frontmatter
**Approval:** pending