Public: anyone can read this; the crew can change it.
An app the owner no longer cares about is archived in one tap and restored in one, starting from the parked archive branch #10890
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/10872-an-app-the-owner-no-longer-cares-about-i → main 4 commits fork 2 threads · 2 unresolved
16 files
changed +95
−38
docs/TOUR.md +10 −6
| @@ -300,16 +300,20 @@ An admin puts a repository away from **Settings → Archive**, with | ||
| 300 | 300 | `PATCH /api/v1/repos/:owner/:name {"archived":true|false}` (admins only; anyone |
| 301 | 301 | else gets 403), or in the terminal with `a` on the About card (`I`). The |
| 302 | 302 | repository page and the card then say when it was archived, and an admin gets |
| 303 | a Wake button or the same key to undo it. The archived list | |
| 304 | (`/apps?archived=1`) has a Wake button on each row its viewer administers, and | |
| 305 | returns to the list; the terminal wakes from the About card with the same key | |
| 303 | a Restore button or the same key to undo it. One tap from an app: an admin opens the app's page, presses Archive under | |
| 304 | the page, and confirms on the page that follows (`POST /:owner/:name/archive`, | |
| 305 | the same `updateRepoSettings` as the Settings form and the `archive` MCP tool). | |
| 306 | Production and previews stop, the app leaves home and `/apps`, and its address | |
| 307 | still answers. In the terminal the door is `a` on the About card. The archived list | |
| 308 | (`/apps?archived=1`) has a Restore button on each row its viewer administers, and | |
| 309 | returns to the list; the terminal restores from the About card with the same key | |
| 306 | 310 | (`src/tui/screens/about.ts`). Archiving twice keeps the first date. |
| 307 | 311 | An archived repository starts no new crew run. Assigning an issue to an agent, |
| 308 | 312 | queueing a run again, asking the crew to propose a resolution, redirecting a |
| 309 | wave and converging a fork each answer "This repository is archived; wake it to | |
| 313 | wave and converging a fork each answer "This repository is archived; restore it to | |
| 310 | 314 | start a run."; the API answers 409 `archived`, and the terminal and MCP show |
| 311 | 315 | the same line. The autonomy tick skips it. A run already going finishes, and |
| 312 | Wake undoes all of it. Clone, push and links keep working, and the repository | |
| 316 | Restore undoes all of it. Clone, push and links keep working, and the repository | |
| 313 | 317 | page still answers. |
| 314 | 318 | |
| 315 | 319 | An archived repository leaves the web home's apps, the inbox (work assigned to |
| @@ -340,7 +344,7 @@ it (`undoFleetOp`). A non-admin has neither key. | ||
| 340 | 344 | |
| 341 | 345 | `docs/FLEET.md` is one change across many repositories, runs or issues: a |
| 342 | 346 | query, a dry-run plan of every row before → after, a receipt, and undo. |
| 343 | `archive` (true or false) archives or wakes many repositories; its undo is kept a day. | |
| 347 | `archive` (true or false) archives or restores many repositories; its undo is kept a day. | |
| 344 | 348 | |
| 345 | 349 | ## Where it came from |
| 346 | 350 |
src/actions/repos.ts +2 −2
| @@ -340,7 +340,7 @@ function inTurn<T>(app: App, repoId: string, work: () => Promise<T>): Promise<T> | ||
| 340 | 340 | } |
| 341 | 341 | |
| 342 | 342 | /** |
| 343 | * Archive (true) or wake (false), and act on it only if this call moved the | |
| 343 | * Archive (true) or restore (false), and act on it only if this call moved the | |
| 344 | 344 | * repository (`moveArchived`). The write and its effect take one turn per |
| 345 | 345 | * repository, so a Wake asked while an Archive is still stopping the app |
| 346 | 346 | * starts it after that stop, not before it. True when this call moved it. |
| @@ -543,7 +543,7 @@ export interface ArchiveAnswer { | ||
| 543 | 543 | } |
| 544 | 544 | |
| 545 | 545 | /** |
| 546 | * Archive (true) or wake (false) a repository, for the MCP: the same | |
| 546 | * Archive (true) or restore (false) a repository, for the MCP: the same | |
| 547 | 547 | * `updateRepoSettings` the API's PATCH goes through. A repository admin's, |
| 548 | 548 | * never an agent's; a dry run answers what would change and changes nothing. |
| 549 | 549 | */ |
src/core/repos.ts +1 −1
| @@ -1325,7 +1325,7 @@ export const withoutArchived = (repos: readonly Repo[]): Repo[] => | ||
| 1325 | 1325 | repos.filter((repo) => repo.archivedAt === null); |
| 1326 | 1326 | |
| 1327 | 1327 | /** The one line said wherever the crew declines to start a run here. */ |
| 1328 | export const ARCHIVED_WHY = "This repository is archived; wake it to start a run."; | |
| 1328 | export const ARCHIVED_WHY = "This repository is archived; restore it to start a run."; | |
| 1329 | 1329 | |
| 1330 | 1330 | /** |
| 1331 | 1331 | * Update the mutable repo fields, leaving unspecified ones untouched. |
src/http/mcp/tools/archive.ts +1 −1
| @@ -43,7 +43,7 @@ export const tool: Tool = { | ||
| 43 | 43 | ? `This is the plan; nothing changed. It is ${set.value.was ? "archived" : "awake"} now; send dryRun: false to ${set.value.archived ? "archive" : "wake"} it.` |
| 44 | 44 | : set.value.archived |
| 45 | 45 | ? "Archived: put away, not deleted." |
| 46 | : "Awake again.", | |
| 46 | : "Restored.", | |
| 47 | 47 | }); |
| 48 | 48 | }, |
| 49 | 49 | }; |
src/http/server.ts +2
| @@ -393,6 +393,7 @@ import { | ||
| 393 | 393 | settingsUpdate, |
| 394 | 394 | waveCut, |
| 395 | 395 | archiveSubmit, |
| 396 | archiveConfirmed, | |
| 396 | 397 | transferSubmit, |
| 397 | 398 | provenanceSubmit, |
| 398 | 399 | mirrorStop, |
| @@ -1472,6 +1473,7 @@ export function buildRouter(): Router<Handler> { | ||
| 1472 | 1473 | router.add("POST", "/:owner/:name/settings", settingsUpdate); |
| 1473 | 1474 | router.add("POST", "/:owner/:name/settings/wave", waveCut); |
| 1474 | 1475 | router.add("POST", "/:owner/:name/settings/archive", archiveSubmit); |
| 1476 | router.add("POST", "/:owner/:name/archive", archiveConfirmed); | |
| 1475 | 1477 | router.add("POST", "/:owner/:name/settings/transfer", transferSubmit); |
| 1476 | 1478 | router.add("POST", "/:owner/:name/settings/provenance", provenanceSubmit); |
| 1477 | 1479 | router.add("POST", "/:owner/:name/settings/mirror/stop", mirrorStop); |
src/tui/screens/about.ts +2 −2
| @@ -201,7 +201,7 @@ export function aboutScreen( | ||
| 201 | 201 | ? [ |
| 202 | 202 | binding( |
| 203 | 203 | ["a"], |
| 204 | repo?.archivedAt === null ? "archive" : "wake", | |
| 204 | repo?.archivedAt === null ? "archive" : "restore", | |
| 205 | 205 | ), |
| 206 | 206 | ] |
| 207 | 207 | : []), |
| @@ -485,7 +485,7 @@ export function aboutScreen( | ||
| 485 | 485 | if (done.ok) { |
| 486 | 486 | repo = getRepoById(f.db, done.value.id) ?? done.value; |
| 487 | 487 | rows = factRows(); |
| 488 | ctx.toast(waking ? "awake again" : "archived", "success"); | |
| 488 | ctx.toast(waking ? "restored" : "archived", "success"); | |
| 489 | 489 | } else ctx.toast("that could not be changed", "danger"); |
| 490 | 490 | ctx.invalidate(); |
| 491 | 491 | }); |
src/tui/screens/archived.ts +7 −7
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | /** |
| 2 | 2 | * Archived repositories in the terminal: what /apps?archived=1 lists on the |
| 3 | 3 | * web. Archiving hides a repository from the dashboard, never from its |
| 4 | * readers; this screen is the way back. Enter opens the About card; `a` wakes | |
| 4 | * readers; this screen is the way back. Enter opens the About card; `a` restores | |
| 5 | 5 | * the selected one through `updateRepoSettings`, offered only to a person who |
| 6 | 6 | * administers it. |
| 7 | 7 | */ |
| @@ -35,12 +35,12 @@ export function archivedScreen(f: Session, ctx: Ctx): Screen { | ||
| 35 | 35 | let { repos, withApp } = archivedRepos(f); |
| 36 | 36 | const selected = (): Repo | undefined => repos[state.index]; |
| 37 | 37 | // A person's, an admin's: the About card's gate for `a`. |
| 38 | const mayWake = (repo: Repo | undefined): repo is Repo => | |
| 38 | const mayRestore = (repo: Repo | undefined): repo is Repo => | |
| 39 | 39 | repo !== undefined && f.user !== undefined && f.user.kind !== "agent" && canAdmin(f.db, repo, f.user); |
| 40 | 40 | |
| 41 | 41 | const screen: Screen = { |
| 42 | 42 | title: "Archived repositories", |
| 43 | keys: [binding(["j", "k"], "move cursor"), binding(["enter"], "about"), binding(["a"], "wake")], | |
| 43 | keys: [binding(["j", "k"], "move cursor"), binding(["enter"], "about"), binding(["a"], "restore")], | |
| 44 | 44 | render(area, s) { |
| 45 | 45 | const c = ctx.theme.c; |
| 46 | 46 | const [head, body] = split(area, "v", [len(1), fill(1)]); |
| @@ -53,7 +53,7 @@ export function archivedScreen(f: Session, ctx: Ctx): Screen { | ||
| 53 | 53 | secondary: (r.archivedAt ?? "").slice(0, 10), |
| 54 | 54 | meta: withApp.has(r.id) ? "app spun down" : "", |
| 55 | 55 | })); |
| 56 | const line = mayWake(selected()) ? "enter opens it · a wakes it" : "enter opens it"; | |
| 56 | const line = mayRestore(selected()) ? "enter opens it · a restores it" : "enter opens it"; | |
| 57 | 57 | s.text(head!.x, head!.y, truncate(line, head!.w), style(c.mutedFg, c.bg), head!.x + head!.w); |
| 58 | 58 | drawList(s, body!, c, state, rows); |
| 59 | 59 | }, |
| @@ -66,12 +66,12 @@ export function archivedScreen(f: Session, ctx: Ctx): Screen { | ||
| 66 | 66 | ctx.push(aboutScreen(f, ctx, screen, local(f, repo), undefined, "")); |
| 67 | 67 | return true; |
| 68 | 68 | } |
| 69 | if (e.key === "a" && mayWake(repo)) { | |
| 69 | if (e.key === "a" && mayRestore(repo)) { | |
| 70 | 70 | void updateRepoSettings(f.app, f.user, repo, { archived: false }).then((done) => { |
| 71 | 71 | if (done.ok) { |
| 72 | 72 | ({ repos, withApp } = archivedRepos(f)); |
| 73 | 73 | state.clamp(repos.length); |
| 74 | ctx.toast("awake again", "success"); | |
| 74 | ctx.toast("restored", "success"); | |
| 75 | 75 | } else ctx.toast("that could not be changed", "danger"); |
| 76 | 76 | ctx.invalidate(); |
| 77 | 77 | }); |
| @@ -87,7 +87,7 @@ export function archivedScreen(f: Session, ctx: Ctx): Screen { | ||
| 87 | 87 | export const entry: PaletteEntry = { |
| 88 | 88 | id: "archived", |
| 89 | 89 | title: "Archived repositories", |
| 90 | hint: "put away, not gone: open one or wake it", | |
| 90 | hint: "put away, not gone: open one or restore it", | |
| 91 | 91 | offered: (session) => session.user !== undefined, |
| 92 | 92 | run: (session, ctx) => ctx.push(archivedScreen(session, ctx)), |
| 93 | 93 | }; |
src/web/pages/apps.ts +4 −4
| @@ -147,8 +147,8 @@ export function archivedAppCount(ctx: Ctx): number { | ||
| 147 | 147 | ).size; |
| 148 | 148 | } |
| 149 | 149 | |
| 150 | /** The archived list's Wake: a small form under each row the viewer administers. */ | |
| 151 | function wakeForms(ctx: Ctx, repos: readonly Repo[]): Map<string, Html> { | |
| 150 | /** The archived list's Restore: a small form under each row the viewer administers. */ | |
| 151 | function restoreForms(ctx: Ctx, repos: readonly Repo[]): Map<string, Html> { | |
| 152 | 152 | const out = new Map<string, Html>(); |
| 153 | 153 | for (const repo of repos) { |
| 154 | 154 | if (!canAdmin(ctx.app.db, repo, ctx.auth.user)) continue; |
| @@ -158,7 +158,7 @@ function wakeForms(ctx: Ctx, repos: readonly Repo[]): Map<string, Html> { | ||
| 158 | 158 | <form method="post" action="${repoBase(repo)}/settings/archive"> |
| 159 | 159 | <input type="hidden" name="archived" value="0" /> |
| 160 | 160 | <input type="hidden" name="to" value="/apps?archived=1" /> |
| 161 | ${btn(html`Wake<span class="sr-only"> ${repo.ownerName}/${repo.name}</span>`)} | |
| 161 | ${btn(html`Restore<span class="sr-only"> ${repo.ownerName}/${repo.name}</span>`)} | |
| 162 | 162 | </form> |
| 163 | 163 | </li>`, |
| 164 | 164 | ); |
| @@ -225,7 +225,7 @@ export async function appsList(ctx: Ctx): Promise<Response> { | ||
| 225 | 225 | }, |
| 226 | 226 | ])} |
| 227 | 227 | </div>`; |
| 228 | const wake = archivedView ? wakeForms(ctx, shown.map((e) => e.repo)) : undefined; | |
| 228 | const wake = archivedView ? restoreForms(ctx, shown.map((e) => e.repo)) : undefined; | |
| 229 | 229 | const rows = await timePhaseAsync(ctx.request, "shelf", () => |
| 230 | 230 | shelfRows( |
| 231 | 231 | ctx, |
src/web/pages/repo.ts +2 −2
| @@ -1318,7 +1318,7 @@ function movedLine(ctx: Ctx, repo: Repo): Html | null { | ||
| 1318 | 1318 | } |
| 1319 | 1319 | |
| 1320 | 1320 | /** |
| 1321 | * "Archived on <date>" with, for an admin, the Wake button. A repository put | |
| 1321 | * "Archived on <date>" with, for an admin, the Restore button. A repository put | |
| 1322 | 1322 | * away still clones, pushes and links as before; this only says so. |
| 1323 | 1323 | */ |
| 1324 | 1324 | function archivedLine(ctx: Ctx, repo: Repo): Html | null { |
| @@ -1333,7 +1333,7 @@ function archivedLine(ctx: Ctx, repo: Repo): Html | null { | ||
| 1333 | 1333 | () => html`<form method="post" action="${repoBase(repo)}/settings/archive"> |
| 1334 | 1334 | <input type="hidden" name="archived" value="0" /> |
| 1335 | 1335 | <input type="hidden" name="to" value="repo" /> |
| 1336 | ${btn("Wake", { size: "sm" })} | |
| 1336 | ${btn("Restore", { size: "sm" })} | |
| 1337 | 1337 | </form>`, |
| 1338 | 1338 | )} |
| 1339 | 1339 | </div>`; |
src/web/pages/search.ts +10
| @@ -564,6 +564,16 @@ const DESTRUCTIVE: { | ||
| 564 | 564 | pattern: RegExp; |
| 565 | 565 | describe: (m: RegExpMatchArray, ctx: Ctx) => Destructive; |
| 566 | 566 | }[] = [ |
| 567 | { | |
| 568 | pattern: /^\/([^/]+)\/([^/]+)\/archive$/, | |
| 569 | describe: (m) => ({ | |
| 570 | verb: "Archive", | |
| 571 | title: `Archive ${m[1]}/${m[2]}?`, | |
| 572 | detail: | |
| 573 | "Its process stops and it leaves home and the apps list. Its data and git stay, its address still answers, and Restore on the Archived list starts it again.", | |
| 574 | phrase: "", | |
| 575 | }), | |
| 576 | }, | |
| 567 | 577 | { |
| 568 | 578 | pattern: /^\/([^/]+)\/([^/]+)\/settings\/delete$/, |
| 569 | 579 | describe: (m) => ({ |
src/web/pages/settings.ts +17 −5
| @@ -409,10 +409,10 @@ export async function settingsPage( | ||
| 409 | 409 | <p class="muted"> |
| 410 | 410 | ${repo.archivedAt === null |
| 411 | 411 | ? "Put a finished repository away without deleting it. Its page says when it was archived; links, clone and push keep working." |
| 412 | : html`Archived on ${repo.archivedAt.slice(0, 10)}. Wake it to take the label off.`} | |
| 412 | : html`Archived on ${repo.archivedAt.slice(0, 10)}. Restore it to take the label off.`} | |
| 413 | 413 | </p> |
| 414 | 414 | <div class="form-actions"> |
| 415 | ${btn(repo.archivedAt === null ? "Archive" : "Wake", { | |
| 415 | ${btn(repo.archivedAt === null ? "Archive" : "Restore", { | |
| 416 | 416 | variant: "primary", |
| 417 | 417 | })} |
| 418 | 418 | </div> |
| @@ -545,14 +545,26 @@ function samePath(ctx: Ctx, to: string | undefined): string | undefined { | ||
| 545 | 545 | } |
| 546 | 546 | |
| 547 | 547 | /** |
| 548 | * POST /:owner/:name/settings/archive — archive or wake. The state asked | |
| 548 | * POST /:owner/:name/settings/archive — archive or restore. The state asked | |
| 549 | 549 | * for travels in the form, so a double submit is idempotent. |
| 550 | 550 | */ |
| 551 | 551 | export async function archiveSubmit(ctx: Ctx): Promise<Response> { |
| 552 | return archiveWith(ctx, undefined); | |
| 553 | } | |
| 554 | ||
| 555 | /** | |
| 556 | * POST /:owner/:name/archive — the one-tap Archive, reached only through | |
| 557 | * `/-/confirm`: the form there carries no field, so the answer is fixed here. | |
| 558 | */ | |
| 559 | export async function archiveConfirmed(ctx: Ctx): Promise<Response> { | |
| 560 | return archiveWith(ctx, "1"); | |
| 561 | } | |
| 562 | ||
| 563 | async function archiveWith(ctx: Ctx, fixed: "1" | undefined): Promise<Response> { | |
| 552 | 564 | const gate = await adminForm(ctx); |
| 553 | 565 | if (gate instanceof Response) return gate; |
| 554 | 566 | const { repo, form } = gate; |
| 555 | const asked = field(form, "archived"); | |
| 567 | const asked = fixed ?? field(form, "archived"); | |
| 556 | 568 | if (asked !== "0" && asked !== "1") |
| 557 | 569 | return flashTo(ctx, `${repoBase(repo)}/settings`, { |
| 558 | 570 | tone: "danger", |
| @@ -571,7 +583,7 @@ export async function archiveSubmit(ctx: Ctx): Promise<Response> { | ||
| 571 | 583 | ctx, |
| 572 | 584 | back, |
| 573 | 585 | updated.ok |
| 574 | ? { message: archived ? "Archived." : "Awake again." } | |
| 586 | ? { message: archived ? "Archived." : "Restored." } | |
| 575 | 587 | : { tone: "danger", message: NOT_CHANGED }, |
| 576 | 588 | ); |
| 577 | 589 | } |
src/web/studio/app.ts +1
| @@ -454,6 +454,7 @@ export async function studioApp( | ||
| 454 | 454 | const more = html`<p class="muted small"> |
| 455 | 455 | <a href="${base}/variants">Variants</a> |
| 456 | 456 | ${when(canAdmin(db, repo, ctx.auth.user), () => html`<a href="${base}/settings/app">Environment settings</a>`)} |
| 457 | ${when(canAdmin(db, repo, ctx.auth.user), () => html`<a href="/-/confirm?to=${encodeURIComponent(`${base}/archive`)}">Archive</a>`)} | |
| 457 | 458 | </p>`; |
| 458 | 459 | |
| 459 | 460 | // An app that follows another says so before anything else: whether it |
test/apps-archived.test.ts +29 −1
| @@ -139,7 +139,7 @@ test("the archived list wakes an app in place, and ignores a return address off | ||
| 139 | 139 | expect(list).toContain('action="/alice/shelved/settings/archive"'); |
| 140 | 140 | expect(list).toContain('name="to" value="/apps?archived=1"'); |
| 141 | 141 | // Each button names its app, so a screen reader can tell the Wakes apart. |
| 142 | expect(list).toMatch(/Wake<span class="sr-only"> alice\/shelved<\/span>/); | |
| 142 | expect(list).toMatch(/Restore<span class="sr-only"> alice\/shelved<\/span>/); | |
| 143 | 143 | const post = (to: string, headers: Record<string, string> = { origin: servers.url }) => |
| 144 | 144 | fetch(`${servers.url}/alice/shelved/settings/archive`, { |
| 145 | 145 | method: "POST", |
| @@ -176,3 +176,31 @@ test("a viewer who does not administer an archived app gets no Wake button", asy | ||
| 176 | 176 | expect(anon).not.toContain("settings/archive"); |
| 177 | 177 | expect((await get("/apps?archived=1")).body).toContain("/alice/openshelf/settings/archive"); |
| 178 | 178 | }); |
| 179 | ||
| 180 | test("one tap: the app's page offers Archive through the confirm page, which archives and Restore undoes", async () => { | |
| 181 | const page = await get("/alice/keeper"); | |
| 182 | expect(page.body).toContain("/-/confirm?to=%2Falice%2Fkeeper%2Farchive"); | |
| 183 | const confirm = await get("/-/confirm?to=%2Falice%2Fkeeper%2Farchive"); | |
| 184 | expect(confirm.status).toBe(200); | |
| 185 | expect(confirm.body).toContain('action="/alice/keeper/archive"'); | |
| 186 | const post = (headers: Record<string, string>) => | |
| 187 | fetch(`${servers.url}/alice/keeper/archive`, { method: "POST", headers: { cookie, ...headers }, redirect: "manual" }); | |
| 188 | expect((await post({ origin: "https://evil.example" })).status).toBeGreaterThanOrEqual(400); | |
| 189 | expect(getRepo(app.db, "alice", "keeper")!.archivedAt).toBeNull(); | |
| 190 | expect((await post({ origin: servers.url })).status).toBe(303); | |
| 191 | expect(getRepo(app.db, "alice", "keeper")!.archivedAt).not.toBeNull(); | |
| 192 | expect((await get("/apps")).body).not.toContain(ROW_KEEPER); | |
| 193 | expect((await get("/apps?archived=1")).body).toContain(ROW_KEEPER); | |
| 194 | expect((await get("/alice/keeper")).status).toBe(200); | |
| 195 | // Restore is the button's own route: the same form the archived list draws. | |
outdated
Restore is done through updateRepoSettings, so the Restore button's route is untested for this flow. POST to /alice/keeper/settings/archive with archived=0 and assert the app is listed in /apps again, and that its process is started. |
||
| 196 | const restore = await fetch(`${servers.url}/alice/keeper/settings/archive`, { | |
| 197 | method: "POST", | |
| 198 | headers: { cookie, origin: servers.url }, | |
| 199 | body: new URLSearchParams({ archived: "0", to: "/apps?archived=1" }), | |
| 200 | redirect: "manual", | |
| 201 | }); | |
| 202 | expect(restore.status).toBe(303); | |
| 203 | expect(getRepo(app.db, "alice", "keeper")!.archivedAt).toBeNull(); | |
| 204 | expect((await get("/apps")).body).toContain(ROW_KEEPER); | |
| 205 | expect((await get("/apps?archived=1")).body).not.toContain(ROW_KEEPER); | |
| 206 | }); |
test/archive.test.ts +1 −1
| @@ -149,7 +149,7 @@ describe("archive and wake", () => { | ||
| 149 | 149 | ).text(); |
| 150 | 150 | expect(asOwner).toContain(`Archived on`); |
| 151 | 151 | expect(asOwner).toContain(day); |
| 152 | expect(asOwner).toContain("Wake"); | |
| 152 | expect(asOwner).toContain("Restore"); | |
| 153 | 153 | |
| 154 | 154 | const asStranger = await ( |
| 155 | 155 | await fetch(`${base}/ada/old`, { headers: { cookie: stranger } }) |
test/palette-roster.test.ts +3 −3
| @@ -152,7 +152,7 @@ test("a site-admin person is offered the whole roster", () => { | ||
| 152 | 152 | ["traffic-sources", "Traffic sources", "where people come from and where they go, for the forge"], |
| 153 | 153 | ["audit", "Who can do what", "each app's doors from its contract.json: yes, own or no"], |
| 154 | 154 | ["security-controls", "Security controls", "each safeguard, and when it was last tested"], |
| 155 | ["archived", "Archived repositories", "put away, not gone: open one or wake it"], | |
| 155 | ["archived", "Archived repositories", "put away, not gone: open one or restore it"], | |
| 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"], |
| @@ -233,7 +233,7 @@ test("a site-admin agent is offered the roster without the person-only entries", | ||
| 233 | 233 | ["bugs", "Bugs", "open bugs and the bug clock"], |
| 234 | 234 | ["traffic-sources", "Traffic sources", "where people come from and where they go, for the forge"], |
| 235 | 235 | ["audit", "Who can do what", "each app's doors from its contract.json: yes, own or no"], |
| 236 | ["archived", "Archived repositories", "put away, not gone: open one or wake it"], | |
| 236 | ["archived", "Archived repositories", "put away, not gone: open one or restore it"], | |
| 237 | 237 | ["cloud", "Cloud accounts", "provider tokens, what they are used for, today's calls"], |
| 238 | 238 | ["pinned", "Pinned forges", "deploy, update and roll back a forge on your cloud"], |
| 239 | 239 | ["feed", "Feed", "what happened, ranked; r replies on a post about an issue or pull, l likes, h who liked"], |
| @@ -310,7 +310,7 @@ test("a member person is offered the signed-in roster without the admin entries" | ||
| 310 | 310 | ["can", "What I can do", "your role and what it allows here"], |
| 311 | 311 | ["bugs", "Bugs", "open bugs and the bug clock"], |
| 312 | 312 | ["audit", "Who can do what", "each app's doors from its contract.json: yes, own or no"], |
| 313 | ["archived", "Archived repositories", "put away, not gone: open one or wake it"], | |
| 313 | ["archived", "Archived repositories", "put away, not gone: open one or restore 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 | 316 | ["origins", "Connected accounts", "the hosts your mirrors come from"], |
test/tui-archived.test.ts +3 −3
| @@ -95,10 +95,10 @@ describe("archived repositories in the terminal", () => { | ||
| 95 | 95 | await h.palette(); |
| 96 | 96 | // Newest archived first: bob/theirs was archived last. |
| 97 | 97 | await h.press("j"); |
| 98 | expect(h.text()).toContain("a wakes it"); | |
| 98 | expect(h.text()).toContain("a restores it"); | |
| 99 | 99 | await h.press("a"); |
| 100 | 100 | expect(getRepo(forge.db, "carol", "shelved")!.archivedAt).toBeNull(); |
| 101 | expect(h.text()).toContain("awake again"); | |
| 101 | expect(h.text()).toContain("restored"); | |
| 102 | 102 | expect(h.text()).not.toContain("carol/shelved"); |
| 103 | 103 | h.stop(); |
| 104 | 104 | }); |
| @@ -107,7 +107,7 @@ describe("archived repositories in the terminal", () => { | ||
| 107 | 107 | const h = boot("carol"); |
| 108 | 108 | await h.palette(); |
| 109 | 109 | expect(h.text()).toContain("bob/theirs"); |
| 110 | expect(h.text()).not.toContain("a wakes it"); | |
| 110 | expect(h.text()).not.toContain("a restores it"); | |
| 111 | 111 | await h.press("a"); |
| 112 | 112 | expect(getRepo(forge.db, "bob", "theirs")!.archivedAt).not.toBeNull(); |
| 113 | 113 | h.stop(); |
new above
offline
Principle 8 (plain words): Restore here, but Wake remains in settings.ts:413/416, repo.ts:1336 and the TUI archived screen. Use one word for the undo.