@nanocollective/roster 0.1.0-alpha.5 → 0.1.0-alpha.7
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/README.md +65 -84
- package/dist/cli.js +4153 -2708
- package/docs/README.md +9 -6
- package/docs/agents.md +24 -20
- package/docs/architecture.md +13 -5
- package/docs/charters/cmo.md +69 -0
- package/docs/charters/cto.md +71 -0
- package/docs/charters/support.md +60 -0
- package/docs/commands.md +81 -5
- package/docs/concepts.md +48 -14
- package/docs/cost.md +36 -1
- package/docs/developing.md +16 -21
- package/docs/doctor-codes.md +8 -2
- package/docs/extending.md +2 -2
- package/docs/getting-started.md +100 -77
- package/docs/images/brain.jpg +0 -0
- package/docs/images/org.jpg +0 -0
- package/docs/images/prompt.jpg +0 -0
- package/docs/images/setup-org.jpg +0 -0
- package/docs/images/setup-plan.jpg +0 -0
- package/docs/images/staff.jpg +0 -0
- package/docs/manual-steps.md +93 -123
- package/docs/memory.md +21 -3
- package/docs/org-yaml.md +37 -2
- package/docs/portal.md +59 -33
- package/docs/prompts.md +25 -4
- package/docs/security.md +29 -5
- package/docs/session-workflow.md +49 -17
- package/docs/staff-yaml.md +13 -2
- package/docs/troubleshooting.md +8 -8
- package/docs/upgrading.md +6 -0
- package/docs/writing-a-charter.md +15 -0
- package/package.json +1 -1
- package/templates/brain/.github/workflows/%%STAFF%%-daily.yaml +6 -0
- package/templates/brain/.github/workflows/%%STAFF%%-mention.yaml +6 -0
- package/templates/brain/CHARTER.md +3 -3
- package/templates/brain/README.md +1 -0
- package/templates/brain/log/decisions.md +3 -0
- package/templates/brain/strategy/ideas.md +7 -0
- package/templates/ops/.github/workflows/session.yaml +108 -14
- package/templates/ops/agents.mjs +7 -3
- package/templates/ops/compose.mjs +16 -3
- package/templates/ops/inflight.mjs +157 -0
- package/templates/ops/org/operating.md +21 -1
- package/templates/ops/org/voice.md +9 -0
- package/templates/ops/prompts/_inflight.md +14 -0
- package/templates/ops/prompts/_paths.md +2 -1
- package/templates/ops/prompts/daily.md +16 -7
- package/templates/ops/prompts/mention.md +2 -0
- package/templates/ops/run-record.mjs +144 -0
- package/templates/portal/css/base.css +133 -73
- package/templates/portal/css/brain.css +23 -20
- package/templates/portal/css/diff.css +10 -9
- package/templates/portal/css/graph.css +12 -7
- package/templates/portal/css/health.css +12 -10
- package/templates/portal/css/inbox.css +24 -21
- package/templates/portal/css/layout.css +64 -46
- package/templates/portal/css/markdown.css +17 -14
- package/templates/portal/css/runs.css +13 -0
- package/templates/portal/css/setup.css +38 -32
- package/templates/portal/index.html +5 -1
- package/templates/portal/js/api.js +29 -3
- package/templates/portal/js/app.js +4 -1
- package/templates/portal/js/state.js +3 -1
- package/templates/portal/js/views/app.js +23 -5
- package/templates/portal/js/views/credential.js +93 -0
- package/templates/portal/js/views/graph.js +1 -1
- package/templates/portal/js/views/health.js +15 -3
- package/templates/portal/js/views/org.js +2 -0
- package/templates/portal/js/views/paste.js +29 -0
- package/templates/portal/js/views/runonce.js +88 -0
- package/templates/portal/js/views/runs.js +165 -0
- package/templates/portal/js/views/setup.js +67 -26
- package/templates/portal/js/views/staff.js +47 -10
|
@@ -1,86 +1,102 @@
|
|
|
1
1
|
/* The page frame: sidebar, main column, and the tree-beside-viewer split every screen uses. */
|
|
2
2
|
|
|
3
|
-
.app{display:grid;grid-template-columns:
|
|
3
|
+
.app{display:grid;grid-template-columns:252px 1fr;min-height:100vh}
|
|
4
4
|
/* ---------- sidebar ---------- */
|
|
5
|
-
.side{background:var(--
|
|
6
|
-
position:sticky;top:0;height:100vh;overflow-y:auto;display:flex;flex-direction:column
|
|
7
|
-
|
|
5
|
+
.side{background:var(--side);border-right:1px solid var(--line);padding:16px 10px 12px;
|
|
6
|
+
position:sticky;top:0;height:100vh;overflow-y:auto;display:flex;flex-direction:column;
|
|
7
|
+
gap:1px}
|
|
8
|
+
.brand{padding:4px 10px 16px;font:600 15px/1.2 var(--sans);letter-spacing:-.02em;
|
|
9
|
+
display:grid;grid-template-columns:auto 1fr;column-gap:10px;align-items:center}
|
|
8
10
|
/* The tab icon, in the corner it belongs in. Drawn rather than fetched so it takes the
|
|
9
11
|
theme's accent and costs no request. */
|
|
10
|
-
.brand .mark{display:inline-block;width:
|
|
11
|
-
background:var(--accent)
|
|
12
|
+
.brand .mark{grid-row:1/3;display:inline-block;width:24px;height:24px;border-radius:7px;
|
|
13
|
+
background:var(--accent-fill)}
|
|
12
14
|
.brand small{display:block;color:var(--ink-faint);font-weight:400;font-size:11.5px;
|
|
13
|
-
letter-spacing
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
letter-spacing:0;margin-top:2px;overflow:hidden;text-overflow:ellipsis;
|
|
16
|
+
white-space:nowrap}
|
|
17
|
+
.refreshall{display:flex;align-items:center;gap:8px;width:100%;margin:0 0 12px;
|
|
18
|
+
background:var(--hover);border:1px solid transparent;color:var(--ink-dim);
|
|
19
|
+
border-radius:8px;padding:6px 10px;cursor:pointer;font:11.5px var(--sans);text-align:left;
|
|
20
|
+
transition:color .15s var(--ease),background .15s var(--ease)}
|
|
21
|
+
.refreshall:hover{color:var(--ink);background:var(--active)}
|
|
18
22
|
.refreshall .sync{display:inline-flex;--ic:13px;flex:none}
|
|
19
23
|
.refreshall.spin .sync{animation:spin .9s linear infinite}
|
|
20
24
|
|
|
21
|
-
.sect{padding:
|
|
25
|
+
.sect{padding:16px 10px 6px;font:600 11px var(--sans);letter-spacing:.08em;
|
|
22
26
|
text-transform:uppercase;color:var(--ink-faint)}
|
|
23
27
|
/* The Staff heading is also the fold for the list under it, so it is a button — and has to
|
|
24
|
-
go on reading as a heading rather than as a control. The caret and the
|
|
28
|
+
go on reading as a heading rather than as a control. The caret and the left padding
|
|
25
29
|
are the staff rows' own, so the two carets line up in one column. */
|
|
26
30
|
.sectfold{display:flex;align-items:center;gap:7px;width:100%;background:none;border:0;
|
|
27
|
-
cursor:pointer;text-align:left;font:600
|
|
28
|
-
text-transform:uppercase;color:var(--ink-faint)
|
|
31
|
+
border-radius:6px;cursor:pointer;text-align:left;font:600 11px var(--sans);letter-spacing:.08em;
|
|
32
|
+
text-transform:uppercase;color:var(--ink-faint);margin-top:12px;
|
|
33
|
+
transition:color .15s var(--ease)}
|
|
29
34
|
.sectfold:hover{color:var(--ink-dim)}
|
|
30
|
-
.sectfold .caret{display:inline-flex;--ic:12px;flex:none;transition:transform .
|
|
35
|
+
.sectfold .caret{display:inline-flex;--ic:12px;flex:none;transition:transform .15s var(--ease)}
|
|
31
36
|
.sectfold[aria-expanded="false"] .caret{transform:rotate(-90deg)}
|
|
32
37
|
/* The count is a number, not a heading: mono, and none of the heading's letterspacing. */
|
|
33
|
-
.sectfold .n{font:10.5px var(--mono);letter-spacing:0;text-transform:none;color:var(--ink-faint)
|
|
38
|
+
.sectfold .n{font:10.5px var(--mono);letter-spacing:0;text-transform:none;color:var(--ink-faint);
|
|
39
|
+
font-variant-numeric:tabular-nums}
|
|
34
40
|
/* Flex, not block with a float. The icons are block-level SVGs and the trailing count has to
|
|
35
41
|
sit hard right; `float` does neither of those inside a flex row, and did neither of them
|
|
36
42
|
outside one once the icons arrived. */
|
|
37
|
-
.nav{display:flex;align-items:center;width:100%;text-align:left;background:none;
|
|
38
|
-
color:var(--ink-dim);font:
|
|
39
|
-
|
|
40
|
-
.
|
|
41
|
-
.nav
|
|
42
|
-
.nav
|
|
43
|
-
.nav
|
|
44
|
-
.nav.
|
|
43
|
+
.nav{position:relative;display:flex;align-items:center;width:100%;text-align:left;background:none;
|
|
44
|
+
border:0;border-radius:7px;color:var(--ink-dim);font:500 13.5px var(--sans);padding:6px 10px;
|
|
45
|
+
cursor:pointer;min-height:32px;
|
|
46
|
+
transition:background .15s var(--ease),color .15s var(--ease)}
|
|
47
|
+
.nav:hover{color:var(--ink);background:var(--hover)}
|
|
48
|
+
.nav:focus-visible{box-shadow:0 0 0 2px var(--accent-soft),inset 0 0 0 1px var(--accent-dim)}
|
|
49
|
+
.nav[aria-current="true"]{color:var(--ink);background:var(--sel)}
|
|
50
|
+
.nav .ic{color:var(--ink-faint);margin-right:10px;--ic:15px;flex:none;
|
|
51
|
+
transition:color .15s var(--ease)}
|
|
52
|
+
.nav:hover .ic{color:var(--ink-dim)}
|
|
53
|
+
.nav[aria-current="true"] .ic{color:var(--accent)}
|
|
54
|
+
.nav .n{margin-left:auto;padding-left:10px;color:var(--ink-faint);font:500 10.5px var(--mono);
|
|
55
|
+
font-variant-numeric:tabular-nums}
|
|
56
|
+
.nav .n:not(:empty):not(.wait){padding:1px 7px;border-radius:999px;margin-left:auto;
|
|
57
|
+
background:var(--sel);color:var(--ink-dim);min-width:20px;
|
|
58
|
+
text-align:center}
|
|
59
|
+
.nav .n.wait{margin-left:auto}
|
|
60
|
+
.nav.top{margin-bottom:0}
|
|
45
61
|
|
|
46
62
|
/* Staff are rows that unfold into their own views, rather than a dropdown plus a list you
|
|
47
63
|
have to connect for yourself. The nesting is the hierarchy, said out loud. */
|
|
48
|
-
.staffrow{display:flex;align-items:center;gap:7px;font:
|
|
49
|
-
padding-right:
|
|
50
|
-
.staffrow .caret{color:var(--ink-faint);--ic:12px;flex:none;transition:transform .
|
|
64
|
+
.staffrow{display:flex;align-items:center;gap:7px;font:500 13px var(--sans);color:var(--ink);
|
|
65
|
+
padding-right:10px}
|
|
66
|
+
.staffrow .caret{color:var(--ink-faint);--ic:12px;flex:none;transition:transform .15s var(--ease)}
|
|
51
67
|
.staffrow[aria-expanded="false"] .caret{transform:rotate(-90deg)}
|
|
52
68
|
.staffrow .t{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
53
69
|
.staffrow .hh{font:10.5px var(--mono);color:var(--ink-faint);flex:none}
|
|
54
70
|
.staffrow[aria-selected="true"]{color:var(--ink)}
|
|
55
|
-
.staffviews{padding-
|
|
56
|
-
/* `subnav`, not `sub`: `.sub` is the page subtitle and carries a
|
|
71
|
+
.staffviews{position:relative;padding:2px 0 6px;margin-left:12px}
|
|
72
|
+
/* `subnav`, not `sub`: `.sub` is the page subtitle and carries a bottom margin, which
|
|
57
73
|
these rows inherited as a gap you could park a car in. */
|
|
58
|
-
.nav.subnav{padding:
|
|
74
|
+
.nav.subnav{padding:4px 10px 4px 16px;font-size:12.5px;min-height:28px}
|
|
59
75
|
.nav.subnav .n{font-size:10px}
|
|
60
|
-
.sidefoot{margin-top:auto;padding-top:14px}
|
|
76
|
+
.sidefoot{margin-top:auto;padding-top:14px;border-top:1px solid var(--line)}
|
|
61
77
|
.sidefoot .nav{font-size:12.5px;color:var(--ink-faint)}
|
|
62
78
|
.sidefoot .nav:hover{color:var(--ink-dim)}
|
|
63
79
|
|
|
64
80
|
/* ---------- main ---------- */
|
|
65
|
-
main{padding:
|
|
81
|
+
main{padding:36px 44px 96px;max-width:1220px;overflow-x:hidden}
|
|
66
82
|
|
|
67
83
|
/* ---------- tree beside viewer ---------- */
|
|
68
84
|
.split{display:grid;grid-template-columns:314px 1fr;gap:16px;align-items:start}
|
|
69
85
|
@media (max-width:900px){.split{grid-template-columns:1fr}}
|
|
70
|
-
.tree{background:var(--panel);border:1px solid var(--line);border-radius:
|
|
71
|
-
max-height:74vh;overflow:auto}
|
|
72
|
-
.tdir{font:600 11px var(--mono);color:var(--ink-faint);padding:
|
|
73
|
-
.tdir.first{padding-top:
|
|
86
|
+
.tree{background:var(--panel);border:1px solid var(--line);border-radius:var(--r-lg);padding:6px;
|
|
87
|
+
max-height:74vh;overflow:auto;box-shadow:var(--shadow-sm)}
|
|
88
|
+
.tdir{font:600 11px var(--mono);color:var(--ink-faint);padding:9px 8px 4px;letter-spacing:.02em}
|
|
89
|
+
.tdir.first{padding-top:3px}
|
|
74
90
|
.tfile{display:flex;align-items:baseline;gap:10px;width:100%;text-align:left;background:none;
|
|
75
|
-
border:0;color:var(--ink-dim);font:12.5px var(--mono);padding:
|
|
76
|
-
cursor:pointer}
|
|
77
|
-
.tfile:hover{background:var(--
|
|
78
|
-
.tfile[aria-current="true"]{background:var(--
|
|
91
|
+
border:0;color:var(--ink-dim);font:12.5px var(--mono);padding:5px 8px;border-radius:6px;
|
|
92
|
+
cursor:pointer;transition:background .12s var(--ease),color .12s var(--ease)}
|
|
93
|
+
.tfile:hover{background:var(--hover);color:var(--ink)}
|
|
94
|
+
.tfile[aria-current="true"]{background:var(--accent-soft);color:var(--accent)}
|
|
79
95
|
.tfile span.t{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
80
96
|
.tfile i{color:var(--ink-faint);font-style:normal;font-size:10.5px;flex:none;
|
|
81
97
|
font-variant-numeric:tabular-nums}
|
|
82
98
|
.tfile:focus{outline:none}
|
|
83
|
-
.tfile:focus-visible{
|
|
99
|
+
.tfile:focus-visible{box-shadow:inset 0 0 0 1px var(--accent-dim)}
|
|
84
100
|
|
|
85
101
|
/* A search result: the page, and the lines the words were actually found in. Without the lines
|
|
86
102
|
this is a filter on titles, which is the thing that was not good enough. */
|
|
@@ -93,8 +109,8 @@ main{padding:28px 34px 80px;max-width:1180px;overflow-x:hidden}
|
|
|
93
109
|
.thit .hitline em{font-style:normal;color:var(--ink);background:color-mix(in srgb,var(--accent) 22%,transparent);
|
|
94
110
|
border-radius:3px;padding:0 2px}
|
|
95
111
|
|
|
96
|
-
.viewer{background:var(--panel);border:1px solid var(--line);border-radius:
|
|
97
|
-
min-height:340px;max-height:74vh;overflow:auto}
|
|
112
|
+
.viewer{background:var(--panel);border:1px solid var(--line);border-radius:var(--r-lg);padding:22px 26px;
|
|
113
|
+
min-height:340px;max-height:74vh;overflow:auto;box-shadow:var(--shadow-sm)}
|
|
98
114
|
.viewer pre{margin:0;white-space:pre-wrap;word-break:break-word;font:12.5px/1.65 var(--mono);color:var(--ink-dim)}
|
|
99
115
|
/* Source keeps its own line breaks and scrolls, rather than wrapping into soup. */
|
|
100
116
|
.viewer pre.code{white-space:pre;word-break:normal;overflow-x:auto}
|
|
@@ -105,12 +121,14 @@ main{padding:28px 34px 80px;max-width:1180px;overflow-x:hidden}
|
|
|
105
121
|
.viewer audio.player{background:none}
|
|
106
122
|
.viewer table{border-collapse:collapse;font:12px var(--mono);width:100%}
|
|
107
123
|
.viewer td,.viewer th{border:1px solid var(--line);padding:4px 8px;text-align:left;white-space:nowrap}
|
|
108
|
-
.viewer th{position:sticky;top:0;background:var(--panel-2);color:var(--ink)}
|
|
124
|
+
.viewer th{position:sticky;top:0;background:var(--panel-2);color:var(--ink);font-weight:600}
|
|
109
125
|
|
|
110
126
|
.gallery{display:grid;gap:10px;grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
|
|
111
127
|
.gallery figure{margin:0;position:relative}
|
|
112
128
|
.gallery img,.gallery video{width:100%;aspect-ratio:4/3;object-fit:contain;background:var(--panel-2);
|
|
113
|
-
border-radius:
|
|
129
|
+
border-radius:var(--r);border:1px solid var(--line);cursor:pointer;display:block;
|
|
130
|
+
transition:border-color .15s var(--ease),transform .15s var(--ease)}
|
|
131
|
+
.gallery img:hover,.gallery video:hover{border-color:var(--border-strong)}
|
|
114
132
|
/* A still frame and a photograph look identical in a grid, so the video says which it is. */
|
|
115
133
|
.playmark{position:absolute;top:8px;right:8px;display:inline-flex;padding:4px;border-radius:50%;
|
|
116
134
|
background:color-mix(in srgb,#000 55%,transparent);color:#fff;--ic:12px;pointer-events:none}
|
|
@@ -1,34 +1,37 @@
|
|
|
1
1
|
/* One renderer, two sizes: .cbody is a comment in a thread, .doc is a file being read.
|
|
2
2
|
Everything shared lives on .md so a fix reaches both. */
|
|
3
3
|
|
|
4
|
-
.md{line-height:1.
|
|
4
|
+
.md{line-height:1.7}
|
|
5
5
|
.md>:first-child{margin-top:0}
|
|
6
6
|
.md p{margin:0 0 10px}
|
|
7
7
|
.md h1,.md h2,.md h3,.md h4,.md h5,.md h6{color:var(--ink);font-weight:600;line-height:1.3;
|
|
8
|
-
margin:
|
|
9
|
-
.md h1{font-size:
|
|
8
|
+
margin:26px 0 10px;letter-spacing:-.02em}
|
|
9
|
+
.md h1{font-size:22px} .md h2{font-size:17.5px} .md h3{font-size:15px}
|
|
10
10
|
.md h4,.md h5,.md h6{font-size:13.5px;color:var(--ink-dim)}
|
|
11
|
-
.md h1,.md h2{padding-bottom:
|
|
11
|
+
.md h1,.md h2{padding-bottom:8px;border-bottom:1px solid var(--line)}
|
|
12
12
|
.md ul,.md ol{margin:0 0 10px;padding-left:20px}
|
|
13
13
|
.md li{margin:3px 0}
|
|
14
14
|
.md li>ul,.md li>ol{margin:4px 0 0}
|
|
15
15
|
.md ul.tasks{list-style:none;padding-left:1px}
|
|
16
16
|
.md .box{color:var(--ink-faint);margin-right:6px;display:inline-flex;--ic:14px;
|
|
17
17
|
vertical-align:-3px}
|
|
18
|
-
.md code{font:12px var(--mono);color:var(--
|
|
19
|
-
padding:1px
|
|
20
|
-
.md pre{background:var(--
|
|
21
|
-
padding:
|
|
18
|
+
.md code{font:12px var(--mono);color:var(--ink);background:color-mix(in srgb,var(--ink) 7%,transparent);
|
|
19
|
+
padding:1px 5px;border-radius:5px;word-break:break-word}
|
|
20
|
+
.md pre{background:var(--surface-2);border:0;border-radius:var(--r);
|
|
21
|
+
padding:13px 15px;font:12px/1.62 var(--mono);margin:10px 0;color:var(--ink-dim);
|
|
22
22
|
overflow-x:auto;white-space:pre}
|
|
23
|
-
.md pre code{background:none;padding:0;color:inherit;font-size:inherit}
|
|
24
|
-
.md a{color:var(--
|
|
25
|
-
|
|
23
|
+
.md pre code{background:none;border:0;padding:0;color:inherit;font-size:inherit}
|
|
24
|
+
.md a{color:var(--accent);text-decoration:underline;text-decoration-color:var(--accent-dim);
|
|
25
|
+
text-underline-offset:3px;text-decoration-thickness:1px}
|
|
26
|
+
.md a:hover{text-decoration-color:var(--accent)}
|
|
27
|
+
.md blockquote{margin:12px 0;padding:2px 0 2px 14px;border-left:3px solid var(--line);color:var(--ink-dim)}
|
|
26
28
|
.md hr{border:0;border-top:1px solid var(--line);margin:16px 0}
|
|
27
29
|
.md img{max-width:100%;border-radius:6px;background:var(--panel-2)}
|
|
28
30
|
.md .ctable{overflow-x:auto;margin:10px 0;max-width:100%}
|
|
29
31
|
.md table{border-collapse:collapse;font-size:12.5px;width:100%}
|
|
30
|
-
.md th,.md td{border:1px solid var(--line);padding:
|
|
31
|
-
.md th{background:var(--panel-2);color:var(--ink);font-weight:600;white-space:nowrap
|
|
32
|
+
.md th,.md td{border:1px solid var(--line);padding:7px 10px;text-align:left;vertical-align:top}
|
|
33
|
+
.md th{background:var(--panel-2);color:var(--ink);font-weight:600;white-space:nowrap;
|
|
34
|
+
font-size:11.5px;letter-spacing:.02em}
|
|
32
35
|
.md td{color:var(--ink-dim)}
|
|
33
36
|
.md td b,.md td strong{color:var(--ink)}
|
|
34
37
|
.cbody{margin-top:8px;font-size:13.5px;max-width:74ch}
|
|
@@ -70,5 +73,5 @@ pre.yaml{color:var(--ink)}
|
|
|
70
73
|
Bordered and inset so it reads as a figure rather than the page bleeding into another one:
|
|
71
74
|
these are pictures of this same UI, and without a frame the two run together. */
|
|
72
75
|
.md img{display:block;max-width:100%;height:auto;margin:14px 0;
|
|
73
|
-
border:1px solid var(--line);border-radius:
|
|
76
|
+
border:1px solid var(--line);border-radius:var(--r);box-shadow:var(--shadow)}
|
|
74
77
|
.md img+em,.md p>img+em{display:block}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/* The Runs screen: a spend strip, then one table per staff member. */
|
|
2
|
+
|
|
3
|
+
.runsum{margin-bottom:6px}
|
|
4
|
+
.hsect i.warn{color:var(--warn)}
|
|
5
|
+
.runtable{border-collapse:collapse;width:100%;margin-top:8px;font:12px var(--mono);
|
|
6
|
+
font-variant-numeric:tabular-nums}
|
|
7
|
+
.runtable th{text-align:left;font:600 10.5px var(--sans);letter-spacing:.06em;text-transform:uppercase;
|
|
8
|
+
color:var(--ink-faint);padding:6px 10px 6px 0;border-bottom:1px solid var(--line)}
|
|
9
|
+
.runtable td{padding:7px 10px 7px 0;border-bottom:1px solid var(--line);color:var(--ink-dim);
|
|
10
|
+
white-space:nowrap}
|
|
11
|
+
.runtable td:last-child,.runtable th:last-child{text-align:right;padding-right:0}
|
|
12
|
+
/* The table is wider than a phone; it scrolls inside its section, never the page. */
|
|
13
|
+
.hbody.runs{overflow-x:auto}
|
|
@@ -1,55 +1,60 @@
|
|
|
1
1
|
/* The setup screen, and the copy-a-prompt panel it shares with the authoring screens. */
|
|
2
2
|
|
|
3
|
-
.step{background:var(--panel);border:1px solid var(--line);border-radius:
|
|
4
|
-
padding:
|
|
5
|
-
.step.done{border-color:var(--accent
|
|
3
|
+
.step{background:var(--panel);border:1px solid var(--line);border-radius:var(--r-lg);
|
|
4
|
+
padding:20px 22px 22px;margin-bottom:14px;box-shadow:var(--shadow-sm)}
|
|
5
|
+
.step.done{border-color:color-mix(in srgb,var(--accent) 30%,var(--line))}
|
|
6
6
|
.stephead{display:flex;align-items:center;gap:11px;margin-bottom:10px}
|
|
7
|
-
.stephead h2{margin:0;font:600 16px var(--sans);letter-spacing:-.
|
|
8
|
-
.stepn{flex:none;width:
|
|
9
|
-
background:var(--
|
|
7
|
+
.stephead h2{margin:0;font:600 16px var(--sans);letter-spacing:-.018em}
|
|
8
|
+
.stepn{flex:none;width:24px;height:24px;border-radius:50%;display:grid;place-items:center;
|
|
9
|
+
background:var(--surface-2);border:1px solid transparent;color:var(--ink-dim);
|
|
10
10
|
font:600 11px var(--mono)}
|
|
11
|
-
.step.done .stepn{background:var(--accent-
|
|
11
|
+
.step.done .stepn{background:var(--accent-fill);border-color:transparent;color:var(--accent-ink)}
|
|
12
12
|
|
|
13
13
|
/* ---------- the form ---------- */
|
|
14
14
|
|
|
15
15
|
.setupform{display:grid;gap:13px;margin:14px 0 18px;max-width:520px}
|
|
16
16
|
.field{display:grid;gap:4px}
|
|
17
17
|
.field>span{font:600 12px var(--sans);color:var(--ink-dim)}
|
|
18
|
-
.field input,.field select{background:var(--
|
|
19
|
-
border-radius:
|
|
20
|
-
|
|
18
|
+
.field input,.field select{background:var(--bg-elev);border:1px solid var(--border-strong);color:var(--ink);
|
|
19
|
+
border-radius:var(--r);padding:8px 11px;font:13px var(--sans);outline:none;width:100%;
|
|
20
|
+
transition:border-color .15s var(--ease),box-shadow .15s var(--ease)}
|
|
21
|
+
.field input:focus,.field select:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
|
|
21
22
|
.field small{color:var(--ink-faint);font-size:11.5px;line-height:1.45}
|
|
22
23
|
|
|
23
24
|
.choices{display:grid;gap:8px;margin:10px 0 4px;max-width:640px}
|
|
24
25
|
.choice{display:grid;grid-template-columns:auto 1fr;gap:3px 10px;align-items:start;
|
|
25
|
-
background:var(--
|
|
26
|
-
padding:
|
|
27
|
-
|
|
26
|
+
background:var(--bg-elev);border:1px solid var(--line);border-radius:var(--r);
|
|
27
|
+
padding:12px 14px;cursor:pointer;
|
|
28
|
+
transition:border-color .15s var(--ease),background .15s var(--ease),box-shadow .15s var(--ease)}
|
|
29
|
+
.choice:hover{border-color:var(--border-strong)}
|
|
30
|
+
.choice:has(input:checked){border-color:var(--accent-dim);background:var(--accent-soft)}
|
|
28
31
|
.choice input{grid-row:1/3;margin-top:3px;accent-color:var(--accent)}
|
|
29
32
|
.choice b{font:600 13px var(--sans)}
|
|
30
33
|
.choice small{color:var(--ink-dim);font-size:12px;line-height:1.45}
|
|
31
34
|
.choice code{grid-column:2;font:10.5px var(--mono);color:var(--ink-faint)}
|
|
32
35
|
|
|
33
36
|
.row{display:flex;gap:9px;align-items:center;flex-wrap:wrap;margin-top:6px}
|
|
34
|
-
.btn{background:var(--
|
|
35
|
-
padding:
|
|
36
|
-
.
|
|
37
|
+
.btn{background:var(--surface-2);border:1px solid transparent;color:var(--ink);border-radius:8px;
|
|
38
|
+
padding:7px 14px;font:500 13px var(--sans);cursor:pointer;text-decoration:none;display:inline-block;
|
|
39
|
+
transition:background .15s var(--ease),opacity .15s var(--ease)}
|
|
40
|
+
.btn:hover{background:color-mix(in srgb,var(--surface-2) 88%,var(--ink) 12%)}
|
|
41
|
+
.btn:active{opacity:.8}
|
|
37
42
|
/* The full accent, not the dim one. Dim on the page background is pale-green-on-white with
|
|
38
43
|
near-white text: the button that does the main thing on the screen read as disabled. */
|
|
39
|
-
.btn.primary{background:var(--accent);border-color:
|
|
40
|
-
.btn.primary:hover{
|
|
41
|
-
.btn[disabled]{opacity:.
|
|
44
|
+
.btn.primary{background:var(--accent-fill);border-color:transparent;color:var(--accent-ink);font-weight:600}
|
|
45
|
+
.btn.primary:hover{background:var(--accent-fill);opacity:.88}
|
|
46
|
+
.btn[disabled]{opacity:.45;cursor:default}
|
|
42
47
|
|
|
43
|
-
.cmd{background:var(--
|
|
48
|
+
.cmd{background:var(--bg);border:1px solid var(--line);border-radius:var(--r);padding:10px 12px;
|
|
44
49
|
font:12px/1.6 var(--mono);color:var(--ink);overflow-x:auto;white-space:pre-wrap;
|
|
45
50
|
word-break:break-word;margin:9px 0}
|
|
46
51
|
|
|
47
52
|
.planout{margin-top:12px}
|
|
48
53
|
.filelist{display:grid;gap:2px;margin:9px 0;max-height:240px;overflow-y:auto;
|
|
49
|
-
background:var(--
|
|
54
|
+
background:var(--bg);border:1px solid var(--line);border-radius:var(--r);padding:10px 12px}
|
|
50
55
|
.filelist code{font:11px var(--mono);color:var(--ink-dim)}
|
|
51
56
|
|
|
52
|
-
.manual{background:var(--
|
|
57
|
+
.manual{background:var(--bg-elev);border:1px solid var(--line);border-radius:var(--r);
|
|
53
58
|
padding:13px 15px;margin:11px 0}
|
|
54
59
|
.manual b{display:block;font:600 13px var(--sans);margin-bottom:4px}
|
|
55
60
|
.manual p{margin:0 0 6px;font-size:13px}
|
|
@@ -59,22 +64,23 @@
|
|
|
59
64
|
|
|
60
65
|
.paste{border-top:1px solid var(--line);margin-top:16px;padding-top:15px}
|
|
61
66
|
.paste h3{margin:0 0 5px;font:600 14px var(--sans)}
|
|
62
|
-
.briefpreview{background:var(--
|
|
67
|
+
.briefpreview{background:var(--bg);border:1px solid var(--line);border-radius:var(--r);
|
|
63
68
|
padding:12px 14px;font:11.5px/1.6 var(--mono);max-height:340px;overflow:auto;
|
|
64
69
|
white-space:pre-wrap;word-break:break-word;margin:10px 0}
|
|
65
|
-
.pastebox{width:100%;background:var(--
|
|
66
|
-
border-radius:
|
|
70
|
+
.pastebox{width:100%;background:var(--bg-elev);border:1px solid var(--border-strong);color:var(--ink);
|
|
71
|
+
border-radius:var(--r);padding:10px 12px;font:12px/1.6 var(--mono);outline:none;resize:vertical;
|
|
67
72
|
margin:4px 0}
|
|
68
|
-
.pastebox:focus{border-color:var(--accent-
|
|
73
|
+
.pastebox:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
|
|
69
74
|
|
|
70
75
|
.pasteresult{margin-top:12px}
|
|
71
76
|
/* A model that replied in prose is the common case and not the person's mistake, so this reads
|
|
72
77
|
as a next step rather than as a failure. */
|
|
73
|
-
.problem{background:
|
|
78
|
+
.problem{background:color-mix(in srgb,var(--warn) 9%,transparent);
|
|
79
|
+
border:1px solid transparent;border-radius:var(--r);
|
|
74
80
|
padding:12px 14px;margin-bottom:10px}
|
|
75
81
|
.problem b{display:block;font:600 13px var(--sans);margin-bottom:7px}
|
|
76
82
|
|
|
77
|
-
.pastefile{background:var(--
|
|
83
|
+
.pastefile{background:var(--bg-elev);border:1px solid var(--line);border-radius:var(--r);
|
|
78
84
|
padding:12px 14px;margin-bottom:10px}
|
|
79
85
|
.pastefile.saved{border-color:var(--accent-dim)}
|
|
80
86
|
.pastefile b{font:600 13px var(--mono)}
|
|
@@ -83,8 +89,8 @@
|
|
|
83
89
|
|
|
84
90
|
/* ---------- the doctor checklist ---------- */
|
|
85
91
|
|
|
86
|
-
.check{display:grid;grid-template-columns:auto 1fr;gap:2px
|
|
87
|
-
padding:
|
|
92
|
+
.check{display:grid;grid-template-columns:auto 1fr;gap:2px 12px;align-items:start;
|
|
93
|
+
padding:12px 0;border-top:1px solid var(--line)}
|
|
88
94
|
.check:first-child{border-top:none}
|
|
89
95
|
.checkmark{font:600 13px var(--mono);line-height:1.5}
|
|
90
96
|
.check.fail .checkmark{color:var(--err)}
|
|
@@ -95,7 +101,7 @@
|
|
|
95
101
|
/* ---------- the repo picker ---------- */
|
|
96
102
|
|
|
97
103
|
.repolist{display:grid;gap:1px;max-height:280px;overflow-y:auto;
|
|
98
|
-
border:1px solid var(--line);border-radius:
|
|
104
|
+
border:1px solid var(--line);border-radius:var(--r);background:var(--line)}
|
|
99
105
|
/* Scoped to the picker. Bare `.repo` also matched the project label on every inbox row,
|
|
100
106
|
which is where the white box and the stray padding behind MARKETING came from. */
|
|
101
107
|
.repolist .repo{display:flex;align-items:center;justify-content:space-between;gap:12px;
|
|
@@ -106,6 +112,6 @@
|
|
|
106
112
|
.repolist .repo .sub{margin:2px 0 0;font-size:11.5px}
|
|
107
113
|
|
|
108
114
|
/* Which of the two things this is: a new org, or one that already runs roster. */
|
|
109
|
-
.verdict{background:var(--
|
|
115
|
+
.verdict{background:var(--accent-soft);border:1px solid transparent;border-radius:var(--r);
|
|
110
116
|
padding:9px 12px;font-size:12.5px}
|
|
111
117
|
.verdict p{margin:0}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>roster</title>
|
|
7
7
|
<!-- Inline, so a local page with no network still has a tab icon and the console stays clean. -->
|
|
8
|
-
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0
|
|
8
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='8' fill='%2334c759'/%3E%3C/svg%3E">
|
|
9
9
|
<!-- Stylesheets and modules are served from /assets by `roster portal`. No build step:
|
|
10
10
|
editing one of these files and reloading is the whole edit loop. -->
|
|
11
11
|
<link rel="stylesheet" href="/assets/css/base.css">
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
<link rel="stylesheet" href="/assets/css/graph.css">
|
|
17
17
|
<link rel="stylesheet" href="/assets/css/diff.css">
|
|
18
18
|
<link rel="stylesheet" href="/assets/css/health.css">
|
|
19
|
+
<link rel="stylesheet" href="/assets/css/runs.css">
|
|
19
20
|
<link rel="stylesheet" href="/assets/css/setup.css">
|
|
20
21
|
</head>
|
|
21
22
|
<body>
|
|
@@ -36,6 +37,9 @@
|
|
|
36
37
|
<span class="ic" data-icon="merged"></span> Pending work
|
|
37
38
|
<span class="n" id="prcount"></span>
|
|
38
39
|
</button>
|
|
40
|
+
<button class="nav" id="runsnav" data-view="runs" title="What ran, how it ended, and what it cost">
|
|
41
|
+
<span class="ic" data-icon="play"></span> Runs
|
|
42
|
+
</button>
|
|
39
43
|
<button class="nav" id="orgnav" data-view="org">
|
|
40
44
|
<span class="ic" data-icon="file"></span> Org
|
|
41
45
|
</button>
|
|
@@ -7,6 +7,9 @@ export const getDocs = () => json("/api/docs");
|
|
|
7
7
|
export const getInbox = (force) => json("/api/inbox" + (force ? "?refresh=1" : ""));
|
|
8
8
|
export const getSync = () => json("/api/sync").catch(() => null);
|
|
9
9
|
|
|
10
|
+
/** Every staff member's recent runs and 30-day spend. Online only; offline it says so. */
|
|
11
|
+
export const getRuns = (force) => json("/api/runs" + (force ? "?refresh=1" : ""));
|
|
12
|
+
|
|
10
13
|
/** The repos org.yaml lists. Off disk, so the new-issue form does not wait on the inbox. */
|
|
11
14
|
export const getRepos = () => json("/api/repos");
|
|
12
15
|
|
|
@@ -47,7 +50,12 @@ export async function post(payload, url = "/api/act") {
|
|
|
47
50
|
body: JSON.stringify(payload),
|
|
48
51
|
});
|
|
49
52
|
const data = await res.json().catch(() => ({}));
|
|
50
|
-
if (!res.ok || data.error)
|
|
53
|
+
if (!res.ok || data.error) {
|
|
54
|
+
// The body travels with the error: a refusal often carries the next step, like a link.
|
|
55
|
+
const err = new Error(data.error || "failed with " + res.status);
|
|
56
|
+
err.data = data;
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
51
59
|
return data;
|
|
52
60
|
}
|
|
53
61
|
|
|
@@ -78,8 +86,12 @@ export const planTenant = (params) =>
|
|
|
78
86
|
export const createTenant = (params) => post(params, "/api/setup/apply");
|
|
79
87
|
|
|
80
88
|
/** The copyable prompt, with every file it refers to carried inside it. */
|
|
81
|
-
export const getBrief = (kind, staff) =>
|
|
82
|
-
json(
|
|
89
|
+
export const getBrief = (kind, staff, example) =>
|
|
90
|
+
json(
|
|
91
|
+
"/api/brief?kind=" + encodeURIComponent(kind) +
|
|
92
|
+
(staff ? "&staff=" + encodeURIComponent(staff) : "") +
|
|
93
|
+
(example ? "&example=" + encodeURIComponent(example) : ""),
|
|
94
|
+
);
|
|
83
95
|
|
|
84
96
|
/** Parse what came back from the model. Reads only: saving is a second, deliberate step. */
|
|
85
97
|
export const parsePaste = (kind, staff, answer) =>
|
|
@@ -105,3 +117,17 @@ export const appResult = (state) => json("/api/setup/app-result?state=" + encode
|
|
|
105
117
|
/** Does this org already run roster. "Create" and "join" are different answers. */
|
|
106
118
|
export const checkOrg = (org) => json("/api/setup/check-org?org=" + encodeURIComponent(org));
|
|
107
119
|
export const joinOrg = (org) => post({ org }, "/api/setup/join");
|
|
120
|
+
|
|
121
|
+
/** The ops repo's Actions access: read it, or ask the server to set it. */
|
|
122
|
+
export const getAccess = () => json("/api/setup/access");
|
|
123
|
+
export const setAccess = () => post({}, "/api/setup/access");
|
|
124
|
+
|
|
125
|
+
/** Where the agent credential goes, and storing it. The value only ever travels in a POST. */
|
|
126
|
+
export const getCredential = () => json("/api/setup/credential");
|
|
127
|
+
export const storeCredential = (value, repoSecrets) =>
|
|
128
|
+
post({ value, repoSecrets }, "/api/setup/credential");
|
|
129
|
+
|
|
130
|
+
/** One daily run, now, and where it has got to. */
|
|
131
|
+
export const startRun = (staff) => post({ staff }, "/api/run/start");
|
|
132
|
+
export const runStatus = (staff, id) =>
|
|
133
|
+
json("/api/run/state?staff=" + encodeURIComponent(staff) + "&id=" + encodeURIComponent(id));
|
|
@@ -23,12 +23,14 @@ import { viewHealth } from "./views/health.js";
|
|
|
23
23
|
import { viewInbox, viewPrs } from "./views/inbox.js";
|
|
24
24
|
import { viewOrg } from "./views/org.js";
|
|
25
25
|
import { viewPrompt } from "./views/prompt.js";
|
|
26
|
+
import { viewRuns } from "./views/runs.js";
|
|
26
27
|
import { viewSetup } from "./views/setup.js";
|
|
27
28
|
import { viewStaff } from "./views/staff.js";
|
|
28
29
|
|
|
29
30
|
const SCREEN = {
|
|
30
31
|
inbox: viewInbox,
|
|
31
32
|
prs: viewPrs,
|
|
33
|
+
runs: viewRuns,
|
|
32
34
|
org: viewOrg,
|
|
33
35
|
staff: viewStaff,
|
|
34
36
|
docs: viewDocs,
|
|
@@ -66,7 +68,7 @@ export async function boot() {
|
|
|
66
68
|
/* Every one of these reads a tenant, so during setup they answer 409 and the sidebar is a
|
|
67
69
|
row of four dead ends beside an empty Staff heading. There is one thing to do on this
|
|
68
70
|
screen; the navigation comes back with the org. */
|
|
69
|
-
for (const el of document.querySelectorAll("#inboxnav, #prsnav, #orgnav, #staffnav, #docsnav, #refreshall, .sect, #stafflist")) {
|
|
71
|
+
for (const el of document.querySelectorAll("#inboxnav, #prsnav, #runsnav, #orgnav, #staffnav, #docsnav, #refreshall, .sect, #stafflist")) {
|
|
70
72
|
el.hidden = true;
|
|
71
73
|
}
|
|
72
74
|
for (const slot of document.querySelectorAll("[data-icon]")) {
|
|
@@ -94,6 +96,7 @@ export async function boot() {
|
|
|
94
96
|
|
|
95
97
|
$("#inboxnav").onclick = () => { S.view = "inbox"; render(); };
|
|
96
98
|
$("#prsnav").onclick = () => { S.view = "prs"; render(); };
|
|
99
|
+
$("#runsnav").onclick = () => { S.view = "runs"; render(); };
|
|
97
100
|
$("#staffnav").onclick = () => { S.view = "staff"; render(); };
|
|
98
101
|
$("#orgnav").onclick = () => { S.view = "org"; render(); };
|
|
99
102
|
$("#docsnav").onclick = () => { S.view = "docs"; render(); };
|
|
@@ -22,7 +22,7 @@ export const VIEWS = [
|
|
|
22
22
|
export const VIEW_ALIAS = { memory: "brain" };
|
|
23
23
|
|
|
24
24
|
/** Screens that belong to the org rather than to one staff member. */
|
|
25
|
-
export const ORG_WIDE = new Set(["inbox", "prs", "org", "staff", "docs"]);
|
|
25
|
+
export const ORG_WIDE = new Set(["inbox", "prs", "runs", "org", "staff", "docs"]);
|
|
26
26
|
|
|
27
27
|
/** The two screens that read the inbox, and so share its filters and its `?t=` thread. */
|
|
28
28
|
const INBOXY = new Set(["inbox", "prs"]);
|
|
@@ -32,6 +32,8 @@ export const S = {
|
|
|
32
32
|
data: null,
|
|
33
33
|
docs: null,
|
|
34
34
|
inbox: null,
|
|
35
|
+
/** /api/runs. Kept once read: it is minutes of downloads on a cold cache. */
|
|
36
|
+
runs: null,
|
|
35
37
|
sync: null,
|
|
36
38
|
loadedAt: null,
|
|
37
39
|
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* it is one browser, one origin, and nothing extra to explain.
|
|
7
7
|
*
|
|
8
8
|
* What it still cannot do is install the App. Installing is a grant of access to specific
|
|
9
|
-
* repositories and GitHub asks a human to
|
|
10
|
-
* be worked around. */
|
|
9
|
+
* repositories and GitHub asks a human to confirm them, which is correct behaviour and should not
|
|
10
|
+
* be worked around. The link it hands back has the right repos already selected. */
|
|
11
11
|
|
|
12
12
|
import { appResult, startApp } from "../api.js";
|
|
13
13
|
import { el } from "../dom.js";
|
|
@@ -48,6 +48,11 @@ export function appPanel(opts) {
|
|
|
48
48
|
it: an App that exists is not a problem, it is a step you have already done. */
|
|
49
49
|
const note = el("div");
|
|
50
50
|
note.append(el("p", { className: "err", textContent: String(err.message || err) }));
|
|
51
|
+
if (err.data?.install) {
|
|
52
|
+
note.append(
|
|
53
|
+
el("a", { className: "btn", href: err.data.install, target: "_blank", textContent: "Install it" }),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
51
56
|
out.replaceChildren(note);
|
|
52
57
|
return;
|
|
53
58
|
}
|
|
@@ -102,9 +107,21 @@ export function appPanel(opts) {
|
|
|
102
107
|
el("p", {
|
|
103
108
|
className: "sub",
|
|
104
109
|
textContent:
|
|
105
|
-
"
|
|
106
|
-
"
|
|
110
|
+
"The link opens GitHub's install page with the org and the repos already selected: " +
|
|
111
|
+
"this staff member's brain, every peer tracker it writes to, and the product repos. " +
|
|
112
|
+
"Check the list and confirm.",
|
|
107
113
|
}),
|
|
114
|
+
...((result.preselected ?? []).length
|
|
115
|
+
? [el("p", { className: "sub", textContent: "Selected: " + result.preselected.join(", ") })]
|
|
116
|
+
: []),
|
|
117
|
+
...((result.missing ?? []).length
|
|
118
|
+
? [
|
|
119
|
+
el("p", {
|
|
120
|
+
className: "err",
|
|
121
|
+
textContent: "Tick these yourself, their ids could not be read: " + result.missing.join(", "),
|
|
122
|
+
}),
|
|
123
|
+
]
|
|
124
|
+
: []),
|
|
108
125
|
el("a", {
|
|
109
126
|
className: "btn primary",
|
|
110
127
|
href: result.install,
|
|
@@ -115,7 +132,8 @@ export function appPanel(opts) {
|
|
|
115
132
|
className: "sub",
|
|
116
133
|
textContent:
|
|
117
134
|
"Do not verify this by reading the API. It reports what an App declares separately " +
|
|
118
|
-
"from what an installation was granted. Only a run that finished proves the chain
|
|
135
|
+
"from what an installation was granted. Only a run that finished proves the chain, " +
|
|
136
|
+
"which is what Run once now on the staff card is for.",
|
|
119
137
|
}),
|
|
120
138
|
);
|
|
121
139
|
out.replaceChildren(done);
|