create-pmos 0.46.0 → 0.48.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/package.json +1 -1
- package/payload/AGENTS-HUMAN.md +1 -1
- package/payload/AGENTS.md +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/mods/pmos-cockpit/.claude-plugin/plugin.json +1 -1
- package/payload/mods/pmos-cockpit/README.md +13 -3
- package/payload/mods/pmos-cockpit/hooks/lib/commands.mjs +34 -6
- package/payload/mods/pmos-cockpit/hooks/lib/grants.mjs +22 -3
- package/payload/mods/pmos-cockpit/hooks/lib/guard.mjs +16 -3
- package/payload/mods/pmos-cockpit/hooks/lib/handover-run.mjs +33 -0
- package/payload/mods/pmos-cockpit/hooks/lib/handover.mjs +267 -0
- package/payload/mods/pmos-cockpit/hooks/register.tsx +189 -18
- package/payload/mods/pmos-cockpit/types/index.d.ts +16 -1
- package/payload/okf/core/concepts/frontmatter-format.md +6 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/pmos-log.sh +18 -8
- package/payload/scripts/pmos-model.js +233 -2
- package/payload/scripts/status-facts.js +204 -0
- package/payload/scripts/workflow-state.js +52 -0
- package/payload/skills/SKILL-FORMAT.md +1 -0
- package/payload/state/discoveries.md +1 -1
- package/workspace/README.md +3 -3
- package/workspace/bin/live-notes.js +133 -0
- package/workspace/design-parity.js +328 -21
- package/workspace/doc-category-bench.js +165 -0
- package/workspace/fixtures/doc-category-labelled.json +167 -0
- package/workspace/guide/initiatives-and-next-steps.md +16 -2
- package/workspace/guide/knowledge-graph.md +89 -2
- package/workspace/guide/layout.md +30 -2
- package/workspace/guide/proactive-engine.md +26 -4
- package/workspace/guide/scheduled-tasks-and-cli.md +1 -0
- package/workspace/guide/statuses.md +1 -0
- package/workspace/guide/validate-with-typesafe.md +17 -1
- package/workspace/lib/claims.js +51 -2
- package/workspace/lib/doc-category-corrections.js +113 -0
- package/workspace/lib/doc-category-writeback.js +110 -0
- package/workspace/lib/doc-category.js +225 -0
- package/workspace/lib/engine/pmos-model.js +233 -2
- package/workspace/lib/engine/status-facts.js +204 -0
- package/workspace/lib/engine/workflow-state.js +52 -0
- package/workspace/lib/jev-categorize.js +399 -0
- package/workspace/lib/jev-claims.js +59 -15
- package/workspace/lib/jev-route.js +129 -0
- package/workspace/lib/kg-cli.js +5 -2
- package/workspace/lib/kg-dangling-baseline.json +1 -0
- package/workspace/lib/kg-dangling-rules.json +1 -0
- package/workspace/lib/kg.js +26 -9
- package/workspace/lib/learn.js +282 -0
- package/workspace/lib/live-notes.js +890 -0
- package/workspace/lib/proactive-cli.js +49 -1
- package/workspace/lib/proactive.js +8 -7
- package/workspace/lib/typesafe-settings.js +10 -2
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +5 -4
- package/workspace/public/pm/app.js +148 -79
- package/workspace/public/pm/data.js +58 -0
- package/workspace/public/pm/fixture.js +40 -2
- package/workspace/public/pm/modes-live.js +267 -21
- package/workspace/public/pm/panel-cockpit.js +636 -0
- package/workspace/public/pm/panel-detail.js +210 -8
- package/workspace/public/pm/panel-initiatives.js +9 -9
- package/workspace/public/pm/panel-newsess.js +145 -86
- package/workspace/public/pm/panel-repos.js +34 -17
- package/workspace/public/pm/panel-tabs.js +145 -27
- package/workspace/public/pm/state.js +45 -4
- package/workspace/public/pm/tab-doc.js +244 -16
- package/workspace/public/pm/tab-term.js +11 -1
- package/workspace/public/pm/tokens.css +4 -3
- package/workspace/public/pm/view-addrepo.js +207 -0
- package/workspace/public/pm/view-files.js +197 -0
- package/workspace/public/pm/view-guide.js +141 -0
- package/workspace/public/pm/view-knowledge.js +225 -0
- package/workspace/public/pm/view-memory.js +122 -0
- package/workspace/public/pm/view-portfolio.js +153 -0
- package/workspace/public/pm/view-reports.js +190 -0
- package/workspace/public/pm/view-scheduled.js +161 -0
- package/workspace/public/pm/view-skills.js +216 -0
- package/workspace/public/pm/view-today.js +168 -0
- package/workspace/public/pm/view-validate.js +171 -0
- package/workspace/server.js +394 -10
- package/workspace/skills/live-notes.skill +58 -0
- package/workspace/skills/morning-brief.skill +1 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/* pm/view-scheduled.js — the Scheduled view tab, drawn from planning/design/cockpit-mockup/ViewScheduled.dc.html.
|
|
2
|
+
*
|
|
3
|
+
* PRD workspace-cockpit-nav, Run 3 (R4). The board is a header, a table of schedules (Task, Skill, Cadence, Next run, Last run,
|
|
4
|
+
* On, and the actions) and a closing note.
|
|
5
|
+
*
|
|
6
|
+
* The rows are the view's existing list (modes-live.js `scheduledLive`): each `data-r4="schedule"` row is laid out as a table
|
|
7
|
+
* row in place, and its own "run now" and "delete" buttons sit in the last column, so every `data-r4`, `data-r7` and `data-r11`
|
|
8
|
+
* hook and every live behaviour stays as it was. The list's set-up buttons (the recommended schedules and the special report)
|
|
9
|
+
* move into the board's toolbar, and its run note moves under it. No node is removed. The row's "next … · last …" line is
|
|
10
|
+
* split into the Next run and Last run columns; the line itself stays in the row, hidden, as the source of both.
|
|
11
|
+
*
|
|
12
|
+
* The Skill column and the tone of the Last run badge come from the board's own read of GET /api/schedules?all=1, the read
|
|
13
|
+
* the list uses. A row is matched by its name and its last run, so a stale read shows no skill and a plain badge rather than
|
|
14
|
+
* a wrong one. The board's on/off switch is drawn as a plain on/off badge: the list has no control that turns a schedule on
|
|
15
|
+
* or off, so the board does not offer one. The board's "+ New schedule" has no form in this app, so it is not drawn. In the
|
|
16
|
+
* fixture the prototype's list is drawn inside the table frame, unchanged.
|
|
17
|
+
*/
|
|
18
|
+
import { el } from './dom.js';
|
|
19
|
+
|
|
20
|
+
let sched = { key: null, data: null, busy: false }; // this board's read of /api/schedules?all=1
|
|
21
|
+
|
|
22
|
+
const BTN = 'font:500 12.5px var(--sans);background:transparent;color:var(--foreground);border:1px solid var(--input);border-radius:8px;padding:0 12px;height:30px;min-height:30px;display:inline-grid;place-items:center;cursor:pointer;flex:none;white-space:nowrap';
|
|
23
|
+
const BTNP = 'font:500 12.5px var(--sans);background:var(--foreground);color:var(--background);border:0;border-radius:8px;padding:0 12px;height:30px;min-height:30px;display:inline-grid;place-items:center;cursor:pointer;flex:none;white-space:nowrap';
|
|
24
|
+
const SMALL = 'font:500 11.5px var(--sans);background:transparent;color:var(--muted-foreground);border:1px solid var(--border);border-radius:6px;padding:0 9px;height:24px;min-height:24px;display:inline-grid;place-items:center;cursor:pointer;flex:none';
|
|
25
|
+
const BADGE = 'font:500 11px var(--sans);border:1px solid var(--border);border-radius:999px;padding:0 8px;line-height:18px;white-space:nowrap;display:inline-block';
|
|
26
|
+
const TONE = {
|
|
27
|
+
s: ';border-color:transparent;background:color-mix(in oklab, var(--success) 16%, transparent);color:var(--success)',
|
|
28
|
+
w: ';border-color:transparent;background:color-mix(in oklab, var(--warning) 16%, transparent);color:var(--warning)',
|
|
29
|
+
d: ';border-color:transparent;background:color-mix(in oklab, var(--destructive) 18%, transparent);color:var(--destructive)',
|
|
30
|
+
};
|
|
31
|
+
const LBL = 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground)';
|
|
32
|
+
const NOTE = 'color:var(--muted-foreground);font-size:13px;line-height:1.5;padding:0';
|
|
33
|
+
const COLS = ['Task', 'Skill', 'Cadence', 'Next run', 'Last run', 'On', ''];
|
|
34
|
+
const GRID = 'display:grid;grid-template-columns:minmax(110px,1.5fr) minmax(90px,1fr) minmax(90px,1fr) minmax(90px,1fr) minmax(100px,1fr) 52px 168px;align-items:center;width:100%;box-sizing:border-box;text-align:left';
|
|
35
|
+
const CELL = 'padding:12px 12px;font-size:13px;grid-row:1;min-width:0;overflow-wrap:anywhere';
|
|
36
|
+
const TH = LBL + ';padding:10px 12px;grid-row:1';
|
|
37
|
+
|
|
38
|
+
const badge = (text, tone, attrs) => el('span', { ...(attrs || {}), style: BADGE + (tone ? TONE[tone] : '') }, String(text));
|
|
39
|
+
const stamp = (v) => (v ? String(v).slice(0, 16).replace('T', ' ') : null);
|
|
40
|
+
const nameOf = (s) => String(s.label || s.name || s.task || s.skill || s.id || '');
|
|
41
|
+
|
|
42
|
+
/** The board's own read of the schedules. It is read again only when the rows change (a new, removed or re-run schedule), so a
|
|
43
|
+
* redraw never loops on it. */
|
|
44
|
+
function schedules(ctx, key) {
|
|
45
|
+
if (ctx.FIXTURE) return null;
|
|
46
|
+
if (sched.key !== key && !sched.busy) {
|
|
47
|
+
sched.key = key; sched.busy = true;
|
|
48
|
+
fetch('/api/schedules?all=1').then((r) => r.json()).then(
|
|
49
|
+
(d) => { sched.data = (d && d.schedules) || null; },
|
|
50
|
+
() => { sched.data = null; },
|
|
51
|
+
).finally(() => { sched.busy = false; ctx.setState({}); });
|
|
52
|
+
}
|
|
53
|
+
return sched.data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Lay out one existing row as a table row. `s` is the matching schedule from the board's read, or null. */
|
|
57
|
+
function restyleRow(row, s) {
|
|
58
|
+
row.setAttribute('style', GRID + ';border-bottom:1px solid var(--border)');
|
|
59
|
+
const [top, line, btns] = row.children;
|
|
60
|
+
let name = null; let cadence = null;
|
|
61
|
+
if (top) {
|
|
62
|
+
top.setAttribute('style', 'display:contents');
|
|
63
|
+
[name, cadence] = top.children;
|
|
64
|
+
if (name) { name.setAttribute('style', CELL + ';grid-column:1;font-weight:600;color:var(--foreground)'); name.setAttribute('data-board-cell', 'task'); }
|
|
65
|
+
if (cadence) { cadence.setAttribute('style', CELL + ';grid-column:3;color:var(--foreground)'); cadence.setAttribute('data-board-cell', 'cadence'); }
|
|
66
|
+
}
|
|
67
|
+
const text = line ? (line.textContent || '') : '';
|
|
68
|
+
const m = /^next (.*) · last (.*)$/.exec(text.trim());
|
|
69
|
+
const next = m ? m[1] : ''; const last = m ? m[2] : '';
|
|
70
|
+
if (line) { line.style.display = 'none'; line.setAttribute('data-board-source', 'next-last'); }
|
|
71
|
+
const off = !!cadence && (cadence.textContent || '').trim() === 'disabled';
|
|
72
|
+
|
|
73
|
+
// the Last run badge: its tone comes from the read only when the read agrees with the row about the last run
|
|
74
|
+
const agrees = s && (stamp(s.lastRun) || 'never') === last;
|
|
75
|
+
let lastEl;
|
|
76
|
+
if (!m) lastEl = el('span', null, '');
|
|
77
|
+
else if (last === 'never') lastEl = el('span', { style: 'color:var(--muted-foreground)' }, 'never');
|
|
78
|
+
else if (agrees && s.lastStatus === 'missed') lastEl = badge('missed · last ' + last, 'w', { 'data-board-badge': 'last' });
|
|
79
|
+
else if (agrees && s.lastStatus === 'fail') lastEl = badge('failed ' + last, 'd', { 'data-board-badge': 'last' });
|
|
80
|
+
else if (agrees && (s.lastStatus === 'ok' || s.lastStatus === 'started')) lastEl = badge('ran ' + last, 's', { 'data-board-badge': 'last' });
|
|
81
|
+
else lastEl = badge(last, null, { 'data-board-badge': 'last' });
|
|
82
|
+
|
|
83
|
+
row.appendChild(el('span', { 'data-board-cell': 'skill', style: CELL + ';grid-column:2;font:12px var(--mono);color:var(--foreground)' }, s ? String(s.skill || s.task || '') : ''));
|
|
84
|
+
row.appendChild(el('span', { 'data-board-cell': 'next', style: CELL + ';grid-column:4;color:var(--foreground)' }, next));
|
|
85
|
+
row.appendChild(el('span', { 'data-board-cell': 'last', style: CELL + ';grid-column:5' }, lastEl));
|
|
86
|
+
row.appendChild(el('span', { 'data-board-cell': 'on', style: CELL + ';grid-column:6' },
|
|
87
|
+
badge(off ? 'off' : 'on', off ? null : 's', { 'data-board-badge': 'on', title: off ? 'this schedule is turned off' : 'this schedule is on' })));
|
|
88
|
+
if (btns) {
|
|
89
|
+
btns.setAttribute('style', CELL + ';grid-column:7;display:flex;gap:6px;justify-content:flex-end;align-items:center');
|
|
90
|
+
btns.setAttribute('data-board-cell', 'actions');
|
|
91
|
+
for (const b of btns.querySelectorAll('button')) b.setAttribute('style', b.getAttribute('data-r4') === 'run' ? BTN : SMALL);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The list's set-up rows (the recommended schedules and the special report) as the board's toolbar buttons. */
|
|
96
|
+
function restyleSetup(n) {
|
|
97
|
+
n.setAttribute('style', 'display:flex;flex-direction:column;gap:6px;align-items:flex-start;max-width:100%');
|
|
98
|
+
for (const b of n.querySelectorAll('button')) b.setAttribute('style', BTN);
|
|
99
|
+
for (const c of n.children) if (c.tagName === 'SPAN') c.setAttribute('style', 'font-size:12px;line-height:1.45;color:var(--muted-foreground);max-width:420px');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function renderBoard(inner, ctx) {
|
|
103
|
+
const { FIXTURE } = ctx;
|
|
104
|
+
const drawn = el('div', null);
|
|
105
|
+
ctx.renderMode(drawn);
|
|
106
|
+
|
|
107
|
+
const actions = el('div', { 'data-board-part': 'actions', style: 'display:flex;gap:8px;flex-wrap:wrap;align-items:center' });
|
|
108
|
+
const setup = el('div', { 'data-board-part': 'toolbar', style: 'display:flex;gap:10px;flex-wrap:wrap;align-items:flex-start' });
|
|
109
|
+
const notes = el('div', { style: 'display:flex;flex-direction:column;gap:6px' });
|
|
110
|
+
const frame = el('div', { 'data-board-section': 'schedules', style: 'border:1px solid var(--border);border-radius:10px;overflow-x:auto;overflow-y:hidden;background:var(--background)' });
|
|
111
|
+
|
|
112
|
+
let count = null;
|
|
113
|
+
if (FIXTURE) {
|
|
114
|
+
// the prototype's rows, unchanged; its "+ schedule a task" is the board's primary action
|
|
115
|
+
const add = [...drawn.querySelectorAll('button')].find((b) => /schedule a task/.test(b.textContent || ''));
|
|
116
|
+
if (add) { add.setAttribute('style', BTNP); actions.appendChild(add); }
|
|
117
|
+
frame.appendChild(el('div', { style: 'padding:4px 0' }, [...drawn.childNodes]));
|
|
118
|
+
} else {
|
|
119
|
+
const wrap = drawn.children.length === 1 && !drawn.firstElementChild.hasAttribute('data-r4') ? drawn.firstElementChild : null;
|
|
120
|
+
const rows = wrap ? [...wrap.querySelectorAll(':scope > [data-r4="schedule"]')] : [];
|
|
121
|
+
if (wrap) {
|
|
122
|
+
for (const k of [...wrap.children]) {
|
|
123
|
+
if (k.hasAttribute('data-r7') || k.getAttribute('data-r11') === 'special') { restyleSetup(k); setup.appendChild(k); }
|
|
124
|
+
else if (k.getAttribute('data-r4') === 'run-note') { k.setAttribute('style', NOTE + (/destructive/.test(k.getAttribute('style') || '') ? ';color:var(--destructive)' : '')); notes.appendChild(k); }
|
|
125
|
+
else if (!k.hasAttribute('data-r4')) k.setAttribute('style', NOTE + ';padding:14px');
|
|
126
|
+
}
|
|
127
|
+
wrap.setAttribute('style', 'display:flex;flex-direction:column');
|
|
128
|
+
const key = rows.map((r) => (r.textContent || '')).join('\n');
|
|
129
|
+
const data = schedules(ctx, key) || [];
|
|
130
|
+
const used = new Set();
|
|
131
|
+
for (const row of rows) {
|
|
132
|
+
const nm = ((row.children[0] && row.children[0].children[0]) || {}).textContent || '';
|
|
133
|
+
const s = data.find((x, i) => !used.has(i) && nameOf(x) === nm.trim() && (used.add(i), true)) || null;
|
|
134
|
+
restyleRow(row, s);
|
|
135
|
+
}
|
|
136
|
+
count = rows.length;
|
|
137
|
+
if (rows.length) frame.appendChild(el('div', { 'data-board-part': 'columns', style: GRID + ';border-bottom:1px solid var(--border)' },
|
|
138
|
+
COLS.map((c, i) => el('span', { style: TH + ';grid-column:' + (i + 1) }, c))));
|
|
139
|
+
frame.appendChild(wrap);
|
|
140
|
+
} else {
|
|
141
|
+
// the read is in progress or failed: the list's own status line, in the frame
|
|
142
|
+
for (const k of [...drawn.children]) k.setAttribute('style', NOTE + ';padding:14px' + (/destructive/.test(k.getAttribute('style') || '') ? ';color:var(--destructive)' : ''));
|
|
143
|
+
frame.appendChild(el('div', null, [...drawn.childNodes]));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (count !== null) actions.appendChild(badge(count + (count === 1 ? ' schedule' : ' schedules') + ' · all repositories', null, { 'data-board-badge': 'count' }));
|
|
147
|
+
|
|
148
|
+
const header = el('div', { 'data-board-part': 'header', style: 'display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap' },
|
|
149
|
+
el('div', null,
|
|
150
|
+
el('div', { 'data-board-title': '', style: 'font:600 22px var(--sans);line-height:1.45' }, 'Scheduled tasks'),
|
|
151
|
+
el('div', { 'data-board-subtitle': '', style: 'color:var(--muted-foreground)' },
|
|
152
|
+
'Skills that run on a cadence on this machine, while the workspace runs. A missed slot is skipped, never caught up.')),
|
|
153
|
+
actions);
|
|
154
|
+
|
|
155
|
+
const setupPart = setup.children.length ? el('div', { style: 'display:flex;flex-direction:column;gap:8px' },
|
|
156
|
+
el('div', { style: LBL }, 'Set up'), setup) : null;
|
|
157
|
+
|
|
158
|
+
inner.appendChild(el('div', { 'data-board': 'scheduled', style: 'display:flex;flex-direction:column;gap:14px;padding:4px 6px;font-size:13px;line-height:1.45;color:var(--foreground)' },
|
|
159
|
+
header, setupPart, notes.children.length ? notes : null, frame,
|
|
160
|
+
el('div', { 'data-board-part': 'note', style: NOTE }, 'Times are this machine’s. A task’s result waits for you in Needs you.')));
|
|
161
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/* pm/view-skills.js — the Skills view tab, drawn from planning/design/cockpit-mockup/ViewSkills.dc.html.
|
|
2
|
+
*
|
|
3
|
+
* PRD workspace-cockpit-nav, Run 3 (R4). The board is two panes. On the left, a 340px column with the title, a switch
|
|
4
|
+
* (All, Built-in, Yours, Discovered) and the skills. On the right, the skill that waits for the person: a quarantined
|
|
5
|
+
* candidate, its text, and Approve and Reject.
|
|
6
|
+
*
|
|
7
|
+
* Both panes are the view's existing list (modes-live.js `skillsLive`, or the fixture's skills), drawn once and split: the
|
|
8
|
+
* skill rows go left, the discovery block and the candidates go right. No node is removed, so every `data-r4="skill"`,
|
|
9
|
+
* `data-r13` and `data-r7` hook and every live behaviour stays as it was: a row still opens its skill as a tab, and a
|
|
10
|
+
* candidate is still read, approved, rejected or retired by its own buttons. Only the frame is new.
|
|
11
|
+
*
|
|
12
|
+
* The switch hides the rows of the other sources. It reads each row's own source label, so it never disagrees with the list.
|
|
13
|
+
* The board's "+ New skill" has no form in this app (the server's POST /api/skills is reached from the CLI), so it is not
|
|
14
|
+
* drawn. The board's "Always produces" and "Procedure" parts belong to a skill's own tab, which a row opens. In the fixture
|
|
15
|
+
* the discovery is not read, and the right pane says so.
|
|
16
|
+
*/
|
|
17
|
+
import { el } from './dom.js';
|
|
18
|
+
|
|
19
|
+
let seg = 'all'; // the left switch: 'all' · 'builtin' · 'user' · 'discovered'
|
|
20
|
+
|
|
21
|
+
const SEGS = [['all', 'All'], ['builtin', 'Built-in'], ['user', 'Yours'], ['discovered', 'Discovered']];
|
|
22
|
+
const BTN = 'font:500 12.5px var(--sans);background:transparent;color:var(--foreground);border:1px solid var(--input);border-radius:8px;padding:0 12px;height:30px;min-height:30px;display:inline-grid;place-items:center;cursor:pointer;flex:none';
|
|
23
|
+
const BTNP = 'font:500 12.5px var(--sans);background:var(--foreground);color:var(--background);border:0;border-radius:8px;padding:0 12px;height:30px;min-height:30px;display:inline-grid;place-items:center;cursor:pointer;flex:none';
|
|
24
|
+
const BADGE = 'font:500 11px var(--sans);border:1px solid var(--border);border-radius:999px;padding:0 8px;line-height:18px;white-space:nowrap;display:inline-block;flex:none';
|
|
25
|
+
const TONE = {
|
|
26
|
+
s: ';border-color:transparent;background:color-mix(in oklab, var(--success) 16%, transparent);color:var(--success)',
|
|
27
|
+
w: ';border-color:transparent;background:color-mix(in oklab, var(--warning) 16%, transparent);color:var(--warning)',
|
|
28
|
+
d: ';border-color:transparent;background:color-mix(in oklab, var(--destructive) 18%, transparent);color:var(--destructive)',
|
|
29
|
+
};
|
|
30
|
+
const LBL = 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground)';
|
|
31
|
+
const NOTE = 'color:var(--muted-foreground);font-size:13px;line-height:1.5;padding:0';
|
|
32
|
+
const ITEM = 'display:flex;flex-direction:column;gap:2px;width:100%;box-sizing:border-box;text-align:left;padding:8px 12px;min-height:40px;border:0;border-radius:8px;background:transparent;color:var(--foreground);cursor:pointer';
|
|
33
|
+
const CARD = 'display:flex;flex-direction:column;gap:8px;width:100%;box-sizing:border-box;text-align:left;border:1px solid var(--border);border-radius:10px;padding:14px 16px;background:var(--background);min-height:0';
|
|
34
|
+
const PROC = 'white-space:pre-wrap;overflow-wrap:anywhere;margin:0;border:1px solid var(--border);border-radius:10px;background:var(--card);padding:12px 14px;font:12px/1.6 var(--mono);color:var(--muted-foreground);max-height:320px;overflow:auto';
|
|
35
|
+
|
|
36
|
+
const badge = (text, tone, attrs) => el('span', { ...(attrs || {}), style: BADGE + (tone ? TONE[tone] : '') }, String(text));
|
|
37
|
+
|
|
38
|
+
/** A row's source, read off the row itself: the live row shows it on the right of its first line. A fixture row has none. */
|
|
39
|
+
function sourceOf(row) {
|
|
40
|
+
const top = row.children[0];
|
|
41
|
+
const src = top && top.children.length > 1 ? (top.children[1].textContent || '').trim() : '';
|
|
42
|
+
return src || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Restyle one skill row in place, as the board's item: the name in bold, the source as a badge, the description below. */
|
|
46
|
+
function restyleRow(row) {
|
|
47
|
+
const active = /background:var\(--accent\)/.test(row.getAttribute('style') || '');
|
|
48
|
+
row.setAttribute('style', ITEM + (active ? ';background:var(--accent)' : ''));
|
|
49
|
+
const [top, desc] = row.children;
|
|
50
|
+
if (top) {
|
|
51
|
+
top.setAttribute('style', 'display:flex;align-items:center;gap:8px;min-width:0');
|
|
52
|
+
const [name, src] = top.children.length > 1 ? top.children : [top, null];
|
|
53
|
+
if (name && name !== top) name.setAttribute('style', 'font:600 13px var(--sans);color:var(--foreground);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap');
|
|
54
|
+
if (name === top) top.setAttribute('style', 'display:block;font:600 13px var(--sans);color:var(--foreground)');
|
|
55
|
+
if (src) {
|
|
56
|
+
const s = (src.textContent || '').trim();
|
|
57
|
+
src.setAttribute('style', BADGE + ';margin-left:auto' + (s === 'discovered' ? TONE.s : ''));
|
|
58
|
+
src.setAttribute('data-board-badge', 'source');
|
|
59
|
+
if (s === 'discovered') src.setAttribute('title', 'discovered, and approved on this machine');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (desc) desc.setAttribute('style', 'display:block;font-size:12.5px;line-height:1.45;color:var(--muted-foreground)');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Restyle one candidate as a card: its name and state as the board's title and badge, its text as the procedure box. */
|
|
66
|
+
function restyleCand(card) {
|
|
67
|
+
card.setAttribute('style', CARD);
|
|
68
|
+
card.setAttribute('data-board-card', 'candidate');
|
|
69
|
+
const st = card.getAttribute('data-state') || '';
|
|
70
|
+
const kids = [...card.children];
|
|
71
|
+
const [top] = kids;
|
|
72
|
+
if (top) {
|
|
73
|
+
top.setAttribute('style', 'display:flex;align-items:center;gap:10px;flex-wrap:wrap');
|
|
74
|
+
const [name, state] = top.children;
|
|
75
|
+
if (name) name.setAttribute('style', 'font:600 16px var(--sans);color:var(--foreground)');
|
|
76
|
+
if (state) {
|
|
77
|
+
const t = state.textContent || '';
|
|
78
|
+
const tone = /running|approved here/.test(t) ? 's' : st === 'quarantined' ? 'w' : st === 'rejected' ? 'd' : null;
|
|
79
|
+
state.setAttribute('style', BADGE + (tone ? TONE[tone] : ''));
|
|
80
|
+
state.setAttribute('data-board-badge', 'state');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const k of kids.slice(1)) {
|
|
84
|
+
if (k.tagName === 'PRE') { k.setAttribute('style', PROC); continue; }
|
|
85
|
+
const btns = k.querySelectorAll('button');
|
|
86
|
+
if (btns.length) {
|
|
87
|
+
k.setAttribute('style', 'display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:2px');
|
|
88
|
+
for (const b of btns) {
|
|
89
|
+
const h = b.getAttribute('data-r13');
|
|
90
|
+
b.setAttribute('style', (h === 'approve' ? BTNP : BTN) + (b.disabled ? ';opacity:.45;cursor:not-allowed' : ''));
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const tone = /var\(--destructive\)/.test(k.getAttribute('style') || '') ? ';color:var(--destructive)' : '';
|
|
95
|
+
const mono = /var\(--mono\)/.test(k.getAttribute('style') || '');
|
|
96
|
+
k.setAttribute('style', 'display:block;' + (mono ? 'font:11px var(--mono);overflow-wrap:anywhere;' : 'font-size:13px;line-height:1.5;') + 'color:var(--muted-foreground)' + tone);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The right pane's parts, restyled in place. `head` is the list's discovery block, in its own order. */
|
|
101
|
+
function restyleReview(nodes) {
|
|
102
|
+
for (const n of nodes) {
|
|
103
|
+
if (!n || n.nodeType !== 1) continue;
|
|
104
|
+
const h = n.getAttribute('data-r13') || n.getAttribute('data-r4') || '';
|
|
105
|
+
if (h === 'cand') { restyleCand(n); continue; }
|
|
106
|
+
if (h === 'discovery-label' || h === 'cands-label') { n.setAttribute('style', LBL + ';padding:6px 0 0'); continue; }
|
|
107
|
+
if (h === 'run-note' || h === 'cand-note') { n.setAttribute('style', NOTE + (/destructive/.test(n.getAttribute('style') || '') ? ';color:var(--destructive)' : '')); continue; }
|
|
108
|
+
if (n.querySelector('button')) {
|
|
109
|
+
// the discovery actions: "find skills now" with its next run, or the one-press weekly set-up
|
|
110
|
+
n.setAttribute('style', 'display:flex;gap:10px;align-items:center;flex-wrap:wrap');
|
|
111
|
+
for (const b of n.querySelectorAll('button')) b.setAttribute('style', b.getAttribute('data-r13') === 'discover-now' ? BTNP : BTN);
|
|
112
|
+
const seed = n.querySelector('[data-r7]');
|
|
113
|
+
if (seed) {
|
|
114
|
+
seed.setAttribute('style', 'display:flex;flex-direction:column;gap:6px;align-items:flex-start');
|
|
115
|
+
for (const c of seed.children) if (c.tagName === 'SPAN') c.setAttribute('style', 'font-size:12px;color:var(--muted-foreground)');
|
|
116
|
+
}
|
|
117
|
+
for (const s of n.children) if (s.tagName === 'SPAN') s.setAttribute('style', 'font:11px var(--mono);color:var(--muted-foreground)');
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const bad = /var\(--destructive\)/.test(n.getAttribute('style') || '');
|
|
121
|
+
n.setAttribute('style', NOTE + (bad ? ';color:var(--destructive)' : ''));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function renderBoard(inner, ctx) {
|
|
126
|
+
const { FIXTURE } = ctx;
|
|
127
|
+
|
|
128
|
+
// The board fills the tab from edge to edge, as the mockup does: the pane's own padding is dropped for this view only.
|
|
129
|
+
const pane = inner.parentElement;
|
|
130
|
+
if (pane) { pane.style.padding = '0'; pane.style.overflow = 'hidden'; pane.style.display = 'flex'; pane.style.flexDirection = 'column'; }
|
|
131
|
+
inner.style.cssText = 'flex:1 1 auto;min-height:0;display:flex;flex-direction:row;gap:0;max-width:none;height:100%';
|
|
132
|
+
inner.setAttribute('data-board', 'skills');
|
|
133
|
+
|
|
134
|
+
// the existing list, drawn once, then split between the two panes
|
|
135
|
+
const drawn = el('div', null);
|
|
136
|
+
ctx.renderMode(drawn);
|
|
137
|
+
const wrap = !FIXTURE && drawn.children.length === 1 && !drawn.firstElementChild.hasAttribute('data-r4') ? drawn.firstElementChild : null;
|
|
138
|
+
const review = [];
|
|
139
|
+
if (wrap) {
|
|
140
|
+
const kids = [...wrap.children];
|
|
141
|
+
// the list's own label "skills · N · click one to read it" starts the skills; everything above it is the discovery block
|
|
142
|
+
let cut = kids.findIndex((k) => !k.hasAttribute('data-r13') && !k.hasAttribute('data-r4') && /^skills · \d+/.test((k.textContent || '').trim()));
|
|
143
|
+
const counted = cut >= 0;
|
|
144
|
+
if (!counted) cut = kids.length - 1; // a read in progress or failed: its status line stays with the list
|
|
145
|
+
for (const k of kids.slice(0, Math.max(0, cut))) review.push(k);
|
|
146
|
+
for (const k of review) k.remove();
|
|
147
|
+
wrap.setAttribute('style', 'display:flex;flex-direction:column;gap:2px');
|
|
148
|
+
const label = kids[cut];
|
|
149
|
+
if (counted && label) {
|
|
150
|
+
label.setAttribute('data-board-part', 'count');
|
|
151
|
+
label.setAttribute('style', LBL + ';padding:6px 12px 4px');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const rows = [...drawn.querySelectorAll(FIXTURE ? 'button' : '[data-r4="skill"]')];
|
|
155
|
+
for (const row of rows) restyleRow(row);
|
|
156
|
+
for (const n of wrap ? wrap.children : []) {
|
|
157
|
+
if (n.hasAttribute('data-r4') || n.hasAttribute('data-board-part')) continue;
|
|
158
|
+
n.setAttribute('style', NOTE + ';padding:6px 12px' + (/destructive/.test(n.getAttribute('style') || '') ? ';color:var(--destructive)' : ''));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// the switch: it hides the rows of the other sources, and never removes one
|
|
162
|
+
const waiting = review.filter((n) => n.getAttribute && n.getAttribute('data-r13') === 'cand'
|
|
163
|
+
&& n.getAttribute('data-state') === 'quarantined' && !/approved here/.test(n.textContent || '')).length;
|
|
164
|
+
if (!SEGS.some(([k]) => k === seg)) seg = 'all';
|
|
165
|
+
let shown = 0;
|
|
166
|
+
for (const row of rows) {
|
|
167
|
+
const src = sourceOf(row);
|
|
168
|
+
const hide = seg !== 'all' && src !== seg;
|
|
169
|
+
row.style.display = hide ? 'none' : '';
|
|
170
|
+
if (!hide) shown++;
|
|
171
|
+
}
|
|
172
|
+
const noMatch = rows.length && !shown ? el('div', { 'data-board-empty': seg, style: NOTE + ';padding:6px 12px' },
|
|
173
|
+
seg === 'user' ? 'You have no skills of your own here. Open a skill and make your own copy to edit it.'
|
|
174
|
+
: seg === 'discovered' ? 'No discovered skill is approved here yet. Candidates wait on the right.'
|
|
175
|
+
: 'No skills of this kind.') : null;
|
|
176
|
+
const segBtn = ([k, label]) => {
|
|
177
|
+
const on = seg === k;
|
|
178
|
+
return el('button', {
|
|
179
|
+
'data-board-seg': k, 'aria-pressed': on ? 'true' : 'false',
|
|
180
|
+
onClick: () => { seg = k; ctx.setState({}); },
|
|
181
|
+
style: 'flex:1 1 0;text-align:center;border:0;border-radius:6px;padding:3px 0;font:' + (on ? '500' : '400') + ' 12.5px var(--sans);cursor:pointer;'
|
|
182
|
+
+ (on ? 'background:var(--background);color:var(--foreground)' : 'background:transparent;color:var(--muted-foreground)'),
|
|
183
|
+
}, label, k === 'discovered' && waiting ? el('span', { 'data-board-waiting': String(waiting), style: 'color:var(--warning);margin-left:4px' }, String(waiting)) : null);
|
|
184
|
+
};
|
|
185
|
+
const toolbar = FIXTURE ? null : el('div', { 'data-board-part': 'toolbar', role: 'group', 'aria-label': 'Show skills by source',
|
|
186
|
+
style: 'display:flex;gap:2px;background:var(--accent);border-radius:8px;padding:3px' }, SEGS.map(segBtn));
|
|
187
|
+
|
|
188
|
+
const left = el('div', { 'data-board-part': 'list',
|
|
189
|
+
style: 'width:340px;flex:none;box-sizing:border-box;border-right:1px solid var(--border);background:var(--sidebar);padding:14px;display:flex;flex-direction:column;gap:10px;min-height:0;overflow-y:auto' },
|
|
190
|
+
el('div', { 'data-board-part': 'header', style: 'display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:30px' },
|
|
191
|
+
el('span', { 'data-board-title': '', style: 'font:600 15px var(--sans);color:var(--foreground)' }, 'Skills')),
|
|
192
|
+
toolbar,
|
|
193
|
+
el('div', { 'data-board-section': 'skills', style: 'display:flex;flex-direction:column;gap:2px;margin:0 -2px' }, [...drawn.childNodes], noMatch));
|
|
194
|
+
|
|
195
|
+
// the right pane: what waits for the person
|
|
196
|
+
restyleReview(review);
|
|
197
|
+
let right;
|
|
198
|
+
if (FIXTURE) {
|
|
199
|
+
right = [el('div', { 'data-board-part': 'review-title', style: 'font:600 22px var(--sans);color:var(--foreground)' }, 'Skill discovery'),
|
|
200
|
+
el('div', { 'data-board-part': 'note', style: NOTE }, 'Skill discovery is not read in the fixture. Choose a skill on the left to see it in the detail.')];
|
|
201
|
+
} else {
|
|
202
|
+
right = [
|
|
203
|
+
el('div', { style: 'display:flex;align-items:center;gap:10px;flex-wrap:wrap' },
|
|
204
|
+
el('span', { 'data-board-part': 'review-title', style: 'font:600 22px var(--sans);color:var(--foreground)' }, 'Skill discovery'),
|
|
205
|
+
waiting ? badge(waiting + (waiting === 1 ? ' waits for you' : ' wait for you'), 'w', { 'data-board-badge': 'waiting' }) : null),
|
|
206
|
+
el('div', { 'data-board-section': 'review', style: 'display:flex;flex-direction:column;gap:12px' }, review),
|
|
207
|
+
review.length ? el('div', { 'data-board-part': 'note', style: NOTE + ';font-size:12px' },
|
|
208
|
+
'Approval is a press on this machine, after you read the full text. It applies to this machine only.') : null,
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
const rightPane = el('div', { 'data-board-part': 'detail',
|
|
212
|
+
style: 'flex:1 1 0;min-width:0;box-sizing:border-box;padding:22px 28px;display:flex;flex-direction:column;gap:12px;overflow-y:auto;background:var(--card);color:var(--foreground);font-size:13px;line-height:1.45' }, right);
|
|
213
|
+
|
|
214
|
+
inner.appendChild(left);
|
|
215
|
+
inner.appendChild(rightPane);
|
|
216
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/* pm/view-today.js — the Today view tab, drawn from planning/design/cockpit-mockup/ViewToday.dc.html (PRD workspace-cockpit-nav, Run 3).
|
|
2
|
+
*
|
|
3
|
+
* The board, top to bottom: a greeting with the date and the count line, the Repository / All toggle on the right, a row of
|
|
4
|
+
* source badges, then two columns (Needs you on the left; Prepared for you and Today's events in a 360px column on the right).
|
|
5
|
+
* Under the board sits the live proactive-engine list (`ctx.renderMode`), unchanged, with every data-r22/r23/r26 hook.
|
|
6
|
+
*
|
|
7
|
+
* Every fact comes from a read that exists: derived status (`state.statuses`, the same as Run 2's board) and the morning
|
|
8
|
+
* brief (`/api/proactive/brief`, the route the engine list reads). The mockup's mail, Jira and Confluence badges and its
|
|
9
|
+
* calendar events show only when a brief reports them. Without a brief the board says that nothing was read.
|
|
10
|
+
* The Run 2 hooks are kept: data-today = board, greeting, count, sources, needs, prepared, events, card, open, dismiss, and
|
|
11
|
+
* data-today-scope on the toggle.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { el } from './dom.js';
|
|
15
|
+
import { currentProduct } from './board-read.js';
|
|
16
|
+
import { repoTuples, selectRepo } from './panel-repos.js';
|
|
17
|
+
import { needsCards } from './panel-cockpit.js';
|
|
18
|
+
|
|
19
|
+
const DISMISS_REASONS = ['not now', 'wrong priority', 'already handled'];
|
|
20
|
+
const NEEDS_SHOWN = 5; // the mockup shows a few cards and then "+ N more"
|
|
21
|
+
let needsAll = false; // the only module state: has the person asked for every card
|
|
22
|
+
|
|
23
|
+
const LBL = 'font:500 10px var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted-foreground)';
|
|
24
|
+
const BADGE = 'font:500 11px var(--sans);border:1px solid var(--border);border-radius:999px;padding:0 8px;line-height:18px;color:var(--foreground);white-space:nowrap;flex:none';
|
|
25
|
+
const TONE = {
|
|
26
|
+
d: 'border-color:transparent;background:color-mix(in oklab, var(--destructive) 18%, transparent);color:var(--destructive)',
|
|
27
|
+
w: 'border-color:transparent;background:color-mix(in oklab, var(--warning) 16%, transparent);color:var(--warning)',
|
|
28
|
+
s: 'border-color:transparent;background:color-mix(in oklab, var(--success) 16%, transparent);color:var(--success)',
|
|
29
|
+
};
|
|
30
|
+
const badge = (text, tone, extra) => el('span', { style: BADGE + (tone ? ';' + TONE[tone] : '') + (extra ? ';' + extra : '') }, text);
|
|
31
|
+
const CARD = 'border:1px solid var(--border);background:var(--card);border-radius:10px;padding:12px 14px;display:flex;flex-direction:column;gap:8px';
|
|
32
|
+
const BTN_P = 'font:500 12.5px var(--sans);background:var(--foreground);color:var(--background);border:0;border-radius:8px;padding:0 12px;height:30px;display:inline-grid;place-items:center;cursor:pointer';
|
|
33
|
+
const BTN = 'font:500 12.5px var(--sans);background:transparent;color:var(--foreground);border:1px solid var(--input);border-radius:8px;padding:0 12px;height:30px;display:inline-grid;place-items:center;cursor:pointer';
|
|
34
|
+
const GHOST = 'font:12px var(--sans);color:var(--muted-foreground);padding:0 8px;height:28px;display:inline-grid;place-items:center;cursor:pointer;background:transparent;border:0';
|
|
35
|
+
const MUTED = 'color:var(--muted-foreground)';
|
|
36
|
+
|
|
37
|
+
const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
38
|
+
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
39
|
+
|
|
40
|
+
/* ── the morning brief: the same route the engine list reads, kept for 15 seconds ─────────────────── */
|
|
41
|
+
let brief = { state: 'idle', data: null, at: 0 };
|
|
42
|
+
function readBrief(ctx) {
|
|
43
|
+
if (ctx.FIXTURE) return null;
|
|
44
|
+
const fresh = brief.state === 'ok' && Date.now() - brief.at < 15000;
|
|
45
|
+
if (brief.state === 'loading' || brief.state === 'error' && Date.now() - brief.at < 15000 || fresh) return brief.data;
|
|
46
|
+
brief = { state: 'loading', data: brief.data, at: brief.at };
|
|
47
|
+
fetch('/api/proactive/brief').then((r) => r.json()).then(
|
|
48
|
+
(d) => { brief = { state: d && d.ok !== false ? 'ok' : 'error', data: d && d.ok !== false ? d : null, at: Date.now() }; ctx.setState({}); },
|
|
49
|
+
() => { brief = { state: 'error', data: null, at: Date.now() }; ctx.setState({}); },
|
|
50
|
+
);
|
|
51
|
+
return brief.data;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const keyOf = (repo, slug, state) => {
|
|
55
|
+
const sg = typeof window !== 'undefined' ? window.PMOSStatusGroups : null;
|
|
56
|
+
return sg ? sg.keyOf(((state.statuses || {})[repo] || {})[slug]) : 'unknown';
|
|
57
|
+
};
|
|
58
|
+
const meaning = (key) => {
|
|
59
|
+
const sg = typeof window !== 'undefined' ? window.PMOSStatusGroups : null;
|
|
60
|
+
return (sg && sg.BY_KEY && sg.BY_KEY[key] && sg.BY_KEY[key].meaning) || '';
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export function renderBoard(inner, ctx) {
|
|
64
|
+
const { state, setState } = ctx;
|
|
65
|
+
const all = state.todayScope === 'all';
|
|
66
|
+
const repo = currentProduct();
|
|
67
|
+
const cards = needsCards(all);
|
|
68
|
+
const repos = repoTuples();
|
|
69
|
+
const prepared = [];
|
|
70
|
+
for (const t of repos) {
|
|
71
|
+
if (!all && t[0] !== repo) continue;
|
|
72
|
+
for (const slug of Object.keys((state.statuses && state.statuses[t[0]]) || {})) {
|
|
73
|
+
if (keyOf(t[0], slug, state) === 'in-progress') prepared.push({ repo: t[0], slug });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const b = readBrief(ctx);
|
|
77
|
+
const br = b && b.brief && !b.stale ? b.brief : null;
|
|
78
|
+
const now = new Date();
|
|
79
|
+
const hour = now.getHours();
|
|
80
|
+
const greet = b && b.greeting ? String(b.greeting).replace(/\.$/, '') : (hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening');
|
|
81
|
+
const open = (c) => { selectRepo(c.repo); setState({ sel: c.slug, detail: { kind: 'init' }, col2: true }); };
|
|
82
|
+
|
|
83
|
+
/* header: greeting and count line on the left, the scope toggle on the right */
|
|
84
|
+
const toggle = el('div', { 'data-today': 'scope', style: 'display:flex;gap:2px;background:var(--accent);border-radius:8px;padding:3px;font-size:12.5px' },
|
|
85
|
+
...[['repo', 'Repository'], ['all', 'All']].map(([k, label]) => el('button', {
|
|
86
|
+
'data-today-scope': k, 'aria-pressed': (k === 'all') === all ? 'true' : 'false',
|
|
87
|
+
title: k === 'all' ? 'Show every repository' : 'Show ' + (repo || 'this repository') + ' only',
|
|
88
|
+
onClick: () => setState({ todayScope: k === 'all' ? 'all' : 'repo' }),
|
|
89
|
+
style: 'font:12.5px var(--sans);padding:3px 12px;border-radius:6px;border:0;cursor:pointer;' + ((k === 'all') === all ? 'background:var(--background);font-weight:500;color:var(--foreground)' : 'background:transparent;color:var(--muted-foreground)'),
|
|
90
|
+
}, label)));
|
|
91
|
+
const date = DAYS[now.getDay()] + ' ' + now.getDate() + ' ' + MONTHS[now.getMonth()];
|
|
92
|
+
const count = cards.length + (cards.length === 1 ? ' needs you' : ' need you') + (all ? ' across ' + repos.length + ' repositories' : ' in ' + (repo || 'this repository'))
|
|
93
|
+
+ ' · ' + prepared.length + ' in progress';
|
|
94
|
+
const header = el('div', { 'data-board-part': 'header', style: 'display:flex;justify-content:space-between;align-items:flex-end;gap:12px;flex-wrap:wrap' },
|
|
95
|
+
el('div', { style: 'min-width:0' },
|
|
96
|
+
el('div', { 'data-today': 'greeting', style: 'font:600 22px var(--sans)' }, greet),
|
|
97
|
+
el('div', { 'data-today': 'count', style: MUTED }, date + ' · ' + count)),
|
|
98
|
+
el('div', { style: 'display:flex;align-items:center;gap:10px' }, toggle));
|
|
99
|
+
|
|
100
|
+
/* the source badges: what was read, and nothing else */
|
|
101
|
+
const srcBadges = [badge('records · read', 's')];
|
|
102
|
+
if (br) {
|
|
103
|
+
for (const [k, v] of Object.entries(br.sources || {})) {
|
|
104
|
+
srcBadges.push(v === 'read' ? badge(k + ' · read', 's') : v === 'off' ? badge(k + ' · off', null, 'opacity:.6') : badge(k + ' · not reached'));
|
|
105
|
+
}
|
|
106
|
+
} else srcBadges.push(badge(b && b.stale ? 'brief · out of date' : ctx.FIXTURE ? 'brief · not read here' : 'brief · none yet', b && b.stale ? 'w' : null));
|
|
107
|
+
const sources = el('div', { 'data-today': 'sources', style: 'display:flex;gap:6px;flex-wrap:wrap;align-items:center' },
|
|
108
|
+
...srcBadges,
|
|
109
|
+
el('span', { style: MUTED + ';font-size:11px' }, br
|
|
110
|
+
? 'Read from the derived status of ' + Object.keys(state.statuses || {}).length + ' repositories and the brief for ' + br.date + '.'
|
|
111
|
+
: 'Read from the derived status of ' + Object.keys(state.statuses || {}).length + ' repositories. No mail or calendar is read here.'));
|
|
112
|
+
|
|
113
|
+
/* Needs you */
|
|
114
|
+
const card = (c) => {
|
|
115
|
+
const needs = c.key === 'needs-you';
|
|
116
|
+
return el('div', { 'data-today': 'card', 'data-slug': c.slug, 'data-repo': c.repo, style: CARD },
|
|
117
|
+
el('div', { style: 'display:flex;align-items:center;gap:8px;min-width:0' },
|
|
118
|
+
badge(c.repo),
|
|
119
|
+
el('b', { style: 'min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, c.slug),
|
|
120
|
+
badge(needs ? 'needs you' : 'acceptance', needs ? 'd' : 'w', 'margin-left:auto')),
|
|
121
|
+
meaning(c.key) ? el('div', { style: MUTED }, meaning(c.key)) : null,
|
|
122
|
+
el('div', { style: 'display:flex;gap:6px;align-items:center;flex-wrap:wrap' },
|
|
123
|
+
el('button', { 'data-today': 'open', onClick: () => open(c), style: BTN_P }, needs ? 'Unblock' : 'Review'),
|
|
124
|
+
...DISMISS_REASONS.map((r, i) => el('button', { 'data-today': 'dismiss', 'data-reason': r, title: 'Dismiss: ' + r,
|
|
125
|
+
onClick: () => setState((s) => ({ todayDismissed: { ...(s.todayDismissed || {}), [c.repo + '/' + c.slug]: r } })),
|
|
126
|
+
style: GHOST + (i === 0 ? ';margin-left:auto' : ''), hover: 'color:var(--foreground)' }, r.charAt(0).toUpperCase() + r.slice(1)))));
|
|
127
|
+
};
|
|
128
|
+
const shown = needsAll ? cards : cards.slice(0, NEEDS_SHOWN);
|
|
129
|
+
const needsCol = el('div', { 'data-today': 'needs', style: 'flex:1 1 320px;min-width:0;display:flex;flex-direction:column;gap:10px' },
|
|
130
|
+
el('div', { style: LBL }, 'Needs you · only a person can close these'),
|
|
131
|
+
...(cards.length ? shown.map(card) : [el('div', { style: MUTED }, 'Nothing is waiting on you.')]),
|
|
132
|
+
cards.length > NEEDS_SHOWN ? el('button', { 'data-today': 'more', onClick: () => { needsAll = !needsAll; setState({}); },
|
|
133
|
+
style: 'text-align:left;background:transparent;border:0;padding:0;cursor:pointer;' + MUTED, hover: 'color:var(--foreground)' },
|
|
134
|
+
needsAll ? 'Show fewer' : '+ ' + (cards.length - NEEDS_SHOWN) + ' more') : null);
|
|
135
|
+
|
|
136
|
+
/* Prepared for you, then today's events */
|
|
137
|
+
const prep = (c) => el('div', { 'data-today': 'prepared', 'data-slug': c.slug, 'data-repo': c.repo, style: CARD },
|
|
138
|
+
el('b', { style: 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap' }, c.slug),
|
|
139
|
+
el('div', { style: MUTED }, meaning('in-progress') || 'In progress.'),
|
|
140
|
+
el('div', { style: 'display:flex;gap:6px' }, el('button', { 'data-today': 'prepared-open', onClick: () => open(c), style: BTN, hover: 'background:var(--accent)' }, 'Open')));
|
|
141
|
+
let events;
|
|
142
|
+
if (!br) events = [el('div', { style: MUTED }, b && b.stale ? 'The brief is out of date, so no events are shown.' : 'No calendar was read. Events come from the morning brief.')];
|
|
143
|
+
else if (br.sources && br.sources.calendar !== 'read') events = [el('div', { style: MUTED }, 'The brief did not read the calendar, so this says nothing about your day.')];
|
|
144
|
+
else if (!(br.calendar || []).length) events = [el('div', { style: MUTED }, 'No events today.')];
|
|
145
|
+
else events = [el('div', { style: 'display:flex;flex-direction:column;gap:6px' }, ...br.calendar.map((e) => el('div', { 'data-today': 'event', style: 'display:flex;gap:10px' },
|
|
146
|
+
el('span', { style: 'font:12px var(--mono);' + MUTED }, new Date(e.start).toTimeString().slice(0, 5)), e.title + (e.with ? ' · ' + e.with : ''))))];
|
|
147
|
+
const side = el('div', { style: 'flex:0 1 360px;min-width:260px;display:flex;flex-direction:column;gap:10px' },
|
|
148
|
+
el('div', { 'data-today': 'prepared', style: 'display:flex;flex-direction:column;gap:10px' },
|
|
149
|
+
el('div', { style: LBL }, 'Prepared for you · in progress'),
|
|
150
|
+
...(prepared.length ? prepared.map(prep) : [el('div', { style: MUTED }, 'Nothing is being prepared.')])),
|
|
151
|
+
el('div', { 'data-today': 'events', style: 'display:flex;flex-direction:column;gap:10px;margin-top:6px' },
|
|
152
|
+
el('div', { style: LBL }, "Today's events"), ...events));
|
|
153
|
+
|
|
154
|
+
const board = el('div', { 'data-today': 'board', 'data-board': 'today', style: 'display:flex;flex-direction:column;gap:16px;font-size:13px;line-height:1.45' },
|
|
155
|
+
header, sources,
|
|
156
|
+
el('div', { 'data-board-part': 'columns', style: 'display:flex;gap:20px;flex-wrap:wrap;align-items:flex-start;min-height:0' }, needsCol, side));
|
|
157
|
+
|
|
158
|
+
/* the live list: the proactive engine, the brief, background prepare, learning and settings */
|
|
159
|
+
const host = el('div', { 'data-today': 'engine', style: 'display:flex;flex-direction:column;gap:2px;border:1px solid var(--border);border-radius:10px;background:var(--background);padding:4px 0' });
|
|
160
|
+
ctx.renderMode(host);
|
|
161
|
+
if (!host.firstChild) host.appendChild(el('div', { style: MUTED + ';padding:8px 14px;font-size:12px' }, ctx.ready ? 'This list has nothing to show.' : 'Reading the list …'));
|
|
162
|
+
const lower = el('div', { 'data-board-part': 'engine', style: 'display:flex;flex-direction:column;gap:10px;padding-top:16px;margin-top:6px;border-top:1px solid var(--border)' },
|
|
163
|
+
el('div', { style: LBL }, 'From the records · scanned on this computer'), host);
|
|
164
|
+
|
|
165
|
+
inner.style.gap = '16px';
|
|
166
|
+
inner.appendChild(board);
|
|
167
|
+
inner.appendChild(lower);
|
|
168
|
+
}
|