docs(20): create phase plan
This commit is contained in:
@@ -1533,10 +1533,26 @@ Plans:
|
||||
3. The demo is deployed publicly: the frontend is open, `/backend` is not reachable from the public internet, and the site carries a "want to see the backend? contact Golem15" call to action.
|
||||
4. The new code has unit tests, delivered in the phase's last plan.
|
||||
|
||||
**Plans:** 0 plans
|
||||
**Plans:** 6 plans
|
||||
|
||||
Plans:
|
||||
- [ ] TBD (run /msd-plan-phase 20 to break down)
|
||||
**Wave 1**
|
||||
- [ ] 20-01-PLAN.md — shared plugin release: siblings fast-forwarded, chat and forum pre-save seams (D-18), new sm-moderation-plugin (D-17), go.mod fixes, eight tags after go-ahead
|
||||
|
||||
**Wave 2** *(blocked on Wave 1 completion)*
|
||||
- [ ] 20-02-PLAN.md — demo backend: oauthserver bump (folded todo), eight plugins mounted by tag, demo context and channel adapter, bootstrap endpoints, realtime for members, slash commands, moderation wiring, EN/PL seed and reset, Centrifugo namespaces, local LiveKit
|
||||
|
||||
**Wave 3** *(blocked on Wave 2 completion)*
|
||||
- [ ] 20-03-PLAN.md — vue-chat-layer v0.1.0 (bm port, channels, threads, pins, command picker, link embeds) and /chat, floating panel, Chat tour card in vue-demo-app
|
||||
|
||||
**Wave 4** *(blocked on Wave 3 completion)*
|
||||
- [ ] 20-04-PLAN.md — vue-forum-layer, vue-userfriends-layer, vue-chatvideo-layer v0.1.0 (DM-only call button), their pages and tour cards, herald VAPID config without UI
|
||||
|
||||
**Wave 5** *(blocked on Wave 4 completion)*
|
||||
- [ ] 20-05-PLAN.md — rome deploy: the demo's own LiveKit, Vuurmuur rule (checkpoint), nginx, .env keys, Centrifugo, release, DEPLOY.md, check-deploy and check-live
|
||||
|
||||
**Wave 6** *(blocked on Wave 5 completion)*
|
||||
- [ ] 20-06-PLAN.md — unit tests (Go, layers, site), two-browser chat and DM call checks, phase gate and live verification
|
||||
|
||||
### Phase 30: Płytarium cutover
|
||||
|
||||
|
||||
@@ -0,0 +1,421 @@
|
||||
---
|
||||
phase: 20-demo-instance-with-every-shared-plugin
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- ../sm-chat-plugin/events/events.go
|
||||
- ../sm-chat-plugin/contracts/rejection.go
|
||||
- ../sm-chat-plugin/classes/service.go
|
||||
- ../sm-chat-plugin/classes/writes.go
|
||||
- ../sm-chat-plugin/api/errors.go
|
||||
- ../sm-chat-plugin/saving_smoke_test.go
|
||||
- ../sm-chat-plugin/README.md
|
||||
- ../sm-forum-plugin/events/events.go
|
||||
- ../sm-forum-plugin/contracts/rejection.go
|
||||
- ../sm-forum-plugin/classes/forum_service.go
|
||||
- ../sm-forum-plugin/classes/lifecycle.go
|
||||
- ../sm-forum-plugin/api/errors.go
|
||||
- ../sm-forum-plugin/saving_smoke_test.go
|
||||
- ../sm-forum-plugin/README.md
|
||||
- ../sm-chatvideo-plugin/go.mod
|
||||
- ../sm-chatvideo-plugin/go.sum
|
||||
- ../sm-chatvideo-plugin/README.md
|
||||
- ../sm-herald-plugin/go.mod
|
||||
- ../sm-herald-plugin/go.sum
|
||||
- ../sm-herald-plugin/README.md
|
||||
- ../sm-userfriends-plugin/go.mod
|
||||
- ../sm-userfriends-plugin/go.sum
|
||||
- ../sm-userfriends-plugin/README.md
|
||||
- ../sm-moderation-plugin/.gitignore
|
||||
- ../sm-moderation-plugin/go.mod
|
||||
- ../sm-moderation-plugin/go.sum
|
||||
- ../sm-moderation-plugin/go.work
|
||||
- ../sm-moderation-plugin/plugin.go
|
||||
- ../sm-moderation-plugin/admin.go
|
||||
- ../sm-moderation-plugin/admin_permissions.go
|
||||
- ../sm-moderation-plugin/logger.go
|
||||
- ../sm-moderation-plugin/classes/filter.go
|
||||
- ../sm-moderation-plugin/classes/filter_smoke_test.go
|
||||
- ../sm-moderation-plugin/models/settings.go
|
||||
- ../sm-moderation-plugin/models/registry.go
|
||||
- ../sm-moderation-plugin/models/settings/fields.yaml
|
||||
- ../sm-moderation-plugin/updates/202610280001_create_golem15_moderation_settings.go
|
||||
- ../sm-moderation-plugin/updates/registry.go
|
||||
- ../sm-moderation-plugin/lang/en/lang.yaml
|
||||
- ../sm-moderation-plugin/lang/pl/lang.yaml
|
||||
- ../sm-moderation-plugin/config/config.yaml
|
||||
- ../sm-moderation-plugin/README.md
|
||||
autonomous: false
|
||||
requirements: [SC-1, D-17, D-18, D-19, D-20]
|
||||
estimate:
|
||||
tokens: 220000
|
||||
raw_tokens: 220000
|
||||
tasks: 6
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "D-18 (chat): with a festival listener on events.MessageSaving that returns contracts.Rejection{Field: \"content\", Message: m}, POST /_chat/api/v1/conversations/{id}/messages, POST /_chat/api/v1/messages/{id}/replies and PUT /_chat/api/v1/messages/{id} answer 422 {\"error\":\"Validation failed\",\"errors\":{\"content\":[m]}}; no golem15_chat_messages row is inserted or changed, MessageCreated and MessageUpdated do not fire and nothing is published."
|
||||
- "D-18 (forum): ForumService.CreateThread, Reply and EditPost fire events.PostSaving before breakwater.Render; a contracts.Rejection from a listener maps to the existing 422 envelope under errors.title or errors.body and nothing is persisted."
|
||||
- "D-18 (parity): with no MessageSaving or PostSaving listener every existing route, payload, status, table, migration ID and config key of sm-chat-plugin and sm-forum-plugin is unchanged; their php_method_map_test.go and golden_test.go pass without edits; the seams name no application and ship as patch-level additions."
|
||||
- "D-17: the new shared plugin golem15.moderation (module git.golem15.com/golem15/sm-moderation-plugin) publishes *classes.Filter at boot; Filter.Check returns a Violation for a banned word or phrase (case-insensitive, Unicode letter boundaries, Polish letters included) or a banned link (a listed domain, its subdomains, with or without scheme or www, and URL-prefix entries), and nil for clean text; the lists are the banned_words and banned_links textareas of its admin settings screen (singleton golem15_moderation_settings) behind the permission golem15.moderation.manage_settings; the plugin imports neither chat nor forum."
|
||||
- "SC-1: sm-chatvideo-plugin, sm-herald-plugin and sm-userfriends-plugin go.mod files keep only the framework replace and require sm-chat-plugin v0.1.3, sm-user-plugin v0.1.13 and summercms v0.5.1 where they depend on them; every one of the eight repos passes go vet and go test -short, and the five chat dependants build with GOWORK=off."
|
||||
- "SC-1 (release, after the Task 5 go-ahead): origin of each repo carries its new tag on the pushed HEAD that passed the checks: sm-chat-plugin v0.1.4 (patch, the seam), sm-chatchannels-plugin, sm-chatbots-plugin, sm-chatvideo-plugin, sm-forum-plugin, sm-herald-plugin, sm-userfriends-plugin and sm-moderation-plugin v0.1.0 (or the next free number read from the remote); a scratch module requiring all eight tags and summercms v0.5.1 builds with GOWORK=off."
|
||||
- "D-20 (D-19 superseded): this is plan 01 of the six plans confirmed at plan time; the chat family release precedes the demo backend (plan 02)."
|
||||
artifacts:
|
||||
- path: "../sm-chat-plugin/events/events.go"
|
||||
provides: "MessageSaving pre-save event and MessageSavingEventName"
|
||||
contains: "MessageSaving"
|
||||
- path: "../sm-chat-plugin/contracts/rejection.go"
|
||||
provides: "Rejection{Field, Message} error a pre-save listener returns"
|
||||
contains: "type Rejection struct"
|
||||
- path: "../sm-forum-plugin/events/events.go"
|
||||
provides: "PostSaving pre-save event and PostSavingEventName"
|
||||
contains: "PostSaving"
|
||||
- path: "../sm-forum-plugin/contracts/rejection.go"
|
||||
provides: "Rejection{Field, Message} for forum listeners"
|
||||
contains: "type Rejection struct"
|
||||
- path: "../sm-moderation-plugin/classes/filter.go"
|
||||
provides: "Filter, NewFilter, Check, Match, ParseList, Violation"
|
||||
contains: "func (f *Filter) Check"
|
||||
- path: "../sm-moderation-plugin/admin.go"
|
||||
provides: "moderation_settings settings item (HasSettings, HasLang)"
|
||||
contains: "moderation_settings"
|
||||
key_links:
|
||||
- from: "../sm-chat-plugin/classes/service.go"
|
||||
to: "../sm-chat-plugin/events/events.go"
|
||||
via: "SendMessageWith fires MessageSaving on s.bus before lagoon.Transaction"
|
||||
pattern: "MessageSaving\\{"
|
||||
- from: "../sm-chat-plugin/api/errors.go"
|
||||
to: "../sm-chat-plugin/contracts/rejection.go"
|
||||
via: "writeChatError maps errors.As(err, &contracts.Rejection) to writeValidation"
|
||||
pattern: "errors\\.As\\(err, &rej\\)"
|
||||
- from: "../sm-forum-plugin/classes/forum_service.go"
|
||||
to: "../sm-forum-plugin/events/events.go"
|
||||
via: "CreateThread and Reply fire PostSaving on s.bus before breakwater.Render"
|
||||
pattern: "PostSaving\\{"
|
||||
- from: "../sm-moderation-plugin/plugin.go"
|
||||
to: "../sm-moderation-plugin/classes/filter.go"
|
||||
via: "Boot publishes classes.NewFilter(db) through app.Publish inside lagoon.OnDatabase"
|
||||
pattern: "app\\.Publish"
|
||||
prohibitions:
|
||||
- statement: "No existing route, request or response field, status code, table, migration ID or config key of sm-chat-plugin or sm-forum-plugin changes; the seams are additive (WavePath parity, Phase 31)."
|
||||
status: unresolved
|
||||
verification: automated
|
||||
- statement: "sm-moderation-plugin depends on neither sm-chat-plugin nor sm-forum-plugin."
|
||||
status: unresolved
|
||||
verification: automated
|
||||
- statement: "No README, comment or identifier in the eight repos names a consuming application."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
- statement: "Nothing is pushed, no remote repository is created and no tag is set before the Task 5 go-ahead."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
ROADMAP Goal (not in user-story format; no story is invented here, see the planning return): `sm-demo-app` runs every shared Golem15 plugin collected so far in one binary, and its public Nuxt site shows them working: chat and channels with realtime, video calls, presentations, quizzes, journal, FAQ, subscriptions and the rest. Phase 19.4.1 deployed the demo early; this phase adds the chat family to that live instance.
|
||||
|
||||
This plan's slice (plan 1 of 6, wave 1, per D-20): the shared plugin release the demo needs before it can require the chat family by tag. Fast-forward the stale sibling checkouts, add the generic pre-save seams to chat and forum (D-18), create `sm-moderation-plugin` (D-17), fix the go.mod files that break module mode, and, after the user's go-ahead, push and tag all eight repositories.
|
||||
|
||||
## Decisions taken in this plan (Claude's discretion, recorded)
|
||||
|
||||
- **Seam shape (D-18 discretion):** a festival "-ing" event fired before the write transaction, mirroring `user.Deleting` / `user.Deactivating` in sm-user-plugin. chat gets `events.MessageSaving`, forum a new `events` package with `events.PostSaving`. A listener refuses by returning the plugin's own `contracts.Rejection{Field, Message}`. Each plugin declares its own type, so forum does not import chat and neither imports moderation. The API maps `errors.As(err, &rej)` to the existing 422 envelope. Rationale: `festival.Bus.Fire` already returns the joined listener errors, chat already holds `s.bus`, forum holds an unused `Bus` field, and no service gains a new registration method. RESEARCH Pattern 2.
|
||||
- **Event names:** `MessageSavingEventName = "golem15.chat.message.saving"` and `PostSavingEventName = "golem15.forum.post.saving"`. They are Go-only seams with no PHP counterpart; the doc comments say so.
|
||||
- **Op values:** chat `OpCreate = "create"` and `OpUpdate = "update"`; forum `OpThread = "thread"`, `OpReply = "reply"` and `OpUpdate = "update"`.
|
||||
- **Moderation lists:** two textareas, one entry per line; an `enabled` switch defaults to true; at most 500 entries of at most 200 characters per list (validated in `BeforeValidate`). The filter reads the singleton row on every `Check` and recompiles its matchers only when the stored text changed. RE2's `\b` is ASCII-only, so word boundaries are built from `[^\p{L}\p{N}_]` to cover Polish letters.
|
||||
- **Violation message:** `Violation{Kind, Match, Message}`. Message comes from the plugin's lang files (`golem15.moderation::lang.violation.word` and `.link`, EN and PL) in the locale `towel.Locale(ctx)` carries, resolved through the framework's phrasebook. Without a locale it is English. The chat-family APIs answer in English today, so English is an acceptable fallback.
|
||||
- **Dependants require existing tags:** chatvideo, herald and userfriends require `sm-chat-plugin v0.1.3` (the newest tag before this plan), `sm-user-plugin v0.1.13` and `summercms v0.5.1`, the set RESEARCH compiled. That keeps their module-mode check independent of the new chat tag; minimal version selection gives the demo chat v0.1.4.
|
||||
- **Repository creation:** with the go-ahead, Claude creates `golem15/sm-moderation-plugin` on git.golem15.com with `tea` when `tea login list` shows a git.golem15.com login, using the same visibility as `sm-sitemap-plugin`. Otherwise the user creates it.
|
||||
- spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip).
|
||||
|
||||
## Artifacts this phase produces (this plan)
|
||||
|
||||
- sm-chat-plugin (v0.1.4): `events.MessageSaving`, `events.MessageSavingEventName`, `events.OpCreate`, `events.OpUpdate`, `contracts.Rejection` (`Error()`), the 422 mapping in `writeChatError`, smoke test `TestMessageSavingTracer` (`saving_smoke_test.go`), a README changelog entry v0.1.4.
|
||||
- sm-forum-plugin (v0.1.0): package `events` (`PostSaving`, `PostSavingEventName`, `OpThread`, `OpReply`, `OpUpdate`), `contracts.Rejection`, the 422 mapping in `writeForumError`, smoke test `TestPostSavingTracer`.
|
||||
- sm-moderation-plugin (new repo, v0.1.0): plugin `golem15.moderation`, package `moderation`, `classes.Filter`, `classes.NewFilter(db *gorm.DB) *Filter`, `(*Filter).Check(ctx context.Context, text string) (*Violation, error)`, `classes.Match(words, links []string, text string) *Violation`, `classes.ParseList(text string) []string`, `classes.Violation{Kind, Match, Message}`, `classes.KindWord`, `classes.KindLink`, table `golem15_moderation_settings` (migration `202610280001_create_golem15_moderation_settings`), settings item `moderation_settings`, permission `golem15.moderation.manage_settings`, config keys `golem15.moderation.max_entries` and `golem15.moderation.max_entry_length`, smoke test `TestFilterSmoke`.
|
||||
- go.mod fixes in sm-chatvideo-plugin, sm-herald-plugin and sm-userfriends-plugin.
|
||||
- Tags: chat v0.1.4; chatchannels, chatbots, chatvideo, forum, herald, userfriends and moderation v0.1.0 (numbers read from the remote at release time).
|
||||
- Full unit tests come in plan 06 (project rule).
|
||||
|
||||
<objective>
|
||||
Release the chat family so the demo can require it by tag: the D-18 pre-save seams in chat and forum, the D-17 moderation plugin, go.mod hygiene, and eight tags after the user's go-ahead.
|
||||
|
||||
Purpose: SC-1 needs `sm-demo-app` to build from tagged modules (19.4.1 D-03, `TestGoModRequiresTags`, `scripts/check-pins.sh`). D-17 and D-18 need the seams and the filter before plan 02 wires them. D-20 makes this plan 01 of six and supersedes D-19's earlier split.
|
||||
Output: commits and, after the go-ahead, pushes and tags in eight plugin repositories. No change in sm-demo-app, vue-demo-app or summercms.go code.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-CONTEXT.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-RESEARCH.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-PATTERNS.md
|
||||
@.planning/notes/core-plugins-own-repos.md
|
||||
|
||||
Repos (siblings of summercms.go): ../sm-chat-plugin, ../sm-chatchannels-plugin, ../sm-chatbots-plugin (all three behind origin: fast-forward first), ../sm-chatvideo-plugin, ../sm-forum-plugin, ../sm-herald-plugin, ../sm-userfriends-plugin, and the new ../sm-moderation-plugin. Read-only references: ../sm-bm-app/plugins/golem15/{chat,chatchannels,chatbots} (the origin-main trees, for reading only), ../sm-sitemap-plugin (plugin and settings analog), ../sm-demo-app/plugins/golem15/user/classes/events.go (the "-ing" event analog).
|
||||
|
||||
Rules: no co-author tags in commits (user instruction overrides any attribution default); stage files by explicit path, never `git add -A`; one logical change per commit; nothing pushed, created or tagged before Task 5. Verify commands run from the framework root and address siblings with `go -C ../<repo>`. Plugin repos with a tracked go.work resolve their siblings through it (the seam-carrying ../sm-chat-plugin included). If the Docker socket is denied, run the Docker tests through `sudo -u golem` (CLAUDE.md).
|
||||
|
||||
<interfaces>
|
||||
chat (origin main 51c9e17 = v0.1.3, same tree as ../sm-bm-app/plugins/golem15/chat):
|
||||
- `classes.Service` fields `bus *festival.Bus` (from `Options.Bus`); `SendMessageWith(ctx, in SendInput) (*models.Message, error)` validates metadata, loads the conversation, checks `canSend`, trims content, returns `ErrEmptyMessage` when content and attachments are empty, then opens `lagoon.Transaction` and fires `events.MessageCreated` inside it (classes/service.go:287-345). `SendMessage` and `ReplyTo` (classes/attachments.go:218) funnel into it.
|
||||
- `EditMessage(ctx, actorID, messageID uint, content string) (*models.Message, error)` in classes/writes.go:248, after `CanEditMessage` (writes.go:202).
|
||||
- events/events.go: `MessageEvent{MessageID, ConversationID, ActorID uint; RootID *uint; Tx *gorm.DB}`, `MessageCreated`, `MessageUpdated`, `MessageDeleted`, consts `MessageCreatedEventName` etc.; the package doc says every event fires inside the transaction.
|
||||
- api/errors.go: `writeValidation(w, map[string][]string)`, `writeChatError(w, what, err)` (switch over errors.Is, default 500).
|
||||
- Test helpers: harness_test.go `bootChat`, `(*chatEnv).user`, `.count`, `.memory`, `.conversation(t, createdBy, typ, alias, contextID, participants...)`; http_tracer_test.go `bootWeb`, `(*webEnv).frontendToken`, `.call(t, method, path, token, body)`, `decodeObject`.
|
||||
forum (../sm-forum-plugin, origin main 2779341):
|
||||
- `classes.ForumService{bus *festival.Bus}` built in boot.go `bootService` with `Bus: app.Events`; `CreateThread(ctx, actorID, contextType, contextID, categoryID, title, body)` renders at forum_service.go:281, `Reply(...)` at :330, `EditPost(ctx, actorID, contextType, contextID, postID, body)` in classes/lifecycle.go:217. No `Fire(` call exists in the plugin today.
|
||||
- api/errors.go: `writeValidation`, `writeForumError` (ErrForbidden/ErrNotFound 404, ErrTitleRequired/ErrBodyRequired 422).
|
||||
- Test helpers: harness_test.go `bootForum`, `.user`, `.count`; http_tracer_test.go `bootWeb`, `.frontendToken`, `.call`.
|
||||
festival (framework): `(*Bus).Listen[T any](owner string, fn func(context.Context, T) error)`, `(*Bus).Fire[T any](ctx, event T) error` returns errors.Join of listener errors (callers use errors.As).
|
||||
sitemap analog: plugin.go (embed config, `pact.HasConfig/HasMigrations/HasModels`), admin.go (`Settings() []pact.SettingsItem` with Code, Label, Description, Category, Icon, Permissions, Form, NewModel; `AdminFS`, `LangFS`), admin_permissions.go (`Permissions() []pact.Permission`), models/settings.go (singleton row id 1, `*string` columns, `BeforeValidate` with a lang-key error), models/settings/fields.yaml, updates/202610110002_create_golem15_sitemap_settings.go (CREATE TABLE plus INSERT id 1), go.mod replace `git.golem15.com/golem15/summercms => ../summercms.go`, tracked go.work.
|
||||
go.mod today: chatvideo requires chat v0.0.0 and replaces chat and user with sibling paths; herald replaces user with ../fonoteka.go/plugins/golem15/user; userfriends requires chat pseudo-version v0.0.0-20261017200243-4d0e764ca443.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: Tracer: a rejecting MessageSaving listener turns a chat send into the existing 422 envelope and nothing is stored</name>
|
||||
<reversibility rating="costly">D-18: the seam becomes a public plugin contract other consumers can start using; changing its shape later touches every listener.</reversibility>
|
||||
<precondition>`git -C ../sm-<name>-plugin status --porcelain` prints nothing for chat, chatchannels, chatbots, chatvideo, forum, herald and userfriends; `docker info` exits 0.</precondition>
|
||||
<files>../sm-chat-plugin/events/events.go, ../sm-chat-plugin/contracts/rejection.go, ../sm-chat-plugin/classes/service.go, ../sm-chat-plugin/classes/writes.go, ../sm-chat-plugin/api/errors.go, ../sm-chat-plugin/saving_smoke_test.go, ../sm-chat-plugin/README.md</files>
|
||||
<read_first>../sm-chat-plugin/classes/service.go (SendMessageWith, Options.Bus), ../sm-chat-plugin/classes/writes.go (EditMessage, CanEditMessage), ../sm-chat-plugin/classes/attachments.go (ReplyTo), ../sm-chat-plugin/events/events.go, ../sm-chat-plugin/api/errors.go, ../sm-chat-plugin/api/messages.go and api/replies.go, ../sm-chat-plugin/http_tracer_test.go and harness_test.go, ../sm-chat-plugin/php_method_map_test.go, ../sm-chat-plugin/README.md, ../sm-demo-app/plugins/golem15/user/classes/events.go (DeactivatingEvent doc style), modules/festival/bus.go, 20-RESEARCH.md (Pattern 2, Pitfall 1), 20-PATTERNS.md (Chat / forum pre-save seam)</read_first>
|
||||
<action>Per D-18 and D-20; wire one path end to end: listener, service, API, HTTP response, database unchanged.
|
||||
|
||||
1. Fast-forward every sibling first (RESEARCH Pitfall 1): for chat, chatchannels, chatbots, chatvideo, forum, herald and userfriends run `git -C ../<repo> pull --ff-only` and confirm `git -C ../<repo> rev-parse HEAD` equals the hash `git -C ../<repo> ls-remote origin refs/heads/main` prints (use the default branch each repo tracks). Record both hashes per repo for the SUMMARY. A non-fast-forward stops the task.
|
||||
2. events/events.go: keep the three existing events untouched. Add `MessageSavingEventName = "golem15.chat.message.saving"` on its own line and a `MessageSaving` struct with `ConversationID`, `ActorID` and `MessageID uint` (MessageID is 0 on create), `RootID *uint`, `Content string` and `Op string`, plus the constants `OpCreate = "create"` and `OpUpdate = "update"`. MessageSaving carries no Tx because it fires before the transaction. Rewrite the package doc: the three message events fire inside the transaction after the save; MessageSaving fires before it, has no PHP counterpart, and a listener error aborts the write; a listener refuses with contracts.Rejection.
|
||||
3. contracts/rejection.go: `type Rejection struct { Field, Message string }` with a value-receiver `Error() string` returning "golem15.chat: rejected " + Field + ": " + Message. The doc comment explains that listeners return the value (not a pointer) and callers match it with errors.As, because Fire joins listener errors.
|
||||
4. classes/service.go `SendMessageWith`: after the ErrEmptyMessage check and before `lagoon.Transaction`, when `s.bus != nil`, fire `&events.MessageSaving{Op: events.OpCreate, ConversationID, ActorID: senderID, RootID: rootID, Content: content}` (the trimmed content) and return `nil, err` on error. classes/writes.go `EditMessage`: after CanEditMessage allows the edit and the new content is validated, before its transaction, fire the same event with `Op: events.OpUpdate` and the MessageID. No other line of either method changes.
|
||||
5. api/errors.go `writeChatError`: before the existing switch, declare `var rej contracts.Rejection` and, when `errors.As(err, &rej)`, call `writeValidation(w, map[string][]string{field: {rej.Message}})` with field = rej.Field, or "content" when Field is empty, then return. The other branches stay byte-identical.
|
||||
6. saving_smoke_test.go `TestMessageSavingTracer` (Docker; skips under -short like the other HTTP tests): boot with bootWeb; register on the app's event bus a `*events.MessageSaving` listener returning `contracts.Rejection{Field: "content", Message: "not allowed"}` when Content contains the word "blocked"; create a group conversation with two participants through the harness; then
|
||||
- POST `/_chat/api/v1/conversations/{id}/messages` with content "this is blocked": 422, body errors.content equals ["not allowed"], and golem15_chat_messages has 0 rows;
|
||||
- POST the content "hello": 201, 1 row;
|
||||
- PUT `/_chat/api/v1/messages/{id}` with "blocked edit": 422, and the stored content is still "hello";
|
||||
- POST `/_chat/api/v1/messages/{id}/replies` with "blocked reply": 422, and the row count is still 1.
|
||||
7. README.md: in Features and API reference, add MessageSaving (when it fires, no Tx, Op values), MessageSavingEventName, OpCreate, OpUpdate and contracts.Rejection, with a neutral example host listener (`acme`). Add a changelog entry "v0.1.4: pre-save MessageSaving event and Rejection (additive; no listener means no change)". Name no consuming application.
|
||||
|
||||
Commit in ../sm-chat-plugin by path (`feat: pre-save MessageSaving event and Rejection`), README in the same commit. Do not push or tag.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-chat-plugin vet ./... && go -C ../sm-chat-plugin test -count=1 -short ./... && go -C ../sm-chat-plugin test -count=1 -run '^TestMessageSavingTracer$' -v . 2>&1 | grep -q -- '^--- PASS: TestMessageSavingTracer'</automated>
|
||||
<fails_when>Non-zero exit: vet or the short suite red (php_method_map or golden tests broken by the change), or no "--- PASS: TestMessageSavingTracer" line (Docker unavailable so it skipped, a 201 where 422 was expected, or a stored row after a refusal).</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `go -C ../sm-chat-plugin test -count=1 ./...` (with Docker) passes, including the unchanged php_method_map_test.go and golden_test.go.
|
||||
- `git -C ../sm-chat-plugin diff HEAD~1 --stat -- php_method_map_test.go golden_test.go testdata` prints nothing.
|
||||
- `grep -c 'MessageSavingEventName = "golem15.chat.message.saving"' ../sm-chat-plugin/events/events.go` prints 1.
|
||||
- `grep -c 'errors.As(err, &rej)' ../sm-chat-plugin/api/errors.go` prints 1.
|
||||
- `grep -c 'v0.1.4' ../sm-chat-plugin/README.md` prints at least 1.
|
||||
- The SUMMARY records the before and after HEAD of all seven siblings, and each after-HEAD equals origin's default branch.
|
||||
</acceptance_criteria>
|
||||
<done>A host can refuse a chat message before it is stored, the client gets the existing 422 envelope, and without a listener chat behaves byte-identically.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: The forum gets the same seam: PostSaving before render on thread, reply and edit</name>
|
||||
<reversibility rating="costly">D-18: a new public event package and Rejection type in a WavePath port-target plugin; later consumers depend on the field names.</reversibility>
|
||||
<files>../sm-forum-plugin/events/events.go, ../sm-forum-plugin/contracts/rejection.go, ../sm-forum-plugin/classes/forum_service.go, ../sm-forum-plugin/classes/lifecycle.go, ../sm-forum-plugin/api/errors.go, ../sm-forum-plugin/saving_smoke_test.go, ../sm-forum-plugin/README.md</files>
|
||||
<read_first>../sm-forum-plugin/classes/forum_service.go (Options, New, CreateThread, Reply), ../sm-forum-plugin/classes/lifecycle.go (EditPost), ../sm-forum-plugin/boot.go (bootService passes app.Events), ../sm-forum-plugin/api/errors.go, ../sm-forum-plugin/api/threads.go and api/posts.go, ../sm-forum-plugin/http_tracer_test.go, harness_test.go and fixture_plugin_test.go (test access resolver), ../sm-forum-plugin/php_method_map_test.go, ../sm-forum-plugin/README.md, ../sm-chat-plugin/events/events.go and contracts/rejection.go (Task 1, for the shape)</read_first>
|
||||
<action>Per D-18.
|
||||
|
||||
- New package events (events/events.go):
|
||||
- `PostSavingEventName = "golem15.forum.post.saving"`;
|
||||
- `PostSaving` with `ContextType string`, `ContextID`, `ActorID`, `ThreadID` and `PostID uint`, `Title`, `Body` and `Op string`;
|
||||
- constants `OpThread = "thread"` (ThreadID 0, Title set), `OpReply = "reply"` and `OpUpdate = "update"`.
|
||||
- The package doc says forum fires no post-save events, PostSaving is a Go-only seam fired before render and persistence, and a listener error aborts the write.
|
||||
- contracts/rejection.go: the same shape as chat's, with "golem15.forum: rejected " in Error().
|
||||
- ForumService, all guarded by `s.bus != nil`, firing after the access and required-field checks and before `breakwater.Render`, returning the error unchanged:
|
||||
- CreateThread fires PostSaving with OpThread, the trimmed title and the body;
|
||||
- Reply fires it with OpReply, the ThreadID and the body;
|
||||
- EditPost (lifecycle.go) fires it with OpUpdate, the PostID, its ThreadID and the body.
|
||||
- api/errors.go `writeForumError`: before the switch, `errors.As(err, &rej)` writes 422 `{rej.Field: [rej.Message]}`, with field "body" when Field is empty.
|
||||
- saving_smoke_test.go `TestPostSavingTracer` (Docker): boot through bootWeb with the fixture resolver that allows posting; register a PostSaving listener rejecting Title or Body containing "blocked" (Field "title" or "body"). Then:
|
||||
- POST a thread whose title holds the word: 422 errors.title, no thread and no post row;
|
||||
- a clean thread: 201;
|
||||
- a reply whose body holds the word: 422 errors.body;
|
||||
- PUT the first post with the word: 422, and content_html is unchanged.
|
||||
- README: add Features "Pre-save seam" and the API reference rows (events.PostSaving, PostSavingEventName, Op constants, contracts.Rejection), and correct the Overview so it says forum fires only this pre-save event. Add a changelog line "v0.1.0: first tag; adds the pre-save PostSaving seam". Name no application.
|
||||
|
||||
Commit in ../sm-forum-plugin by path (`feat: pre-save PostSaving event and Rejection`). Do not push or tag.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-forum-plugin vet ./... && go -C ../sm-forum-plugin test -count=1 -short ./... && go -C ../sm-forum-plugin test -count=1 -run '^TestPostSavingTracer$' -v . 2>&1 | grep -q -- '^--- PASS: TestPostSavingTracer'</automated>
|
||||
<fails_when>Non-zero exit: vet or the short suite red, or no "--- PASS: TestPostSavingTracer" line (skipped without Docker, a refused write that still stored a row, or the wrong 422 field).</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `go -C ../sm-forum-plugin test -count=1 ./...` passes with Docker, php_method_map_test.go and golden_test.go unedited (`git -C ../sm-forum-plugin diff HEAD~1 --stat -- php_method_map_test.go golden_test.go` prints nothing).
|
||||
- `grep -rn 'sm-chat-plugin' ../sm-forum-plugin/go.mod ../sm-forum-plugin/events ../sm-forum-plugin/contracts` prints nothing.
|
||||
- `grep -c 'PostSaving{' ../sm-forum-plugin/classes/forum_service.go` prints 2 and `grep -c 'PostSaving{' ../sm-forum-plugin/classes/lifecycle.go` prints 1.
|
||||
</acceptance_criteria>
|
||||
<done>Forum threads, replies and edits can be refused before render and storage with the existing 422 envelope; without a listener nothing changes.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: New shared plugin sm-moderation-plugin: banned words and links, editable in admin settings, published as a Filter</name>
|
||||
<files>../sm-moderation-plugin/.gitignore, ../sm-moderation-plugin/go.mod, ../sm-moderation-plugin/go.sum, ../sm-moderation-plugin/go.work, ../sm-moderation-plugin/plugin.go, ../sm-moderation-plugin/admin.go, ../sm-moderation-plugin/admin_permissions.go, ../sm-moderation-plugin/logger.go, ../sm-moderation-plugin/classes/filter.go, ../sm-moderation-plugin/classes/filter_smoke_test.go, ../sm-moderation-plugin/models/settings.go, ../sm-moderation-plugin/models/registry.go, ../sm-moderation-plugin/models/settings/fields.yaml, ../sm-moderation-plugin/updates/202610280001_create_golem15_moderation_settings.go, ../sm-moderation-plugin/updates/registry.go, ../sm-moderation-plugin/lang/en/lang.yaml, ../sm-moderation-plugin/lang/pl/lang.yaml, ../sm-moderation-plugin/config/config.yaml, ../sm-moderation-plugin/README.md</files>
|
||||
<read_first>../sm-sitemap-plugin/plugin.go, admin.go, admin_permissions.go, logger.go, models/settings.go, models/registry.go, models/settings/fields.yaml, updates/202610110002_create_golem15_sitemap_settings.go, updates/registry.go, lang/en/lang.yaml, config/config.yaml, go.mod, go.work, README.md (the whole plugin layout), modules/phrasebook/README.md and modules/towel/README.md (locale lookup), `summer help make:plugin` (scaffold option), .planning/notes/core-plugins-own-repos.md, 20-PATTERNS.md (sm-moderation-plugin)</read_first>
|
||||
<action>Per D-17. Create the repository at /media/nvme/dev/golem15/summercms.io/summercms/sm-moderation-plugin with `git init -b main`. Add no remote yet. Start from `summer make:plugin golem15.moderation` if it emits a standalone module layout, otherwise copy the sitemap layout. Module `git.golem15.com/golem15/sm-moderation-plugin`, package `moderation`, plugin ID `golem15.moderation`, `Requires()` nil, display name Moderation / Moderacja. Standard library plus gorm, gormigrate and framework modules only.
|
||||
|
||||
- go.mod: go 1.27.0, require `git.golem15.com/golem15/summercms v0.5.1` plus gorm and gormigrate at the versions sitemap's go.mod uses, and `replace git.golem15.com/golem15/summercms => ../summercms.go`. Run `go mod tidy`. Track a go.work with `use .` and the same replace, as sitemap does.
|
||||
- plugin.go: the interface assertions (party.Plugin, pact.HasConfig, pact.HasMigrations, pact.HasModels) and `//go:embed config`. Register keeps the app. Boot calls lagoon.OnDatabase and publishes `classes.NewFilter(db)` with `app.Publish`. The package doc says the plugin is a content filter other plugins' hosts wire into their pre-save seams, and that it imports no content plugin.
|
||||
- config/config.yaml: `max_entries: 500`, `max_entry_length: 200`.
|
||||
- models/settings.go: `Settings` singleton on `golem15_moderation_settings` (`ID uint`, `Enabled bool`, `BannedWords *string`, `BannedLinks *string`). `BeforeValidate` refuses:
|
||||
- a list longer than max_entries (lang key `golem15.moderation::lang.errors.too_many`);
|
||||
- an entry longer than max_entry_length (`errors.too_long`);
|
||||
- a link entry that is neither a host name nor an http(s) URL (`errors.link`).
|
||||
models/registry.go `All()`.
|
||||
- models/settings/fields.yaml: `enabled` (switch), `banned_words` (textarea, comment "one word or phrase per line"), `banned_links` (textarea, comment "one domain or URL per line; subdomains match"), with the sitemap header comment block.
|
||||
- updates/202610280001_create_golem15_moderation_settings.go: `CREATE TABLE IF NOT EXISTS golem15_moderation_settings (id INTEGER PRIMARY KEY, enabled BOOLEAN NOT NULL DEFAULT TRUE, banned_words TEXT NULL, banned_links TEXT NULL)` and `INSERT INTO golem15_moderation_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING`. Rollback drops the table. updates/registry.go `All()`.
|
||||
- admin.go: `Settings()` returns item Code `moderation_settings` (lang keys for label, description and category, Icon "shield", Permissions [`golem15.moderation.manage_settings`], Form `models/settings/fields.yaml`, NewModel), plus AdminFS and LangFS, with the sitemap assertions (AdminAssets, HasSettings, HasLang). admin_permissions.go declares the permission on the role list sitemap uses.
|
||||
- lang/en and lang/pl: plugin name, settings label, description and category, field labels and comments, `errors.*`, `violation.word` ("The text contains a word that is not allowed." / "Tekst zawiera niedozwolone słowo.") and `violation.link` ("The text contains a link that is not allowed." / "Tekst zawiera niedozwolony link.").
|
||||
- classes/filter.go:
|
||||
- `KindWord = "word"`, `KindLink = "link"`; `Violation{Kind, Match, Message string}`;
|
||||
- `ParseList(text string) []string` splits lines, trims them, lowercases them and drops empty lines and duplicates;
|
||||
- `Match(words, links []string, text string) *Violation` is pure and returns the first violation, words before links:
|
||||
- a word or phrase matches case-insensitively between non-letter, non-digit boundaries (built from `[^\p{L}\p{N}_]`, start or end of text) with regexp.QuoteMeta;
|
||||
- a link entry that is a host (example.com) matches any host in the text equal to it or ending in "." plus it: http(s) URLs, `www.` prefixes and bare domains (labels of letters, digits and hyphens with at least one dot);
|
||||
- an entry with a path matches a URL whose host and path start with it.
|
||||
- `NewFilter(db) *Filter`. `(*Filter).Check(ctx, text) (*Violation, error)` reads the singleton row, returns nil when Enabled is false, recompiles the matchers only when the stored text differs from the cached text (guard the cache with a sync.Mutex), and fills Message from the lang phrase for `towel.Locale(ctx)` through the framework phrasebook, falling back to the English text.
|
||||
- classes/filter_smoke_test.go `TestFilterSmoke` (pure, no Docker): "Spam here" against the word spam gives a word violation; "spamming" gives none; "Kup TANIO teraz" against the phrase "kup tanio" gives a word violation; "visit https://www.bad.example/x" and "bad.example" against the link bad.example give link violations; "good.example" gives none.
|
||||
- README.md in the standard plugin structure:
|
||||
- H1, one-sentence summary, import line, a `**Name:**` beach mnemonic, Overview, Features, Usage, API reference, Configuration (the two keys), Dependencies, Testing;
|
||||
- Usage shows a neutral host (`acme`) wiring Check into another plugin's pre-save event without importing it here.
|
||||
|
||||
Commit in ../sm-moderation-plugin (`feat: golem15.moderation banned word and link filter`). Do not create a remote, push or tag.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-moderation-plugin vet ./... && go -C ../sm-moderation-plugin test -count=1 -short ./... && go -C ../sm-moderation-plugin test -count=1 -run '^TestFilterSmoke$' -v ./classes 2>&1 | grep -q -- '^--- PASS: TestFilterSmoke'</automated>
|
||||
<fails_when>Non-zero exit: the module does not build or vet, the short suite fails, or no "--- PASS: TestFilterSmoke" line (a boundary or host rule wrong).</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `go -C ../sm-moderation-plugin list -deps ./... | grep -cE 'sm-(chat|forum)-plugin'` prints 0.
|
||||
- `grep -c 'golem15_moderation_settings' ../sm-moderation-plugin/updates/202610280001_create_golem15_moderation_settings.go` prints at least 2.
|
||||
- `grep -c 'golem15.moderation.manage_settings' ../sm-moderation-plugin/admin_permissions.go` prints at least 1.
|
||||
- `grep -c '^\*\*Name:\*\*' ../sm-moderation-plugin/README.md` prints 1.
|
||||
- `git -C ../sm-moderation-plugin remote` prints nothing.
|
||||
</acceptance_criteria>
|
||||
<done>A shared, chat-agnostic moderation plugin exists locally: admins edit banned words and links in settings, and any host can call Filter.Check from a pre-save seam.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Module-mode hygiene: chatvideo, herald and userfriends require tags, and the whole family builds and tests</name>
|
||||
<files>../sm-chatvideo-plugin/go.mod, ../sm-chatvideo-plugin/go.sum, ../sm-chatvideo-plugin/README.md, ../sm-herald-plugin/go.mod, ../sm-herald-plugin/go.sum, ../sm-herald-plugin/README.md, ../sm-userfriends-plugin/go.mod, ../sm-userfriends-plugin/go.sum, ../sm-userfriends-plugin/README.md</files>
|
||||
<read_first>../sm-chatvideo-plugin/go.mod and go.work, ../sm-herald-plugin/go.mod and go.work, ../sm-userfriends-plugin/go.mod and go.work, ../sm-chatchannels-plugin/go.mod and ../sm-chatbots-plugin/go.mod (after the Task 1 fast-forward), ../sm-bm-app/plugins/golem15/chatbots/go.mod (the "drop sibling replaces" analog), each README's Dependencies section, 20-RESEARCH.md (Repo State and Tagging, Pitfall 2)</read_first>
|
||||
<action>Per SC-1 and RESEARCH Pitfall 2. With `GOWORK=off GOPRIVATE=git.golem15.com/*`, edit each go.mod:
|
||||
- sm-chatvideo-plugin: delete the sm-chat-plugin and sm-user-plugin replace lines. Require `git.golem15.com/golem15/sm-chat-plugin v0.1.3`, `sm-user-plugin v0.1.13` and `summercms v0.5.1`. Keep `replace git.golem15.com/golem15/summercms => ../summercms.go`.
|
||||
- sm-herald-plugin: delete the sm-user-plugin replace. Require sm-user-plugin v0.1.13 and summercms v0.5.1.
|
||||
- sm-userfriends-plugin: replace the chat pseudo-version with v0.1.3. Require sm-user-plugin v0.1.13 and summercms v0.5.1.
|
||||
- sm-chatchannels-plugin and sm-chatbots-plugin (fast-forwarded in Task 1): confirm their go.mod holds no replace other than the framework one and only tag requires. If either does not, fix it the same way and add it to this task's commits.
|
||||
Run `go mod tidy` in each changed repo (GOWORK=off). The tracked go.work files stay: they are development conveniences and module consumers ignore them. Update each changed README's Dependencies section with the required versions and add a changelog line "v0.1.0: first tag; requires tagged chat, user and SummerCMS modules" where the README keeps a changelog.
|
||||
|
||||
Then verify the family:
|
||||
- all eight repos (chat, chatchannels, chatbots, chatvideo, forum, herald, userfriends, moderation) pass `go vet ./...` and `go test -count=1 -short ./...` through their go.work;
|
||||
- chatchannels, chatbots, chatvideo, herald and userfriends pass `GOWORK=off go build ./...`, which resolves chat v0.1.3 and user v0.1.13 from git.golem15.com and proves no sibling path is needed.
|
||||
Grep the changed files of all eight repos for consuming application names (the sm-*-app repository names next to summercms.go, and their vendor prefixes) and remove any hit in the files this plan changed.
|
||||
|
||||
Commit per repo by path (`chore: require tagged chat, user and SummerCMS modules; drop sibling replaces`). Do not push or tag.</action>
|
||||
<verify>
|
||||
<automated>for r in sm-chat-plugin sm-chatchannels-plugin sm-chatbots-plugin sm-chatvideo-plugin sm-forum-plugin sm-herald-plugin sm-userfriends-plugin sm-moderation-plugin; do go -C ../$r vet ./... && go -C ../$r test -count=1 -short ./... || exit 1; done && for r in sm-chatchannels-plugin sm-chatbots-plugin sm-chatvideo-plugin sm-herald-plugin sm-userfriends-plugin; do GOWORK=off GOPRIVATE='git.golem15.com/*' go -C ../$r build ./... || exit 1; done</automated>
|
||||
<fails_when>Non-zero exit: any repo fails vet or its short suite, or a GOWORK=off build fails (for example "unknown revision v0.0.0" or a sibling replace path that no longer resolves).</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -cE '^replace .*=> \.\./(sm-|fonoteka)' ../sm-chatvideo-plugin/go.mod ../sm-herald-plugin/go.mod ../sm-userfriends-plugin/go.mod` prints 0 for each file.
|
||||
- `grep -c 'v0.0.0' ../sm-chatvideo-plugin/go.mod ../sm-userfriends-plugin/go.mod` prints 0 for each file.
|
||||
- `grep -c 'sm-chat-plugin v0.1.3' ../sm-chatvideo-plugin/go.mod ../sm-userfriends-plugin/go.mod` prints 1 for each file.
|
||||
- `git -C ../sm-<repo> status --porcelain` prints nothing for all eight repos after the commits.
|
||||
</acceptance_criteria>
|
||||
<done>Every chat-family module resolves its dependencies by tag, so a demo build without go.work can require them.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking">
|
||||
<name>Task 5: Go-ahead for the one-way release: create sm-moderation-plugin on git.golem15.com, push eight repositories and tag them</name>
|
||||
<read_first>The release table Claude prints before asking, one row per repository: current remote tags (`git ls-remote --tags origin`), the next free tag, the commit it will sit on, the commits it adds since the last tag, and why (seam, first tag, go.mod fix, new repo). Also whether `tea login list` shows a git.golem15.com login, and the visibility of sm-sitemap-plugin.</read_first>
|
||||
<acceptance_criteria>
|
||||
- The user chose one option and it is recorded in the SUMMARY. With "release", git.golem15.com/golem15/sm-moderation-plugin exists (`git ls-remote git@git.golem15.com:golem15/sm-moderation-plugin.git` exits 0) before Task 6 pushes.
|
||||
</acceptance_criteria>
|
||||
<decision>Create the sm-moderation-plugin repository, push the commits of this plan and set the eight tags now?</decision>
|
||||
<context>A pushed Go module tag is cached by module proxies and can never point anywhere else (one-way, as 19.4.1 D-03). Tags freeze exactly the commits Tasks 1-4 verified. chat v0.1.4 is a patch: the seam is additive and WavePath's parity tests stay green (D-18). Plan 02 cannot mount the chat family by tag until this runs. Creating a repository on git.golem15.com and pushing need your go-ahead (CLAUDE.md).</context>
|
||||
<options>
|
||||
<option id="release">
|
||||
<name>Create the repository, push and tag</name>
|
||||
<pros>Plan 02 can mount all eight plugins by tag; other applications can adopt the seams and the filter.</pros>
|
||||
<cons>The tags are permanent; a mistake needs a new patch tag, not a moved tag.</cons>
|
||||
</option>
|
||||
<option id="hold">
|
||||
<name>Hold</name>
|
||||
<pros>Nothing permanent happens.</pros>
|
||||
<cons>Plan 02 and every later plan are blocked; the demo cannot require the chat family.</cons>
|
||||
</option>
|
||||
</options>
|
||||
<instructions>For "release": if Claude reported no tea login for git.golem15.com, create the repository golem15/sm-moderation-plugin (empty, same visibility as sm-sitemap-plugin, default branch main) in the git.golem15.com web UI first, then reply. Do not paste tokens into the chat.</instructions>
|
||||
<resume-signal>Reply "release" (add "repo ready" if you created the repository yourself) or "hold".</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 6: Release: push, tag and prove the eight tags resolve in module mode</name>
|
||||
<reversibility rating="one-way">Pushed module tags are cached by proxies and every consumer's go.sum; they can only be superseded, never moved.</reversibility>
|
||||
<precondition>Task 5 resolved with "release"; `git ls-remote git@git.golem15.com:golem15/sm-moderation-plugin.git` exits 0 or `tea login list` lists git.golem15.com.</precondition>
|
||||
<files>../sm-moderation-plugin/README.md</files>
|
||||
<read_first>The Task 5 release table, each repository's README changelog, .planning/notes/core-plugins-own-repos.md</read_first>
|
||||
<action>Per SC-1 and D-17/D-18.
|
||||
1. If the repository does not exist yet, create it with `tea repo create --name sm-moderation-plugin --owner golem15` at sm-sitemap-plugin's visibility. Then `git -C ../sm-moderation-plugin remote add origin git@git.golem15.com:golem15/sm-moderation-plugin.git` and push main with upstream.
|
||||
2. Push the default branch of chat, forum, chatvideo, herald and userfriends (and chatchannels or chatbots only if Task 4 committed there). No force-push.
|
||||
3. Read the next free tag from `git ls-remote --tags origin` per repository: chat the next v0.1.x patch (v0.1.4 expected), the others v0.1.0 unless taken. Create annotated tags (`git tag -a <tag> -m "<one-line summary>"`) on the HEADs Tasks 1-4 verified, chat first. Push each tag. Confirm `git ls-remote origin refs/tags/<tag>^{}` equals the local HEAD.
|
||||
4. If the moderation README says no tag exists yet, fix it in a commit before tagging that repository, never after.
|
||||
5. Module-mode proof: in a fresh `mktemp -d` directory, `go mod init example.invalid/familycheck`, require all eight modules at their new tags plus `git.golem15.com/golem15/summercms v0.5.1`, write a main.go that blank-imports the eight plugin packages, then run `GOWORK=off GOPRIVATE=git.golem15.com/* GOFLAGS=-mod=mod go build ./...`. Delete the directory afterwards. The SUMMARY records every tag with its commit hash for plan 02.</action>
|
||||
<verify>
|
||||
<automated>for r in sm-chat-plugin sm-chatchannels-plugin sm-chatbots-plugin sm-chatvideo-plugin sm-forum-plugin sm-herald-plugin sm-userfriends-plugin sm-moderation-plugin; do t=$(git -C ../$r describe --tags --exact-match HEAD) && [ "$(git -C ../$r ls-remote origin "refs/tags/$t^{}" | cut -f1)" = "$(git -C ../$r rev-parse HEAD)" ] || exit 1; done</automated>
|
||||
<fails_when>Non-zero exit: a repository HEAD carries no tag (describe fails), or the remote tag is missing or points at a different commit than the local HEAD.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `git -C ../sm-chat-plugin describe --tags --exact-match HEAD` prints a v0.1.x tag at least v0.1.4.
|
||||
- The scratch module build printed no error, and the SUMMARY lists the eight tags with commit hashes.
|
||||
- `git -C ../sm-<repo> status --porcelain` prints nothing in all eight repositories.
|
||||
</acceptance_criteria>
|
||||
<done>All eight chat-family modules are on git.golem15.com at verified tags and resolve together without go.work.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| chat/forum HTTP clients to the plugin APIs | message and post bodies are untrusted input; the seam runs on them before storage |
|
||||
| host listeners to plugin services | host code decides refusals; a listener error must not leak internals |
|
||||
| admin settings to the filter | administrators edit the lists that every message is matched against |
|
||||
| build machine to git.golem15.com and module proxies | tags become immutable inputs of every consumer |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-20-01 | Tampering | chat write paths that skip the seam | high | mitigate | MessageSaving fires in SendMessageWith (SendMessage, replies and ReplyTo funnel into it) and EditMessage; TestMessageSavingTracer covers send, reply and edit; plan 06 adds the full matrix |
|
||||
| T-20-02 | Information disclosure | listener errors surfacing in API bodies | medium | mitigate | only contracts.Rejection maps to 422 with its own Field and Message; any other listener error keeps the existing opaque 500 path of writeChatError and writeForumError |
|
||||
| T-20-03 | Denial of service | filter matching cost | medium | mitigate | RE2 linear-time regexps from QuoteMeta'd entries; at most 500 entries of at most 200 characters per list (BeforeValidate); matchers compiled once per list version |
|
||||
| T-20-04 | Elevation of privilege | moderation settings screen | medium | mitigate | settings item behind the permission golem15.moderation.manage_settings, granted to the same role as sitemap settings |
|
||||
| T-20-05 | Tampering | one-way module tags | medium | mitigate | tags only on HEADs that passed vet and tests in Tasks 1-4; next free number read from the remote; go-ahead at Task 5; ls-remote check and a GOWORK=off scratch build after pushing |
|
||||
| T-20-SC | Tampering | npm/pip/cargo installs | high | mitigate | no package-manager install: Go modules are the project's own tagged repositories plus gorm and gormigrate at versions already in sm-sitemap-plugin's go.sum (GOPRIVATE, go.sum committed) |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- Task 1: chat seam tracer green, chat parity tests unchanged, seven siblings at origin HEAD.
|
||||
- Task 2: forum seam tracer green, forum parity tests unchanged.
|
||||
- Task 3: moderation plugin builds, filter smoke green, no dependency on chat or forum.
|
||||
- Task 4: all eight repositories vet and test; five chat dependants build with GOWORK=off.
|
||||
- Task 5: go-ahead recorded.
|
||||
- Task 6: eight tags on origin equal to the verified HEADs; the scratch module builds them together.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
The chat family and the moderation plugin are released by tag (SC-1 prerequisite), with the D-18 seams additive and parity-safe and the D-17 filter shared and chat-agnostic.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/20-demo-instance-with-every-shared-plugin/20-01-SUMMARY.md` when done: per repository the before and after HEAD, the tag and its commit, the go.mod changes, how the moderation repository was created, and the checkpoint answer.
|
||||
</output>
|
||||
@@ -0,0 +1,400 @@
|
||||
---
|
||||
phase: 20-demo-instance-with-every-shared-plugin
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["20-01"]
|
||||
files_modified:
|
||||
- ../sm-demo-app/.gitmodules
|
||||
- ../sm-demo-app/go.work
|
||||
- ../sm-demo-app/go.mod
|
||||
- ../sm-demo-app/go.sum
|
||||
- ../sm-demo-app/summer.yaml
|
||||
- ../sm-demo-app/plugins.gen.go
|
||||
- ../sm-demo-app/plugins/golem15/oauthserver
|
||||
- ../sm-demo-app/plugins/golem15/chat
|
||||
- ../sm-demo-app/plugins/golem15/chatchannels
|
||||
- ../sm-demo-app/plugins/golem15/chatbots
|
||||
- ../sm-demo-app/plugins/golem15/chatvideo
|
||||
- ../sm-demo-app/plugins/golem15/forum
|
||||
- ../sm-demo-app/plugins/golem15/herald
|
||||
- ../sm-demo-app/plugins/golem15/userfriends
|
||||
- ../sm-demo-app/plugins/golem15/moderation
|
||||
- ../sm-demo-app/config/golem15.chatvideo.yaml
|
||||
- ../sm-demo-app/plugins/demo/plugin.go
|
||||
- ../sm-demo-app/plugins/demo/config/config.yaml
|
||||
- ../sm-demo-app/plugins/demo/chat.go
|
||||
- ../sm-demo-app/plugins/demo/chat_api.go
|
||||
- ../sm-demo-app/plugins/demo/chatbots.go
|
||||
- ../sm-demo-app/plugins/demo/moderation.go
|
||||
- ../sm-demo-app/plugins/demo/realtime_access.go
|
||||
- ../sm-demo-app/plugins/demo/routes.go
|
||||
- ../sm-demo-app/plugins/demo/classes/chat_context.go
|
||||
- ../sm-demo-app/plugins/demo/classes/forum_access.go
|
||||
- ../sm-demo-app/plugins/demo/console/seed.go
|
||||
- ../sm-demo-app/plugins/demo/console/seed_chat.go
|
||||
- ../sm-demo-app/plugins/demo/console/seed_forum.go
|
||||
- ../sm-demo-app/plugins/demo/console/seed_friends.go
|
||||
- ../sm-demo-app/plugins/demo/console/reset.go
|
||||
- ../sm-demo-app/chat_smoke_test.go
|
||||
- ../sm-demo-app/boot_test.go
|
||||
- ../sm-demo-app/realtime_test.go
|
||||
- ../sm-demo-app/deploy/centrifugo.json.example
|
||||
- ../sm-demo-app/deploy/livekit.yaml.example
|
||||
- ../sm-demo-app/deploy/env.example
|
||||
- ../sm-demo-app/scripts/dev.sh
|
||||
- ../sm-demo-app/README.md
|
||||
- .planning/todos/pending/2026-10-26-sm-demo-app-journal-oauthserver-pointers.md
|
||||
- .planning/todos/done/2026-10-26-sm-demo-app-journal-oauthserver-pointers.md
|
||||
autonomous: true
|
||||
requirements: [SC-1, D-05, D-06, D-07, D-08, D-09, D-10, D-12, D-14, D-17, D-20, D-22, D-23]
|
||||
estimate:
|
||||
tokens: 320000
|
||||
raw_tokens: 320000
|
||||
tasks: 4
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "SC-1: sm-demo-app requires chat, chatchannels, chatbots, chatvideo, forum, herald, userfriends and moderation at the plan 01 tags, mounts them as submodules at plugins/golem15/<name> checked out on those tags, lists them in summer.yaml before golem15.demo, and passes TestGoModRequiresTags, TestBootAllPlugins and scripts/check-pins.sh; scripts/build.sh dev builds."
|
||||
- "Folded todo: plugins/golem15/oauthserver sits on tag v0.1.3 (which contains b6a9586), go.mod requires sm-oauthserver-plugin v0.1.3, the change is one commit `chore: bump sm-oauthserver-plugin to v0.1.3`, and the todo file moves to .planning/todos/done with g15office task ssuyq2os set to Live."
|
||||
- "D-06: golem15.demo registers the chat context alias demo (id 1, broadcast chat:context:1) whose members are every activated, non-deleted, non-banned user, and the alias channel (chatchannels' ChannelAlias) that resolves a seeded channel's backing conversation to the same membership with broadcast chat:channel:{id}; both require live membership."
|
||||
- "D-07: channels exist only through demo:seed; the chatchannels access resolver answers CanManageContext false, so POST /_chatchannels/api/v1/contexts/demo/1/channels is refused for every member; a member can join and leave seeded channels and post in them."
|
||||
- "D-08: any member can open a DM with any other member through POST /_chat/api/v1/contexts/demo/1/dm; userfriends does not gate it."
|
||||
- "D-09: demo:seed creates persona users (no usable password) with EN and PL channel history, threads, reactions and pins, forum categories, threads and posts in EN and PL, friendships and a friend group, the demo-moderators user group and default moderation lists; a second run reports exists; demo:reset wipes every chat-family table in foreign-key order and reseeds, keeping golem15_moderation_settings and the userfriends context singleton."
|
||||
- "D-17: a banned word or link in a chat message (create, reply, edit) or a forum thread, reply or edit is refused with 422 through the demo's listeners on events.MessageSaving and events.PostSaving calling the published moderation Filter."
|
||||
- "D-22: members of the user group named by golem15.demo.chat.moderator_group (default demo-moderators) can delete other members' messages in channel conversations (CanModerateMessage) and manage forum posts (forum CanManage); the bootstrap reports moderator true for them; banning stays in the user admin."
|
||||
- "D-05: chatbots lists the demo's ephemeral slash commands in GET /_chatbots/api/v1/commands and POST /_chatbots/api/v1/dispatch answers them in ephemeral mode; there is no bot persona."
|
||||
- "D-12, D-23: config/golem15.chatvideo.yaml sets max_duration 900, max_participants 4, token_ttl 900 and screen_share false; the server does not restrict calls by conversation type (calls are limited to DMs by the UI, plan 04)."
|
||||
- "GET /_demo/api/v1/chat answers a member with {member, moderator, context, channels[], conversations[], limits} and GET /_demo/api/v1/chat/members with {members[]}; both need a JWT."
|
||||
- "GET /api/realtime/token answers 200 for any member (guard demo.realtime-access) and 401 without a JWT; Centrifugo's committed config has the namespaces presentations, chat, user and video with the subscribe proxy; scripts/dev.sh up starts a local LiveKit on 127.0.0.1:7890 (TCP 7891, UDP 7892) with keys written to .env only (D-10 locally)."
|
||||
- "D-14: the forum resolver lets every member read and post in context demo/1 and the seeded EN and PL categories are listed by GET /_forum/api/v1/contexts/demo/1/categories."
|
||||
artifacts:
|
||||
- path: "../sm-demo-app/plugins/demo/classes/chat_context.go"
|
||||
provides: "DemoContext, ChannelContext adapter, ChatAccess resolver, membership query"
|
||||
contains: "ChatContextType"
|
||||
- path: "../sm-demo-app/plugins/demo/chat.go"
|
||||
provides: "registerChat: registry aliases demo and channel, resolvers, limits"
|
||||
contains: "ChannelAlias"
|
||||
- path: "../sm-demo-app/plugins/demo/chat_api.go"
|
||||
provides: "GET /_demo/api/v1/chat and /chat/members (host bootstrap contract)"
|
||||
contains: "/chat/members"
|
||||
- path: "../sm-demo-app/plugins/demo/moderation.go"
|
||||
provides: "MessageSaving and PostSaving listeners calling the moderation Filter"
|
||||
contains: "MessageSaving"
|
||||
- path: "../sm-demo-app/plugins/demo/realtime_access.go"
|
||||
provides: "demo.realtime-access middleware"
|
||||
contains: "demo.realtime-access"
|
||||
- path: "../sm-demo-app/plugins/demo/console/seed_chat.go"
|
||||
provides: "persona users, channels, messages, threads, reactions, pins (EN/PL)"
|
||||
contains: "persona"
|
||||
- path: "../sm-demo-app/deploy/livekit.yaml.example"
|
||||
provides: "LiveKit config template: port 7890, tcp 7891, udp mux 7892, room caps"
|
||||
contains: "udp_port: 7892"
|
||||
- path: "../sm-demo-app/chat_smoke_test.go"
|
||||
provides: "TestDemoChatTracer, TestDemoModerationSmoke, TestSeedChatSmoke"
|
||||
contains: "TestDemoChatTracer"
|
||||
key_links:
|
||||
- from: "../sm-demo-app/plugins/demo/chat.go"
|
||||
to: "../sm-demo-app/plugins/golem15/chat/contracts/registry.go"
|
||||
via: "reg.Add(p.ID(), chatcontracts.Registration{Alias: chanmodels.ChannelAlias, ...})"
|
||||
pattern: "Alias: +chanmodels\\.ChannelAlias"
|
||||
- from: "../sm-demo-app/plugins/demo/moderation.go"
|
||||
to: "../sm-demo-app/plugins/golem15/moderation/classes/filter.go"
|
||||
via: "app.Lookup[*modclasses.Filter]() then Check(ctx, content)"
|
||||
pattern: "Check\\(ctx"
|
||||
- from: "../sm-demo-app/plugins/demo/routes.go"
|
||||
to: "../sm-demo-app/plugins/demo/realtime_access.go"
|
||||
via: "lighthouse.Mount UserAuth surf.Use(\"jwt.auth\", \"demo.realtime-access\", bucketRealtime)"
|
||||
pattern: "demo\\.realtime-access"
|
||||
- from: "../sm-demo-app/plugins/demo/console/reset.go"
|
||||
to: "../sm-demo-app/plugins/demo/console/seed_chat.go"
|
||||
via: "resetTables lists the chat-family tables before users; runSeed reseeds them"
|
||||
pattern: "golem15_chat_messages"
|
||||
prohibitions:
|
||||
- statement: "No file inside a plugin submodule is edited from sm-demo-app; shared plugin fixes go to the plugin repos (plan 01 pattern) and need a new tag."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
- statement: "go.mod carries no replace directive and no pseudo-version for a golem15 module."
|
||||
status: unresolved
|
||||
verification: automated
|
||||
- statement: "No LiveKit key, VAPID key or persona password appears in a committed file or in command output; .env holds them."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
- statement: "Nothing is pushed in this plan (plan 05 pushes with the deploy go-ahead)."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
ROADMAP Goal (not in user-story format; no story is invented here): `sm-demo-app` runs every shared Golem15 plugin in one binary, and its public Nuxt site shows them working; this phase adds the chat family to the live demo from Phase 19.4.1.
|
||||
|
||||
This plan's slice (plan 2 of 6, wave 2, per D-20): the demo backend. After it, a registered demo member can read and post in seeded channels, DM anyone, use slash commands, read and post in the forum and manage friends through the plugins' APIs. Moderation refuses banned content, and a local LiveKit can mint calls. Nothing in the Nuxt site changes yet (plans 03 and 04).
|
||||
|
||||
## Decisions taken in this plan (Claude's discretion, recorded)
|
||||
|
||||
- **Contexts (D-06):** alias `demo`, id 1, group name "General", broadcast `chat:context:1`. Alias `channel` (chatchannels' `models.ChannelAlias`, legacy class `models.ChannelMorphName`) loads the channel by id, with broadcast `chat:channel:{id}`. Neither broadcast is subscribable, because AuthorizeChat admits only `chat:conversation:{id}`. Both implement `contracts.LiveMembership` (true) and `contracts.MessageModerator` (moderator group, non-DM conversations only), as bm does.
|
||||
- **Joining a channel (RESEARCH assumption A3):** the layer opens a channel with `GET /_chat/api/v1/contexts/channel/{channelId}/conversation`, whose GetGroupChat lazy-joins a context member. The tracer test proves it. If it does not join, the bootstrap adds the caller as participant of each joined channel with chat's `AddUserToGroupChat`, and the SUMMARY records which path holds.
|
||||
- **Personas (discretion):** four, Ada and Ben (English) and Ola and Tomek (Polish). Emails are `<name>@personas.demo.invalid` (RFC 2606, undeliverable). Each gets a random bcrypt hash that is never printed, so nobody can log in as a persona. Channels: `general` (#general), `announcements` (#announcements), `ogolny` (#ogólny, Polish). About 15 to 25 messages per language with two threads, reactions and one pin per channel. Persona Ada sends a welcome DM to the visitor account when golem15.demo.visitor.email is set.
|
||||
- **Seeding writes:** raw SQL in the existing seed style (`insertID`, `exists`), or a ChannelService instance built with a seed-only resolver and no bus or realtime, for the channel rows. The demo's own resolver refuses CanManageContext (RESEARCH Anti-Patterns), so it cannot seed. Forum posts store content_html from `breakwater.Render`, the plugin's own renderer.
|
||||
- **Moderator group (D-22):** user group code `demo-moderators` (config `golem15.demo.chat.moderator_group`), created by the seed with no members. demo:reset wipes memberships with the users, so moderators are reassigned after a reset.
|
||||
- **Slash commands (D-05 discretion):** `/demo` explains what the demo shows and `/tour` lists the site sections with their paths. Both are ephemeral, English and Polish chosen from the actor's locale when known (else English). `/help` is chatbots' built-in.
|
||||
- **Bootstrap contract:** bm's `/_bm/api/v1/chat` shape with `general` replaced by `channels[]`. Each channel is `{id, slug, name, conversation_id, joined, unread, mentions}`. vue-chat-layer documents this contract under a neutral name (plan 03).
|
||||
- **Local LiveKit (D-10 locally):** ports 7890/7891/7892, the same as rome (RESEARCH Pitfall 7), so dev and production configs differ only in keys and external IP. `livekit_url` locally is `ws://127.0.0.1:7890`.
|
||||
- spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip).
|
||||
|
||||
## Artifacts this phase produces (this plan)
|
||||
|
||||
- Submodules `plugins/golem15/{chat,chatchannels,chatbots,chatvideo,forum,herald,userfriends,moderation}`; summer.yaml entries; go.mod requires at the plan 01 tags; sm-oauthserver-plugin v0.1.3.
|
||||
- golem15.demo: `classes.DemoContext`, `classes.ChannelContext`, `classes.ChatAccess`, `classes.ForumAccess`, `classes.ChatContextType = "demo"`, `classes.ChatContextID = 1`, `classes.ChatBroadcastChannel = "chat:context:1"`, `classes.ChannelBroadcastChannel(id uint) string`, `classes.MemberIDs`; `(*Plugin).registerChat`; `MiddlewareRealtimeAccess = "demo.realtime-access"` with `pact.HasMiddleware`; routes `GET /_demo/api/v1/chat` and `GET /_demo/api/v1/chat/members`; slash commands `/demo` and `/tour`; seam listeners for `chatevents.MessageSaving` and `forumevents.PostSaving`.
|
||||
- Config keys: `golem15.demo.chat.moderator_group` (default `demo-moderators`), `golem15.demo.chat.max_embeds` (default 3); `config/golem15.chatvideo.yaml` (`livekit_url`, `livekit_admin_url`, `max_duration: 900`, `max_participants: 4`, `token_ttl: 900`, `screen_share: false`).
|
||||
- demo:seed steps: `chat personas`, `chat channels and history (en)`, `chat channels and history (pl)`, `forum categories and threads (en, pl)`, `friends and group`, `moderator group demo-moderators`, `moderation lists`; demo:reset table list extended.
|
||||
- Deploy and dev: `deploy/livekit.yaml.example`, Centrifugo namespaces `chat`, `user`, `video`; `scripts/dev.sh` LiveKit start and stop (pid name `livekit`); env keys `SUMMER_GOLEM15__CHATVIDEO__LIVEKIT_API_KEY` and `SUMMER_GOLEM15__CHATVIDEO__LIVEKIT_API_SECRET` (names only in env.example).
|
||||
- Smoke tests: `TestDemoChatTracer`, `TestDemoModerationSmoke`, `TestSeedChatSmoke` (chat_smoke_test.go). Full unit tests come in plan 06.
|
||||
|
||||
<objective>
|
||||
Mount the released chat family in sm-demo-app and give it a demo host: community context, channel adapter, resolvers, bootstrap endpoints, realtime for every member, moderation wiring, slash commands, EN/PL showcase seed with reset, and a local LiveKit.
|
||||
|
||||
Purpose: SC-1 (builds with every shared plugin; demo:seed fills each), D-05 to D-09, D-12, D-14, D-17, D-22, D-23 on the server side, and the folded oauthserver todo. Plans 03 and 04 build the site UI on top of these endpoints.
|
||||
Output: commits in ../sm-demo-app (code) and one planning commit in summercms.go (the todo move). Nothing pushed.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-CONTEXT.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-RESEARCH.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-PATTERNS.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-01-SUMMARY.md
|
||||
@.planning/todos/pending/2026-10-26-sm-demo-app-journal-oauthserver-pointers.md
|
||||
@.planning/G15OFFICE.md
|
||||
|
||||
Repo: ../sm-demo-app (all code edits). Read-only: the mounted plugin submodules, ../sm-bm-app/plugins/jz/bm (host analog), ../sm-bm-app/deploy/centrifugo/bm-config.json (namespace options).
|
||||
|
||||
Rules: no co-author tags; stage by explicit path; one `feat: mount golem15.<id> <tag>` commit per plugin (precedent 9451b31: submodule, .gitmodules, go.work, go.mod, go.sum, summer.yaml, plugins.gen.go, boot_test.go); planning files in their own commit in summercms.go. Other sessions (Phase 33) may have uncommitted edits in sm-demo-app or vue-demo-app: check `git status --porcelain` before each commit, never stage or revert files this plan did not change, and stop if a file this plan must change carries foreign edits. Verify commands run from the framework root with `go -C ../sm-demo-app` and scripts by path (they cd to their own root). Docker tests: if the socket is denied, run through `sudo -u golem`.
|
||||
|
||||
<interfaces>
|
||||
Host analog ../sm-bm-app/plugins/jz/bm:
|
||||
- chat.go: `registerChat` (HasPlugin guard, `app.Lookup[*chatcontracts.Registry]()`, `reg.Add(p.ID(), chatcontracts.Registration{Alias, Loader})`, `lagoon.OnDatabase` then `app.Lookup[*chanclasses.ChannelService]()` and `SetAccessResolver`; `installChatLimits` via `svc.SetMaxMessageBytes`).
|
||||
- classes/chat_context.go: assertions `chatcontracts.Context`, `MessageModerator`, `LiveMembership`, `chancontracts.AccessResolver`; methods ID, Type, ChatEnabled, GroupName, BroadcastChannel, CanActFor (`chatcontracts.DefaultCanActFor`), CanOverseeChats, CanModerateMessage, RequireLiveMembership, HasMember, Members; `ChatContextLoader`; member query joins users_groups and user_groups.
|
||||
- realtime_access.go: `MiddlewareRealtimeAccess`, `Middlewares() map[string]pact.Middleware`, `realtimeAccess`, `writeRealtimeGuard`.
|
||||
- controllers/api/chat.go: bootstrap JSON structs (member, moderator, context, general, conversations, limits; members with id, name, surname, handle, is_bot).
|
||||
chatchannels: `models.ChannelAlias = "channel"`, `models.ChannelMorphName`, `models.Channel{ContextType, Name, Slug, Visibility...}`; `chancontracts.AccessResolver{CanAccessContext, CanManageContext}`; `ChannelService.CreateChannel` gated by CanManageContext (channel_service.go:136); `JoinChannel` only upserts a pref row (:959).
|
||||
chat: `Service.GetGroupChat(ctx, actorID, alias, contextID)`, `AddUserToGroupChat(ctx, userID, alias, contextID)`, `GetConversationsForUser`, `GetAvailableDmRecipients`, `SetMaxMessageBytes`, `MaxMessageBytes`; events `MessageSaving` and `contracts.Rejection` (plan 01).
|
||||
forum: `forum.Plugin.SetAccessResolver`, `contracts.AccessResolver` (CanRead, CanPost, CanManage, CanManageContext), `ForumService.RegisterContext(alias, legacyPHPClass)`, events `PostSaving`, `contracts.Rejection` (plan 01).
|
||||
chatbots: `contracts.SlashCommand{Trigger, Description, IsAvailable, Handle}`, `classes.Ephemeral(payload)`, `(*classes.Registry).Register` (help is reserved), Registry published in chatbots' OnDatabase hook.
|
||||
moderation: `*classes.Filter` published at boot, `Check(ctx, text) (*Violation, error)` (plan 01).
|
||||
chatvideo config keys (config/config.yaml of the plugin): livekit_url, livekit_api_key, livekit_api_secret, max_duration, token_ttl, livekit_admin_url, max_participants, screen_share; env `SUMMER_GOLEM15__CHATVIDEO__*` or PHP names `LIVEKIT_*`.
|
||||
sm-demo-app today: plugins/demo/plugin.go (Requires, Boot calls wireRealtime), routes.go (`APIPrefix`, `mountRealtime` limited to presentations with `presentations.realtime-owner`), console/seed.go (`runSeed` step table, `created`, `insertID`, `exists`), console/reset.go (`resetTables`, children first), boot_test.go (`wantPlugins`, routed prefixes), realtime_test.go (`TestRealtimeRoutes`, `wantNamespaces`), testmain_test.go (`bootDemo`), go_mod_test.go, scripts/dev.sh (`ensure_env`, `start_centrifugo`, `claim_port`, `up`/`down`/`status`), deploy/centrifugo.json.example (namespaces array with `subscribe_proxy_enabled`).
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: Tracer: a registered member reads the chat bootstrap and posts in the seeded #general channel through the chat API</name>
|
||||
<precondition>The plan 01 SUMMARY lists the eight tags and `git ls-remote --tags git@git.golem15.com:golem15/sm-chat-plugin.git` shows the chat tag; `git -C ../sm-demo-app status --porcelain` shows nothing except ` M plugins/golem15/oauthserver`; `docker info` exits 0.</precondition>
|
||||
<files>../sm-demo-app/plugins/golem15/oauthserver, ../sm-demo-app/.gitmodules, ../sm-demo-app/go.work, ../sm-demo-app/go.mod, ../sm-demo-app/go.sum, ../sm-demo-app/summer.yaml, ../sm-demo-app/plugins.gen.go, ../sm-demo-app/plugins/golem15/chat, ../sm-demo-app/plugins/golem15/chatchannels, ../sm-demo-app/plugins/demo/plugin.go, ../sm-demo-app/plugins/demo/chat.go, ../sm-demo-app/plugins/demo/classes/chat_context.go, ../sm-demo-app/plugins/demo/chat_api.go, ../sm-demo-app/plugins/demo/routes.go, ../sm-demo-app/plugins/demo/console/seed.go, ../sm-demo-app/plugins/demo/console/seed_chat.go, ../sm-demo-app/boot_test.go, ../sm-demo-app/chat_smoke_test.go, .planning/todos/pending/2026-10-26-sm-demo-app-journal-oauthserver-pointers.md, .planning/todos/done/2026-10-26-sm-demo-app-journal-oauthserver-pointers.md</files>
|
||||
<read_first>../sm-demo-app/summer.yaml, go.mod, go.work, .gitmodules, plugins.gen.go, `git -C ../sm-demo-app show 9451b31 --stat` (mount commit shape), ../sm-demo-app/plugins/demo/plugin.go, routes.go, console/seed.go, ../sm-demo-app/boot_test.go, testmain_test.go, internal/demotest/postgres.go, ../sm-bm-app/plugins/jz/bm/chat.go, classes/chat_context.go, controllers/api/chat.go, ../sm-demo-app/plugins/golem15/chat/contracts/*.go and classes/writes.go (GetGroupChat, AddUserToGroupChat) after mounting, ../sm-demo-app/plugins/golem15/chatchannels/models/channel.go, classes/channel_service.go (CreateChannel, JoinChannel), contracts/*.go, ../sm-demo-app/scripts/check-pins.sh, the folded todo, .planning/G15OFFICE.md</read_first>
|
||||
<action>Per D-06, D-07, SC-1 and the folded todo.
|
||||
|
||||
1. Folded todo first:
|
||||
- `git -C ../sm-demo-app/plugins/golem15/oauthserver fetch --tags`, confirm `git merge-base --is-ancestor b6a9586 v0.1.3`, then check out v0.1.3;
|
||||
- in ../sm-demo-app run `GOWORK=off GOPRIVATE=git.golem15.com/* go get git.golem15.com/golem15/sm-oauthserver-plugin@v0.1.3` and `go mod tidy`;
|
||||
- commit the gitlink with go.mod and go.sum as `chore: bump sm-oauthserver-plugin to v0.1.3`.
|
||||
In summercms.go, `git mv` the todo to .planning/todos/done/, add a "Done in Phase 20 plan 02" line, and commit it as a planning commit. Move g15office task ssuyq2os to Live with a one-line comment, as .planning/G15OFFICE.md describes.
|
||||
2. Mount chat and chatchannels at their plan 01 tags:
|
||||
- `git submodule add -b main git@git.golem15.com:golem15/sm-<name>-plugin.git plugins/golem15/<name>`, then check out the tag;
|
||||
- add both to go.work `use`, and run `GOWORK=off go get <module>@<tag>` plus `go mod tidy`;
|
||||
- add summer.yaml entries after golem15.user (chat, then chatchannels) and before golem15.demo;
|
||||
- regenerate plugins.gen.go the way 9451b31 did (the summer build generator);
|
||||
- append the IDs to boot_test.go wantPlugins and `/_chat/api/v1` and `/_chatchannels/api/v1` to its routed prefixes.
|
||||
One `feat: mount golem15.<id> <tag>` commit per plugin.
|
||||
3. plugins/demo/classes/chat_context.go, ported from bm's chat_context.go:
|
||||
- constants ChatContextType "demo", ChatContextID 1, ChatGroupName "General", ChatBroadcastChannel "chat:context:1", and `ChannelBroadcastChannel(id)` returning "chat:channel:{id}";
|
||||
- `MemberIDs(ctx, db)` and `IsMember(ctx, db, userID)`: users that are activated, have no deleted_at and are not banned (use the user plugin's own ban column; read its models);
|
||||
- DemoContext and ChannelContext implement chatcontracts.Context, MessageModerator and LiveMembership. CanModerateMessage is true only for a member of the moderator group in a non-DM conversation. RequireLiveMembership returns true;
|
||||
- ChatAccess (chancontracts.AccessResolver): CanAccessContext is true for a member when the type is "demo" and the id is 1; CanManageContext is always false (D-07);
|
||||
- keep bm's `WithMembership` and `MemberFunc` injection so tests can stub membership.
|
||||
4. plugins/demo/chat.go `registerChat(app)`, called from Boot after wireRealtime:
|
||||
- with the HasPlugin guard, register alias demo (loader returns DemoContext for id 1 and the unknown-context error otherwise) and alias `chanmodels.ChannelAlias` with `LegacyPHPClass: chanmodels.ChannelMorphName` (loader reads the channel row by id and returns ChannelContext);
|
||||
- in lagoon.OnDatabase, install ChatAccess on the ChannelService.
|
||||
Requires() gains golem15.chat and golem15.chatchannels. Config keys go into plugins/demo/config/config.yaml in Task 2.
|
||||
5. console/seed_chat.go: a step `chat channels and history (en)` that, in this task, creates only #general in context demo/1 (channel row plus backing conversation, exactly the rows CreateChannel writes, through a seed-only resolver or raw SQL) and one welcome message from the first admin or a persona. It is idempotent by slug. Add it to the runSeed step table.
|
||||
6. plugins/demo/chat_api.go: `GET /_demo/api/v1/chat` behind `jwt.auth` and `throttle:120,1`, mounted from Routes. For a member it answers `{member: true, moderator, context: {type: "demo", id: 1}, channels: [{id, slug, name, conversation_id, joined, unread, mentions}], conversations: [DMs as bm's chatConversationJSON], limits: {max_files, max_bytes, max_embeds}}`. For a non-member it answers `{member: false}`. Errors use `wire.WriteJSON` with `{"error": ...}`.
|
||||
7. chat_smoke_test.go `TestDemoChatTracer` (Docker): bootDemo, run demo:seed, create and activate a user, then with its JWT:
|
||||
- GET /_demo/api/v1/chat shows #general with a conversation_id;
|
||||
- GET /_chat/api/v1/contexts/channel/{channelId}/conversation answers 200;
|
||||
- POST /_chat/api/v1/conversations/{conversation_id}/messages answers 201;
|
||||
- GET .../messages contains the text;
|
||||
- POST /_chatchannels/api/v1/contexts/demo/1/channels is refused (403 or 404).
|
||||
If the lazy join does not make the user a participant, implement the AddUserToGroupChat fallback in the bootstrap and record it.
|
||||
|
||||
Commit the host wiring and the test as `feat: demo chat context, channel adapter and bootstrap`.</action>
|
||||
<verify>
|
||||
<automated>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 -run '^(TestDemoChatTracer|TestGoModRequiresTags|TestBootAllPlugins)$' -v . 2>&1 | grep -c -- '^--- PASS: Test' | grep -qx 3 && ../sm-demo-app/scripts/check-pins.sh</automated>
|
||||
<fails_when>Non-zero exit: vet or the short suite red; fewer than three "--- PASS" lines (a replace or pseudo-version in go.mod, a plugin not activated, a missing route prefix, the member cannot post, or the channel create was allowed); or check-pins prints a mismatch line instead of "check-pins: ok".</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `git -C ../sm-demo-app log --oneline -6` shows `chore: bump sm-oauthserver-plugin to v0.1.3`, `feat: mount golem15.chat`, `feat: mount golem15.chatchannels` and the host commit, each separate.
|
||||
- `git -C ../sm-demo-app submodule status plugins/golem15/oauthserver plugins/golem15/chat plugins/golem15/chatchannels` shows no leading `+` and each describes as its tag.
|
||||
- `test -f .planning/todos/done/2026-10-26-sm-demo-app-journal-oauthserver-pointers.md && ! test -e .planning/todos/pending/2026-10-26-sm-demo-app-journal-oauthserver-pointers.md` exits 0.
|
||||
- `grep -c 'Alias: *chanmodels.ChannelAlias' ../sm-demo-app/plugins/demo/chat.go` prints 1.
|
||||
</acceptance_criteria>
|
||||
<done>A demo member can open the seeded #general channel and post in it through the shared chat API, from a build that requires every module by tag.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: The rest of the family is mounted and wired: forum, friends, slash commands, video config, herald, and moderation refusing banned content</name>
|
||||
<files>../sm-demo-app/.gitmodules, ../sm-demo-app/go.work, ../sm-demo-app/go.mod, ../sm-demo-app/go.sum, ../sm-demo-app/summer.yaml, ../sm-demo-app/plugins.gen.go, ../sm-demo-app/plugins/golem15/chatbots, ../sm-demo-app/plugins/golem15/chatvideo, ../sm-demo-app/plugins/golem15/forum, ../sm-demo-app/plugins/golem15/herald, ../sm-demo-app/plugins/golem15/userfriends, ../sm-demo-app/plugins/golem15/moderation, ../sm-demo-app/config/golem15.chatvideo.yaml, ../sm-demo-app/plugins/demo/plugin.go, ../sm-demo-app/plugins/demo/config/config.yaml, ../sm-demo-app/plugins/demo/chat.go, ../sm-demo-app/plugins/demo/chatbots.go, ../sm-demo-app/plugins/demo/moderation.go, ../sm-demo-app/plugins/demo/classes/forum_access.go, ../sm-demo-app/boot_test.go, ../sm-demo-app/chat_smoke_test.go</files>
|
||||
<read_first>../sm-demo-app/plugins/golem15/forum/README.md, boot.go, contracts/access_resolver.go, ../sm-demo-app/plugins/golem15/chatbots/README.md, contracts/slash_command.go, classes/registry.go, classes/result.go, boot.go, ../sm-demo-app/plugins/golem15/chatvideo/README.md and config/config.yaml, ../sm-demo-app/plugins/golem15/herald/README.md and config/config.yaml, ../sm-demo-app/plugins/golem15/userfriends/README.md, ../sm-demo-app/plugins/golem15/moderation/README.md and classes/filter.go, ../sm-demo-app/plugins/golem15/chat/events/events.go and contracts/rejection.go, ../sm-demo-app/plugins/golem15/forum/events/events.go and contracts/rejection.go, ../sm-demo-app/plugins/demo/chat.go (Task 1), ../sm-bm-app/plugins/jz/bm/chat.go (config key block)</read_first>
|
||||
<action>Per D-05, D-08, D-12, D-14, D-17, D-22, D-23, SC-1.
|
||||
|
||||
1. Mount chatbots, chatvideo, forum, herald, userfriends and moderation at their plan 01 tags as in Task 1, one `feat: mount golem15.<id> <tag>` commit each. Order in summer.yaml: chatbots and chatvideo after chatchannels; forum, herald and userfriends after them (userfriends after chat, so it sees the chat registry at Boot); moderation; all before golem15.demo. Extend boot_test.go wantPlugins and its prefixes: /_chatbots/api/v1, /_chatvideo/api/v1, /_forum/api/v1, /_herald/api/v1, /_userfriends/api/v1 and /_demo/api/v1/chat.
|
||||
2. config/golem15.chatvideo.yaml (D-12, D-23): livekit_url "ws://127.0.0.1:7890", livekit_admin_url "http://127.0.0.1:7890", max_duration 900, max_participants 4, token_ttl 900, screen_share false. API key and secret come only from the environment. Herald gets no committed config: its keys come from .env (plan 04).
|
||||
3. plugins/demo/config/config.yaml: `chat: {moderator_group: demo-moderators, max_embeds: 3}`. chat.go reads them as in bm's config key block and reports the limits in the bootstrap (max_bytes from the chat service's MaxMessageBytes).
|
||||
4. Forum (D-14, D-22): classes/forum_access.go `ForumAccess`. CanRead and CanPost are true for a member when the context is demo/1; CanManage and CanManageContext are true for members of the moderator group. registerChat looks up `*forum.Plugin` and calls SetAccessResolver, and registers the context alias demo with the forum service when its API requires registration (check ListCategories on an unregistered alias).
|
||||
5. DMs (D-08): nothing to add beyond DemoContext.HasMember. Verify in the smoke test that two members open a DM with POST /_chat/api/v1/contexts/demo/1/dm.
|
||||
6. chatbots.go (D-05): `/demo` and `/tour` implement contracts.SlashCommand with IsAvailable true for any non-zero actor and Handle returning classes.Ephemeral with a short text (/tour lists /blog, /presentations, /quizzes, /subscriptions, /faq, /chat, /forum, /friends). Register them in lagoon.OnDatabase after chatbots published its Registry; a missing registry is a boot error naming the summer.yaml order.
|
||||
7. moderation.go (D-17): in Boot, `app.Events.Listen[*chatevents.MessageSaving](p.ID(), ...)` looks up `*modclasses.Filter` per event and returns `chatcontracts.Rejection{Field: "content", Message: v.Message}` when Check finds a violation. `app.Events.Listen[*forumevents.PostSaving]` checks Title (Field "title"), then Body (Field "body"), returning `forumcontracts.Rejection`. A Check error is returned as is (500). A missing filter is a boot error, because golem15.demo requires golem15.moderation.
|
||||
8. plugin.go Requires() lists every plugin whose tables the seed or reset writes or that the demo wires: chat, chatchannels, chatbots, chatvideo, forum, herald, userfriends, moderation, plus the existing ones. The package doc mentions the chat host.
|
||||
9. chat_smoke_test.go `TestDemoModerationSmoke` (Docker): seed, then set golem15_moderation_settings banned_words to "blockedword" and banned_links to "bad.example". A member's message "this has blockedword" in #general answers 422 errors.content; a forum thread titled "visit bad.example" in a seeded category answers 422 errors.title; a clean message answers 201. Also assert the DM open from step 5 and that GET /_chatbots/api/v1/commands lists demo and tour.
|
||||
|
||||
Commit wiring (`feat: demo hosts forum, friends, slash commands and moderation`) separately from the six mount commits.</action>
|
||||
<verify>
|
||||
<automated>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 -run '^(TestDemoModerationSmoke|TestBootAllPlugins|TestGoModRequiresTags|TestDemoChatTracer)$' -v . 2>&1 | grep -c -- '^--- PASS: Test' | grep -qx 4 && ../sm-demo-app/scripts/check-pins.sh && ../sm-demo-app/scripts/build.sh dev</automated>
|
||||
<fails_when>Non-zero exit: vet or short suite red; fewer than four "--- PASS" lines (banned content stored, a plugin not activated, a prefix not routed, a go.mod rule broken); check-pins reports a mismatch; or the dev build fails.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -cE '^ - id: golem15\.(chat|chatchannels|chatbots|chatvideo|forum|herald|userfriends|moderation)$' ../sm-demo-app/summer.yaml` prints 8.
|
||||
- `grep -c 'max_participants: 4' ../sm-demo-app/config/golem15.chatvideo.yaml` prints 1 and `grep -c 'max_duration: 900' ../sm-demo-app/config/golem15.chatvideo.yaml` prints 1.
|
||||
- `grep -rn 'livekit_api_secret: *"[^"]' ../sm-demo-app/config` prints nothing (no committed secret).
|
||||
- `git -C ../sm-demo-app status --porcelain -- plugins/golem15` prints nothing (submodules clean, on tags).
|
||||
</acceptance_criteria>
|
||||
<done>Every chat-family plugin boots in the demo with its host policy; banned words and links are refused in chat and forum; slash commands answer; video is configured with the D-12 caps.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: demo:seed fills chat, forum and friends with EN and PL showcase data, and demo:reset wipes and reseeds all of it</name>
|
||||
<files>../sm-demo-app/plugins/demo/console/seed.go, ../sm-demo-app/plugins/demo/console/seed_chat.go, ../sm-demo-app/plugins/demo/console/seed_forum.go, ../sm-demo-app/plugins/demo/console/seed_friends.go, ../sm-demo-app/plugins/demo/console/reset.go, ../sm-demo-app/chat_smoke_test.go</files>
|
||||
<read_first>../sm-demo-app/plugins/demo/console/seed.go (step table, created, insertID, exists, visitor), seed_locales.go, reset.go (resetTables order and comments), ../sm-demo-app/reset_test.go and seed_test.go, the baseline migrations of every mounted chat-family plugin (../sm-demo-app/plugins/golem15/{chat,chatchannels,chatbots,chatvideo,forum,herald,userfriends}/updates/*.go) for table names and foreign keys, ../sm-demo-app/plugins/golem15/chatchannels/models (pins, mentions, members, prefs), ../sm-demo-app/plugins/golem15/chat/models (message, reaction, thread summary), modules/breakwater/README.md, 20-RESEARCH.md (Pitfall 10, Anti-Patterns)</read_first>
|
||||
<action>Per D-09, D-14, D-15 (data), D-22, D-17.
|
||||
|
||||
- seed_chat.go:
|
||||
- `chat personas`: Ada, Ben, Ola and Tomek, as decided above. Each is activated, has a random password hash that is never printed, and is skipped when the email exists.
|
||||
- `chat channels and history (en)`: #general and #announcements, with English history from the personas, two threads (root plus 2 to 4 replies, keeping chat's thread summary rows consistent the way the service writes them), reactions on a few messages using the keys chat allows (GET /reactions catalog in the plugin), and one pin per channel in chatchannels' pins table.
|
||||
- `chat channels and history (pl)`: #ogólny (slug ogolny) with Polish history, a thread, reactions and a pin.
|
||||
- A welcome DM from Ada to the visitor when golem15.demo.visitor.email is set (skipped otherwise).
|
||||
- Rows are written in created order with timestamps spread over the last few days. Messages are inserted directly: no realtime, no jobs, no events. Steps are idempotent by channel slug and persona email.
|
||||
- seed_forum.go `forum categories and threads (en, pl)`: categories "General discussion" and "Show and tell" (EN) and "Rozmowy" (PL) in context demo/1, two or three threads each with replies by personas. content is Markdown and content_html comes from `breakwater.Render`. Idempotent by category slug.
|
||||
- seed_friends.go `friends and group`: accepted friendships among the personas, one pending request from Ola to the visitor when set, and a friend group "Demo crew" owned by Ada with Ben and Tomek as members. Use userfriends' tables and the context singleton id 1. Idempotent.
|
||||
- seed.go: steps `moderator group demo-moderators` (a user_groups row with the configured code and name "Demo moderators", no members) and `moderation lists` (when banned_words and banned_links are both empty, fill a short, mild default list of common spam words and one link-shortener domain; otherwise "exists"). Order the new steps after the visitor and before the roadmap decks.
|
||||
- reset.go `resetTables` (RESEARCH Pitfall 10), children first, all before `users`:
|
||||
- chatvideo participants, then sessions;
|
||||
- chat reactions, thread mutes, thread summaries, messages, participants, conversations;
|
||||
- chatchannels mentions, pins, members, prefs, user context prefs, personal categories, channels, categories;
|
||||
- forum thread reads, posts, threads, categories;
|
||||
- userfriends group bans, invites, group members, groups, friendships;
|
||||
- herald push subscriptions;
|
||||
- chatbots bot flags.
|
||||
Use the exact table names from the migrations. Keep golem15_moderation_settings and golem15_userfriends_contexts. Update the comment and the command description. Remove the chat-family uploads (chat attachments in system_files) the same way the reset already removes other owners' attachments, keeping resetKeptAttachments.
|
||||
- chat_smoke_test.go `TestSeedChatSmoke` (Docker): seed with a visitor email.
|
||||
- Three channels exist, #ogólny holds Polish text, at least one thread root has replies, at least one reaction and one pin exist, the forum has three categories with posts whose content_html is non-empty, there are at least three accepted friendships and the group "Demo crew", and the visitor has a DM.
|
||||
- A second seed run reports every new step as "exists".
|
||||
- demo:reset --force empties golem15_chat_messages before reseeding and leaves golem15_moderation_settings unchanged.
|
||||
|
||||
Commit `feat: seed and reset the chat family (EN/PL)`.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-demo-app vet . ./plugins/demo/... && go -C ../sm-demo-app test -count=1 -run '^(TestSeedChatSmoke|TestResetKeepsPlans|TestSeedRoadmapUpsert)$' -v . 2>&1 | grep -c -- '^--- PASS: Test' | grep -qx 3</automated>
|
||||
<fails_when>Non-zero exit or fewer than three "--- PASS" lines: seed not idempotent, a missing EN/PL item, a foreign-key violation in the reset order, moderation settings wiped, or the existing reset and roadmap tests broken.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c 'golem15_chat_messages' ../sm-demo-app/plugins/demo/console/reset.go` prints at least 1, and TestSeedChatSmoke asserts the moderation settings row is identical before and after demo:reset --force.
|
||||
- `grep -c 'personas.demo.invalid' ../sm-demo-app/plugins/demo/console/seed_chat.go` prints at least 1.
|
||||
- Running `../sm-demo-app/scripts/dev.sh seed` twice against the local stack prints no "created" for the new steps on the second run.
|
||||
</acceptance_criteria>
|
||||
<done>A fresh or reset demo shows lived-in EN and PL channels, threads, reactions, pins, forum threads, friendships and a friend group, and the reset job keeps it that way.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Every member gets realtime, the layers get their members endpoint, and dev runs Centrifugo namespaces and a local LiveKit</name>
|
||||
<files>../sm-demo-app/plugins/demo/realtime_access.go, ../sm-demo-app/plugins/demo/routes.go, ../sm-demo-app/plugins/demo/plugin.go, ../sm-demo-app/plugins/demo/chat_api.go, ../sm-demo-app/realtime_test.go, ../sm-demo-app/deploy/centrifugo.json.example, ../sm-demo-app/deploy/livekit.yaml.example, ../sm-demo-app/deploy/env.example, ../sm-demo-app/scripts/dev.sh, ../sm-demo-app/README.md</files>
|
||||
<read_first>../sm-bm-app/plugins/jz/bm/realtime_access.go, ../sm-demo-app/plugins/demo/routes.go (mountRealtime), realtime.go, ../sm-demo-app/realtime_test.go (TestRealtimeRoutes, wantNamespaces, realtimeApp), ../sm-bm-app/deploy/centrifugo/bm-config.json (chat, user and video namespace options), ../sm-demo-app/deploy/centrifugo.json.example, ../sm-demo-app/scripts/dev.sh (ensure_env, start_centrifugo, claim_port, up, down, status, the usage header), ../sm-demo-app/deploy/env.example, ../sm-demo-app/README.md, ../sm-demo-app/plugins/golem15/chatvideo/README.md (keys, admin URL, TrustedMode), 20-RESEARCH.md (Pitfalls 4, 5, 7; LiveKit config block)</read_first>
|
||||
<action>Per D-10 (locally), D-06, SC-1.
|
||||
|
||||
1. realtime_access.go: `MiddlewareRealtimeAccess = "demo.realtime-access"`, exported through `Middlewares() map[string]pact.Middleware` (assert pact.HasMiddleware in plugin.go). The guard reads `bouncer.User(r.Context())`: no user gives 401, a database not yet published gives 503, and a user for whom classes.IsMember is true passes. Otherwise it falls back to the presentations owner guard looked up once (sync.Once) by name, as bm does. routes.go mountRealtime mounts when golem15.presentations or golem15.chat is active, with `UserAuth: surf.Use("jwt.auth", MiddlewareRealtimeAccess, bucketRealtime)`. Update its doc comment.
|
||||
2. realtime_test.go: the token route's expected middleware becomes ["jwt.auth", "demo.realtime-access", "throttle:golem15.demo.realtime"], and wantNamespaces gains the namespaces the chat, chatvideo and user plugins register. Plan 06 adds the member/non-member cases.
|
||||
3. chat_api.go `GET /_demo/api/v1/chat/members` (jwt.auth, throttle:120,1) answers `{members: [{id, name, surname, handle, is_bot}]}` for every member except the caller, as bm's ChatMembers does (handle from the user plugin's username or a slug of the name; is_bot false). A non-member gets 403.
|
||||
4. deploy/centrifugo.json.example: add namespaces `chat` (presence, join_leave, force_push_join_leave, allow_presence_for_subscriber and subscribe_proxy_enabled, with the option names bm's config uses translated to this file's v6 format), `user` (subscribe_proxy_enabled) and `video` (subscribe_proxy_enabled). Run `centrifugo checkconfig -c deploy/centrifugo.json.example`, then diff the option names against `centrifugo defaultconfig`, because checkconfig misses misspelt names. Update the _comment lines.
|
||||
5. deploy/livekit.yaml.example (RESEARCH LiveKit block):
|
||||
- port 7890, bind_addresses [127.0.0.1];
|
||||
- rtc tcp_port 7891, udp_port 7892 (UDP mux only, no port-range keys), use_external_ip true and a comment that local runs set it false;
|
||||
- room max_participants 4, empty_timeout 120, departure_timeout 20;
|
||||
- keys with one placeholder pair (CHANGE_ME_KEY: CHANGE_ME_SECRET);
|
||||
- a header comment naming rome's path /srv/summercms-demo/livekit.yaml (0640 root:summercms-demo), the supervisor program plan 05 adds, and the env keys.
|
||||
6. scripts/dev.sh:
|
||||
- LIVEKIT=${LIVEKIT:-$(command -v livekit-server || true)};
|
||||
- ensure_env appends SUMMER_GOLEM15__CHATVIDEO__LIVEKIT_API_KEY and SUMMER_GOLEM15__CHATVIDEO__LIVEKIT_API_SECRET (random, umask 077, never echoed) when missing;
|
||||
- `livekit_config` writes logs/livekit.yaml from the example with the .env keys and use_external_ip false;
|
||||
- `start_livekit` claims ports 7890 and 7891, starts `livekit-server --config logs/livekit.yaml` under the pid name livekit with output to logs/livekit.log, waits for http://127.0.0.1:7890/ and warns and continues without video when the binary is missing;
|
||||
- up starts it after Centrifugo, down, destroy and status include livekit, and the header usage lines mention it.
|
||||
7. deploy/env.example: the two LiveKit key names with empty values and a comment. README.md: a "Chat family" section covering the mounted plugins, the contexts, the bootstrap contract of /_demo/api/v1/chat and /chat/members, the seed data, the moderator group, slash commands and local LiveKit.
|
||||
|
||||
Commit `feat: realtime for every demo member, members endpoint, Centrifugo namespaces and local LiveKit`.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-demo-app vet . ./plugins/demo/... && go -C ../sm-demo-app test -count=1 -short . ./plugins/demo/... && centrifugo checkconfig -c ../sm-demo-app/deploy/centrifugo.json.example && ../sm-demo-app/scripts/dev.sh up && curl -fsS http://127.0.0.1:7890/ >/dev/null && ../sm-demo-app/scripts/check-local.sh realtime</automated>
|
||||
<fails_when>Non-zero exit: vet or the short suite red (TestRealtimeRoutes middleware or namespaces), checkconfig refuses the Centrifugo file, dev.sh up fails to start a service, LiveKit does not answer on 127.0.0.1:7890, or check-local realtime reports a non-200 token for the visitor or a token without a JWT.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c '"name": "chat"' ../sm-demo-app/deploy/centrifugo.json.example` prints 1, and the same for "user" and "video".
|
||||
- `grep -v '^\s*#' ../sm-demo-app/deploy/livekit.yaml.example | grep -cE 'port_range_(start|end)'` prints 0, and `grep -c 'udp_port: 7892' ../sm-demo-app/deploy/livekit.yaml.example` prints 1.
|
||||
- With the local stack up, `curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $VISITOR_JWT" http://127.0.0.1:8097/api/realtime/token` prints 200 (JWT from the login API).
|
||||
- `grep -c 'LIVEKIT_API_SECRET=' ../sm-demo-app/deploy/env.example` prints 1 and that line has no value after the equals sign.
|
||||
</acceptance_criteria>
|
||||
<done>Every demo member gets a realtime token and the member list, Centrifugo knows the chat namespaces, and a local LiveKit runs with the dev stack.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| browser to /_demo, /_chat, /_chatchannels, /_forum, /_userfriends, /_chatbots, /_chatvideo | every request untrusted; actor only from the JWT |
|
||||
| Centrifugo subscribe proxy to Go | channel names come from clients; authorizers decide |
|
||||
| seed and reset commands to the database | trusted operator commands; persona accounts must stay unusable |
|
||||
| Go to LiveKit RoomService on loopback | admin calls with the API secret |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-20-06 | Elevation of privilege | channel creation by members | high | mitigate | ChatAccess.CanManageContext is always false; TestDemoChatTracer asserts POST channels is refused (D-07) |
|
||||
| T-20-07 | Information disclosure | subscribing to other conversations | high | mitigate | broadcast channels chat:context:1 and chat:channel:{id} are not subscribable (AuthorizeChat admits only chat:conversation:{id}); user:{id} only to itself (user plugin authorizer); video:session:{id} only to session participants |
|
||||
| T-20-08 | Spoofing | persona accounts | medium | mitigate | random password hashes never printed, emails under .invalid, so no login and no mail; reset wipes and recreates them |
|
||||
| T-20-09 | Elevation of privilege | moderator rights | medium | mitigate | moderator group membership only through the user admin (D-22); CanModerateMessage limited to non-DM conversations; banned users fail HasMember with live membership on |
|
||||
| T-20-10 | Tampering | spam and abuse in public chat and forum | high | mitigate | moderation listeners on both seams (TestDemoModerationSmoke), plugin buckets (chat-api, chat-dm, forum-api, video-initiate), Turnstile on register (19.4.1), reset job |
|
||||
| T-20-11 | Denial of service | realtime token minting for every member | low | mitigate | guard needs a JWT of a live member, per-user bucket golem15.demo.realtime 120/min unchanged |
|
||||
| T-20-12 | Information disclosure | LiveKit and VAPID secrets | high | mitigate | values only in .env (dev.sh writes them with umask 077, never echoed); env.example holds names; config files hold no secret (acceptance grep) |
|
||||
| T-20-13 | Denial of service | stale chat-family rows after reset | low | mitigate | reset table list in FK order covering every chat-family table; TestSeedChatSmoke checks messages are emptied |
|
||||
| T-20-SC | Tampering | npm/pip/cargo installs | high | mitigate | no package-manager install in this plan; Go modules are the project's tagged plugins from plan 01 (go.sum committed, GOPRIVATE) |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- Task 1: tracer test, go.mod tags, boot test, check-pins.
|
||||
- Task 2: moderation smoke, boot and go.mod tests, check-pins, dev build.
|
||||
- Task 3: seed smoke, reset and roadmap tests.
|
||||
- Task 4: short suite, Centrifugo checkconfig, dev stack up with LiveKit, check-local realtime.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
sm-demo-app builds with every shared plugin including the chat family, and demo:seed fills each with EN/PL showcase data (SC-1). The demo hosts one community where every member can chat, DM, post in the forum and befriend others, with moderation, slash commands and video configured (D-05 to D-09, D-12, D-14, D-17, D-22, D-23).
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/20-demo-instance-with-every-shared-plugin/20-02-SUMMARY.md` when done: mount commits with tags, which channel join path holds (lazy join or bootstrap), persona and channel names, the reset table order, the Centrifugo option names used, and any boot warnings left by plugins (for example herald's presence gate).
|
||||
</output>
|
||||
@@ -0,0 +1,446 @@
|
||||
---
|
||||
phase: 20-demo-instance-with-every-shared-plugin
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: ["20-02"]
|
||||
files_modified:
|
||||
- ../vue-chat-layer/.gitignore
|
||||
- ../vue-chat-layer/.nvmrc
|
||||
- ../vue-chat-layer/package.json
|
||||
- ../vue-chat-layer/pnpm-lock.yaml
|
||||
- ../vue-chat-layer/pnpm-workspace.yaml
|
||||
- ../vue-chat-layer/tsconfig.json
|
||||
- ../vue-chat-layer/nuxt.config.ts
|
||||
- ../vue-chat-layer/vitest.config.ts
|
||||
- ../vue-chat-layer/README.md
|
||||
- ../vue-chat-layer/.playground/nuxt.config.ts
|
||||
- ../vue-chat-layer/.playground/app/app.vue
|
||||
- ../vue-chat-layer/app/lib/client.ts
|
||||
- ../vue-chat-layer/app/lib/types.ts
|
||||
- ../vue-chat-layer/app/lib/realtime.ts
|
||||
- ../vue-chat-layer/app/lib/chatEvents.ts
|
||||
- ../vue-chat-layer/app/lib/chatFormat.ts
|
||||
- ../vue-chat-layer/app/lib/chatText.ts
|
||||
- ../vue-chat-layer/app/lib/chatUnread.ts
|
||||
- ../vue-chat-layer/app/lib/linkEmbeds.ts
|
||||
- ../vue-chat-layer/app/lib/format.ts
|
||||
- ../vue-chat-layer/app/lib/media.ts
|
||||
- ../vue-chat-layer/app/lib/tabs.ts
|
||||
- ../vue-chat-layer/app/composables/useChatApi.ts
|
||||
- ../vue-chat-layer/app/composables/useChat.ts
|
||||
- ../vue-chat-layer/app/composables/useChatNotice.ts
|
||||
- ../vue-chat-layer/app/components/Chat/Page.vue
|
||||
- ../vue-chat-layer/app/components/Chat/ChannelList.vue
|
||||
- ../vue-chat-layer/app/components/Chat/Panel.vue
|
||||
- ../vue-chat-layer/app/components/Chat/MessageList.vue
|
||||
- ../vue-chat-layer/app/components/Chat/Message.vue
|
||||
- ../vue-chat-layer/app/components/Chat/Composer.vue
|
||||
- ../vue-chat-layer/app/components/Chat/Header.vue
|
||||
- ../vue-chat-layer/app/components/Chat/DmList.vue
|
||||
- ../vue-chat-layer/app/components/Chat/MemberPicker.vue
|
||||
- ../vue-chat-layer/app/components/Chat/MentionPopup.vue
|
||||
- ../vue-chat-layer/app/components/Chat/ReactionBar.vue
|
||||
- ../vue-chat-layer/app/components/Chat/LinkEmbed.vue
|
||||
- ../vue-chat-layer/app/components/Chat/StagingChips.vue
|
||||
- ../vue-chat-layer/app/components/Chat/DeleteDialog.vue
|
||||
- ../vue-chat-layer/app/components/Chat/Avatar.vue
|
||||
- ../vue-chat-layer/app/components/Chat/ThreadPane.vue
|
||||
- ../vue-chat-layer/app/components/Chat/PinsPanel.vue
|
||||
- ../vue-chat-layer/app/components/Chat/CommandPicker.vue
|
||||
- ../vue-chat-layer/app/components/Chat/FloatingPanel.vue
|
||||
- ../vue-chat-layer/app/components/Chat/Icon.vue
|
||||
- ../vue-chat-layer/app/components/Chat/Notices.vue
|
||||
- ../vue-chat-layer/app/assets/css/chat-tokens.css
|
||||
- ../vue-chat-layer/app/assets/css/chat-base.css
|
||||
- ../vue-chat-layer/i18n/locales/en.json
|
||||
- ../vue-chat-layer/i18n/locales/pl.json
|
||||
- ../vue-chat-layer/tests/smoke.test.ts
|
||||
- ../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/plugins/auth.ts
|
||||
- ../sm-demo-app/vue-demo-app/app/layouts/default.vue
|
||||
- ../sm-demo-app/vue-demo-app/app/pages/chat.vue
|
||||
- ../sm-demo-app/vue-demo-app/app/pages/index.vue
|
||||
- ../sm-demo-app/vue-demo-app/app/components/TourCard.vue
|
||||
- ../sm-demo-app/vue-demo-app/app/lib/realtime.ts
|
||||
- ../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-chat.cjs
|
||||
- ../sm-demo-app/scripts/check-local.sh
|
||||
- ../sm-demo-app/scripts/smoke.sh
|
||||
autonomous: false
|
||||
requirements: [SC-2, D-01, D-02, D-03, D-04, D-05, D-07, D-20, D-22, D-23]
|
||||
estimate:
|
||||
tokens: 380000
|
||||
raw_tokens: 380000
|
||||
tasks: 6
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "D-01: vue-chat-layer is a new shared Nuxt layer (own repository, tag v0.1.0) ported from the bm SPA's chat components, store, API client and chat libraries, with no Pinia (state is useState composables), no bm entity embeds and no name of a consuming application; host specifics enter only through runtime config (public.chat.hostApiBase), the $chatAuth hook, slots and the link-embed renderer registry."
|
||||
- "D-02: on /chat a logged-in member sees the seeded channels in a sidebar, opens one, reads history, sends and edits messages, replies in threads (thread pane), sees typing, unread and mention counts, adds and removes reactions, attaches files, mentions members with @, picks a DM partner from the member picker, deletes own messages (moderators also others' in channels, D-22) through the delete dialog, sees channel pins and sees URLs as links with a generic link embed."
|
||||
- "D-03: every page of vue-demo-app shows a floating chat panel (DM list and unread badge) for logged-in users (not on /chat and not on the full-screen deck pages); the homepage tour has a Chat card; the site is EN and PL."
|
||||
- "D-04: an anonymous visitor on /chat sees a description and a register-to-chat link to /register?next=/chat; there is no read-only preview, and the floating panel is not rendered."
|
||||
- "D-05: typing / at the start of the composer opens a command picker filled from GET /_chatbots/api/v1/commands; choosing or sending a command calls POST /_chatbots/api/v1/dispatch and shows the ephemeral answer only to the sender; there is no chatbots layer."
|
||||
- "D-07: the layer offers no channel creation UI; members join and leave seeded channels."
|
||||
- "D-23 (extension point): the conversation header exposes a slot that the layer renders only for DM conversations, so a call button placed there (plan 04) appears only in DMs."
|
||||
- "SC-2: messages from another member arrive without reload through the shared Centrifuge client (one connection per page, shared with the presentations remote through vue-demo-app's app/lib/realtime.ts re-export)."
|
||||
- "After the Task 5 go-ahead, git.golem15.com/golem15/vue-chat-layer exists with tag v0.1.0 and vue-demo-app pins git+https://git.golem15.com/golem15/vue-chat-layer.git#v0.1.0 in a committed package.json and pnpm-lock.yaml."
|
||||
artifacts:
|
||||
- path: "../vue-chat-layer/nuxt.config.ts"
|
||||
provides: "layer config: runtimeConfig chat.apiBase, public.chat.hostApiBase, css, i18n registration (no routes, no route rules)"
|
||||
contains: "hostApiBase"
|
||||
- path: "../vue-chat-layer/app/composables/useChat.ts"
|
||||
provides: "useState-based chat store (channels, DMs, threads, unread, typing, realtime)"
|
||||
contains: "useState"
|
||||
- path: "../vue-chat-layer/app/lib/realtime.ts"
|
||||
provides: "one shared Centrifuge client with connect, subscribe, presence, openChannel and onUserEvent"
|
||||
contains: "onUserEvent"
|
||||
- path: "../vue-chat-layer/app/lib/linkEmbeds.ts"
|
||||
provides: "linkify and the link-embed renderer registry (registerEmbedRenderer)"
|
||||
contains: "registerEmbedRenderer"
|
||||
- path: "../vue-chat-layer/app/components/Chat/CommandPicker.vue"
|
||||
provides: "slash command picker over the chatbots catalog and dispatch"
|
||||
contains: "dispatch"
|
||||
- path: "../sm-demo-app/vue-demo-app/app/pages/chat.vue"
|
||||
provides: "/chat: ChatPage for members, register prompt for anonymous visitors"
|
||||
contains: "registerToChat"
|
||||
- path: "../sm-demo-app/scripts/check-chat.cjs"
|
||||
provides: "Playwright check: the visitor sends a message in #general on /chat"
|
||||
contains: "check-chat: ok"
|
||||
key_links:
|
||||
- from: "../vue-chat-layer/app/composables/useChatApi.ts"
|
||||
to: "../sm-demo-app/vue-demo-app/app/plugins/auth.ts"
|
||||
via: "the layer reads useNuxtApp().$chatAuth {token, refresh, unauthorized}; the app provides it"
|
||||
pattern: "chatAuth"
|
||||
- from: "../vue-chat-layer/app/composables/useChat.ts"
|
||||
to: "../sm-demo-app/plugins/demo/chat_api.go"
|
||||
via: "bootstrap GET {public.chat.hostApiBase}/chat and /chat/members"
|
||||
pattern: "hostApiBase"
|
||||
- from: "../sm-demo-app/vue-demo-app/app/lib/realtime.ts"
|
||||
to: "../vue-chat-layer/app/lib/realtime.ts"
|
||||
via: "re-export so presentations and chat share one connection"
|
||||
pattern: "vue-chat-layer/app/lib/realtime"
|
||||
- from: "../sm-demo-app/vue-demo-app/app/layouts/default.vue"
|
||||
to: "../vue-chat-layer/app/components/Chat/FloatingPanel.vue"
|
||||
via: "ClientOnly ChatFloatingPanel when logged in"
|
||||
pattern: "ChatFloatingPanel"
|
||||
prohibitions:
|
||||
- statement: "vue-chat-layer names no consuming application and has no default host bootstrap path."
|
||||
status: unresolved
|
||||
verification: automated
|
||||
- statement: "No message, name or attachment field is rendered with v-html except the escaped output of the ported chatText renderer."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
- statement: "No Pinia and no new npm package except those already pinned in the workspace (centrifuge 5.7.3, @nuxtjs/i18n 10.4.0, vitest 3.2.7, nuxt 4.4.8, vue 3.5.35, typescript 5.9.3)."
|
||||
status: unresolved
|
||||
verification: automated
|
||||
- statement: "Nothing is pushed and no repository is created before the Task 5 go-ahead; vue-demo-app is not pushed in this plan (plan 05)."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
ROADMAP Goal (not in user-story format; no story is invented here): the demo's public Nuxt site shows the shared plugins working, chat and channels with realtime included, through the shared layers.
|
||||
|
||||
This plan's slice (plan 3 of 6, wave 3, per D-20): `vue-chat-layer` v0.1.0, ported from bm's chat SPA and widened to several channels, threads, pins, a command picker and generic link embeds, then used in vue-demo-app as `/chat`, a floating panel and a tour card, in EN and PL.
|
||||
|
||||
## Decisions taken in this plan (Claude's discretion, recorded)
|
||||
|
||||
- **Layout of the layer:** pure modules in `app/lib/` (imported explicitly, never auto-imported, so generic names like `initials` cannot clash with an app's utils). Composables `useChat`, `useChatApi` and `useChatNotice` in `app/composables/`. Components in `app/components/Chat/`, auto-registered with the `Chat` prefix (`ChatPage`, `ChatFloatingPanel`). The layer ships components; the app owns routes, page copy and the anonymous prompt (as vue-paymentgateway-layer does).
|
||||
- **Host contract:** `runtimeConfig.public.chat.hostApiBase` defaults to an empty string, and the layer refuses to bootstrap without it, with a console error naming the key. vue-demo-app sets `/_demo/api/v1`. The README documents the `GET {hostApiBase}/chat` and `/chat/members` JSON shapes as the contract any host implements, under neutral names.
|
||||
- **Auth hook:** `$chatAuth` with `token(): string | null`, `refresh(): Promise<string | null>` and `unauthorized(): void`. One refresh-and-retry on 401 (bm's client renews through `/_user/api/v1/fetch`), then `unauthorized()`.
|
||||
- **Store:** bm's Pinia setup store becomes a module-level composable over `useState` keys prefixed `chat:` (19.4.1 no-Pinia rule). `general`/`generalId` become `channels[]` and `activeId`.
|
||||
- **Realtime:** bm's current `lib/realtime.ts` (shared client) is ported with `onUserEvent(handler)`, so other layers (chatvideo, plan 04) get `user:{me}` events they do not own without a second subscription. vue-demo-app's `app/lib/realtime.ts` becomes a re-export, so the presentations remote and chat share one WebSocket.
|
||||
- **Embeds:** bm's entity embeds and picker are dropped. URLs in text are linkified (escaped, `rel="noopener noreferrer nofollow"`, `target="_blank"`). A message with a URL shows a `ChatLinkEmbed` card with host and path; there is no unfurl backend. `registerEmbedRenderer(match, component)` lets a host render its own entity links (D-01 extension point).
|
||||
- **Extension slots:** `conversation-actions` in ChatHeader, rendered only for `type === 'dm'` (D-23), and `sidebar-footer` in ChannelList (the friends link, plan 04).
|
||||
- **Floating panel:** hidden on `/chat`, `/p/**` and `/presentations/**` (full-screen decks) and for anonymous visitors.
|
||||
- **Development pin:** while the layer has no remote, vue-demo-app depends on `link:../../vue-chat-layer`, committed with the first vue-demo-app commit so every commit builds on this machine. Task 6 replaces it with the tag pin; vue-demo-app is pushed only in plan 05, after the swap, so no pushed commit needs the local path.
|
||||
- spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip).
|
||||
|
||||
## Artifacts this phase produces (this plan)
|
||||
|
||||
- Repository `vue-chat-layer` (tag v0.1.0):
|
||||
- components `ChatPage`, `ChatChannelList`, `ChatPanel`, `ChatMessageList`, `ChatMessage`, `ChatComposer`, `ChatHeader`, `ChatDmList`, `ChatMemberPicker`, `ChatMentionPopup`, `ChatReactionBar`, `ChatLinkEmbed`, `ChatStagingChips`, `ChatDeleteDialog`, `ChatAvatar`, `ChatThreadPane`, `ChatPinsPanel`, `ChatCommandPicker`, `ChatFloatingPanel`, `ChatIcon`, `ChatNotices`;
|
||||
- composables `useChat`, `useChatApi`, `useChatNotice`;
|
||||
- lib exports: `createChatClient`, `REALTIME_WS_PATH`, `connect`, `subscribe`, `presence`, `openChannel`, `onUserEvent`, `disconnect`, `linkify`, `registerEmbedRenderer`, `embedRendererFor`, and the ported chatEvents, chatFormat, chatText and chatUnread functions;
|
||||
- runtime config `chat.apiBase` (`NUXT_CHAT_API_BASE`), `public.chat.apiBase`, `public.chat.channelsApiBase`, `public.chat.botsApiBase`, `public.chat.hostApiBase`;
|
||||
- Nuxt plugin hook `$chatAuth`; slots `conversation-actions` and `sidebar-footer`; i18n keys under `chat.*` (EN and PL);
|
||||
- smoke test `tests/smoke.test.ts`.
|
||||
- vue-demo-app: page `/chat`, the floating panel in the default layout, Chat tour card (TourCard icon `chat`), i18n keys `chat.*` and `tour.chat.*`, goPaths `/_chat`, `/_chatchannels`, `/_chatbots`, ChatPage inside ClientOnly (the page itself is server-rendered so the banner and the anonymous prompt are in the HTML), `chatAuth` provided in app/plugins/auth.ts, app/lib/realtime.ts re-exporting the layer client.
|
||||
- sm-demo-app: `scripts/check-chat.cjs`, check-local check `chat`, check-local `tour` expecting the Chat card, smoke.sh covering `/chat`.
|
||||
|
||||
<objective>
|
||||
Build and release vue-chat-layer v0.1.0 and put chat on the demo site: `/chat`, the floating panel, the Chat tour card and the anonymous prompt, in EN and PL.
|
||||
|
||||
Purpose: SC-2 (chat and channels with realtime on the public site), D-01 to D-05, D-07 (no channel creation), D-22 (moderator delete) and the D-23 extension point plan 04 uses.
|
||||
Output: a new repository vue-chat-layer (pushed and tagged after the go-ahead), commits in ../sm-demo-app/vue-demo-app (the submodule; not pushed) and ../sm-demo-app (scripts; not pushed).
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-CONTEXT.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-RESEARCH.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-PATTERNS.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-02-SUMMARY.md
|
||||
|
||||
Repos: new ../vue-chat-layer; ../sm-demo-app/vue-demo-app (the submodule, origin golem15/vue-demo-app; never the stale ../vue-demo-app sibling); ../sm-demo-app (scripts). Read-only port source: ../sm-bm-app/frontend/src (components/chat/*, stores/chat.ts and chat.test.ts, api/chat.ts, api/types.ts, lib/chat*.ts, lib/realtime.ts, lib/format.ts, lib/media.ts, lib/tabs.ts, components/Icon.vue). Layer conventions: ../vue-faq-layer and ../vue-paymentgateway-layer.
|
||||
|
||||
Rules: no co-author tags; stage by explicit path; the layer repository gets no remote before Task 5; vue-demo-app is committed in its own repository (never staged from sm-demo-app) and not pushed here; check `git -C ../sm-demo-app/vue-demo-app status --porcelain` before each commit and leave other sessions' edits alone (Phase 33 also edits nuxt.config.ts). The local dev stack runs with `../sm-demo-app/scripts/dev.sh up` (Go 127.0.0.1:8097, Nuxt 127.0.0.1:3020, Centrifugo, LiveKit). Playwright is the global install (`NODE_PATH="$(npm root -g)"`, scripts/lib/browser.cjs).
|
||||
|
||||
<interfaces>
|
||||
Host bootstrap (plan 02, ../sm-demo-app/plugins/demo/chat_api.go): GET /_demo/api/v1/chat gives {member, moderator, context: {type, id}, channels: [{id, slug, name, conversation_id, joined, unread, mentions}], conversations: [{id, type: "dm", other_user_id, unread, last_message: {id, sender_id, content, created_at}}], limits: {max_files, max_bytes, max_embeds}}; GET /_demo/api/v1/chat/members gives {members: [{id, name, surname, handle, is_bot}]}.
|
||||
chat API /_chat/api/v1: GET|POST /contexts/{type}/{id}/conversation; GET|POST /conversations/{id}/messages; POST /conversations/{id}/read, /typing, /mute; GET|PUT|DELETE /messages/{id}; GET /reactions; POST /messages/{id}/reactions; DELETE /messages/{id}/reactions/{key}; GET|POST /messages/{id}/replies; PUT /threads/{rootId}/mute; POST /attachments (multipart, body limit); GET /attachments/{messageId}/{fileId}; POST /contexts/{type}/{id}/dm. 422 envelope {"error":"Validation failed","errors":{field:[msg]}}, other refusals {"error": msg}, IDOR 404.
|
||||
chatchannels /_chatchannels/api/v1/contexts/demo/1: GET channels, POST channels/{id}/join and /leave, GET|POST channels/{id}/pins, DELETE channels/{id}/pins/{messageId}, GET channels/{id}/mentions, GET mentions.
|
||||
chatbots /_chatbots/api/v1: GET /commands gives {commands: [{trigger, description}]}; POST /dispatch {input} gives {mode, payload}; unknown gives 422 errors.input.
|
||||
Realtime: token GET /api/realtime/token; WebSocket /ws; channels user:{me} (chat:message, typing, read, unread, deleted, reacted, updated events per bm's chatEvents.ts; video:* from chatvideo), chat:conversation:{id}.
|
||||
vue-demo-app today: nuxt.config.ts (`goPaths`, `extends`, `runtimeConfig`, `routeRules`, `robots.disallow: goPaths`), app/plugins/auth.ts (`paymentGatewayAuth`), app/utils/authCookie.ts (`DEMO_TOKEN_COOKIE`), app/composables/useDemoAuth.ts (`loggedIn`), app/pages/quizzes/index.vue (anonymous prompt with `registerLink` and `quiz.registerToTake`), app/pages/index.vue (`stops`, TourCard), app/components/TourCard.vue (`icon` union, `ICONS` paths), app/lib/realtime.ts (openChannel subset of bm's file), vitest.config.ts (app/**/*.test.ts), package.json (centrifuge 5.7.3, layers pinned by git+https tag).
|
||||
Layer analogs: ../vue-faq-layer/nuxt.config.ts (currentDir, absolute paths, runtimeConfig), ../vue-paymentgateway-layer/nuxt.config.ts (i18n:registerModule for EN/PL), app/composables/usePaymentGatewayApi.ts (auth hook through useNuxtApp), package.json (main nuxt.config.ts, peerDependencies nuxt 4.4.8, engines node >=22.18).
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: Tracer: a logged-in visitor opens /chat, picks #general from the seeded channels and sends a message that appears; an anonymous visitor gets the register prompt</name>
|
||||
<reversibility rating="costly">D-01: a public layer with tagged releases that later sites pin; its runtime-config keys, auth hook and bootstrap contract become a contract.</reversibility>
|
||||
<precondition>The plan 02 SUMMARY exists; `../sm-demo-app/scripts/dev.sh up` brings the stack up and `curl -fsS http://127.0.0.1:8097/_demo/api/v1/roadmap` answers; `git -C ../sm-demo-app/vue-demo-app status --porcelain` lists no file this task changes.</precondition>
|
||||
<files>../vue-chat-layer/.gitignore, ../vue-chat-layer/.nvmrc, ../vue-chat-layer/package.json, ../vue-chat-layer/pnpm-lock.yaml, ../vue-chat-layer/pnpm-workspace.yaml, ../vue-chat-layer/tsconfig.json, ../vue-chat-layer/nuxt.config.ts, ../vue-chat-layer/vitest.config.ts, ../vue-chat-layer/.playground/nuxt.config.ts, ../vue-chat-layer/.playground/app/app.vue, ../vue-chat-layer/app/lib/client.ts, ../vue-chat-layer/app/lib/types.ts, ../vue-chat-layer/app/composables/useChatApi.ts, ../vue-chat-layer/app/composables/useChat.ts, ../vue-chat-layer/app/components/Chat/Page.vue, ../vue-chat-layer/app/components/Chat/ChannelList.vue, ../vue-chat-layer/app/components/Chat/MessageList.vue, ../vue-chat-layer/app/components/Chat/Composer.vue, ../vue-chat-layer/tests/smoke.test.ts, ../sm-demo-app/vue-demo-app/package.json, ../sm-demo-app/vue-demo-app/nuxt.config.ts, ../sm-demo-app/vue-demo-app/app/plugins/auth.ts, ../sm-demo-app/vue-demo-app/app/pages/chat.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-chat.cjs, ../sm-demo-app/scripts/check-local.sh</files>
|
||||
<read_first>../vue-faq-layer/{nuxt.config.ts,package.json,pnpm-workspace.yaml,tsconfig.json,.gitignore,.nvmrc,.playground/nuxt.config.ts}, ../vue-paymentgateway-layer/nuxt.config.ts and app/composables/usePaymentGatewayApi.ts and app/utils/client.ts, ../sm-bm-app/frontend/src/api/chat.ts and api/types.ts, ../sm-bm-app/frontend/src/stores/chat.ts (bootstrap, open, send), ../sm-bm-app/frontend/src/components/chat/{ChatPanel,MessageList,ChatComposer}.vue, ../sm-demo-app/plugins/demo/chat_api.go, ../sm-demo-app/vue-demo-app/{nuxt.config.ts,package.json,app/plugins/auth.ts,app/pages/quizzes/index.vue,app/composables/useDemoAuth.ts,app/utils/authCookie.ts,i18n/locales/en.json}, ../sm-demo-app/scripts/check-local.sh (visitor_jwt, check_remote, ALL_CHECKS), ../sm-demo-app/scripts/check-remote.cjs and scripts/lib/browser.cjs, 20-PATTERNS.md (vue-chat-layer)</read_first>
|
||||
<action>Per D-01, D-04, D-07, SC-2. One path through every layer: app page, layer component, store, client, host bootstrap, chat API, database, back to the list.
|
||||
|
||||
1. Create the repository: `git init -b main` at /media/nvme/dev/golem15/summercms.io/summercms/vue-chat-layer with the vue-faq-layer files (.gitignore, .nvmrc, tsconfig.json, pnpm-workspace.yaml allowing the same builds plus `protobufjs: false`).
|
||||
- package.json: name vue-chat-layer, version 0.1.0, private, type module, main ./nuxt.config.ts, engines node >=22.18, peerDependencies nuxt 4.4.8;
|
||||
- dependencies centrifuge 5.7.3 and @nuxtjs/i18n 10.4.0; devDependencies nuxt 4.4.8, vue 3.5.35, typescript 5.9.3, vitest 3.2.7;
|
||||
- scripts test (`vitest run`), dev (`nuxi dev .playground`), build:playground and dev:prepare.
|
||||
Run pnpm install.
|
||||
2. nuxt.config.ts:
|
||||
- the header comment says an app consumes it with `extends: ['vue-chat-layer']`, provides `$chatAuth` and sets `public.chat.hostApiBase`; currentDir with absolute paths;
|
||||
- runtimeConfig.chat.apiBase for SSR (`http://127.0.0.1:8096/_chat/api/v1`, NUXT_CHAT_API_BASE);
|
||||
- public.chat: apiBase `/_chat/api/v1`, channelsApiBase `/_chatchannels/api/v1`, botsApiBase `/_chatbots/api/v1`, hostApiBase empty;
|
||||
- no pages and no route rules: the app owns its routes and wraps the chat components in ClientOnly (the components touch window and WebSocket, RESEARCH Pitfall 9);
|
||||
- the i18n:registerModule block of vue-paymentgateway-layer for en and pl (strings come in Task 3; create empty-object locale files now if the module needs them).
|
||||
3. app/lib/client.ts: `createChatClient({ bases, auth, fetch })`, pure and Nuxt-free so tests import it. It builds URLs, sends Bearer from `auth.token()`, retries once after `auth.refresh()` on 401 and then calls `auth.unauthorized()`, and turns the 422 envelope into a typed `ChatApiError` with field errors. Methods for the tracer: `bootstrap()`, `members()`, `openConversation(type, id)`, `messages(conversationId, opts)` and `send(conversationId, body)`. app/lib/types.ts holds the bootstrap and message types ported from bm's api/types.ts. app/composables/useChatApi.ts binds the client to runtime config and `useNuxtApp().$chatAuth`, following usePaymentGatewayApi.
|
||||
4. app/composables/useChat.ts, minimal for now, over `useState('chat:…')`: `start()` loads the bootstrap (and logs a console error, then stops, when hostApiBase is empty); `channels`; `activeId`; `open(channel)` calls openConversation('channel', channel.id) then messages; `send(text)`.
|
||||
5. Components ChatPage (sidebar plus pane), ChatChannelList (channel names, no create button), ChatMessageList (plain text in this task) and ChatComposer (textarea plus send). No v-html.
|
||||
6. vue-demo-app:
|
||||
- run `pnpm --dir ../sm-demo-app/vue-demo-app add vue-chat-layer@link:../../vue-chat-layer` (development pin, committed with this task's vue-demo-app commit; Task 6 swaps it for the tag before any push);
|
||||
- nuxt.config.ts: append 'vue-chat-layer' to extends; add '/_chat', '/_chatchannels' and '/_chatbots' to goPaths; set `public.chat.hostApiBase: '/_demo/api/v1'` and `chat.apiBase` from `api`; no route rule for /chat (the page is server-rendered with the chat inside ClientOnly, so smoke.sh still sees the banner and the prompt);
|
||||
- app/plugins/auth.ts provides `chatAuth: { token, refresh, unauthorized }`. token reads DEMO_TOKEN_COOKIE. refresh calls the user plugin's refresh endpoint that the login flow uses (read useDemoAuth) and stores the new token. unauthorized clears the cookie;
|
||||
- app/pages/chat.vue: for loggedIn, `<ClientOnly><ChatPage /></ClientOnly>`; otherwise a description and a `NuxtLink class="button"` to `{ path: '/register', query: { next: '/chat' } }` labelled `t('chat.registerToChat')`, as the quizzes page does. useSeoMeta with title and description;
|
||||
- i18n en and pl: chat.title, chat.lead and chat.registerToChat.
|
||||
7. sm-demo-app/scripts/check-chat.cjs (CommonJS, Playwright, modeled on check-remote.cjs):
|
||||
- `--base` (Nuxt origin) and DEMO_JWT from the environment;
|
||||
- sets the DEMO_TOKEN_COOKIE cookie with the JWT, opens {base}/chat, clicks the channel whose name is general and sends a unique text;
|
||||
- waits up to 10 s for the text in the message list, then fails on a console error or a 4xx/5xx response from /_chat;
|
||||
- opens {base}/chat in a fresh context without the cookie and asserts the register link to /register?next=/chat;
|
||||
- prints `check-chat: ok`.
|
||||
check-local.sh gets `check_chat`, which takes visitor_jwt and runs the script as check_remote does, and `chat` in ALL_CHECKS plus the header list.
|
||||
8. tests/smoke.test.ts (vitest): createChatClient builds `/_chat/api/v1/conversations/7/messages`, sends the Bearer header, retries once on 401 after refresh and maps a 422 body to field errors.
|
||||
|
||||
Commit vue-chat-layer (`feat: chat layer tracer: bootstrap, channel list, messages, composer`). Commit vue-demo-app package.json, pnpm-lock.yaml (link pin), nuxt.config.ts, auth.ts, chat.vue and locales by path (`feat: /chat page on vue-chat-layer`). Commit sm-demo-app scripts (`test: check-chat browser check`).</action>
|
||||
<verify>
|
||||
<automated>pnpm --dir ../vue-chat-layer test && pnpm --dir ../sm-demo-app/vue-demo-app run build && ../sm-demo-app/scripts/dev.sh up && ../sm-demo-app/scripts/check-local.sh chat</automated>
|
||||
<fails_when>Non-zero exit: the layer smoke test fails; the Nuxt build fails (layer not resolved, type or template error); the stack does not come up; or check-local prints "check-local: chat failed" (the sent text does not appear, /_chat answered 4xx/5xx, or the anonymous page lacks the register link).</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `git -C ../vue-chat-layer remote` prints nothing.
|
||||
- `grep -c "hostApiBase: ''" ../vue-chat-layer/nuxt.config.ts` prints 1.
|
||||
- `grep -rniE 'sm-bm|jz\.bm|_demo/api' ../vue-chat-layer/app ../vue-chat-layer/nuxt.config.ts ../vue-chat-layer/README.md 2>/dev/null | wc -l` prints 0.
|
||||
- `grep -c "'/_chat'" ../sm-demo-app/vue-demo-app/nuxt.config.ts` prints 1.
|
||||
- `node -p "require('../sm-demo-app/vue-demo-app/package.json').dependencies['vue-chat-layer']"` prints `link:../../vue-chat-layer` (the development pin, swapped in Task 6).
|
||||
</acceptance_criteria>
|
||||
<done>A visitor can chat in #general on the local demo site through the new layer, and an anonymous visitor is invited to register.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: The bm chat experience on the layer: realtime, DMs, member picker, mentions, unread counts and typing</name>
|
||||
<files>../vue-chat-layer/app/lib/realtime.ts, ../vue-chat-layer/app/lib/chatEvents.ts, ../vue-chat-layer/app/lib/chatFormat.ts, ../vue-chat-layer/app/lib/chatText.ts, ../vue-chat-layer/app/lib/chatUnread.ts, ../vue-chat-layer/app/lib/format.ts, ../vue-chat-layer/app/lib/media.ts, ../vue-chat-layer/app/lib/tabs.ts, ../vue-chat-layer/app/lib/client.ts, ../vue-chat-layer/app/composables/useChat.ts, ../vue-chat-layer/app/composables/useChatNotice.ts, ../vue-chat-layer/app/components/Chat/Panel.vue, ../vue-chat-layer/app/components/Chat/MessageList.vue, ../vue-chat-layer/app/components/Chat/Message.vue, ../vue-chat-layer/app/components/Chat/Composer.vue, ../vue-chat-layer/app/components/Chat/Header.vue, ../vue-chat-layer/app/components/Chat/DmList.vue, ../vue-chat-layer/app/components/Chat/MemberPicker.vue, ../vue-chat-layer/app/components/Chat/MentionPopup.vue, ../vue-chat-layer/app/components/Chat/Avatar.vue, ../vue-chat-layer/app/components/Chat/Icon.vue, ../vue-chat-layer/app/components/Chat/Notices.vue, ../vue-chat-layer/app/components/Chat/Page.vue, ../sm-demo-app/vue-demo-app/app/lib/realtime.ts</files>
|
||||
<read_first>../sm-bm-app/frontend/src/stores/chat.ts (whole store), stores/chat.test.ts, lib/realtime.ts, lib/chatEvents.ts, lib/chatFormat.ts, lib/chatText.ts, lib/chatUnread.ts, lib/format.ts, lib/media.ts, lib/tabs.ts, components/Icon.vue, components/chat/{ChatPanel,MessageList,ChatMessage,ChatComposer,ChatHeader,DmList,MemberPicker,MentionPopup,Avatar}.vue, ../sm-demo-app/vue-demo-app/app/lib/realtime.ts and realtime.test.ts, app/pages/presentations/[slug]/index.vue and pilot.vue (openChannel callers), ../vue-chat-layer/app/composables/useChat.ts (Task 1), 20-RESEARCH.md (Pattern 3, Pitfall 9)</read_first>
|
||||
<action>Per D-01, D-02, SC-2.
|
||||
|
||||
- Port bm's lib/realtime.ts into app/lib/realtime.ts. It keeps the same exports (REALTIME_WS_PATH, tokenFailureIsFinal, wsURL, connect, subscribe, presence, onConnection, onUnavailable, realtimeState, realtimeUnavailable, disconnect, openChannel). The token is fetched from `/api/realtime/token` with the `$chatAuth` token through a setter `setRealtimeAuth(getToken)`, so the module stays Nuxt-free. `centrifuge` is loaded with a dynamic import. Add `onUserEvent(handler): () => void`: the chat store's `user:{me}` subscription hands every event it does not handle to the registered handlers, so a second layer never subscribes to `user:{me}` itself.
|
||||
- Port chatEvents, chatFormat, chatText and chatUnread unchanged except for import paths, and the helpers they need from format, media and tabs. The bm tests come in plan 06. chatText's renderer escapes before it marks mentions; keep it the only raw-HTML binding in the layer.
|
||||
- useChat: port bm's store as a composable over useState. Replace `general` and `generalId` with `channels`, `activeId` and a `kind` ('channel' or 'dm') for the open conversation. Keep:
|
||||
- DMs (`conversations`; `openDm(userId)` through POST /contexts/demo/1/dm, using the context from the bootstrap);
|
||||
- the member list from `/chat/members`;
|
||||
- unread and mention counts, read marks and typing, with bm's polling fallback when realtime is unavailable (15 s open conversation, 60 s counts);
|
||||
- realtime start for a member session, edits and deletions from events.
|
||||
Channel join and leave call chatchannels join and leave and refresh the bootstrap. Errors go to useChatNotice (a small toast state rendered by ChatNotices inside ChatPage and the floating panel), replacing bm's toast store.
|
||||
- Port the components ChatPanel, ChatMessageList, ChatMessage (text through chatText, attachment rendering in Task 3), ChatComposer (mention popup, Enter to send, Shift+Enter newline), ChatHeader (unread and mention badges), ChatDmList, ChatMemberPicker, ChatMentionPopup and ChatAvatar, plus ChatIcon with the icons they use. ChatPage composes sidebar (channels, DMs, a new-DM button opening the member picker) and pane. Strip every bm-specific element (entity embeds, deadline pins, the student app's tabs) or leave a named slot in its place.
|
||||
- vue-demo-app app/lib/realtime.ts becomes a re-export of `vue-chat-layer/app/lib/realtime` (with a comment), and the app plugin calls `setRealtimeAuth` with the DEMO_TOKEN_COOKIE reader. If the existing app/lib/realtime.test.ts fails against the shared client, replace it with bm's current realtime.test.ts, adjusted only in import paths.
|
||||
|
||||
Commit vue-chat-layer (`feat: port the chat store, realtime client and components`) and vue-demo-app (`refactor: share the chat layer's realtime client`).</action>
|
||||
<verify>
|
||||
<automated>pnpm --dir ../vue-chat-layer test && pnpm --dir ../sm-demo-app/vue-demo-app test && pnpm --dir ../sm-demo-app/vue-demo-app run build && ../sm-demo-app/scripts/check-local.sh chat realtime remote</automated>
|
||||
<fails_when>Non-zero exit: layer smoke or vue-demo-app vitest red (the realtime re-export broke the presentations tests), the build fails, or check-local reports chat, realtime or remote failed (the presentations remote no longer works through the shared client).</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c 'export function onUserEvent' ../vue-chat-layer/app/lib/realtime.ts` prints 1.
|
||||
- `grep -rn "from 'pinia'" ../vue-chat-layer/app | wc -l` prints 0.
|
||||
- `grep -c 'vue-chat-layer/app/lib/realtime' ../sm-demo-app/vue-demo-app/app/lib/realtime.ts` prints 1.
|
||||
- `grep -rn 'v-html' ../vue-chat-layer/app/components | grep -v 'renderChatText\|rendered' | wc -l` prints 0.
|
||||
</acceptance_criteria>
|
||||
<done>The layer behaves like bm's chat across several channels and DMs, live, with one shared realtime connection on every page.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Threads, pins, reactions, attachments, moderator delete, link embeds and the slash command picker</name>
|
||||
<files>../vue-chat-layer/app/components/Chat/ThreadPane.vue, ../vue-chat-layer/app/components/Chat/PinsPanel.vue, ../vue-chat-layer/app/components/Chat/ReactionBar.vue, ../vue-chat-layer/app/components/Chat/StagingChips.vue, ../vue-chat-layer/app/components/Chat/DeleteDialog.vue, ../vue-chat-layer/app/components/Chat/LinkEmbed.vue, ../vue-chat-layer/app/components/Chat/CommandPicker.vue, ../vue-chat-layer/app/components/Chat/Message.vue, ../vue-chat-layer/app/components/Chat/Composer.vue, ../vue-chat-layer/app/components/Chat/Header.vue, ../vue-chat-layer/app/components/Chat/ChannelList.vue, ../vue-chat-layer/app/lib/linkEmbeds.ts, ../vue-chat-layer/app/lib/client.ts, ../vue-chat-layer/app/composables/useChat.ts, ../vue-chat-layer/app/assets/css/chat-tokens.css, ../vue-chat-layer/app/assets/css/chat-base.css, ../vue-chat-layer/i18n/locales/en.json, ../vue-chat-layer/i18n/locales/pl.json, ../vue-chat-layer/nuxt.config.ts, ../vue-chat-layer/README.md</files>
|
||||
<read_first>../sm-bm-app/frontend/src/components/chat/{ReactionBar,StagingChips,DeleteDialog,ChatMessage,ChatComposer,EmbedCard}.vue, ../sm-bm-app/frontend/src/lib/embeds.ts (what to drop), ../sm-demo-app/plugins/golem15/chat/README.md (replies, thread mute, reactions catalog, attachments and limits), ../sm-demo-app/plugins/golem15/chatchannels/README.md (pins, mentions), ../sm-demo-app/plugins/golem15/chatbots/README.md and api/commands.go, ../vue-paymentgateway-layer/{README.md,app/assets/css/pg-tokens.css}, ../vue-faq-layer/README.md (layer README structure), 20-RESEARCH.md (Open Question 4, Anti-Patterns)</read_first>
|
||||
<action>Per D-02, D-05, D-22, D-23, D-01.
|
||||
|
||||
- Threads: a reply count on root messages opens ChatThreadPane, which lists replies (GET /messages/{id}/replies), posts replies (POST /messages/{id}/replies) and mutes the thread (PUT /threads/{rootId}/mute). Realtime reply events update the count.
|
||||
- Pins: ChatPinsPanel, opened from the header of a channel, lists GET .../channels/{id}/pins. Pin and unpin show only when the API allows it: a refused pin shows a notice and hides the action for the session.
|
||||
- Reactions: ChatReactionBar ported, with the keys from GET /reactions.
|
||||
- Attachments: ChatStagingChips ported. Uploads go to POST /attachments within the bootstrap limits (max_files, max_bytes), and the message shows a download link (GET /attachments/{messageId}/{fileId} with the auth header, fetched as a blob).
|
||||
- Delete: ChatDeleteDialog ported. Own messages always; others' only when the bootstrap's moderator is true and the conversation is a channel (D-22).
|
||||
- Edits: an Edit action on own messages through PUT /messages/{id}.
|
||||
- Link embeds: app/lib/linkEmbeds.ts with `linkify(escapedHtml)`, `registerEmbedRenderer(match: (url: URL) => boolean, component)` and `embedRendererFor(url)`. ChatMessage renders the first URL of a message with a registered renderer or with ChatLinkEmbed (host and path, no fetch). max_embeds caps the cards.
|
||||
- Commands (D-05): typing "/" at the start of the composer opens ChatCommandPicker, filled from GET /commands (cached per session) and filtered by prefix. Sending a "/" message calls POST /dispatch with {input} instead of posting. An ephemeral answer renders as a local, dismissable, sender-only system row with the payload text (escaped); a 422 shows the error.
|
||||
- Slots: ChatHeader renders `<slot name="conversation-actions" :conversation>` only when the open conversation is a DM (D-23). ChatChannelList renders `<slot name="sidebar-footer" />`. ChatPage forwards both slots.
|
||||
- Styling: chat-tokens.css and chat-base.css (neutral tokens an app overrides, as pg-tokens.css does), registered in nuxt.config.ts css with absolute paths.
|
||||
- i18n: every UI string of the layer in en and pl under `chat.*`. The app's own copy stays in the app.
|
||||
- README.md in the layer structure of vue-faq-layer and vue-paymentgateway-layer:
|
||||
- install by tag; extends;
|
||||
- the `$chatAuth` hook; the runtime-config keys;
|
||||
- the host bootstrap contract (the two JSON shapes with a neutral `acme` host);
|
||||
- slots, `registerEmbedRenderer`, `onUserEvent`;
|
||||
- client-only rendering (wrap ChatPage and ChatFloatingPanel in ClientOnly); testing.
|
||||
Name no consuming application.
|
||||
|
||||
Commit vue-chat-layer (`feat: threads, pins, reactions, attachments, link embeds and the command picker`).</action>
|
||||
<verify>
|
||||
<automated>pnpm --dir ../vue-chat-layer test && pnpm --dir ../vue-chat-layer run build:playground && pnpm --dir ../sm-demo-app/vue-demo-app run build && ../sm-demo-app/scripts/check-local.sh chat</automated>
|
||||
<fails_when>Non-zero exit: the layer smoke test, the playground build or the app build fails, or check-local prints "check-local: chat failed".</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c 'conversation-actions' ../vue-chat-layer/app/components/Chat/Header.vue` prints at least 1, and the slot sits inside a condition on the DM type (`grep -c "type === 'dm'" ../vue-chat-layer/app/components/Chat/Header.vue` prints at least 1).
|
||||
- `grep -c 'export function registerEmbedRenderer' ../vue-chat-layer/app/lib/linkEmbeds.ts` prints 1.
|
||||
- `grep -c '/dispatch' ../vue-chat-layer/app/lib/client.ts` prints at least 1.
|
||||
- `node -e "const a=require('../vue-chat-layer/i18n/locales/en.json'),b=require('../vue-chat-layer/i18n/locales/pl.json');const k=o=>Object.entries(o).flatMap(([x,v])=>typeof v==='object'?k(v).map(y=>x+'.'+y):[x]);const d=k(a).filter(x=>!k(b).includes(x));if(d.length){console.error(d);process.exit(1)}"` exits 0 (PL covers every EN key).
|
||||
- In a manual run on the local stack, a "/demo" command in the composer shows the ephemeral answer and nothing is posted to the channel (recorded in the SUMMARY).
|
||||
</acceptance_criteria>
|
||||
<done>The layer has the full D-02 feature set plus the slash command picker and the extension points other layers and hosts plug into.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Chat everywhere on the demo site: floating panel, Chat tour card, EN/PL copy and updated checks</name>
|
||||
<files>../vue-chat-layer/app/components/Chat/FloatingPanel.vue, ../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/components/TourCard.vue, ../sm-demo-app/vue-demo-app/app/pages/chat.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-demo-app/scripts/smoke.sh</files>
|
||||
<read_first>../sm-demo-app/vue-demo-app/app/layouts/default.vue, app/pages/index.vue (stops, TourCard), app/components/TourCard.vue (icon union, ICONS), app/composables/useDemoAuth.ts, i18n/locales/en.json and pl.json (tour.*), ../sm-demo-app/scripts/check-local.sh (check_tour, card_titles), ../sm-demo-app/scripts/smoke.sh (page loop at line 237), ../sm-bm-app/frontend/src/components/chat/DmList.vue</read_first>
|
||||
<action>Per D-03, D-04.
|
||||
- Layer ChatFloatingPanel: a corner button with the total unread badge (DMs plus channel mentions). It opens a compact panel with ChatDmList and the open DM's message list and composer, and has a link to the full chat page whose path comes from a `chatPath` prop (default '/chat'). It starts the chat store only for a member and renders nothing otherwise.
|
||||
- vue-demo-app default.vue: inside `<ClientOnly>`, render `<ChatFloatingPanel>` when loggedIn and the route is not /chat, /p/** or /presentations/**.
|
||||
- index.vue: a Chat stop `{ key: 'chat', to: '/chat' }` after quizzes. TourCard gets an outline chat icon in ICONS and 'chat' in the icon union. en and pl get tour.chat.title and tour.chat.text, and the tour heading text matches the new count ("Seven things to try" / the Polish equivalent).
|
||||
- chat.vue: final copy (EN/PL) explaining channels, DMs, threads and slash commands, plus the register prompt.
|
||||
- check-local.sh check_tour expects Chat (EN) and Czat (PL) among the cards and seven cards. smoke.sh adds /chat to the page loop (banner present; the anonymous prompt link to /register?next=%2Fchat in the HTML).
|
||||
|
||||
Commit the layer (`feat: floating chat panel`), vue-demo-app (`feat: floating chat panel, Chat tour card`) and sm-demo-app scripts separately.</action>
|
||||
<verify>
|
||||
<automated>pnpm --dir ../vue-chat-layer test && pnpm --dir ../sm-demo-app/vue-demo-app run build && ../sm-demo-app/scripts/check-local.sh tour chat</automated>
|
||||
<fails_when>Non-zero exit: build or tests fail, or check-local reports tour failed (missing Chat or Czat card, wrong card count) or chat failed.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c 'ChatFloatingPanel' ../sm-demo-app/vue-demo-app/app/layouts/default.vue` prints 1 and the element sits inside ClientOnly.
|
||||
- `grep -c "key: 'chat'" ../sm-demo-app/vue-demo-app/app/pages/index.vue` prints 1.
|
||||
- `grep -c '/chat' ../sm-demo-app/scripts/smoke.sh` prints at least 1.
|
||||
</acceptance_criteria>
|
||||
<done>Chat is reachable from every page for members and from the tour for everyone, in EN and PL.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking">
|
||||
<name>Task 5: Go-ahead to create vue-chat-layer on git.golem15.com, push it and tag v0.1.0</name>
|
||||
<read_first>Claude's summary before asking: `git -C ../vue-chat-layer log --oneline`, the README's install line, the visibility of vue-faq-layer on git.golem15.com (consumers install layers over git+https), whether `tea login list` shows a git.golem15.com login, and the result of the Task 4 checks.</read_first>
|
||||
<acceptance_criteria>
|
||||
- The user chose one option and it is recorded in the SUMMARY; with "release", `git ls-remote https://git.golem15.com/golem15/vue-chat-layer.git` exits 0 before Task 6 pushes, or Task 6 creates it with tea.
|
||||
</acceptance_criteria>
|
||||
<decision>Create the vue-chat-layer repository, push main and tag v0.1.0 now?</decision>
|
||||
<context>D-01 rates the layer costly: later sites pin it by tag, so v0.1.0 freezes the hook, runtime-config and bootstrap contract shown in the README. A pushed tag must not move. vue-demo-app can only pin the layer by tag after this; until then it builds from a local link that cannot be pushed. Repository creation and pushes need your go-ahead (CLAUDE.md).</context>
|
||||
<options>
|
||||
<option id="release">
|
||||
<name>Create, push and tag v0.1.0</name>
|
||||
<pros>vue-demo-app pins the tag and plan 04 can build vue-chatvideo-layer against it.</pros>
|
||||
<cons>The v0.1.0 contract is public; changes need v0.1.1 or v0.2.0.</cons>
|
||||
</option>
|
||||
<option id="hold">
|
||||
<name>Hold</name>
|
||||
<pros>The layer's contract can still change freely.</pros>
|
||||
<cons>Plan 04 (chatvideo extends the chat contract) and the deploy are blocked; vue-demo-app keeps a local link pin, so it cannot be pushed.</cons>
|
||||
</option>
|
||||
</options>
|
||||
<instructions>For "release" without a tea login: create golem15/vue-chat-layer (empty, default branch main, same visibility as vue-faq-layer) in the git.golem15.com web UI, then reply. Do not paste tokens.</instructions>
|
||||
<resume-signal>Reply "release" (add "repo ready" if you created it) or "hold".</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 6: Release vue-chat-layer v0.1.0 and pin it in vue-demo-app</name>
|
||||
<reversibility rating="one-way">A pushed layer tag is installed by consumers' lockfiles; it can be superseded but must never move.</reversibility>
|
||||
<precondition>Task 5 resolved with "release".</precondition>
|
||||
<files>../vue-chat-layer/README.md, ../sm-demo-app/vue-demo-app/package.json, ../sm-demo-app/vue-demo-app/pnpm-lock.yaml</files>
|
||||
<read_first>../vue-chat-layer/README.md, ../sm-demo-app/vue-demo-app/package.json (other layer pins), the Task 5 answer</read_first>
|
||||
<action>Per D-01.
|
||||
1. If the repository is missing, create it with `tea repo create --name vue-chat-layer --owner golem15` at vue-faq-layer's visibility.
|
||||
2. Add the remote `git@git.golem15.com:golem15/vue-chat-layer.git` and push main.
|
||||
3. If the README's install line says no tag exists yet, fix it before tagging. Create the annotated tag v0.1.0, push it, and confirm `git ls-remote origin refs/tags/v0.1.0^{}` equals HEAD.
|
||||
4. In vue-demo-app run `pnpm add "vue-chat-layer@git+https://git.golem15.com/golem15/vue-chat-layer.git#v0.1.0"` (replacing the link pin), then build and test.
|
||||
5. Commit package.json and pnpm-lock.yaml (`feat: pin vue-chat-layer v0.1.0`). Do not push vue-demo-app (plan 05).</action>
|
||||
<verify>
|
||||
<automated>[ "$(git -C ../vue-chat-layer ls-remote origin 'refs/tags/v0.1.0^{}' | cut -f1)" = "$(git -C ../vue-chat-layer rev-parse HEAD)" ] && grep -q 'vue-chat-layer.git#v0.1.0' ../sm-demo-app/vue-demo-app/package.json && pnpm --dir ../sm-demo-app/vue-demo-app test && pnpm --dir ../sm-demo-app/vue-demo-app run build</automated>
|
||||
<fails_when>Non-zero exit: the remote tag is missing or differs from HEAD, package.json still holds the link pin, or the vue-demo-app tests or build fail against the tagged layer.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `node -p "require('../sm-demo-app/vue-demo-app/package.json').dependencies['vue-chat-layer']"` prints `git+https://git.golem15.com/golem15/vue-chat-layer.git#v0.1.0`.
|
||||
- `git -C ../sm-demo-app/vue-demo-app status --porcelain -- package.json pnpm-lock.yaml` prints nothing.
|
||||
- `../sm-demo-app/scripts/check-local.sh chat tour` prints `check-local: ok chat tour`.
|
||||
</acceptance_criteria>
|
||||
<done>vue-chat-layer v0.1.0 is published and the demo site builds against the tag.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| other members' messages, names and URLs to the browser | untrusted content rendered in every member's page |
|
||||
| browser to the chat-family APIs | JWT bearer from the demo cookie; actor fixed server-side |
|
||||
| browser to Centrifugo | connection token per member; subscriptions authorized server-side |
|
||||
| pnpm install from git.golem15.com | the layer tag becomes an input of every consuming site |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-20-14 | Tampering | stored XSS through message text, names or URLs | high | mitigate | text rendered through the ported chatText renderer that escapes before marking mentions; linkify runs on escaped HTML and only emits http(s) anchors with rel noopener noreferrer nofollow; names and payloads are text bindings; acceptance grep limits v-html to the renderer output |
|
||||
| T-20-15 | Information disclosure | JWT handling in the layer | medium | mitigate | the token comes only from the app's $chatAuth hook (cookie reader), is sent as a Bearer header to same-origin paths, never stored by the layer, and cleared through unauthorized() after a failed refresh |
|
||||
| T-20-16 | Information disclosure | attachment downloads | medium | mitigate | downloads go through GET /attachments/{messageId}/{fileId} with the Bearer header (participants only server-side); no public URL is built |
|
||||
| T-20-17 | Spoofing | slash command answers | low | mitigate | ephemeral answers are rendered locally as escaped text marked as a system row and never posted as the user |
|
||||
| T-20-18 | Tampering | layer supply chain | medium | mitigate | the layer pins exact versions already audited in the workspace (centrifuge 5.7.3, @nuxtjs/i18n 10.4.0); vue-demo-app pins the layer by tag with a committed lockfile |
|
||||
| T-20-19 | Denial of service | realtime connections | low | mitigate | one shared Centrifuge client per page (re-export in vue-demo-app); polling fallback intervals of 15 s and 60 s from bm |
|
||||
| T-20-SC | Tampering | npm installs | high | mitigate | no new package: centrifuge 5.7.3 was audited in 19.4.1 (protobufjs postinstall disabled in pnpm-workspace.yaml); @nuxtjs/i18n 10.4.0, vitest 3.2.7, nuxt, vue and typescript are the pins already in the workspace lockfiles |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- Task 1: layer smoke, app build, check-local chat (browser send, anonymous prompt).
|
||||
- Task 2: layer and app vitest, build, check-local chat, realtime and remote.
|
||||
- Task 3: layer smoke, playground build, app build, check-local chat, PL key coverage.
|
||||
- Task 4: build, check-local tour and chat, smoke.sh page list.
|
||||
- Task 5: go-ahead recorded.
|
||||
- Task 6: remote tag equals HEAD, vue-demo-app builds against the tag.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
The public demo site has `/chat` with channels, DMs, threads, pins, reactions, attachments, mentions, typing, unread counts, link embeds and slash commands working live (SC-2, D-01 to D-05), a floating panel for members and an invitation for visitors. vue-chat-layer v0.1.0 is a reusable, app-neutral layer.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/20-demo-instance-with-every-shared-plugin/20-03-SUMMARY.md` when done: the layer's public API (runtime keys, hook, slots, exports), what was stripped from bm and why, the refresh endpoint used by chatAuth, which realtime test file is used in vue-demo-app, the tag and commit, and the checkpoint answer.
|
||||
</output>
|
||||
@@ -0,0 +1,517 @@
|
||||
---
|
||||
phase: 20-demo-instance-with-every-shared-plugin
|
||||
plan: 04
|
||||
type: execute
|
||||
wave: 4
|
||||
depends_on: ["20-03"]
|
||||
files_modified:
|
||||
- ../vue-forum-layer/.gitignore
|
||||
- ../vue-forum-layer/.nvmrc
|
||||
- ../vue-forum-layer/package.json
|
||||
- ../vue-forum-layer/pnpm-lock.yaml
|
||||
- ../vue-forum-layer/pnpm-workspace.yaml
|
||||
- ../vue-forum-layer/tsconfig.json
|
||||
- ../vue-forum-layer/nuxt.config.ts
|
||||
- ../vue-forum-layer/README.md
|
||||
- ../vue-forum-layer/.playground/nuxt.config.ts
|
||||
- ../vue-forum-layer/.playground/app/app.vue
|
||||
- ../vue-forum-layer/app/utils/forumClient.ts
|
||||
- ../vue-forum-layer/app/composables/useForumApi.ts
|
||||
- ../vue-forum-layer/app/composables/useForum.ts
|
||||
- ../vue-forum-layer/app/components/Forum/Page.vue
|
||||
- ../vue-forum-layer/app/components/Forum/CategoryList.vue
|
||||
- ../vue-forum-layer/app/components/Forum/ThreadList.vue
|
||||
- ../vue-forum-layer/app/components/Forum/Thread.vue
|
||||
- ../vue-forum-layer/app/components/Forum/PostEditor.vue
|
||||
- ../vue-forum-layer/app/assets/css/forum-base.css
|
||||
- ../vue-forum-layer/i18n/locales/en.json
|
||||
- ../vue-forum-layer/i18n/locales/pl.json
|
||||
- ../vue-forum-layer/tests/client.test.ts
|
||||
- ../vue-userfriends-layer/.gitignore
|
||||
- ../vue-userfriends-layer/.nvmrc
|
||||
- ../vue-userfriends-layer/package.json
|
||||
- ../vue-userfriends-layer/pnpm-lock.yaml
|
||||
- ../vue-userfriends-layer/pnpm-workspace.yaml
|
||||
- ../vue-userfriends-layer/tsconfig.json
|
||||
- ../vue-userfriends-layer/nuxt.config.ts
|
||||
- ../vue-userfriends-layer/README.md
|
||||
- ../vue-userfriends-layer/.playground/nuxt.config.ts
|
||||
- ../vue-userfriends-layer/.playground/app/app.vue
|
||||
- ../vue-userfriends-layer/app/utils/friendsClient.ts
|
||||
- ../vue-userfriends-layer/app/composables/useFriendsApi.ts
|
||||
- ../vue-userfriends-layer/app/composables/useFriends.ts
|
||||
- ../vue-userfriends-layer/app/components/Friends/Page.vue
|
||||
- ../vue-userfriends-layer/app/components/Friends/List.vue
|
||||
- ../vue-userfriends-layer/app/components/Friends/Requests.vue
|
||||
- ../vue-userfriends-layer/app/components/Friends/Groups.vue
|
||||
- ../vue-userfriends-layer/app/components/Friends/AddFriend.vue
|
||||
- ../vue-userfriends-layer/app/assets/css/friends-base.css
|
||||
- ../vue-userfriends-layer/i18n/locales/en.json
|
||||
- ../vue-userfriends-layer/i18n/locales/pl.json
|
||||
- ../vue-userfriends-layer/tests/client.test.ts
|
||||
- ../vue-chatvideo-layer/.gitignore
|
||||
- ../vue-chatvideo-layer/.nvmrc
|
||||
- ../vue-chatvideo-layer/package.json
|
||||
- ../vue-chatvideo-layer/pnpm-lock.yaml
|
||||
- ../vue-chatvideo-layer/pnpm-workspace.yaml
|
||||
- ../vue-chatvideo-layer/tsconfig.json
|
||||
- ../vue-chatvideo-layer/nuxt.config.ts
|
||||
- ../vue-chatvideo-layer/README.md
|
||||
- ../vue-chatvideo-layer/.playground/nuxt.config.ts
|
||||
- ../vue-chatvideo-layer/.playground/app/app.vue
|
||||
- ../vue-chatvideo-layer/app/lib/videoClient.ts
|
||||
- ../vue-chatvideo-layer/app/lib/callMachine.ts
|
||||
- ../vue-chatvideo-layer/app/composables/useCall.ts
|
||||
- ../vue-chatvideo-layer/app/composables/useIncomingCalls.ts
|
||||
- ../vue-chatvideo-layer/app/components/Chatvideo/Call.vue
|
||||
- ../vue-chatvideo-layer/app/components/Chatvideo/Tile.vue
|
||||
- ../vue-chatvideo-layer/app/components/Chatvideo/CallButton.vue
|
||||
- ../vue-chatvideo-layer/app/components/Chatvideo/IncomingCall.vue
|
||||
- ../vue-chatvideo-layer/app/assets/css/chatvideo-base.css
|
||||
- ../vue-chatvideo-layer/i18n/locales/en.json
|
||||
- ../vue-chatvideo-layer/i18n/locales/pl.json
|
||||
- ../vue-chatvideo-layer/tests/callMachine.test.ts
|
||||
- ../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/plugins/auth.ts
|
||||
- ../sm-demo-app/vue-demo-app/app/layouts/default.vue
|
||||
- ../sm-demo-app/vue-demo-app/app/pages/forum/index.vue
|
||||
- ../sm-demo-app/vue-demo-app/app/pages/forum/t/[id].vue
|
||||
- ../sm-demo-app/vue-demo-app/app/pages/friends.vue
|
||||
- ../sm-demo-app/vue-demo-app/app/pages/call/[conversation].vue
|
||||
- ../sm-demo-app/vue-demo-app/app/pages/chat.vue
|
||||
- ../sm-demo-app/vue-demo-app/app/pages/index.vue
|
||||
- ../sm-demo-app/vue-demo-app/app/pages/account/security.vue
|
||||
- ../sm-demo-app/vue-demo-app/app/components/SiteHeader.vue
|
||||
- ../sm-demo-app/vue-demo-app/app/components/TourCard.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-demo-app/scripts/smoke.sh
|
||||
- ../sm-demo-app/scripts/dev.sh
|
||||
- ../sm-demo-app/deploy/env.example
|
||||
- ../sm-demo-app/README.md
|
||||
- .planning/phases/20-demo-instance-with-every-shared-plugin/COVERAGE.md
|
||||
autonomous: false
|
||||
requirements: [SC-2, D-11, D-12, D-13, D-14, D-15, D-16, D-20, D-21, D-22, D-23]
|
||||
estimate:
|
||||
tokens: 380000
|
||||
raw_tokens: 380000
|
||||
tasks: 7
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "D-14, D-21: /forum shows a member the seeded EN and PL categories and their threads; a thread page lists posts as the server's content_html; a member creates a thread, replies, edits own posts and deletes own posts (moderators any post, D-22); an anonymous visitor on /forum and on a thread URL sees a description and a register-to-join link (/register?next=...), and no forum API call is made without a JWT."
|
||||
- "D-15: /friends shows a member their friends, incoming and sent requests (accept, reject, cancel, unfriend) and friend groups (list, create, join, leave, members); a member sends a request to another member chosen from the app-provided directory; /friends is linked from the chat sidebar (sidebar-footer slot) and from the account area (header link and /account/security)."
|
||||
- "D-11, D-23: the call button (ChatvideoCallButton) appears only in DM conversations through vue-chat-layer's conversation-actions slot; there is no call button in channels; the server is not changed to refuse channel calls."
|
||||
- "D-13: /call/{conversation} is a minimal call page: own camera, remote tiles, mute, camera toggle and leave, built on livekit-client loaded with a dynamic import; an incoming video:call:incoming event (received through vue-chat-layer's onUserEvent, no second user:{me} subscription) shows an accept/decline prompt on every page for members."
|
||||
- "D-12: the call page shows the D-12 caps' effects (a started call ends at the server's max_duration; a fifth participant is refused by LiveKit's room cap) without client-side limits of its own."
|
||||
- "D-16: herald runs with VAPID keys from .env (SUMMER_GOLEM15__HERALD__VAPID__PUBLIC_KEY, __PRIVATE_KEY, __SUBJECT; dev.sh generates them with herald:vapid-generate, never printing them) and there is no subscribe UI; /_herald is not added to vue-demo-app goPaths."
|
||||
- "The homepage tour has Forum, Friends and Calls cards next to Chat (ten cards, EN and PL)."
|
||||
- "livekit-client is installed only after the blocking human-verify checkpoint, at an exact version the user confirmed."
|
||||
- "After the Task 6 go-ahead, vue-forum-layer, vue-userfriends-layer and vue-chatvideo-layer exist on git.golem15.com with tag v0.1.0 each, and vue-demo-app pins all three by git+https tag in a committed package.json and pnpm-lock.yaml."
|
||||
artifacts:
|
||||
- path: "../vue-forum-layer/app/components/Forum/Page.vue"
|
||||
provides: "categories and thread list of one forum context"
|
||||
contains: "contextType"
|
||||
- path: "../vue-userfriends-layer/app/components/Friends/Page.vue"
|
||||
provides: "friends, requests and groups"
|
||||
contains: "Requests"
|
||||
- path: "../vue-chatvideo-layer/app/lib/callMachine.ts"
|
||||
provides: "pure call state machine (idle, starting, connecting, connected, ended, failed)"
|
||||
contains: "connected"
|
||||
- path: "../vue-chatvideo-layer/app/composables/useCall.ts"
|
||||
provides: "LiveKit room lifecycle over the chatvideo API, dynamic import of livekit-client"
|
||||
contains: "import('livekit-client')"
|
||||
- path: "../sm-demo-app/vue-demo-app/app/pages/call/[conversation].vue"
|
||||
provides: "/call/{conversation} page"
|
||||
contains: "ChatvideoCall"
|
||||
- path: ".planning/phases/20-demo-instance-with-every-shared-plugin/COVERAGE.md"
|
||||
provides: "API coverage decisions for livekit-client, LiveKit server, Centrifugo and Web Push"
|
||||
contains: "livekit-client"
|
||||
key_links:
|
||||
- from: "../sm-demo-app/vue-demo-app/app/pages/chat.vue"
|
||||
to: "../vue-chatvideo-layer/app/components/Chatvideo/CallButton.vue"
|
||||
via: "template #conversation-actions renders ChatvideoCallButton (DM only, D-23)"
|
||||
pattern: "conversation-actions"
|
||||
- from: "../vue-chatvideo-layer/app/composables/useIncomingCalls.ts"
|
||||
to: "../vue-chat-layer/app/lib/realtime.ts"
|
||||
via: "onUserEvent receives video:call:incoming from the chat store's user:{me} subscription"
|
||||
pattern: "onUserEvent"
|
||||
- from: "../vue-chatvideo-layer/app/composables/useCall.ts"
|
||||
to: "../sm-demo-app/plugins/golem15/chatvideo/api/video.go"
|
||||
via: "POST /_chatvideo/api/v1/conversations/{id}/video/start or /join gives {livekit_url, token}"
|
||||
pattern: "video/(start|join)"
|
||||
- from: "../sm-demo-app/vue-demo-app/app/plugins/auth.ts"
|
||||
to: "../vue-forum-layer/app/composables/useForumApi.ts"
|
||||
via: "provide forumAuth and userfriendsAuth token getters (chatvideo reuses chatAuth)"
|
||||
pattern: "forumAuth"
|
||||
prohibitions:
|
||||
- statement: "None of the three layers names a consuming application or carries a default host path."
|
||||
status: unresolved
|
||||
verification: automated
|
||||
- statement: "Forum post HTML is rendered only from the server's content_html; user Markdown is never rendered client-side."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
- statement: "livekit-client is not added to any package.json before the Task 3 checkpoint is approved."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
- statement: "No push, repository creation or tag before the Task 6 go-ahead; vue-demo-app and sm-demo-app are not pushed (plan 05)."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
ROADMAP Goal (not in user-story format; no story is invented here): the demo's public Nuxt site shows the shared plugins working through shared layers, video calls included.
|
||||
|
||||
This plan's slice (plan 4 of 6, wave 4, per D-20): three new shared layers at v0.1.0 (`vue-forum-layer`, `vue-userfriends-layer`, `vue-chatvideo-layer`), their pages and tour cards in vue-demo-app, and herald's VAPID configuration without UI.
|
||||
|
||||
**Wave note (deviation from D-20, recorded):** D-20 lets plans 03 and 04 run in parallel after plan 02. This plan still depends on plan 03 and runs after it, for two reasons:
|
||||
1. It consumes plan 03's vue-chat-layer outputs: vue-chatvideo-layer imports the shared realtime client (`onUserEvent`, `subscribe`) and renders its call button in vue-chat-layer's DM-only `conversation-actions` slot (D-23), and the friends link goes into its `sidebar-footer` slot.
|
||||
2. Both plans edit the same vue-demo-app files (package.json, pnpm-lock.yaml, nuxt.config.ts, app/plugins/auth.ts, app/pages/index.vue, the locales) in one checkout. Parallel execution runs without worktrees, so the two plans would collide in the same wave.
|
||||
|
||||
## Decisions taken in this plan (Claude's discretion, recorded)
|
||||
|
||||
- **Layer shape:** like vue-faq-layer and vue-paymentgateway-layer. A Nuxt-free client in `app/utils/*Client.ts` (tested with `node --test`), an auth-hook composable, components with a directory prefix (`Forum*`, `Friends*`, `Chatvideo*`), EN/PL strings through `i18n:registerModule`, and no pages or route rules: the app owns routes and wraps client-side parts in ClientOnly.
|
||||
- **Auth hooks:** `$forumAuth` and `$userfriendsAuth` are token getters, the vue-paymentgateway-layer shape. vue-chatvideo-layer reuses vue-chat-layer's `$chatAuth`, because it has a peer dependency on that layer.
|
||||
- **Forum context:** props `contextType` and `contextId`; vue-demo-app passes `demo` and `1`. Thread URLs are built by an app-supplied `threadPath(id)` prop. Markdown is written in a textarea with the server preview endpoint (`POST /contexts/{type}/{id}/preview`); nothing is rendered client-side.
|
||||
- **Friend directory:** the layer takes an optional `$userfriendsDirectory(): Promise<{id, name}[]>` hook for the add-friend picker and falls back to a username field (the API accepts user_id, username or email). vue-demo-app provides the directory from `GET /_demo/api/v1/chat/members`.
|
||||
- **chatvideo and chat:** vue-chatvideo-layer lists vue-chat-layer v0.1.0 as a peer dependency and dev dependency, without `extends`. The app extends both, so the chat layer is not loaded twice. It imports `onUserEvent` and `subscribe` from `vue-chat-layer/app/lib/realtime`.
|
||||
- **Call flow:** the call button starts (`video/start`) and navigates to `/call/{conversation}`. The call page joins (`video/join`) when it was not the starter and connects with the returned `livekit_url` and `token`. Leave calls `video/leave` then `room.disconnect()`. An incoming call prompt offers Accept (navigate) and Decline (`video/leave`; the service publishes video:call:declined for a participant who never joined; Task 4 confirms this in video_service.go). Screen share stays off (D-13 minimal, `screen_share: false`).
|
||||
- **Profile link (D-15):** the demo has no profile page. The account area stands for it: a "Friends" header link for members next to "Security", and a link on /account/security.
|
||||
- **Tour:** ten cards (Journal, Presentations, Quizzes, Chat, Forum, Friends, Calls, Payments, FAQ, Accounts). The Calls card links to /chat with text saying calls start from a DM.
|
||||
- **Development pins:** `link:` dependencies to the three local checkouts until Task 7 swaps them for tags, as in plan 03.
|
||||
- spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip).
|
||||
|
||||
## Artifacts this phase produces (this plan)
|
||||
|
||||
- Repository `vue-forum-layer` v0.1.0:
|
||||
- components `ForumPage`, `ForumCategoryList`, `ForumThreadList`, `ForumThread`, `ForumPostEditor`;
|
||||
- composables `useForum`, `useForumApi`; client `createForumClient`;
|
||||
- runtime keys `forum.apiBase` (`NUXT_FORUM_API_BASE`) and `public.forum.apiBase`; hook `$forumAuth`; i18n `forum.*`.
|
||||
- Repository `vue-userfriends-layer` v0.1.0:
|
||||
- components `FriendsPage`, `FriendsList`, `FriendsRequests`, `FriendsGroups`, `FriendsAddFriend`;
|
||||
- composables `useFriends`, `useFriendsApi`; client `createFriendsClient`;
|
||||
- runtime keys `userfriends.apiBase` and `public.userfriends.apiBase`; hooks `$userfriendsAuth` and `$userfriendsDirectory`; i18n `friends.*`.
|
||||
- Repository `vue-chatvideo-layer` v0.1.0:
|
||||
- components `ChatvideoCall`, `ChatvideoTile`, `ChatvideoCallButton`, `ChatvideoIncomingCall`;
|
||||
- composables `useCall`, `useIncomingCalls`; lib `createVideoClient`, `callMachine` (`initialCallState`, `transition`);
|
||||
- runtime key `public.chatvideo.apiBase`; prop `callPath`; i18n `chatvideo.*`; dependency `livekit-client` at the confirmed exact version.
|
||||
- vue-demo-app:
|
||||
- pages `/forum`, `/forum/t/[id]`, `/friends`, `/call/[conversation]`;
|
||||
- ChatvideoIncomingCall in the default layout; the call button in /chat's conversation-actions slot; the friends link in /chat's sidebar-footer slot, the header and /account/security;
|
||||
- tour cards forum, friends and calls; goPaths `/_forum`, `/_userfriends`, `/_chatvideo`; hooks `forumAuth`, `userfriendsAuth`, `userfriendsDirectory`.
|
||||
- sm-demo-app: check-local checks `forum`, `friends`, `call`, `herald`; tour expects ten cards; smoke.sh pages /forum and /friends; dev.sh VAPID generation; env keys `SUMMER_GOLEM15__HERALD__VAPID__PUBLIC_KEY`, `__PRIVATE_KEY`, `__SUBJECT` (names only in env.example).
|
||||
- `.planning/phases/20-demo-instance-with-every-shared-plugin/COVERAGE.md` (API coverage matrix).
|
||||
|
||||
<objective>
|
||||
Build and release the forum, friends and video-call layers, put their pages and tour cards on the demo site, and configure herald's VAPID keys without UI.
|
||||
|
||||
Purpose: SC-2 (video calls and the other plugins visible through shared layers), D-11, D-12, D-13, D-14, D-15, D-16, D-21, D-22 (forum moderation), D-23.
|
||||
Output: three new repositories (pushed and tagged after the go-ahead), commits in vue-demo-app and sm-demo-app (not pushed), and the phase COVERAGE.md.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-CONTEXT.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-RESEARCH.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-PATTERNS.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-03-SUMMARY.md
|
||||
|
||||
Repos: new ../vue-forum-layer, ../vue-userfriends-layer and ../vue-chatvideo-layer; ../sm-demo-app/vue-demo-app (submodule); ../sm-demo-app. Layer analogs: ../vue-faq-layer (structure, node --test, client in app/utils), ../vue-paymentgateway-layer (auth hook, i18n registration), ../vue-chat-layer (plan 03: slots, onUserEvent, $chatAuth).
|
||||
|
||||
Rules: as in plan 03 (no co-author tags, stage by path, no remote before the go-ahead, vue-demo-app committed in its own repository and not pushed, check for other sessions' edits before each commit). The local stack: `../sm-demo-app/scripts/dev.sh up` with LiveKit on 127.0.0.1:7890.
|
||||
|
||||
Size (accepted under D-20): seven tasks over three new layer repositories are above the usual per-plan budget, but the user confirmed exactly six plans (D-20, the lean-planning rule in CLAUDE.md), so this plan is not split. After Task 4 is committed, write a progress note into a draft `20-04-SUMMARY.md` (Tasks 1 to 4 with their commits, the approved livekit-client version, the decline endpoint, open points) so a context reset resumes from Task 5.
|
||||
|
||||
<interfaces>
|
||||
forum API /_forum/api/v1 (JWT, forum-api 120/min): GET /contexts/{type}/{id}/categories; GET /contexts/{type}/{id}/categories/{categoryId}/threads; POST same (create thread {title, body}); GET /contexts/{type}/{id}/threads; GET /contexts/{type}/{id}/threads/{threadId}/posts; POST same (reply {body}); PUT /contexts/{type}/{id}/posts/{postId} ({body}); DELETE /contexts/{type}/{id}/posts/{postId}; DELETE /contexts/{type}/{id}/threads/{threadId}; POST /contexts/{type}/{id}/threads/{threadId}/read; GET /contexts/{type}/{id}/unread; POST /contexts/{type}/{id}/preview. Posts carry content_html (server-rendered with breakwater). 422 envelope as chat.
|
||||
userfriends API /_userfriends/api/v1 (JWT; userfriends-api 120/min, community-join 10/min): GET /friends, /friends/requests, /friends/sent; POST /friends/request ({user_id} or {username} or {email}); POST /friends/requests/{id}/accept; DELETE /friends/requests/{id}; DELETE /friends/sent/{id}; DELETE /friends/{userId}; GET|POST /groups; GET|PUT|DELETE /groups/{id}; POST /groups/{id}/join, /leave; GET|POST /groups/{id}/members; DELETE /groups/{id}/members/{userId}; GET|POST|DELETE /groups/{id}/invite; GET|POST /groups/join/{code}.
|
||||
chatvideo API /_chatvideo/api/v1 (JWT): POST /conversations/{id}/video/start (video-initiate 5/min) and /join give {livekit_url, token, ...}; POST /leave, /end; GET /status. Realtime: video:call:incoming on user:{id}; video:call:accepted, declined, ended and participant events on video:session:{id}.
|
||||
vue-chat-layer v0.1.0 (plan 03): slots conversation-actions (DM only) and sidebar-footer on ChatPage; `onUserEvent(handler)`, `subscribe(channel, hooks)` in `vue-chat-layer/app/lib/realtime`; `$chatAuth {token, refresh, unauthorized}`.
|
||||
herald: keys golem15.herald.vapid.{subject, public_key, private_key}; env SUMMER_GOLEM15__HERALD__VAPID__*; command herald:vapid-generate (--json prints {"public_key", "private_key"}; -q quiet).
|
||||
vue-demo-app (after plan 03): app/pages/chat.vue (ChatPage in ClientOnly), app/layouts/default.vue (ChatFloatingPanel), app/pages/index.vue (stops with chat), app/components/TourCard.vue, app/components/SiteHeader.vue (links mine and security), app/pages/account/security.vue, app/plugins/auth.ts (paymentGatewayAuth, chatAuth).
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: Tracer: a member opens /forum, sees the seeded categories, opens a thread and posts a reply that the server renders; an anonymous visitor gets the register prompt</name>
|
||||
<precondition>The plan 03 SUMMARY records vue-chat-layer v0.1.0 on git.golem15.com; `../sm-demo-app/scripts/dev.sh up` is running; `git -C ../sm-demo-app/vue-demo-app status --porcelain` lists no file this task changes.</precondition>
|
||||
<files>../vue-forum-layer/.gitignore, ../vue-forum-layer/.nvmrc, ../vue-forum-layer/package.json, ../vue-forum-layer/pnpm-lock.yaml, ../vue-forum-layer/pnpm-workspace.yaml, ../vue-forum-layer/tsconfig.json, ../vue-forum-layer/nuxt.config.ts, ../vue-forum-layer/README.md, ../vue-forum-layer/.playground/nuxt.config.ts, ../vue-forum-layer/.playground/app/app.vue, ../vue-forum-layer/app/utils/forumClient.ts, ../vue-forum-layer/app/composables/useForumApi.ts, ../vue-forum-layer/app/composables/useForum.ts, ../vue-forum-layer/app/components/Forum/Page.vue, ../vue-forum-layer/app/components/Forum/CategoryList.vue, ../vue-forum-layer/app/components/Forum/ThreadList.vue, ../vue-forum-layer/app/components/Forum/Thread.vue, ../vue-forum-layer/app/components/Forum/PostEditor.vue, ../vue-forum-layer/app/assets/css/forum-base.css, ../vue-forum-layer/i18n/locales/en.json, ../vue-forum-layer/i18n/locales/pl.json, ../vue-forum-layer/tests/client.test.ts, ../sm-demo-app/vue-demo-app/package.json, ../sm-demo-app/vue-demo-app/nuxt.config.ts, ../sm-demo-app/vue-demo-app/app/plugins/auth.ts, ../sm-demo-app/vue-demo-app/app/pages/forum/index.vue, ../sm-demo-app/vue-demo-app/app/pages/forum/t/[id].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-demo-app/scripts/smoke.sh</files>
|
||||
<read_first>../vue-faq-layer/{nuxt.config.ts,package.json,README.md,app/utils/client.ts,app/composables/useFaqApi.ts,tests/client.test.ts}, ../vue-paymentgateway-layer/{nuxt.config.ts,app/composables/usePaymentGatewayApi.ts}, ../sm-demo-app/plugins/golem15/forum/README.md and api/*.go (payloads), ../sm-demo-app/plugins/demo/classes/forum_access.go, ../sm-demo-app/vue-demo-app/{nuxt.config.ts,app/plugins/auth.ts,app/pages/chat.vue,app/pages/quizzes/index.vue}, ../sm-demo-app/scripts/check-local.sh (visitor_jwt, json_has, ALL_CHECKS) and smoke.sh (page loop)</read_first>
|
||||
<action>Per D-14, D-21, D-22, SC-2.
|
||||
|
||||
1. Create ../vue-forum-layer (`git init -b main`) with the vue-faq-layer skeleton:
|
||||
- package.json: name vue-forum-layer, version 0.1.0, main nuxt.config.ts, peer nuxt 4.4.8, dependency @nuxtjs/i18n 10.4.0, dev deps nuxt, vue and typescript at the workspace pins, test `node --test --test-reporter=tap tests/*.test.ts`;
|
||||
- nuxt.config.ts: header comment, currentDir, runtimeConfig `forum.apiBase` (NUXT_FORUM_API_BASE) and `public.forum.apiBase: '/_forum/api/v1'`, the EN/PL i18n registration, absolute css path.
|
||||
2. app/utils/forumClient.ts `createForumClient({ base, token, fetch })`: categories, threads (per category and all), posts, createThread, reply, editPost, deletePost, deleteThread, markRead, unread and preview, each building `/contexts/{type}/{id}/...` paths, plus the 422 field-error mapping. app/composables/useForumApi.ts binds it to runtime config and `$forumAuth`. useForum keeps the selected category, the threads, and the open thread and posts.
|
||||
3. Components:
|
||||
- ForumPage (props contextType, contextId, threadPath(id)): ForumCategoryList plus ForumThreadList with a "new thread" ForumPostEditor;
|
||||
- ForumThread (props contextType, contextId, threadId): each post renders the server's content_html as raw HTML (the only raw-HTML binding in the layer), with author and date, reply editor, Edit and Delete on own posts and on all posts when the API reports manage rights (a refused delete hides the action);
|
||||
- ForumPostEditor: textarea plus a server preview button.
|
||||
Mark the thread read on open.
|
||||
4. vue-demo-app:
|
||||
- run `pnpm add vue-forum-layer@link:../../vue-forum-layer` (development pin, committed; Task 7 swaps it for the tag before any push);
|
||||
- nuxt.config.ts: extends gains 'vue-forum-layer'; goPaths gains '/_forum'; `forum.apiBase` comes from `api`;
|
||||
- auth.ts provides `forumAuth`;
|
||||
- app/pages/forum/index.vue and forum/t/[id].vue: for members, ClientOnly ForumPage and ForumThread with context demo/1 and `threadPath: id => '/forum/t/' + id`; for anonymous visitors, a server-rendered description and a register link with next set to the current path (D-21, the chat page pattern). useSeoMeta on both;
|
||||
- EN/PL copy: forum.title, forum.lead and forum.registerToJoin.
|
||||
5. check-local.sh `check_forum`:
|
||||
- with the visitor JWT, GET /_forum/api/v1/contexts/demo/1/categories answers 200 with at least three categories;
|
||||
- POST a reply to the first thread answers 201 and its content_html holds a rendered paragraph;
|
||||
- GET $NUXT/forum without a cookie holds the register link with next=%2Fforum.
|
||||
Add it to ALL_CHECKS. smoke.sh adds /forum to the page loop.
|
||||
6. tests/client.test.ts: URL building for every method, the Bearer header, and the 422 mapping.
|
||||
|
||||
Commit the layer (`feat: forum layer: categories, threads and Markdown posts`), vue-demo-app (package.json and pnpm-lock.yaml with the link pin, pages, config, auth and locales) and sm-demo-app scripts separately.</action>
|
||||
<verify>
|
||||
<automated>pnpm --dir ../vue-forum-layer test && pnpm --dir ../sm-demo-app/vue-demo-app run build && ../sm-demo-app/scripts/dev.sh up && ../sm-demo-app/scripts/check-local.sh forum</automated>
|
||||
<fails_when>Non-zero exit: the forum client tests fail, the Nuxt build fails, the stack does not start, or check-local prints "check-local: forum failed" (categories missing, the reply not rendered, or no register link for anonymous visitors).</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -rniE 'golem15\.demo|sm-demo|demo/1' ../vue-forum-layer/app ../vue-forum-layer/nuxt.config.ts ../vue-forum-layer/README.md | wc -l` prints 0.
|
||||
- `grep -rn 'v-html' ../vue-forum-layer/app/components | grep -vc 'content_html'` prints 0.
|
||||
- `grep -c "'/_forum'" ../sm-demo-app/vue-demo-app/nuxt.config.ts` prints 1.
|
||||
</acceptance_criteria>
|
||||
<done>Members read and post in the seeded forum on the demo site through a new shared layer; anonymous visitors are invited to register.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Friends: list, requests and groups on /friends, linked from the chat sidebar and the account area</name>
|
||||
<files>../vue-userfriends-layer/.gitignore, ../vue-userfriends-layer/.nvmrc, ../vue-userfriends-layer/package.json, ../vue-userfriends-layer/pnpm-lock.yaml, ../vue-userfriends-layer/pnpm-workspace.yaml, ../vue-userfriends-layer/tsconfig.json, ../vue-userfriends-layer/nuxt.config.ts, ../vue-userfriends-layer/README.md, ../vue-userfriends-layer/.playground/nuxt.config.ts, ../vue-userfriends-layer/.playground/app/app.vue, ../vue-userfriends-layer/app/utils/friendsClient.ts, ../vue-userfriends-layer/app/composables/useFriendsApi.ts, ../vue-userfriends-layer/app/composables/useFriends.ts, ../vue-userfriends-layer/app/components/Friends/Page.vue, ../vue-userfriends-layer/app/components/Friends/List.vue, ../vue-userfriends-layer/app/components/Friends/Requests.vue, ../vue-userfriends-layer/app/components/Friends/Groups.vue, ../vue-userfriends-layer/app/components/Friends/AddFriend.vue, ../vue-userfriends-layer/app/assets/css/friends-base.css, ../vue-userfriends-layer/i18n/locales/en.json, ../vue-userfriends-layer/i18n/locales/pl.json, ../vue-userfriends-layer/tests/client.test.ts, ../sm-demo-app/vue-demo-app/package.json, ../sm-demo-app/vue-demo-app/nuxt.config.ts, ../sm-demo-app/vue-demo-app/app/plugins/auth.ts, ../sm-demo-app/vue-demo-app/app/pages/friends.vue, ../sm-demo-app/vue-demo-app/app/pages/chat.vue, ../sm-demo-app/vue-demo-app/app/pages/account/security.vue, ../sm-demo-app/vue-demo-app/app/components/SiteHeader.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-demo-app/scripts/smoke.sh</files>
|
||||
<read_first>../sm-demo-app/plugins/golem15/userfriends/README.md and api/friends.go and api/groups.go (bodies, 422 behaviour), ../vue-forum-layer (Task 1, same skeleton), ../vue-chat-layer/README.md (sidebar-footer slot), ../sm-demo-app/vue-demo-app/{app/pages/chat.vue,app/components/SiteHeader.vue,app/pages/account/security.vue,app/plugins/auth.ts}, ../sm-demo-app/plugins/demo/chat_api.go (members endpoint)</read_first>
|
||||
<action>Per D-15.
|
||||
- Create ../vue-userfriends-layer with the Task 1 skeleton (runtime `userfriends.apiBase`, `public.userfriends.apiBase: '/_userfriends/api/v1'`).
|
||||
- app/utils/friendsClient.ts `createFriendsClient`: friends, incoming, sent, request({user_id} or {username}), accept, reject, cancel, unfriend, groups, group, createGroup, updateGroup, deleteGroup, join, leave, members, kick, invite, joinByCode. useFriendsApi binds `$userfriendsAuth`.
|
||||
- Components:
|
||||
- FriendsList: friends with Unfriend;
|
||||
- FriendsRequests: incoming with Accept and Reject, sent with Cancel;
|
||||
- FriendsAddFriend: picks from `$userfriendsDirectory()` when provided, else a username field; the generic 422 shows the server message;
|
||||
- FriendsGroups: my groups, create (name, visibility as the API allows), join by code, leave, member list;
|
||||
- FriendsPage: all four in sections.
|
||||
- vue-demo-app:
|
||||
- pnpm link dependency; extends 'vue-userfriends-layer'; goPaths '/_userfriends'; runtime config;
|
||||
- auth.ts provides `userfriendsAuth` and `userfriendsDirectory` (GET /_demo/api/v1/chat/members with the JWT, mapped to {id, name: name plus surname});
|
||||
- app/pages/friends.vue: ClientOnly FriendsPage for members, the register prompt for anonymous visitors;
|
||||
- chat.vue passes `<template #sidebar-footer>` with a NuxtLink to /friends;
|
||||
- SiteHeader shows "Friends" for members next to Security, and account/security.vue links to /friends;
|
||||
- EN/PL copy.
|
||||
- check-local.sh `check_friends`, repeatable without touching seed data:
|
||||
- with the visitor JWT, GET /_userfriends/api/v1/friends/requests lists the seeded pending request (read-only);
|
||||
- two fresh users from the throwaway registration helper (extend it so the caller also gets the e-mail it registered): A sends POST /friends/request with {email: B's e-mail} and gets 2xx, B finds the request in GET /friends/requests and accepts it with 2xx, and A's GET /friends lists B.
|
||||
Add it to ALL_CHECKS. smoke.sh adds /friends.
|
||||
- tests/client.test.ts: URL building, Bearer header, 422 mapping.
|
||||
|
||||
Commit the layer, vue-demo-app (with the link pin) and the scripts separately.</action>
|
||||
<verify>
|
||||
<automated>pnpm --dir ../vue-userfriends-layer test && pnpm --dir ../sm-demo-app/vue-demo-app run build && ../sm-demo-app/scripts/check-local.sh friends forum chat</automated>
|
||||
<fails_when>Non-zero exit: the friends client tests or the build fail, or check-local reports friends, forum or chat failed (the seeded request missing, accept refused, or the chat page broken by the slot).</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c 'sidebar-footer' ../sm-demo-app/vue-demo-app/app/pages/chat.vue` prints 1.
|
||||
- `grep -c "to=\"/friends\"" ../sm-demo-app/vue-demo-app/app/components/SiteHeader.vue` prints 1.
|
||||
- `grep -rniE 'golem15\.demo|sm-demo|_demo/api' ../vue-userfriends-layer/app ../vue-userfriends-layer/nuxt.config.ts ../vue-userfriends-layer/README.md | wc -l` prints 0.
|
||||
</acceptance_criteria>
|
||||
<done>Members manage friends, requests and friend groups on /friends, reachable from the chat sidebar and the account area.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking-human">
|
||||
<name>Task 3: Package legitimacy check before installing livekit-client</name>
|
||||
<read_first>20-RESEARCH.md Package Legitimacy Audit (livekit-client flagged [SUS] too-new for its latest release; assumption A7), the output Claude prints before asking: `npm view livekit-client versions --json | tail -15`, `npm view livekit-client time --json` for the last five versions, `npm view livekit-client repository.url maintainers dist-tags scripts --json`, and the proposed exact version (the newest release at least 7 days old)</read_first>
|
||||
<acceptance_criteria>
|
||||
- The user approved a named exact version (recorded in the SUMMARY) or rejected the package; Task 4 installs only that version with `--save-exact`.
|
||||
</acceptance_criteria>
|
||||
<what-built>Nothing is installed yet. Claude proposes an exact livekit-client version for vue-chatvideo-layer, the client SDK the minimal call page (D-13) is built on.</what-built>
|
||||
<how-to-verify>
|
||||
1. Open https://www.npmjs.com/package/livekit-client and check that the publisher and maintainers are the LiveKit organization and the repository is github.com/livekit/client-sdk-js.
|
||||
2. Check that the proposed version exists, is at least 7 days old and that `scripts` shows no install or postinstall hook.
|
||||
3. Check the client SDK's server compatibility notes for LiveKit server 1.13.1 (the rome and local binary).
|
||||
</how-to-verify>
|
||||
<resume-signal>Reply "approved <version>" or "rejected" with the reason.</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Video calls from DMs: the call button, the minimal call page and incoming-call prompts</name>
|
||||
<precondition>Task 3 answered "approved <version>"; LiveKit answers on http://127.0.0.1:7890/ (dev stack).</precondition>
|
||||
<files>../vue-chatvideo-layer/.gitignore, ../vue-chatvideo-layer/.nvmrc, ../vue-chatvideo-layer/package.json, ../vue-chatvideo-layer/pnpm-lock.yaml, ../vue-chatvideo-layer/pnpm-workspace.yaml, ../vue-chatvideo-layer/tsconfig.json, ../vue-chatvideo-layer/nuxt.config.ts, ../vue-chatvideo-layer/README.md, ../vue-chatvideo-layer/.playground/nuxt.config.ts, ../vue-chatvideo-layer/.playground/app/app.vue, ../vue-chatvideo-layer/app/lib/videoClient.ts, ../vue-chatvideo-layer/app/lib/callMachine.ts, ../vue-chatvideo-layer/app/composables/useCall.ts, ../vue-chatvideo-layer/app/composables/useIncomingCalls.ts, ../vue-chatvideo-layer/app/components/Chatvideo/Call.vue, ../vue-chatvideo-layer/app/components/Chatvideo/Tile.vue, ../vue-chatvideo-layer/app/components/Chatvideo/CallButton.vue, ../vue-chatvideo-layer/app/components/Chatvideo/IncomingCall.vue, ../vue-chatvideo-layer/app/assets/css/chatvideo-base.css, ../vue-chatvideo-layer/i18n/locales/en.json, ../vue-chatvideo-layer/i18n/locales/pl.json, ../vue-chatvideo-layer/tests/callMachine.test.ts, ../sm-demo-app/vue-demo-app/package.json, ../sm-demo-app/vue-demo-app/nuxt.config.ts, ../sm-demo-app/vue-demo-app/app/pages/call/[conversation].vue, ../sm-demo-app/vue-demo-app/app/pages/chat.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/scripts/check-local.sh, .planning/phases/20-demo-instance-with-every-shared-plugin/COVERAGE.md</files>
|
||||
<read_first>../sm-demo-app/plugins/golem15/chatvideo/README.md, api/video.go, classes/video_service.go (Start, Join, Leave, End, declined path), classes/notifier.go (event names), ../vue-chat-layer/app/lib/realtime.ts (onUserEvent, subscribe) and README.md (slots), the livekit-client README and API docs for the approved version (Room, RoomEvent, Track, connect, localParticipant.setCameraEnabled and setMicrophoneEnabled, track attach and detach; through Context7 or ctx7 when available), ../sm-demo-app/vue-demo-app/{app/pages/chat.vue,app/layouts/default.vue}, .planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/COVERAGE.md (matrix format), 20-RESEARCH.md (Pitfalls 8 and 9, assumption A1)</read_first>
|
||||
<action>Per D-11, D-12, D-13, D-23.
|
||||
|
||||
1. Create ../vue-chatvideo-layer with the skeleton:
|
||||
- dependencies `livekit-client@<approved>` installed with `--save-exact`, and @nuxtjs/i18n 10.4.0;
|
||||
- peerDependencies nuxt 4.4.8 and `vue-chat-layer: "^0.1.0"`; devDependency vue-chat-layer at the git+https v0.1.0 tag for the playground;
|
||||
- test script `vitest run` with vitest 3.2.7;
|
||||
- runtime `public.chatvideo.apiBase: '/_chatvideo/api/v1'`;
|
||||
- no `extends` of vue-chat-layer (the app extends both).
|
||||
2. app/lib/callMachine.ts (pure): states idle, starting, connecting, connected, ended and failed, and `transition(state, event)` for the events start, joined(token, url), connected, participantJoined, participantLeft, mute, unmute, cameraOff, cameraOn, leave, ended(reason) and error(code). Mute and camera flags live in the state. Token errors (401, 403, 404, 422 too many participants) map to failed with a reason key.
|
||||
3. app/lib/videoClient.ts: start, join, leave, end and status over `/conversations/{id}/video/...` with the `$chatAuth` token getter.
|
||||
4. app/composables/useCall.ts:
|
||||
- drives the machine; on start or join, `await import('livekit-client')`, creates a Room (adaptiveStream and dynacast on), connects with livekit_url and token, and enables camera and microphone;
|
||||
- maps RoomEvent participant and track events to tiles and attaches and detaches tracks to video elements;
|
||||
- offers mute and camera toggles through the local participant, and leave (disconnect, then POST leave);
|
||||
- subscribes to the session channel through vue-chat-layer's `subscribe` for ended and declined events, with the channel name always built by an exported helper `videoSessionChannel(id)` in app/lib/videoClient.ts, and disconnects on route leave and pagehide.
|
||||
useIncomingCalls registers `onUserEvent` for `video:call:incoming` (never its own user:{me} subscription) and exposes the pending call.
|
||||
5. Components:
|
||||
- ChatvideoCall (prop conversationId, start boolean): own camera tile, remote tiles (ChatvideoTile), mute, camera and leave buttons, the state text and failure messages from i18n;
|
||||
- ChatvideoCallButton (props conversationId and callPath(id)): starts and navigates;
|
||||
- ChatvideoIncomingCall: the caller name from the event payload, Accept (navigate to callPath) and Decline (POST leave; confirm in video_service.go that this publishes declined for a participant who never joined, else use the endpoint that does).
|
||||
No screen share (D-13).
|
||||
6. vue-demo-app:
|
||||
- pnpm link dependency; extends gains 'vue-chatvideo-layer' after 'vue-chat-layer'; goPaths '/_chatvideo';
|
||||
- app/pages/call/[conversation].vue: ClientOnly ChatvideoCall for members (start when the query has start=1), the register prompt otherwise;
|
||||
- chat.vue `<template #conversation-actions="{ conversation }">` renders ChatvideoCallButton with callPath `id => '/call/' + id + '?start=1'` (the slot only renders in DMs, D-23);
|
||||
- default.vue renders ClientOnly ChatvideoIncomingCall for members;
|
||||
- EN/PL copy (call.title, call.registerToCall).
|
||||
7. check-local.sh `check_call`:
|
||||
- with the visitor JWT, POST /_chat/api/v1/contexts/demo/1/dm to a persona gives the DM id;
|
||||
- POST /_chatvideo/api/v1/conversations/{dm}/video/start answers 2xx with livekit_url and token;
|
||||
- GET http://127.0.0.1:7890/rtc/validate?access_token=<token> answers 200 (LiveKit accepts the token, proving the keys and the room);
|
||||
- POST .../video/end answers 2xx.
|
||||
Add `call` to ALL_CHECKS.
|
||||
8. tests/callMachine.test.ts (smoke): start, joined, connected, mute, leave and ended; a 422 start ends in failed with the too-many reason.
|
||||
9. COVERAGE.md in the phase directory (format of 19.4.1's):
|
||||
- livekit-client capabilities (Room connect and disconnect, publish camera and microphone, subscribe and attach remote tracks, mute and unmute, camera toggle, participant events, connection state and reconnection, adaptiveStream and dynacast INTEGRATE; screen share, data channel, device selection, E2EE, recording and egress, simulcast tuning, audio-only mode OPT-OUT, each with a reason);
|
||||
- LiveKit server through chatvideo (room create and delete, token mint, list participants, reconcile INTEGRATE; egress, ingress, SIP and webhooks OPT-OUT);
|
||||
- Centrifugo (the chat, user and video namespaces with presence on chat INTEGRATE; history, recovery and client publish OPT-OUT);
|
||||
- Web Push through herald (VAPID keys and the public key route INTEGRATE; subscribe UI and push on chat events OPT-OUT, deferred per D-16).
|
||||
|
||||
Commit the layer, vue-demo-app (with the link pin), the scripts and COVERAGE.md (planning commit) separately.</action>
|
||||
<verify>
|
||||
<automated>pnpm --dir ../vue-chatvideo-layer test && pnpm --dir ../sm-demo-app/vue-demo-app run build && ../sm-demo-app/scripts/check-local.sh call chat</automated>
|
||||
<fails_when>Non-zero exit: the call machine tests fail, the build fails (livekit-client not resolvable or a type error), or check-local reports call failed (start refused, no token or livekit_url, LiveKit's rtc/validate not 200) or chat failed.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c "import('livekit-client')" ../vue-chatvideo-layer/app/composables/useCall.ts` prints 1 and `grep -rn "^import .*from 'livekit-client'" ../vue-chatvideo-layer/app | grep -vc 'import type'` prints 0 (no static runtime import).
|
||||
- `grep -rn 'subscribe(' ../vue-chatvideo-layer/app | grep -vc 'videoSessionChannel'` prints 0 (the layer's only subscriptions are session channels; incoming calls come through onUserEvent).
|
||||
- `node -p "require('../vue-chatvideo-layer/package.json').dependencies['livekit-client']"` prints the approved version with no range character.
|
||||
- `grep -c 'conversation-actions' ../sm-demo-app/vue-demo-app/app/pages/chat.vue` prints 1.
|
||||
- COVERAGE.md has a reason on every OPT-OUT row (`grep -c 'OPT-OUT | *|' .planning/phases/20-demo-instance-with-every-shared-plugin/COVERAGE.md` prints 0).
|
||||
</acceptance_criteria>
|
||||
<done>A member can start a video call from a DM, the other member gets an accept prompt on any page, and both meet on a minimal call page served by LiveKit.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 5: Herald's VAPID keys without UI, and the Forum, Friends and Calls tour cards</name>
|
||||
<files>../sm-demo-app/scripts/dev.sh, ../sm-demo-app/deploy/env.example, ../sm-demo-app/README.md, ../sm-demo-app/vue-demo-app/app/pages/index.vue, ../sm-demo-app/vue-demo-app/app/components/TourCard.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</files>
|
||||
<read_first>../sm-demo-app/plugins/golem15/herald/README.md (keys, herald:vapid-generate, presence gate warning), ../sm-demo-app/scripts/dev.sh (ensure_env, build, up), ../sm-demo-app/deploy/env.example, ../sm-demo-app/vue-demo-app/app/pages/index.vue, app/components/TourCard.vue, i18n/locales/*.json (tour.*), ../sm-demo-app/scripts/check-local.sh (check_tour)</read_first>
|
||||
<action>Per D-16 and the tour part of D-03.
|
||||
- dev.sh: after build in `up` and `seed`, when .env lacks SUMMER_GOLEM15__HERALD__VAPID__PUBLIC_KEY, run `./bin/demo herald:vapid-generate --json -q`, append SUMMER_GOLEM15__HERALD__VAPID__PUBLIC_KEY and __PRIVATE_KEY from the JSON (python3 parse, umask 077, values never echoed) and SUMMER_GOLEM15__HERALD__VAPID__SUBJECT=mailto:demo@example.test. Print only "added the herald VAPID keys to .env".
|
||||
- deploy/env.example: the three herald key names with empty values and a comment (generated on rome with herald:vapid-generate; no subscribe UI in this phase, D-16). README "Chat family" section: herald is mounted without UI.
|
||||
- Tour:
|
||||
- index.vue stops in order journal, presentations, quizzes, chat, forum (/forum), friends (/friends), calls (/chat with text tour.calls.text), payments, faq, accounts;
|
||||
- TourCard icons forum, friends and calls added to the union and ICONS (outline, 24-unit grid, currentColor);
|
||||
- en and pl tour.forum, tour.friends and tour.calls titles and texts, and the heading for ten cards.
|
||||
- check-local.sh check_tour expects ten cards including Forum, Friends and Calls (EN) and their Polish titles. New `check_herald`: GET $GO/_herald/api/v1/push/vapid-public-key answers 401 without a JWT and 200 with the visitor's JWT and a non-empty publicKey (json_has); add `herald` to ALL_CHECKS and the header list.
|
||||
|
||||
Commit sm-demo-app (dev.sh, env.example, README, check-local) and vue-demo-app (tour) separately.</action>
|
||||
<verify>
|
||||
<automated>pnpm --dir ../sm-demo-app/vue-demo-app run build && ../sm-demo-app/scripts/dev.sh up && grep -q '^SUMMER_GOLEM15__HERALD__VAPID__PUBLIC_KEY=.' ../sm-demo-app/.env && ../sm-demo-app/scripts/check-local.sh herald tour</automated>
|
||||
<fails_when>Non-zero exit: the build fails, dev.sh does not write the VAPID public key into .env, or check-local reports herald failed (the public-key route is not 200 with a non-empty publicKey for the visitor, or not 401 without a JWT) or tour failed (card count or titles wrong).</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c 'HERALD__VAPID__PRIVATE_KEY=' ../sm-demo-app/deploy/env.example` prints 1, with no value after the equals sign.
|
||||
- `grep -c "'/_herald'" ../sm-demo-app/vue-demo-app/nuxt.config.ts` prints 0.
|
||||
- `grep -cE "key: '(forum|friends|calls)'" ../sm-demo-app/vue-demo-app/app/pages/index.vue` prints 3.
|
||||
- dev.sh output contains no line with a key value (manual look at the `up` output, recorded in the SUMMARY).
|
||||
</acceptance_criteria>
|
||||
<done>Herald runs with VAPID keys and no UI, and the tour shows all ten features in EN and PL.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking">
|
||||
<name>Task 6: Go-ahead to create the forum, friends and video-call layer repositories, push them and tag v0.1.0</name>
|
||||
<read_first>Claude's summary: `git log --oneline` of the three layers, their README install lines, vue-faq-layer's visibility, whether `tea login list` shows a git.golem15.com login, the livekit-client version approved in Task 3, and the results of check-local forum friends call tour chat.</read_first>
|
||||
<acceptance_criteria>
|
||||
- The user chose one option and it is recorded in the SUMMARY; with "release", the three repositories exist on git.golem15.com (`git ls-remote` exits 0 for each) before Task 7 pushes, or Task 7 creates them with tea.
|
||||
</acceptance_criteria>
|
||||
<decision>Create vue-forum-layer, vue-userfriends-layer and vue-chatvideo-layer on git.golem15.com, push main and tag v0.1.0 each?</decision>
|
||||
<context>Each layer is public with tagged releases that later sites pin (like D-01 for chat). v0.1.0 freezes their hooks, props and runtime keys. vue-chatvideo-layer's peer dependency on vue-chat-layer ^0.1.0 is part of its contract. vue-demo-app can pin them only after this. Repository creation and pushes need your go-ahead.</context>
|
||||
<options>
|
||||
<option id="release">
|
||||
<name>Create, push and tag all three</name>
|
||||
<pros>vue-demo-app pins the tags; the deploy (plan 05) can build.</pros>
|
||||
<cons>The v0.1.0 contracts are public.</cons>
|
||||
</option>
|
||||
<option id="hold">
|
||||
<name>Hold</name>
|
||||
<pros>Contracts stay open for changes.</pros>
|
||||
<cons>The deploy is blocked; vue-demo-app keeps local link pins, so it cannot be pushed.</cons>
|
||||
</option>
|
||||
</options>
|
||||
<instructions>For "release" without a tea login: create the three repositories (empty, default branch main, vue-faq-layer's visibility) in the git.golem15.com web UI, then reply. Do not paste tokens.</instructions>
|
||||
<resume-signal>Reply "release" (add "repos ready" if you created them) or "hold".</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 7: Release the three layers v0.1.0 and pin them in vue-demo-app</name>
|
||||
<reversibility rating="one-way">Pushed layer tags are installed by consumers' lockfiles; they can be superseded but must never move.</reversibility>
|
||||
<precondition>Task 6 resolved with "release".</precondition>
|
||||
<files>../vue-forum-layer/README.md, ../vue-userfriends-layer/README.md, ../vue-chatvideo-layer/README.md, ../sm-demo-app/vue-demo-app/package.json, ../sm-demo-app/vue-demo-app/pnpm-lock.yaml</files>
|
||||
<read_first>The three READMEs, ../sm-demo-app/vue-demo-app/package.json, the Task 6 answer</read_first>
|
||||
<action>For each of the three layers:
|
||||
- create the repository with tea if missing, at vue-faq-layer's visibility;
|
||||
- add the origin `git@git.golem15.com:golem15/<layer>.git` and push main;
|
||||
- fix a README install line that says the tag does not exist before tagging;
|
||||
- create the annotated tag v0.1.0, push it and check `ls-remote` against HEAD.
|
||||
Then in vue-demo-app run `pnpm add` with the three `git+https://git.golem15.com/golem15/<layer>.git#v0.1.0` specs (replacing the link pins), run the tests and the build, and commit package.json and pnpm-lock.yaml (`feat: pin the forum, friends and video-call layers v0.1.0`). Do not push vue-demo-app.</action>
|
||||
<verify>
|
||||
<automated>for l in vue-forum-layer vue-userfriends-layer vue-chatvideo-layer; do [ "$(git -C ../$l ls-remote origin 'refs/tags/v0.1.0^{}' | cut -f1)" = "$(git -C ../$l rev-parse HEAD)" ] || exit 1; grep -q "$l.git#v0.1.0" ../sm-demo-app/vue-demo-app/package.json || exit 1; done && pnpm --dir ../sm-demo-app/vue-demo-app test && pnpm --dir ../sm-demo-app/vue-demo-app run build && ../sm-demo-app/scripts/check-local.sh forum friends call chat tour</automated>
|
||||
<fails_when>Non-zero exit: a remote tag is missing or differs from HEAD, a tag pin is missing from package.json, the vue-demo-app tests or build fail, or a check-local check fails against the tagged layers.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `git -C ../sm-demo-app/vue-demo-app status --porcelain -- package.json pnpm-lock.yaml` prints nothing.
|
||||
- `node -p "Object.values(require('../sm-demo-app/vue-demo-app/package.json').dependencies).filter(v=>v.startsWith('link')).length"` prints 0.
|
||||
</acceptance_criteria>
|
||||
<done>Forum, friends and video-call layers are published at v0.1.0 and the demo site builds against all tagged layers.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| forum posts and friend names to other members' browsers | untrusted content |
|
||||
| browser to LiveKit (signal through nginx, media direct) | WebRTC with a short-lived join token |
|
||||
| realtime events to the incoming-call prompt | payloads from the server about other users |
|
||||
| npm registry to the chatvideo layer | a new third-party dependency (livekit-client) |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-20-20 | Tampering | forum stored XSS | high | mitigate | posts render only content_html, which the server sanitizes on write with breakwater (D-18 of the forum port); Markdown is previewed through the server endpoint; acceptance grep limits v-html to content_html |
|
||||
| T-20-21 | Spoofing | call joins | high | mitigate | tokens are minted by chatvideo only for conversation participants (sessionForActor) with token_ttl 900; the client never builds tokens; LiveKit room max_participants 4 (D-12) |
|
||||
| T-20-22 | Information disclosure | camera and microphone | medium | mitigate | media starts only after an explicit start or accept on the call page; leave and pagehide disconnect and stop local tracks |
|
||||
| T-20-23 | Tampering | incoming-call payloads | low | mitigate | caller names render as text bindings; Accept navigates only to the app's callPath built from the numeric conversation id |
|
||||
| T-20-24 | Denial of service | call spam | medium | mitigate | server bucket video-initiate 5/min, max_duration 900 and the reconcile job (D-12); calls only offered in DMs (D-23) |
|
||||
| T-20-25 | Information disclosure | friend enumeration | low | mitigate | the API's generic 422 for refused requests is shown as is; the directory lists demo members only (the same set the chat member picker shows) |
|
||||
| T-20-26 | Information disclosure | VAPID private key | high | mitigate | generated into .env only (umask 077, never echoed); env.example holds names; /_herald is not routed by the site's goPaths |
|
||||
| T-20-SC | Tampering | npm installs | high | mitigate | livekit-client installed only after the blocking human-verify legitimacy checkpoint (Task 3, gate blocking-human, never auto-approved) at an exact version with no install scripts; every other package is an existing workspace pin |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- Task 1: forum client tests, app build, check-local forum.
|
||||
- Task 2: friends client tests, build, check-local friends forum chat.
|
||||
- Task 3: livekit-client version approved by the user.
|
||||
- Task 4: call machine tests, build, check-local call chat, COVERAGE.md complete.
|
||||
- Task 5: build, VAPID keys in .env, herald route present, check-local tour.
|
||||
- Task 6: go-ahead recorded.
|
||||
- Task 7: three remote tags equal HEAD; vue-demo-app tests, build and checks green on tags.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
The demo site shows the forum, friends and DM video calls through three new shared layers (SC-2, D-11 to D-15, D-21, D-23), herald is configured without UI (D-16), and the tour lists every feature.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/20-demo-instance-with-every-shared-plugin/20-04-SUMMARY.md` when done: the approved livekit-client version, the decline endpoint used, each layer's public API, the three tags and commits, and both checkpoint answers.
|
||||
</output>
|
||||
@@ -0,0 +1,359 @@
|
||||
---
|
||||
phase: 20-demo-instance-with-every-shared-plugin
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 5
|
||||
depends_on: ["20-02", "20-03", "20-04"]
|
||||
files_modified:
|
||||
- ../sm-demo-app/deploy/nginx/demo.summercms.io.conf
|
||||
- ../sm-demo-app/deploy/supervisor/summercms-demo-livekit.conf
|
||||
- ../sm-demo-app/deploy/livekit.yaml.example
|
||||
- ../sm-demo-app/deploy/env.example
|
||||
- ../sm-demo-app/config/env/production/golem15.chatvideo.yaml
|
||||
- ../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
|
||||
autonomous: false
|
||||
requirements: [SC-2, SC-3, D-10, D-12, D-16, D-20, D-21]
|
||||
estimate:
|
||||
tokens: 240000
|
||||
raw_tokens: 240000
|
||||
tasks: 6
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "D-10: rome runs the demo's own LiveKit 1.13.1 under supervisor as summercms-demo-livekit (user summercms-demo, config /srv/summercms-demo/livekit.yaml, 0640 root:summercms-demo, own key pair also in /srv/summercms-demo/.env), signal on 127.0.0.1:7890 behind nginx location /livekit/, ICE TCP 7891 and UDP mux 7892 on the public address; it does not touch WavePath's LiveKit (7880, 7881, 50000-60000) or its keys."
|
||||
- "D-10: Vuurmuur on rome allows TCP 7891 and UDP 7892 from the internet to the firewall through a rule the user approved; DEPLOY.md documents the service, the rule, how to verify it and how to remove it."
|
||||
- "D-12: the deployed LiveKit has room max_participants 4, empty_timeout 120 and departure_timeout 20; production chatvideo config keeps max_duration 900 and max_participants 4; the chatvideo reconcile job runs every 5 minutes inside serve."
|
||||
- "D-16: rome's .env holds herald VAPID keys generated on rome with herald:vapid-generate; /_herald is not routed by nginx."
|
||||
- "SC-3: after the release https://demo.summercms.io serves /chat, /forum, /friends and /call/{id} with the banner and the backend call to action; /backend and /backend/x answer 404; anonymous /chat and /forum show the register prompt (D-04, D-21); the admin stays at /smbackend as decided in 19.4.1."
|
||||
- "SC-2: on the live demo the chat-family APIs answer 401 without a JWT under /_chat, /_chatchannels, /_chatbots, /_chatvideo, /_forum and /_userfriends, /livekit/rtc/validate reaches LiveKit, /ws upgrades to Centrifugo, and the Centrifugo config on rome has the chat, user and video namespaces."
|
||||
- "scripts/check-deploy.sh proves locally that nginx, supervisor, livekit.yaml.example and the production chatvideo config agree on ports (7890, 7891, 7892) and caps, that each chat-family prefix goes to the Go port and that /_herald is not routed; scripts/check-live.sh https://demo.summercms.io prints check-live: ok after the release."
|
||||
- "vue-demo-app's master is pushed before sm-demo-app's pointer bump commit; sm-demo-app is pushed with the go-ahead; robots.txt on the live site disallows the new API prefixes."
|
||||
artifacts:
|
||||
- path: "../sm-demo-app/deploy/supervisor/summercms-demo-livekit.conf"
|
||||
provides: "supervisor program summercms-demo-livekit"
|
||||
contains: "program:summercms-demo-livekit"
|
||||
- path: "../sm-demo-app/deploy/nginx/demo.summercms.io.conf"
|
||||
provides: "Go locations for the chat family and the /livekit/ WebSocket location"
|
||||
contains: "location ^~ /livekit/"
|
||||
- path: "../sm-demo-app/config/env/production/golem15.chatvideo.yaml"
|
||||
provides: "production livekit_url and livekit_admin_url"
|
||||
contains: "wss://demo.summercms.io/livekit"
|
||||
- path: "../sm-demo-app/DEPLOY.md"
|
||||
provides: "LiveKit, firewall, chat family release, verify and rollback"
|
||||
contains: "summercms-demo-livekit"
|
||||
key_links:
|
||||
- from: "../sm-demo-app/deploy/nginx/demo.summercms.io.conf"
|
||||
to: "../sm-demo-app/deploy/livekit.yaml.example"
|
||||
via: "proxy_pass http://127.0.0.1:7890/ equals LiveKit port 7890 (check-deploy compares)"
|
||||
pattern: "127\\.0\\.0\\.1:7890"
|
||||
- from: "../sm-demo-app/config/env/production/golem15.chatvideo.yaml"
|
||||
to: "../sm-demo-app/deploy/supervisor/summercms-demo-livekit.conf"
|
||||
via: "livekit_admin_url http://127.0.0.1:7890 is the program's signal port"
|
||||
pattern: "livekit_admin_url"
|
||||
- from: "../sm-demo-app/scripts/check-live.sh"
|
||||
to: "../sm-demo-app/deploy/nginx/demo.summercms.io.conf"
|
||||
via: "live 401s under the chat-family prefixes and LiveKit's answer on /livekit/rtc/validate"
|
||||
pattern: "/livekit/rtc/validate"
|
||||
prohibitions:
|
||||
- statement: "No change to WavePath's LiveKit program, its config /etc/livekit/livekit.yaml or its ports, and no second key on its server."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
- statement: "No secret value (LiveKit, VAPID, Centrifugo) appears in a committed file, the SUMMARY, a log line or a command line; values are generated on rome."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
- statement: "No push, rome change or firewall change happens before its checkpoint is answered; raw iptables is never used (Vuurmuur manages rome's INPUT chain)."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
ROADMAP Goal (not in user-story format; no story is invented here): the demo is deployed publicly with the chat family, the frontend open, `/backend` unreachable and the backend call to action shown.
|
||||
|
||||
This plan's slice (plan 5 of 6, wave 5, per D-20): the rome deploy of everything plans 01 to 04 built. That means the demo's own LiveKit under supervisor, the Vuurmuur rule for its media ports (a checkpoint, shared production host), nginx locations, `.env` keys (LiveKit, VAPID), the Centrifugo namespaces, the release, DEPLOY.md, and the check-deploy and check-live extensions.
|
||||
|
||||
## Decisions taken in this plan (Claude's discretion, recorded)
|
||||
|
||||
- **Media path (D-10 discretion):** ICE over the UDP mux port 7892 with ICE-TCP 7891 as fallback, and no embedded TURN. nginx `stream` with `ssl_preread` would restructure every 443 vhost on a shared host (RESEARCH Alternatives). If NAT'd browsers fail in plan 06's live check, the fallback recorded in DEPLOY.md is LiveKit's embedded TURN on UDP 3478 and TLS 5349 with a DNS-only host name, which needs a new checkpoint.
|
||||
- **Signal path:** same origin under `/livekit` (`livekit_url: wss://demo.summercms.io/livekit`). livekit-client appends `/rtc`, so nginx `location ^~ /livekit/` proxies to `http://127.0.0.1:7890/` with the WebSocket upgrade headers, `proxy_read_timeout 7d` and `proxy_buffering off`, as lk.wavepath.org does. Go talks to the RoomService on loopback (`livekit_admin_url: http://127.0.0.1:7890`).
|
||||
- **ICE reveals rome's public address** despite Cloudflare. This is accepted, because rome already exposes services on that address (RESEARCH Security Domain). DEPLOY.md says so.
|
||||
- **Admin stays at /smbackend** (19.4.1's decision change of 2026-10-20, RESEARCH Open Question 1). `/backend` stays 404. The deploy checkpoint restates it.
|
||||
- **Robots:** the new prefixes go into SEO › robots.txt on rome (the DEPLOY.md "SEO" procedure). `/_herald` stays unrouted and so needs no line.
|
||||
- spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip).
|
||||
|
||||
## Artifacts this phase produces (this plan)
|
||||
|
||||
- Supervisor program `summercms-demo-livekit` (`deploy/supervisor/summercms-demo-livekit.conf`); rome files `/srv/summercms-demo/livekit.yaml` and `/etc/supervisor/conf.d/summercms-demo-livekit.conf`.
|
||||
- nginx locations `^~ /_chat/`, `^~ /_chatchannels/`, `^~ /_chatbots/`, `^~ /_chatvideo/`, `^~ /_forum/`, `^~ /_userfriends/` (Go, 127.0.0.1:8098) and `^~ /livekit/` (LiveKit, 127.0.0.1:7890).
|
||||
- `config/env/production/golem15.chatvideo.yaml` (`livekit_url`, `livekit_admin_url`).
|
||||
- env keys on rome: `SUMMER_GOLEM15__CHATVIDEO__LIVEKIT_API_KEY`, `SUMMER_GOLEM15__CHATVIDEO__LIVEKIT_API_SECRET`, `SUMMER_GOLEM15__HERALD__VAPID__PUBLIC_KEY`, `__PRIVATE_KEY`, `__SUBJECT` (names only in git).
|
||||
- Vuurmuur service `summercms-demo-livekit` and its rule (rome).
|
||||
- check-deploy.sh LiveKit and chat-family checks; check-live.sh chat-family checks; DEPLOY.md sections "LiveKit (the demo's own)", "Firewall (Vuurmuur)", "Chat family"; the sm-demo-app pointer bump of vue-demo-app.
|
||||
|
||||
<objective>
|
||||
Deploy the chat family to the live demo on rome: LiveKit, firewall, nginx, keys, Centrifugo namespaces and the release, with the checks and the runbook to keep it repeatable.
|
||||
|
||||
Purpose: SC-3 (deployed publicly, /backend unreachable, call to action) and the live half of SC-2, D-10, D-12 and D-16 on rome.
|
||||
Output: commits in ../sm-demo-app, pushes of vue-demo-app and sm-demo-app, and changes on rome, all after the go-ahead.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-CONTEXT.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-RESEARCH.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-02-SUMMARY.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-04-SUMMARY.md
|
||||
|
||||
Repo: ../sm-demo-app (deploy files, scripts, DEPLOY.md), its submodule vue-demo-app (push only). Host: rome over `ssh -p 2222 debian@10.8.3.118` (passwordless sudo; the `rome` alias does not resolve here), with the variables `$RSH` and `$ROME` used in DEPLOY.md. Read-only on rome: WavePath's `/etc/livekit/livekit.yaml` and `wavepath-livekit` program, `/etc/vuurmuur/services`, `/etc/vuurmuur/rules/rules.conf`.
|
||||
|
||||
Rules: no co-author tags; stage by explicit path; push vue-demo-app first, then commit and push the pointer bump in sm-demo-app (core-plugins-own-repos note); secrets are generated on rome with `openssl rand` or `herald:vapid-generate` and written by remote commands that never echo values; follow DEPLOY.md's Build, Upload and Release sections; nothing changes on rome and nothing is pushed before Task 3's answer; the firewall changes only after Task 5's answer.
|
||||
|
||||
<interfaces>
|
||||
sm-demo-app deploy today: deploy/nginx/demo.summercms.io.conf (one `location ^~ /_<prefix>/` per Go prefix with proxy_pass http://127.0.0.1:8098, `location = /ws` to Centrifugo 8010 with Upgrade headers, `location ^~ /backend { return 404; }`, `/smbackend` to Go), deploy/supervisor/summercms-demo-centrifugo.conf (program shape), deploy/centrifugo.json.example (namespaces from plan 02), deploy/livekit.yaml.example (plan 02), scripts/check-deploy.sh (nginx -t in a temp prefix, supervisor parse, port agreement, prints "check-deploy: ok"), scripts/check-live.sh (bad, get, header, ws_upgrade helpers; prints "check-live: ok"), DEPLOY.md (Overview, Launch checklist, One-time server setup, Realtime, MCP endpoint, SEO, Build, Upload, Release, Verify, Rollback, Reset, Secrets).
|
||||
RESEARCH facts: rome LiveKit 1.13.1 at /usr/local/bin/livekit-server; WavePath's program uses 7880, 7881/tcp and 50000-60000/udp; ports 7890-7892 were free on 2026-10-28 (re-check with `ss -ltnup`); rome's INPUT policy is DROP under Vuurmuur (vrmr NFLOG prefixes); a Vuurmuur service Livekit exists but no rule uses it.
|
||||
LiveKit validate endpoint: GET /rtc/validate answers 401 without a token and 200 with a valid one.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: Tracer: the deploy files carry one chat-family release end to end, proven locally by check-deploy before rome is touched</name>
|
||||
<precondition>The plan 04 SUMMARY records the three layer tags pinned in vue-demo-app; `git -C ../sm-demo-app status --porcelain` and `git -C ../sm-demo-app/vue-demo-app status --porcelain` list no foreign edit in the files of this task.</precondition>
|
||||
<files>../sm-demo-app/deploy/nginx/demo.summercms.io.conf, ../sm-demo-app/deploy/supervisor/summercms-demo-livekit.conf, ../sm-demo-app/deploy/livekit.yaml.example, ../sm-demo-app/deploy/env.example, ../sm-demo-app/config/env/production/golem15.chatvideo.yaml, ../sm-demo-app/scripts/check-deploy.sh, ../sm-demo-app/scripts/check-live.sh, ../sm-demo-app/DEPLOY.md, ../sm-demo-app/README.md</files>
|
||||
<read_first>../sm-demo-app/deploy/nginx/demo.summercms.io.conf (whole file), deploy/supervisor/summercms-demo-centrifugo.conf, deploy/livekit.yaml.example, deploy/env.example, config/env/production/*.yaml, scripts/check-deploy.sh (whole file), scripts/check-live.sh (whole file), DEPLOY.md (whole file), 20-RESEARCH.md (Pitfalls 6, 7, 8, 11; Supervisor program and LiveKit config blocks)</read_first>
|
||||
<action>Per D-10, D-12, D-16, SC-2, SC-3.
|
||||
- nginx: add `location ^~ /_chat/`, `/_chatchannels/`, `/_chatbots/`, `/_chatvideo/`, `/_forum/` and `/_userfriends/` with the exact /_demo/ block, and `client_max_body_size 2m` on /_chat/ for attachments (chat's 1 MiB upload limit plus multipart overhead). Add `location ^~ /livekit/` with proxy_pass `http://127.0.0.1:7890/`, `proxy_http_version 1.1`, the Upgrade and Connection headers, Host and X-Forwarded headers, `proxy_read_timeout 7d`, `proxy_send_timeout 7d` and `proxy_buffering off`, and a comment that media bypasses nginx. Update the `/api/realtime/token` comment: every demo member now gets a token (demo.realtime-access). Add nothing for /_herald.
|
||||
- deploy/supervisor/summercms-demo-livekit.conf: RESEARCH's program block, with a header comment in the Centrifugo file's style (ports, config path and mode, binary, that WavePath's instance is separate).
|
||||
- deploy/livekit.yaml.example: confirm or adjust the plan 02 file so production uses `use_external_ip: true`, and comment the rome install command.
|
||||
- config/env/production/golem15.chatvideo.yaml: `livekit_url: "wss://demo.summercms.io/livekit"`, `livekit_admin_url: "http://127.0.0.1:7890"`.
|
||||
- deploy/env.example: the herald subject key with its comment, if plan 04 left it out.
|
||||
- check-deploy.sh, new checks with header lines:
|
||||
- summercms-demo-livekit.conf parses and declares its program with the user summercms-demo;
|
||||
- LiveKit ports agree: the example's `port` equals the nginx /livekit/ proxy_pass port and the production livekit_admin_url port; `rtc.tcp_port` 7891 and `rtc.udp_port` 7892 are set, no port_range key is set, and bind_addresses is 127.0.0.1;
|
||||
- room.max_participants equals config/golem15.chatvideo.yaml max_participants (D-12, D-23);
|
||||
- each of the six chat-family prefixes is a location proxied to the Go port;
|
||||
- no location routes /_herald;
|
||||
- the example holds placeholder keys only.
|
||||
Parse YAML with python3 as the script already does for JSON, and fail with one line per mismatch.
|
||||
- check-live.sh, new checks with header lines:
|
||||
- /chat, /forum and /friends answer 200 with the banner and a register link (D-04, D-21);
|
||||
- GET /_chatchannels/api/v1/contexts/demo/1/channels, /_forum/api/v1/contexts/demo/1/categories, /_userfriends/api/v1/friends, /_chatbots/api/v1/commands and /_chatvideo/api/v1/conversations/1/video/status answer 401;
|
||||
- GET /livekit/rtc/validate answers 401 (or 400) from LiveKit, not Nuxt's HTML 404 (the body is not text/html);
|
||||
- GET /_herald/api/v1/push/vapid-public-key does not answer JSON from Go;
|
||||
- robots.txt holds `Disallow: /_chat` and `Disallow: /_forum`.
|
||||
- DEPLOY.md:
|
||||
- Overview: ports 7890-7892, the new program;
|
||||
- One-time server setup "LiveKit (the demo's own)": install the config with generated keys, the supervisor program, verify with curl on 127.0.0.1:7890;
|
||||
- "Firewall (Vuurmuur)": the service definition (TCP 7891, UDP 7892), the rule from any to the firewall, `vuurmuur_script` apply and reload, verification with `ss -lunp` and `iptables -S | grep 789`, and removal;
|
||||
- "Chat family": the .env keys, the Centrifugo namespaces and when to restart Centrifugo, the VAPID keys, the robots.txt lines to add;
|
||||
- Verify: the new check-live items;
|
||||
- Rollback: stopping summercms-demo-livekit turns calls off only;
|
||||
- Reset: the chat family is included.
|
||||
README links to the new sections.
|
||||
|
||||
Commit `feat: deploy files for the chat family and the demo's LiveKit`.</action>
|
||||
<verify>
|
||||
<automated>../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</automated>
|
||||
<fails_when>The last line is not "check-deploy: ok" (nginx -t fails with the new locations, a port or cap mismatch, a missing prefix, a routed /_herald, or a supervisor parse error), or check-live.sh has a syntax error.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c 'location ^~ /livekit/' ../sm-demo-app/deploy/nginx/demo.summercms.io.conf` prints 1.
|
||||
- `grep -cE 'location \^~ /_(chat|chatchannels|chatbots|chatvideo|forum|userfriends)/' ../sm-demo-app/deploy/nginx/demo.summercms.io.conf` prints 6.
|
||||
- `grep -c '\[program:summercms-demo-livekit\]' ../sm-demo-app/deploy/supervisor/summercms-demo-livekit.conf` prints 1.
|
||||
- `grep -c 'Vuurmuur' ../sm-demo-app/DEPLOY.md` prints at least 2.
|
||||
</acceptance_criteria>
|
||||
<done>The repository holds every file the rome release needs, and the local check proves they agree.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Read rome's current state and prepare the release build without changing rome</name>
|
||||
<files>../sm-demo-app/DEPLOY.md</files>
|
||||
<read_first>../sm-demo-app/DEPLOY.md (Build, Upload, Release, Secrets), ../sm-demo-app/scripts/build.sh, .planning/phases/19.4.1-early-public-demo-on-demo-summercms-io/19.4.1-02-SUMMARY.md (first deploy notes)</read_first>
|
||||
<action>Per D-10 and SC-3. Read-only on rome:
|
||||
- `ss -ltnup` filtered on 7890, 7891 and 7892 (free?);
|
||||
- `supervisorctl status`;
|
||||
- `grep -c` for each new key name in /srv/summercms-demo/.env (counts only, never values);
|
||||
- whether /srv/summercms-demo/centrifugo.json lists the chat namespace (`grep -c '"chat"'`);
|
||||
- `ls /etc/vuurmuur/services` and `grep -n Livekit /etc/vuurmuur/rules/rules.conf` (no edit);
|
||||
- `/usr/local/bin/livekit-server --version`.
|
||||
Locally:
|
||||
- `scripts/check-pins.sh`, `scripts/build.sh release` (refuses a dirty tree or off-tag submodules) and `scripts/smoke.sh` against the release build as DEPLOY.md's Build section says;
|
||||
- `git -C vue-demo-app status` (the commits from plans 03 and 04 are local only) and `git log origin/master..master` in both repositories.
|
||||
Record everything in a deploy table for the Task 3 question. If a port is taken, pick the next free trio and update the deploy files and check-deploy in a commit before asking. If DEPLOY.md's Build steps no longer match the scripts, fix them in DEPLOY.md.</action>
|
||||
<verify>
|
||||
<automated>../sm-demo-app/scripts/check-pins.sh && ../sm-demo-app/scripts/build.sh release && ../sm-demo-app/scripts/smoke.sh</automated>
|
||||
<fails_when>Non-zero exit: a submodule off its go.mod tag, the release build refuses (dirty tree, stale ROADMAP snapshot) or fails to compile without go.work, or smoke.sh reports a missing banner, an admin path or a credential on a rendered page.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- The SUMMARY's deploy table lists rome's listeners on 7890-7892 (or the chosen trio), the supervisor programs, which env keys exist (counts only), whether centrifugo.json has the chat namespace, the LiveKit version and the unpushed commits per repository.
|
||||
- No file on rome changed (no sudo write command was run).
|
||||
</acceptance_criteria>
|
||||
<done>The release build is ready and the go-ahead question rests on rome's actual state.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking">
|
||||
<name>Task 3: Go-ahead to push the demo and deploy the chat family to rome</name>
|
||||
<read_first>The Task 2 deploy table; ../sm-demo-app/DEPLOY.md sections "LiveKit (the demo's own)", "Chat family" and "Release"</read_first>
|
||||
<acceptance_criteria>
|
||||
- The user chose one option and it is recorded in the SUMMARY.
|
||||
</acceptance_criteria>
|
||||
<decision>Push vue-demo-app and sm-demo-app and release the chat family on rome now (LiveKit program, keys, nginx, Centrifugo namespaces, binary and site)?</decision>
|
||||
<context>The release changes a shared production host: a new supervisor program (summercms-demo-livekit on 127.0.0.1:7890), new .env keys generated on rome (LiveKit and VAPID), an nginx reload for the demo vhost, a Centrifugo restart for the demo's own instance, a migration of the chat-family tables and demo:seed. WavePath's LiveKit and keys are not touched. The admin stays at /smbackend with login (19.4.1's decision) and /backend stays 404. Media ports stay closed until the separate firewall question (Task 5), so calls connect only after that. Rollback is in DEPLOY.md (previous binary and site; stop the LiveKit program).</context>
|
||||
<options>
|
||||
<option id="deploy">
|
||||
<name>Push and deploy</name>
|
||||
<pros>Chat, forum and friends go live; calls follow after the firewall rule.</pros>
|
||||
<cons>Changes rome; the new tables are migrated (not reversed on rollback).</cons>
|
||||
</option>
|
||||
<option id="push-only">
|
||||
<name>Push the repositories, deploy later</name>
|
||||
<pros>Code is on origin and reviewed; rome is unchanged.</pros>
|
||||
<cons>SC-3 stays open; plan 06's live verification waits.</cons>
|
||||
</option>
|
||||
<option id="hold">
|
||||
<name>Hold</name>
|
||||
<pros>Nothing leaves this machine.</pros>
|
||||
<cons>The phase cannot verify live.</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>Reply "deploy", "push-only" or "hold".</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Release: push, install LiveKit and keys on rome, roll out the binary and site, and check the live demo</name>
|
||||
<precondition>Task 3 answered "deploy" (or "push-only", which runs steps 1 and 2 only).</precondition>
|
||||
<files>../sm-demo-app/vue-demo-app, ../sm-demo-app/DEPLOY.md</files>
|
||||
<read_first>../sm-demo-app/DEPLOY.md (whole runbook), the Task 2 deploy table</read_first>
|
||||
<action>Per D-10, D-12, D-16, SC-2, SC-3.
|
||||
1. Push vue-demo-app master, then in sm-demo-app commit the pointer bump `chore: bump vue-demo-app (chat, forum, friends, calls)` and push sm-demo-app.
|
||||
2. Confirm both remotes equal local HEAD (`git ls-remote`).
|
||||
3. On rome, following DEPLOY.md. Never echo or log a value; write secrets with remote heredocs fed from `openssl rand` inside the remote shell.
|
||||
- Generate a LiveKit key and secret.
|
||||
- Install /srv/summercms-demo/livekit.yaml (0640 root:summercms-demo) from the example with them, and append the same values to .env as the SUMMER_GOLEM15__CHATVIDEO__ keys.
|
||||
- Run `herald:vapid-generate --json -q` with the new binary on rome and append the VAPID keys. Set the subject to the demo's contact address from ~/.config/summercms/demo-secrets.env, sent over SSH stdin.
|
||||
- Update /srv/summercms-demo/centrifugo.json: merge the chat, user and video namespaces from the example while keeping its secrets, run `centrifugo checkconfig`, and restart summercms-demo-centrifugo.
|
||||
- Install and start summercms-demo-livekit (reread, update, status RUNNING) and check that `curl -s http://127.0.0.1:7890/` answers OK on rome.
|
||||
- Upload and release the build per DEPLOY.md (migrate, demo:seed, swap, restart), then install the nginx file and reload.
|
||||
- Add the robots.txt lines for the six prefixes through SEO › robots.txt (DEPLOY.md "SEO"; through an SSH tunnel to the admin with the credentials in /root/summercms-demo-admin.txt, or the seo MCP tools with the administrator's connector). If neither path works unattended, list it for the user in Task 6.
|
||||
4. Run `scripts/check-live.sh https://demo.summercms.io`. The /livekit/rtc/validate check passes without the firewall rule, because signalling goes through nginx.
|
||||
5. Record in DEPLOY.md any step that differed from the runbook.</action>
|
||||
<verify>
|
||||
<automated>[ "$(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'</automated>
|
||||
<fails_when>sm-demo-app's remote master differs from HEAD (not pushed), or check-live's last line is not "check-live: ok" (a chat-family prefix not 401, /livekit not reaching LiveKit, /backend not 404, missing banner or call to action, robots lines missing, or /ws not 101).</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `ssh -p 2222 debian@10.8.3.118 sudo supervisorctl status summercms-demo-livekit` shows RUNNING.
|
||||
- `ssh -p 2222 debian@10.8.3.118 'sudo grep -c "^SUMMER_GOLEM15__CHATVIDEO__LIVEKIT_API_SECRET=." /srv/summercms-demo/.env'` prints 1 (count only).
|
||||
- `ssh -p 2222 debian@10.8.3.118 sudo supervisorctl status wavepath-livekit` shows the same uptime it showed in Task 2 (not restarted).
|
||||
- The SUMMARY holds no secret value.
|
||||
</acceptance_criteria>
|
||||
<done>The chat family is live on demo.summercms.io: chat, forum, friends and the call signalling path answer, with WavePath's services untouched.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking">
|
||||
<name>Task 5: Go-ahead for the Vuurmuur rule that opens LiveKit's media ports on rome</name>
|
||||
<read_first>DEPLOY.md "Firewall (Vuurmuur)"; the read-only output Claude prints: the proposed service file content (TCP 7891, UDP 7892), the proposed rule line, the current rules.conf lines around the Livekit service, and `iptables -S | grep -c vrmr` before the change</read_first>
|
||||
<acceptance_criteria>
|
||||
- The user chose one option and it is recorded in the SUMMARY.
|
||||
</acceptance_criteria>
|
||||
<decision>Open TCP 7891 and UDP 7892 on rome for the demo's LiveKit through Vuurmuur?</decision>
|
||||
<context>rome's INPUT chain is managed by Vuurmuur with policy DROP; raw iptables rules would be overwritten. Without this rule calls signal but never connect media. The rule only admits traffic to the demo's LiveKit ports (the program binds media on the public address; signalling stays on loopback behind nginx). It is reversible: removing the rule and reloading Vuurmuur closes the ports. WavePath's own Livekit service definition stays unused and unchanged.</context>
|
||||
<options>
|
||||
<option id="claude-applies">
|
||||
<name>Claude applies the rule</name>
|
||||
<pros>Done now, with the verification commands from DEPLOY.md.</pros>
|
||||
<cons>A change to the shared host's firewall made by the agent.</cons>
|
||||
</option>
|
||||
<option id="user-applies">
|
||||
<name>I apply it myself</name>
|
||||
<pros>You make the firewall change on the shared host.</pros>
|
||||
<cons>Calls stay disconnected until you reply that it is done.</cons>
|
||||
</option>
|
||||
<option id="hold">
|
||||
<name>Hold</name>
|
||||
<pros>The firewall stays as it is.</pros>
|
||||
<cons>Video calls do not connect on the live demo (D-10 open); plan 06's call check is skipped.</cons>
|
||||
</option>
|
||||
</options>
|
||||
<instructions>For "user-applies": follow DEPLOY.md "Firewall (Vuurmuur)" and reply "firewall done".</instructions>
|
||||
<resume-signal>Reply "claude-applies", "firewall done" or "hold".</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 6: Apply and verify the media ports, and hand the live demo to plan 06</name>
|
||||
<precondition>Task 5 answered "claude-applies" or "firewall done".</precondition>
|
||||
<files>../sm-demo-app/DEPLOY.md</files>
|
||||
<read_first>DEPLOY.md "Firewall (Vuurmuur)", the Task 5 answer</read_first>
|
||||
<action>Per D-10.
|
||||
- For "claude-applies": on rome, add the service file /etc/vuurmuur/services/summercms-demo-livekit (TCP 7891, UDP 7892) and the rule line from DEPLOY.md, apply with Vuurmuur's own tooling (`vuurmuur_script` apply and reload, never raw iptables), and back up rules.conf first to rules.conf.bak-<date>.
|
||||
- Either way, verify:
|
||||
- `sudo iptables -S | grep -E '7891|7892'` shows ACCEPT rules with the vrmr prefix;
|
||||
- `ss -lunp | grep 7892` shows livekit-server;
|
||||
- from this machine, `nc -zv -w 3 <rome public address> 7891` connects (UDP is verified by the live call in plan 06).
|
||||
- Note in DEPLOY.md the date applied and the backup path.
|
||||
- If robots.txt could not be updated unattended in Task 4, list the exact lines for the user in the SUMMARY.
|
||||
Commit any DEPLOY.md change (`docs: record the LiveKit firewall rule`), then push sm-demo-app.</action>
|
||||
<verify>
|
||||
<automated>ssh -p 2222 debian@10.8.3.118 "sudo iptables -S | grep -cE 'dport (7891|7892)'" | grep -qvx 0 && nc -zv -w 3 54.36.173.176 7891</automated>
|
||||
<fails_when>The iptables count is 0 (Vuurmuur did not apply the rule), or the TCP connect to 7891 on the public address times out or is refused.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `ssh -p 2222 debian@10.8.3.118 test -f /etc/vuurmuur/services/summercms-demo-livekit` exits 0.
|
||||
- `../sm-demo-app/scripts/check-live.sh https://demo.summercms.io` still prints `check-live: ok`.
|
||||
- The SUMMARY states whether robots.txt was updated and lists any item left for the user.
|
||||
</acceptance_criteria>
|
||||
<done>LiveKit's media ports are reachable from the internet through Vuurmuur and the live demo passes its checks.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| internet to nginx (Cloudflare) on demo.summercms.io | untrusted HTTP and WebSocket, now including /livekit/ and six API prefixes |
|
||||
| internet to rome's public address on 7891/tcp and 7892/udp | WebRTC media straight to LiveKit, bypassing Cloudflare |
|
||||
| build machine to rome over SSH | secrets generated on rome, never transferred |
|
||||
| demo services to WavePath services on the same host | must stay isolated |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-20-27 | Elevation of privilege | LiveKit co-tenancy with WavePath | high | mitigate | own instance, own key pair, own ports and config file (D-10); no key added to /etc/livekit/livekit.yaml; Task 4 asserts wavepath-livekit was not restarted |
|
||||
| T-20-28 | Information disclosure | LiveKit, VAPID and Centrifugo secrets | high | mitigate | generated on rome inside the remote shell, written to 0640 root:summercms-demo files, never echoed; repository files hold names and placeholders; check-deploy fails on non-placeholder keys in the example |
|
||||
| T-20-29 | Denial of service | open media ports | medium | mitigate | only TCP 7891 and UDP 7892 via a Vuurmuur rule the user approved; room max_participants 4, empty_timeout 120, departure_timeout 20, max_duration 900 with reconcile (D-12) |
|
||||
| T-20-30 | Information disclosure | origin IP through ICE candidates | low | accept | rome already exposes services on its public address; recorded in DEPLOY.md |
|
||||
| T-20-31 | Elevation of privilege | unintended Go routes | medium | mitigate | nginx allow-list gains exactly six prefixes and /livekit/; /_herald stays unrouted; check-deploy asserts both; /backend 404 unchanged |
|
||||
| T-20-32 | Tampering | firewall drift | medium | mitigate | changes only through Vuurmuur with a dated backup of rules.conf; DEPLOY.md documents removal |
|
||||
| T-20-33 | Denial of service | release breaking the live demo | medium | mitigate | release build and smoke before the go-ahead (Task 2); DEPLOY.md rollback keeps bin/demo.prev and site/.output.prev; check-live after release |
|
||||
| T-20-34 | Spoofing | signalling WebSocket | low | mitigate | LiveKit validates every join token (HS256, token_ttl 900) minted only for conversation participants by chatvideo |
|
||||
| T-20-SC | Tampering | npm/pip/cargo installs | high | mitigate | no new package: the release installs vue-demo-app's frozen lockfile (layers pinned by tag, livekit-client approved in plan 04) and Go modules by tag with go.sum |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- Task 1: check-deploy ok with the new locations, program and port agreement.
|
||||
- Task 2: check-pins, release build and smoke; rome read-only table.
|
||||
- Task 3: go-ahead recorded.
|
||||
- Task 4: remotes pushed; check-live ok on https://demo.summercms.io; LiveKit running; WavePath untouched.
|
||||
- Task 5: firewall decision recorded.
|
||||
- Task 6: iptables shows the Vuurmuur rule; TCP 7891 reachable; check-live still ok.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
The chat family runs on the public demo with its own LiveKit and open media ports, /backend refused and the call to action shown (SC-3), with a runbook and checks that make the next release routine.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/20-demo-instance-with-every-shared-plugin/20-05-SUMMARY.md` when done: the deploy table (no secret values), ports used, the commits pushed, what changed on rome, both checkpoint answers, the robots.txt state, and anything left for the user.
|
||||
</output>
|
||||
@@ -0,0 +1,447 @@
|
||||
---
|
||||
phase: 20-demo-instance-with-every-shared-plugin
|
||||
plan: 06
|
||||
type: execute
|
||||
wave: 6
|
||||
depends_on: ["20-01", "20-02", "20-03", "20-04", "20-05"]
|
||||
files_modified:
|
||||
- ../sm-moderation-plugin/classes/filter_test.go
|
||||
- ../sm-moderation-plugin/models/settings_test.go
|
||||
- ../sm-moderation-plugin/plugin_test.go
|
||||
- ../sm-chat-plugin/saving_test.go
|
||||
- ../sm-forum-plugin/saving_test.go
|
||||
- ../sm-demo-app/moderation_test.go
|
||||
- ../sm-demo-app/chat_test.go
|
||||
- ../sm-demo-app/video_test.go
|
||||
- ../sm-demo-app/seed_chat_test.go
|
||||
- ../sm-demo-app/reset_test.go
|
||||
- ../sm-demo-app/realtime_test.go
|
||||
- ../sm-demo-app/boot_test.go
|
||||
- ../sm-demo-app/plugins/demo/classes/chat_context_test.go
|
||||
- ../sm-demo-app/plugins/demo/chatbots_test.go
|
||||
- ../sm-demo-app/scripts/check-chat.cjs
|
||||
- ../sm-demo-app/scripts/check-local.sh
|
||||
- ../sm-demo-app/scripts/check-all.sh
|
||||
- ../sm-demo-app/README.md
|
||||
- ../vue-chat-layer/vitest.config.ts
|
||||
- ../vue-chat-layer/app/lib/chatEvents.test.ts
|
||||
- ../vue-chat-layer/app/lib/chatFormat.test.ts
|
||||
- ../vue-chat-layer/app/lib/chatText.test.ts
|
||||
- ../vue-chat-layer/app/lib/chatUnread.test.ts
|
||||
- ../vue-chat-layer/app/lib/realtime.test.ts
|
||||
- ../vue-chat-layer/app/lib/linkEmbeds.test.ts
|
||||
- ../vue-chat-layer/tests/client.test.ts
|
||||
- ../vue-chat-layer/tests/store.test.ts
|
||||
- ../vue-chatvideo-layer/tests/callMachine.test.ts
|
||||
- ../vue-chatvideo-layer/tests/videoClient.test.ts
|
||||
- ../vue-chatvideo-layer/tests/useCall.test.ts
|
||||
- ../vue-forum-layer/tests/client.test.ts
|
||||
- ../vue-userfriends-layer/tests/client.test.ts
|
||||
- ../sm-demo-app/vue-demo-app/app/utils/communityAuth.ts
|
||||
- ../sm-demo-app/vue-demo-app/app/utils/communityAuth.test.ts
|
||||
- ../sm-demo-app/vue-demo-app/app/plugins/auth.ts
|
||||
- ../sm-demo-app/vue-demo-app/app/lib/realtime.test.ts
|
||||
autonomous: false
|
||||
requirements: [SC-1, SC-2, SC-3, SC-4, D-01, D-02, D-04, D-05, D-06, D-07, D-08, D-09, D-11, D-12, D-13, D-14, D-15, D-17, D-18, D-20, D-21, D-22, D-23]
|
||||
estimate:
|
||||
tokens: 360000
|
||||
raw_tokens: 360000
|
||||
tasks: 8
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "SC-4: every behaviour row of 20-VALIDATION.md has a passing automated test under the name it lists, in the repository it lists; the new code of plans 01 to 05 is covered by unit tests in this last plan (project rule)."
|
||||
- "D-18: TestMessageSaving* (sm-chat-plugin) and TestPostSaving* (sm-forum-plugin) pin: no listener gives byte-identical responses and stored rows; a rejecting listener on create, reply, ReplyTo and edit (chat) and thread, reply and edit (forum) gives 422 under the Rejection's field, writes and changes nothing, fires no post-save event and publishes nothing; a non-Rejection listener error keeps the 500 path; the existing php_method_map and golden tests pass unedited."
|
||||
- "D-17: TestMatch*, TestParseList and TestFilterCheck* (sm-moderation-plugin) pin case-insensitive word and phrase matching with Unicode letter boundaries (Polish letters, digits, underscores), host, subdomain, www and scheme-less link matching, URL-prefix entries, the disabled switch, the recompile-on-change cache, the list limits in BeforeValidate and the locale fallback of the message; TestDemoModeration pins the demo wiring end to end through HTTP."
|
||||
- "D-06, D-07, D-08, D-22: TestDemoChatContext*, TestDemoChannelAdapter, TestDemoChannelsSeededOnly, TestDemoChatBootstrap, TestDemoChatMembers, TestDemoDMs and TestDemoModeratorDelete pin membership (banned and deleted users excluded with live membership), the channel alias, refused channel creation, the bootstrap and members JSON shapes, DMs between any members, and moderator deletion limited to channel conversations."
|
||||
- "D-11, D-12, D-23: TestDemoVideoStart pins that a DM call starts with a token and livekit_url against a fake RoomService and that the server does not refuse by conversation type (calls are limited to DMs by the UI); the vue-chat-layer tests pin that the conversation-actions slot renders only for DMs."
|
||||
- "D-05: TestDemoCommands pins the catalog entries demo and tour and their ephemeral dispatch; the layer tests pin the command picker's dispatch path."
|
||||
- "D-09: TestSeedChat pins EN and PL channels, threads, reactions, pins, forum categories and posts, friendships and the group, and idempotency; TestResetKeepsPlans pins that every chat-family table is emptied and reseeded and that moderation settings survive."
|
||||
- "D-01, D-02, D-04: bm's chatEvents, chatFormat, chatText, chatUnread and realtime tests run under vitest in vue-chat-layer with only import paths changed, next to new tests for the multi-channel store, threads, pins, link embeds, the client's refresh-and-retry and the anonymous no-bootstrap path."
|
||||
- "D-13, D-14, D-15, D-21: the chatvideo machine and useCall (mocked livekit-client Room) tests, and the forum and friends client tests pass under their layers' test scripts."
|
||||
- "SC-2: locally, scripts/check-local.sh chatlive callmedia passes: two browsers see each other's channel message live, and a DM call shows each side a playing remote video with fake media through the local LiveKit."
|
||||
- "SC-2, SC-3: scripts/check-all.sh (including check-live against https://demo.summercms.io) prints check-all: ok, and the user confirms a two-browser chat and a DM call on the live demo."
|
||||
artifacts:
|
||||
- path: "../sm-moderation-plugin/classes/filter_test.go"
|
||||
provides: "TestMatch*, TestParseList, TestFilterCheck*"
|
||||
contains: "TestMatch"
|
||||
- path: "../sm-chat-plugin/saving_test.go"
|
||||
provides: "TestMessageSaving*"
|
||||
contains: "TestMessageSaving"
|
||||
- path: "../sm-forum-plugin/saving_test.go"
|
||||
provides: "TestPostSaving*"
|
||||
contains: "TestPostSaving"
|
||||
- path: "../sm-demo-app/chat_test.go"
|
||||
provides: "TestDemoChat*, TestDemoChannelsSeededOnly, TestDemoDMs, TestDemoModeratorDelete, TestDemoCommands"
|
||||
contains: "TestDemoChannelsSeededOnly"
|
||||
- path: "../sm-demo-app/video_test.go"
|
||||
provides: "TestDemoVideoStart with a fake RoomService"
|
||||
contains: "TestDemoVideoStart"
|
||||
- path: "../sm-demo-app/scripts/check-chat.cjs"
|
||||
provides: "two-browser realtime and DM call checks"
|
||||
contains: "use-fake-device-for-media-stream"
|
||||
- path: "../vue-chat-layer/tests/store.test.ts"
|
||||
provides: "multi-channel store, threads, pins, commands"
|
||||
contains: "describe"
|
||||
key_links:
|
||||
- from: "../sm-demo-app/scripts/check-all.sh"
|
||||
to: "../sm-demo-app/scripts/check-live.sh"
|
||||
via: "the gate runs every suite, the layers and the chat-family plugins, then check-live"
|
||||
pattern: "check-live\\.sh"
|
||||
- from: "../sm-demo-app/scripts/check-local.sh"
|
||||
to: "../sm-demo-app/scripts/check-chat.cjs"
|
||||
via: "check_chatlive and check_callmedia run the Playwright script with two contexts"
|
||||
pattern: "check-chat\\.cjs"
|
||||
- from: "../sm-demo-app/moderation_test.go"
|
||||
to: "../sm-demo-app/plugins/demo/moderation.go"
|
||||
via: "HTTP send with a banned word answers 422 through the demo listener and the moderation Filter"
|
||||
pattern: "422"
|
||||
prohibitions:
|
||||
- statement: "Tests use no real secret, no production database and no network service except the Docker Postgres, the local dev stack and in-process fakes."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
- statement: "No production behaviour changes in this plan except fixes for bugs the tests expose, within Task 7's fix budget (small, no feature, schema or contract change, at most one patch tag per repository and one redeploy), each in its own commit, named in the SUMMARY, and released through the Task 6 checkpoint; the app-wiring extraction in vue-demo-app is behaviour-preserving."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
- statement: "Test-only commits in plugin and layer repositories get no tag; nothing is pushed before the Task 6 go-ahead."
|
||||
status: unresolved
|
||||
verification: judgment
|
||||
---
|
||||
|
||||
## Phase Goal
|
||||
|
||||
ROADMAP Goal (not in user-story format; no story is invented here): SC-4 says the new code has unit tests, delivered in the phase's last plan. The live demo shows the chat family working.
|
||||
|
||||
This plan's slice (plan 6 of 6, wave 6, per D-20 and the project rule that unit tests are the last plan): unit tests for everything plans 01 to 05 built, in Go, the four layers and the site, then a local two-browser chat and call check and the live verification, ending with the user's look at the live demo.
|
||||
|
||||
## Decisions taken in this plan (Claude's discretion, recorded)
|
||||
|
||||
- **Where tests live:**
|
||||
- plugin seam tests next to the plan 01 smoke tests;
|
||||
- the demo's database tests in the root package (they compile every plugin through PluginIDs and the shared Postgres harness, as 19.4.1 did), with pure demo classes tested in plugins/demo/classes;
|
||||
- bm's chat tests next to the ported sources in vue-chat-layer/app/lib (copied with only import paths changed, the 19.4.1 vue-demo-app pattern), new layer tests in tests/.
|
||||
- **Fake LiveKit:** TestDemoVideoStart points `golem15.chatvideo.livekit_admin_url` at an httptest server answering the Twirp RoomService calls Start makes (CreateRoom, and ListParticipants if used), so chatvideo's real code path runs without a LiveKit.
|
||||
- **useCall test:** livekit-client is replaced with vi.mock of a minimal Room (connect, disconnect, localParticipant setters, event emitter). The real SDK is exercised by check-chat.cjs against the local LiveKit with fake media.
|
||||
- **Site tests:** the hook factories in vue-demo-app's app/plugins/auth.ts (token, refresh and unauthorized for chat; token getters; the friends directory mapping) move unchanged into app/utils/communityAuth.ts, so vitest can test them without Nuxt.
|
||||
- spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip).
|
||||
|
||||
## Artifacts this phase produces (this plan)
|
||||
|
||||
- Go tests:
|
||||
- sm-moderation-plugin `TestMatchWords`, `TestMatchLinks`, `TestParseList`, `TestFilterCheckCache`, `TestFilterCheckDisabled`, `TestSettingsLimits`, `TestBootPublishesFilter`;
|
||||
- sm-chat-plugin `TestMessageSavingNoListener`, `TestMessageSavingRejects`, `TestMessageSavingOtherError`;
|
||||
- sm-forum-plugin `TestPostSavingNoListener`, `TestPostSavingRejects`;
|
||||
- sm-demo-app `TestDemoModeration`, `TestDemoChatContext*`, `TestDemoChannelAdapter`, `TestDemoChannelsSeededOnly`, `TestDemoChatBootstrap`, `TestDemoChatMembers`, `TestDemoDMs`, `TestDemoModeratorDelete`, `TestDemoCommands`, `TestDemoVideoStart`, `TestSeedChat`, extended `TestResetKeepsPlans`, `TestRealtimeRoutes`, `TestRealtimeMemberToken`, `TestBootAllPlugins`.
|
||||
- Layer tests: vue-chat-layer (ported bm tests plus `store.test.ts`, `client.test.ts`, `linkEmbeds.test.ts`), vue-chatvideo-layer (`callMachine`, `videoClient`, `useCall`), vue-forum-layer and vue-userfriends-layer client tests.
|
||||
- Site: `app/utils/communityAuth.ts` with its test.
|
||||
- Scripts: check-chat.cjs modes `--two` (realtime) and `--call` (fake media); check-local checks `chatlive` and `callmedia`; check-all.sh steps for the chat-family plugins and the four layers.
|
||||
|
||||
<objective>
|
||||
Cover the phase's new code with unit tests, prove two-browser chat and DM calls locally, and verify the live demo with the user.
|
||||
|
||||
Purpose: SC-4 (unit tests in the last plan) and the live proof of SC-2 and SC-3 for every decision the phase implemented.
|
||||
Output: test commits in eight Go and five frontend repositories, script commits in sm-demo-app, pushes after the go-ahead, and the user's sign-off on the live demo.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/msd-core/workflows/execute-plan.md
|
||||
@~/.claude/msd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-VALIDATION.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-RESEARCH.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-01-SUMMARY.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-02-SUMMARY.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-03-SUMMARY.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-04-SUMMARY.md
|
||||
@.planning/phases/20-demo-instance-with-every-shared-plugin/20-05-SUMMARY.md
|
||||
|
||||
Repos: ../sm-moderation-plugin, ../sm-chat-plugin, ../sm-forum-plugin, ../sm-demo-app (root tests, plugins/demo, scripts), ../sm-demo-app/vue-demo-app, ../vue-chat-layer, ../vue-chatvideo-layer, ../vue-forum-layer, ../vue-userfriends-layer. bm test sources (read-only): ../sm-bm-app/frontend/src/lib/{chatEvents,chatFormat,chatText,chatUnread,realtime}.test.ts and stores/chat.test.ts.
|
||||
|
||||
Rules: no co-author tags; stage by explicit path; Docker tests skip under -short and the TestMain calls flag.Parse and testing.Short (CLAUDE.md); if the Docker socket is denied, run through `sudo -u golem`; production code changes only for bugs the tests expose and only within the fix budget in Task 7 (own commit each, named in the SUMMARY; anything larger becomes a todo and decimal-phase candidate); nothing is pushed before Task 6.
|
||||
|
||||
<interfaces>
|
||||
sm-demo-app test harness: testmain_test.go `bootDemo(t, environ, set) *demotest.Demo`; internal/demotest (App, DB, Options{PluginIDs, Environ, Set}); existing tests TestGoModRequiresTags, TestBootAllPlugins (wantPlugins), TestRealtimeRoutes (realtimeApp, realtimeRoutes, wantNamespaces), TestResetKeepsPlans, TestSeedRoadmapUpsert; smoke tests from plan 02: TestDemoChatTracer, TestDemoModerationSmoke, TestSeedChatSmoke (chat_smoke_test.go).
|
||||
Plugin harnesses: chat bootWeb, frontendToken, call, conversation, count, memory (lighthouse MemoryDriver publications); forum bootWeb, call, count; plan 01 smoke tests TestMessageSavingTracer, TestPostSavingTracer, TestFilterSmoke.
|
||||
Demo classes (plan 02): DemoContext, ChannelContext, ChatAccess, ForumAccess with WithMembership and MemberFunc injection; chat_api.go bootstrap and members; chatbots.go demo and tour; moderation.go listeners; realtime_access.go guard.
|
||||
Layers (plans 03 and 04): vue-chat-layer createChatClient, useChat, linkify, registerEmbedRenderer, onUserEvent, slots; vue-chatvideo-layer callMachine (initialCallState, transition), createVideoClient, videoSessionChannel, useCall; createForumClient; createFriendsClient.
|
||||
Scripts: check-local.sh (visitor_jwt, throwaway registration helper, ALL_CHECKS, NODE_PATH="$(npm root -g)" for Playwright scripts), check-chat.cjs (plan 03 single-browser), check-all.sh (steps, "check-all: ok").
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="tracer">
|
||||
<name>Task 1: Tracer: one banned word travels from the admin settings through the filter, the demo listener and the chat seam to a 422 in a member's request, under test</name>
|
||||
<precondition>`docker info` exits 0; the plan 05 SUMMARY exists.</precondition>
|
||||
<files>../sm-moderation-plugin/classes/filter_test.go, ../sm-demo-app/moderation_test.go</files>
|
||||
<read_first>../sm-moderation-plugin/classes/filter.go and filter_smoke_test.go, ../sm-demo-app/plugins/demo/moderation.go, ../sm-demo-app/chat_smoke_test.go (TestDemoModerationSmoke), ../sm-demo-app/testmain_test.go, 20-VALIDATION.md (D-17 rows)</read_first>
|
||||
<action>Per D-17, D-18, SC-4.
|
||||
- sm-moderation-plugin classes/filter_test.go, table tests for the pure functions:
|
||||
- TestMatchWords: case-insensitive; a word inside a longer word does not match; phrases; Polish letters on both sides of a boundary (for example "żółw" against "żółwik" and "żółw!"); digits and underscores count as word characters; QuoteMeta'd specials such as "c++".
|
||||
- TestMatchLinks: a host entry matches the bare domain, www., http and https URLs, subdomains, upper case and trailing paths; "notbad.example" does not match "bad.example"; a URL-prefix entry matches only below its path.
|
||||
- TestParseList: trims, lowercases, drops empty lines and duplicates.
|
||||
- sm-demo-app moderation_test.go `TestDemoModeration` (Docker), end to end through HTTP:
|
||||
- after demo:seed, update golem15_moderation_settings with banned_words "blockedword" and banned_links "bad.example";
|
||||
- a member's message, reply and edit containing either answer 422 with errors.content, and the message count does not change;
|
||||
- a forum thread title answers 422 errors.title and a reply body 422 errors.body;
|
||||
- after changing the list to another word without restarting, the old word is accepted (cache refresh);
|
||||
- with enabled false, everything is accepted.
|
||||
|
||||
Commit per repository (`test: ...`).</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-moderation-plugin test -count=1 ./... && go -C ../sm-demo-app test -count=1 -run '^TestDemoModeration$' -v . 2>&1 | grep -q -- '^--- PASS: TestDemoModeration'</automated>
|
||||
<fails_when>Non-zero exit from the moderation suite (a boundary, host or cache case wrong), or no "--- PASS: TestDemoModeration" line (skipped without Docker, banned content stored, or a list change not picked up).</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c '^func TestMatch' ../sm-moderation-plugin/classes/filter_test.go` prints at least 2.
|
||||
- `grep -c 'żółw' ../sm-moderation-plugin/classes/filter_test.go` prints at least 1.
|
||||
</acceptance_criteria>
|
||||
<done>The D-17/D-18 chain is pinned from the settings row to the HTTP answer.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Seam and moderation plugin unit tests: parity without a listener, refusals on every write path</name>
|
||||
<files>../sm-chat-plugin/saving_test.go, ../sm-forum-plugin/saving_test.go, ../sm-moderation-plugin/models/settings_test.go, ../sm-moderation-plugin/plugin_test.go</files>
|
||||
<read_first>../sm-chat-plugin/saving_smoke_test.go, http_tracer_test.go, harness_test.go (memory driver), classes/attachments.go (ReplyTo), ../sm-forum-plugin/saving_smoke_test.go, http_tracer_test.go, ../sm-moderation-plugin/models/settings.go, plugin.go, updates/*.go, ../sm-sitemap-plugin/models/definition_test.go (settings test style)</read_first>
|
||||
<action>Per D-18, D-17.
|
||||
- sm-chat-plugin saving_test.go:
|
||||
- TestMessageSavingNoListener: the same send, reply, edit and ReplyTo requests answer the same status and body keys as before the seam, and MessageCreated and MessageUpdated still fire;
|
||||
- TestMessageSavingRejects: table over send, reply (POST replies), ReplyTo (service) and edit. Each gives 422 under the Rejection field (and "content" when the field is empty), no row inserted or changed, no MessageCreated or MessageUpdated fired (a counting listener), and no publication on the memory driver;
|
||||
- TestMessageSavingOtherError: a plain error from a listener gives 500 with the generic body and nothing stored;
|
||||
- the event carries Op, ConversationID, ActorID, RootID, MessageID and the trimmed Content.
|
||||
- sm-forum-plugin saving_test.go: TestPostSavingNoListener and TestPostSavingRejects over thread, reply and edit (field title or body, nothing stored, content_html unchanged on a refused edit, Op values).
|
||||
- sm-moderation-plugin:
|
||||
- models/settings_test.go TestSettingsLimits: too many entries, too long entries and a bad link entry refused with their lang keys; valid lists saved;
|
||||
- plugin_test.go TestBootPublishesFilter (Docker, with the sitemap-style harness): migrations create the singleton row, Boot publishes *classes.Filter, and Check on a clean text gives nil.
|
||||
Keep php_method_map_test.go and golden_test.go untouched in both content plugins. Commit per repository.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-chat-plugin test -count=1 ./... && go -C ../sm-forum-plugin test -count=1 ./... && go -C ../sm-moderation-plugin test -count=1 ./... && go -C ../sm-chat-plugin test -count=1 -run '^TestMessageSaving' -v . 2>&1 | grep -c -- '^--- PASS: TestMessageSaving' | grep -qvx '[0-2]'</automated>
|
||||
<fails_when>Non-zero exit from any of the three suites (parity broken, a write path that bypasses the seam, an event fired after a refusal), or fewer than three passing TestMessageSaving* lines (tests skipped without Docker).</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `git -C ../sm-chat-plugin diff HEAD~1 --stat -- php_method_map_test.go golden_test.go` and the same in ../sm-forum-plugin print nothing.
|
||||
- `grep -c 'ReplyTo' ../sm-chat-plugin/saving_test.go` prints at least 1.
|
||||
</acceptance_criteria>
|
||||
<done>The seams' contract (silent without listeners, total refusal with one) and the moderation plugin's settings and boot are pinned.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Demo host unit tests: context, adapter, bootstrap, DMs, moderator delete, commands, video start, realtime, seed and reset</name>
|
||||
<files>../sm-demo-app/plugins/demo/classes/chat_context_test.go, ../sm-demo-app/plugins/demo/chatbots_test.go, ../sm-demo-app/chat_test.go, ../sm-demo-app/video_test.go, ../sm-demo-app/seed_chat_test.go, ../sm-demo-app/reset_test.go, ../sm-demo-app/realtime_test.go, ../sm-demo-app/boot_test.go</files>
|
||||
<read_first>../sm-demo-app/plugins/demo/{chat.go,chat_api.go,chatbots.go,moderation.go,realtime_access.go,routes.go,classes/chat_context.go,classes/forum_access.go,console/seed_chat.go,console/seed_forum.go,console/seed_friends.go,console/reset.go}, ../sm-demo-app/{chat_smoke_test.go,demo_api_test.go,realtime_test.go,reset_test.go,seed_test.go,boot_test.go,testmain_test.go}, ../sm-demo-app/plugins/golem15/chatvideo/classes/livekit.go (RoomAPI, NewProductionRooms, FakeRooms) and boot.go, ../sm-bm-app/realtime_test.go, 20-VALIDATION.md</read_first>
|
||||
<action>Per D-05, D-06, D-07, D-08, D-09, D-11, D-12, D-22, D-23, SC-1.
|
||||
- plugins/demo/classes/chat_context_test.go (pure, with WithMembership and MemberFunc stubs): constants, BroadcastChannel never empty for either alias, CanModerateMessage only for moderator-group members in non-DM conversations, RequireLiveMembership true, ChatAccess CanManageContext always false and CanAccessContext only for demo/1 members, ForumAccess read, post and manage rules.
|
||||
- plugins/demo/chatbots_test.go: TestDemoCommands. Triggers demo and tour, IsAvailable false for actor 0, Handle returns an ephemeral result whose text lists the site paths.
|
||||
- chat_test.go (Docker), with a seeded demo and members created in the test:
|
||||
- TestDemoChatContextMembership: a banned user and a deleted user are not members; a member banned after joining loses access at the next request (live membership);
|
||||
- TestDemoChannelAdapter: the conversation of each seeded channel resolves through the channel alias;
|
||||
- TestDemoChannelsSeededOnly: POST channels is refused, join and leave work;
|
||||
- TestDemoChatBootstrap: the JSON keys and types of /_demo/api/v1/chat for a member, {member: false} for a non-member, 401 without a JWT;
|
||||
- TestDemoChatMembers: excludes the caller and personas count as members;
|
||||
- TestDemoDMs: any two members open a DM, and opening it again returns the same id;
|
||||
- TestDemoModeratorDelete: a moderator-group member deletes another member's channel message (200) but not a DM message of others (404); a plain member gets 404;
|
||||
- TestDemoCommands over HTTP: GET /_chatbots/api/v1/commands lists demo and tour; POST dispatch "/demo" answers mode ephemeral; "/nope" answers 422 errors.input.
|
||||
- video_test.go (Docker) TestDemoVideoStart: an httptest server answers the Twirp RoomService calls chatvideo's RoomAPI makes, with livekit_admin_url pointing at it and test keys in the environment. POST /_chatvideo/api/v1/conversations/{dm}/video/start answers 2xx with token and livekit_url, the token's claims grant the room for that user, and a second start returns the same session. A start in a seeded channel conversation is not refused by type (D-23 UI-only); assert the status the plugin gives with the channel's participant count and max_participants 4, and record it.
|
||||
- realtime_test.go: TestRealtimeRoutes keeps the middleware list from plan 02. New TestRealtimeMemberToken (Docker): a member's JWT gives 200 with a token, a banned user's gives 403 (or the guard's documented status), no JWT gives 401.
|
||||
- seed_chat_test.go TestSeedChat: exact counts per EN and PL channel, threads with replies and summary rows consistent, reactions, pins, three forum categories with content_html, friendships, the group, the visitor's DM and pending request when the visitor is set, the moderator group, and idempotency (every new step reports exists on the second run).
|
||||
- reset_test.go: TestResetKeepsPlans also asserts that every chat-family table named in resetTables is empty after the wipe and repopulated after the reseed, and that golem15_moderation_settings and the userfriends context row are unchanged.
|
||||
- boot_test.go: the chat-family route prefixes, and jwt.auth on GET /_demo/api/v1/chat and /chat/members.
|
||||
Commit `test: demo chat family`.</action>
|
||||
<verify>
|
||||
<automated>go -C ../sm-demo-app vet . ./plugins/demo/... && go -C ../sm-demo-app test -count=1 . ./plugins/demo/... && go -C ../sm-demo-app test -count=1 -run '^(TestDemoChannelsSeededOnly|TestDemoVideoStart|TestSeedChat|TestResetKeepsPlans|TestRealtimeMemberToken)$' -v . 2>&1 | grep -c -- '^--- PASS: Test' | grep -qx 5</automated>
|
||||
<fails_when>Non-zero exit or a FAIL line from the full app suite, or fewer than five "--- PASS" lines for the named tests (skipped without Docker, a refused channel create that succeeds, a video start error against the fake RoomService, a seed count mismatch, or a reset leaving chat rows).</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c '^func Test' ../sm-demo-app/chat_test.go` prints at least 7.
|
||||
- `go -C ../sm-demo-app test -count=1 -short . ./plugins/demo/...` passes without Docker.
|
||||
</acceptance_criteria>
|
||||
<done>Every demo host decision of the phase is pinned by a unit or database test.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Layer and site unit tests: bm's chat tests on the layer, new store and client tests, the call machine and the app wiring</name>
|
||||
<files>../vue-chat-layer/vitest.config.ts, ../vue-chat-layer/app/lib/chatEvents.test.ts, ../vue-chat-layer/app/lib/chatFormat.test.ts, ../vue-chat-layer/app/lib/chatText.test.ts, ../vue-chat-layer/app/lib/chatUnread.test.ts, ../vue-chat-layer/app/lib/realtime.test.ts, ../vue-chat-layer/app/lib/linkEmbeds.test.ts, ../vue-chat-layer/tests/client.test.ts, ../vue-chat-layer/tests/store.test.ts, ../vue-chatvideo-layer/tests/callMachine.test.ts, ../vue-chatvideo-layer/tests/videoClient.test.ts, ../vue-chatvideo-layer/tests/useCall.test.ts, ../vue-forum-layer/tests/client.test.ts, ../vue-userfriends-layer/tests/client.test.ts, ../sm-demo-app/vue-demo-app/app/utils/communityAuth.ts, ../sm-demo-app/vue-demo-app/app/utils/communityAuth.test.ts, ../sm-demo-app/vue-demo-app/app/plugins/auth.ts, ../sm-demo-app/vue-demo-app/app/lib/realtime.test.ts</files>
|
||||
<read_first>../sm-bm-app/frontend/src/lib/{chatEvents,chatFormat,chatText,chatUnread,realtime}.test.ts and stores/chat.test.ts, ../sm-bm-app/frontend/vite.config.ts (its test block), ../vue-chat-layer/app/lib/*.ts, app/composables/useChat.ts, tests/smoke.test.ts, ../vue-chatvideo-layer/app/lib/*.ts and app/composables/useCall.ts, ../vue-forum-layer/app/utils/forumClient.ts, ../vue-userfriends-layer/app/utils/friendsClient.ts, ../sm-demo-app/vue-demo-app/app/plugins/auth.ts, vitest.config.ts, app/lib/realtime.test.ts</read_first>
|
||||
<action>Per D-01, D-02, D-04, D-05, D-13, D-14, D-15, D-21, D-23.
|
||||
- vue-chat-layer:
|
||||
- vitest.config.ts includes app/**/*.test.ts and tests/**/*.test.ts in the node environment, with aliases so bm's tests run with only import paths changed;
|
||||
- copy bm's chatEvents, chatFormat, chatText, chatUnread and realtime tests next to the ports;
|
||||
- linkEmbeds.test.ts: linkify escapes, only http(s), rel and target set, the registry order, max_embeds;
|
||||
- client.test.ts: every endpoint's URL and method, 401 refresh-and-retry once, unauthorized after a failed refresh, the 422 mapping, multipart upload;
|
||||
- store.test.ts (bm's chat.test.ts rewritten for the useState composable, with a fake client and a fake realtime):
|
||||
- the bootstrap fills channels and DMs; an empty hostApiBase starts nothing; a non-member bootstrap starts nothing (D-04);
|
||||
- opening a channel lazy-joins; a realtime message increments unread in other conversations and appends in the open one;
|
||||
- thread replies and counts, pins load, reactions toggle;
|
||||
- a "/" input dispatches instead of posting and the ephemeral answer stays local (D-05);
|
||||
- polling starts when realtime is unavailable;
|
||||
- onUserEvent receives a video:* event the store does not handle;
|
||||
- the conversation-actions slot renders only for DMs (mount ChatHeader with @vue/test-utils only if it is already in the workspace lockfiles, else test the computed visibility helper the header uses).
|
||||
- vue-chatvideo-layer:
|
||||
- callMachine.test.ts: every transition, including failure reasons;
|
||||
- videoClient.test.ts: URLs, auth, videoSessionChannel;
|
||||
- useCall.test.ts: vi.mock livekit-client with a fake Room; start, connect, a remote track event adds a tile, mute and camera toggles call the local participant, leave disconnects and posts leave, an ended event on the session channel ends the call, and a 422 start fails with the too-many reason.
|
||||
- vue-forum-layer and vue-userfriends-layer: extend the client tests to every method and error mapping.
|
||||
- vue-demo-app: move the hook factories from app/plugins/auth.ts unchanged into app/utils/communityAuth.ts (`createChatAuth`, `tokenGetter`, `friendsDirectory`). auth.ts imports them. communityAuth.test.ts covers the token read, refresh storing the new token, unauthorized clearing the cookie, and the directory mapping. app/lib/realtime.test.ts still passes through the re-export.
|
||||
Commit per repository (`test: ...`; the vue-demo-app extraction as `refactor: community auth hooks into app/utils` before its test commit).</action>
|
||||
<verify>
|
||||
<automated>pnpm --dir ../vue-chat-layer test && pnpm --dir ../vue-chatvideo-layer test && pnpm --dir ../vue-forum-layer test && pnpm --dir ../vue-userfriends-layer test && pnpm --dir ../sm-demo-app/vue-demo-app test && pnpm --dir ../sm-demo-app/vue-demo-app run build</automated>
|
||||
<fails_when>Non-zero exit from any layer's test script, the vue-demo-app vitest suite, or the app build after the hook extraction.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `ls ../vue-chat-layer/app/lib/*.test.ts | wc -l` prints at least 6.
|
||||
- `diff <(sed -n '/^describe/,$p' ../sm-bm-app/frontend/src/lib/chatText.test.ts) <(sed -n '/^describe/,$p' ../vue-chat-layer/app/lib/chatText.test.ts)` prints nothing (only imports differ).
|
||||
- `grep -c "vi.mock('livekit-client'" ../vue-chatvideo-layer/tests/useCall.test.ts` prints 1.
|
||||
- `grep -c 'from .*communityAuth' ../sm-demo-app/vue-demo-app/app/plugins/auth.ts` prints 1.
|
||||
</acceptance_criteria>
|
||||
<done>The four layers and the site's wiring are covered by unit tests, with bm's chat tests running unchanged on the port.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 5: Two browsers locally: live channel messages and a DM call with fake media, and the phase gate in check-all</name>
|
||||
<precondition>`../sm-demo-app/scripts/dev.sh up` runs with LiveKit on 127.0.0.1:7890; the global Playwright resolves (`NODE_PATH="$(npm root -g)" node -e "require('playwright')"` exits 0).</precondition>
|
||||
<files>../sm-demo-app/scripts/check-chat.cjs, ../sm-demo-app/scripts/check-local.sh, ../sm-demo-app/scripts/check-all.sh, ../sm-demo-app/README.md</files>
|
||||
<read_first>../sm-demo-app/scripts/check-chat.cjs (plan 03), check-remote.cjs, lib/browser.cjs, check-local.sh (visitor_jwt, throwaway registration helper, check_remote), check-all.sh (steps and plugin dirs), README.md (checks section)</read_first>
|
||||
<action>Per SC-2, D-11, D-13, D-23.
|
||||
- check-chat.cjs: add `--two` and `--call` modes. Each launches Chromium with `--use-fake-ui-for-media-stream` and `--use-fake-device-for-media-stream` and opens two contexts with two JWTs (DEMO_JWT and DEMO_JWT2).
|
||||
- `--two`: both open /chat#general; A sends a unique text; B sees it within 10 s without reload; B reacts and A sees the reaction; B types and A sees the typing indicator.
|
||||
- `--call`: A opens a DM with B (POST /_chat/api/v1/contexts/demo/1/dm through page.request with A's token) and opens it on /chat. The DM header shows the call button and no call button shows in #general (D-23). A clicks it; B's page shows the incoming prompt within 10 s and accepts. Both call pages reach the connected state and each shows one remote tile whose video element has videoWidth above 0 within 20 s. A mutes and B sees the muted marker if the tile shows one. Both leave and the session ends (GET status).
|
||||
- Print `check-chat: ok two` or `check-chat: ok call`.
|
||||
- check-local.sh: `check_chatlive` and `check_callmedia` take the visitor JWT and a throwaway user's JWT, run the script in the matching mode, and join ALL_CHECKS and the header list.
|
||||
- check-all.sh, new steps after the existing plugin suites:
|
||||
- `go test ./...` in ../sm-chat-plugin, ../sm-forum-plugin and ../sm-moderation-plugin, and `go test -short ./...` in chatchannels, chatbots, chatvideo, herald and userfriends (directories overridable by env, as USER_PLUGIN_DIR is);
|
||||
- `pnpm test` in the four layer checkouts (overridable);
|
||||
- the local browser step runs `check-local.sh chatlive callmedia` alongside back and remote.
|
||||
Update the header comment and README's checks section.
|
||||
Commit `test: two-browser chat and call checks; phase gate covers the chat family`.</action>
|
||||
<verify>
|
||||
<automated>../sm-demo-app/scripts/check-local.sh chat chatlive callmedia forum friends call herald tour realtime remote</automated>
|
||||
<fails_when>The last line is not "check-local: ok ..." listing the checks; a "check-local: chatlive failed" or "callmedia failed" line (message not delivered live, no incoming prompt, no remote video within 20 s, or a call button in a channel) or any other named check failing.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -c 'use-fake-device-for-media-stream' ../sm-demo-app/scripts/check-chat.cjs` prints at least 1.
|
||||
- `grep -cE 'chatlive|callmedia' ../sm-demo-app/scripts/check-local.sh` prints at least 4.
|
||||
- `grep -c 'sm-moderation-plugin' ../sm-demo-app/scripts/check-all.sh` prints at least 1.
|
||||
</acceptance_criteria>
|
||||
<done>Two-browser realtime chat and a DM video call are proven automatically on the local stack, and the phase gate runs every new suite.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:decision" gate="blocking">
|
||||
<name>Task 6: Go-ahead to push the test commits (and to release fixes if the tests found bugs)</name>
|
||||
<read_first>Claude's table: per repository the unpushed commits (`git log origin/<branch>..HEAD --oneline`), which are test-only and which are fixes; for each fix, the proposed patch tag (next free number from the remote) and the pin bumps and redeploy it needs (plan 05 runbook); failures outside Task 7's fix budget are listed as todos, not as fixes</read_first>
|
||||
<acceptance_criteria>
|
||||
- The user chose one option and it is recorded in the SUMMARY.
|
||||
</acceptance_criteria>
|
||||
<decision>Push the test commits, and tag and redeploy fixes if there are any?</decision>
|
||||
<context>Test-only commits get no tag (19.4.1 precedent). A production fix in a shared plugin or layer needs a new patch tag (one-way), a go.mod or package.json pin bump in the demo and a redeploy through DEPLOY.md; the table lists each case. If there are no fixes, this only pushes.</context>
|
||||
<options>
|
||||
<option id="push">
|
||||
<name>Push (and release the listed fixes)</name>
|
||||
<pros>Tests are on origin; any fix reaches the live demo before the final check.</pros>
|
||||
<cons>Fix tags are permanent.</cons>
|
||||
</option>
|
||||
<option id="push-tests-only">
|
||||
<name>Push test commits only</name>
|
||||
<pros>No new tags.</pros>
|
||||
<cons>Listed fixes stay local and the live demo keeps the bug.</cons>
|
||||
</option>
|
||||
<option id="hold">
|
||||
<name>Hold</name>
|
||||
<pros>Nothing leaves this machine.</pros>
|
||||
<cons>check-all's suites still run locally, but origin lacks the tests.</cons>
|
||||
</option>
|
||||
</options>
|
||||
<resume-signal>Reply "push", "push-tests-only" or "hold".</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 7: Push, release any fixes, and run the phase gate against the live demo</name>
|
||||
<reversibility rating="one-way">Only when fixes are released: their patch tags are cached by module proxies and lockfiles.</reversibility>
|
||||
<precondition>Task 6 answered "push" or "push-tests-only".</precondition>
|
||||
<files>../sm-demo-app/README.md</files>
|
||||
<read_first>The Task 6 table and answer, ../sm-demo-app/DEPLOY.md (Release, Verify)</read_first>
|
||||
<action>Per SC-2, SC-3, SC-4.
|
||||
- Push the test commits of every repository (plugins and layers first, then vue-demo-app, then sm-demo-app).
|
||||
- For "push" with fixes, in one release batch:
|
||||
- tag each fixed plugin or layer with the next patch (annotated, on the verified HEAD) and push;
|
||||
- bump the demo's go.mod (GOWORK=off go get) or package.json pins in one `chore: bump <name> to <tag>` commit each;
|
||||
- push vue-demo-app before the sm-demo-app pointer bump;
|
||||
- release on rome through DEPLOY.md.
|
||||
- Run `scripts/check-all.sh --local` (every suite, the browser checks, check-live). Fix a failure only within the fix budget below, then run it again. Do not loop past the budget.
|
||||
- Fix budget. It bounds the whole plan: fixes made in Tasks 1 to 5, in this task and for the Task 8 findings.
|
||||
- Source: a fix is allowed only for a failure found by this plan's own tests or checks (Tasks 1 to 5, check-all, check-live, the Task 8 UAT).
|
||||
- Size: each fix is small. It adds no feature, changes no schema or migration, and changes no API route, payload, event, config key, hook, prop or other published contract.
|
||||
- Release: at most one new patch tag per affected repository and at most one redeploy on rome in this plan. Every in-budget fix goes into that one release batch. Under "push-tests-only", fixes are committed but not tagged or deployed, and each one is recorded as a todo.
|
||||
- After the release: a fix that needs no tag or redeploy (a check script or a test in sm-demo-app) is still allowed if it is small. Anything that needs a second tag of the same repository or a second redeploy is outside the budget.
|
||||
- Outside the budget: do not fix it in this plan. Record it as a todo in `.planning/todos/pending/` with its g15office task (`.planning/G15OFFICE.md` rules). List it in the SUMMARY as a decimal-phase candidate (for example `/msd-phase --insert 20.1`). Leave the check-all result as it is, and stop at Task 8's checkpoint with the open failures listed for the user.
|
||||
- Record in README's checks section what the gate now covers.</action>
|
||||
<verify>
|
||||
<automated>../sm-demo-app/scripts/check-all.sh --local 2>&1 | tail -1 | grep -qx 'check-all: ok'</automated>
|
||||
<fails_when>The last line is not "check-all: ok" (a suite, the release build, check-pins, smoke, check-deploy, a browser check or check-live failed; the failing step is named in its own "check-all: <step> failed" line). A failure outside the fix budget keeps this red: it is recorded as a todo and the plan stops at Task 8 instead of fixing further.</fails_when>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- For each repository touched in this plan, `git ls-remote origin` shows the default branch at the local HEAD (or the SUMMARY records the "hold" answer).
|
||||
- Any fix tag equals its pushed HEAD and the demo pins it (`check-pins.sh` prints `check-pins: ok`).
|
||||
- The fix budget held. The SUMMARY lists every production fix with its commit and the failure that found it, every tag created in this plan (no repository twice), and at most one DEPLOY.md release. `git -C <repo> show --stat <fix>` for each listed fix touches no migration (`updates/`) and no parity test file (`php_method_map_test.go`, `golden_test.go`).
|
||||
- Either the last line of check-all is `check-all: ok`, or every remaining failure is recorded as a todo in `.planning/todos/pending/` with a `g15office:` ID and named as a decimal-phase candidate in the SUMMARY, and execution stopped at Task 8 with those failures shown to the user.
|
||||
</acceptance_criteria>
|
||||
<done>Every test is on origin, every in-budget fix is live, and the phase gate is green against the live demo, or each remaining failure is recorded as a todo and decimal-phase candidate for the user at Task 8.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 8: The user checks chat and a DM video call on the live demo with two browsers</name>
|
||||
<read_first>The Task 7 check-all output, ../sm-demo-app/DEPLOY.md (Verify), 20-VALIDATION.md (Manual-Only Verifications)</read_first>
|
||||
<acceptance_criteria>
|
||||
- The user replied "approved". Or each issue they listed was fixed within Task 7's fix budget (released in the plan's one release batch only if it is still unused, check-live green again) and the user approved. Or the issues outside the budget are recorded as todos and decimal-phase candidates in the SUMMARY and the user approved closing the plan with them open.
|
||||
</acceptance_criteria>
|
||||
<what-built>https://demo.summercms.io with chat (channels, DMs, threads, pins, reactions, attachments, slash commands, floating panel), forum, friends and DM video calls through the demo's own LiveKit, moderation of banned words and links, EN and PL, and the tour cards. Anonymous visitors get register prompts on /chat and /forum.</what-built>
|
||||
<how-to-verify>
|
||||
1. Browser A: log in as the visitor (DEMO_VISITOR_EMAIL; the password is SUMMER_GOLEM15__DEMO__VISITOR__PASSWORD in /srv/summercms-demo/.env on rome). Browser B (another profile or device on a different network if possible, for example a phone on mobile data): register a new account.
|
||||
2. Both open /chat, then #general. Send a message from A and see it arrive in B without reloading. Reply in a thread, react, and pin if allowed. Type "/demo" and see the answer only in your own window.
|
||||
3. Add a banned word in the admin (log in at /smbackend with the admin credentials in /root/summercms-demo-admin.txt on rome, Settings › Moderation), then send a message containing it: it is refused with a message. Remove the word again.
|
||||
4. From A, open a DM with B's account and press the call button (it must not appear in #general). In B, accept the prompt. Both see and hear each other; mute, toggle the camera, then leave.
|
||||
5. Check /forum and /friends as a member and anonymously (register prompt), the floating chat panel on /blog, and the Polish language switch.
|
||||
</how-to-verify>
|
||||
<resume-signal>Reply "approved", or describe what is wrong.</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| test harnesses to Docker Postgres and local services | test data only, no production systems |
|
||||
| browser checks to the local and live demo | throwaway accounts, the visitor credentials from .env |
|
||||
| build machine to origin and rome (fix releases only) | tags and releases after the go-ahead |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-20-35 | Information disclosure | credentials in tests and check output | medium | mitigate | tests use generated users and test keys; check scripts read the visitor password from .env and never print it; the live UAT names the .env key, not the value |
|
||||
| T-20-36 | Tampering | regressions hiding behind skipped tests | medium | mitigate | verify commands count "--- PASS" lines so a Docker-less skip fails the gate; check-all runs full (non-short) suites |
|
||||
| T-20-37 | Denial of service | throwaway accounts on the live demo | low | accept | the live check registers no accounts (check-live is read-only); the human UAT creates one, removed by the reset job |
|
||||
| T-20-SC | Tampering | npm/pip/cargo installs | high | mitigate | no new package: tests use vitest 3.2.7 and the existing workspace pins; @vue/test-utils only if already in a lockfile (otherwise the helper is tested directly); Playwright is the existing global install |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- Task 1: moderation suite and TestDemoModeration.
|
||||
- Task 2: chat, forum and moderation suites with Docker; parity tests unedited.
|
||||
- Task 3: sm-demo-app full suite plus the named database tests.
|
||||
- Task 4: four layer suites, vue-demo-app vitest and build.
|
||||
- Task 5: check-local with the two-browser chat and call checks.
|
||||
- Task 6: push and fix-release decision recorded.
|
||||
- Task 7: check-all --local prints check-all: ok against the live demo.
|
||||
- Task 8: the user approves the live demo.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
SC-4: the phase's new code is unit-tested in its last plan. SC-2 and SC-3 are proven live, automatically through check-all and check-live and by the user through a two-browser chat and a DM video call on https://demo.summercms.io.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/20-demo-instance-with-every-shared-plugin/20-06-SUMMARY.md` when done: the test names per repository, bugs found and fixed (commits and tags), what TestDemoVideoStart recorded for a channel start, the check-all output, both checkpoint answers and the user's UAT notes.
|
||||
</output>
|
||||
@@ -527,7 +527,9 @@ stdout_logfile=/var/log/supervisor/summercms-demo-livekit.log
|
||||
| A6 | Centrifugo v6 namespace option names copied from bm's running config are correct for the demo's config format (`channel.proxy.subscribe` + `subscribe_proxy_enabled`, unlike bm's named `proxies`) | Pitfall 5 | Subscribes denied; `centrifugo checkconfig` + `defaultconfig` comparison catches most |
|
||||
| A7 | `livekit-client` is the official LiveKit package (name from chatvideo README/training, registry repo github.com/livekit/client-sdk-js) | Legitimacy | Human-verify checkpoint covers it |
|
||||
|
||||
## Open Questions
|
||||
## Open Questions (RESOLVED)
|
||||
|
||||
Resolved 2026-10-28: Q1 by the 19.4.1 decision (admin at `/smbackend`, `/backend` 404; restated in plan 05's deploy checkpoint); Q2 by D-22 (moderator group, admin screens deferred); Q3 by D-21 (register prompt); Q4 by plans 03/04 (threads, pins, linkified URLs and an embed slot).
|
||||
|
||||
1. **Is the admin public or not?**
|
||||
- What we know: ROADMAP's goal says "The admin backend is not public". 19.4.1-02's DECISION CHANGE (user, 2026-10-20) made the admin public at `/smbackend` with a login required, throttled to 5/min, while `/backend` returns 404. SC-3 says "`/backend` is not reachable". That holds literally.
|
||||
|
||||
@@ -42,7 +42,43 @@ Filled by the planner from the plans; source map in `20-RESEARCH.md` § Validati
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||
| 20-xx-xx | — | — | SC-1..SC-4, D-01..D-18 | — | see RESEARCH Security Domain | unit/integration/live | see RESEARCH test map | ❌ W0 | ⬜ pending |
|
||||
| 20-01-01 | 01 | 1 | D-18, SC-1 | T-20-01, T-20-02 | a rejecting MessageSaving listener gives 422 and stores nothing; no listener changes nothing | integration (Docker) | `go -C ../sm-chat-plugin test -count=1 -run '^TestMessageSavingTracer$' -v .` | ❌ created in task (saving_smoke_test.go) | ⬜ pending |
|
||||
| 20-01-02 | 01 | 1 | D-18 | T-20-01 | forum thread, reply and edit refused before render | integration (Docker) | `go -C ../sm-forum-plugin test -count=1 -run '^TestPostSavingTracer$' -v .` | ❌ created in task | ⬜ pending |
|
||||
| 20-01-03 | 01 | 1 | D-17 | T-20-03, T-20-04 | word and link matching; list caps; settings behind a permission | unit | `go -C ../sm-moderation-plugin test -count=1 -run '^TestFilterSmoke$' -v ./classes` | ❌ new repo | ⬜ pending |
|
||||
| 20-01-04 | 01 | 1 | SC-1 | T-20-05 | modules resolve by tag only | build | `for r in …8 repos; do go -C ../$r vet ./... && go -C ../$r test -short ./...; done` + `GOWORK=off go build` of 5 dependants | ✅ | ⬜ pending |
|
||||
| 20-01-05 | 01 | 1 | — | T-20-05 | go-ahead before one-way tags | checkpoint | — | — | ⬜ pending |
|
||||
| 20-01-06 | 01 | 1 | SC-1 | T-20-05 | remote tags equal verified HEADs | script | `git ls-remote origin refs/tags/<tag>^{}` loop | ✅ | ⬜ pending |
|
||||
| 20-02-01 | 02 | 2 | SC-1, D-06, D-07 | T-20-06, T-20-07 | member posts in #general; channel creation refused | integration (Docker) | `go -C ../sm-demo-app test -count=1 -run '^(TestDemoChatTracer\|TestGoModRequiresTags\|TestBootAllPlugins)$' -v .` + `scripts/check-pins.sh` | ❌ created in task (chat_smoke_test.go) | ⬜ pending |
|
||||
| 20-02-02 | 02 | 2 | D-05, D-08, D-12, D-14, D-17, D-22, D-23 | T-20-09, T-20-10 | banned words and links refused in chat and forum | integration (Docker) | `go -C ../sm-demo-app test -count=1 -run '^TestDemoModerationSmoke$' -v .` + `scripts/build.sh dev` | ❌ created in task | ⬜ pending |
|
||||
| 20-02-03 | 02 | 2 | D-09 | T-20-08, T-20-13 | personas cannot log in; reset empties every chat-family table | integration (Docker) | `go -C ../sm-demo-app test -count=1 -run '^(TestSeedChatSmoke\|TestResetKeepsPlans\|TestSeedRoadmapUpsert)$' -v .` | ❌ / ✅ | ⬜ pending |
|
||||
| 20-02-04 | 02 | 2 | D-06, D-10 | T-20-11, T-20-12 | realtime token only for live members; no secret committed | unit + script | `go -C ../sm-demo-app test -short .` + `centrifugo checkconfig` + `scripts/dev.sh up` + `scripts/check-local.sh realtime` | ✅ | ⬜ pending |
|
||||
| 20-03-01 | 03 | 3 | D-01, D-04, D-07, SC-2 | T-20-15 | anonymous prompt; Bearer only to same-origin APIs | unit + browser | `pnpm --dir ../vue-chat-layer test` + vue-demo-app build + `scripts/check-local.sh chat` | ❌ created in task | ⬜ pending |
|
||||
| 20-03-02 | 03 | 3 | D-02, SC-2 | T-20-14, T-20-19 | text escaped; one realtime connection | unit + browser | layer and vue-demo-app tests + `scripts/check-local.sh chat realtime remote` | ✅ (app) | ⬜ pending |
|
||||
| 20-03-03 | 03 | 3 | D-02, D-05, D-22, D-23 | T-20-14, T-20-16, T-20-17 | linkify on escaped HTML; authorised downloads; ephemeral answers stay local | unit + build | `pnpm --dir ../vue-chat-layer test` + `build:playground` + `scripts/check-local.sh chat` | ✅ | ⬜ pending |
|
||||
| 20-03-04 | 03 | 3 | D-03, D-04 | — | floating panel hidden for anonymous visitors | browser | `scripts/check-local.sh tour chat` | ✅ extend | ⬜ pending |
|
||||
| 20-03-05 | 03 | 3 | D-01 | — | go-ahead before the public tag | checkpoint | — | — | ⬜ pending |
|
||||
| 20-03-06 | 03 | 3 | D-01 | T-20-18 | layer pinned by tag with a committed lockfile | script | remote tag equals HEAD + pin grep + vue-demo-app test and build | ✅ | ⬜ pending |
|
||||
| 20-04-01 | 04 | 4 | D-14, D-21, D-22 | T-20-20 | posts render only server content_html | unit + script | `pnpm --dir ../vue-forum-layer test` + build + `scripts/check-local.sh forum` | ❌ created in task | ⬜ pending |
|
||||
| 20-04-02 | 04 | 4 | D-15 | T-20-25 | friend refusals stay generic | unit + script | `pnpm --dir ../vue-userfriends-layer test` + `scripts/check-local.sh friends forum chat` | ❌ created in task | ⬜ pending |
|
||||
| 20-04-03 | 04 | 4 | D-13 | T-20-SC | livekit-client legitimacy confirmed before install | checkpoint (blocking-human) | — | — | ⬜ pending |
|
||||
| 20-04-04 | 04 | 4 | D-11, D-12, D-13, D-23 | T-20-21, T-20-22, T-20-23, T-20-24 | server-minted tokens; call button only in DMs; media only after start or accept | unit + script | `pnpm --dir ../vue-chatvideo-layer test` + build + `scripts/check-local.sh call chat` | ❌ created in task | ⬜ pending |
|
||||
| 20-04-05 | 04 | 4 | D-16 | T-20-26 | VAPID keys only in .env | script | `scripts/dev.sh up` + `scripts/check-local.sh herald tour` | ❌ check added in task | ⬜ pending |
|
||||
| 20-04-06 | 04 | 4 | D-14, D-15, D-13 | — | go-ahead before public tags | checkpoint | — | — | ⬜ pending |
|
||||
| 20-04-07 | 04 | 4 | SC-2 | T-20-SC | layers pinned by tag | script | remote tags equal HEAD + vue-demo-app test, build + `scripts/check-local.sh forum friends call chat tour` | ✅ | ⬜ pending |
|
||||
| 20-05-01 | 05 | 5 | D-10, D-12, SC-3 | T-20-31 | exact Go allow-list; /_herald unrouted; ports and caps agree | script | `scripts/check-deploy.sh` (last line `check-deploy: ok`) | ✅ extend | ⬜ pending |
|
||||
| 20-05-02 | 05 | 5 | SC-1 | T-20-33 | release build from tags, smoke clean | script | `scripts/check-pins.sh` + `scripts/build.sh release` + `scripts/smoke.sh` | ✅ | ⬜ pending |
|
||||
| 20-05-03 | 05 | 5 | SC-3 | T-20-27 | go-ahead before rome changes | checkpoint | — | — | ⬜ pending |
|
||||
| 20-05-04 | 05 | 5 | SC-2, SC-3, D-10, D-16 | T-20-27, T-20-28, T-20-34 | WavePath untouched; secrets generated on rome | live | `scripts/check-live.sh https://demo.summercms.io` | ✅ extend | ⬜ pending |
|
||||
| 20-05-05 | 05 | 5 | D-10 | T-20-29, T-20-32 | go-ahead before the firewall change | checkpoint | — | — | ⬜ pending |
|
||||
| 20-05-06 | 05 | 5 | D-10 | T-20-29, T-20-32 | only 7891/tcp and 7892/udp opened, through Vuurmuur | live | `ssh … iptables -S` count + `nc -zv … 7891` | ✅ | ⬜ pending |
|
||||
| 20-06-01 | 06 | 6 | D-17, D-18, SC-4 | T-20-10 | settings to filter to listener to 422 | unit + integration | `go -C ../sm-moderation-plugin test ./...` + `-run '^TestDemoModeration$'` | ❌ created in task | ⬜ pending |
|
||||
| 20-06-02 | 06 | 6 | D-18, D-17 | T-20-01, T-20-02 | parity without listeners; refusal on every write path | unit + integration | chat, forum and moderation full suites | ❌ created in task | ⬜ pending |
|
||||
| 20-06-03 | 06 | 6 | D-05 to D-09, D-11, D-12, D-22, D-23, SC-1 | T-20-06, T-20-07, T-20-09, T-20-11 | membership, refused channel create, moderator scope, member-only tokens | integration | `go -C ../sm-demo-app test -count=1 . ./plugins/demo/...` + named tests | ❌ created in task | ⬜ pending |
|
||||
| 20-06-04 | 06 | 6 | D-01, D-02, D-04, D-05, D-13, D-14, D-15, D-21, D-23 | T-20-14 | escaping and DM-only slot pinned | unit | `pnpm test` in four layers + vue-demo-app test and build | ❌ created in task | ⬜ pending |
|
||||
| 20-06-05 | 06 | 6 | SC-2, D-11, D-13, D-23 | — | two-browser realtime; DM call with fake media | browser E2E | `scripts/check-local.sh chat chatlive callmedia forum friends call herald tour realtime remote` | ❌ created in task | ⬜ pending |
|
||||
| 20-06-06 | 06 | 6 | — | — | go-ahead before pushes and fix tags | checkpoint | — | — | ⬜ pending |
|
||||
| 20-06-07 | 06 | 6 | SC-2, SC-3, SC-4 | T-20-36 | full gate including check-live | gate | `scripts/check-all.sh --local` (last line `check-all: ok`) | ✅ extend | ⬜ pending |
|
||||
| 20-06-08 | 06 | 6 | SC-2, SC-3 | — | two-browser chat and DM call on the live demo | manual (UAT) | — | — | ⬜ pending |
|
||||
|
||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||
|
||||
@@ -50,12 +86,14 @@ Filled by the planner from the plans; source map in `20-RESEARCH.md` § Validati
|
||||
|
||||
## Wave 0 Requirements
|
||||
|
||||
- [ ] `sm-demo-app/seed_chat_test.go`, `chat_test.go` (context, channel adapter, resolvers, DM-only calls, bootstrap JSON)
|
||||
- [ ] `sm-chat-plugin` and `sm-forum-plugin` pre-save seam tests
|
||||
- [ ] `sm-moderation-plugin` package and tests (new repo)
|
||||
- [ ] `vue-chat-layer` vitest config and ported bm chat tests (composable store setup)
|
||||
- [ ] `vue-chatvideo-layer`, `vue-forum-layer`, `vue-userfriends-layer` `tests/*.test.ts`
|
||||
- [ ] `scripts/dev.sh` local livekit-server; `scripts/check-local.sh chat`
|
||||
Each gap is closed inside the plan that creates the code (a smoke test or check in the same task), and the full tests come in plan 06 (project rule: unit tests are the last plan).
|
||||
|
||||
- [ ] `sm-demo-app/seed_chat_test.go`, `chat_test.go` (context, channel adapter, resolvers, DM calls, bootstrap JSON): smoke in 20-02 (`chat_smoke_test.go`), full in 20-06 Task 3
|
||||
- [ ] `sm-chat-plugin` and `sm-forum-plugin` pre-save seam tests: smoke in 20-01 Tasks 1-2, full in 20-06 Task 2
|
||||
- [ ] `sm-moderation-plugin` package and tests (new repo): smoke in 20-01 Task 3, full in 20-06 Tasks 1-2
|
||||
- [ ] `vue-chat-layer` vitest config and ported bm chat tests (composable store setup): config and smoke in 20-03 Task 1, ported tests in 20-06 Task 4
|
||||
- [ ] `vue-chatvideo-layer`, `vue-forum-layer`, `vue-userfriends-layer` `tests/*.test.ts`: smoke in 20-04 Tasks 1, 2 and 4, full in 20-06 Task 4
|
||||
- [ ] `scripts/dev.sh` local livekit-server (20-02 Task 4); `scripts/check-local.sh chat` (20-03 Task 1), `forum`, `friends`, `call`, `herald` (20-04), `chatlive` and `callmedia` (20-06 Task 5)
|
||||
|
||||
---
|
||||
|
||||
@@ -70,10 +108,10 @@ Filled by the planner from the plans; source map in `20-RESEARCH.md` § Validati
|
||||
|
||||
## Validation Sign-Off
|
||||
|
||||
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
|
||||
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
|
||||
- [x] All tasks have `<automated>` verify or Wave 0 dependencies (checkpoints excepted)
|
||||
- [x] Sampling continuity: no 3 consecutive tasks without automated verify
|
||||
- [ ] Wave 0 covers all MISSING references
|
||||
- [ ] No watch-mode flags
|
||||
- [x] No watch-mode flags (vitest run, node --test)
|
||||
- [ ] Feedback latency < 60s
|
||||
- [ ] `nyquist_compliant: true` set in frontmatter
|
||||
|
||||
|
||||
Reference in New Issue
Block a user