@nanocollective/roster 0.1.0-alpha.5 → 0.1.0-alpha.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +70 -84
  2. package/dist/cli.js +4833 -2752
  3. package/docs/README.md +9 -6
  4. package/docs/agents.md +24 -20
  5. package/docs/architecture.md +13 -5
  6. package/docs/charters/analyst.md +65 -0
  7. package/docs/charters/cmo.md +69 -0
  8. package/docs/charters/community.md +63 -0
  9. package/docs/charters/cto.md +71 -0
  10. package/docs/charters/designer.md +65 -0
  11. package/docs/charters/devops.md +65 -0
  12. package/docs/charters/pm.md +70 -0
  13. package/docs/charters/qa.md +65 -0
  14. package/docs/charters/support.md +60 -0
  15. package/docs/charters/writer.md +63 -0
  16. package/docs/commands.md +93 -7
  17. package/docs/concepts.md +61 -14
  18. package/docs/cost.md +36 -1
  19. package/docs/developing.md +16 -21
  20. package/docs/doctor-codes.md +10 -2
  21. package/docs/export.md +2 -0
  22. package/docs/extending.md +2 -2
  23. package/docs/getting-started.md +128 -78
  24. package/docs/images/brain.jpg +0 -0
  25. package/docs/images/org.jpg +0 -0
  26. package/docs/images/prompt.jpg +0 -0
  27. package/docs/images/setup-org.jpg +0 -0
  28. package/docs/images/setup-plan.jpg +0 -0
  29. package/docs/images/staff.jpg +0 -0
  30. package/docs/manual-steps.md +94 -123
  31. package/docs/memory.md +21 -3
  32. package/docs/org-yaml.md +40 -2
  33. package/docs/portal.md +165 -48
  34. package/docs/prompts.md +31 -4
  35. package/docs/security.md +37 -5
  36. package/docs/session-workflow.md +63 -17
  37. package/docs/staff-yaml.md +30 -3
  38. package/docs/troubleshooting.md +8 -8
  39. package/docs/upgrading.md +9 -3
  40. package/docs/writing-a-charter.md +28 -0
  41. package/package.json +18 -20
  42. package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +26 -1
  43. package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +41 -7
  44. package/templates/brain/CHARTER.md +3 -3
  45. package/templates/brain/README.md +1 -0
  46. package/templates/brain/log/decisions.md +3 -0
  47. package/templates/brain/staff.yaml +4 -1
  48. package/templates/brain/strategy/ideas.md +7 -0
  49. package/templates/briefs/priorities.md +46 -0
  50. package/templates/ops/.github/workflows/session.yaml +236 -15
  51. package/templates/ops/agents.mjs +7 -3
  52. package/templates/ops/compose.mjs +31 -3
  53. package/templates/ops/inflight.mjs +157 -0
  54. package/templates/ops/org/operating.md +43 -4
  55. package/templates/ops/org/voice.md +9 -0
  56. package/templates/ops/prompts/_inflight.md +14 -0
  57. package/templates/ops/prompts/_paths.md +2 -1
  58. package/templates/ops/prompts/daily.md +37 -9
  59. package/templates/ops/prompts/mention.md +21 -0
  60. package/templates/ops/run-record.mjs +146 -0
  61. package/templates/portal/css/base.css +167 -73
  62. package/templates/portal/css/brain.css +23 -20
  63. package/templates/portal/css/diff.css +10 -9
  64. package/templates/portal/css/graph.css +12 -7
  65. package/templates/portal/css/health.css +13 -11
  66. package/templates/portal/css/home.css +93 -0
  67. package/templates/portal/css/inbox.css +45 -25
  68. package/templates/portal/css/layout.css +90 -46
  69. package/templates/portal/css/markdown.css +36 -14
  70. package/templates/portal/css/runs.css +13 -0
  71. package/templates/portal/css/setup.css +116 -34
  72. package/templates/portal/index.html +21 -9
  73. package/templates/portal/js/api.js +44 -4
  74. package/templates/portal/js/app.js +94 -9
  75. package/templates/portal/js/dialog.js +83 -0
  76. package/templates/portal/js/homesort.js +174 -0
  77. package/templates/portal/js/icons.js +37 -0
  78. package/templates/portal/js/inflight.js +18 -0
  79. package/templates/portal/js/md.js +5 -2
  80. package/templates/portal/js/mdedit.js +84 -0
  81. package/templates/portal/js/readiness.js +70 -0
  82. package/templates/portal/js/refresh.js +10 -2
  83. package/templates/portal/js/state.js +11 -5
  84. package/templates/portal/js/views/app.js +24 -7
  85. package/templates/portal/js/views/brain.js +1 -1
  86. package/templates/portal/js/views/checklist.js +10 -4
  87. package/templates/portal/js/views/credential.js +84 -0
  88. package/templates/portal/js/views/graph.js +1 -1
  89. package/templates/portal/js/views/health.js +17 -4
  90. package/templates/portal/js/views/hire.js +593 -0
  91. package/templates/portal/js/views/home.js +546 -0
  92. package/templates/portal/js/views/inbox.js +226 -70
  93. package/templates/portal/js/views/org.js +46 -106
  94. package/templates/portal/js/views/orgedit.js +234 -0
  95. package/templates/portal/js/views/paste.js +87 -21
  96. package/templates/portal/js/views/prompt.js +11 -4
  97. package/templates/portal/js/views/repos.js +20 -15
  98. package/templates/portal/js/views/runonce.js +94 -0
  99. package/templates/portal/js/views/runs.js +170 -0
  100. package/templates/portal/js/views/setup.js +261 -75
  101. package/templates/portal/js/views/staff.js +170 -243
  102. package/templates/portal/js/views/todo.js +62 -0
@@ -5,19 +5,118 @@
5
5
  * is rather than as it was left. Setup cannot be finished in one sitting, so it must survive
6
6
  * being abandoned halfway. */
7
7
 
8
- import { checkOrg, createTenant, getSetup, joinOrg, planTenant } from "../api.js";
8
+ import {
9
+ checkOrg,
10
+ createTenant,
11
+ getAccess,
12
+ getOrg,
13
+ getSetup,
14
+ joinOrg,
15
+ planTenant,
16
+ setAccess,
17
+ } from "../api.js";
9
18
  import { $, el, esc, toClipboard } from "../dom.js";
19
+ import { go } from "../router.js";
20
+ import { S as App } from "../state.js";
21
+ import { checkAll, notReady, progress, sentence } from "../readiness.js";
10
22
  import { checklist } from "./checklist.js";
23
+ import { businessForm, prioritiesForm } from "./orgedit.js";
24
+ import { credentialPanel } from "./credential.js";
11
25
  import { repoPicker } from "./repos.js";
12
- import { paste } from "./paste.js";
26
+ import { todo } from "./todo.js";
13
27
 
14
28
  let S = null; // /api/setup/status, refreshed after anything that changes the world
15
29
 
30
+ /* Where the doctor checklist is drawn on this screen, so a save can re-run it. Left stale, it
31
+ went on saying business.stub after business.md had been saved, until "Check again". */
32
+ let healthHost = null;
33
+ let healthStep = null;
34
+
35
+ /* What the "What is left" cards already ask for. Doctor reporting them as well, right after a
36
+ successful create, read as the create having failed. */
37
+ const COVERED = ["business", "business.stub", "priorities", "priorities.stub", "actions-access", "secrets", "agent"];
38
+ const HEALTH = { skip: COVERED, hideWhenClean: true };
39
+
40
+ /* Each "what is left" step that is a file, by id, so a save can mark it done in place rather
41
+ than repainting the page and throwing away the diff somebody is still reading. */
42
+ const fileSteps = new Map();
43
+
16
44
  export async function viewSetup(main) {
17
45
  S = await getSetup();
18
46
  render(main);
19
47
  }
20
48
 
49
+ /**
50
+ * The same steps once a tenant exists, as their own screen in the portal.
51
+ *
52
+ * Setup used to be reachable only before there was a tenant, so the reload that followed
53
+ * creating one landed on an empty Inbox with the repo picker, the credential and both files
54
+ * gone. The sidebar offers this while anything is unfinished, and an org nobody has been hired
55
+ * into opens on it.
56
+ */
57
+ export async function viewGettingStarted(main) {
58
+ main.append(el("h1", { textContent: "Getting started" }));
59
+ const sub = el("p", { className: "sub", textContent: "Reading what is left…" });
60
+ main.append(sub);
61
+ S = await getSetup();
62
+ // Somebody may have clicked elsewhere while GitHub was answering.
63
+ if (App.view !== "setup") return;
64
+ await checkAll();
65
+ if (App.view !== "setup") return;
66
+ const p = progress();
67
+ sub.textContent = (p ? `${p.done} of ${p.total} done. ` : "") + "Work through these in order.";
68
+ const list = afterCreate();
69
+
70
+ // Somebody hired but not ready to run is the most important thing left, so it comes first.
71
+ for (const s of notReady().reverse()) {
72
+ const t = todo("!", "Finish setting up " + s.name, false);
73
+ t.hint("Still to do: " + sentence(App.readiness?.[s.handle] ?? []) + ".");
74
+ const cont = el("button", { className: "btn primary", textContent: "Continue" });
75
+ cont.onclick = () => {
76
+ App.staffOpen = { handle: s.handle, name: s.name, dir: s.dir };
77
+ go({ view: "staff" });
78
+ };
79
+ t.body.append(el("div", { className: "row" }, [cont]));
80
+ list.prepend(t.card);
81
+ }
82
+ main.append(list);
83
+
84
+ const health = step("!", "Other problems", false);
85
+ healthHost = el("div", { style: "margin-top:12px" });
86
+ health.append(healthHost);
87
+ main.append(health);
88
+ healthStep = health;
89
+ checklist(healthHost, { ...HEALTH, onEmpty: () => health.remove() });
90
+ }
91
+
92
+ /** Whether the sidebar offers Getting started: while anything is left, or while it is open. */
93
+ export function paintSetupNav() {
94
+ const nav = $("#setupnav");
95
+ if (!nav) return;
96
+ nav.hidden = !App.data?.unfinished?.length && !notReady().length && App.view !== "setup";
97
+ const count = $("#setupcount");
98
+ const p = progress();
99
+ if (count) count.textContent = p ? `${p.done}/${p.total}` : "";
100
+ }
101
+
102
+ /* After a save: the checklist and the step list are both derived from disk, so both are asked
103
+ again rather than updated by hand. The sidebar's list comes from /api/org, which is the one
104
+ the portal already reads. */
105
+ async function recheck() {
106
+ if (healthHost) checklist(healthHost, { ...HEALTH, onEmpty: () => healthStep?.remove() });
107
+ try {
108
+ S = await getSetup();
109
+ for (const [id, mark] of fileSteps) mark(!(S.tenant.left ?? []).includes(id));
110
+ } catch {
111
+ // The saved file is saved either way; a stale tick is not worth an error on top of it.
112
+ }
113
+ if (App.data) {
114
+ const fresh = await getOrg().catch(() => null);
115
+ if (fresh?.unfinished) App.data.unfinished = fresh.unfinished;
116
+ paintSetupNav();
117
+ }
118
+ }
119
+
21
120
  function render(main) {
22
121
  /* The sidebar is written by boot() before any of this is known. Once a tenant has been
23
122
  adopted, leaving it on "no org yet" contradicts the card directly below it. */
@@ -30,8 +129,7 @@ function render(main) {
30
129
  el("p", {
31
130
  className: "sub",
32
131
  textContent:
33
- "Four things here, then two that GitHub insists a human does. Nothing is created until " +
34
- "you have read the plan.",
132
+ "Choose an organisation and a coding agent. Nothing is created until you press Create it.",
35
133
  }),
36
134
  );
37
135
 
@@ -41,7 +139,11 @@ function render(main) {
41
139
 
42
140
  /* Drawn whenever a tenant exists, not only in the seconds after creating one. Setup takes
43
141
  days: the repo picker and the two manual steps have to still be here tomorrow. */
44
- if (S.tenant.found) main.append(afterCreate());
142
+ if (S.tenant.found) {
143
+ const rest = step(3, "Finish setting up", false);
144
+ rest.append(afterCreate());
145
+ main.append(rest);
146
+ }
45
147
  }
46
148
 
47
149
  /* ---------------------------------- 1 · gh ---------------------------------- */
@@ -57,8 +159,7 @@ function stepGh() {
57
159
  el("p", {
58
160
  className: "sub",
59
161
  textContent:
60
- "roster does everything through your own gh, so it holds no token of its own. Run this " +
61
- "in a terminal, then reload:",
162
+ "Run this in a terminal, then reload:",
62
163
  }),
63
164
  el("pre", { className: "cmd", textContent: "gh auth login" }),
64
165
  );
@@ -78,12 +179,10 @@ function stepOrg(main) {
78
179
  }),
79
180
  el("p", {
80
181
  className: "sub",
81
- textContent: "Reload the page to leave setup and use the portal.",
182
+ textContent:
183
+ "Finish the steps below. They stay under Getting started in the sidebar until they're done.",
82
184
  }),
83
185
  );
84
- const rows = el("div", { style: "margin-top:12px" });
85
- card.append(rows);
86
- checklist(rows, {});
87
186
  return card;
88
187
  }
89
188
 
@@ -113,8 +212,7 @@ function stepOrg(main) {
113
212
  orgWrap.append(
114
213
  el("small", {
115
214
  textContent:
116
- "Repos are created here. GitHub has no API for creating an organisation, so make one on " +
117
- "github.com first if you need to.",
215
+ "Don't have one? Create it on github.com first.",
118
216
  }),
119
217
  );
120
218
  form.append(orgWrap);
@@ -157,8 +255,8 @@ function stepOrg(main) {
157
255
  });
158
256
  other.input.addEventListener("change", look);
159
257
 
160
- const name = field("name", "What the business is called", "", "Shown to the agents. Defaults to the org.");
161
- const human = field("human", "Your GitHub login", S.gh.login ?? "", "The person the agents answer to.");
258
+ const name = field("name", "What the business is called", "", "Defaults to the organisation name.");
259
+ const human = field("human", "Your GitHub login", S.gh.login ?? "", "The person the staff report to.");
162
260
 
163
261
  card.append(form);
164
262
  const agentHead = el("h3", { textContent: "Which coding agent runs a session" });
@@ -171,7 +269,7 @@ function stepOrg(main) {
171
269
  const opt = el("label", { className: "choice" });
172
270
  const radio = el("input", { name: "agent", value: a.id, checked: i === 0 });
173
271
  radio.setAttribute("type", "radio");
174
- opt.append(radio, el("b", { textContent: a.label }), el("small", { textContent: a.note }));
272
+ opt.append(radio, el("b", { textContent: a.label }));
175
273
  opt.append(el("code", { textContent: a.tokenEnv }));
176
274
  agents.append(opt);
177
275
  });
@@ -179,8 +277,7 @@ function stepOrg(main) {
179
277
  const agentNote = el("p", {
180
278
  className: "sub",
181
279
  textContent:
182
- "Anything else works too: a runner is an install command, a run command and the name of " +
183
- "the secret holding its credential. Write those three into org.yaml afterwards.",
280
+ "Using a different agent? Pick any of these, then set it in org.yaml afterwards.",
184
281
  });
185
282
  card.append(agentNote);
186
283
 
@@ -256,23 +353,20 @@ function stepOrg(main) {
256
353
  out.replaceChildren(el("p", { className: "sub", textContent: "Working…" }));
257
354
  try {
258
355
  const result = await planTenant(p);
259
- out.replaceChildren();
260
- out.append(
261
- el("p", {
262
- textContent: `${result.files.length} files in ${result.dir}, and one private repo, ${p.org}/roster-ops.`,
263
- }),
264
- );
265
- const list = el("div", { className: "filelist" });
266
- for (const f of result.files) list.append(el("code", { textContent: "+ " + f }));
267
- out.append(list);
268
- out.append(
269
- el("p", {
270
- className: "sub",
271
- textContent:
272
- "org/business.md arrives as questions, not prose. Answering it is the next step and " +
273
- "it is the one that decides whether any of this is worth running.",
274
- }),
275
- );
356
+ const steps = el("ol", { className: "plansteps" }, [
357
+ el("li", { textContent: `Create a private repo, ${p.org}/roster-ops, to hold the org's shared settings.` }),
358
+ el("li", { textContent: `Put a copy of it in ${result.dir}.` }),
359
+ el("li", { textContent: `Let the other repos in ${p.org} run its workflow.` }),
360
+ ]);
361
+ const files = el("details", { className: "planfiles" }, [
362
+ el("summary", { textContent: `The ${result.files.length} files it writes` }),
363
+ el("div", { className: "filelist" }, result.files.map((f) => el("code", { textContent: f }))),
364
+ ]);
365
+ const next = el("p", {
366
+ className: "sub",
367
+ textContent: "After that: describe the business, set this month's priorities, and hire your first staff member.",
368
+ });
369
+ out.replaceChildren(el("p", { textContent: "Create it will:" }), steps, files, next);
276
370
  go.hidden = false;
277
371
  } catch (err) {
278
372
  out.replaceChildren(el("p", { className: "err", textContent: String(err.message || err) }));
@@ -303,53 +397,145 @@ function stepOrg(main) {
303
397
  /* ------------------------- 5 · the two GitHub insists on ------------------------- */
304
398
 
305
399
  function afterCreate() {
306
- const card = step(3, "What is left", false);
307
- card.append(
308
- el("p", {
309
- textContent:
310
- "None of these can be done for you, and the first fails in a way that wastes an " +
311
- "afternoon if you skip it.",
312
- }),
313
- );
314
-
315
- const one = el("div", { className: "manual" });
316
- one.append(
317
- el("b", { textContent: "Let the ops repo's workflow be called" }),
318
- el("p", {
319
- textContent:
320
- "Settings → Actions → General → Access → “Accessible from repositories in the " +
321
- "organisation”, on roster-ops.",
322
- }),
323
- el("small", {
324
- textContent:
325
- "Skip it and every workflow fails with “workflow not found”, which reads like a typo in " +
326
- "a path and is not one. It is an org permission on a repo, so it is yours to click.",
327
- }),
328
- );
329
- if (S.tenant.org) {
330
- const url = `https://github.com/${S.tenant.org}/roster-ops/settings/actions`;
331
- one.append(el("a", { className: "btn", href: url, target: "_blank", textContent: "Open that page" }));
332
- }
333
- card.append(one);
400
+ fileSteps.clear();
401
+ const list = el("div", { className: "todos" });
402
+ const left = S.tenant.left ?? [];
403
+ const staffCount = App.data?.staff?.length ?? 0;
404
+ let n = 0;
334
405
 
335
406
  if (S.tenant.org) {
336
- card.append(repoPicker({ org: S.tenant.org, known: S.tenant.repos ?? [] }));
407
+ const repos = todo(++n, "Choose the repos your staff work on", (S.tenant.products ?? []).length > 0);
408
+ repos.hint("Pick at least one. Staff open pull requests there, and you review them.");
409
+ repos.body.append(
410
+ repoPicker({ org: S.tenant.org, known: S.tenant.repos ?? [], bare: true, onAdded: () => repos.setDone(true) }),
411
+ );
412
+ list.append(repos.card);
337
413
  }
338
414
 
339
- const two = el("div", { className: "manual" });
340
- two.append(
341
- el("b", { textContent: "Answer org/business.md" }),
342
- el("p", {
343
- textContent:
344
- "It ships as questions. It is composed into the top of every prompt, every run, and an " +
345
- "agent that cannot answer them writes work that is plausible and generic.",
415
+ const business = todo(++n, "Describe the business", !left.includes("business"));
416
+ business.hint("A few short answers. Every staff member reads them before they start work.");
417
+ business.body.append(
418
+ businessForm({
419
+ path: opsName() + "/org/business.md",
420
+ name: App.data?.name ?? S.tenant.name ?? S.tenant.org,
421
+ onSaved: () => (business.setDone(true), recheck()),
346
422
  }),
347
423
  );
348
- card.append(two);
424
+ fileSteps.set("business", business.setDone);
425
+ list.append(business.card);
426
+
427
+ const priorities = todo(++n, "Set this month's priorities", !left.includes("priorities"));
428
+ priorities.hint("Up to three, in order. Staff pick work that serves them.");
429
+ priorities.body.append(
430
+ prioritiesForm({
431
+ path: opsName() + "/org/priorities.md",
432
+ onSaved: () => (priorities.setDone(true), recheck()),
433
+ }),
434
+ );
435
+ fileSteps.set("priorities", priorities.setDone);
436
+ list.append(priorities.card);
437
+
438
+ const hire = todo(++n, "Hire your first staff member", staffCount > 0);
439
+ hire.hint("Pick a role. The Staff screen then lists each step to set them up.");
440
+ const hireBtn = el("button", { className: "btn primary", textContent: "Hire someone" });
441
+ hireBtn.onclick = () => {
442
+ // Before the org has loaded, this screen is the whole app, so the Staff screen needs a reload.
443
+ if (App.data) go({ view: "staff" });
444
+ else location.assign("/#/-/staff") || location.reload();
445
+ };
446
+ hire.body.append(el("div", { className: "row" }, [hireBtn]));
447
+ list.append(hire.card);
349
448
 
350
- // The copy-a-prompt loop, which is the whole answer to "how do I write this file".
351
- card.append(paste({ kind: "discover", title: "Write it with your own AI" }));
352
- return card;
449
+ const cred = todo(++n, "Add your agent credential", false);
450
+ cred.hint(staffCount ? "Staff use it to run. You only add it once." : "Do this after your first hire.");
451
+ cred.body.append(credentialPanel({ bare: true, onStatus: (ok) => cred.setDone(ok) }));
452
+ list.append(cred.card);
453
+
454
+ const access = todo(++n, "Let staff repos use the shared workflow", false);
455
+ access.body.append(accessStep({ onStatus: (ok) => access.setDone(ok) }));
456
+ list.append(access.card);
457
+
458
+ return list;
459
+ }
460
+
461
+ /**
462
+ * One of the files only a person can write, with whether it is written yet.
463
+ *
464
+ * Read from the status rather than from the click that saved it, so a file written in an
465
+ * editor, or by a coding agent, shows as done here too.
466
+ */
467
+
468
+ /**
469
+ * org/priorities.md, edited in place.
470
+ *
471
+ * There is no brief for it, and it does not want one: business.md needs an interview because
472
+ * nobody can write it off the top of their head, while a ranked list of three things is
473
+ * quicker to type than to explain to a model. So the file opens as it is on disk, stub and
474
+ * all, and saving it is the same commit-and-push as every other editor here.
475
+ */
476
+
477
+
478
+
479
+
480
+
481
+
482
+ /** The ops repo's directory name, which is what every workspace-relative path starts with. */
483
+ function opsName() {
484
+ return App.data?.opsName ?? String(S.tenant.opsDir ?? "roster-ops").split("/").pop();
485
+ }
486
+
487
+ /**
488
+ * The ops repo's Actions access. Read on arrival, so a page reloaded after it was set says it
489
+ * is done without anybody pressing anything; the button asks the server to set it, and a
490
+ * refusal comes back with GitHub's reason and the page to click instead.
491
+ *
492
+ * Skipped, every workflow fails with "workflow not found", which reads like a typo in a path.
493
+ */
494
+ function accessStep(opts = {}) {
495
+ const box = el("div");
496
+ const out = el("div");
497
+ box.append(out);
498
+ const say = (text, cls) => el("p", { className: cls ?? "sub", textContent: text });
499
+
500
+ const offer = (why) => {
501
+ const go = el("button", { className: "btn primary", textContent: "Set it for me" });
502
+ out.replaceChildren(
503
+ say(
504
+ (why ? why + " " : "") +
505
+ "Roster tried to set this when it created the org, and GitHub didn't allow it.",
506
+ ),
507
+ el("div", { className: "row" }, [go]),
508
+ );
509
+ go.onclick = async () => {
510
+ go.disabled = true;
511
+ try {
512
+ const r = await setAccess();
513
+ if (r.ok) {
514
+ out.replaceChildren(say("Done."));
515
+ opts.onStatus?.(true);
516
+ return;
517
+ }
518
+ out.replaceChildren(
519
+ say("GitHub refused: " + r.error + ".", "err"),
520
+ el("a", { className: "btn", href: r.link, target: "_blank", textContent: "Set it by hand" }),
521
+ say("Access → “Accessible from repositories in the organisation”."),
522
+ );
523
+ } catch (err) {
524
+ out.replaceChildren(say(String(err.message || err), "err"));
525
+ go.disabled = false;
526
+ }
527
+ };
528
+ };
529
+
530
+ out.replaceChildren(say("Checking…"));
531
+ getAccess()
532
+ .then((r) => {
533
+ opts.onStatus?.(Boolean(r.ok));
534
+ if (r.ok) out.replaceChildren(say("Done."));
535
+ else offer(r.level ? "It is set to “" + r.level + "”." : "");
536
+ })
537
+ .catch(() => offer(""));
538
+ return box;
353
539
  }
354
540
 
355
541
  /* ---------------------------------- furniture ---------------------------------- */