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

An app's domain DNS records draw §5's table, and .domain-records leaves the stylesheet #4676

Merged opened this

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

Merged

scout/forge:scout/4613-an-app-s-domain-dns-records-draw-5-s-tab → main 17 commits fork 2 threads · 2 unresolved

  • Merged. scout/4613-an-app-s-domain-dns-records-draw-5-s-tab was merged into main as 2249e8b.

Review

Sign in to review this change.
7 files changed +51 −22
docs/DESIGN.md −1
@@ -433,7 +433,6 @@ the same line (#4888).
433433 - `.break`
434434 - `.commit-body`
435435 - `.commit-head`
436 - `.domain-records`
437436 - `.filters`
438437 - `.front-admin`
439438 - `.front-crew`
docs/design/areas/backups.md added +3
@@ -0,0 +1,3 @@
1 # Backups
2
3 - `.restore-command` — the one-line restore command on /settings/backups: a `<pre>` that wraps, scrolls with the keyboard (`tabindex="0"`) and takes its height from its text, so a one-liner is not a log pane.
src/web/pages/backups.ts +1 −1
@@ -335,7 +335,7 @@ async function movingPart(
335335 now,
336336 });
337337 const restore = snapshots[0]
338 ? html`<pre class="domain-records" tabindex="0" aria-label="Restore command">bun run src/snapshot/restore.ts ${snapshots[0].name} /path/to/empty/data</pre>`
338 ? html`<pre class="restore-command" tabindex="0" aria-label="Restore command">bun run src/snapshot/restore.ts ${snapshots[0].name} /path/to/empty/data</pre>`

The class rename has no test that would fail without it. Assert restore-command is present and domain-records is absent in the backups page test.

339339 : null;
340340 return html`<div id="backups-live" data-live="backup.*" data-live-src="/settings/backups/live">${recovery}${card(
341341 html`<p>
src/web/pages/settings.ts +38 −18
@@ -923,7 +923,7 @@ function appDomainsView(
923923 html`<li>
924924 <div class="row">
925925 <span class="row-lead">${icon("globe")}</span>
926 <span class="row-main">
926 <div class="row-main">
927927 <span class="row-title"
928928 ><span class="mono">${d.host}</span> ${d.verified
929929 ? badge("live", { tone: "success" })
@@ -935,8 +935,8 @@ function appDomainsView(
935935 ? null
936936 : html` · checked ${time(d.checkedAt)}`}</span
937937 >
938 ${when(!d.verified, () => recordsToPublish(d.records))}
939 </span>
938 ${when(!d.verified, () => recordsToPublish(d.host, d.records))}
939 </div>
940940 <span class="row-trail">
941941 <form method="post" action="${at(d.host)}/check">
942942 ${btn("Check now", { size: "sm", icon: "globe" })}
@@ -1005,21 +1005,41 @@ function appDomainsView(
10051005 }
10061006
10071007 /** The records as a person copies them into a zone: type, name, value. */
outdated

Add a caption or aria-label naming the host. Wrap the scroll container as a labelled, focusable region (tabindex=0) so the overflow is reachable by keyboard at 360px.

1008 function recordsToPublish(records: DnsRecord[]): Html {
1009 return html`${records.map(
1010 (r) =>
1011 html`<span class="row-meta"
1012 >${badge(r.type, { variant: "outline" })}
1013 <span class="mono">${r.name}</span>
1014 <span aria-hidden="true">→</span>
1015 <span class="mono record-value" title="${r.value}"
1016 >${r.value}</span
1017 ></span
1018 >`,
1019 )}
1020 ${records
1021 .filter((r) => r.note !== undefined)
1022 .map((r) => html`<span class="row-meta">${r.note}</span>`)}`;
1008 function recordsToPublish(host: string, records: DnsRecord[]): Html {
1009 return html`<div
1010 class="table-scroll"
1011 role="region"
1012 tabindex="0"
1013 aria-label="DNS records for ${host}"
1014 >
1015 <table class="table" data-dense>
1016 <caption class="sr-only">
1017 DNS records for ${host}
1018 </caption>
1019 <thead>
1020 <tr>
1021 <th>Type</th>
1022 <th>Name</th>
1023 <th>Value</th>
1024 </tr>
1025 </thead>
1026 <tbody>
1027 ${records.map(
1028 (r) =>
1029 html`<tr>
1030 <td>${badge(r.type, { variant: "outline" })}</td>
1031 <td class="mono">${r.name}</td>
1032 <td class="mono record-value" title="${r.value}">
1033 ${r.value}
1034 </td>
1035 </tr>`,
1036 )}
1037 </tbody>
1038 </table>
1039 </div>
1040 ${records
1041 .filter((r) => r.note !== undefined)
1042 .map((r) => html`<span class="row-meta">${r.note}</span>`)}`;
10231043 }
10241044
10251045 const appValues = (repo: Repo, record: AppRecord | undefined): AppValues => ({
src/web/style/content.ts +1 −2
@@ -219,8 +219,7 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); }
219219 .fg-s5 { fill: var(--series-5); color: var(--series-5); }
220220 .fg-s6 { fill: var(--series-6); color: var(--series-6); }
221221 .fg-s7 { fill: var(--series-7); color: var(--series-7); }
222 /* The records a domain waits on: type, name, value — as a zone file reads. */
223 .domain-records { margin: 6px 0 2px; padding: 8px 10px; font-size: 12.5px; line-height: 1.6; white-space: pre; overflow-x: auto; scrollbar-width: thin; background: var(--muted); border-radius: var(--radius); }
222 .restore-command { margin: 8px 0 0; padding: 8px 10px; font-size: 12.5px; white-space: pre-wrap; word-break: break-all; background: var(--muted); border-radius: var(--radius); }
224223 .policy-line {
225224 display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
226225 margin: 12px 0 0; font-size: 13px; color: var(--muted-fg);
test/app-domains.test.ts +6
@@ -596,6 +596,12 @@ describe("an app gets its own domain", () => {
596596 );
597597 expect(page).toContain("ALIAS");
598598 expect(page).not.toContain("domain-records");
599 // The table is named, and its scroller is a focusable labelled region.
600 expect(page).toContain("<caption");
601 expect(page).toMatch(
602 /<div\s+class="table-scroll"\s+role="region"\s+tabindex="0"\s+aria-label="DNS records for example\.com"/,
603 );
604 expect(page).toMatch(/<table class="table" data-dense>[\s\S]*?<th>Type<\/th>/);
599605 // The records wrap instead of truncating (.row-meta only wraps inside
600606 // .setting-list), carry the type badge, and the whole value is selectable.
601607 const token = getDomain(app.db, "example.com")!.token;
test/backups-parts.test.ts +2
@@ -76,6 +76,8 @@ test("the page carries none of the old classes, and keeps its words, links and f
7676 test("the restore command is a scrollable pre in the card body, not in the foot", async () => {
7777 const page = await get();
7878 const pre = /<pre[^>]*>[^]*?<\/pre>/.exec(page)?.[0] ?? "";
79 expect(pre).toContain('class="restore-command"');
80 expect(page).not.toContain("domain-records");
7981 expect(pre).toContain('tabindex="0"');
8082 expect(pre).toContain("aria-label=");
8183 expect(pre).toContain("bun run src/snapshot/restore.ts");

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs