@cmgmyr/hive 1.2.2 → 1.4.0
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 +6 -1
- package/claude-plugin/README.md +4 -1
- package/claude-plugin/kickoff.mjs +2 -1
- package/claude-plugin/skills/profile/SKILL.md +78 -0
- package/claude-plugin/skills/profile/references/interview.md +139 -0
- package/claude-plugin/skills/profile/references/recipes/index.md +12 -0
- package/claude-plugin/skills/profile/references/recipes/review-decision.md +41 -0
- package/claude-plugin/skills/profile/references/recipes/session-continuity.md +37 -0
- package/claude-plugin/skills/profile/references/recipes/shared-test-resource.md +45 -0
- package/claude-plugin/skills/profile/references/recipes/verification.md +50 -0
- package/claude-plugin/skills/profile/references/recipes/worker-context-checkpoint.md +51 -0
- package/dist/brief.js +1 -1
- package/dist/build-info.json +1 -1
- package/dist/cli.js +436 -112
- package/dist/codexHome.js +23 -8
- package/dist/config.js +8 -61
- package/dist/context.js +191 -2
- package/dist/db.js +80 -20
- package/dist/firstMessage.js +5 -0
- package/dist/globalConfig.js +247 -0
- package/dist/harnesses.js +14 -4
- package/dist/help.js +29 -11
- package/dist/hook.js +9 -0
- package/dist/hooks.js +2 -1
- package/dist/index.js +11 -5
- package/dist/kickoff.js +42 -29
- package/dist/leadState.js +63 -0
- package/dist/leadWatch.js +73 -0
- package/dist/portfolio.js +210 -0
- package/dist/projectYml.js +242 -178
- package/dist/queenAudit.js +118 -0
- package/dist/queenDashboard.js +573 -0
- package/dist/scheduler.js +190 -20
- package/dist/spawn.js +17 -4
- package/dist/strictInput.js +24 -7
- package/dist/tmux.js +65 -0
- package/dist/tools/agents.js +153 -15
- package/dist/tools/kv.js +24 -0
- package/dist/tools/leases.js +14 -2
- package/dist/tools/meta.js +65 -3
- package/dist/tools/pads.js +42 -0
- package/dist/tools/queenAudit.js +13 -0
- package/dist/tools/todos.js +72 -13
- package/dist/tools/wakes.js +233 -65
- package/package.json +4 -4
- package/profiles/orchestration/worker.md +1 -1
- package/profiles/queen/posture.md +31 -0
- package/profiles/queen/runbook.md +26 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import { join, sep } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { db } from "./db.js";
|
|
5
|
+
export const QUEEN_BRIEF_KEY = "queen:brief";
|
|
6
|
+
const LANE_ORDER = ["waiting_on_you", "stuck", "moving", "quiet"];
|
|
7
|
+
const LANE_RANK = { waiting_on_you: 0, stuck: 1, moving: 2, quiet: 3 };
|
|
8
|
+
const TIMESTAMP = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/;
|
|
9
|
+
const PROJECT_ID_KEY = /^[1-9]\d*$/;
|
|
10
|
+
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
11
|
+
const positiveInt = (v) => typeof v === "number" && Number.isSafeInteger(v) && v > 0;
|
|
12
|
+
const plainText = (v) => typeof v === "string" && v.trim().length > 0;
|
|
13
|
+
const hasExactKeys = (o, keys) => Object.keys(o).length === keys.length && keys.every((k) => Object.hasOwn(o, k));
|
|
14
|
+
const isLane = (v) => typeof v === "string" && Object.hasOwn(LANE_RANK, v);
|
|
15
|
+
export function parseQueenBrief(raw) {
|
|
16
|
+
let value;
|
|
17
|
+
try {
|
|
18
|
+
value = JSON.parse(raw);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return { kind: "invalid", reason: "malformed" };
|
|
22
|
+
}
|
|
23
|
+
if (!isRecord(value) || !Object.hasOwn(value, "schema_version"))
|
|
24
|
+
return { kind: "invalid", reason: "malformed" };
|
|
25
|
+
if (value.schema_version !== 1)
|
|
26
|
+
return { kind: "invalid", reason: "wrong_version" };
|
|
27
|
+
const malformed = { kind: "invalid", reason: "malformed" };
|
|
28
|
+
if (!hasExactKeys(value, ["schema_version", "written_at", "summary", "picks", "lanes_at_brief"]))
|
|
29
|
+
return malformed;
|
|
30
|
+
if (typeof value.written_at !== "string" || !TIMESTAMP.test(value.written_at))
|
|
31
|
+
return malformed;
|
|
32
|
+
if (!plainText(value.summary))
|
|
33
|
+
return malformed;
|
|
34
|
+
if (!Array.isArray(value.picks) || !isRecord(value.lanes_at_brief))
|
|
35
|
+
return malformed;
|
|
36
|
+
const picks = [];
|
|
37
|
+
for (const p of value.picks) {
|
|
38
|
+
if (!isRecord(p) || !hasExactKeys(p, ["project_id", "todo_id", "action", "reason"]))
|
|
39
|
+
return malformed;
|
|
40
|
+
if (!positiveInt(p.project_id))
|
|
41
|
+
return malformed;
|
|
42
|
+
if (p.todo_id !== null && !positiveInt(p.todo_id))
|
|
43
|
+
return malformed;
|
|
44
|
+
if (!plainText(p.action) || !plainText(p.reason))
|
|
45
|
+
return malformed;
|
|
46
|
+
picks.push({ project_id: p.project_id, todo_id: p.todo_id, action: p.action, reason: p.reason });
|
|
47
|
+
}
|
|
48
|
+
const lanes = {};
|
|
49
|
+
for (const [key, lane] of Object.entries(value.lanes_at_brief)) {
|
|
50
|
+
if (!PROJECT_ID_KEY.test(key) || !Number.isSafeInteger(Number(key)) || !isLane(lane))
|
|
51
|
+
return malformed;
|
|
52
|
+
lanes[key] = lane;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
kind: "ready",
|
|
56
|
+
brief: { schema_version: 1, written_at: value.written_at, summary: value.summary, picks, lanes_at_brief: lanes },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function readQueenBrief(queenProjectId, asOf) {
|
|
60
|
+
const row = db
|
|
61
|
+
.prepare("SELECT value FROM kv WHERE project_id = ? AND key = ? AND (expires_at IS NULL OR expires_at >= ?)")
|
|
62
|
+
.get(queenProjectId, QUEEN_BRIEF_KEY, asOf);
|
|
63
|
+
return row ? parseQueenBrief(row.value) : { kind: "missing" };
|
|
64
|
+
}
|
|
65
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
66
|
+
const LANE_TITLE = {
|
|
67
|
+
waiting_on_you: "Waiting on you",
|
|
68
|
+
stuck: "Stuck",
|
|
69
|
+
moving: "Moving",
|
|
70
|
+
quiet: "Quiet",
|
|
71
|
+
};
|
|
72
|
+
const LANE_RULE = {
|
|
73
|
+
waiting_on_you: "a needs-human todo and nothing else moving",
|
|
74
|
+
stuck: "a stall, or blocked or stale work",
|
|
75
|
+
moving: "a worker or an unblocked todo in progress",
|
|
76
|
+
quiet: "nothing asked, nothing broken",
|
|
77
|
+
};
|
|
78
|
+
const LANE_COLOR = {
|
|
79
|
+
waiting_on_you: "var(--warn)",
|
|
80
|
+
stuck: "var(--fail)",
|
|
81
|
+
moving: "var(--live)",
|
|
82
|
+
quiet: "var(--border-strong)",
|
|
83
|
+
};
|
|
84
|
+
const REASON_LABEL = {
|
|
85
|
+
needs_human: "Needs-human todo",
|
|
86
|
+
dead_lead_pane: "Lead pane is dead",
|
|
87
|
+
missing_root_with_work: "Project folder is missing",
|
|
88
|
+
worker_needs_input: "A worker is waiting for input",
|
|
89
|
+
wake_overdue_5m: "A wake is overdue",
|
|
90
|
+
in_progress_blocked: "An in-progress todo is blocked",
|
|
91
|
+
all_active_todos_blocked: "Every active todo is blocked",
|
|
92
|
+
stale_in_progress_48h: "In-progress work untouched for 48 hours",
|
|
93
|
+
worker_working: "A worker is working",
|
|
94
|
+
todo_in_progress: "A todo is in progress",
|
|
95
|
+
wake_due_24h: "A wake is due within 24 hours",
|
|
96
|
+
quiet: "Nothing asked, nothing broken",
|
|
97
|
+
};
|
|
98
|
+
const REASON_SEVERE = new Set([
|
|
99
|
+
"dead_lead_pane",
|
|
100
|
+
"missing_root_with_work",
|
|
101
|
+
"wake_overdue_5m",
|
|
102
|
+
]);
|
|
103
|
+
function parseUtc(ts) {
|
|
104
|
+
return Date.parse(`${ts.replace(" ", "T")}Z`);
|
|
105
|
+
}
|
|
106
|
+
function age(from, asOf) {
|
|
107
|
+
const seconds = Math.max(0, Math.floor((parseUtc(asOf) - parseUtc(from)) / 1000));
|
|
108
|
+
if (!Number.isFinite(seconds))
|
|
109
|
+
return "unknown";
|
|
110
|
+
const d = Math.floor(seconds / 86400);
|
|
111
|
+
const h = Math.floor((seconds % 86400) / 3600);
|
|
112
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
113
|
+
if (d > 0)
|
|
114
|
+
return h > 0 ? `${d}d ${h}h` : `${d}d`;
|
|
115
|
+
if (h > 0)
|
|
116
|
+
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
117
|
+
if (m > 0)
|
|
118
|
+
return `${m}m`;
|
|
119
|
+
return "now";
|
|
120
|
+
}
|
|
121
|
+
function ageClass(from, asOf) {
|
|
122
|
+
const seconds = (parseUtc(asOf) - parseUtc(from)) / 1000;
|
|
123
|
+
if (seconds >= 2 * 86400)
|
|
124
|
+
return " age-fail";
|
|
125
|
+
if (seconds >= 4 * 3600)
|
|
126
|
+
return " age-hot";
|
|
127
|
+
return "";
|
|
128
|
+
}
|
|
129
|
+
function dashboardHref(root) {
|
|
130
|
+
try {
|
|
131
|
+
const file = join(root, ".hive", "dashboard.html");
|
|
132
|
+
if (!existsSync(file))
|
|
133
|
+
return null;
|
|
134
|
+
const real = realpathSync(file);
|
|
135
|
+
const base = realpathSync(root);
|
|
136
|
+
return real.startsWith(base + sep) ? pathToFileURL(real).href : null;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function projectLink(p, href) {
|
|
143
|
+
const url = href(p);
|
|
144
|
+
return url === null
|
|
145
|
+
? `<span class="proj">${esc(p.name)}</span>`
|
|
146
|
+
: `<a class="proj" href="${esc(url)}" title="Open ${esc(p.name)}’s dashboard">${esc(p.name)}</a>`;
|
|
147
|
+
}
|
|
148
|
+
function turnText(p) {
|
|
149
|
+
return p.lead.turn === "working" ? "working" : p.lead.turn === "turn_ended" ? "turn ended" : "";
|
|
150
|
+
}
|
|
151
|
+
function turnPill(p) {
|
|
152
|
+
const t = turnText(p);
|
|
153
|
+
return t === "" ? "" : ` <span class="turn turn-${p.lead.turn === "working" ? "work" : "end"}">${esc(t)}</span>`;
|
|
154
|
+
}
|
|
155
|
+
function leadPill(p) {
|
|
156
|
+
switch (p.lead.state) {
|
|
157
|
+
case "alive":
|
|
158
|
+
return `<span class="status status-ok">lead up</span>${turnPill(p)}`;
|
|
159
|
+
case "dead_pane":
|
|
160
|
+
return '<span class="status status-fail">lead dead</span>';
|
|
161
|
+
case "unknown":
|
|
162
|
+
return '<span class="status status-off">lead unknown</span>';
|
|
163
|
+
default:
|
|
164
|
+
return '<span class="status status-off">no lead</span>';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function workersCell(p) {
|
|
168
|
+
const w = p.workers;
|
|
169
|
+
const out = [];
|
|
170
|
+
if (w.working)
|
|
171
|
+
out.push(`<span class="w w-work">${w.working} working</span>`);
|
|
172
|
+
if (w.needs_input)
|
|
173
|
+
out.push(`<span class="w w-input">${w.needs_input} needs input</span>`);
|
|
174
|
+
if (w.idle)
|
|
175
|
+
out.push(`<span class="w w-idle">${w.idle} idle</span>`);
|
|
176
|
+
if (w.unreachable)
|
|
177
|
+
out.push(`<span class="w w-input">${w.unreachable} unreachable</span>`);
|
|
178
|
+
if (w.unconfirmed)
|
|
179
|
+
out.push(`<span class="w w-idle">${w.unconfirmed} unconfirmed</span>`);
|
|
180
|
+
if (w.other)
|
|
181
|
+
out.push(`<span class="w w-idle">${w.other} other</span>`);
|
|
182
|
+
return out.length ? `<span class="workers">${out.join("")}</span>` : '<span class="zero">no workers</span>';
|
|
183
|
+
}
|
|
184
|
+
const num = (n, cls = "") => n ? `<span class="${cls}">${n}</span>` : '<span class="zero">0</span>';
|
|
185
|
+
function wakesCell(p) {
|
|
186
|
+
const { pending, overdue } = p.wakes;
|
|
187
|
+
if (overdue) {
|
|
188
|
+
const rest = pending - overdue;
|
|
189
|
+
return `<span class="n-high">${overdue} late</span>${rest > 0 ? ` +${rest}` : ""}`;
|
|
190
|
+
}
|
|
191
|
+
return num(pending);
|
|
192
|
+
}
|
|
193
|
+
function wakesText(p) {
|
|
194
|
+
const { pending, overdue } = p.wakes;
|
|
195
|
+
if (pending === 0)
|
|
196
|
+
return "";
|
|
197
|
+
if (overdue)
|
|
198
|
+
return `<span><span class="n-high">${overdue} wake late</span></span>`;
|
|
199
|
+
return `<span>${pending} wake${pending > 1 ? "s" : ""} pending</span>`;
|
|
200
|
+
}
|
|
201
|
+
function footCounts(p, asOf, rank) {
|
|
202
|
+
const t = p.todos;
|
|
203
|
+
return (`<span>${t.open} open</span><span>${t.in_progress} doing</span>` +
|
|
204
|
+
(t.blocked ? `<span class="n-blocked">${t.blocked} blocked</span>` : "") +
|
|
205
|
+
(t.high ? `<span class="n-high">${t.high} high</span>` : "") +
|
|
206
|
+
wakesText(p) +
|
|
207
|
+
(rank ? `<span class="age">seen ${esc(age(p.last_activity_at, asOf))}</span>` : ""));
|
|
208
|
+
}
|
|
209
|
+
function movingBadges(p) {
|
|
210
|
+
if (p.lane !== "moving")
|
|
211
|
+
return "";
|
|
212
|
+
return ((p.needs_human > 0 ? `<span class="n-needs">${p.needs_human} need you</span>` : "") +
|
|
213
|
+
(p.todos.blocked_in_progress > 0 ? `<span class="n-muted">${p.todos.blocked_in_progress} blocked</span>` : ""));
|
|
214
|
+
}
|
|
215
|
+
function card(p, report, rank, href) {
|
|
216
|
+
const asOf = report.as_of;
|
|
217
|
+
const items = [];
|
|
218
|
+
for (const it of p.needs_human_items) {
|
|
219
|
+
items.push(`<li><span><span class="ref">#${it.todo_id}</span> ${esc(it.title)}</span>` +
|
|
220
|
+
`<span class="age${ageClass(it.updated_at, asOf)}">${esc(age(it.updated_at, asOf))}</span></li>`);
|
|
221
|
+
}
|
|
222
|
+
if (p.needs_human > p.needs_human_items.length) {
|
|
223
|
+
items.push(`<li><span class="ref">+${p.needs_human - p.needs_human_items.length} more</span></li>`);
|
|
224
|
+
}
|
|
225
|
+
for (const r of p.reasons) {
|
|
226
|
+
if (r === "needs_human" || r === "quiet")
|
|
227
|
+
continue;
|
|
228
|
+
const cls = REASON_SEVERE.has(r) ? " age-fail" : "";
|
|
229
|
+
items.push(`<li><span class="${cls.trim()}">${esc(REASON_LABEL[r])}</span></li>`);
|
|
230
|
+
}
|
|
231
|
+
return (`<article class="card pc" style="--lane:${LANE_COLOR[p.lane]}" data-project="${p.id}">` +
|
|
232
|
+
`<div class="pc-top">${projectLink(p, href)}` +
|
|
233
|
+
(rank
|
|
234
|
+
? `<span class="pc-pick">brief pick ${rank}</span>`
|
|
235
|
+
: `<span class="age">${esc(age(p.last_activity_at, asOf))}</span>`) +
|
|
236
|
+
"</div>" +
|
|
237
|
+
`<div class="pc-badges">${leadPill(p)}${workersCell(p)}${movingBadges(p)}</div>` +
|
|
238
|
+
(items.length ? `<ul class="pc-items">${items.join("")}</ul>` : "") +
|
|
239
|
+
`<a class="pc-foot" href="#row-${p.id}" data-jump="${p.id}" title="Show ${esc(p.name)}’s row in the grid below">` +
|
|
240
|
+
`${footCounts(p, asOf, rank)}</a></article>`);
|
|
241
|
+
}
|
|
242
|
+
function quietList(list, asOf, href) {
|
|
243
|
+
return ('<section class="card"><ul class="quiet-list">' +
|
|
244
|
+
list
|
|
245
|
+
.map((p) => {
|
|
246
|
+
const sub = `${p.lead.state === "alive" ? (turnText(p) ? `lead up, ${turnText(p)}` : "lead up") : p.lead.state === "dead_pane" ? "lead dead" : p.lead.state === "unknown" ? "lead unknown" : "no lead"}, ${p.todos.open} open`;
|
|
247
|
+
return (`<li data-project="${p.id}"><span>${projectLink(p, href)}` +
|
|
248
|
+
`<a class="sub" href="#row-${p.id}" data-jump="${p.id}">${esc(sub)}</a></span>` +
|
|
249
|
+
`<span class="age">${esc(age(p.last_activity_at, asOf))}</span></li>`);
|
|
250
|
+
})
|
|
251
|
+
.join("") +
|
|
252
|
+
"</ul></section>");
|
|
253
|
+
}
|
|
254
|
+
function gridRow(p, asOf, href) {
|
|
255
|
+
const t = p.todos;
|
|
256
|
+
const note = p.root_exists ? "" : '<span class="g-note">project folder is missing</span>';
|
|
257
|
+
return (`<div class="g-row" role="row" id="row-${p.id}" style="--lane:${LANE_COLOR[p.lane]}">` +
|
|
258
|
+
`<span class="c-name g-name">${projectLink(p, href)}${note}</span>` +
|
|
259
|
+
`<span class="c-lead">${leadPill(p)}</span>` +
|
|
260
|
+
`<span class="c-work">${workersCell(p)}</span>` +
|
|
261
|
+
`<span class="c-n g-num">${num(t.open)}</span><span class="c-n g-num">${num(t.in_progress)}</span>` +
|
|
262
|
+
`<span class="c-n g-num">${num(t.blocked, "n-blocked")}</span><span class="c-n g-num">${num(t.high, "n-high")}</span>` +
|
|
263
|
+
`<span class="c-n g-num">${num(p.needs_human, "n-needs")}</span><span class="c-n g-num c-wake">${wakesCell(p)}</span>` +
|
|
264
|
+
`<span class="c-last g-num age">${esc(age(p.last_activity_at, asOf))}</span>` +
|
|
265
|
+
'<span class="c-nums">' +
|
|
266
|
+
`<span>${t.open} open</span><span>${t.in_progress} doing</span>` +
|
|
267
|
+
(t.blocked ? `<span class="n-blocked">${t.blocked} blocked</span>` : "") +
|
|
268
|
+
(t.high ? `<span class="n-high">${t.high} high</span>` : "") +
|
|
269
|
+
(p.needs_human ? `<span class="n-needs">${p.needs_human} need you</span>` : "") +
|
|
270
|
+
wakesText(p) +
|
|
271
|
+
"</span></div>");
|
|
272
|
+
}
|
|
273
|
+
const GRID_HEAD = '<div class="g-row g-head" role="row"><span>Project</span><span>Lead</span><span>Workers</span>' +
|
|
274
|
+
'<span class="g-num">Open</span><span class="g-num">Doing</span><span class="g-num">Blocked</span>' +
|
|
275
|
+
'<span class="g-num">High</span><span class="g-num">Needs you</span><span class="g-num c-wake">Wakes</span>' +
|
|
276
|
+
'<span class="g-num">Last seen</span></div>';
|
|
277
|
+
function laneName(lane) {
|
|
278
|
+
return LANE_TITLE[lane].toLowerCase();
|
|
279
|
+
}
|
|
280
|
+
function driftLine(brief, report, queenHomeId) {
|
|
281
|
+
const then = brief.lanes_at_brief;
|
|
282
|
+
const live = new Map(report.projects.map((p) => [String(p.id), p]));
|
|
283
|
+
const changes = [];
|
|
284
|
+
for (const p of report.projects) {
|
|
285
|
+
if (p.id === queenHomeId)
|
|
286
|
+
continue;
|
|
287
|
+
const before = then[String(p.id)];
|
|
288
|
+
if (before === undefined) {
|
|
289
|
+
changes.push({ rank: LANE_RANK[p.lane], id: p.id, text: `${esc(p.name)} is new since the brief` });
|
|
290
|
+
}
|
|
291
|
+
else if (before !== p.lane) {
|
|
292
|
+
changes.push({
|
|
293
|
+
rank: LANE_RANK[p.lane],
|
|
294
|
+
id: p.id,
|
|
295
|
+
text: `${esc(p.name)} moved from ${laneName(before)} to ${laneName(p.lane)}`,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
for (const [key, before] of Object.entries(then)) {
|
|
300
|
+
if (live.has(key) || key === String(queenHomeId))
|
|
301
|
+
continue;
|
|
302
|
+
changes.push({
|
|
303
|
+
rank: LANE_RANK[before] + LANE_ORDER.length,
|
|
304
|
+
id: Number(key),
|
|
305
|
+
text: `project #${key} is no longer registered (was ${laneName(before)})`,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
changes.sort((a, b) => a.rank - b.rank || a.id - b.id);
|
|
309
|
+
const body = changes.length ? `${changes.map((c) => c.text).join("; ")}.` : "No project changed lanes since the brief.";
|
|
310
|
+
return `<p class="drift"><strong>Since the brief:</strong> ${body}</p>`;
|
|
311
|
+
}
|
|
312
|
+
function briefSection(state, report, href, queenHomeId) {
|
|
313
|
+
const head = (stamp) => `<div><h2>Picks today</h2>${stamp}</div>`;
|
|
314
|
+
if (state.kind === "missing") {
|
|
315
|
+
return `<section class="card c-brief">${head("")}<p class="brief-empty">No queen brief yet.</p></section>`;
|
|
316
|
+
}
|
|
317
|
+
if (state.kind === "invalid") {
|
|
318
|
+
const msg = state.reason === "wrong_version" ? "Queen brief version is unsupported." : "Queen brief could not be read.";
|
|
319
|
+
return `<section class="card c-brief">${head("")}<p class="brief-empty">${msg}</p></section>`;
|
|
320
|
+
}
|
|
321
|
+
const { brief } = state;
|
|
322
|
+
const byId = new Map(report.projects.map((p) => [p.id, p]));
|
|
323
|
+
const written = age(brief.written_at, report.as_of);
|
|
324
|
+
const briefAge = written === "now" ? "just now" : `${written} ago`;
|
|
325
|
+
const stale = (parseUtc(report.as_of) - parseUtc(brief.written_at)) / 1000 > 86400;
|
|
326
|
+
const stamp = `<span class="brief-stamp${stale ? " brief-stale" : ""}" title="written ${esc(brief.written_at)} UTC">` +
|
|
327
|
+
`written ${esc(briefAge)}</span>`;
|
|
328
|
+
const picks = brief.picks
|
|
329
|
+
.map((k) => {
|
|
330
|
+
const p = byId.get(k.project_id);
|
|
331
|
+
const who = p ? projectLink(p, href) : `<span class="proj">#${k.project_id} (not registered)</span>`;
|
|
332
|
+
const todo = k.todo_id === null ? "" : ` <span class="ref">#${k.todo_id}</span>`;
|
|
333
|
+
return `<li>${who}${todo} ${esc(k.action)}<span class="why">${esc(k.reason)}</span></li>`;
|
|
334
|
+
})
|
|
335
|
+
.join("");
|
|
336
|
+
return (`<section class="card c-brief">${head(stamp)}` +
|
|
337
|
+
`<p class="brief-summary">${esc(brief.summary)}</p>` +
|
|
338
|
+
(picks ? `<ol>${picks}</ol>` : "") +
|
|
339
|
+
`${driftLine(brief, report, queenHomeId)}</section>`);
|
|
340
|
+
}
|
|
341
|
+
const CSS = `
|
|
342
|
+
:root { color-scheme: light;
|
|
343
|
+
--font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
344
|
+
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
345
|
+
--bg: #f5f7fa; --panel: #ffffff; --panel-sunken: #f1f4f8;
|
|
346
|
+
--fg: #171a1f; --fg-muted: #5c6672; --fg-subtle: #6e7885;
|
|
347
|
+
--border: #e4e8ee; --border-strong: #d2d8e0;
|
|
348
|
+
--ok: #0f7a4d; --warn: #8a5300; --fail: #b4271c; --live: #1f5fd0;
|
|
349
|
+
--ok-bg: #e6f4ec; --warn-bg: #fbf0dd; --fail-bg: #fceceb; --live-bg: #e8f0fd;
|
|
350
|
+
--accent: #1f5fd0; --focus: #1f5fd0; --hover: #f1f4f8;
|
|
351
|
+
--shadow: 0 1px 2px rgba(19, 26, 38, 0.06), 0 2px 8px rgba(19, 26, 38, 0.05);
|
|
352
|
+
--card-border: transparent; --r-card: 14px; --r-ctl: 9px; --r-pill: 999px;
|
|
353
|
+
--ease-out: cubic-bezier(0.23, 1, 0.32, 1); }
|
|
354
|
+
@media (prefers-color-scheme: dark) {
|
|
355
|
+
:root:not([data-theme="light"]) { color-scheme: dark;
|
|
356
|
+
--bg: #101317; --panel: #171b21; --panel-sunken: #1c2129;
|
|
357
|
+
--fg: #e8ebef; --fg-muted: #9aa5b1; --fg-subtle: #8b95a1;
|
|
358
|
+
--border: #262c35; --border-strong: #333b46;
|
|
359
|
+
--ok: #4cc98d; --warn: #e0a548; --fail: #f0736a; --live: #6aa6ee;
|
|
360
|
+
--ok-bg: #14291f; --warn-bg: #2c2213; --fail-bg: #2e1a19; --live-bg: #16233a;
|
|
361
|
+
--accent: #6aa6ee; --focus: #6aa6ee; --hover: #1c2129; --shadow: none; --card-border: #262c35; } }
|
|
362
|
+
:root[data-theme="dark"] { color-scheme: dark;
|
|
363
|
+
--bg: #101317; --panel: #171b21; --panel-sunken: #1c2129;
|
|
364
|
+
--fg: #e8ebef; --fg-muted: #9aa5b1; --fg-subtle: #8b95a1;
|
|
365
|
+
--border: #262c35; --border-strong: #333b46;
|
|
366
|
+
--ok: #4cc98d; --warn: #e0a548; --fail: #f0736a; --live: #6aa6ee;
|
|
367
|
+
--ok-bg: #14291f; --warn-bg: #2c2213; --fail-bg: #2e1a19; --live-bg: #16233a;
|
|
368
|
+
--accent: #6aa6ee; --focus: #6aa6ee; --hover: #1c2129; --shadow: none; --card-border: #262c35; }
|
|
369
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
370
|
+
body { font-family: var(--font-sans); font-size: 0.9375rem; line-height: 1.5; margin: 0; padding-block: 0 4rem;
|
|
371
|
+
background: var(--bg); color: var(--fg); accent-color: var(--accent); }
|
|
372
|
+
a { color: inherit; }
|
|
373
|
+
a:focus-visible, button:focus-visible, input:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
|
374
|
+
.wrap { max-width: 76rem; margin: 0 auto; padding-inline: 1.25rem; }
|
|
375
|
+
@media (max-width: 30rem) { .wrap { padding-inline: 1rem; } }
|
|
376
|
+
.topbar { border-bottom: 1px solid var(--border); background: var(--bg); position: sticky; top: 0; z-index: 5; }
|
|
377
|
+
.topbar-inner { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; padding-block: 0.8rem 0.7rem; }
|
|
378
|
+
.brand { display: flex; align-items: center; gap: 0.7rem; min-width: 0; }
|
|
379
|
+
.mark { width: 1.75rem; height: 1.75rem; flex: 0 0 auto; color: var(--accent); }
|
|
380
|
+
h1 { font-size: 1.0625rem; font-weight: 650; letter-spacing: -0.011em; margin: 0; }
|
|
381
|
+
.generated { color: var(--fg-muted); font-size: 0.78125rem; margin: 0.1rem 0 0; }
|
|
382
|
+
.live-toggle { display: inline-flex; align-items: center; gap: 0.45rem; cursor: pointer; font-size: 0.8125rem; font-weight: 500; color: var(--fg-muted); user-select: none; }
|
|
383
|
+
.live-toggle input { appearance: none; -webkit-appearance: none; width: 2.15rem; height: 1.2rem; background: var(--border-strong); border-radius: var(--r-pill); position: relative; margin: 0; cursor: pointer; transition: background-color 160ms var(--ease-out); }
|
|
384
|
+
.live-toggle input::before { content: ""; position: absolute; top: 2px; left: 2px; width: 1rem; height: 1rem; border-radius: 50%; background: var(--panel); box-shadow: 0 1px 2px rgba(19, 26, 38, 0.28); transition: transform 160ms var(--ease-out); }
|
|
385
|
+
.live-toggle input:checked { background: var(--accent); }
|
|
386
|
+
.live-toggle input:checked::before { transform: translateX(0.95rem); }
|
|
387
|
+
h2 { font-size: 0.9375rem; font-weight: 650; margin: 0; letter-spacing: -0.005em; display: flex; align-items: baseline; gap: 0.5rem; }
|
|
388
|
+
h2 .count { font-family: var(--font-mono); font-size: 0.78125rem; color: var(--fg-subtle); font-weight: 500; }
|
|
389
|
+
.card { background: var(--panel); border: 1px solid var(--card-border); border-radius: var(--r-card); box-shadow: var(--shadow); }
|
|
390
|
+
.card-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; flex-wrap: wrap; padding: 0.9rem 1.1rem 0.5rem; }
|
|
391
|
+
.card-sub { font-size: 0.78125rem; color: var(--fg-muted); }
|
|
392
|
+
.status { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.71875rem; font-weight: 600; padding: 0.12rem 0.5rem; border-radius: var(--r-pill); white-space: nowrap; }
|
|
393
|
+
.status::before { content: ""; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: currentColor; }
|
|
394
|
+
.status-ok { color: var(--ok); background: var(--ok-bg); }
|
|
395
|
+
.status-warn { color: var(--warn); background: var(--warn-bg); }
|
|
396
|
+
.status-fail { color: var(--fail); background: var(--fail-bg); }
|
|
397
|
+
.status-off { color: var(--fg-subtle); background: var(--panel-sunken); }
|
|
398
|
+
.proj { font-weight: 600; text-decoration: none; overflow-wrap: anywhere; }
|
|
399
|
+
a.proj:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
|
|
400
|
+
.ref { font-family: var(--font-mono); font-size: 0.8125rem; color: var(--fg-subtle); }
|
|
401
|
+
.age { font-family: var(--font-mono); font-size: 0.8125rem; color: var(--fg-muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
|
|
402
|
+
.age-hot { color: var(--warn); font-weight: 600; }
|
|
403
|
+
.age-fail { color: var(--fail); font-weight: 600; }
|
|
404
|
+
.brief-stamp { font-size: 0.78125rem; font-weight: 400; color: var(--fg-muted); }
|
|
405
|
+
.brief-stamp.brief-stale { color: var(--warn); }
|
|
406
|
+
.drift { margin: 0.5rem 0 0; grid-column: 1 / -1; padding: 0.55rem 0.75rem; border-radius: var(--r-ctl); background: var(--warn-bg); color: var(--fg); font-size: 0.8125rem; }
|
|
407
|
+
.drift strong { color: var(--warn); font-weight: 650; }
|
|
408
|
+
.grid { display: grid; }
|
|
409
|
+
.g-row { display: grid; grid-template-columns: minmax(8.5rem, 1.3fr) 10.5rem minmax(8rem, 1.2fr) repeat(4, 3.2rem) 4.6rem 5.2rem 5.6rem; align-items: center; gap: 0.6rem; padding: 0.5rem 1.1rem; border-top: 1px solid var(--border); font-size: 0.875rem; }
|
|
410
|
+
.g-row:hover:not(.g-head) { background: var(--hover); }
|
|
411
|
+
.g-head { font-size: 0.75rem; color: var(--fg-subtle); font-weight: 550; border-top: 0; padding-block: 0.2rem 0.4rem; }
|
|
412
|
+
.g-num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; text-align: right; }
|
|
413
|
+
.g-head .g-num { font-family: var(--font-sans); }
|
|
414
|
+
.zero { color: var(--fg-subtle); opacity: 0.55; }
|
|
415
|
+
.n-high { color: var(--fail); font-weight: 650; }
|
|
416
|
+
.n-muted { color: var(--fg-muted); }
|
|
417
|
+
.n-blocked { color: var(--warn); font-weight: 650; }
|
|
418
|
+
.n-needs { color: var(--warn); font-weight: 700; }
|
|
419
|
+
.workers { display: flex; gap: 0.45rem; flex-wrap: wrap; font-size: 0.8125rem; color: var(--fg-muted); }
|
|
420
|
+
.w { display: inline-flex; align-items: center; gap: 0.25rem; white-space: nowrap; }
|
|
421
|
+
.w::before { content: ""; width: 0.45rem; height: 0.45rem; border-radius: 50%; background: currentColor; }
|
|
422
|
+
.w-work { color: var(--live); }
|
|
423
|
+
.w-idle { color: var(--fg-subtle); }
|
|
424
|
+
.w-input { color: var(--warn); font-weight: 600; }
|
|
425
|
+
.g-name { display: flex; flex-direction: column; min-width: 0; }
|
|
426
|
+
.g-note { font-size: 0.75rem; color: var(--fg-subtle); }
|
|
427
|
+
.c-nums { display: none !important; gap: 0.35rem 0.8rem; flex-wrap: wrap; font-size: 0.8125rem; color: var(--fg-muted); }
|
|
428
|
+
.c-nums span { white-space: nowrap; }
|
|
429
|
+
@media (max-width: 52rem) {
|
|
430
|
+
.g-head { display: none; }
|
|
431
|
+
.g-row { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "name last" "lead lead" "work work" "nums nums"; gap: 0.35rem 0.75rem; padding-block: 0.75rem; }
|
|
432
|
+
.g-row > .c-name { grid-area: name; }
|
|
433
|
+
.g-row > .c-last { grid-area: last; }
|
|
434
|
+
.g-row > .c-lead { grid-area: lead; }
|
|
435
|
+
.g-row > .c-work { grid-area: work; }
|
|
436
|
+
.g-row > .c-n { display: none; }
|
|
437
|
+
.g-row > .c-nums { grid-area: nums; display: flex !important; }
|
|
438
|
+
}
|
|
439
|
+
.freshnote { font-size: 0.75rem; color: var(--fg-subtle); padding: 0.6rem 1.1rem 0.9rem; margin: 0; }
|
|
440
|
+
.c-brief { margin-top: 1.25rem; padding: 0.85rem 1.1rem; display: grid; grid-template-columns: minmax(0, 15rem) minmax(0, 1fr); gap: 0.4rem 1.25rem; }
|
|
441
|
+
.c-brief ol { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.9rem; counter-reset: pick; }
|
|
442
|
+
.c-brief > div:first-child { grid-row: 1 / span 2; }
|
|
443
|
+
.c-brief ol { grid-column: 2; }
|
|
444
|
+
.c-brief li { counter-increment: pick; font-size: 0.875rem; min-width: 0; overflow-wrap: anywhere; }
|
|
445
|
+
.c-brief li::before { content: counter(pick); font-family: var(--font-mono); font-weight: 650; color: var(--accent); margin-right: 0.4rem; }
|
|
446
|
+
.c-brief .why { display: block; color: var(--fg-muted); font-size: 0.8125rem; margin-top: 0.15rem; }
|
|
447
|
+
.brief-summary, .brief-empty { margin: 0; font-size: 0.875rem; color: var(--fg-muted); grid-column: 2; }
|
|
448
|
+
.brief-summary { color: var(--fg); }
|
|
449
|
+
@media (max-width: 52rem) {
|
|
450
|
+
.c-brief { grid-template-columns: minmax(0, 1fr); }
|
|
451
|
+
.c-brief > div:first-child { grid-row: auto; }
|
|
452
|
+
.c-brief ol { grid-template-columns: minmax(0, 1fr); gap: 0.6rem; grid-column: 1; }
|
|
453
|
+
.brief-summary, .brief-empty { grid-column: 1; }
|
|
454
|
+
}
|
|
455
|
+
.lanes { display: grid; grid-template-columns: 1.15fr 1.15fr 1fr 0.8fr; gap: 1rem; margin-top: 1rem; align-items: start; }
|
|
456
|
+
@media (max-width: 64rem) { .lanes { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }
|
|
457
|
+
@media (max-width: 36rem) { .lanes { grid-template-columns: minmax(0, 1fr); } }
|
|
458
|
+
.lane { display: grid; gap: 0.6rem; min-width: 0; }
|
|
459
|
+
.lane-head { display: flex; align-items: baseline; gap: 0.5rem; padding: 0 0.2rem; }
|
|
460
|
+
.lane-head h2::before { content: ""; display: inline-block; width: 0.55rem; height: 0.55rem; border-radius: 50%; margin-right: 0.1rem; background: var(--lane); align-self: center; }
|
|
461
|
+
.lane-rule { font-size: 0.75rem; color: var(--fg-subtle); padding: 0 0.2rem; margin: -0.4rem 0 0; }
|
|
462
|
+
.pc { padding: 0.8rem 0.95rem; display: grid; gap: 0.45rem; border-top: 3px solid var(--lane); }
|
|
463
|
+
.pc-top { display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem; }
|
|
464
|
+
.pc-badges { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
|
|
465
|
+
.pc-items { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.35rem; }
|
|
466
|
+
.pc-items li { font-size: 0.8125rem; display: flex; justify-content: space-between; gap: 0.6rem; }
|
|
467
|
+
.pc-items li span:first-child { min-width: 0; overflow-wrap: anywhere; }
|
|
468
|
+
.pc-foot { display: flex; gap: 0.3rem 0.8rem; flex-wrap: wrap; font-size: 0.75rem; color: var(--fg-muted); padding-top: 0.4rem; border-top: 1px solid var(--border); text-decoration: none; border-radius: 0 0 var(--r-ctl) var(--r-ctl); }
|
|
469
|
+
.pc-foot span { white-space: nowrap; }
|
|
470
|
+
.pc-foot:hover { color: var(--accent); }
|
|
471
|
+
.pc-foot::after { content: "row"; margin-left: auto; color: var(--fg-subtle); font-size: 0.71875rem; }
|
|
472
|
+
.pc-foot:hover::after { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
|
|
473
|
+
.pc-pick { font-size: 0.71875rem; font-weight: 650; color: var(--accent); background: var(--live-bg); border-radius: var(--r-pill); padding: 0.05rem 0.45rem; white-space: nowrap; }
|
|
474
|
+
.quiet-list { list-style: none; margin: 0; padding: 0.3rem 0; }
|
|
475
|
+
.quiet-list li { display: flex; justify-content: space-between; gap: 0.6rem; align-items: baseline; padding: 0.45rem 0.95rem; border-top: 1px solid var(--border); font-size: 0.8125rem; }
|
|
476
|
+
.quiet-list li:first-child { border-top: 0; }
|
|
477
|
+
.quiet-list .sub { display: block; color: var(--fg-subtle); font-size: 0.75rem; text-decoration: none; }
|
|
478
|
+
a.sub:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
|
|
479
|
+
.turn { font-size: 0.75rem; color: var(--fg-muted); }
|
|
480
|
+
.turn-work { color: var(--live); }
|
|
481
|
+
.c-audit { margin-top: 1.5rem; padding-bottom: 0.85rem; }
|
|
482
|
+
.c-audit .audit-list, .c-audit .brief-empty { padding: 0 1.1rem; }
|
|
483
|
+
.audit-list { list-style: none; margin: 0.5rem 0 0; padding: 0; display: grid; gap: 0.4rem; font-size: 0.8125rem; }
|
|
484
|
+
.audit-list li { display: flex; flex-wrap: wrap; gap: 0.15rem 0.75rem; align-items: baseline; }
|
|
485
|
+
.audit-op { font-weight: 600; }
|
|
486
|
+
.audit-sum { color: var(--fg-muted); min-width: 0; overflow-wrap: anywhere; }
|
|
487
|
+
.d-grid { margin-top: 1.5rem; }
|
|
488
|
+
.d-grid .g-row:not(.g-head) { box-shadow: inset 3px 0 0 var(--lane, transparent); }
|
|
489
|
+
.d-grid .g-row { scroll-margin-top: 6rem; transition: background-color 600ms ease; }
|
|
490
|
+
.d-grid .g-row.is-flash { background: var(--live-bg); transition: none; }
|
|
491
|
+
.empty { margin-top: 1.25rem; padding: 1.25rem 1.1rem; color: var(--fg-muted); }
|
|
492
|
+
@media (prefers-reduced-motion: reduce) { * { transition: none !important; } }
|
|
493
|
+
`;
|
|
494
|
+
const SCRIPT = `(function () {
|
|
495
|
+
var KEY = "queen-autoreload", box = document.getElementById("autoreload"), timer = null;
|
|
496
|
+
function arm() { clearTimeout(timer); timer = box.checked ? setTimeout(function () { location.reload(); }, 60000) : null; }
|
|
497
|
+
box.checked = true;
|
|
498
|
+
try { box.checked = sessionStorage.getItem(KEY) !== "0"; } catch (e) {}
|
|
499
|
+
box.addEventListener("change", function () { try { sessionStorage.setItem(KEY, box.checked ? "1" : "0"); } catch (e) {} arm(); });
|
|
500
|
+
arm();
|
|
501
|
+
document.addEventListener("click", function (e) {
|
|
502
|
+
var j = e.target.closest && e.target.closest("[data-jump]");
|
|
503
|
+
if (!j) return;
|
|
504
|
+
var row = document.getElementById("row-" + j.getAttribute("data-jump"));
|
|
505
|
+
if (!row) return;
|
|
506
|
+
e.preventDefault();
|
|
507
|
+
var still = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
508
|
+
row.scrollIntoView({ behavior: still ? "auto" : "smooth", block: "center" });
|
|
509
|
+
row.classList.add("is-flash");
|
|
510
|
+
setTimeout(function () { row.classList.remove("is-flash"); }, 900);
|
|
511
|
+
});
|
|
512
|
+
})();`;
|
|
513
|
+
const MARK = '<symbol id="mark" viewBox="0 0 32 32"><path d="M16 3.2 27 9.6v12.8L16 28.8 5 22.4V9.6Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M16 11 21 14v6l-5 3-5-3v-6Z" fill="currentColor" fill-opacity="0.22" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round"/></symbol>';
|
|
514
|
+
export const QUEEN_GENERATED_MARKER = "data-generated";
|
|
515
|
+
function auditSection(audit, report) {
|
|
516
|
+
const names = new Map(report.projects.map((p) => [p.id, p.name]));
|
|
517
|
+
const body = audit.length
|
|
518
|
+
? '<ul class="audit-list">' +
|
|
519
|
+
audit
|
|
520
|
+
.map((a) => `<li><span class="age">${esc(a.created_at)} UTC</span>` +
|
|
521
|
+
`<span class="proj">${esc(names.get(a.target_project_id) ?? `project #${a.target_project_id}`)}</span>` +
|
|
522
|
+
`<span class="audit-op">${esc(a.operation)}</span>` +
|
|
523
|
+
`<span class="ref">${esc(a.resource_type)} #${a.resource_id}</span>` +
|
|
524
|
+
`<span class="audit-sum">${esc(a.summary)}</span></li>`)
|
|
525
|
+
.join("") +
|
|
526
|
+
"</ul>"
|
|
527
|
+
: '<p class="brief-empty">No queen actions recorded yet.</p>';
|
|
528
|
+
return `<section class="card c-audit"><div class="card-head"><h2>Recent queen actions</h2><span class="card-sub">the newest writes the queen made into other projects</span></div>${body}</section>`;
|
|
529
|
+
}
|
|
530
|
+
export function renderQueenDashboard(report, briefState, href = (p) => dashboardHref(p.root), audit = [], queenHomeId = null) {
|
|
531
|
+
const asOf = report.as_of;
|
|
532
|
+
const byLane = new Map(LANE_ORDER.map((l) => [l, []]));
|
|
533
|
+
for (const p of report.projects)
|
|
534
|
+
byLane.get(p.lane)?.push(p);
|
|
535
|
+
const ordered = LANE_ORDER.flatMap((l) => byLane.get(l) ?? []);
|
|
536
|
+
const rank = new Map();
|
|
537
|
+
if (briefState.kind === "ready") {
|
|
538
|
+
briefState.brief.picks.forEach((k, i) => {
|
|
539
|
+
if (!rank.has(k.project_id))
|
|
540
|
+
rank.set(k.project_id, i + 1);
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
const n = report.totals.projects;
|
|
544
|
+
const lanes = LANE_ORDER.map((lane) => {
|
|
545
|
+
const list = byLane.get(lane) ?? [];
|
|
546
|
+
const body = lane === "quiet"
|
|
547
|
+
? list.length
|
|
548
|
+
? quietList(list, asOf, href)
|
|
549
|
+
: ""
|
|
550
|
+
: list.map((p) => card(p, report, rank.get(p.id) ?? 0, href)).join("");
|
|
551
|
+
return (`<section class="lane" data-lane="${lane}" style="--lane:${LANE_COLOR[lane]}"><div class="lane-head"><h2>${LANE_TITLE[lane]} ` +
|
|
552
|
+
`<span class="count">${report.totals.lanes[lane]}</span></h2></div>` +
|
|
553
|
+
`<p class="lane-rule">${LANE_RULE[lane]}</p>${body}</section>`);
|
|
554
|
+
}).join("");
|
|
555
|
+
const main = n === 0
|
|
556
|
+
? `${briefSection(briefState, report, href, queenHomeId)}<section class="card empty"><h2>No projects registered</h2><p>Run <code>hive lead</code> in a project folder to register it, and it will appear here.</p></section>`
|
|
557
|
+
: `${briefSection(briefState, report, href, queenHomeId)}<div class="lanes">${lanes}</div>` +
|
|
558
|
+
`<section class="card d-grid"><div class="card-head"><h2>Every project <span class="count">${n}</span></h2>` +
|
|
559
|
+
'<span class="card-sub">in lane order; a name opens that project’s dashboard</span></div>' +
|
|
560
|
+
`<div class="grid" role="table">${GRID_HEAD}${ordered.map((p) => gridRow(p, asOf, href)).join("")}</div>` +
|
|
561
|
+
'<p class="freshnote">Every row here is read from the store when the page is written. Only the picks are written by a model, which is why they carry a time.</p></section>' +
|
|
562
|
+
auditSection(audit, report);
|
|
563
|
+
return ('<!doctype html><html lang="en"><head><meta charset="utf-8">' +
|
|
564
|
+
'<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">' +
|
|
565
|
+
`<title>All projects</title><style>${CSS}</style></head><body>` +
|
|
566
|
+
`<svg style="position:absolute;width:0;height:0" aria-hidden="true"><defs>${MARK}</defs></svg>` +
|
|
567
|
+
'<header class="topbar"><div class="wrap topbar-inner"><div class="brand">' +
|
|
568
|
+
'<svg class="mark" role="img" aria-label="hive"><use href="#mark"/></svg><div><h1>All projects</h1>' +
|
|
569
|
+
`<p class="generated" ${QUEEN_GENERATED_MARKER}>Live rows as of ${esc(asOf)} UTC. ${n} project${n === 1 ? "" : "s"} on this machine.</p></div></div>` +
|
|
570
|
+
'<label class="live-toggle" title="Off stops this page reloading. The scheduler still rewrites the file.">' +
|
|
571
|
+
'<input type="checkbox" id="autoreload"> Reload every minute</label></div></header>' +
|
|
572
|
+
`<main class="wrap">${main}</main><script>${SCRIPT}</script></body></html>\n`);
|
|
573
|
+
}
|