create-pmos 0.137.0 → 0.139.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/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-briefing-test.js +12 -18
- package/workspace/guide/briefing.md +2 -2
- package/workspace/guide/data-freshness.md +1 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/briefing-deck.js +13 -35
- package/workspace/public/pm/c3-shell.js +2 -2
- package/workspace/public/pm/components.css +3 -0
- package/workspace/public/pm/console3.css +0 -2
- package/workspace/public/pm/freshness-line.js +32 -11
- package/workspace/public/pm/freshness-words.js +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.139.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T01:17:12Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T01:17:12Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-12T01:17:12Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Loads /c3.html?demo=1 (demo mode answers every /api call, so nothing reaches the server) and holds:
|
|
5
5
|
* R1 two columns: stance + deck + Needs you on the left; Today + Portfolio pulse on the right
|
|
6
6
|
* R2 Review, Handled, What I could stop and Data you can lean on are not on the Briefing
|
|
7
|
-
* R3 the greeting
|
|
7
|
+
* R3 the greeting and the lens line follow the clock; there is no day-part switch (initiative c3-daypart-auto-only)
|
|
8
8
|
* R4 Today: a lead line with a ring, "Open calendar", rows with chips (nrow), no old two-column prepared/before layout
|
|
9
9
|
* R5 Needs you: "N decisions . M of yours" header, decision tags, a Work home chip on own items
|
|
10
10
|
* R6 Portfolio pulse: "N need attention", rows, "All initiatives" goes to Initiatives; 400 wide has no sideways scroll
|
|
@@ -74,22 +74,16 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
74
74
|
ok('R2 no Review, Handled, Stop or Data trust on the Briefing', await p.evaluate(() => !document.querySelector('[data-c3pg="review-host"], [data-c3pg="handled-host"], [data-c3pg="stop-host"], [data-c3pg="trust-host"]') && !/Data you can lean on|What I could stop|handled for you/i.test(document.querySelector('.c3 .brief').textContent)));
|
|
75
75
|
|
|
76
76
|
/* R3 */
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
ok('R3
|
|
86
|
-
await p.
|
|
87
|
-
const dn = await until(async () => { const d = await dp(); return d.on[0] === 'night' ? d : null; }, 4000);
|
|
88
|
-
ok('R3 Night previews: "Good night" and the Reflect line', !!dn && /^Good night/.test(dn.greet) && /^Reflect/.test(dn.line), JSON.stringify(dn));
|
|
89
|
-
ok('R3 the Night preview names Needs you "Carries over"', await p.evaluate(() => /Carries over/i.test(document.querySelector('[data-c3pg="needs"] .sh2 h2').textContent)));
|
|
90
|
-
await p.click(`.stance [data-pg-dp="${clockPart}"]`);
|
|
91
|
-
const d2 = await until(async () => { const d = await dp(); return d.on[0] === clockPart ? d : null; }, 4000);
|
|
92
|
-
ok('R3 a tap on the clock part brings it back', !!d2 && d2.greet === d0.greet, JSON.stringify(d2));
|
|
77
|
+
const d0 = await p.evaluate(() => {
|
|
78
|
+
const h = new Date().getHours();
|
|
79
|
+
const k = h >= 5 && h < 11 ? 'morning' : h >= 11 && h < 14 ? 'midday' : h >= 14 && h < 17 ? 'afternoon' : h >= 17 && h < 21 ? 'evening' : 'night';
|
|
80
|
+
const st = document.querySelector('.stance');
|
|
81
|
+
return { k, part: st.dataset.part, greet: st.querySelector('b').textContent, line: st.querySelector('.dim').textContent, buttons: document.querySelectorAll('[data-pg-dp], .seg.dp').length };
|
|
82
|
+
});
|
|
83
|
+
const want = { morning: ['Good morning', 'Plan'], midday: ['Good afternoon', 'Unblock'], afternoon: ['Good afternoon', 'Close'], evening: ['Good evening', 'Reflect'], night: ['Good night', 'Reflect'] }[d0.k];
|
|
84
|
+
ok('R3 no day-part switch on the page', d0.buttons === 0, JSON.stringify(d0));
|
|
85
|
+
ok('R3 the stance follows the clock: its part, greeting and lens line', d0.part === d0.k && d0.greet.startsWith(want[0] + ', ') && d0.line.startsWith(want[1] + ':'), JSON.stringify(d0));
|
|
86
|
+
ok('R3 at night or in the evening Needs you reads "Carries over"', want[1] !== 'Reflect' || await p.evaluate(() => /Carries over/i.test(document.querySelector('[data-c3pg="needs"] .sh2 h2').textContent)));
|
|
93
87
|
|
|
94
88
|
/* R4 */
|
|
95
89
|
const td = await p.evaluate(() => {
|
|
@@ -123,7 +117,7 @@ process.on('unhandledRejection', (e) => { console.error(e); finish(1); });
|
|
|
123
117
|
|
|
124
118
|
const M = await open({ viewport: { width: 400, height: 900 } });
|
|
125
119
|
ok('R6 at 400 wide the Briefing does not scroll sideways', M.up && await M.page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1 && document.getElementById('c3-root').scrollWidth <= innerWidth + 1), 'scrollWidth');
|
|
126
|
-
ok('R6 at 400 wide the
|
|
120
|
+
ok('R6 at 400 wide the stance and Needs you are on the page, with no day-part switch', await M.page.evaluate(() => !!document.querySelector('.stance') && document.querySelectorAll('[data-pg-dp]').length === 0 && !!document.querySelector('[data-c3pg="needs"]')));
|
|
127
121
|
ok('0 no page error (400 wide)', M.errors.length === 0, M.errors.join(' | '));
|
|
128
122
|
await M.ctx.close();
|
|
129
123
|
return finish();
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "The Briefing: today's brief"
|
|
4
4
|
description: "The Briefing page of the console (/c3.html): the headline cards, Today, Needs you, Portfolio pulse, and what Done, Not yet and Drop do to an item."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-12
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# The Briefing: today's brief
|
|
@@ -17,7 +17,7 @@ The Briefing shows only what PMOS already holds. When PMOS holds nothing for a s
|
|
|
17
17
|
|
|
18
18
|
The cards at the top are the signals that pass the relevance bar. Turn a card over to read its case.
|
|
19
19
|
|
|
20
|
-
Above the cards, the greeting names the part of the day, for example "Good morning, Wawan". The name is the first word of the git user name on the computer where PMOS runs. When git holds no name, the greeting has none.
|
|
20
|
+
Above the cards, the greeting names the part of the day, for example "Good morning, Wawan". The name is the first word of the git user name on the computer where PMOS runs. When git holds no name, the greeting has none. The part of the day follows the clock. There is no button to change it. When the clock moves to a new part, the line under the greeting and the order of the cards change after you stop using the page for a minute.
|
|
21
21
|
|
|
22
22
|
## Today
|
|
23
23
|
|
|
@@ -43,6 +43,6 @@ Each page shows a short line under its title that says when each source was last
|
|
|
43
43
|
- A source that was never read says **not read yet**, in amber.
|
|
44
44
|
- A source whose last read failed is red and says when it was last read.
|
|
45
45
|
- A watched repository is not named, because the workspace checks it every few seconds.
|
|
46
|
-
-
|
|
46
|
+
- The refresh icon at the end of the line reads every source again: the initiatives on this machine, and an agent read for Calendar, Jira status, My work and tracker items. A source that is switched off or is being read already is left alone. A short note says how many reads started, or which could not start.
|
|
47
47
|
|
|
48
48
|
Settings › Data freshness shows every source in one table: when it was read, its state, its schedule, and **Refresh**.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.139.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.139.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* pm/briefing-deck.js — the Briefing view (console3 wave C1), built on the console3 mockup's own markup and CSS.
|
|
2
|
-
* The mockup (prototype/pmos-console-handoff/mockups/console3.html) is the UI: the stance bar
|
|
3
|
-
*
|
|
2
|
+
* The mockup (prototype/pmos-console-handoff/mockups/console3.html) is the UI: the stance bar (.stance; the part of the day and its
|
|
3
|
+
* lens follow the clock, with no switch), the running line (.tick, .tkt), the card deck (.deck, .fc, .cf, .hero, .dk, .dkd), the flip to the case file (.fb,
|
|
4
4
|
* .case, .chgl, .opts, .slt, .cft), the tilt and the flip animation. The styles are pm/console3.css (see pm/console3-lift.md).
|
|
5
5
|
* This file keeps the data call (GET /api/briefing, the signal engine's cards) and the model (briefing-model.js) and draws
|
|
6
6
|
* what the engine sent: at most five cards, none invented. A field the engine does not supply is left out.
|
|
@@ -12,12 +12,11 @@
|
|
|
12
12
|
* Not registered with the registry (Today has no view registry): dest-today.js imports mountBriefing/disposeBriefing. */
|
|
13
13
|
import { el } from './dom.js';
|
|
14
14
|
import { createIdleGate, watchIdle, applyDayPart } from './c3-daypart.js';
|
|
15
|
-
import {
|
|
15
|
+
import { TAG_STYLE, PART_STYLE, partByKey, partFor, cardView, nextCards, swipeDir, sizeFor, caseModelOf, rawOption } from './briefing-model.js';
|
|
16
16
|
import { BX, caseFile, toast } from './c3-brief.js';
|
|
17
17
|
import { ensureC3Css, c3ic, c3ring, c3Phone } from './pm-work-view.js';
|
|
18
18
|
import { listenButton } from './shell-readaloud.js';
|
|
19
19
|
import { onBriefPage } from './c3-brief-data.js';
|
|
20
|
-
import { button } from './primitives.js';
|
|
21
20
|
|
|
22
21
|
const NS = 'http://www.w3.org/2000/svg';
|
|
23
22
|
const roots = new WeakMap(); // host -> state
|
|
@@ -336,41 +335,21 @@ async function load(S, lens) {
|
|
|
336
335
|
S.data[lens] = null;
|
|
337
336
|
S.data[lens] = await readLens(lens, S.zoom, S.ctx && S.ctx.scope);
|
|
338
337
|
if (S.dead) return; // a deck built before its host is attached still draws: it only touches its own root
|
|
339
|
-
if (lens === S.lens) draw(S);
|
|
340
|
-
else { const old = S.root.querySelector('.stance'); if (old) old.replaceWith(stance(S)); } // another lens arrived: update the switch, keep the open card
|
|
338
|
+
if (lens === S.lens) draw(S); // a lens the clock has since left stays read for later, undrawn
|
|
341
339
|
}
|
|
342
340
|
|
|
343
|
-
/* the stance bar (the mockup's stanceBar): the greeting for the part of the day, the lens and its line,
|
|
344
|
-
*
|
|
345
|
-
*
|
|
341
|
+
/* the stance bar (the mockup's stanceBar): the icon and greeting for the part of the day, the lens and its line, and the read-aloud button.
|
|
342
|
+
* The part follows the clock and nothing else: there is no switch (the PM's feedback, initiative c3-daypart-auto-only). The name is the
|
|
343
|
+
* PM's first name when PMOS holds one (GET /api/brief-page `me`); without one the greeting stands alone. */
|
|
346
344
|
function stance(S) {
|
|
347
345
|
const P = PART_STYLE[S.lens] || PART_STYLE.Plan;
|
|
348
|
-
const part = partByKey(S.
|
|
349
|
-
|
|
350
|
-
...PARTS.map((x) => {
|
|
351
|
-
const b = button({ label: x.label, variant: 'ghost', size: 'sm', onClick: () => setPart(S, x.k), attrs: { 'aria-pressed': String(x.k === part.k), 'data-pg-dp': x.k, title: x.label + ' · ' + x.at + (x.k === S.clockPart ? ' (now)' : ''), 'aria-label': x.label } });
|
|
352
|
-
b.replaceChildren(c3ic(x.icon, 'sm')); // the icon alone; the label is the aria-label
|
|
353
|
-
return b;
|
|
354
|
-
}));
|
|
355
|
-
return el('div', { class: 'stance', 'data-bf': 'lens-bar' },
|
|
346
|
+
const part = partByKey(S.clockPart);
|
|
347
|
+
return el('div', { class: 'stance', 'data-bf': 'lens-bar', 'data-part': part.k },
|
|
356
348
|
el('span', { class: 'ki' }, c3ic(part.icon)),
|
|
357
349
|
el('div', null, el('b', { title: 'Follows the clock: this is the part of the day it is now.' }, part.greet + (S.me ? ', ' + S.me : '')), el('span', { class: 'dim' }, S.lens + ': ' + P.sub)),
|
|
358
|
-
seg,
|
|
359
350
|
listenButton(() => { const x = S.data[S.lens]; return x && x.ok ? x.cards[S.i] || x.cards[0] : null; })); // the mockup's speaker (data-a=listen) reads the card on top
|
|
360
351
|
}
|
|
361
352
|
|
|
362
|
-
/* Preview one part of the day: its lens, its cards. Nothing is saved; the clock's own part comes back by tapping it, or when the clock moves on. */
|
|
363
|
-
function setPart(S, k) {
|
|
364
|
-
const part = partByKey(k);
|
|
365
|
-
S.preview = k === S.clockPart ? null : k;
|
|
366
|
-
const lens = part.lens;
|
|
367
|
-
const changed = lens !== S.lens;
|
|
368
|
-
S.lens = lens; S.i = 0; S.flip = null;
|
|
369
|
-
if (changed) load(S, lens);
|
|
370
|
-
draw(S);
|
|
371
|
-
if (S.ctx.onLens) S.ctx.onLens(lens);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
353
|
/* the mockup's running line (.tick > .tkt, lines 539 to 545): the engine's ticker strings, twice so the marquee loops */
|
|
375
354
|
function ticker(items) {
|
|
376
355
|
const one = (hide) => items.map((t) => el('span', { class: 'tki', 'aria-hidden': hide ? 'true' : null }, el('b', { style: 'color:var(--acc)' }, c3ic('bolt', 'sm')), String(t)));
|
|
@@ -416,19 +395,18 @@ export function mountBriefing(host, ctx) {
|
|
|
416
395
|
host.replaceChildren(root);
|
|
417
396
|
const clockPart = partFor(new Date()).k;
|
|
418
397
|
const clock = partByKey(clockPart).lens;
|
|
419
|
-
S = { host, root, ctx: ctx || {}, lens: clock, clock, clockPart,
|
|
398
|
+
S = { host, root, ctx: ctx || {}, lens: clock, clock, clockPart, me: null, zoom: 'week', i: 0, flip: null, data: {}, dead: false };
|
|
420
399
|
S.stopPhone = c3Phone(root);
|
|
421
400
|
roots.set(host, S);
|
|
422
401
|
draw(S);
|
|
423
402
|
if (!host.closest('[data-daypart]')) applyDayPart(root); // a page without the console3 shell still gets the day part tokens
|
|
424
403
|
/* The clock moves the lens, but never under the reader: a new part waits for 60 s without pointer or key activity and for every card to be turned back. */
|
|
425
404
|
const gate = createIdleGate({ current: S.clockPart, part: (ms) => partFor(new Date(ms)).k, busy: () => !!S.flip || !!S.root.querySelector('.fc.back'),
|
|
426
|
-
apply: (k) => { if (S.dead) return; const l = partByKey(k).lens; S.clockPart = k; S.
|
|
405
|
+
apply: (k) => { if (S.dead) return; const l = partByKey(k).lens; S.clockPart = k; S.clock = l; S.lens = l; S.i = 0; S.flip = null; load(S, l); draw(S); if (S.ctx.onLens) S.ctx.onLens(l); } });
|
|
427
406
|
S.idle = watchIdle(gate);
|
|
428
407
|
if (!(S.ctx && S.ctx.bare)) onBriefPage((j) => { const me = j && j.ok !== false && j.me ? String(j.me) : null; if (me === S.me || S.dead) return; S.me = me; const old = S.root.querySelector('.stance'); if (old) old.replaceWith(stance(S)); }); // the greeting's name
|
|
429
408
|
if (S.ctx.onLens) S.ctx.onLens(S.lens); // the page may follow the lens from the start (Reflect: "Carries over", "What I could stop")
|
|
430
|
-
// the
|
|
431
|
-
load(S, S.lens).then(() => Promise.all(LENSES.filter((l) => l !== S.lens).map((l) => load(S, l))));
|
|
409
|
+
load(S, S.lens); // only the clock's lens: the next one is read when the clock moves to it
|
|
432
410
|
}
|
|
433
411
|
|
|
434
412
|
/* Demo: a tab whose deck follows another control (the Work lens) changes the scope the deck asks for. Real mode never calls it. */
|
|
@@ -436,7 +414,7 @@ export function setBriefingScope(host, scope) {
|
|
|
436
414
|
const S = roots.get(host);
|
|
437
415
|
if (!S || S.ctx.scope === scope) return;
|
|
438
416
|
S.ctx.scope = scope; S.data = {}; S.i = 0; S.flip = null; draw(S);
|
|
439
|
-
load(S, S.lens)
|
|
417
|
+
load(S, S.lens);
|
|
440
418
|
}
|
|
441
419
|
|
|
442
420
|
/* Open one card on its case file (the bell's "go to the card", console3.html nopen 2533). True when the card is on this deck. */
|
|
@@ -265,7 +265,7 @@ function moveInd() {
|
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
/* ── the Briefing tab: two columns, decisions and reads ──
|
|
268
|
-
* Left: the stance bar
|
|
268
|
+
* Left: the stance bar (the part of the day follows the clock) and the headline deck (briefing-deck.js), then Needs you (c3-brief-needs.js).
|
|
269
269
|
* Right: Today (c3-brief-today.js) and Portfolio pulse (c3-brief-pulse.js). Review lives in Work's Review lens; Handled, What I could stop and
|
|
270
270
|
* Data you can lean on live in Reports > Drafts (console3-briefing-parity, PM decision 1). The ribbon and the embedded calendar are not here:
|
|
271
271
|
* the calendar is the Calendar tab. */
|
|
@@ -290,7 +290,7 @@ function briefTab(pane) {
|
|
|
290
290
|
loadBriefPage();
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
/* The
|
|
293
|
+
/* The clock's part of the day: Reflect names Needs you "Carries over" (the mockup's vBrief, line 1470). */
|
|
294
294
|
function lensChanged(l) {
|
|
295
295
|
setNeedsTitle(S.needsBox, l === 'Reflect');
|
|
296
296
|
}
|
|
@@ -3507,6 +3507,8 @@
|
|
|
3507
3507
|
gap: 8px;
|
|
3508
3508
|
font-size: 12px;
|
|
3509
3509
|
color: var(--muted-foreground);
|
|
3510
|
+
/* the header row can be wider than a phone (its tools row); the line wraps inside the screen, not past it */
|
|
3511
|
+
max-width: calc(100vw - 24px);
|
|
3510
3512
|
}
|
|
3511
3513
|
.fr-fresh__state--fresh { color: var(--foreground); }
|
|
3512
3514
|
.fr-fresh__state--stale { color: var(--warning); }
|
|
@@ -3515,6 +3517,7 @@
|
|
|
3515
3517
|
.fr-fresh__state--building,
|
|
3516
3518
|
.fr-fresh__state--off { color: var(--muted-foreground); }
|
|
3517
3519
|
.fr-fresh__sep { color: var(--muted-foreground); }
|
|
3520
|
+
.fr-fresh__note { color: var(--muted-foreground); }
|
|
3518
3521
|
.fr-fresh-table__grid {
|
|
3519
3522
|
width: 100%;
|
|
3520
3523
|
border-collapse: collapse;
|
|
@@ -435,7 +435,6 @@
|
|
|
435
435
|
.c3 .stance>div:nth-child(2){flex:1;min-width:0;display:flex;gap:8px;align-items:baseline;flex-wrap:wrap}
|
|
436
436
|
.c3 .stance b{font-size:var(--fs-3)}
|
|
437
437
|
.c3 .stance .dim{font-size:var(--fs-3)}
|
|
438
|
-
.c3 .dp button{padding:4px 8px}
|
|
439
438
|
.c3 .lens{display:flex;align-items:center;gap:6px;max-width:100%}
|
|
440
439
|
.c3 .lens .seg{overflow-x:auto;min-width:0}
|
|
441
440
|
.c3 .lh{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:10px;align-items:center;padding:2px 0 6px}
|
|
@@ -1634,7 +1633,6 @@
|
|
|
1634
1633
|
.c3 .psess .c{font:var(--fs-1) var(--mono)}
|
|
1635
1634
|
/* Briefing as the brief (initiative briefing-brief): one column; Today agenda; own-item rows of Needs you; inline Portfolio pulse */
|
|
1636
1635
|
/* Briefing, console3-briefing-parity: the day-part switch in the stance bar, the tags, the Today rows */
|
|
1637
|
-
.c3 .stance .seg.dp{flex:none}
|
|
1638
1636
|
.c3 .stance{flex-wrap:wrap}
|
|
1639
1637
|
.c3 .tag.acc{background:var(--accw);color:var(--acc)}
|
|
1640
1638
|
.c3 .tag.ok{color:var(--foreground)}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
*
|
|
3
3
|
* freshnessLine(ws) → element the console header's short line: when each source was last read ("Calendar read
|
|
4
4
|
* 07:15 · Jira status read 08:02"), the old, failed and never-read ones flagged, and
|
|
5
|
-
* a
|
|
5
|
+
* a refresh icon, always there, that reads every source again. It never says "up to date": the sources
|
|
6
6
|
* are read on a schedule, not live (pm/freshness-words.js). Updates itself every 15 s.
|
|
7
7
|
* freshnessTable(ws) → element Settings › Data freshness: every source, when it was read, its state, its schedule,
|
|
8
8
|
* Refresh, and the repository's Watch for changes switch.
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { el, setChildren } from './dom.js';
|
|
16
16
|
import { pmGet, pmPost } from './http.js';
|
|
17
|
-
import { button, SEL } from './primitives.js';
|
|
18
|
-
import { lineItems,
|
|
17
|
+
import { button, iconButton, SEL } from './primitives.js';
|
|
18
|
+
import { lineItems, refreshTargets } from './freshness-words.js';
|
|
19
19
|
|
|
20
20
|
const POLL_MS = 15000;
|
|
21
21
|
const live = new Map(); // kind|ws -> { node, timer, version }
|
|
@@ -67,25 +67,46 @@ function poll(key, ws, draw) {
|
|
|
67
67
|
return cur.node;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/* The line's refresh icon: one press reads every source again (the PM's choice, 2026-10-12). The initiatives rebuild on
|
|
71
|
+
* this machine; Calendar, Jira, My work and tracker items each start their agent read. A source already being read, or
|
|
72
|
+
* switched off, is left alone. The icon and its result note are kept on the line's element, so a redraw does not lose them. */
|
|
73
|
+
function refreshAll(n, d) {
|
|
74
|
+
n._frData = d;
|
|
75
|
+
if (n._frBtn) return n._frBtn;
|
|
76
|
+
const btn = iconButton({ icon: 'refresh-cw', ariaLabel: 'Read every source again', size: 'sm', attrs: { 'data-c3pg': 'freshness-refresh' },
|
|
77
|
+
onClick: async () => {
|
|
78
|
+
const cur = n._frData;
|
|
79
|
+
const ids = refreshTargets(cur.sources);
|
|
80
|
+
btn.disabled = true;
|
|
81
|
+
note(n, 'Starting ' + ids.length + ' reads…');
|
|
82
|
+
const res = await Promise.all(ids.map((id) => pmPost('/api/sources/refresh', { ws: cur.ws, source: id }).catch(() => null)));
|
|
83
|
+
const bad = ids.filter((id, i) => !(res[i] && res[i].ok)).map((id) => (cur.sources.find((s) => s.id === id) || {}).label || id);
|
|
84
|
+
note(n, bad.length ? 'Could not start: ' + bad.join(', ') : 'Reading ' + ids.length + ' sources again');
|
|
85
|
+
const c = live.get('line|' + cur.ws); if (c) c.tick();
|
|
86
|
+
setTimeout(() => { btn.disabled = false; note(n, ''); }, 30000);
|
|
87
|
+
} });
|
|
88
|
+
n._frBtn = btn;
|
|
89
|
+
return btn;
|
|
90
|
+
}
|
|
91
|
+
function note(n, text) {
|
|
92
|
+
if (!text) { if (n._frMsg) n._frMsg.remove(); n._frMsg = null; return; }
|
|
93
|
+
if (!n._frMsg) { n._frMsg = el('span', { class: 'fr-fresh__note', role: 'status' }); n.append(n._frMsg); }
|
|
94
|
+
n._frMsg.textContent = text;
|
|
95
|
+
}
|
|
96
|
+
|
|
70
97
|
/** The header line: when each source was last read, old, failed and never-read ones flagged. It never claims "up to date". */
|
|
71
98
|
export function freshnessLine(ws) {
|
|
72
99
|
return poll('line|' + ws, ws, (node, d) => {
|
|
73
100
|
const n = node || el('div', { class: 'fr-fresh', 'data-c3pg': 'freshness' });
|
|
74
101
|
if (!d) { if (node) setChildren(n, [el('span', { class: tone('never') }, 'Could not read when the sources were read')]); else n.textContent = ''; return n; }
|
|
75
102
|
const items = lineItems(d.sources);
|
|
76
|
-
const behind = behindOf(items);
|
|
77
103
|
const kids = [];
|
|
78
104
|
items.forEach((it, i) => {
|
|
79
105
|
if (i) kids.push(el('span', { class: 'fr-fresh__sep', 'aria-hidden': 'true' }, '·'));
|
|
80
106
|
kids.push(el('span', { class: tone(it.state), title: it.title, 'data-fr-line': it.id }, it.text));
|
|
81
107
|
});
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const src = d.sources.find((s) => s.id === first.id);
|
|
85
|
-
const btn = button({ label: 'Refresh ' + (src ? src.label : first.id), size: 'sm', attrs: { 'data-c3pg': 'freshness-refresh' },
|
|
86
|
-
onClick: () => refresh(d.ws, first.id, btn) });
|
|
87
|
-
kids.push(btn);
|
|
88
|
-
}
|
|
108
|
+
kids.push(refreshAll(n, d));
|
|
109
|
+
if (n._frMsg) kids.push(n._frMsg);
|
|
89
110
|
setChildren(n, kids);
|
|
90
111
|
return n;
|
|
91
112
|
});
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* lineItems(sources, now?) → [{ id, state, text, title }] one honest phrase per source: when it was last read, or
|
|
4
4
|
* that it was never read, failed or is reading now
|
|
5
5
|
* behindOf(items) → [item] the sources to flag: old, failed or never read
|
|
6
|
+
* refreshTargets(sources) → [id] what the refresh icon reads again: every source but the
|
|
7
|
+
* repository (its rebuild is the initiatives'), one switched
|
|
8
|
+
* off, and one being read now
|
|
6
9
|
* whenRead(iso, now?) → '07:15' | 'yesterday 18:02' | '3 d ago'
|
|
7
10
|
*
|
|
8
11
|
* The line never says "up to date". Calendar, Jira, My work and tracker items are read by an agent on a schedule, not
|
|
@@ -44,3 +47,5 @@ export function lineItems(sources, now = Date.now()) {
|
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
export const behindOf = (items) => items.filter((i) => i.state === 'stale' || i.state === 'failed' || i.state === 'never');
|
|
50
|
+
|
|
51
|
+
export const refreshTargets = (sources) => (sources || []).filter((s) => s && s.id !== 'repository' && s.state !== 'off' && s.state !== 'building').map((s) => s.id);
|