Public: anyone can read this; the crew can change it.
The terminal line on the account and site pages draws §5's code and row, and .terminal-line leaves the markup #4249
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4242-the-terminal-line-on-the-account-and-sit → main 1 commit fork
5 files
changed +39
−21
src/web/pages/account.ts +4 −7
| @@ -57,6 +57,7 @@ import { | ||
| 57 | 57 | confirmAction, |
| 58 | 58 | empty, |
| 59 | 59 | selectField, |
| 60 | terminalCommand, | |
| 60 | 61 | time, |
| 61 | 62 | field as uiField, |
| 62 | 63 | } from "../ui.ts"; |
| @@ -760,13 +761,9 @@ function keysView( | ||
| 760 | 761 | const ssh = sshCommand(ctx.app); |
| 761 | 762 | return html`${ssh === undefined |
| 762 | 763 | ? html`<p class="mb muted" id="terminal-off">The terminal is off on this forge.</p>` |
| 763 | : html`<p class="mb terminal-line" id="terminal"> | |
| 764 | <code class="mono">${ssh}</code> | |
| 765 | ${when( | |
| 766 | ctx.app.config.sshHostKey, | |
| 767 | (fp) => html`<span class="muted small">host key ${fp}</span>`, | |
| 768 | )} | |
| 769 | </p>`} | |
| 764 | : html`<div class="mb" id="terminal"> | |
| 765 | ${terminalCommand(ssh, ctx.app.config.sshHostKey)} | |
| 766 | </div>`} | |
| 770 | 767 | <p class="muted small mb"> |
| 771 | 768 | Keys that sign you in over SSH. Paste a public key (<span class="mono" |
| 772 | 769 | >~/.ssh/id_ed25519.pub</span |
src/web/pages/site.ts +6 −8
| @@ -45,6 +45,7 @@ import { | ||
| 45 | 45 | listCard, |
| 46 | 46 | row, |
| 47 | 47 | selectField, |
| 48 | terminalCommand, | |
| 48 | 49 | } from "../ui.ts"; |
| 49 | 50 | import { existsSync } from "node:fs"; |
| 50 | 51 | import { join } from "node:path"; |
| @@ -193,14 +194,11 @@ export function terminalCard(ctx: Ctx): Html { | ||
| 193 | 194 | const ssh = sshCommand(ctx.app); |
| 194 | 195 | if (ssh === undefined) return html``; |
| 195 | 196 | return card( |
| 196 | html`<div class="terminal-line"> | |
| 197 | <code class="mono">${ssh}</code> | |
| 198 | <a class="small" href="/settings/keys">SSH keys</a> | |
| 199 | </div> | |
| 200 | ${when( | |
| 201 | ctx.app.config.sshHostKey, | |
| 202 | (fp) => html`<p class="muted small mt">host key ${fp}</p>`, | |
| 203 | )}`, | |
| 197 | terminalCommand( | |
| 198 | ssh, | |
| 199 | ctx.app.config.sshHostKey, | |
| 200 | html`<a class="small" href="/settings/keys">SSH keys</a>`, | |
| 201 | ), | |
| 204 | 202 | { head: html`<h2>Terminal</h2>` }, |
| 205 | 203 | ); |
| 206 | 204 | } |
src/web/ui.ts +15 −4
| @@ -560,13 +560,24 @@ export function terminalLine( | ||
| 560 | 560 | <span class="mono">FORGE_SSH_PORT</span> to turn it on. |
| 561 | 561 | </p>` |
| 562 | 562 | : html`<p class="muted small">The terminal is off.</p>`; |
| 563 | return html`<p class="terminal-line"> | |
| 564 | <code class="mono"><!--email_off-->${terminal.command}<!--/email_off--></code> | |
| 563 | return terminalCommand(terminal.command, fingerprint); | |
| 564 | } | |
| 565 | ||
| 566 | /** The command, copyable as one wrapping line, the host key under it and | |
| 567 | * anything `extra` (a link) under that — a §5 `.stack`, no class of its own. */ | |
| 568 | export function terminalCommand( | |
| 569 | command: string, | |
| 570 | fingerprint?: string | undefined, | |
| 571 | extra?: Html, | |
| 572 | ): Html { | |
| 573 | return html`<div class="stack stack-tight"> | |
| 574 | <code class="mono break"><!--email_off-->${command}<!--/email_off--></code> | |
| 565 | 575 | ${when( |
| 566 | 576 | fingerprint, |
| 567 | (fp) => html`<span class="muted small">host key ${fp}</span>`, | |
| 577 | (fp) => html`<span class="muted small break">host key ${fp}</span>`, | |
| 568 | 578 | )} |
| 569 | </p>`; | |
| 579 | ${extra ?? null} | |
| 580 | </div>`; | |
| 570 | 581 | } |
| 571 | 582 | |
| 572 | 583 | /** |
test/apps.test.ts +1 −1
| @@ -911,7 +911,7 @@ describe("the App page's terminal line survives an email-scanning edge (issue #2 | ||
| 911 | 911 | // Strip only the markers: what is left is exactly the command inside |
| 912 | 912 | // its tag, unchanged — nothing dropped or split, no stray space added. |
| 913 | 913 | const withoutMarkers = html.replace(/<!--\/?email_off-->/g, ""); |
| 914 | expect(withoutMarkers).toContain(`<code class="mono">${command}</code>`); | |
| 914 | expect(withoutMarkers).toContain(`<code class="mono break">${command}</code>`); | |
| 915 | 915 | expect(html).toContain("host key SHA256:abcdefghijklmnopqrstuvwxyz"); |
| 916 | 916 | }); |
| 917 | 917 | }); |
test/ssh-site.test.ts +13 −1
| @@ -64,7 +64,7 @@ describe("the terminal on the website", () => { | ||
| 64 | 64 | test("the landing page invites you in over ssh", async () => { |
| 65 | 65 | const page = await fetch(`${base}/`).then((r) => r.text()); |
| 66 | 66 | expect(page).toContain("ssh -p 2222 forge.example"); |
| 67 | expect(page).toContain("terminal-line"); | |
| 67 | expect(page).not.toContain("terminal-line"); | |
| 68 | 68 | expect(page).toContain("your key is your identity"); |
| 69 | 69 | expect(page).not.toContain("hero-terminal"); |
| 70 | 70 | expect(page).not.toContain('class="hero'); |
| @@ -85,6 +85,18 @@ describe("the terminal on the website", () => { | ||
| 85 | 85 | }).then((r) => r.text()); |
| 86 | 86 | expect(page).toContain("ssh -p 2222 forge.example"); |
| 87 | 87 | expect(page).toContain("host key SHA256:"); |
| 88 | expect(page).not.toContain("terminal-line"); | |
| 89 | expect(page).toContain('id="terminal"'); | |
| 90 | expect(page).toContain('<code class="mono break">'); | |
| 91 | }); | |
| 92 | ||
| 93 | test("the dashboard's Terminal card has no terminal-line, keeps the keys link", async () => { | |
| 94 | const page = await fetch(`${base}/`, { headers: { cookie } }).then((r) => | |
| 95 | r.text(), | |
| 96 | ); | |
| 97 | expect(page).not.toContain("terminal-line"); | |
| 98 | expect(page).toContain('<a class="small" href="/settings/keys">SSH keys</a>'); | |
| 99 | expect(page).toContain("host key SHA256:"); | |
| 88 | 100 | }); |
| 89 | 101 | |
| 90 | 102 | test("the crew form picks a model from a list", async () => { |
new above
offline