@nanocollective/roster 0.1.0-alpha.25 → 0.1.0-alpha.27
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.
- package/dist/cli.js +78 -18
- package/docs/charters/analyst.md +65 -0
- package/docs/charters/community.md +63 -0
- package/docs/charters/designer.md +65 -0
- package/docs/charters/devops.md +65 -0
- package/docs/charters/pm.md +70 -0
- package/docs/charters/qa.md +65 -0
- package/docs/charters/writer.md +63 -0
- package/docs/commands.md +1 -1
- package/docs/writing-a-charter.md +19 -6
- package/package.json +1 -1
- package/templates/portal/css/base.css +9 -0
- package/templates/portal/css/layout.css +9 -0
- package/templates/portal/css/setup.css +1 -0
- package/templates/portal/js/app.js +10 -0
- package/templates/portal/js/dialog.js +35 -0
- package/templates/portal/js/readiness.js +35 -0
- package/templates/portal/js/state.js +2 -0
- package/templates/portal/js/views/hire.js +73 -16
- package/templates/portal/js/views/paste.js +13 -1
- package/templates/portal/js/views/setup.js +19 -2
- package/templates/portal/js/views/staff.js +74 -91
|
@@ -28,10 +28,10 @@ layer. That is the part that matters most, because without them the model writes
|
|
|
28
28
|
of whoever it was shown. The brief then interviews you, drafts from your answers, and tells you
|
|
29
29
|
what it cut and why.
|
|
30
30
|
|
|
31
|
-
**So is a worked example, when one fits.** A staff member whose handle or role reads as
|
|
32
|
-
|
|
31
|
+
**So is a worked example, when one fits.** A staff member whose handle or role reads as one of
|
|
32
|
+
the ten roles below gets the matching [example](#worked-examples) inside the brief, labelled as a
|
|
33
33
|
model for the shape and not content to copy. The copy-a-prompt panel has a picker to choose
|
|
34
|
-
another or none; in a terminal it is `--example
|
|
34
|
+
another or none; in a terminal it is `--example <handle>` or `--example none`. It is still a brief you
|
|
35
35
|
answer: the interview comes first, and nothing in the charter should come from the example
|
|
36
36
|
rather than from you.
|
|
37
37
|
|
|
@@ -72,9 +72,22 @@ status issue, and the surfaces the manifest declares.
|
|
|
72
72
|
|
|
73
73
|
## Worked examples
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
Ten, for an invented company called Acme. The handle in brackets is the one `--example` takes.
|
|
76
|
+
|
|
77
|
+
| Example | What they do |
|
|
78
|
+
|---|---|
|
|
79
|
+
| [CTO](charters/cto.md) (`cto`) | Builds the product: fixes, features and tests, as pull requests. |
|
|
80
|
+
| [CMO](charters/cmo.md) (`cmo`) | Posts, SEO, copy and launch plans, as drafts to approve. |
|
|
81
|
+
| [Head of Support](charters/support.md) (`support`) | Answers issues, writes help docs, and turns user reports into bugs. |
|
|
82
|
+
| [Product Manager](charters/pm.md) (`pm`) | Turns ideas and user feedback into clear specs and a ranked backlog. |
|
|
83
|
+
| [Designer](charters/designer.md) (`designer`) | Improves the product's look and usability, with accessibility fixes, as pull requests. |
|
|
84
|
+
| [QA Engineer](charters/qa.md) (`qa`) | Tests the product, finds bugs, and writes clear reproductions and tests. |
|
|
85
|
+
| [DevOps Engineer](charters/devops.md) (`devops`) | Keeps CI, deploys and dependencies healthy, and patches security updates. |
|
|
86
|
+
| [Technical Writer](charters/writer.md) (`writer`) | Writes and keeps up the docs, guides and changelog. |
|
|
87
|
+
| [Data Analyst](charters/analyst.md) (`analyst`) | Reads the numbers and writes a short weekly report on what changed. |
|
|
88
|
+
| [Community Manager](charters/community.md) (`community`) | Answers discussions, welcomes contributors, and drafts release announcements. |
|
|
89
|
+
|
|
90
|
+
They are examples to adapt, not templates to fill in. Read them for what a finished charter covers and how specific it gets, then write your own
|
|
78
91
|
about your business. A charter copied from one of these describes Acme. The brief carries the matching one for
|
|
79
92
|
you; these links are for reading them first.
|
|
80
93
|
|
package/package.json
CHANGED
|
@@ -276,3 +276,12 @@ body[data-lightbox] {overflow:hidden}
|
|
|
276
276
|
|
|
277
277
|
/* And the affordance on the document side: an image you can open should say so. */
|
|
278
278
|
.md img,img.zoomable{cursor:zoom-in}
|
|
279
|
+
|
|
280
|
+
/* ---------- a dialog for a whole task (sheet() in dialog.js) ---------- */
|
|
281
|
+
dialog.sheet{border:1px solid var(--border-strong);border-radius:var(--r-lg);background:var(--bg);
|
|
282
|
+
color:var(--ink);padding:0;width:min(860px,calc(100vw - 32px));max-height:calc(100vh - 48px);
|
|
283
|
+
box-shadow:var(--shadow-lg)}
|
|
284
|
+
dialog.sheet[open]{animation:dlgin .18s var(--ease)}
|
|
285
|
+
dialog.sheet::backdrop{background:rgba(0,0,0,.35);backdrop-filter:blur(8px)}
|
|
286
|
+
.sheetbody{padding:22px 24px 26px;overflow:auto;max-height:calc(100vh - 50px)}
|
|
287
|
+
.sheettitle{margin:0 0 14px;font:600 18px var(--sans);letter-spacing:-.02em}
|
|
@@ -148,3 +148,12 @@ main{padding:36px 44px 96px;max-width:1220px;overflow-x:hidden}
|
|
|
148
148
|
.side{position:static;height:auto;border-right:none;border-bottom:1px solid var(--line)}
|
|
149
149
|
.sidefoot{margin-top:0}
|
|
150
150
|
}
|
|
151
|
+
|
|
152
|
+
/* A staff member who cannot run yet (readiness.js). */
|
|
153
|
+
.readydot{width:7px;height:7px;border-radius:50%;background:var(--warn);flex:none;margin-left:4px}
|
|
154
|
+
.staffcard.notready{border-color:color-mix(in srgb,var(--warn) 45%,var(--line))}
|
|
155
|
+
.hirecard{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;min-height:120px;
|
|
156
|
+
border:1.5px dashed var(--border-strong);border-radius:var(--r-lg);background:none;color:var(--ink-dim);
|
|
157
|
+
font:600 14px var(--sans);cursor:pointer}
|
|
158
|
+
.hirecard:hover{color:var(--ink);border-color:var(--accent)}
|
|
159
|
+
.hirecard::before{content:"+";font-size:22px;line-height:1}
|
|
@@ -182,3 +182,4 @@
|
|
|
182
182
|
.pastenotes summary{cursor:pointer;font:600 12.5px var(--sans);color:var(--ink-dim);margin-bottom:6px}
|
|
183
183
|
.pastenotes .md{border-left:3px solid var(--line);padding-left:12px}
|
|
184
184
|
.pastefile .diff{white-space:pre-wrap;overflow-wrap:anywhere}
|
|
185
|
+
.warnline{margin:10px 0 0;padding:8px 11px;border-radius:var(--r);background:color-mix(in srgb,var(--warn) 12%,transparent);color:var(--ink);font-size:13px}
|
|
@@ -24,6 +24,7 @@ import { viewInbox, viewPrs } from "./views/inbox.js";
|
|
|
24
24
|
import { viewOrg } from "./views/org.js";
|
|
25
25
|
import { viewPrompt } from "./views/prompt.js";
|
|
26
26
|
import { viewRuns } from "./views/runs.js";
|
|
27
|
+
import { sentence, whatsLeft } from "./readiness.js";
|
|
27
28
|
import { paintSetupNav, viewGettingStarted, viewSetup } from "./views/setup.js";
|
|
28
29
|
import { viewStaff } from "./views/staff.js";
|
|
29
30
|
|
|
@@ -112,6 +113,7 @@ export async function boot() {
|
|
|
112
113
|
S.data = first;
|
|
113
114
|
S.loadedAt = new Date();
|
|
114
115
|
S.staffOpen = null; // an open hire list survives a refresh, not a page load
|
|
116
|
+
S.readiness = null;
|
|
115
117
|
S.staffHandle = S.data.staff[0]?.handle ?? null;
|
|
116
118
|
$("#orgname").textContent = S.data.name + " · " + S.data.staff.length + " staff";
|
|
117
119
|
|
|
@@ -262,11 +264,19 @@ function paintSidebar() {
|
|
|
262
264
|
const head = el("button", { className: "nav staffrow" });
|
|
263
265
|
head.dataset.staff = s.handle;
|
|
264
266
|
head.setAttribute("aria-expanded", String(open));
|
|
267
|
+
const dot = el("span", { className: "readydot", hidden: true });
|
|
265
268
|
head.append(
|
|
266
269
|
icon("chevron", "caret"),
|
|
267
270
|
el("span", { className: "t", textContent: s.name }),
|
|
271
|
+
dot,
|
|
268
272
|
el("span", { className: "hh", textContent: s.handle }),
|
|
269
273
|
);
|
|
274
|
+
whatsLeft(s).then((left) => {
|
|
275
|
+
if (!left.length) return;
|
|
276
|
+
dot.hidden = false;
|
|
277
|
+
head.title = "Not ready to run: " + sentence(left);
|
|
278
|
+
paintSetupNav();
|
|
279
|
+
});
|
|
270
280
|
|
|
271
281
|
const views = el("div", { className: "staffviews" });
|
|
272
282
|
views.hidden = !open;
|
|
@@ -158,3 +158,38 @@ export function askYes({ title, hint, confirm = "Continue" }) {
|
|
|
158
158
|
cancel.focus();
|
|
159
159
|
});
|
|
160
160
|
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* A larger dialog for a whole task: hiring someone, or finishing their setup. Native <dialog>
|
|
164
|
+
* again, appended to <body> rather than to the screen, so a background refresh repainting the
|
|
165
|
+
* screen leaves it open with whatever was typed into it.
|
|
166
|
+
*
|
|
167
|
+
* @returns {{set: (node: Node) => void, close: () => void, isOpen: () => boolean} | null}
|
|
168
|
+
* null where there is no dialog element (the test shim), and the caller draws inline instead.
|
|
169
|
+
*/
|
|
170
|
+
export function sheet({ node, onClose }) {
|
|
171
|
+
const box = document.createElement("dialog");
|
|
172
|
+
if (typeof box.showModal !== "function") return null;
|
|
173
|
+
box.className = "sheet";
|
|
174
|
+
const body = el("div", { className: "sheetbody" });
|
|
175
|
+
body.append(node);
|
|
176
|
+
box.append(body);
|
|
177
|
+
let open = true;
|
|
178
|
+
const close = () => {
|
|
179
|
+
if (!open) return;
|
|
180
|
+
open = false;
|
|
181
|
+
box.close();
|
|
182
|
+
box.remove();
|
|
183
|
+
onClose?.();
|
|
184
|
+
};
|
|
185
|
+
box.addEventListener("cancel", (e) => {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
close();
|
|
188
|
+
});
|
|
189
|
+
box.addEventListener("click", (e) => {
|
|
190
|
+
if (onBackdrop(e, box.getBoundingClientRect())) close();
|
|
191
|
+
});
|
|
192
|
+
document.body.append(box);
|
|
193
|
+
box.showModal();
|
|
194
|
+
return { set: (n) => body.replaceChildren(n), close, isOpen: () => open };
|
|
195
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* Whether each staff member can actually run yet.
|
|
2
|
+
*
|
|
3
|
+
* A hire that had its App created but not installed, and no credential, looked finished: the
|
|
4
|
+
* card said so and Getting started went away. This is the one answer every screen asks. */
|
|
5
|
+
|
|
6
|
+
import { getStaffProgress } from "./api.js";
|
|
7
|
+
import { S } from "./state.js";
|
|
8
|
+
|
|
9
|
+
/** What is left for one staff member, as short instructions. Empty when they are ready. */
|
|
10
|
+
export async function whatsLeft(s, fresh = false) {
|
|
11
|
+
const left = [];
|
|
12
|
+
if (s.rig?.charterStub) left.push("write the charter");
|
|
13
|
+
const p = await getStaffProgress(s.handle, fresh).catch(() => ({}));
|
|
14
|
+
if (p.installed === false) left.push(p.app ? "install the GitHub App" : "create the GitHub App");
|
|
15
|
+
if (p.credential === false) left.push("add the agent credential");
|
|
16
|
+
if (p.ran === false) left.push("run once");
|
|
17
|
+
S.readiness = { ...(S.readiness ?? {}), [s.handle]: left };
|
|
18
|
+
return left;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Every staff member's, then `then` once all have answered. */
|
|
22
|
+
export function checkAll(then) {
|
|
23
|
+
return Promise.all((S.data?.staff ?? []).map((s) => whatsLeft(s))).then(() => then?.());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The staff with anything left, from what has been checked so far. */
|
|
27
|
+
export function notReady() {
|
|
28
|
+
return (S.data?.staff ?? []).filter((s) => (S.readiness?.[s.handle] ?? []).length);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** "install the GitHub App, add the agent credential and run once" */
|
|
32
|
+
export function sentence(left) {
|
|
33
|
+
if (left.length < 2) return left.join("");
|
|
34
|
+
return left.slice(0, -1).join(", ") + " and " + left.at(-1);
|
|
35
|
+
}
|
|
@@ -38,6 +38,8 @@ export const S = {
|
|
|
38
38
|
loadedAt: null,
|
|
39
39
|
/** The role whose hire list is open on the Staff screen, kept across a refresh. */
|
|
40
40
|
staffOpen: null,
|
|
41
|
+
/** Per staff member, what is left before they can run. See readiness.js. */
|
|
42
|
+
readiness: null,
|
|
41
43
|
|
|
42
44
|
staffHandle: null,
|
|
43
45
|
/* What is waiting on you, not whose brain you read last. The org-wide screens are where a
|
|
@@ -39,10 +39,52 @@ const ROLES = [
|
|
|
39
39
|
name: "Head of Support",
|
|
40
40
|
about: "Answers issues, writes help docs, and turns user reports into bugs.",
|
|
41
41
|
},
|
|
42
|
+
{
|
|
43
|
+
handle: "pm",
|
|
44
|
+
label: "Product manager",
|
|
45
|
+
name: "Product Manager",
|
|
46
|
+
about: "Turns ideas and user feedback into clear specs and a ranked backlog.",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
handle: "designer",
|
|
50
|
+
label: "Designer",
|
|
51
|
+
name: "Designer",
|
|
52
|
+
about: "Improves the product's look and usability, with accessibility fixes, as pull requests.",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
handle: "qa",
|
|
56
|
+
label: "QA",
|
|
57
|
+
name: "QA Engineer",
|
|
58
|
+
about: "Tests the product, finds bugs, and writes clear reproductions and tests.",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
handle: "devops",
|
|
62
|
+
label: "DevOps",
|
|
63
|
+
name: "DevOps Engineer",
|
|
64
|
+
about: "Keeps CI, deploys and dependencies healthy, and patches security updates.",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
handle: "writer",
|
|
68
|
+
label: "Writer",
|
|
69
|
+
name: "Technical Writer",
|
|
70
|
+
about: "Writes and keeps up the docs, guides and changelog.",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
handle: "analyst",
|
|
74
|
+
label: "Analyst",
|
|
75
|
+
name: "Data Analyst",
|
|
76
|
+
about: "Reads the numbers and writes a short weekly report on what changed.",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
handle: "community",
|
|
80
|
+
label: "Community",
|
|
81
|
+
name: "Community Manager",
|
|
82
|
+
about: "Answers discussions, welcomes contributors, and drafts release announcements.",
|
|
83
|
+
},
|
|
42
84
|
];
|
|
43
85
|
|
|
44
86
|
/**
|
|
45
|
-
* The roles as cards.
|
|
87
|
+
* The roles as cards. Each has a worked example, and is offered until it is hired;
|
|
46
88
|
* Something else asks for a name and a sentence.
|
|
47
89
|
* @param {{onPick: (role: {handle: string, name: string, dir: string, about?: string}) => void}} o
|
|
48
90
|
*/
|
|
@@ -141,7 +183,7 @@ export function hireFlow(o) {
|
|
|
141
183
|
box.append(list);
|
|
142
184
|
|
|
143
185
|
const hire = todo(1, "Hire", hired);
|
|
144
|
-
const app = todo(2, "Create their GitHub App", false);
|
|
186
|
+
const app = todo(2, "Create and install their GitHub App", false);
|
|
145
187
|
const charter = todo(3, "Write their charter", hired && !s.rig?.charterStub);
|
|
146
188
|
const cred = todo(4, "Add your agent credential", false);
|
|
147
189
|
const run = todo(5, "Run once now", false);
|
|
@@ -182,14 +224,17 @@ export function hireFlow(o) {
|
|
|
182
224
|
|
|
183
225
|
/* 2 · the App. The public identity only matters when a product repo is public, and hire
|
|
184
226
|
treats one with no visibility written as public, so this does too. */
|
|
227
|
+
/* Done means installed, not created: an App with its keys saved and no install looked
|
|
228
|
+
finished, and every run would have failed. */
|
|
185
229
|
let needPublic = false;
|
|
186
230
|
const made = { private: false, public: false };
|
|
187
231
|
const appDone = () => app.setDone(made.private && (!needPublic || made.public));
|
|
188
232
|
const panels = el("div");
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
);
|
|
233
|
+
const installNote = el("p", { className: "warnline", hidden: true });
|
|
234
|
+
const recheck = el("button", { className: "ghbtn", textContent: "Check again" });
|
|
235
|
+
recheck.onclick = () => checkProgress();
|
|
236
|
+
app.body.append(panels, installNote, el("div", { className: "row" }, [recheck]));
|
|
237
|
+
panels.append(appPanel({ staff: s.handle, name: s.name, scope: "private", onDone: () => checkProgress() }));
|
|
193
238
|
getRepos()
|
|
194
239
|
.then((r) => {
|
|
195
240
|
needPublic = (r.repos ?? []).some(
|
|
@@ -198,7 +243,7 @@ export function hireFlow(o) {
|
|
|
198
243
|
if (needPublic) {
|
|
199
244
|
app.hint("One of your product repos is public. Public work uses a second App, shared by all staff.");
|
|
200
245
|
panels.append(
|
|
201
|
-
appPanel({ staff: s.handle, name: s.name, scope: "public", onDone: () => (
|
|
246
|
+
appPanel({ staff: s.handle, name: s.name, scope: "public", onDone: () => checkProgress() }),
|
|
202
247
|
);
|
|
203
248
|
}
|
|
204
249
|
appDone();
|
|
@@ -225,15 +270,27 @@ export function hireFlow(o) {
|
|
|
225
270
|
}),
|
|
226
271
|
);
|
|
227
272
|
|
|
228
|
-
/* What only GitHub knows: the App
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
273
|
+
/* What only GitHub knows: whether the App is installed, whether the credential is stored,
|
|
274
|
+
and whether a run has succeeded. Asked again by Check again, after a trip to GitHub. */
|
|
275
|
+
function checkProgress() {
|
|
276
|
+
return getStaffProgress(s.handle, true)
|
|
277
|
+
.then((p) => {
|
|
278
|
+
made.private = p.installed === true;
|
|
279
|
+
made.public = p.publicInstalled === true;
|
|
280
|
+
appDone();
|
|
281
|
+
installNote.hidden = !(p.app && p.installed === false);
|
|
282
|
+
installNote.textContent =
|
|
283
|
+
"Created, but not installed yet. Press Install, and choose All repositories or include roster-ops.";
|
|
284
|
+
if (p.installed && p.installSelection === "selected") {
|
|
285
|
+
installNote.hidden = false;
|
|
286
|
+
installNote.textContent =
|
|
287
|
+
"Installed on selected repos. Make sure roster-ops is one of them, or runs will fail.";
|
|
288
|
+
}
|
|
289
|
+
if (p.ran) run.setDone(true);
|
|
290
|
+
})
|
|
291
|
+
.catch(() => {});
|
|
292
|
+
}
|
|
293
|
+
checkProgress();
|
|
237
294
|
|
|
238
295
|
return box;
|
|
239
296
|
}
|
|
@@ -49,7 +49,19 @@ export function paste(opts) {
|
|
|
49
49
|
|
|
50
50
|
let brief = null;
|
|
51
51
|
|
|
52
|
-
const NAMES = {
|
|
52
|
+
const NAMES = {
|
|
53
|
+
cto: "the CTO example",
|
|
54
|
+
cmo: "the CMO example",
|
|
55
|
+
support: "the support example",
|
|
56
|
+
pm: "the product manager example",
|
|
57
|
+
designer: "the designer example",
|
|
58
|
+
qa: "the QA example",
|
|
59
|
+
devops: "the DevOps example",
|
|
60
|
+
writer: "the writer example",
|
|
61
|
+
analyst: "the analyst example",
|
|
62
|
+
community: "the community example",
|
|
63
|
+
none: "no example",
|
|
64
|
+
};
|
|
53
65
|
model.onchange = () => {
|
|
54
66
|
copy.disabled = true;
|
|
55
67
|
getBrief(opts.kind, opts.staff, model.value, opts.about)
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { $, el, esc, toClipboard } from "../dom.js";
|
|
19
19
|
import { go } from "../router.js";
|
|
20
20
|
import { S as App } from "../state.js";
|
|
21
|
+
import { checkAll, notReady, sentence } from "../readiness.js";
|
|
21
22
|
import { checklist } from "./checklist.js";
|
|
22
23
|
import { businessForm, prioritiesForm } from "./orgedit.js";
|
|
23
24
|
import { credentialPanel } from "./credential.js";
|
|
@@ -60,8 +61,24 @@ export async function viewGettingStarted(main) {
|
|
|
60
61
|
S = await getSetup();
|
|
61
62
|
// Somebody may have clicked elsewhere while GitHub was answering.
|
|
62
63
|
if (App.view !== "setup") return;
|
|
64
|
+
await checkAll();
|
|
65
|
+
if (App.view !== "setup") return;
|
|
63
66
|
sub.textContent = "Work through these in order.";
|
|
64
|
-
|
|
67
|
+
const list = afterCreate();
|
|
68
|
+
|
|
69
|
+
// Somebody hired but not ready to run is the most important thing left, so it comes first.
|
|
70
|
+
for (const s of notReady().reverse()) {
|
|
71
|
+
const t = todo("!", "Finish setting up " + s.name, false);
|
|
72
|
+
t.hint("Still to do: " + sentence(App.readiness?.[s.handle] ?? []) + ".");
|
|
73
|
+
const cont = el("button", { className: "btn primary", textContent: "Continue" });
|
|
74
|
+
cont.onclick = () => {
|
|
75
|
+
App.staffOpen = { handle: s.handle, name: s.name, dir: s.dir };
|
|
76
|
+
go({ view: "staff" });
|
|
77
|
+
};
|
|
78
|
+
t.body.append(el("div", { className: "row" }, [cont]));
|
|
79
|
+
list.prepend(t.card);
|
|
80
|
+
}
|
|
81
|
+
main.append(list);
|
|
65
82
|
|
|
66
83
|
const health = step("!", "Other problems", false);
|
|
67
84
|
healthHost = el("div", { style: "margin-top:12px" });
|
|
@@ -75,7 +92,7 @@ export async function viewGettingStarted(main) {
|
|
|
75
92
|
export function paintSetupNav() {
|
|
76
93
|
const nav = $("#setupnav");
|
|
77
94
|
if (!nav) return;
|
|
78
|
-
nav.hidden = !App.data?.unfinished?.length && App.view !== "setup";
|
|
95
|
+
nav.hidden = !App.data?.unfinished?.length && !notReady().length && App.view !== "setup";
|
|
79
96
|
}
|
|
80
97
|
|
|
81
98
|
/* After a save: the checklist and the step list are both derived from disk, so both are asked
|
|
@@ -5,16 +5,17 @@
|
|
|
5
5
|
* and nothing happens until you say so.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import { askYes } from "../dialog.js";
|
|
8
|
+
import { post } from "../api.js";
|
|
9
|
+
import { askYes, sheet } from "../dialog.js";
|
|
10
10
|
import { ago, el, esc } from "../dom.js";
|
|
11
|
+
import { whatsLeft, sentence } from "../readiness.js";
|
|
11
12
|
import { refreshAll } from "../refresh.js";
|
|
12
13
|
import { S } from "../state.js";
|
|
13
|
-
import { appPanel } from "./app.js";
|
|
14
|
-
import { credentialPanel } from "./credential.js";
|
|
15
14
|
import { hireFlow, line, rolePicker } from "./hire.js";
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
|
|
16
|
+
/* The open dialog, if any. It lives on <body>, so it outlives a repaint of this screen, and a
|
|
17
|
+
background refresh leaves whatever was typed into it alone. */
|
|
18
|
+
let current = null;
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* @param {{open?: string, result?: object}} [o] `open` keeps one staff member's steps on
|
|
@@ -27,24 +28,52 @@ export function viewStaff(m, o = {}) {
|
|
|
27
28
|
|
|
28
29
|
const pane = el("div", { className: "hirepane" });
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const cancel = el("button", { className: "ghbtn", textContent: "Cancel" });
|
|
37
|
-
cancel.onclick = () => pane.replaceChildren();
|
|
38
|
-
pane.append(el("div", { className: "row" }, [cancel]));
|
|
31
|
+
/* Tasks open in a dialog. Where there is no dialog element (the test shim) they draw into
|
|
32
|
+
the pane instead, which is how this screen always drew them. */
|
|
33
|
+
function present(node) {
|
|
34
|
+
if (current?.isOpen()) {
|
|
35
|
+
current.set(node);
|
|
36
|
+
return;
|
|
39
37
|
}
|
|
40
|
-
|
|
38
|
+
current = sheet({
|
|
39
|
+
node,
|
|
40
|
+
onClose: () => {
|
|
41
|
+
current = null;
|
|
42
|
+
S.staffOpen = null;
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
if (!current) pane.replaceChildren(node);
|
|
46
|
+
}
|
|
47
|
+
function dismiss() {
|
|
48
|
+
if (current) current.close();
|
|
49
|
+
else pane.replaceChildren();
|
|
50
|
+
S.staffOpen = null;
|
|
51
|
+
if (empty) pick();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// With nobody hired the roles are the page. After that they are a dialog behind Hire someone.
|
|
55
|
+
function pick() {
|
|
56
|
+
if (empty) {
|
|
57
|
+
pane.replaceChildren(rolePicker({ onPick: (role) => open(role) }));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const cancel = el("button", { className: "ghbtn", textContent: "Cancel" });
|
|
61
|
+
cancel.onclick = dismiss;
|
|
62
|
+
present(
|
|
63
|
+
el("div", {}, [
|
|
64
|
+
el("h2", { className: "sheettitle", textContent: "Pick a role" }),
|
|
65
|
+
rolePicker({ onPick: (role) => open(role) }),
|
|
66
|
+
el("div", { className: "row", style: "margin-top:14px" }, [cancel]),
|
|
67
|
+
]),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
41
70
|
|
|
42
71
|
/* One staff member's steps, hired or not yet. A hire repaints the whole screen, so the new
|
|
43
72
|
card is in the list and the steps reopen on the same person. */
|
|
44
73
|
function open(role, result) {
|
|
45
74
|
// Remembered, so a refresh after a trip to GitHub (creating the App) reopens it.
|
|
46
75
|
S.staffOpen = role;
|
|
47
|
-
|
|
76
|
+
present(
|
|
48
77
|
hireFlow({
|
|
49
78
|
role,
|
|
50
79
|
result,
|
|
@@ -52,32 +81,29 @@ export function viewStaff(m, o = {}) {
|
|
|
52
81
|
m.replaceChildren();
|
|
53
82
|
viewStaff(m, { open: handle, result: r });
|
|
54
83
|
},
|
|
55
|
-
onClose:
|
|
56
|
-
S.staffOpen = null;
|
|
57
|
-
if (empty) pick();
|
|
58
|
-
else pane.replaceChildren();
|
|
59
|
-
},
|
|
84
|
+
onClose: dismiss,
|
|
60
85
|
}),
|
|
61
86
|
);
|
|
62
|
-
pane.scrollIntoView?.({ behavior: "smooth", block: "start" });
|
|
63
87
|
}
|
|
64
88
|
|
|
65
89
|
if (!empty) {
|
|
66
90
|
const list = el("div", { className: "grid", style: "margin-bottom:18px" });
|
|
67
91
|
for (const s of S.data.staff) list.append(card(s));
|
|
68
|
-
|
|
69
|
-
const hire = el("button", { className: "
|
|
92
|
+
// In the grid, where the next person would go, rather than a button under it.
|
|
93
|
+
const hire = el("button", { className: "hirecard", textContent: "Hire someone" });
|
|
70
94
|
hire.onclick = pick;
|
|
71
|
-
|
|
95
|
+
list.append(hire);
|
|
96
|
+
m.append(list);
|
|
72
97
|
}
|
|
73
98
|
m.append(pane);
|
|
74
99
|
|
|
75
100
|
const opened = o.open && S.data.staff.find((s) => s.handle === o.open);
|
|
76
101
|
const kept = !opened && S.staffOpen;
|
|
77
102
|
const keptStaff = kept && S.data.staff.find((s) => s.handle === kept.handle);
|
|
103
|
+
if (empty) pick();
|
|
78
104
|
if (opened) open(roleOf(opened), o.result);
|
|
79
|
-
|
|
80
|
-
else if (
|
|
105
|
+
// An open dialog is still open; only the inline fallback has to be drawn again.
|
|
106
|
+
else if (kept && !current?.isOpen()) open(keptStaff ? roleOf(keptStaff) : kept);
|
|
81
107
|
|
|
82
108
|
/* --------------------------- one staff member --------------------------- */
|
|
83
109
|
|
|
@@ -93,73 +119,30 @@ export function viewStaff(m, o = {}) {
|
|
|
93
119
|
|
|
94
120
|
const status = el("span", { className: "meta" });
|
|
95
121
|
const go = el("button", { className: "ghbtn", textContent: "Retire" });
|
|
96
|
-
go.onclick = () =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
it refers to already inside it and somewhere to put the answer. */
|
|
101
|
-
const write = el("button", { className: "ghbtn", textContent: "Write the charter" });
|
|
102
|
-
write.onclick = () => {
|
|
103
|
-
pane.replaceChildren(
|
|
104
|
-
el("div", { className: "card" }, [
|
|
105
|
-
paste({
|
|
106
|
-
kind: "charter",
|
|
107
|
-
staff: s.handle,
|
|
108
|
-
title: "Write " + s.name + "'s charter with your own AI",
|
|
109
|
-
onSaved: () => refreshAll(false),
|
|
110
|
-
}),
|
|
111
|
-
]),
|
|
112
|
-
);
|
|
113
|
-
pane.scrollIntoView?.({ behavior: "smooth", block: "start" });
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
/* The step `hire` has always had to hand back: there is no API that creates a GitHub App,
|
|
117
|
-
so it is a browser hand-off either way. It may as well be this browser. */
|
|
118
|
-
const app = el("button", { className: "ghbtn", textContent: "GitHub App" });
|
|
119
|
-
app.onclick = () => {
|
|
120
|
-
pane.replaceChildren(
|
|
121
|
-
el("div", { className: "card" }, [
|
|
122
|
-
el("h3", { textContent: s.name + "'s identity" }),
|
|
123
|
-
appPanel({ staff: s.handle, name: s.name, scope: "private" }),
|
|
124
|
-
appPanel({ staff: s.handle, name: s.name, scope: "public" }),
|
|
125
|
-
]),
|
|
126
|
-
);
|
|
127
|
-
pane.scrollIntoView?.({ behavior: "smooth", block: "start" });
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
/* The credential is once for the org, but the moment somebody looks for it is while setting
|
|
131
|
-
up one staff member, so it is reachable from each card. */
|
|
132
|
-
const cred = el("button", { className: "ghbtn", textContent: "Agent credential" });
|
|
133
|
-
cred.onclick = () => {
|
|
134
|
-
pane.replaceChildren(el("div", { className: "card" }, [credentialPanel()]));
|
|
135
|
-
pane.scrollIntoView?.({ behavior: "smooth", block: "start" });
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const run = el("button", { className: "ghbtn", textContent: "Run once now" });
|
|
139
|
-
run.onclick = () => {
|
|
140
|
-
pane.replaceChildren(
|
|
141
|
-
el("div", { className: "card" }, [
|
|
142
|
-
runOnce({ staff: s.handle, name: s.name, onDone: (ok) => ok && refreshAll(true) }),
|
|
143
|
-
]),
|
|
144
|
-
);
|
|
145
|
-
pane.scrollIntoView?.({ behavior: "smooth", block: "start" });
|
|
122
|
+
go.onclick = () => {
|
|
123
|
+
const host = el("div");
|
|
124
|
+
present(host);
|
|
125
|
+
retireForm(host, s, status);
|
|
146
126
|
};
|
|
147
127
|
|
|
148
|
-
/*
|
|
149
|
-
|
|
150
|
-
|
|
128
|
+
/* Every setup step (the App, the charter, the credential, a run) is in the one list, so the
|
|
129
|
+
card has one way in. It says Finish setting up until setup looks finished: the charter
|
|
130
|
+
from disk at once, and the App's secrets and a first run from GitHub when it answers. */
|
|
131
|
+
const finish = el("button", { className: "ghbtn", textContent: "Set up" });
|
|
151
132
|
finish.onclick = () => open(roleOf(s));
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
133
|
+
const warn = el("p", { className: "warnline", hidden: true });
|
|
134
|
+
whatsLeft(s).then((left) => {
|
|
135
|
+
if (!left.length) return;
|
|
136
|
+
finish.textContent = "Finish setting up";
|
|
137
|
+
finish.className = "ghbtn primary";
|
|
138
|
+
d.classList.add("notready");
|
|
139
|
+
warn.hidden = false;
|
|
140
|
+
warn.textContent = "Not ready to run. Still to do: " + sentence(left) + ".";
|
|
141
|
+
});
|
|
142
|
+
d.append(warn);
|
|
160
143
|
|
|
161
144
|
d.append(
|
|
162
|
-
el("div", { className: "row", style: "margin-top:10px" }, [finish,
|
|
145
|
+
el("div", { className: "row", style: "margin-top:10px" }, [finish, go, status]),
|
|
163
146
|
);
|
|
164
147
|
return d;
|
|
165
148
|
}
|
|
@@ -206,7 +189,7 @@ export function viewStaff(m, o = {}) {
|
|
|
206
189
|
const status = el("span", { className: "meta" });
|
|
207
190
|
const go = el("button", { className: "ghbtn", textContent: "Retire " + plan.handle });
|
|
208
191
|
const cancel = el("button", { className: "ghbtn", textContent: "Cancel" });
|
|
209
|
-
cancel.onclick =
|
|
192
|
+
cancel.onclick = dismiss;
|
|
210
193
|
go.onclick = () =>
|
|
211
194
|
apply(
|
|
212
195
|
{ action: "retire", handle: plan.handle },
|