From f1236743ae912b80a08fc30b191f2eee01ee493e Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Sat, 31 Oct 2026 14:19:57 +0100 Subject: [PATCH] docs(20-03): vue-chat-layer v0.1.0 and chat on the demo site summary --- .../20-03-SUMMARY.md | 298 ++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 .planning/phases/20-demo-instance-with-every-shared-plugin/20-03-SUMMARY.md diff --git a/.planning/phases/20-demo-instance-with-every-shared-plugin/20-03-SUMMARY.md b/.planning/phases/20-demo-instance-with-every-shared-plugin/20-03-SUMMARY.md new file mode 100644 index 00000000..e8fdf8c0 --- /dev/null +++ b/.planning/phases/20-demo-instance-with-every-shared-plugin/20-03-SUMMARY.md @@ -0,0 +1,298 @@ +--- +phase: 20-demo-instance-with-every-shared-plugin +plan: 03 +subsystem: vue-chat-layer, vue-demo-app +tags: [chat, nuxt-layer, realtime, centrifuge, i18n, slash-commands, threads, pins] + +requires: + - phase: 20-02 + provides: "Host bootstrap GET /_demo/api/v1/chat and /chat/members, the chat family mounted, realtime for every member" +provides: + - "vue-chat-layer v0.1.0 on git.golem15.com (public), tag v0.1.0 = c465414" + - "The page's one Centrifuge client with onUserEvent, shared by the chat and the presentations remote" + - "vue-demo-app /chat, the floating chat panel, the Chat tour card, EN/PL" + - "check-local chat (check-chat.cjs), tour with seven cards, smoke.sh /chat" +affects: [20-04, 20-05, 20-06] + +actuals: + tokens: 80100 # chars/4: layer tracked files without the lockfile (279087) + vue-demo-app diff 44a5229..3fe8018 without the lockfile (30546) + sm-demo-app scripts diff f4ed2c2..b4fc7dc (10832) + tasks: 6 + commits: 0 # MEASURED in summercms.go before this SUMMARY commit (docs only here); code commits: vue-chat-layer 4, vue-demo-app 4, sm-demo-app 2 +plan_head_before: baa0f20eb2cd316cb3424fe6b4fd957d079713c0 # summercms.go +plan_head_after: baa0f20eb2cd316cb3424fe6b4fd957d079713c0 # summercms.go before this SUMMARY commit + +tech-stack: + added: + - "vue-chat-layer (new shared Nuxt layer) with centrifuge 5.7.3 and @nuxtjs/i18n 10.4.0; vitest 3.2.7, nuxt 4.4.8, vue 3.5.35, typescript 5.9.3 as dev deps (all versions already pinned in the workspace)" + patterns: + - "Store as a composable over useState keys prefixed chat:, one reactive store per Nuxt app (no Pinia)" + - "Auth hook $chatAuth {token, refresh, unauthorized, userId?}; pure Nuxt-free client in app/lib, imported explicitly" + - "One realtime client per page; other layers get user:{me} events through onUserEvent" + +key-files: + created: + - ../vue-chat-layer/nuxt.config.ts + - ../vue-chat-layer/README.md + - ../vue-chat-layer/app/lib/client.ts + - ../vue-chat-layer/app/lib/realtime.ts + - ../vue-chat-layer/app/lib/linkEmbeds.ts + - ../vue-chat-layer/app/composables/useChat.ts + - ../vue-chat-layer/app/components/Chat/FloatingPanel.vue + - ../vue-chat-layer/app/components/Chat/CommandPicker.vue + - ../vue-chat-layer/app/components/Chat/ThreadPane.vue + - ../vue-chat-layer/app/assets/css/chat-tokens.css + - ../vue-chat-layer/tests/smoke.test.ts + - ../sm-demo-app/vue-demo-app/app/pages/chat.vue + - ../sm-demo-app/scripts/check-chat.cjs + modified: + - ../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/lib/realtime.ts + - ../sm-demo-app/vue-demo-app/app/lib/realtime.test.ts + - ../sm-demo-app/vue-demo-app/vitest.config.ts + - ../sm-demo-app/vue-demo-app/app/layouts/default.vue + - ../sm-demo-app/vue-demo-app/app/pages/index.vue + - ../sm-demo-app/vue-demo-app/app/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 + +key-decisions: + - "Task 5 checkpoint answer: release. golem15/vue-chat-layer created public with tea (same visibility as vue-faq-layer), main pushed, annotated tag v0.1.0 on c465414" + - "The layer reads the caller's user id from the JWT's numeric sub claim; an optional $chatAuth.userId() overrides it" + - "vue-demo-app chatAuth.refresh posts to POST /_user/api/v1/refresh and stores the rotated token through lib/api/client.ts tokens (document.cookie)" + - "Ported libs keep bm's signatures with optional localized copy and setFormatLocale (defaults pl-PL / Europe/Warsaw), so bm's tests port unchanged in plan 06" + - "Slash command answers render through renderChatText, the only v-html besides message bodies" + +patterns-established: + - "A layer's realtime client is re-exported by the app (export * from 'vue-chat-layer/app/lib/realtime'); vitest inlines the layer and dedupes centrifuge" + +requirements-completed: [SC-2, D-01, D-02, D-03, D-04, D-05, D-07, D-20, D-22, D-23] + +coverage: + - id: D1 + description: "A logged-in visitor opens #general on /chat and a message sent from the composer appears; anonymous /chat has the register link to /register?next=/chat and no chat" + requirement: "D-02, D-04" + verification: + - kind: e2e + ref: "../sm-demo-app/scripts/check-chat.cjs (check-local.sh chat)" + status: pass + - kind: smoke + ref: "../sm-demo-app/scripts/smoke.sh (/chat in the page loop, register prompt, no chat in the HTML)" + status: pass + human_judgment: false + - id: D2 + description: "The chat client sends the bearer, refreshes once on 401, calls unauthorized after a failed refresh, maps 422 to field errors" + requirement: "D-01" + verification: + - kind: unit + ref: "../vue-chat-layer/tests/smoke.test.ts" + status: pass + human_judgment: false + - id: D3 + description: "One shared Centrifuge client: the presentations remote and the chat ride one socket; bm's realtime test passes against it" + requirement: "SC-2" + verification: + - kind: unit + ref: "../sm-demo-app/vue-demo-app/app/lib/realtime.test.ts" + status: pass + - kind: e2e + ref: "check-local.sh realtime remote" + status: pass + human_judgment: false + - id: D4 + description: "Live delivery, /demo command (ephemeral, nothing posted), unknown command row, link card, reaction, thread reply and live thread reply, pins, attachment upload and send, DM from the picker, edit, delete, PL UI, floating panel badge and DM reply, panel hidden on /chat and decks" + requirement: "SC-2, D-02, D-03, D-05" + verification: + - kind: manual + ref: "Playwright probes against the local stack with two members (scratchpad live.cjs, features.cjs, floating.cjs)" + status: pass + human_judgment: true + - id: D5 + description: "Tour has seven cards with Chat (EN) and Czat (PL); vue-demo-app builds and tests against the tag" + requirement: "D-03, D-01" + verification: + - kind: e2e + ref: "check-local.sh chat tour after the tag pin" + status: pass + - kind: build + ref: "pnpm --dir ../sm-demo-app/vue-demo-app test (209) && run build" + status: pass + human_judgment: false + +duration: 52min +completed: 2026-10-31 +status: complete +--- + +# Phase 20 Plan 03: vue-chat-layer v0.1.0 and chat on the demo site Summary + +**vue-chat-layer v0.1.0 is released on git.golem15.com. It carries bm's chat ported to a Pinia-free Nuxt layer and widened to several channels, threads, pins, link cards and a slash command picker. The demo site now has /chat for members, a register prompt for visitors, a floating DM panel on every page and a Chat tour card, in EN and PL.** + +## Performance + +- **Duration:** about 52 min +- **Started:** 2026-10-31T12:27Z +- **Completed:** 2026-10-31T13:19Z +- **Tasks:** 6/6. Task 5 was the checkpoint, answered "release". +- **Files:** vue-chat-layer has 52 tracked files without the lockfile. vue-demo-app has 13 files changed (436 insertions, 147 deletions). sm-demo-app scripts have 3 files changed. + +## The layer's public API (v0.1.0) + +**Runtime config:** + +| Key | Env | Purpose | +|-----|-----|---------| +| `chat.apiBase` | `NUXT_CHAT_API_BASE` | Server-side base of the chat API | +| `public.chat.apiBase` | | `/_chat/api/v1` | +| `public.chat.channelsApiBase` | | `/_chatchannels/api/v1` | +| `public.chat.botsApiBase` | | `/_chatbots/api/v1` | +| `public.chat.hostApiBase` | | `''`. With an empty value the chat does not start, and a console error names the key | + +**Hook:** `$chatAuth` with `token()`, `refresh()`, `unauthorized()` and an optional `userId()`. + +**Host contract:** `GET {hostApiBase}/chat` and `GET {hostApiBase}/chat/members`. The README documents both with a neutral `acme` host. + +**Slots:** +- `conversation-actions`, with `{ conversation }`. It renders only for a DM, in `ChatHeader`, `ChatPanel`, `ChatPage` and `ChatFloatingPanel`. +- `sidebar-footer`, in `ChatChannelList` and `ChatPage`. + +**Components:** `ChatPage`, `ChatFloatingPanel` (prop `chatPath`), `ChatChannelList`, `ChatDmList`, `ChatPanel`, `ChatHeader`, `ChatMessageList`, `ChatMessage`, `ChatComposer`, `ChatThreadPane`, `ChatPinsPanel`, `ChatReactionBar`, `ChatStagingChips`, `ChatDeleteDialog`, `ChatMemberPicker`, `ChatMentionPopup`, `ChatCommandPicker`, `ChatLinkEmbed`, `ChatAvatar`, `ChatIcon` and `ChatNotices`. + +**Composables:** `useChat`, `useChatApi` (with `useChatAuth` and `useChatConfig`) and `useChatNotice`. + +**Library exports:** +- `client.ts`: `createChatClient`, `ChatApiError`, `userIdFromToken`. +- `realtime.ts`: `REALTIME_WS_PATH`, `connect`, `subscribe`, `presence`, `openChannel`, `onUserEvent`, `dispatchUserEvent`, `setRealtimeAuth`, `disconnect`, `onConnection`, `onUnavailable`. +- `linkEmbeds.ts`: `linkify`, `messageUrls`, `registerEmbedRenderer`, `embedRendererFor`. +- The ported `chatEvents`, `chatFormat`, `chatText` and `chatUnread`, plus `format` (`setFormatLocale`), `attachments`, `markdown`, `media` and `tabs`. + +**Copy:** EN and PL under `chat.*`, 131 keys. PL covers every EN key. + +## What was stripped from bm and why + +- **bm's embeds and the embed picker were dropped** (`deadline`, `class` and `reading`, plus `EmbedPicker`, `EmbedCard`, `lib/embeds.ts` and the paste-to-embed logic). They are bm entities. In their place: + - A generic link card shows the host and path, with no fetch. + - `registerEmbedRenderer` lets a host render its own entity links (the D-01 extension point). +- **The single `# ogólny` model became `channels[]`, `activeId` and `kind`.** bm's "Czat roku" tab layout became a sidebar plus pane. Threads, pins and the command picker are new. +- **The Pinia stores went away.** `useChat` became `useState` and `useToast` became `useChatNotice`. +- **The bm cached membership flag in localStorage and its BM bootstrap paths were removed.** Every string went to i18n. + +## Task Commits + +| # | Repo | Commit | Message | +|---|------|--------|---------| +| 1 | vue-chat-layer | fa6f995 | feat: chat layer tracer: bootstrap, channel list, messages, composer | +| 1 | vue-demo-app | 5b7648f | feat: /chat page on vue-chat-layer | +| 1 | sm-demo-app | b6ece43 | test: check-chat browser check | +| 2 | vue-chat-layer | 5d58153 | feat: port the chat store, realtime client and components | +| 2 | vue-demo-app | ae3dbfc | refactor: share the chat layer's realtime client | +| 3 | vue-chat-layer | 269c75b | feat: threads, pins, reactions, attachments, link embeds and the command picker | +| 4 | vue-chat-layer | c465414 | feat: floating chat panel | +| 4 | vue-demo-app | 8636aa3 | feat: floating chat panel, Chat tour card | +| 4 | sm-demo-app | b4fc7dc | test: tour expects the Chat card, smoke covers /chat | +| 6 | vue-chat-layer | tag v0.1.0 (1e6a907, peels to c465414) | vue-chat-layer v0.1.0 | +| 6 | vue-demo-app | 3fe8018 | feat: pin vue-chat-layer v0.1.0 | + +**Pushed:** only vue-chat-layer `main` and the tag `v0.1.0`. They went to the new public repository `git@git.golem15.com:golem15/vue-chat-layer.git`, created with `tea repo create`. `git ls-remote origin 'refs/tags/v0.1.0^{}'` equals HEAD c465414. + +**Not pushed:** vue-demo-app and sm-demo-app (plan 05). sm-demo-app shows the moved `vue-demo-app` submodule pointer as uncommitted; plan 05 bumps it. + +## Checkpoint answer + +Task 5: **release**. The user gave the go-ahead to create the repository, push main and tag v0.1.0. + +## Verification + +- **Layer:** `pnpm test` passes 6/6, and `pnpm build:playground` builds. +- **vue-demo-app:** `pnpm test` passes 209/209 and `pnpm run build` builds, both against the link pin and against the tag. +- **check-local.sh:** these runs all printed ok. + - `chat` (Task 1) + - `chat realtime remote` (Task 2) + - `chat` (Task 3) + - `tour chat realtime remote` (Task 4) + - `chat tour` after the tag pin (Task 6) +- **smoke.sh:** `smoke: ok` with /chat included. +- **Manual (Task 3 criterion):** `/demo` typed in the #general composer opened the picker, and Enter showed the ephemeral answer. The channel had 18 messages before and 18 after, so nothing was posted. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 2 - Missing critical] The caller's user id** +- **Found during:** Task 2. +- **Issue:** The demo bootstrap and `/chat/members` do not include the caller. The store needs the id for `user:{me}` and for telling its own messages apart. +- **Fix:** `userIdFromToken` reads the JWT's numeric `sub` claim. An optional `$chatAuth.userId()` overrides it. Documented in the README. Commit 5d58153. + +**2. Refresh endpoint** +- useDemoAuth has no refresh. +- `chatAuth.refresh` calls `POST /_user/api/v1/refresh` with the bearer token. The user plugin answers the rotated token in the body. +- The token is stored through `lib/api/client.ts` `tokens` (`document.cookie`), because the hook runs outside a component's setup, where `useCookie` fails. Commits 5b7648f and ae3dbfc. + +**3. [Rule 3 - Blocking] Realtime test and vitest config** +- vue-demo-app's `app/lib/realtime.test.ts` is now bm's current realtime test against the shared client. Two adjustments go beyond import paths: + - The token route is a stubbed `fetch` instead of a mocked `@/api/client`, because the layer fetches the token itself. + - The 503 case asserts `{status: 503}`, since the error is now `RealtimeTokenError`. +- `vitest.config.ts` (outside Task 2's file list) got `resolve.dedupe: ['centrifuge']` and `test.server.deps.inline: ['vue-chat-layer']`. With these the mock applies, and the tag-installed layer's TypeScript is transformed. Commit ae3dbfc. + +**4. Extra layer files and the shape of the ported libs** +- Two files are outside the plan's list: + - `app/lib/markdown.ts`: `escapeHtml`, `safeUrl` and `formatSpans`, for chatText. + - `app/lib/attachments.ts`: `checkFiles`. +- The ported libs keep bm's signatures, with optional localized copy. `setFormatLocale` configures the format locale; its defaults stay pl-PL and Europe/Warsaw, so bm's tests port unchanged in plan 06. The components set the app's locale and the browser's time zone. +- `chatEvents` adds an optional `rootId` to created events, so replies stay out of the main list. + +**5. Ephemeral answers** +- Slash command answers render through `renderChatText`. This keeps the prohibition: `v-html` is used only for the escaped renderer output. +- `linkify` is exported but not used by the components. + +**6. Naming and copy** +- The app's `chat.*` keys (`eyebrow`, `title`, `lead`, `slashHint`, `anonymous`, `registerToChat`) avoid the layer's `chat.*` tree. The layer's own title key is `chat.name`. +- `{'@'}` escapes @ in the app copy, because a bare @ is vue-i18n link syntax. +- /chat also has a Log in link next to the register link. + +**7. Dev-stack note** +- Running `nuxt build` while `nuxt dev` is up makes the dev server loop on "Another Nuxt dev is already running". +- The stack was restarted with `dev.sh down` and `dev.sh up` after each build. + +**8. Pins** +- The visitor was allowed to pin in #general, and the pin succeeded. +- The refused-pin path (a notice, then the action hidden for the session) is in the code but was not exercised. + +**Total deviations:** 8. **Impact:** none on the plan goal. + +## Known Stubs + +None. Link cards are generic by design (D-01; there is no unfurl backend). + +## Issues Encountered + +- **vue-tsc on the playground** (run with summercms.go/admin's vue-tsc, not a gate) reports only `noUncheckedIndexedAccess` errors (the "possibly undefined" kind). They are mostly in the modules ported from bm and come from the source. Builds don't typecheck. + +## Threat Flags + +| Flag | File | Description | +|------|------|-------------| +| threat_flag: supply-chain | ../sm-demo-app/vue-demo-app/package.json | New git dependency vue-chat-layer pinned by tag v0.1.0 (commit c465414 in the lockfile), as planned in T-20-18 | + +## Next Phase Readiness + +- **Plan 04** (vue-chatvideo-layer) can extend `vue-chat-layer#v0.1.0`: + - Call button: put it in the `conversation-actions` slot, which renders for DMs only. + - `video:*` events: listen through `onUserEvent`. + - Session channels: subscribe through the shared `subscribe`. +- **Plan 05** needs to know: + - vue-demo-app is ready to push. Its `package.json` and lockfile pin the tag, with no local path. + - The sm-demo-app submodule pointer bump is still to do. + +## Self-Check: PASSED + +- The listed files exist in their repos: vue-chat-layer, vue-demo-app and sm-demo-app scripts. +- Commits exist: + - vue-chat-layer: fa6f995, 5d58153, 269c75b, c465414. + - vue-demo-app: 5b7648f, ae3dbfc, 8636aa3, 3fe8018. + - sm-demo-app: b6ece43, b4fc7dc. +- The remote tag v0.1.0 peels to c465414.