oficina / meet Public

docs: slice 2 strings

Copy review waived per suite #5; catalogued for the later pass.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzvnuKhLzudvq7jZ7SvVt2
Paul Campbell pushed by paul@keymail.dev 14a972b9bad93d8ad58a1e31db45f0b784a4c128 parent 6354af6
1 file changed, +148 −0
  • docs/superpowers/copy/2026-09-10-slice-2-strings.md +148 −0

CI passed — run details

diff --git a/docs/superpowers/copy/2026-09-10-slice-2-strings.md b/docs/superpowers/copy/2026-09-10-slice-2-strings.md
new file mode 100644
index 0000000..c9f7b34
--- /dev/null
+++ b/docs/superpowers/copy/2026-09-10-slice-2-strings.md
@@ -0,0 +1,148 @@
+# Slice 2 copy list
+
+Copy review waived for this round per suite discussion 5; catalogued for the later pass.
+
+This covers every user-facing string introduced on branch `slice/2-room`
+(`77227de..6354af6`): the holding screen's scripted half, the room and its
+client, the removal and takeover confirm pages, and the one `AGENTS.md`
+prose change a person could trace back to a screen. Sources: `git diff
+77227de..6354af6 -- internal/meet/templates internal/meet/static
+internal/meet/handlers_room.go internal/meet/handlers_join.go AGENTS.md`,
+task-6/7/8-report.md's own "new/changed user-facing strings" lists, and the
+reviewers' copy notes recorded in `.superpowers/sdd/2026-09-10-meet-slice-2-room/progress.md`.
+No string below has been through `copy-review`; none was changed to write
+this file.
+
+A note on the JSON refusal bodies: `stream.go`'s 410 body carries only a
+machine reason code (`buffer`, `replaced`, `standing`, `expired`,
+`removed`, `ended`) in `body["reason"]` — nothing in that JSON is prose a
+person reads. `room.js`'s `terminal(reason)` looks the code up in its own
+`TERMINAL` table to get the sentence a person actually sees, so the
+sentences in the Terminal sentences table below are the complete set of
+what those refusal bodies can cause to appear.
+
+## Holding page (`/m/{sqid}/join`)
+
+`templates/join.html`
+
+| String | Where | Note |
+| --- | --- | --- |
+| This meeting is already open in another tab. | `join.html`, `content` block, `OpenElsewhere` notice | Reviewer note (progress.md, Task 6 review): untrue during the resume window — a session whose stream has merely lapsed, not collided, still sees this. Suggested replacement, not applied to code: "You are already in this meeting from this browser." |
+| Use this tab instead | `join.html`, `content` block, link inside the `OpenElsewhere` notice, to `/m/{sqid}/takeover` | Same notice as above; this is the link text, not a separate sentence. |
+| The call needs JavaScript to reach your camera and the other participants; everything else on this page works without it. | `join.html`, `content` block, `<noscript>` | |
+| Your camera preview | `join.html`, `content` block, `aria-label` on `#preview` | Added in Task 7's fix round (task-7-report.md item 11). |
+| Camera | `join.html`, `content` block, `field-select` Label for `#pick-camera` | Same word is the fixed `aria-label`/button text below, on `#toggle-camera`. |
+| Microphone | `join.html`, `content` block, `field-select` Label for `#pick-mic` | Same word is the fixed `aria-label`/button text below, on `#toggle-mute`. |
+| Speaker | `join.html`, `content` block, `field-select` Label for `#pick-speaker` (row hidden unless `setSinkId` is supported and a speaker is found) | |
+| Microphone | `join.html`, `content` block, `#toggle-mute` — `aria-label` and visible button text | Task 7 fix: the accessible name is now fixed; only `aria-pressed` and the state word move. Replaces earlier "Mute"/"Unmute" wording (task-7-report.md item 10). |
+| Camera | `join.html`, `content` block, `#toggle-camera` — `aria-label` and visible button text | Same fix; replaces earlier "Camera off"/"Camera on". |
+| Join the call | `join.html`, `content` block, `#join` button | |
+
+`static/devices.js`
+
+| String | Where | Note |
+| --- | --- | --- |
+| This browser cannot reach a camera or microphone here. You can still join with neither. | `devices.js`, top level (no `navigator.mediaDevices.getUserMedia`) | "You can still join with neither" recurs in all four device-error/no-support sentences below — the room is built to allow joining with no local media, and every sentence says so the same way. |
+| Asking for your camera and microphone… | `devices.js`, `startPreview()` | Task 7 fix: now said before the `getUserMedia` call, not only after it fails (task-7-report.md item 9). |
+| Meet needs permission to use your camera and microphone. You can still join with neither. | `devices.js`, `errorSentence()` (`NotAllowedError`) | |
+| Your camera or microphone is already in use by another app. You can still join with neither. | `devices.js`, `errorSentence()` (`NotReadableError`) | New in Task 7's fix round; previously fell through to the generic sentence (task-7-report.md item 8). |
+| Meet could not reach a camera or microphone. You can still join with neither. | `devices.js`, `errorSentence()` (default — anything but `NotAllowedError`/`NotReadableError`) | Task 7 fix replaces an earlier "No camera or microphone was found…" (task-7-report.md item 8). |
+| Muted / On | `devices.js`, `applyToggles()`, `#toggle-mute-state` | State word, never colour alone; moves with `prefs.muted`. |
+| Off / On | `devices.js`, `applyToggles()`, `#toggle-camera-state` | State word; moves with `prefs.cameraOff`. |
+| Camera `<n>` | `devices.js`, `labelFor()` | Fallback option label in `#pick-camera` when the browser gives no `device.label` (before permission is granted). |
+| Microphone `<n>` | `devices.js`, `labelFor()` | Same fallback, for `#pick-mic`. |
+
+## Room page (`/m/{sqid}`)
+
+`templates/room.html` — an entirely new file this branch. Several of its
+lines are the holding screen's own wording, reused: noted per row.
+
+| String | Where | Note |
+| --- | --- | --- |
+| Starts {{.Starts}}. | `room.html`, `content` block (meeting has a start time) | Mirrors the holding screen's identical line (already catalogued for slice 1). |
+| You are {{.Name}}, joining as a {{.Standing}}. | `room.html`, `content` block | Task 6 changed this from an initial "in the call as a `<kind>`." to match the holding screen's wording exactly — true before any participation exists, since this renders server-side ahead of anything `room.js` does (task-6-report.md item 14). |
+| This team's Meet has no relay yet. Calls usually connect only between people on the same network. | `room.html`, `content` block, floor notice (no relay configured) | Mirrors the holding screen's identical notice. |
+| The call needs JavaScript to reach your camera and the other participants; everything else on this page works without it. | `room.html`, `content` block, `<noscript>` | Same sentence as the holding screen's `<noscript>`, with a link appended (next row) the holding screen's version doesn't have. |
+| Back to the holding screen | `room.html`, `content` block, link inside the `<noscript>`, to `/m/{sqid}/join` | Same wording `room.js`'s own `wayOut()` uses for its link of the same name (see Links, below) — the scriptless and scripted paths land on the same words. |
+| Microphone | `room.html`, `content` block, `#mute` — `aria-label` and visible button text | Same fixed-name pattern as the holding screen's toggle. |
+| Camera | `room.html`, `content` block, `#camera` — `aria-label` and visible button text | Same fixed-name pattern as the holding screen's toggle. |
+| Leave | `room.html`, `content` block, `#leave` button (`rst-btn="danger"`) | |
+| Waiting to join | `room.html`, `content` block, `#queue` heading (`h2`) | Shown only when the admission queue is non-empty. |
+| Meetings | `room.html`, `content` block, back-nav Label (members only) | Same established back-nav label used across the app (already catalogued for slice 1); reused here, not new wording. |
+| End the meeting | `room.html`, `content` block, member-only link to `/meetings/{sqid}/end` | Same established link text (already catalogued for slice 1); `target="_blank" rel="noopener"` is new here — the room holds the call in this tab, so ending opens in a new one. |
+
+`static/room.js`
+
+### Status sentences (`renderStatus()`, via `say()`)
+
+| String | Where | Note |
+| --- | --- | --- |
+| The room is full. | `room.js`, `renderStatus()` | Shown while `state.full` is set, cleared by the next roster. |
+| Waiting for someone to let you in. | `room.js`, `renderStatus()` | Shown to a guest that has knocked (joined, not yet admitted). A member in the same window is "connecting", never "waiting" — a member is admitted by its own join. |
+| Connecting to the call. | `room.js`, `renderStatus()` | Default state between `hello` and admission, for a member or a guest that hasn't joined yet. Task 8 review flagged the risk of this sentence showing forever behind an unnamed refusal; the stall counter (`MAX_STALLS`/`STALL_WINDOW`) and the terminal sentences below are the fix. |
+| You are the only one here. | `room.js`, `renderStatus()` | Shown once admitted with no other peers in the mesh. |
+
+### Terminal sentences (`TERMINAL`, said by `terminal()`)
+
+| String | Where | Note |
+| --- | --- | --- |
+| This meeting is open in another tab. | `room.js`, `TERMINAL.replaced` | The one reason with no "Back to the holding screen" link appended — it already has its own real next step (the takeover link, below), and the browser gate reads `#status a` as a single element (task-8-report.md, Deviations item 1). |
+| You no longer have access to this meeting. | `room.js`, `TERMINAL.standing` | |
+| Your invitation to this meeting has expired. | `room.js`, `TERMINAL.expired` | |
+| Someone removed you from this meeting. | `room.js`, `TERMINAL.removed` | |
+| This meeting has ended. | `room.js`, `TERMINAL.ended` | Also the fallback for any reason code `TERMINAL` doesn't recognise. |
+| You cannot join this meeting. | `room.js`, `TERMINAL.gone` | Task 8 review: "the fresh-404 sentence must be neutral" — replaces an earlier "This meeting is no longer open." (progress.md Task 8 note; task-8-report.md item 1 of "New user-facing strings"). |
+| You have been signed out. | `room.js`, `TERMINAL.signedout` | |
+| This browser could not open the call’s connection. | `room.js`, `TERMINAL.accept` (406) and `TERMINAL.badlast` (400) | Same sentence for both refusals; written as `’` in the source. |
+| Meet cannot reach the call. The room may be full, or you may have been signed out. | `room.js`, `TERMINAL.unreachable` | Reached after `MAX_STALLS` rounds with no `hello` inside `STALL_WINDOW` — a guess covering the two causes the client can't tell apart. |
+| This browser cannot make calls. | `room.js`, `TERMINAL.nowebrtc` | |
+
+### Links (`wayOut()`)
+
+| String | Where | Note |
+| --- | --- | --- |
+| Sign in again | `room.js`, `wayOut()`, link to `/signin` | Shown only for `signedout`, and it is the *only* link on that screen — no "Back to the holding screen" is appended, because that would just bounce the person straight back to sign-in (task-8-report.md, Deviations item 1). |
+| Use this tab instead | `room.js`, `wayOut()`, link to `/m/{sqid}/takeover` | Shown only for `replaced`. Same wording as the takeover confirm page's own submit button (see below). |
+| Try again | `room.js`, `wayOut()`, link to `/m/{sqid}` | Shown only for `unreachable`, alongside "Back to the holding screen". |
+| Back to the holding screen | `room.js`, `wayOut()`, link to `/m/{sqid}/join` | Shown for every terminal reason except `replaced` and `signedout` — the rule is that no terminal screen is a dead end. |
+
+### Tile captions
+
+| String | Where | Note |
+| --- | --- | --- |
+| `<name>` (you) | `room.js`, `tile()` | The self tile's caption; `<name>` is the server-rendered `data-self-name`, never typed by this browser. |
+| Someone | `room.js`, `renderTiles()` | Fallback caption for a peer whose name hasn't arrived from the server yet. |
+| Could not connect. | `room.js`, `tile()` | Shown on a tile whose pair has failed; never colour alone — this sentence plus `.meet-tile-note`'s weight are the two cues. |
+| Retry | `room.js`, `tile()` | Button shown beside "Could not connect.", restarts ICE (or requests a fresh generation if the browser has no `restartIce`). |
+| Play | `room.js`, `tile()` / `startPlaying()` | Shown only if the browser refuses autoplay on a remote track; new in Task 8's fix round (task-8-report.md item 7 / "New user-facing strings"). |
+| Remove | `room.js`, `removeLink()` | A member's removal control on another peer's tile; opens `/m/{sqid}/remove/{peer}` in a new tab (§3.5 destructive-action rule). |
+
+### The queue
+
+| String | Where | Note |
+| --- | --- | --- |
+| Someone | `room.js`, `renderQueue()` | Fallback name for a waiting guest whose name hasn't been sent. Same fallback word as the tile caption above, independent occurrence. |
+| Admit | `room.js`, `renderQueue()` | Button beside each waiting guest's name; posts `admit` for that peer. |
+
+## Removal and takeover confirm pages
+
+`handlers_room.go`, rendered through `templates/confirm.html`
+
+| String | Where | Note |
+| --- | --- | --- |
+| Remove `<name>` from the call? | `handlers_room.go`, `confirmRemove()` — `Title` | `<name>` comes from `hub.PeerNameFor`, the same authority `Remove` itself checks — this page can never name a peer it couldn't actually remove. Earlier draft named the meeting instead of the person (task-6-report.md, Concerns); fixed in Task 6's fix round. Applies equally to a waiting guest in the admission queue — there is no separate wording for that case (progress.md, Task 6 review note). |
+| They will be disconnected and can rejoin only if they are still allowed in. | `handlers_room.go`, `confirmRemove()` — `Body` | One sentence for both actor kinds rather than branching copy: true for a guest, who has only the link, and for a member, who can also come back from the meeting list (code comment, `handlers_room.go`; noted there for this catalogue). |
+| Remove | `handlers_room.go`, `confirmRemove()` — `Label` (submit button, via `confirm-form`) | |
+| Cancel | `templates/confirm.html`, `content` block, `confirm-form` `CancelLabel` | Pre-existing text (already catalogued for slice 1); `confirmRemove`'s and `confirmTakeover`'s `CancelHref` now both point at `/m/{sqid}/join`, not back into the room — a second `/m/{sqid}` would open a second stream and trip the takeover 409 for someone who only pressed Cancel (code comment, `handlers_room.go`; issue 11 half-closed per progress.md, Task 6 review). |
+| `<meeting title>` | `handlers_room.go`, `confirmRemove()` and `confirmTakeover()` — `BackLabel`, via `a.store.title(m)` | Back-nav label on both confirm pages, replacing the generic "Meetings" default; `confirm.html`'s `{{or .BackLabel "Meetings"}}` is what makes this possible (task-6-report.md item 9). |
+| This meeting is open in another tab | `handlers_room.go`, `confirmTakeover()` — `Title` | |
+| Use this tab instead? The other tab will leave the call. | `handlers_room.go`, `confirmTakeover()` — `Body` | |
+| Use this tab | `handlers_room.go`, `confirmTakeover()` — `Label` (submit button) | Distinct from `room.js`'s "Use this tab instead" link text (`wayOut()`) and the holding screen's identical link text — three slightly different wordings for the same move, not yet reconciled. |
+
+## The `AGENTS.md` exception line
+
+| String | Where | Note |
+| --- | --- | --- |
+| Screens work with JavaScript disabled, with one exception: browser media participation (the holding screen's camera preview and join button, and the call itself in the room) needs script; both pages render their title, standing, floor notice and navigation without it. Destructive actions get their own confirm-page URL. | `AGENTS.md`, "Rules the family holds hard" | Replaces the prior unqualified "Screens work with JavaScript disabled; destructive actions get their own confirm-page URL." — the exception is scoped to exactly the two pages above. |
+| `make browser` is the second gate, for any change to a page or to the client: a real call in real Chromium. It needs the Claude Code bash sandbox **off** — the sandbox blocks UDP, so Chromium gathers no ICE candidates and every call fails with `frames` stuck at 0, which reads exactly like a client bug and is not one. | `AGENTS.md`, "Rules the family holds hard", new bullet | Developer-facing (this doc, and whoever else reads `AGENTS.md`), not end-user copy, but a string introduced on this branch a person reads. |
+| Accessible by construction, progressively enhanced, and never dependent on JavaScript to be usable, with the one exception named above: browser media participation, on the holding screen and in the room. | `AGENTS.md`, "UX conventions" intro line | Same exception restated where the UX-conventions section makes the blanket claim; kept consistent with the Rules-section wording rather than contradicting it (task-6-report.md item 10). |