Public: anyone can read this; the crew can change it.
A repository's Settings › Machines speaks the home page's language #9801
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/9788-a-repository-s-settings-machines-speaks → main 3 commits fork 2 threads · 2 unresolved
3 files
changed +58
−47
docs/design/areas/machines.md +7
| @@ -15,3 +15,10 @@ table and `btn` it already had; no new class there. | ||
| 15 | 15 | shifts as the words change; muted text, a polite live region. The server |
| 16 | 16 | renders it empty and the script says "Connecting…"; without JavaScript a |
| 17 | 17 | `<noscript>` line in the same class says the shell needs it. |
| 18 | ||
| 19 | Settings › Machines (`/:owner/:name/settings/machines`) adds no component class. As on the home page it is headings over | |
| 20 | rows: Machines and Last sessions are each a `<section class="settings-block">` with an `<h2>`. Each machine is a `row()` in | |
| 21 | `<ul class="list setting-list" id="machines">` (tools and reach in its meta, "Open shell" as its trail, writers only as the | |
| 22 | page is); each session a `row()` in `#machine-sessions`, a running one marked by a ● glyph and a "running" badge, an ended | |
| 23 | one by a dash and its reason in words, so state is never colour alone. With no session, one plain sentence says so. | |
| 24 | `test/machine-shell-page.test.ts` holds the shape. |
src/web/pages/machines.ts +37 −47
| @@ -31,7 +31,7 @@ import { | ||
| 31 | 31 | } from "../chrome.ts"; |
| 32 | 32 | import { page } from "../layout.ts"; |
| 33 | 33 | import { html } from "../html.ts"; |
| 34 | import { btn, card, icon, time } from "../ui.ts"; | |
| 34 | import { badge, btn, icon, row, time } from "../ui.ts"; | |
| 35 | 35 | import { ghosttyWasmAsset, machineTermAsset } from "../machine-term-asset.ts"; |
| 36 | 36 | import { settingsPageResponse } from "./settings.ts"; |
| 37 | 37 | |
| @@ -62,52 +62,42 @@ export async function machinesSettingsPage(ctx: Ctx): Promise<Response> { | ||
| 62 | 62 | } |
| 63 | 63 | |
| 64 | 64 | function render(ctx: Ctx, repo: Repo, list: readonly Machine[], sessions: readonly SessionRecord[]): Response { |
| 65 | const machines = card( | |
| 66 | html`<p class="muted"> | |
| 67 | A machine is where the crew's shell runs. A writer can open a shell into one; each | |
| 68 | writer has a directory and home of their own on each machine, never shared with the | |
| 69 | next. A shell outlives its tab: reload, or open it again, and you are back in it. | |
| 70 | </p> | |
| 71 | <div class="table-scroll"> | |
| 72 | <table class="table"> | |
| 73 | <thead><tr><th>Machine</th><th>Tools</th><th>Reach</th><th><span class="sr-only">Shell</span></th></tr></thead> | |
| 74 | <tbody> | |
| 75 | ${list.map( | |
| 76 | (m) => html`<tr> | |
| 77 | <td><strong>${m.name}</strong></td> | |
| 78 | <td class="muted">${m.tools.length === 0 ? "none" : m.tools.join(", ")}</td> | |
| 79 | <td class="muted">${m.reach.length === 0 ? "the forge only" : "the forge and its apps"}</td> | |
| 80 | <td>${btn("Open shell", { href: shellPath(repo, m.name), icon: "terminal", size: "sm" })}</td> | |
| 81 | </tr>`, | |
| 82 | )} | |
| 83 | </tbody> | |
| 84 | </table> | |
| 85 | </div>`, | |
| 86 | { head: html`${icon("terminal")}<h2>Machines</h2>` }, | |
| 87 | ); | |
| 88 | const recent = card( | |
| 89 | sessions.length === 0 | |
| 90 | ? html`<p class="muted">No shell has been opened here yet.</p>` | |
| 91 | : html`<div class="table-scroll"> | |
| 92 | <table class="table"> | |
| 93 | <thead><tr><th>Machine</th><th>Who</th><th>Started</th><th>Ended</th></tr></thead> | |
| 94 | <tbody> | |
| 95 | ${sessions.map( | |
| 96 | (s) => html`<tr> | |
| 97 | <td><strong>${s.machine}</strong></td> | |
| 98 | <td>${s.ownerName}</td> | |
| 99 | <td class="muted">${time(s.startedAt)}</td> | |
| 100 | <td class="muted">${s.endedAt === undefined | |
| 101 | ? "running" | |
| 102 | : html`${time(s.endedAt)} · ${ENDED[s.endReason ?? "stopped"]}`}</td> | |
| 103 | </tr>`, | |
| 104 | )} | |
| 105 | </tbody> | |
| 106 | </table> | |
| 107 | </div>`, | |
| 108 | { head: html`${icon("history")}<h2>Last sessions</h2>` }, | |
| 109 | ); | |
| 110 | const body = html`${machines}<div class="mt">${recent}</div>`; | |
| 65 | const machines = html`<section class="settings-block"> | |
| 66 | <h2>Machines</h2> | |
| 67 | <p class="muted">A machine is where the crew shell runs; each writer gets a directory and home of their own on it.</p> | |
| 68 | <ul class="list setting-list" id="machines"> | |
| 69 | ${list.map((m) => | |
| 70 | row({ | |
| 71 | lead: icon("terminal"), | |
| 72 | title: m.name, | |
| 73 | meta: html`Tools: ${m.tools.length === 0 ? "none" : m.tools.join(", ")} · Reach: ${m.reach.length === 0 ? "the forge only" : "the forge and its apps"}`, | |
| 74 | trail: btn("Open shell", { href: shellPath(repo, m.name), icon: "terminal", size: "sm" }), | |
| 75 | data: { machine: m.name }, | |
| 76 | }), | |
| 77 | )} | |
| 78 | </ul> | |
| 79 | </section>`; | |
| 80 | // The mark is not colour alone: a running shell carries the word in a badge, | |
| 81 | // an ended one says why in words and a dash. | |
| 82 | const recent = html`<section class="settings-block"> | |
| 83 | <h2>Last sessions</h2> | |
| 84 | ${sessions.length === 0 | |
| 85 | ? html`<p class="small">No shell has been opened here yet.</p>` | |
| 86 | : html`<ul class="list setting-list" id="machine-sessions"> | |
outdated
The comment says "a check or a dash" but the code draws only a dot or a dash. Fix the comment to match. |
||
| 87 | ${sessions.map((s) => | |
| 88 | row({ | |
| 89 | lead: html`<span aria-hidden="true">${s.endedAt === undefined ? "●" : "–"}</span>`, | |
| 90 | title: html`${s.machine} · ${s.ownerName}`, | |
| 91 | meta: html`Started ${time(s.startedAt)}${s.endedAt === undefined | |
| 92 | ? null | |
| 93 | : html` · ended ${time(s.endedAt)} · ${ENDED[s.endReason ?? "stopped"]}`}`, | |
| 94 | ...(s.endedAt === undefined ? { trail: badge("running", { tone: "success" }) } : {}), | |
| 95 | data: { session: s.id }, | |
| 96 | }), | |
| 97 | )} | |
| 98 | </ul>`} | |
| 99 | </section>`; | |
| 100 | const body = html`${machines}${recent}`; | |
| 111 | 101 | // The settings frame and its sections are an admin's; a writer who is not |
| 112 | 102 | // one sees this pane alone, so no link offers them a page that refuses. |
| 113 | 103 | if (canAdmin(ctx.app.db, repo, ctx.auth.user)) |
test/machine-shell-page.test.ts +14
| @@ -167,3 +167,17 @@ test("a joining connection's first size redraws the screen; a new size is set on | ||
| 167 | 167 | const one = { cols: 80, rows: RESIZE.rows.min }; |
| 168 | 168 | expect(resizePlan(one, one, true)).toEqual([{ cols: 80, rows: 2 }, one]); |
| 169 | 169 | }); |
| 170 | ||
| 171 | test("Settings › Machines is headed sections of rows, never a card (#9788)", async () => { | |
| 172 | const body = await (await get("/alice/lab/settings/machines", "alice")).text(); | |
| 173 | const main = body.slice(body.indexOf("<main"), body.indexOf("</main>")); | |
| 174 | expect(main).toContain('<section class="settings-block">'); | |
| 175 | expect(main).toContain("<h2>Machines</h2>"); | |
| 176 | expect(main).toContain("<h2>Last sessions</h2>"); | |
| 177 | const machines = main.slice(main.indexOf('id="machines"')); | |
| 178 | expect(machines.slice(0, machines.indexOf("</ul>"))).toContain('class="row-title"'); | |
| 179 | expect(main).toContain('data-machine="standard"'); | |
| 180 | expect(main).toContain('href="/alice/lab/machines/standard/shell"'); | |
| 181 | expect(main).toContain("No shell has been opened here yet."); | |
| 182 | for (const boxed of ['class="card', "card-head", "card-body", "<table"]) expect(main).not.toContain(boxed); | |
| 183 | }); |
Principle 1 and 8: four sentences explaining the page. Keep one confident sentence, e.g. "A machine is where the crew shell runs; each writer gets a directory and home of their own on it." The rest (a shell outlives its tab) belongs in docs/features/machines.md, not on the page.