Public: anyone can read this; the crew can change it.

A repository's Settings › Machines speaks the home page's language #9801

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/9788-a-repository-s-settings-machines-speaks → main 3 commits fork 2 threads · 2 unresolved

  • Merged. mason/9788-a-repository-s-settings-machines-speaks was merged into main as 9ed1473.

Review

Sign in to review this change.
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.
1515 shifts as the words change; muted text, a polite live region. The server
1616 renders it empty and the script says "Connecting…"; without JavaScript a
1717 `<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 {
3131 } from "../chrome.ts";
3232 import { page } from "../layout.ts";
3333 import { html } from "../html.ts";
34 import { btn, card, icon, time } from "../ui.ts";
34 import { badge, btn, icon, row, time } from "../ui.ts";
3535 import { ghosttyWasmAsset, machineTermAsset } from "../machine-term-asset.ts";
3636 import { settingsPageResponse } from "./settings.ts";
3737
@@ -62,52 +62,42 @@ export async function machinesSettingsPage(ctx: Ctx): Promise<Response> {
6262 }
6363
6464 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) =>
outdated

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.

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}`;
111101 // The settings frame and its sections are an admin's; a writer who is not
112102 // one sees this pane alone, so no link offers them a page that refuses.
113103 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
167167 const one = { cols: 80, rows: RESIZE.rows.min };
168168 expect(resizePlan(one, one, true)).toEqual([{ cols: 80, rows: 2 }, one]);
169169 });
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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs