Public: anyone can read this; the crew can change it.
The first-login jargon test covers the site menu and command palette labels, and what it finds is reworded #10815
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/10791-the-first-login-jargon-test-covers-the-s → main 8 commits fork 10 threads · 10 unresolved
docs/design/areas/palette.md +1 −1
| @@ -1,4 +1,4 @@ | ||
| 1 | # Command palette | |
| 1 | # Jump to anywhere | |
| 2 | 2 | |
| 3 | 3 | The command palette is the keyboard-first fuzzy-find overlay (`⌘K` / `Ctrl K`, or the topbar's search button): the §5 `.palette` dialog, with its input, list and foot drawn by `layout.ts` and its results drawn by `openPalette()` in `src/web/client.ts`. Without script it degrades to `/search`. The file finder (`openFinder()`, `.dialog.palette.finder`) draws its rows and its empty note with the same classes. The rules live in `style/components.ts`. |
| 4 | 4 |
docs/design/areas/settings.md +2 −2
| @@ -94,8 +94,8 @@ accounts are `row()`s in `<ul class="list setting-list" id="accounts-list">` und | ||
| 94 | 94 | (Sign-ins, Verify, Suspend, Remove) in the row's `after` slot; the mail, ownerless-organization, sign-in reach and |
| 95 | 95 | confirm/refusal notices are plain `<section class="settings-block">` blocks with an `<h2>`. `test/accounts-rows.test.ts` holds the shape. |
| 96 | 96 | |
| 97 | Settings › Origins (`src/web/pages/origins.ts`) adds no component class. As on the home page it is headings over rows: Connected | |
| 98 | origins, Frozen mirrors, Trusted internal hosts (admin) and Connect an origin are each a `<section class="settings-block">` | |
| 97 | Settings › Connected accounts (`src/web/pages/origins.ts`) adds no component class. As on the home page it is headings over rows: Connected | |
| 98 | accounts, Frozen mirrors, Trusted internal hosts (admin) and Connect an account are each a `<section class="settings-block">` | |
| 99 | 99 | with an `<h2>`; each origin and each frozen mirror is a `row()` in a `.list`; with no origins, one plain sentence says how to |
| 100 | 100 | connect one. An origin's error, notes (re-authorize, another forge, writes paused) and acts sit in the row's `after` |
| 101 | 101 | slot; the list carries the attribute `data-origins` (not a class), which stacks that slot as a full-width block under |
src/actions/migrate.ts +1 −1
| @@ -269,7 +269,7 @@ export function migrateMessage(error: MigrateError, limit = AT_REPOSITORY_LIMIT) | ||
| 269 | 269 | case "no_such_origin": |
| 270 | 270 | return "That origin is not one you have connected, or it has no such repository."; |
| 271 | 271 | case "unreachable": |
| 272 | return "The origin could not be reached. Try again, or reconnect it under Settings → Origins."; | |
| 272 | return "The origin could not be reached. Try again, or reconnect it under Settings → Connected accounts."; | |
| 273 | 273 | case "no_such_agent": |
| 274 | 274 | return NO_SUCH_AGENT; |
| 275 | 275 | case "not_a_builder": |
src/actions/origin_words.ts +1 −1
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | /** |
| 2 | 2 | * What an origin's sync and disconnect say, in one place so the page in |
| 3 | * Settings › Origins and the terminal's Origins screen say the same thing. | |
| 3 | * Settings › Connected accounts and the terminal's Connected accounts screen say the same thing. | |
| 4 | 4 | */ |
| 5 | 5 | |
| 6 | 6 | export const NO_SUCH_ORIGIN = "No such origin."; |
src/apps/feature.ts +1
| @@ -34,6 +34,7 @@ const BUILT_IN_MENU_LABELS = [ | ||
| 34 | 34 | "your profile", "repositories", "apps", "notes", "talk", "explore", "packs", "displays", "map", "features", |
| 35 | 35 | "new app", "new repository", "new issue", "new organization", "migrate", "settings", "access tokens", "ssh keys", |
| 36 | 36 | "origins", "boards", "crew", "system memory", "help", "sign out", "log out", "sign in", "admin", "administration", |
| 37 | "access codes", "computer keys", "connected accounts", "jump to anywhere", | |
| 37 | 38 | "theme", "create", "new forge", "create new", "appearance and help", "features from apps", "keyboard shortcuts", |
| 38 | 39 | ]; |
| 39 | 40 |
src/http/mcp/tools/origins.ts +1 −1
| @@ -7,7 +7,7 @@ import { good, bad, type Tool } from "../shared.ts"; | ||
| 7 | 7 | |
| 8 | 8 | export const tool: Tool = { |
| 9 | 9 | name: "origins", |
| 10 | doc: "`{ action, id?, hosts? }`: `list` your connected origins, `repos` the repositories one holds (`id`; `mirroredAs` says which are already here), `sync` one now, or `hosts`: with no `hosts`, read the internal hosts an origin may live on (a forge admin, `fromEnv` read-only), with `hosts` (one per line) replace them as Settings → Origins does, a rejected line named in the error; the same as `GET /api/v1/user/origins`, `…/:id/repos` and `POST …/:id/sync`, for you only, an admin token as there; never a credential in an answer", | |
| 10 | doc: "`{ action, id?, hosts? }`: `list` your connected origins, `repos` the repositories one holds (`id`; `mirroredAs` says which are already here), `sync` one now, or `hosts`: with no `hosts`, read the internal hosts an origin may live on (a forge admin, `fromEnv` read-only), with `hosts` (one per line) replace them as Settings → Connected accounts does, a rejected line named in the error; the same as `GET /api/v1/user/origins`, `…/:id/repos` and `POST …/:id/sync`, for you only, an admin token as there; never a credential in an answer", | |
| 11 | 11 | description: |
| 12 | 12 | "Origins: list, repos, sync, hosts.", |
| 13 | 13 | inputSchema: { |
src/http/mcp/trusted_hosts.ts +1 −1
| @@ -5,7 +5,7 @@ import { good, bad, type Answer, type Args, type Tool } from "./shared.ts"; | ||
| 5 | 5 | |
| 6 | 6 | type Ctx = Parameters<Tool["run"]>[0]; |
| 7 | 7 | |
| 8 | /** The `origins` tool's `hosts` action: Settings → Origins' internal-host list, for a forge admin's admin-scope token. */ | |
| 8 | /** The `origins` tool's `hosts` action: Settings → Connected accounts' internal-host list, for a forge admin's admin-scope token. */ | |
| 9 | 9 | export function trustedHosts(ctx: Ctx, user: User, args: Args): Answer { |
| 10 | 10 | if (!mayEditTrustedHosts(user)) return bad("forbidden", "Only a forge admin sets this."); |
| 11 | 11 | if (!scopeAllows(ctx.auth.scope ?? "ro", "admin")) |
src/origins/credential.ts +1 −1
| @@ -255,7 +255,7 @@ export const READ_ONLY = | ||
| 255 | 255 | |
| 256 | 256 | /** What a write through a paused origin is told, everywhere. */ |
| 257 | 257 | export const WRITES_PAUSED = |
| 258 | "This forge could not verify this connection against the operator's rules, so writes through it are paused. Reads and sync go on. Check it again under Settings → Origins."; | |
| 258 | "This forge could not verify this connection against the operator's rules, so writes through it are paused. Reads and sync go on. Check it again under Settings → Connected accounts."; | |
| 259 | 259 | |
| 260 | 260 | /** |
| 261 | 261 | * Why writes through this origin are paused, or undefined when they are |
src/tui/forge.ts +5 −5
| @@ -403,7 +403,7 @@ export function forgeCommands(session: Session): (ctx: Ctx) => Command[] { | ||
| 403 | 403 | if (session.signedIn) |
| 404 | 404 | commands.push({ |
| 405 | 405 | id: "ssh-keys", |
| 406 | title: "SSH keys", | |
| 406 | title: "Computer keys", | |
| 407 | 407 | hint: "list, add and remove the keys that sign in as you", |
| 408 | 408 | run: () => ctx.push(sshKeysScreen(session, ctx)), |
| 409 | 409 | }); |
| @@ -429,11 +429,11 @@ export function forgeCommands(session: Session): (ctx: Ctx) => Command[] { | ||
| 429 | 429 | hint: "your browsers and tokens; d signs a browser out", |
| 430 | 430 | run: () => ctx.push(signInsScreen(session, ctx)), |
| 431 | 431 | }, |
| 432 | // Settings → Origins' OAuth apps and MCP `origin_apps` (#7291). | |
| 432 | // Settings → Connected accounts' OAuth apps and MCP `origin_apps` (#7291). | |
| 433 | 433 | { |
| 434 | 434 | id: "origin-apps", |
| 435 | title: "Origin apps", | |
| 436 | hint: "your OAuth apps for connecting origins; a adds, d removes", | |
| 435 | title: "Connection apps", | |
| 436 | hint: "your OAuth apps for connecting accounts; a adds, d removes", | |
| 437 | 437 | run: () => ctx.push(originAppsScreen(session, ctx)), |
| 438 | 438 | }, |
| 439 | 439 | ); |
| @@ -538,7 +538,7 @@ export function forgeCommands(session: Session): (ctx: Ctx) => Command[] { | ||
| 538 | 538 | hint: "declared flags and their live values", |
| 539 | 539 | run: () => ctx.push(flagsScreen(session, ctx)), |
| 540 | 540 | }); |
| 541 | // Settings → Origins' trusted internal hosts: the same admin gate as the page. | |
| 541 | // Settings → Connected accounts' trusted internal hosts: the same admin gate as the page. | |
| 542 | 542 | if (mayEditTrustedHosts(session.user)) |
| 543 | 543 | commands.push({ |
| 544 | 544 | id: "trusted-hosts", |
src/tui/screens/connections.ts +7 −7
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | /** |
| 2 | * Origins in the terminal: the hosts your mirrors come from, as | |
| 3 | * Settings › Origins lists them. `s` syncs the selected one now, `d` disconnects | |
| 2 | * Connected accounts in the terminal: the hosts your mirrors come from, as | |
| 3 | * Settings › Connected accounts lists them. `s` syncs the selected one now, `d` disconnects | |
| 4 | 4 | * it after a confirm — through `actions/origins.ts`, in `origin_words.ts`'s words. |
| 5 | 5 | */ |
| 6 | 6 | |
| @@ -27,7 +27,7 @@ export function connectionsScreen(f: Session, ctx: Ctx): Screen { | ||
| 27 | 27 | const counts = () => (f.user === undefined ? new Map<string, { count: number }>() : mirrorsByOrigin(f.app.db, f.user.id)); |
| 28 | 28 | |
| 29 | 29 | const screen: Screen = { |
| 30 | title: "Origins", | |
| 30 | title: "Connected accounts", | |
| 31 | 31 | keys: [binding(["j", "k"], "move cursor"), binding(["c"], "connect"), binding(["s"], "sync now"), binding(["v"], "verify again"), binding(["d"], "disconnect"), binding(["esc"], "back")], |
| 32 | 32 | render(area, s) { |
| 33 | 33 | const c = ctx.theme.c; |
| @@ -68,14 +68,14 @@ export function connectionsScreen(f: Session, ctx: Ctx): Screen { | ||
| 68 | 68 | const user = f.user; |
| 69 | 69 | ctx.push( |
| 70 | 70 | promptScreen(ctx, screen, { |
| 71 | title: "Connect an origin", | |
| 71 | title: "Connect an account", | |
| 72 | 72 | label: "Host or address (a forge, Forgejo, Gitea, GitHub)", |
| 73 | 73 | placeholder: "forge.example.com", |
| 74 | 74 | onSubmit: (hostText) => { |
| 75 | 75 | if (hostText === "") return "Type the address of the forge."; |
| 76 | 76 | return later(ctx, () => |
| 77 | 77 | promptScreen(ctx, screen, { |
| 78 | title: "Connect an origin", | |
| 78 | title: "Connect an account", | |
| 79 | 79 | label: `Access token for ${hostText}`, |
| 80 | 80 | hint: "Made in that forge's settings; kept encrypted here.", |
| 81 | 81 | mask: true, |
| @@ -83,7 +83,7 @@ export function connectionsScreen(f: Session, ctx: Ctx): Screen { | ||
| 83 | 83 | if (token.trim() === "") return "Paste the token."; |
| 84 | 84 | return later(ctx, () => |
| 85 | 85 | promptScreen(ctx, screen, { |
| 86 | title: "Connect an origin", | |
| 86 | title: "Connect an account", | |
| 87 | 87 | label: "Read-only? Type yes to never write back to it", |
| 88 | 88 | hint: "Leave empty to connect the ordinary way.", |
| 89 | 89 | onSubmit: (answer) => { |
| @@ -162,7 +162,7 @@ export function connectionsScreen(f: Session, ctx: Ctx): Screen { | ||
| 162 | 162 | /** Origins: a signed-in person, not an agent. */ |
| 163 | 163 | export const entry: PaletteEntry = { |
| 164 | 164 | id: "origins", |
| 165 | title: "Origins", | |
| 165 | title: "Connected accounts", | |
| 166 | 166 | hint: "the hosts your mirrors come from", |
| 167 | 167 | offered: (session) => session.user !== undefined && session.user.kind !== "agent", |
| 168 | 168 | run: (session, ctx) => ctx.push(connectionsScreen(session, ctx)), |
src/tui/screens/migrate.ts +1 −1
| @@ -311,7 +311,7 @@ export function migrateScreen(f: Session, ctx: Ctx): Screen { | ||
| 311 | 311 | : rows.length === 0 && unreachable.length > 0 |
| 312 | 312 | ? `${down} Try again, or press u to paste a URL.` |
| 313 | 313 | : rows.length === 0 |
| 314 | ? "No connected origin has repositories. Press u to paste a URL, or connect one on the web under Settings → Origins." | |
| 314 | ? "No connected origin has repositories. Press u to paste a URL, or connect one on the web under Settings → Connected accounts." | |
| 315 | 315 | : "Nothing matches.", |
| 316 | 316 | list!.w, |
| 317 | 317 | ), |
src/tui/screens/originapps.ts +1 −1
| @@ -93,7 +93,7 @@ export function originAppsScreen(f: Session, ctx: Ctx): Screen { | ||
| 93 | 93 | })); |
| 94 | 94 | |
| 95 | 95 | const screen: Screen = { |
| 96 | title: "Origin apps", | |
| 96 | title: "Connection apps", | |
| 97 | 97 | keys: [binding(["a"], "register one"), binding(["d"], "remove"), binding(["j", "k"], "move cursor")], |
| 98 | 98 | render(area, s) { |
| 99 | 99 | const c = ctx.theme.c; |
src/tui/screens/signins.ts +7 −7
| @@ -104,7 +104,7 @@ function hardWrap(text: string, width: number): string[] { | ||
| 104 | 104 | function secretScreen(ctx: Ctx, secret: string, market: { add: string; install: string } | null): Screen { |
| 105 | 105 | let copied = ""; |
| 106 | 106 | return { |
| 107 | title: "New access token", | |
| 107 | title: "New access code", | |
| 108 | 108 | keys: [ |
| 109 | 109 | binding(["y"], "copy it"), |
| 110 | 110 | ...(market === null ? [] : [binding(["a"], "copy the marketplace line"), binding(["i"], "copy the install line")]), |
| @@ -166,7 +166,7 @@ export function signInsScreen(f: Session, ctx: Ctx): Screen { | ||
| 166 | 166 | |
| 167 | 167 | const end = (picked: SignIn): void => { |
| 168 | 168 | if (picked.kind === "terminal") { |
| 169 | note = "This terminal cannot be ended from here: disconnect, or remove its key in Settings → SSH keys."; | |
| 169 | note = "This terminal cannot be ended from here: disconnect, or remove its key in Settings → Computer keys."; | |
| 170 | 170 | ctx.invalidate(); |
| 171 | 171 | return; |
| 172 | 172 | } |
| @@ -222,7 +222,7 @@ export function signInsScreen(f: Session, ctx: Ctx): Screen { | ||
| 222 | 222 | const confirmAdmin = (name: string) => |
| 223 | 223 | later(ctx, () => |
| 224 | 224 | confirmScreen(ctx, screen, { |
| 225 | title: "Make an admin access token", | |
| 225 | title: "Make an admin access code", | |
| 226 | 226 | body: [`${name} will act as admin: y to make it, n to cancel.`], |
| 227 | 227 | actions: [{ label: "Make it", variant: "danger" }, { label: "Cancel", variant: "ghost" }], |
| 228 | 228 | yesNo: true, |
| @@ -237,7 +237,7 @@ export function signInsScreen(f: Session, ctx: Ctx): Screen { | ||
| 237 | 237 | const askScope = (name: string, isAdmin: boolean) => |
| 238 | 238 | later(ctx, () => |
| 239 | 239 | promptScreen(ctx, screen, { |
| 240 | title: "Make an access token", | |
| 240 | title: "Make an access code", | |
| 241 | 241 | label: isAdmin ? "Scope: ro, rw or admin" : "Scope: ro or rw", |
| 242 | 242 | value: "ro", |
| 243 | 243 | onSubmit: (word) => { |
| @@ -252,7 +252,7 @@ export function signInsScreen(f: Session, ctx: Ctx): Screen { | ||
| 252 | 252 | |
| 253 | 253 | const askName = (isAdmin: boolean) => |
| 254 | 254 | promptScreen(ctx, screen, { |
| 255 | title: "Make an access token", | |
| 255 | title: "Make an access code", | |
| 256 | 256 | label: "Name", |
| 257 | 257 | value: "token", |
| 258 | 258 | onSubmit: (name) => { |
| @@ -323,7 +323,7 @@ export function signInsScreen(f: Session, ctx: Ctx): Screen { | ||
| 323 | 323 | keys: [ |
| 324 | 324 | binding(["j", "k"], "move cursor"), |
| 325 | 325 | binding(["d"], "sign a browser out or revoke a token"), |
| 326 | binding(["m"], "make an access token"), | |
| 326 | binding(["m"], "make an access code"), | |
| 327 | 327 | binding(["p"], "change your password"), |
| 328 | 328 | ], |
| 329 | 329 | render(area, s) { |
| @@ -337,7 +337,7 @@ export function signInsScreen(f: Session, ctx: Ctx): Screen { | ||
| 337 | 337 | ? "Sign in to see where you are signed in." |
| 338 | 338 | : f.user.kind === "agent" |
| 339 | 339 | ? "Where you are signed in. d signs a browser out or revokes a token." |
| 340 | : "Where you are signed in. d signs a browser out or revokes a token, m makes an access token, p changes your password."; | |
| 340 | : "Where you are signed in. d signs a browser out or revokes a token, m makes an access code, p changes your password."; | |
| 341 | 341 | s.text(head!.x, head!.y, truncate(text, head!.w), style(c.mutedFg, c.bg), head!.x + head!.w); |
| 342 | 342 | drawList(s, body!, c, state, rows(all)); |
| 343 | 343 | }, |
src/tui/screens/sshkeys.ts +1 −1
| @@ -30,7 +30,7 @@ export function sshKeysScreen(f: Session, ctx: Ctx): Screen { | ||
| 30 | 30 | })); |
| 31 | 31 | |
| 32 | 32 | const screen: Screen = { |
| 33 | title: "SSH keys", | |
| 33 | title: "Computer keys", | |
| 34 | 34 | keys: [binding(["j", "k"], "move cursor"), binding(["a"], "add"), binding(["e"], "rename"), binding(["d"], "remove")], |
| 35 | 35 | render(area, s) { |
| 36 | 36 | const c = ctx.theme.c; |
src/web/client.ts +1 −1
| @@ -1618,7 +1618,7 @@ const shellCore = /* js */ ` | ||
| 1618 | 1618 | // ---- help overlay, generated from the shortcuts actually on the page ---- |
| 1619 | 1619 | var STATIC_KEYS = [ |
| 1620 | 1620 | ['j / k', 'Move between rows'], ['o or Enter', 'Open the focused row'], |
| 1621 | ['/', 'Focus search'], ['Cmd/Ctrl K', 'Command palette'], [':', 'Go to a page by name'], ['?', 'This help'], | |
| 1621 | ['/', 'Focus search'], ['Cmd/Ctrl K', 'Jump to anywhere'], [':', 'Go to a page by name'], ['?', 'This help'], | |
| 1622 | 1622 | ['Esc', 'Leave a box, close a menu or dialog'], |
| 1623 | 1623 | ]; |
| 1624 | 1624 | // The rows a page earns by what it carries: a box to type in, a form |
src/web/layout.ts +11 −42
| @@ -347,9 +347,9 @@ function userMenu( | ||
| 347 | 347 | ...forgeDoor(ctx), |
| 348 | 348 | { separator: true, label: "" }, |
| 349 | 349 | { label: "Settings", href: "/settings", icon: "settings" }, |
outdated
Principle 8 / navigation: 'Access codes' opens a page still headed 'Access tokens'. Reword the destination page and settings sidebar too, or the label does not lead where it says. |
||
| 350 | { label: "Access tokens", href: "/settings/tokens", icon: "key" }, | |
| 351 | { label: "SSH keys", href: "/settings/keys", icon: "lock" }, | |
| 352 | { label: "Origins", href: "/settings/origins", icon: "globe" }, | |
| 350 | { label: "Access codes", href: "/settings/tokens", icon: "key" }, | |
outdated
'Computer keys' opens a page headed 'SSH keys' (settings sidebar, site.ts:204, TUI screen title in src/tui/forge.ts:406). Reword those so both doors agree. |
||
| 351 | { label: "Computer keys", href: "/settings/keys", icon: "lock" }, | |
outdated
'Connected accounts' opens 'Origins'. Reword the page heading and sidebar to match. |
||
| 352 | { label: "Connected accounts", href: "/settings/origins", icon: "globe" }, | |
| 353 | 353 | ...(!user.isAdmin && administersAny(ctx.app.db, user) |
| 354 | 354 | ? [{ label: "Boards", href: "/boards", icon: "activity" as const }] |
| 355 | 355 | : []), |
| @@ -410,9 +410,9 @@ const STATIC_PALETTE: PaletteItem[] = [ | ||
| 410 | 410 | { t: "Migrate a repository", h: "/migrate", c: "Create" }, |
| 411 | 411 | { t: "New organization", h: "/organizations/new", c: "Create" }, |
| 412 | 412 | { t: "Account settings", h: "/settings", c: "Settings" }, |
| 413 | { t: "Access tokens", h: "/settings/tokens", c: "Settings" }, | |
| 414 | { t: "SSH keys", h: "/settings/keys", c: "Settings" }, | |
| 415 | { t: "Origins", h: "/settings/origins", c: "Settings" }, | |
| 413 | { t: "Access codes", h: "/settings/tokens", c: "Settings" }, | |
| 414 | { t: "Computer keys", h: "/settings/keys", c: "Settings" }, | |
| 415 | { t: "Connected accounts", h: "/settings/origins", c: "Settings" }, | |
| 416 | 416 | { t: "Your shells", h: "/settings/shells", c: "Settings" }, |
| 417 | 417 | ]; |
| 418 | 418 | |
| @@ -576,33 +576,10 @@ function paletteIndex(ctx: Ctx, extra: PaletteItem[]): Html { | ||
| 576 | 576 | * one that should close the dialog. |
| 577 | 577 | */ |
| 578 | 578 | function overlays(): Html { |
| 579 | return html`<dialog | |
| 580 | class="dialog palette" | |
| 581 | id="palette" | |
| 582 | role="dialog" | |
| 583 | aria-modal="true" | |
| 584 | aria-label="Command palette" | |
| 585 | > | |
| 586 | <input | |
| 587 | class="input palette-input" | |
| 588 | type="text" | |
| 589 | role="combobox" | |
| 590 | aria-expanded="true" | |
| 591 | aria-haspopup="listbox" | |
| 592 | aria-controls="palette-list" | |
| 593 | aria-autocomplete="list" | |
| 594 | placeholder="Jump to a repository, page or action…" | |
| 595 | aria-label="Command palette" | |
| 596 | autocomplete="off" | |
| 597 | autocapitalize="off" | |
| 598 | spellcheck="false" | |
| 599 | /> | |
| 600 | <div | |
| 601 | class="palette-list" | |
| 602 | id="palette-list" | |
| 603 | role="listbox" | |
| 604 | aria-label="Results" | |
| 605 | ></div> | |
| 579 | // One line per element: this markup ships on every page, so its indentation is bytes too. | |
| 580 | return html`<dialog class="dialog palette" id="palette" role="dialog" aria-modal="true" aria-label="Jump to anywhere"> | |
| 581 | <input class="input palette-input" type="text" role="combobox" aria-expanded="true" aria-haspopup="listbox" aria-controls="palette-list" aria-autocomplete="list" placeholder="Jump to a repository, page or action…" aria-label="Jump to anywhere" autocomplete="off" autocapitalize="off" spellcheck="false" /> | |
| 582 | <div class="palette-list" id="palette-list" role="listbox" aria-label="Results"></div> | |
| 606 | 583 | <div class="palette-foot"> |
| 607 | 584 | ${kbd(["↑", "↓"])} navigate ${kbd(["↵"])} open ${kbd(["⌘", "↵"])} its app ${kbd(["esc"])} close |
| 608 | 585 | </div> |
| @@ -736,15 +713,7 @@ function footer(ctx: Ctx, own?: Html): Html { | ||
| 736 | 713 | ${ctx.auth.user !== undefined && ctx.auth.user.kind !== "org" |
| 737 | 714 | ? html`<a class="btn" data-variant="link" data-size="sm" href="/#composer">Tell the crew</a>` |
| 738 | 715 | : ""} |
| 739 | <button | |
| 740 | class="btn js-only" | |
| 741 | data-variant="link" | |
| 742 | data-size="sm" | |
| 743 | type="button" | |
| 744 | data-palette | |
| 745 | > | |
| 746 | Command palette | |
| 747 | </button> | |
| 716 | <button class="btn js-only" data-variant="link" data-size="sm" type="button" data-palette>Jump to anywhere</button> | |
| 748 | 717 | </footer>`; |
| 749 | 718 | } |
| 750 | 719 |
src/web/pages/account.ts +19 −19
| @@ -429,7 +429,7 @@ function profileView(ctx: Ctx, values: ProfileForm): Html { | ||
| 429 | 429 | ], |
| 430 | 430 | })} |
| 431 | 431 | <div class="form-actions"> |
| 432 | ${btn("Save profile", { variant: "primary" })} | |
| 432 | ${btn("Save", { variant: "primary" })} | |
| 433 | 433 | </div> |
| 434 | 434 | ${refusal(values.errors?.form)} |
| 435 | 435 | </form>`, |
| @@ -751,7 +751,7 @@ function tokensView(ctx: Ctx): Html { | ||
| 751 | 751 | listSessions(ctx.app.db, ctx.auth.user!.id, cookieValue(ctx.request, "forge_session")), |
| 752 | 752 | ); |
| 753 | 753 | const list = tokens.length === 0 |
| 754 | ? html`<p class="muted">No access tokens yet. Mint one when a script, a CI job, or another machine needs to act as you.</p>` | |
| 754 | ? html`<p class="muted">No access codes yet. Mint one when a script, a CI job, or another machine needs to act as you.</p>` | |
| 755 | 755 | : html`<ul class="list setting-list"> |
| 756 | 756 | ${tokens.map((t) => |
| 757 | 757 | row({ |
| @@ -766,18 +766,18 @@ function tokensView(ctx: Ctx): Html { | ||
| 766 | 766 | </ul>`; |
| 767 | 767 | return html`${help([ |
| 768 | 768 | [ |
| 769 | "Access tokens", | |
| 770 | "Use a token as the password when pushing over HTTPS, or as a Bearer token for the API. Your account password works for git too — tokens are for scoped or automated access.", | |
| 769 | "Access codes", | |
| 770 | "Use an access code as the password when pushing over HTTPS, or to call the API (sent as a Bearer header). Your account password works for git too — access codes are for scoped or automated access.", | |
outdated
Principle 8: this help text still says 'token' under an 'Access codes' heading. Use one word. |
||
| 771 | 771 | ], |
| 772 | ["Access token name", "Name it after where it lives — laptop, CI, deploy box."], | |
| 773 | ["Scope", "A token can never mint another token, whatever its scope."], | |
| 772 | ["Access code name", "Name it after where it lives — laptop, CI, deploy box."], | |
| 773 | ["Scope", "An access code can never make another access code, whatever its scope."], | |
| 774 | 774 | ])}${connectCard(ctx, readFlash(ctx.request)?.code)} |
| 775 | ${block("Access tokens", list)} | |
| 775 | ${block("Access codes", list)} | |
| 776 | 776 | ${block( |
| 777 | "New access token", | |
| 777 | "New access code", | |
| 778 | 778 | html`<form method="post" action="/settings/tokens"> |
| 779 | 779 | ${uiField({ |
| 780 | label: "Access token name", | |
| 780 | label: "Access code name", | |
| 781 | 781 | name: "name", |
| 782 | 782 | required: true, |
| 783 | 783 | })} |
| @@ -797,14 +797,14 @@ function tokensView(ctx: Ctx): Html { | ||
| 797 | 797 | <label class="check"> |
| 798 | 798 | <input type="checkbox" name="aiAdmin" required /> |
| 799 | 799 | <span |
| 800 | >I mean to give this token admin: you are the forge's admin, | |
| 800 | >I mean to give this access code admin: you are the forge's admin, | |
| 801 | 801 | so whatever holds it — an AI included — acts as admin.</span |
| 802 | 802 | > |
| 803 | 803 | </label> |
| 804 | 804 | </div>` |
| 805 | 805 | : null} |
| 806 | 806 | <div class="form-actions"> |
| 807 | ${btn("Generate access token", { variant: "primary" })} | |
| 807 | ${btn("Generate access code", { variant: "primary" })} | |
| 808 | 808 | </div> |
| 809 | 809 | </form>`, |
| 810 | 810 | )} |
| @@ -816,7 +816,7 @@ export function tokensPage(ctx: Ctx): Response { | ||
| 816 | 816 | if (!ctx.auth.user) return signInFirst(ctx); |
| 817 | 817 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); |
| 818 | 818 | const body = tokensView(ctx); |
| 819 | return timePhase(ctx.request, "render", () => accountShell(ctx, "tokens", "Access tokens", body)); | |
| 819 | return timePhase(ctx.request, "render", () => accountShell(ctx, "tokens", "Access codes", body)); | |
| 820 | 820 | } |
| 821 | 821 | |
| 822 | 822 | // POST /settings/tokens |
| @@ -826,7 +826,7 @@ export async function tokenCreate(ctx: Ctx): Promise<Response> { | ||
| 826 | 826 | // the platform edge's sign-in, or the password — never from another token |
| 827 | 827 | // (no privilege bootstrapping). readForm below refuses a cross-site post. |
| 828 | 828 | if (!isAccountItself(ctx.auth)) { |
| 829 | return forbiddenPage(ctx, "A token is made from your own signed-in account, never by another token."); | |
| 829 | return forbiddenPage(ctx, "An access code is made from your own signed-in account, never by another access code."); | |
| 830 | 830 | } |
| 831 | 831 | const form = await readForm(ctx, "admin"); |
| 832 | 832 | if (form instanceof Response) return form; |
| @@ -835,7 +835,7 @@ export async function tokenCreate(ctx: Ctx): Promise<Response> { | ||
| 835 | 835 | if (ctx.auth.user.isAdmin && field(form, "aiAdmin") !== "on") |
| 836 | 836 | return flashTo(ctx, "/settings/tokens", { |
| 837 | 837 | tone: "danger", |
| 838 | message: "You are the forge's admin: tick that you mean to give this token admin, or make it from your everyday account.", | |
| 838 | message: "You are the forge's admin: tick that you mean to give this access code admin, or make it from your everyday account.", | |
| 839 | 839 | }); |
| 840 | 840 | const scope = field(form, "scope"); |
| 841 | 841 | const chosen: TokenScope = scope === "ro" || scope === "admin" ? scope : "rw"; |
| @@ -907,9 +907,9 @@ function keysView( | ||
| 907 | 907 | >) — ed25519, ecdsa, or rsa (2048 bits or more). |
| 908 | 908 | </p> |
| 909 | 909 | ${block( |
| 910 | "SSH keys", | |
| 910 | "Computer keys", | |
| 911 | 911 | keys.length === 0 |
| 912 | ? html`<p class="muted">No SSH keys yet. Add one and git over SSH stops asking for a password.</p>` | |
| 912 | ? html`<p class="muted">No computer keys yet. Add one and git over SSH stops asking for a password.</p>` | |
| 913 | 913 | : html`<ul class="list setting-list"> |
| 914 | 914 | ${keys.map((k) => |
| 915 | 915 | row({ |
| @@ -940,7 +940,7 @@ function keysView( | ||
| 940 | 940 | placeholder: "ssh-ed25519 AAAA…", |
| 941 | 941 | })} |
| 942 | 942 | <div class="form-actions"> |
| 943 | ${btn("Add SSH key", { variant: "primary" })} | |
| 943 | ${btn("Add computer key", { variant: "primary" })} | |
| 944 | 944 | </div> |
| 945 | 945 | ${refusal(values.error)} |
| 946 | 946 | </form>`, |
| @@ -952,7 +952,7 @@ export function keysPage(ctx: Ctx): Response { | ||
| 952 | 952 | if (!ctx.auth.user) return signInFirst(ctx); |
| 953 | 953 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); |
| 954 | 954 | const body = keysView(ctx, { name: "", key: "" }); |
| 955 | return timePhase(ctx.request, "render", () => accountShell(ctx, "keys", "SSH keys", body)); | |
| 955 | return timePhase(ctx.request, "render", () => accountShell(ctx, "keys", "Computer keys", body)); | |
| 956 | 956 | } |
| 957 | 957 | |
| 958 | 958 | // POST /settings/keys |
| @@ -971,7 +971,7 @@ export async function keyCreate(ctx: Ctx): Promise<Response> { | ||
| 971 | 971 | return accountShell( |
| 972 | 972 | ctx, |
| 973 | 973 | "keys", |
| 974 | "SSH keys", | |
| 974 | "Computer keys", | |
| 975 | 975 | keysView(ctx, { |
| 976 | 976 | name, |
| 977 | 977 | key, |
src/web/pages/branches.ts +1 −1
| @@ -342,7 +342,7 @@ export const CREATE_ERRORS: Record<string, string> = { | ||
| 342 | 342 | origin_protected: |
| 343 | 343 | "The operator protects this repository at its origin, so no branch is pushed to it from here. Propose the change from your fork.", |
| 344 | 344 | origin_paused: |
| 345 | "This connection is not verified yet, so writes through it are paused. Nothing was created. Check it again under Settings → Origins.", | |
| 345 | "This connection is not verified yet, so writes through it are paused. Nothing was created. Check it again under Settings → Connected accounts.", | |
| 346 | 346 | forbidden: "You need write access to create a branch.", |
| 347 | 347 | unauthorized: "You need write access to create a branch.", |
| 348 | 348 | not_found: "You need write access to create a branch.", |
src/web/pages/browse.ts +1 −1
| @@ -953,7 +953,7 @@ export function forkFailure(error: string): string { | ||
| 953 | 953 | if (error === "origin_read_only") |
| 954 | 954 | return "This account was connected read-only, so nothing is written through it."; |
| 955 | 955 | if (error === "origin_paused") |
| 956 | return "This connection is not verified yet, so writes through it are paused. Check it again under Settings → Origins."; | |
| 956 | return "This connection is not verified yet, so writes through it are paused. Check it again under Settings → Connected accounts."; | |
| 957 | 957 | if (error === "mirror_failed") |
| 958 | 958 | return "The fork was made, but a local copy of it could not be."; |
| 959 | 959 | if (error === "unauthorized") return "Sign in first."; |
src/web/pages/edit.ts +1 −1
| @@ -131,7 +131,7 @@ export function editErrorText(error: unknown): string { | ||
| 131 | 131 | if (error === "origin_read_only") |
| 132 | 132 | return "This account was connected read-only, so nothing is written through it."; |
| 133 | 133 | if (error === "origin_paused") |
| 134 | return "This connection is not verified yet, so writes through it are paused. Check it again under Settings → Origins."; | |
| 134 | return "This connection is not verified yet, so writes through it are paused. Check it again under Settings → Connected accounts."; | |
| 135 | 135 | return "That edit could not be saved."; |
| 136 | 136 | } |
| 137 | 137 | const e = error as { kind?: string; branch?: string; message?: string }; |
src/web/pages/frozen.ts +2 −2
| @@ -70,7 +70,7 @@ export function frozenBlock(ctx: Ctx, repo: Repo): Html | null { | ||
| 70 | 70 | // Only the viewer's own working origins on the same site — scheme, host |
| 71 | 71 | // and port: reconnecting lends the mirror a credential, nobody can lend |
| 72 | 72 | // one that is not theirs, and one that needs re-authorizing has nothing |
| 73 | // to lend until it is (which, done under Settings → Origins, reconnects | |
| 73 | // to lend until it is (which, done under Settings → Connected accounts, reconnects | |
| 74 | 74 | // this mirror by itself). |
| 75 | 75 | const origins = |
| 76 | 76 | admin && user !== undefined |
| @@ -87,7 +87,7 @@ export function frozenBlock(ctx: Ctx, repo: Repo): Html | null { | ||
| 87 | 87 | : origins.length === 0 |
| 88 | 88 | ? html`Connect an |
| 89 | 89 | account${host === "" ? null : html` at ${hostLabel(d.htmlUrl)}`} under |
| 90 | <a href="${connect}">Settings → Origins</a>: connecting one | |
| 90 | <a href="${connect}">Settings → Connected accounts</a>: connecting one | |
| 91 | 91 | reconnects this mirror by itself.` |
| 92 | 92 | : html`<form |
| 93 | 93 | method="post" |
src/web/pages/migrate.ts +2 −2
| @@ -291,7 +291,7 @@ function accounts(ctx: Ctx, origins: readonly OriginChoices[]): Html { | ||
| 291 | 291 | : html`<p class="muted small"> |
| 292 | 292 | ${icon("github")} Connect GitHub needs this forge's GitHub |
| 293 | 293 | application, set up once under |
| 294 | <a href="/settings/origins">Settings → Origins</a>; a token pasted | |
| 294 | <a href="/settings/origins">Settings → Connected accounts</a>; a token pasted | |
| 295 | 295 | there works until then. |
| 296 | 296 | </p>`; |
| 297 | 297 | return html`<section class="mb" id="migrate-accounts-section"> |
| @@ -335,7 +335,7 @@ function picker( | ||
| 335 | 335 | if (listed.length === 0) |
| 336 | 336 | return html`<p class="muted small mb"> |
| 337 | 337 | ${icon("globe")} Connect an account above — or another forge under |
| 338 | <a href="/settings/origins">Settings → Origins</a> — to pick from your | |
| 338 | <a href="/settings/origins">Settings → Connected accounts</a> — to pick from your | |
| 339 | 339 | repositories there, private ones included. |
| 340 | 340 | </p>`; |
| 341 | 341 | return html`<div class="field" id="migrate-picker"> |
src/web/pages/origin-archived.ts +1 −1
| @@ -117,7 +117,7 @@ async function render( | ||
| 117 | 117 | const head = html`<div class="bar mb"> |
| 118 | 118 | <h2>Archived on ${host}</h2> |
| 119 | 119 | <span class="spacer"></span> |
| 120 | ${btn("Back to origins", { | |
| 120 | ${btn("Back to connected accounts", { | |
| 121 | 121 | href: "/settings/origins", |
| 122 | 122 | size: "sm", |
| 123 | 123 | variant: "ghost", |
src/web/pages/origin-flow.ts +2 −2
| @@ -85,10 +85,10 @@ export function originFlowPage(ctx: Ctx): Response { | ||
| 85 | 85 | ${authorize === undefined |
| 86 | 86 | ? html`<p> |
| 87 | 87 | This connect cannot be continued from here — the authorization |
| 88 | link was lost. Start it again from Origins. | |
| 88 | link was lost. Start it again from Connected accounts. | |
| 89 | 89 | </p> |
| 90 | 90 | <div class="bar mt"> |
| 91 | ${btn("Back to origins", { | |
| 91 | ${btn("Back to connected accounts", { | |
| 92 | 92 | href: "/settings/origins", |
| 93 | 93 | variant: "primary", |
| 94 | 94 | })} |
src/web/pages/origin-repos.ts +1 −1
| @@ -293,7 +293,7 @@ function groupSection( | ||
| 293 | 293 | icon: "alert", |
| 294 | 294 | title: read.error === "unreachable" ? "The origin could not be reached" : "This group could not be read", |
| 295 | 295 | note: "Nothing was changed. Try again, or reconnect the origin if it needs re-authorizing.", |
| 296 | action: btn("Back to origins", { href: "/settings/origins", variant: "primary" }), | |
| 296 | action: btn("Back to connected accounts", { href: "/settings/origins", variant: "primary" }), | |
| 297 | 297 | }) |
| 298 | 298 | : found.items.length === 0 |
| 299 | 299 | ? empty({ |
src/web/pages/origins.ts +12 −12
| @@ -144,14 +144,14 @@ const SECTION_ROWS: readonly (readonly [ | ||
| 144 | 144 | ["Account", "Profile", "/settings", "user", (c) => c === "profile", false], |
| 145 | 145 | ["Account", "Password", "/settings/password", "lock", (c) => c === "password", false], |
| 146 | 146 | ["Account", "Passkeys", "/settings/passkeys", "key", (c) => c === "passkeys", false], |
| 147 | ["Account", "SSH keys", "/settings/keys", "lock", (c) => c === "keys", false], | |
| 148 | ["Account", "Access tokens", "/settings/tokens", "key", (c) => c === "tokens", false], | |
| 147 | ["Account", "Computer keys", "/settings/keys", "lock", (c) => c === "keys", false], | |
| 148 | ["Account", "Access codes", "/settings/tokens", "key", (c) => c === "tokens", false], | |
| 149 | 149 | ["Account", "Confirms", "/settings/confirms", "check", (c) => c === "confirms", false], |
| 150 | 150 | ["Crew and AI", "Model", "/settings/model", "key", (c) => c === "model", false], |
| 151 | 151 | ["Crew and AI", "Crew", "/settings/crew", "bot", (c) => c === "crew", true], |
| 152 | 152 | ["Crew and AI", "Subscriptions", "/settings/subscriptions", "key", (c) => c === "subscriptions", true], |
| 153 | 153 | ["Crew and AI", "Usage", "/settings/spend", "activity", (c) => USAGE.has(c), true], |
| 154 | ["Connections", "Origins", "/settings/origins", "globe", (c) => c === "origins", false], | |
| 154 | ["Connections", "Connected accounts", "/settings/origins", "globe", (c) => c === "origins", false], | |
| 155 | 155 | ["Connections", "Cloud accounts", "/settings/cloud", "key", (c) => c === "cloud", true], |
| 156 | 156 | ["Connections", "Secrets", "/settings/secrets", "lock", (c) => c === "secrets", true], |
| 157 | 157 | ["Connections", "Devices", "/settings/devices", "globe", (c) => c === "devices", true], |
| @@ -689,7 +689,7 @@ function forgeSection(ctx: Ctx, values: ConnectValues): Html { | ||
| 689 | 689 | hint: "Read is enough to bring an app; private repositories come with it.", |
| 690 | 690 | })} |
| 691 | 691 | ${readOnlyField()} |
| 692 | <p class="muted small">Make the token there: ${tokens} (Settings → Access tokens).</p> | |
| 692 | <p class="muted small">Make the token there: ${tokens} (Settings → Access codes).</p> | |
| 693 | 693 | <div class="form-actions"> |
| 694 | 694 | ${btn("Connect that forge", { variant: "primary", icon: "globe" })} |
| 695 | 695 | </div> |
| @@ -794,14 +794,14 @@ function originsView( | ||
| 794 | 794 | </p> |
| 795 | 795 | ${origins.length === 0 |
| 796 | 796 | ? html`<section class="settings-block"> |
| 797 | <h2>Origins</h2> | |
| 797 | <h2>Connected accounts</h2> | |
| 798 | 798 | <p class="small"> |
| 799 | No origins yet: connect another forge, GitHub, a Forgejo | |
| 799 | No connected accounts yet: connect another forge, GitHub, a Forgejo | |
| 800 | 800 | instance or a Gitea instance below. |
| 801 | 801 | </p> |
| 802 | 802 | </section>` |
| 803 | 803 | : html`<section class="settings-block"> |
| 804 | <h2>Origins</h2> | |
| 804 | <h2>Connected accounts</h2> | |
| 805 | 805 | <ul class="list setting-list" data-origins> |
| 806 | 806 | ${origins.map((o) => |
| 807 | 807 | originCard( |
| @@ -820,7 +820,7 @@ function originsView( | ||
| 820 | 820 | ) |
| 821 | 821 | : null} |
| 822 | 822 | <section class="settings-block" id="connect"> |
| 823 | <h2>Connect an origin</h2> | |
| 823 | <h2>Connect an account</h2> | |
| 824 | 824 | <ul class="list"> |
| 825 | 825 | ${connectPath( |
| 826 | 826 | "connect-forge", |
| @@ -925,9 +925,9 @@ export function shell( | ||
| 925 | 925 | return page( |
| 926 | 926 | { |
| 927 | 927 | ctx, |
| 928 | title: "Origins", | |
| 928 | title: "Connected accounts", | |
| 929 | 929 | ...(scriptJs === undefined ? {} : { scriptJs }), |
| 930 | body: html`<div class="page-head"><h1>Origins</h1></div> | |
| 930 | body: html`<div class="page-head"><h1>Connected accounts</h1></div> | |
| 931 | 931 | ${settingsLayout( |
| 932 | 932 | accountSections("origins", { |
| 933 | 933 | admin: ctx.auth.user?.isAdmin === true, |
| @@ -1218,8 +1218,8 @@ export function notFoundNote(): Html { | ||
| 1218 | 1218 | return empty({ |
| 1219 | 1219 | icon: "search", |
| 1220 | 1220 | title: "No such connect", |
| 1221 | note: "It finished, expired, or was never yours. Start again from Origins.", | |
| 1222 | action: btn("Back to origins", { | |
| 1221 | note: "It finished, expired, or was never yours. Start again from Connected accounts.", | |
| 1222 | action: btn("Back to connected accounts", { | |
outdated
Principle 8: the page is now 'Connected accounts' but this button still says 'Back to origins'. The same page keeps 'Connect an origin' and 'Trusted internal hosts'. Reword them so one thing has one name. |
||
| 1223 | 1223 | href: "/settings/origins", |
| 1224 | 1224 | variant: "primary", |
| 1225 | 1225 | }), |
src/web/pages/site.ts +1 −1
| @@ -201,7 +201,7 @@ export function terminalCard(ctx: Ctx): Html { | ||
| 201 | 201 | ${terminalCommand( |
| 202 | 202 | ssh, |
| 203 | 203 | ctx.app.config.sshHostKey, |
| 204 | html`<a class="small" href="/settings/keys">SSH keys</a>`, | |
| 204 | html`<a class="small" href="/settings/keys">Computer keys</a>`, | |
| 205 | 205 | )} |
| 206 | 206 | </section>`; |
| 207 | 207 | } |
src/web/pages/thread.ts +1 −1
| @@ -95,7 +95,7 @@ export function originError(error: string): string | undefined { | ||
| 95 | 95 | origin_protected: |
| 96 | 96 | "The operator protects this repository at its origin, so nothing is pushed to it from here. Propose the change from your fork.", |
| 97 | 97 | origin_paused: |
| 98 | "This connection is not verified yet, so writes through it are paused. Nothing was changed. Check it again under Settings → Origins.", | |
| 98 | "This connection is not verified yet, so writes through it are paused. Nothing was changed. Check it again under Settings → Connected accounts.", | |
| 99 | 99 | origin_unreachable: "The origin could not be reached. Nothing was changed.", |
| 100 | 100 | }; |
| 101 | 101 | return words[error]; |
src/web/style/components.ts +1 −1
| @@ -507,7 +507,7 @@ a.row:hover .row-title { color: var(--accent); } | ||
| 507 | 507 | /* A settings row's acts (Accounts): always visible, wrapping, a full line under 640px. */ |
| 508 | 508 | .setting-list > li.has-after > .row-after { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; } |
| 509 | 509 | @media (max-width: 640px) { .setting-list > li.has-after > .row-after { flex-basis: 100%; justify-content: flex-start; padding: 0 14px 10px; } } |
| 510 | /* An origin's error, notes and acts (Settings › Origins): a block of their own under the row, at every width. */ | |
| 510 | /* An origin's error, notes and acts (Settings › Connected accounts): a block of their own under the row, at every width. */ | |
| 511 | 511 | .setting-list[data-origins] > li.has-after > .row-after, .setting-list[data-bets] > li.has-after > .row-after { flex-basis: 100%; display: block; min-width: 0; padding: 0 14px 12px; overflow-wrap: anywhere; } |
| 512 | 512 | .list > li.has-after > .row-children { flex-basis: 100%; } |
| 513 | 513 | /* A disk's alert reasons (Settings › Machines, docs/design/areas/machines.md): a line under its folded row, inset to the title. */ |
test/account-help.test.ts +1 −1
| @@ -59,7 +59,7 @@ test("the password and token explanations live inside the help toggle, before th | ||
| 59 | 59 | expect(pw.replace(helpOf(pw), "")).not.toContain("A passphrase beats a puzzle"); |
| 60 | 60 | expect(pw.indexOf("data-page-help")).toBeLessThan(pw.indexOf("Change password</h2>")); |
| 61 | 61 | const tk = await get("/settings/tokens"); |
| 62 | expect(helpOf(tk)).toContain("can never mint another token"); | |
| 62 | expect(helpOf(tk)).toContain("can never make another access code"); | |
| 63 | 63 | expect(helpOf(tk)).toContain("laptop, CI, deploy box"); |
| 64 | 64 | expect(tk.replace(helpOf(tk), "")).not.toContain("laptop, CI, deploy box"); |
| 65 | 65 | }); |
test/account-rows.test.ts +7 −7
| @@ -63,18 +63,18 @@ test("no account page is a card or holds a table", async () => { | ||
| 63 | 63 | test("tokens are rows under a Tokens heading, each with Revoke", async () => { |
| 64 | 64 | const m = await main("/settings/tokens"); |
| 65 | 65 | expect(m).toContain('<section class="settings-block">'); |
| 66 | const list = m.slice(m.indexOf("<h2>Access tokens</h2>")); | |
| 67 | expect(list).toMatch(/^<h2>Access tokens<\/h2>\s*<ul class="list setting-list">/); | |
| 66 | const list = m.slice(m.indexOf("<h2>Access codes</h2>")); | |
| 67 | expect(list).toMatch(/^<h2>Access codes<\/h2>\s*<ul class="list setting-list">/); | |
| 68 | 68 | const item = list.slice(0, list.indexOf("</ul>")); |
| 69 | 69 | expect(item).toContain("<strong>laptop</strong>"); |
| 70 | 70 | expect(item).toMatch(/settings%2Ftokens%2F[^"]+%2Fdelete/); |
| 71 | 71 | expect(m).toContain('action="/settings/tokens"'); |
| 72 | 72 | }); |
| 73 | 73 | |
| 74 | test("keys are rows under an SSH keys heading, each with its Remove link", async () => { | |
| 74 | test("keys are rows under a Computer keys heading, each with its Remove link", async () => { | |
outdated
Grammar slip from the mechanical rename: 'an Computer keys heading' should read 'a Computer keys heading'. |
||
| 75 | 75 | const m = await main("/settings/keys"); |
| 76 | const list = m.slice(m.indexOf("<h2>SSH keys</h2>")); | |
| 77 | expect(list).toMatch(/^<h2>SSH keys<\/h2>\s*<ul class="list setting-list">/); | |
| 76 | const list = m.slice(m.indexOf("<h2>Computer keys</h2>")); | |
| 77 | expect(list).toMatch(/^<h2>Computer keys<\/h2>\s*<ul class="list setting-list">/); | |
| 78 | 78 | const item = list.slice(0, list.indexOf("</ul>")); |
| 79 | 79 | expect(item).toContain("<strong>work</strong>"); |
| 80 | 80 | expect(item).toMatch(/settings%2Fkeys%2F[^"]+%2Fdelete/); |
| @@ -90,10 +90,10 @@ test("empty lists are one muted sentence, not a panel", async () => { | ||
| 90 | 90 | const c = (other.headers.get("set-cookie") ?? "").split(";")[0]!; |
| 91 | 91 | const get = async (path: string) => (await (await fetch(`${base}${path}`, { headers: { cookie: c } })).text()); |
| 92 | 92 | const tokens = await get("/settings/tokens"); |
| 93 | expect(tokens).toContain('<p class="muted">No access tokens yet.'); | |
| 93 | expect(tokens).toContain('<p class="muted">No access codes yet.'); | |
| 94 | 94 | expect(tokens).not.toContain("empty-title"); |
| 95 | 95 | const keys = await get("/settings/keys"); |
| 96 | expect(keys).toContain('<p class="muted">No SSH keys yet.'); | |
| 96 | expect(keys).toContain('<p class="muted">No computer keys yet.'); | |
| 97 | 97 | expect(keys).not.toContain("empty-title"); |
| 98 | 98 | const card = String(sessionsCard([], "/settings")); |
| 99 | 99 | expect(card).toContain('<p class="muted">Not signed in on any browser.</p>'); |
test/everyday.test.ts +2 −2
| @@ -223,7 +223,7 @@ describe("the everyday account", () => { | ||
| 223 | 223 | test("an admin's token from the page is minted only when they tick that they mean it", async () => { |
| 224 | 224 | const root = await signIn("ada"); |
| 225 | 225 | const form = await page(root, "/settings/tokens"); |
| 226 | expect(form).toContain("I mean to give this token admin"); | |
| 226 | expect(form).toContain("I mean to give this access code admin"); | |
| 227 | 227 | const count = () => |
| 228 | 228 | ( |
| 229 | 229 | app.db |
| @@ -241,7 +241,7 @@ describe("the everyday account", () => { | ||
| 241 | 241 | expect(count()).toBe(before + 1); |
| 242 | 242 | // Someone who is not an admin is not asked. |
| 243 | 243 | expect(await page(await signIn("kim"), "/settings/tokens")).not.toContain( |
| 244 | "I mean to give this token admin", | |
| 244 | "I mean to give this access code admin", | |
| 245 | 245 | ); |
| 246 | 246 | }); |
| 247 | 247 |
test/first-login-words.test.ts +7 −7
| @@ -24,13 +24,13 @@ describe("the first-login page of a spawned forge, in plain words", () => { | ||
| 24 | 24 | |
| 25 | 25 | afterAll(() => forge.stop()); |
| 26 | 26 | |
| 27 | /** The page itself: what sits in `<main>`; the menus around it are their own issue. */ | |
| 27 | /** The page itself, to check the first-login content is what was read. */ | |
| 28 | 28 | const mainOf = (html: string) => /<main\b[\s\S]*?<\/main>/.exec(html)?.[0] ?? ""; |
| 29 | 29 | |
| 30 | 30 | test("the owner's first page has no jargon", async () => { |
| 31 | const main = mainOf(await (await fetch(url("/"), { headers: { cookie: owner } })).text()); | |
| 32 | expect(main).toContain("Your next steps"); | |
| 33 | expect(jargonIn(main)).toEqual([]); | |
| 31 | const html = await (await fetch(url("/"), { headers: { cookie: owner } })).text(); | |
| 32 | expect(mainOf(html)).toContain("Your next steps"); | |
| 33 | expect(jargonIn(html)).toEqual([]); | |
| 34 | 34 | }); |
| 35 | 35 | |
| 36 | 36 | test("the everyday account's first page has no jargon", async () => { |
| @@ -41,9 +41,9 @@ describe("the first-login page of a spawned forge, in plain words", () => { | ||
| 41 | 41 | }); |
| 42 | 42 | expect(res.status).toBe(303); |
| 43 | 43 | const daily = (res.headers.get("set-cookie") ?? "").split(";")[0]!; |
| 44 | const main = mainOf(await (await fetch(url("/"), { headers: { cookie: daily } })).text()); | |
| 45 | expect(main).toContain("Your next steps"); | |
| 46 | expect(jargonIn(main)).toEqual([]); | |
| 44 | const html = await (await fetch(url("/"), { headers: { cookie: daily } })).text(); | |
| 45 | expect(mainOf(html)).toContain("Your next steps"); | |
| 46 | expect(jargonIn(html)).toEqual([]); | |
| 47 | 47 | }); |
| 48 | 48 | |
| 49 | 49 | test("the word list sees the words it is for", () => { |
test/frozen-line.test.ts +2 −2
| @@ -62,9 +62,9 @@ for (const path of ["/alice/frozen", "/alice/frozen/branches"]) { | ||
| 62 | 62 | }); |
| 63 | 63 | } |
| 64 | 64 | |
| 65 | test("an admin without an origin is pointed at Settings → Origins", async () => { | |
| 65 | test("an admin without an origin is pointed at Settings → Connected accounts", async () => { | |
| 66 | 66 | const m = await get("/alice/frozen", admin); |
| 67 | expect(m).toContain("Settings → Origins"); | |
| 67 | expect(m).toContain("Settings → Connected accounts"); | |
| 68 | 68 | expect(m).not.toContain("/settings/origin/reconnect"); |
| 69 | 69 | }); |
| 70 | 70 |
test/keys.test.ts +1 −1
| @@ -284,7 +284,7 @@ describe.skipIf(NO_KEYGEN)("ssh keys", () => { | ||
| 284 | 284 | const empty = await ( |
| 285 | 285 | await fetch(`${base}/settings/keys`, { headers: { cookie } }) |
| 286 | 286 | ).text(); |
| 287 | expect(empty).toContain("No SSH keys yet"); | |
| 287 | expect(empty).toContain("No computer keys yet"); | |
| 288 | 288 | |
| 289 | 289 | const added = await fetch(`${base}/settings/keys`, { |
| 290 | 290 | method: "POST", |
test/mcp-trusted-hosts.test.ts +1 −1
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | /** |
| 2 | * The `origins` tool's `hosts` action is Settings → Origins' internal-host list over | |
| 2 | * The `origins` tool's `hosts` action is Settings → Connected accounts' internal-host list over | |
| 3 | 3 | * MCP: the same rules, the same refusals, a forge admin only. |
| 4 | 4 | */ |
| 5 | 5 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; |
test/now-links.test.ts +3 −3
| @@ -119,9 +119,9 @@ const MENU_TITLES: Record<string, string> = { | ||
| 119 | 119 | Migrate: "Migrate", |
| 120 | 120 | "New forge": "New forge", |
| 121 | 121 | Settings: "Settings", |
| 122 | "Access tokens": "Access tokens", | |
| 123 | "SSH keys": "SSH keys", | |
| 124 | Origins: "Origins", | |
| 122 | "Access codes": "Access codes", | |
outdated
The expected titles still read 'Personal access tokens', 'SSH keys', 'Origins': the page is not renamed along with its link. Update once the destinations are reworded. |
||
| 123 | "Computer keys": "Computer keys", | |
| 124 | "Connected accounts": "Connected accounts", | |
| 125 | 125 | Crew: "Crew", |
| 126 | 126 | Boards: "Boards", |
| 127 | 127 | "System memory": "System", |
test/oauth-connect-browser.test.ts +2 −2
| @@ -3,7 +3,7 @@ | ||
| 3 | 3 | * form's redirect wherever it points; a browser first checks the posting |
| 4 | 4 | * page's `form-action` policy against it, and refuses a redirect off this |
| 5 | 5 | * forge without a word on the page. So this drives headless Chromium |
| 6 | * (`test/helpers/chrome.ts`): click Authorize on Settings → Origins, then | |
| 6 | * (`test/helpers/chrome.ts`): click Authorize on Settings → Connected accounts, then | |
| 7 | 7 | * Continue on the flow's page, and see the browser actually ask the origin |
| 8 | 8 | * for its consent screen — the URL stored on the flow — with no |
| 9 | 9 | * Content-Security-Policy refusal on the way. |
| @@ -68,7 +68,7 @@ describe.skipIf(chromium === undefined)("connecting an origin in a real browser" | ||
| 68 | 68 | await rm(dir, { recursive: true, force: true }); |
| 69 | 69 | }); |
| 70 | 70 | |
| 71 | test("Settings → Origins: Authorize, then Continue, reaches the consent screen", async () => { | |
| 71 | test("Settings → Connected accounts: Authorize, then Continue, reaches the consent screen", async () => { | |
| 72 | 72 | const made = await createUser(app.db, { |
| 73 | 73 | username: "bea", |
| 74 | 74 | email: "[email protected]", |
test/onboarding.test.ts +1 −1
| @@ -123,7 +123,7 @@ describe("onboarding: a person with just a login", () => { | ||
| 123 | 123 | const pageHtml = await ( |
| 124 | 124 | await fetch(`${base}/settings/tokens`, { headers: { cookie } }) |
| 125 | 125 | ).text(); |
| 126 | expect(pageHtml).toContain("Access tokens"); | |
| 126 | expect(pageHtml).toContain("Access codes"); | |
| 127 | 127 | |
| 128 | 128 | // Post-redirect-get: minting redirects, and the secret rides a one-shot |
| 129 | 129 | // flash cookie so a refresh shows the list instead of minting again. |
test/origin-flow-page.test.ts +1 −1
| @@ -53,7 +53,7 @@ describe("the connect flow page", () => { | ||
| 53 | 53 | ...(form === undefined ? {} : { body: new URLSearchParams(form) }), |
| 54 | 54 | }); |
| 55 | 55 | |
| 56 | /** Start a code flow from Settings → Origins: its page, and its id. */ | |
| 56 | /** Start a code flow from Settings → Connected accounts: its page, and its id. */ | |
| 57 | 57 | const start = async (): Promise<{ page: string; id: string }> => { |
| 58 | 58 | const started = await browse("/settings/origins/oauth", { host: fake.url }); |
| 59 | 59 | expect(started.status).toBe(303); |
test/origin-guard.test.ts +2 −2
| @@ -677,7 +677,7 @@ describe("the guard against an operator's Forgejo", () => { | ||
| 677 | 677 | const synced = await syncOriginNow(local.app, local.alice, id); |
| 678 | 678 | expect(synced.ok && synced.value.error).toBe(""); |
| 679 | 679 | |
| 680 | // Settings → Origins says so, with the way out. | |
| 680 | // Settings → Connected accounts says so, with the way out. | |
| 681 | 681 | const page = await (await browse(local, "/settings/origins")).text(); |
| 682 | 682 | expect(page).toContain("Couldn't verify this connection; writes paused"); |
| 683 | 683 | expect(page).toContain(`action="/settings/origins/${id}/check"`); |
| @@ -717,7 +717,7 @@ describe("the guard against an operator's Forgejo", () => { | ||
| 717 | 717 | reset(); |
| 718 | 718 | }); |
| 719 | 719 | |
| 720 | test("Check again on Settings → Origins runs the guard and says what it found", async () => { | |
| 720 | test("Check again on Settings → Connected accounts runs the guard and says what it found", async () => { | |
| 721 | 721 | reset(); |
| 722 | 722 | const local = await boot(); |
| 723 | 723 | // Connected before the guard: never checked, so paused once guarded. |
test/origins-page-shape.test.ts +5 −5
| @@ -50,14 +50,14 @@ test("the origins page has settings blocks with headings and no card", async () | ||
| 50 | 50 | expect(res.status).toBe(200); |
| 51 | 51 | const body = await res.text(); |
| 52 | 52 | expect(body).not.toMatch(/class="(?:[^"]*\s)?card(?:\s[^"]*)?"/); |
| 53 | for (const h of ["Origins", "Trusted internal hosts", "Connect an origin"]) { | |
| 53 | for (const h of ["Connected accounts", "Trusted internal hosts", "Connect an account"]) { | |
| 54 | 54 | expect(body).toMatch(new RegExp(`<section class="settings-block"[^>]*>\\s*<h2>${h}</h2>`)); |
| 55 | 55 | } |
| 56 | 56 | expect(body).toContain( |
| 57 | "No origins yet: connect another forge, GitHub, a Forgejo\n instance or a Gitea instance below.", | |
| 57 | "No connected accounts yet: connect another forge, GitHub, a Forgejo\n instance or a Gitea instance below.", | |
| 58 | 58 | ); |
| 59 | 59 | // One sentence: no full stop before its end. |
| 60 | const empty = body.split("<h2>Origins</h2>")[1]!.split("</p>")[0]!; | |
| 60 | const empty = body.split("<h2>Connected accounts</h2>")[1]!.split("</p>")[0]!; | |
| 61 | 61 | expect(empty.replace(/<[^>]+>/g, "").trim().split(". ").length).toBe(1); |
| 62 | 62 | expect(body).toContain('id="connect"'); |
| 63 | 63 | }); |
| @@ -77,7 +77,7 @@ test("connected origins are rows of one list, with no wrapper box", async () => | ||
| 77 | 77 | } |
| 78 | 78 | const body = await (await fetch(`${servers.url}/settings/origins`, { headers: { cookie } })).text(); |
| 79 | 79 | expect(body).not.toContain("origin-item"); |
| 80 | const block = body.split("<h2>Origins</h2>")[1]!.split("</section>")[0]!; | |
| 80 | const block = body.split("<h2>Connected accounts</h2>")[1]!.split("</section>")[0]!; | |
| 81 | 81 | expect(block.match(/<ul class="list setting-list" data-origins>/g)?.length).toBe(1); |
| 82 | 82 | expect(block.match(/<li[ >]/g)!.length).toBeGreaterThanOrEqual(2); |
| 83 | 83 | expect(block).toContain("Sync now"); |
| @@ -100,7 +100,7 @@ test("an origin's error and notes render inside its own row", async () => { | ||
| 100 | 100 | .query("UPDATE origin SET last_error = ?1 WHERE id = ?2") |
| 101 | 101 | .run("upstream said 502", o.value.id); |
| 102 | 102 | const body = await (await fetch(`${servers.url}/settings/origins`, { headers: { cookie } })).text(); |
| 103 | const block = body.split("<h2>Origins</h2>")[1]!.split("</section>")[0]!; | |
| 103 | const block = body.split("<h2>Connected accounts</h2>")[1]!.split("</section>")[0]!; | |
| 104 | 104 | const li = block |
| 105 | 105 | .split(/<li[ >]/) |
| 106 | 106 | .slice(1) |
test/origins-reconnect.test.ts +1 −1
| @@ -397,7 +397,7 @@ describe("a frozen mirror is said, and reconnected in one click", () => { | ||
| 397 | 397 | expect(page).toContain(`>${day}</time>`); |
| 398 | 398 | expect(page).toContain("it has been frozen since"); |
| 399 | 399 | // Alice has no origin on that host now: the way back is connecting one. |
| 400 | expect(page).toContain("Settings → Origins"); | |
| 400 | expect(page).toContain("Settings → Connected accounts"); | |
| 401 | 401 | expect(page).toContain("connecting one reconnects this mirror by itself"); |
| 402 | 402 | } |
| 403 | 403 | // A reader who cannot administer it reads the sentence, not the form. |
test/origins-timing.test.ts +1 −1
| @@ -47,7 +47,7 @@ test("GET /settings/origins names its phases", async () => { | ||
| 47 | 47 | const res = await fetch(`${servers.url}/settings/origins`, { headers: { cookie } }); |
| 48 | 48 | const body = await res.text(); |
| 49 | 49 | expect(res.status).toBe(200); |
| 50 | expect(body).toContain("<h1>Origins</h1>"); | |
| 50 | expect(body).toContain("<h1>Connected accounts</h1>"); | |
| 51 | 51 | const parts = (res.headers.get("server-timing") ?? "").split(", "); |
| 52 | 52 | for (const name of ["app", "session", "origins", "mirrors", "apps", "frozen", "render"]) |
| 53 | 53 | expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/); |
test/palette-repos.test.ts +1 −1
| @@ -117,7 +117,7 @@ test("a visitor's palette holds places to go and Sign in; a signed-in one holds | ||
| 117 | 117 | expect(out.map((i) => i.t)).toEqual(["Home", "Apps", "Explore", "Forge packs", "Front page", "Search", "Sign in"]); |
| 118 | 118 | expect(out.find((i) => i.t === "Sign in")!.h).toBe("/signin"); |
| 119 | 119 | const inn = (await rows({ cookie }, "/explore")).map((i) => i.t); |
| 120 | for (const t of ["New app", "New repository", "Migrate a repository", "New organization", "What you asked for", "Account settings", "Access tokens", "SSH keys", "Origins", "Map"]) expect(inn).toContain(t); | |
| 120 | for (const t of ["New app", "New repository", "Migrate a repository", "New organization", "What you asked for", "Account settings", "Access codes", "Computer keys", "Connected accounts", "Map"]) expect(inn).toContain(t); | |
| 121 | 121 | expect(inn).not.toContain("Sign in"); |
| 122 | 122 | }); |
| 123 | 123 |
test/palette-roster.test.ts +9 −9
| @@ -89,7 +89,7 @@ test("a site-admin person is offered the whole roster", () => { | ||
| 89 | 89 | ["confirms", "Confirms", "what the forge's agent asked to do; y does it once"], |
| 90 | 90 | ["trusted", "Trusted sources", "who folds in without review; a trusts, x removes, s stops all, v reverts"], |
| 91 | 91 | ["conversations", "Conversations", "your talks with the forge's agent; n starts one, r renames, x archives"], |
| 92 | ["ssh-keys", "SSH keys", "list, add and remove the keys that sign in as you"], | |
| 92 | ["ssh-keys", "Computer keys", "list, add and remove the keys that sign in as you"], | |
| 93 | 93 | ["passkeys", "Passkeys", "the passkeys that sign in as you"], |
| 94 | 94 | ["account", "Account settings", undefined], |
| 95 | 95 | ["signins", "Signed in", "your browsers and tokens; d signs a browser out"], |
| @@ -100,7 +100,7 @@ test("a site-admin person is offered the whole roster", () => { | ||
| 100 | 100 | ["home-pins", "Pinned panels", "the panels pinned to home: reorder and unpin"], |
| 101 | 101 | ["now", "Now", "what check, review, land and live count, with the rows"], |
| 102 | 102 | ["forge-view", "Forge view", "the forge's counts, and Foundry when it is installed"], |
| 103 | ["origin-apps", "Origin apps", "your OAuth apps for connecting origins; a adds, d removes"], | |
| 103 | ["origin-apps", "Connection apps", "your OAuth apps for connecting accounts; a adds, d removes"], | |
| 104 | 104 | ["system-memory", "System memory", "RSS beside the JS heap, hourly for 24 h"], |
| 105 | 105 | ["flow", "Flow", "the pipeline's bottleneck and throughput"], |
| 106 | 106 | ["system", "System", "the machine now: CPU, memory, disks, processes"], |
| @@ -156,7 +156,7 @@ test("a site-admin person is offered the whole roster", () => { | ||
| 156 | 156 | ["cloud", "Cloud accounts", "provider tokens, what they are used for, today's calls"], |
| 157 | 157 | ["pinned", "Pinned forges", "deploy, update and roll back a forge on your cloud"], |
| 158 | 158 | ["bets", "Bets", "back, stop and finish a repository\'s bets"], |
| 159 | ["origins", "Origins", "the hosts your mirrors come from"], | |
| 159 | ["origins", "Connected accounts", "the hosts your mirrors come from"], | |
| 160 | 160 | ["domains", "Failure domains", "failure domains and their last drill"], |
| 161 | 161 | ["feed", "Feed", "what happened, ranked; r replies on a post about an issue or pull, l likes, h who liked"], |
| 162 | 162 | ["feature-review", "Menu entries to review", "absorbed apps whose declared name or page changed: accept them"], |
| @@ -208,12 +208,12 @@ test("a site-admin agent is offered the roster without the person-only entries", | ||
| 208 | 208 | ["asked", "Asked", "your thoughts and what became of them"], |
| 209 | 209 | ["thought-notes", "Notes", "write an idea and watch it become shipped work"], |
| 210 | 210 | ["decider", "Decider", "what it closed, deferred and asked you about today"], |
| 211 | ["ssh-keys", "SSH keys", "list, add and remove the keys that sign in as you"], | |
| 211 | ["ssh-keys", "Computer keys", "list, add and remove the keys that sign in as you"], | |
| 212 | 212 | ["account", "Account settings", undefined], |
| 213 | 213 | ["signins", "Signed in", "your browsers and tokens; d signs a browser out"], |
| 214 | 214 | ["sentinel", "Sentinel", "the forge's open faults"], |
| 215 | 215 | ["resilience", "Resilience", "where snapshots go and the mirror; each protection green, red or untested"], |
| 216 | ["origin-apps", "Origin apps", "your OAuth apps for connecting origins; a adds, d removes"], | |
| 216 | ["origin-apps", "Connection apps", "your OAuth apps for connecting accounts; a adds, d removes"], | |
| 217 | 217 | ["api", "API", "the forge's endpoints; run a GET"], |
| 218 | 218 | ["changelog", "Changelog", "the last 30 releases and their pulls"], |
| 219 | 219 | ["releases", "Releases", "the live release and the kept ones"], |
| @@ -248,7 +248,7 @@ test("a site-admin agent is offered the roster without the person-only entries", | ||
| 248 | 248 | ]); |
| 249 | 249 | }); |
| 250 | 250 | |
| 251 | test("no agent is offered Origins", () => { | |
| 251 | test("no agent is offered Connected accounts", () => { | |
| 252 | 252 | expect(roster({ ...admin, kind: "agent" }).map(([id]) => id)).not.toContain("origins"); |
| 253 | 253 | }); |
| 254 | 254 | |
| @@ -294,11 +294,11 @@ test("a member person is offered the signed-in roster without the admin entries" | ||
| 294 | 294 | ["notify", "Phone notifications", "where the crew reaches your phone; s sets, x turns off"], |
| 295 | 295 | ["confirms", "Confirms", "what the forge's agent asked to do; y does it once"], |
| 296 | 296 | ["conversations", "Conversations", "your talks with the forge's agent; n starts one, r renames, x archives"], |
| 297 | ["ssh-keys", "SSH keys", "list, add and remove the keys that sign in as you"], | |
| 297 | ["ssh-keys", "Computer keys", "list, add and remove the keys that sign in as you"], | |
| 298 | 298 | ["passkeys", "Passkeys", "the passkeys that sign in as you"], |
| 299 | 299 | ["account", "Account settings", undefined], |
| 300 | 300 | ["signins", "Signed in", "your browsers and tokens; d signs a browser out"], |
| 301 | ["origin-apps", "Origin apps", "your OAuth apps for connecting origins; a adds, d removes"], | |
| 301 | ["origin-apps", "Connection apps", "your OAuth apps for connecting accounts; a adds, d removes"], | |
| 302 | 302 | ["api", "API", "the forge's endpoints; run a GET"], |
| 303 | 303 | ["changelog", "Changelog", "the last 30 releases and their pulls"], |
| 304 | 304 | ["stalled", "Stalled steps", "pulls whose check is red or was never asked for; enter re-runs"], |
| @@ -313,7 +313,7 @@ test("a member person is offered the signed-in roster without the admin entries" | ||
| 313 | 313 | ["archived", "Archived repositories", "put away, not gone: open one or wake it"], |
| 314 | 314 | ["bulk", "Bulk actions", "your last bulk actions; u undoes one inside its day"], |
| 315 | 315 | ["bets", "Bets", "back, stop and finish a repository\'s bets"], |
| 316 | ["origins", "Origins", "the hosts your mirrors come from"], | |
| 316 | ["origins", "Connected accounts", "the hosts your mirrors come from"], | |
| 317 | 317 | ["feed", "Feed", "what happened, ranked; r replies on a post about an issue or pull, l likes, h who liked"], |
| 318 | 318 | ["feature-feed", "Feature feeds", "the feed items about an absorbed app, as Settings › Features shows them"], |
| 319 | 319 | ["front", "Front page", "the apps a first visitor sees, ranked; an admin pins or hides"], |
test/ssh-site.test.ts +1 −1
| @@ -95,7 +95,7 @@ describe("the terminal on the website", () => { | ||
| 95 | 95 | r.text(), |
| 96 | 96 | ); |
| 97 | 97 | expect(page).not.toContain("terminal-line"); |
| 98 | expect(page).toContain('<a class="small" href="/settings/keys">SSH keys</a>'); | |
| 98 | expect(page).toContain('<a class="small" href="/settings/keys">Computer keys</a>'); | |
| 99 | 99 | expect(page).toContain("host key SHA256:"); |
| 100 | 100 | }); |
| 101 | 101 |
test/tui-originapps.test.ts +2 −2
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | /** |
| 2 | * The terminal's Origin apps screen (src/tui/screens/originapps.ts, #7291): | |
| 2 | * The terminal's Connection apps screen (src/tui/screens/originapps.ts, #7291): | |
| 3 | 3 | * add, list without the secret, a bad host refused in the API's words, and |
| 4 | 4 | * remove after a confirm. No frame and no log line holds the secret. |
| 5 | 5 | */ |
| @@ -18,7 +18,7 @@ import { settleApp } from "./helpers/tui.ts"; | ||
| 18 | 18 | |
| 19 | 19 | const SECRET = "s3cr3t_Qw9zXv"; |
| 20 | 20 | const RIGHT = "\x1b[C"; |
| 21 | const open = [":", "Origin apps", "\r"]; | |
| 21 | const open = [":", "Connection apps", "\r"]; | |
| 22 | 22 | |
| 23 | 23 | let dir: string; |
| 24 | 24 | let app: App; |
test/tui-origins-forge.test.ts +2 −2
| @@ -73,7 +73,7 @@ test("c connects another forge by address and token, and the list says what did | ||
| 73 | 73 | }; |
| 74 | 74 | try { |
| 75 | 75 | await key("k"); |
| 76 | await type("Origins"); | |
| 76 | await type("Connected accounts"); | |
| 77 | 77 | await key("enter"); |
| 78 | 78 | expect(app.screen.toText()).toContain("Press c to connect one."); |
| 79 | 79 | // A forge has no device sign-in: the terminal says so and points at the page. |
| @@ -123,7 +123,7 @@ test("a token the forge refuses connects nothing and says so", async () => { | ||
| 123 | 123 | }; |
| 124 | 124 | try { |
| 125 | 125 | await press("k", true); |
| 126 | await type("Origins"); | |
| 126 | await type("Connected accounts"); | |
| 127 | 127 | await press("enter"); |
| 128 | 128 | await press("c"); |
| 129 | 129 | await type(friendServers.url); |
test/tui-origins-guard.test.ts +1 −1
| @@ -68,7 +68,7 @@ describe("a paused origin in the terminal", () => { | ||
| 68 | 68 | }; |
| 69 | 69 | try { |
| 70 | 70 | await key("k", true); |
| 71 | for (const ch of "Origins") await key(ch); | |
| 71 | for (const ch of "Connected accounts") await key(ch); | |
| 72 | 72 | await key("enter"); |
| 73 | 73 | expect(app.screen.toText()).toContain("writes paused"); |
| 74 | 74 | expect(app.screen.toText()).toContain("Press v to check again"); |
test/tui-origins.test.ts +5 −5
| @@ -1,4 +1,4 @@ | ||
| 1 | /** Origins in the terminal: listed, disconnected after a confirm, never offered to a guest. */ | |
| 1 | /** Connected accounts in the terminal: listed, disconnected after a confirm, never offered to a guest. */ | |
| 2 | 2 | |
| 3 | 3 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; |
| 4 | 4 | import { mkdtemp, rm } from "node:fs/promises"; |
| @@ -47,7 +47,7 @@ describe("connected origins in the terminal", () => { | ||
| 47 | 47 | |
| 48 | 48 | const open = async (h: ReturnType<typeof boot>) => { |
| 49 | 49 | await h.ctrl("k"); |
| 50 | await h.type("Origins"); | |
| 50 | await h.type("Connected accounts"); | |
| 51 | 51 | return h; |
| 52 | 52 | }; |
| 53 | 53 | |
| @@ -116,7 +116,7 @@ describe("connected origins in the terminal", () => { | ||
| 116 | 116 | const h = boot({ sshUsername: "t", remoteAddress: "127.0.0.1", user: getUserByName(forge.db, "carol")! } as TuiIdentity); |
| 117 | 117 | try { |
| 118 | 118 | await h.ctrl("k"); |
| 119 | await h.type("Origins"); | |
| 119 | await h.type("Connected accounts"); | |
| 120 | 120 | await h.press("enter"); |
| 121 | 121 | expect(listOrigins(forge.db, made.value.id)[0]!.lastError).toBe(""); |
| 122 | 122 | await h.press("s"); |
| @@ -167,7 +167,7 @@ describe("connected origins in the terminal", () => { | ||
| 167 | 167 | const h = boot({ ...alice(), user: { ...getUserByName(forge.db, "alice")!, kind: "agent" } } as TuiIdentity); |
| 168 | 168 | try { |
| 169 | 169 | await h.ctrl("k"); |
| 170 | await h.type("Origins"); | |
| 170 | await h.type("Connected accounts"); | |
| 171 | 171 | expect(h.text()).not.toContain("the hosts your mirrors come from"); |
| 172 | 172 | } finally { |
| 173 | 173 | h.stop(); |
| @@ -200,7 +200,7 @@ describe("connected origins in the terminal", () => { | ||
| 200 | 200 | const h = boot({ sshUsername: "git", remoteAddress: "127.0.0.1", fingerprint: "SHA256:unknown" }); |
| 201 | 201 | try { |
| 202 | 202 | await h.ctrl("k"); |
| 203 | await h.type("Origins"); | |
| 203 | await h.type("Connected accounts"); | |
| 204 | 204 | expect(h.text()).not.toContain("the hosts your mirrors come from"); |
| 205 | 205 | } finally { |
| 206 | 206 | h.stop(); |
test/tui-sshkeys.test.ts +3 −3
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | /** |
| 2 | * SSH keys in the terminal (#7242): your own keys listed, one added, a bad | |
| 2 | * Computer keys in the terminal (#7242): your own keys listed, one added, a bad | |
| 3 | 3 | * line refused in MCP's words, one removed after a confirm. |
| 4 | 4 | */ |
| 5 | 5 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; |
| @@ -27,7 +27,7 @@ const line = (fill: number, comment = "") => { | ||
| 27 | 27 | return `ssh-ed25519 ${blob.toString("base64")} ${comment}`.trim(); |
| 28 | 28 | }; |
| 29 | 29 | |
| 30 | describe("SSH keys in the terminal", () => { | |
| 30 | describe("Computer keys in the terminal", () => { | |
| 31 | 31 | let dataDir: string; |
| 32 | 32 | let app: App; |
| 33 | 33 | |
| @@ -62,7 +62,7 @@ describe("SSH keys in the terminal", () => { | ||
| 62 | 62 | open: async function () { |
| 63 | 63 | key("k", true); |
| 64 | 64 | await settleApp(tui); |
| 65 | await this.type("SSH keys"); | |
| 65 | await this.type("Computer keys"); | |
| 66 | 66 | await this.press("enter"); |
| 67 | 67 | }, |
| 68 | 68 | stop: () => tui.stop(), |
test/web-origins.test.ts +2 −2
| @@ -232,7 +232,7 @@ describe("origins on the web", () => { | ||
| 232 | 232 | |
| 233 | 233 | test("the origins page starts empty and offers the three ways in", async () => { |
| 234 | 234 | const html = await (await asUser(alice, "/settings/origins")).text(); |
| 235 | expect(html).toContain("No origins yet"); | |
| 235 | expect(html).toContain("No connected accounts yet"); | |
| 236 | 236 | expect(html).toContain("Forgejo / Gitea, one click"); |
| 237 | 237 | // Another forge leads (#9388); the rest are folded. |
| 238 | 238 | expect(html).toContain("Another forge"); |
| @@ -363,7 +363,7 @@ describe("origins on the web", () => { | ||
| 363 | 363 | |
| 364 | 364 | test("another user cannot see or act on that origin", async () => { |
| 365 | 365 | const html = await (await asUser(bob, "/settings/origins")).text(); |
| 366 | expect(html).toContain("No origins yet"); | |
| 366 | expect(html).toContain("No connected accounts yet"); | |
| 367 | 367 | const id = listOrigins(app.db, getUserByName(app.db, "alice")!.id)[0]!.id; |
| 368 | 368 | const stolen = await asUser(bob, `/settings/origins/${id}/repos`); |
| 369 | 369 | expect(stolen.status).toBe(404); |
Stale comment: 'Settings › Origins' should now read Connected accounts.