create-pmos 0.210.0 → 0.212.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-light-load-test.js +110 -0
- package/workspace/c3-settings-test.js +53 -2
- package/workspace/guide/briefing.md +7 -1
- package/workspace/guide/data-freshness.md +35 -8
- package/workspace/guide/index.md +1 -1
- package/workspace/lib/driver-run.js +4 -3
- package/workspace/lib/freshness.js +2 -0
- package/workspace/lib/refresh-run.js +300 -0
- package/workspace/lib/refresh.js +464 -0
- package/workspace/lib/report-finder.js +22 -1
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-calendar-views.js +10 -2
- package/workspace/lib/routes-pm-items.js +2 -0
- package/workspace/lib/routes-refresh.js +93 -0
- package/workspace/lib/settings-store.js +0 -2
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-brief-pulse.js +4 -4
- package/workspace/public/pm/c3-calpop.js +11 -3
- package/workspace/public/pm/c3-dock.js +9 -0
- package/workspace/public/pm/c3-driver.js +1 -1
- package/workspace/public/pm/c3-look.js +27 -0
- package/workspace/public/pm/c3-plan.js +3 -2
- package/workspace/public/pm/c3-quiet.css +6 -1
- package/workspace/public/pm/c3-report-store.js +2 -1
- package/workspace/public/pm/c3-scan-setup.js +3 -7
- package/workspace/public/pm/c3-scan-words.js +7 -0
- package/workspace/public/pm/c3-scan.js +6 -4
- package/workspace/public/pm/c3-settings.css +11 -0
- package/workspace/public/pm/c3-settings.js +236 -27
- package/workspace/public/pm/c3-shell.js +56 -100
- package/workspace/replaced-tests.json +11 -0
- package/workspace/server.js +106 -12
- package/workspace/test-lists/c3-desktop-light-load.json +6 -0
- package/workspace/test-lists/data-refresh-settings.json +5 -0
|
@@ -15,29 +15,18 @@ import { row, button } from './primitives.js';
|
|
|
15
15
|
import { pmGet } from './http.js';
|
|
16
16
|
import { ensureC3Css, c3ic, c3Phone, mountPmWork } from './pm-work-view.js';
|
|
17
17
|
import { mountBriefing, showCard, flipBack } from './briefing-deck.js';
|
|
18
|
-
import { mountCalendarViews } from './calendar-views.js';
|
|
19
18
|
import { todaySection } from './c3-brief-today.js';
|
|
20
19
|
import { needsSection, needsCount } from './c3-brief-needs.js';
|
|
21
20
|
import { scanSection } from './c3-scan.js';
|
|
22
21
|
import { reportFoundSection } from './c3-report-store.js';
|
|
23
22
|
import { pulse } from './c3-brief-pulse.js';
|
|
24
23
|
import { loadBriefPage, onBriefPage } from './c3-brief-data.js';
|
|
25
|
-
import { mountLifecycle } from './lifecycle-lens.js';
|
|
26
|
-
import { mountForecast } from './timeline-forecast-view.js';
|
|
27
|
-
import { mountReleases } from './releases-view.js';
|
|
28
|
-
import { mountReports, showReportsView } from './c3-reports.js';
|
|
29
24
|
import { createDock } from './c3-dock.js';
|
|
30
25
|
import { reviewSection, footLine, rowApi, BX, toast, setNeedsTitle, hooks as briefHooks } from './c3-brief.js';
|
|
31
26
|
import { openSheet } from './c3-sheet.js';
|
|
32
27
|
import { mountCalPopupTrigger } from './c3-calpop.js';
|
|
33
|
-
import { planNode
|
|
34
|
-
import { initiativesViewCtx } from './registry.js';
|
|
35
|
-
import { mount as mountRoadmap } from './c3-roadmap.js';
|
|
36
|
-
import { mount as mountBreakdown } from './c3-breakdown.js';
|
|
28
|
+
import { planNode } from './c3-plan.js';
|
|
37
29
|
import { ic } from './c3-terminal-icons.js';
|
|
38
|
-
import { mount as mountTickets, badge as ticketsBadge } from './c3-tickets.js';
|
|
39
|
-
import { mount as mountDocs, badge as docsBadge } from './c3-docs.js';
|
|
40
|
-
import { mount as mountSignals, badge as signalsBadge } from './c3-signals.js';
|
|
41
30
|
import { mountDaypart } from './c3-daypart.js';
|
|
42
31
|
import { mountAmbient } from './c3-ambient.js';
|
|
43
32
|
import { mountTicker } from './shell-ticker.js';
|
|
@@ -45,20 +34,12 @@ import { mountBell, openBellList } from './shell-bell.js';
|
|
|
45
34
|
import { onBriefing, loadBriefing } from './shell-ticker.js';
|
|
46
35
|
import { registry, filterPalette, rowStep } from './briefing-model.js';
|
|
47
36
|
import { freshnessLine } from './freshness-line.js';
|
|
48
|
-
import {
|
|
49
|
-
import { mountSettings, gearIcon, applyLook } from './c3-settings.js';
|
|
37
|
+
import { gearIcon, applyLook } from './c3-look.js';
|
|
50
38
|
import { mountQuiet } from './c3-quiet.js';
|
|
51
|
-
import { mountPhone, phoneWanted } from './c3-phone.js';
|
|
52
39
|
|
|
53
40
|
/* In demo mode each tab's deck asks for its own cards (the mockup draws a different deck per tab); real mode sends no scope. Reports always sends its own (repTab). */
|
|
54
41
|
const sc = (name) => (DEMO ? name : undefined);
|
|
55
42
|
const TABS = [['brief', 'Briefing', 'sun'], ['cal', 'Calendar', 'cal'], ['work', 'Work', 'todo'], ['init', 'Initiatives', 'init'], ['rep', 'Reports', 'report']];
|
|
56
|
-
/* The mockup's eight Initiatives views, in its order: Lifecycle, Roadmap, Breakdown, Timeline (= Forecast), Tickets, Releases, Documents, Signals. */
|
|
57
|
-
const INITS = [['lifecycle', 'Lifecycle', mountLifecycle, 'init'], ['roadmap', 'Roadmap', mountRoadmap, 'flag'], ['breakdown', 'Breakdown', mountBreakdown, 'tree'], ['forecast', 'Timeline', mountForecast, 'cal'],
|
|
58
|
-
['tickets', 'Tickets', mountTickets, 'check'], ['releases', 'Releases', mountReleases, 'out'], ['documents', 'Documents', mountDocs, 'doc'], ['signals', 'Signals', mountSignals, 'alert']];
|
|
59
|
-
/* The five views of this page's own (c3-roadmap.js ... c3-signals.js) draw the Plan lens in their own left column, as Lifecycle does. A view with a `badge` gets a count on its button. */
|
|
60
|
-
const OWN = new Set(['roadmap', 'breakdown', 'tickets', 'documents', 'signals']);
|
|
61
|
-
const BADGES = { tickets: ticketsBadge, documents: docsBadge, signals: signalsBadge };
|
|
62
43
|
const THEME_KEY = 'pmos.c3.theme';
|
|
63
44
|
const q = new URLSearchParams(location.search);
|
|
64
45
|
const store = { get(k) { try { return localStorage.getItem(k); } catch { return null; } }, set(k, v) { try { localStorage.setItem(k, v); } catch { /* no storage */ } } };
|
|
@@ -66,7 +47,7 @@ const DEMO = q.get('demo') === '1'; // demo mode: c3-demo.js answers the page'
|
|
|
66
47
|
/* The dock's open or folded choice, kept on this device. No choice yet: folded. */
|
|
67
48
|
const dockPref = () => store.get('pmos.c3.dock') === 'open';
|
|
68
49
|
const keepDock = () => store.set('pmos.c3.dock', S.dockOpen ? 'open' : 'folded');
|
|
69
|
-
const S = { demo: DEMO, ws: q.get('ws') || '', workspaces: [], tab: 'brief', theme: 'light', panes: {}, mounted: {},
|
|
50
|
+
const S = { demo: DEMO, ws: q.get('ws') || '', workspaces: [], tab: 'brief', theme: 'light', panes: {}, mounted: {}, dockOpen: true, wide: true };
|
|
70
51
|
|
|
71
52
|
/* A view reads a route with no ws and the server answers for its home workspace. This page names one, so a GET to /api/ with no ws gets it. */
|
|
72
53
|
function withWs() {
|
|
@@ -152,12 +133,15 @@ function toggleMore() {
|
|
|
152
133
|
}
|
|
153
134
|
/* The phone companion (c3-phone.js): at 640px or less it fills the page and the desktop grid is hidden. There is no Full console:
|
|
154
135
|
* the old choice (pmos.c3.phone = 'desk', stored before 2026-10-13) is cleared so no phone stays on the desktop pages. */
|
|
136
|
+
/* as c3-phone.js phoneWanted, without loading that module on a desktop */
|
|
137
|
+
const phoneWanted = () => typeof matchMedia === 'function' && matchMedia('(max-width:640px)').matches && store.get('pmos.c3.testDesk') !== 'desk';
|
|
155
138
|
function syncPhone() {
|
|
156
139
|
if (store.get('pmos.c3.phone')) store.set('pmos.c3.phone', '');
|
|
157
|
-
const want = phoneWanted(
|
|
140
|
+
const want = phoneWanted();
|
|
158
141
|
if (want && !S.phone) {
|
|
159
142
|
S.root.classList.add('c3-phone-on');
|
|
160
|
-
S.phone =
|
|
143
|
+
const p = S.phone = { dispose: () => { p.gone = true; } }; // the companion's code loads first (c3-phone.js); narrowing back before it lands cancels it
|
|
144
|
+
import('./c3-phone.js').then((m) => { if (p.gone || S.phone !== p) return; S.phone = m.mountPhone(S.root, { ws: S.ws, demo: S.demo }); }).catch(() => { if (S.phone === p) { S.phone = null; S.root.classList.remove('c3-phone-on'); } });
|
|
161
145
|
} else if (!want && S.phone) {
|
|
162
146
|
S.phone.dispose(); S.phone = null;
|
|
163
147
|
S.root.classList.remove('c3-phone-on');
|
|
@@ -319,12 +303,12 @@ function lensChanged(l) {
|
|
|
319
303
|
if (typeof window !== 'undefined') window.addEventListener('pmos:go-tab', (e) => {
|
|
320
304
|
const d = (e && e.detail) || {}; if (!d.tab) return;
|
|
321
305
|
showTab(d.tab, true);
|
|
322
|
-
if (d.tab === 'settings' && d.section
|
|
323
|
-
if (d.tab === 'rep' && d.view)
|
|
306
|
+
if (d.tab === 'settings' && d.section) settingsReady().then((st) => st.show(d.section), () => {});
|
|
307
|
+
if (d.tab === 'rep' && d.view) reportsView(d.view);
|
|
324
308
|
});
|
|
325
309
|
function goTarget(t) {
|
|
326
310
|
/* a dashboard schedule's notice (routes-briefing.js notices): Reports, then its Drafts tab */
|
|
327
|
-
if (t && t.tab && BUILD[t.tab]) { showTab(t.tab, true); if (t.tab === 'rep' && t.view)
|
|
311
|
+
if (t && t.tab && BUILD[t.tab]) { showTab(t.tab, true); if (t.tab === 'rep' && t.view) reportsView(t.view); return; }
|
|
328
312
|
showTab('brief', true);
|
|
329
313
|
if (!t) return;
|
|
330
314
|
if (t.card !== undefined && t.card !== null) { showCard(S.deckHost, t.card); return; }
|
|
@@ -363,7 +347,7 @@ function paletteItems() {
|
|
|
363
347
|
...TABS.map(([id, label, icon]) => ({ label, kind: 'place', icon, run: () => showTab(id, true) })),
|
|
364
348
|
{ label: 'Settings', kind: 'place', icon: 'shield', run: () => showTab('settings', true) }];
|
|
365
349
|
for (const k of Object.keys(registry('people') || {})) { const p = registry('people')[k]; items.push({ label: p.n + ' · ' + p.role, kind: 'person', icon: 'user', run: () => openPerson(k) }); }
|
|
366
|
-
for (const d of registry('docs') || []) items.push({ label: d.n, kind: 'document', icon: d.icon || 'doc', run: () =>
|
|
350
|
+
for (const d of registry('docs') || []) items.push({ label: d.n, kind: 'document', icon: d.icon || 'doc', run: () => showTab('init', true) });
|
|
367
351
|
for (const x of S.palSessions || []) items.push({ label: (x.branch || x.tree || '') ? ((x.branch || String(x.tree).split('/').pop()) + ' · ' + x.name) : x.name, kind: 'session', icon: 'branch', run: () => openSession(x) });
|
|
368
352
|
return items;
|
|
369
353
|
}
|
|
@@ -457,78 +441,44 @@ function dock() {
|
|
|
457
441
|
}
|
|
458
442
|
/* The dock is open on the Briefing and folded away on the other tabs. A folded dock leaves no rail (PM, 2026-10-12): the header's Sessions button, Ctrl J and the More menu reach it. */
|
|
459
443
|
function syncDock() {
|
|
444
|
+
const opening = S.dockEl.hidden && S.dockOpen;
|
|
460
445
|
S.dockEl.hidden = !S.dockOpen;
|
|
446
|
+
if (opening && S.dockStarted) S.dock.refresh(); // a folded dock reads only the runs; opening it reads the tree (c3-dock.js refresh)
|
|
461
447
|
}
|
|
462
448
|
|
|
463
449
|
/* ── the other tabs ── */
|
|
464
|
-
function initTab(pane) {
|
|
465
|
-
S.initSeg = el('div', { class: 'seg', role: 'group', 'aria-label': 'Initiatives views', 'data-c3pg': 'init-switch', style: 'overflow-x:auto;max-width:100%' });
|
|
466
|
-
S.initHosts = {};
|
|
467
|
-
const ws = () => S.workspaces.find((w) => w.id === S.ws) || {};
|
|
468
|
-
const reload = () => { S.initRows = null; S.mounted = Object.fromEntries(Object.entries(S.mounted).filter(([k]) => !k.startsWith('init-'))); loadInitRows().then(() => showInit(S.init)); };
|
|
469
|
-
const add = newInitLine({ get repo() { return ws().root || null; }, ws: S.ws, get rows() { return (S.initRows && S.initRows.rows) || []; }, onAdded: reload });
|
|
470
|
-
const bar = el('div', { class: 'calbar' }, S.initSeg);
|
|
471
|
-
const body = el('div', { class: 'c3pg-initbody' });
|
|
472
|
-
for (const [id] of INITS) { S.initHosts[id] = el('div', { 'data-in-host': id, hidden: true }); body.append(S.initHosts[id]); }
|
|
473
|
-
pane.append(add, bar, body);
|
|
474
|
-
S.initNote = el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)', role: 'status', 'data-c3pg': 'init-note' }, 'Reading the repository');
|
|
475
|
-
body.prepend(S.initNote);
|
|
476
|
-
drawInitSwitch();
|
|
477
|
-
loadInitRows().then(() => { showInit(S.init); loadBadges(); });
|
|
478
|
-
}
|
|
479
|
-
/* The counts on the sub-view buttons: each view's own `badge(ctx)` (a number, or null when it cannot say). */
|
|
480
|
-
function loadBadges() {
|
|
481
|
-
const ws = S.workspaces.find((w) => w.id === S.ws) || {};
|
|
482
|
-
const ctx = { repo: ws.root || null, ws: S.ws, rows: (S.initRows && S.initRows.rows) || [] };
|
|
483
|
-
S.badges = S.badges || {};
|
|
484
|
-
for (const [id, fn] of Object.entries(BADGES)) Promise.resolve().then(() => fn(ctx)).then((n) => { S.badges[id] = Number(n) || 0; drawInitSwitch(); }, () => { /* no count */ });
|
|
485
|
-
}
|
|
486
|
-
function drawInitSwitch() {
|
|
487
|
-
S.initSeg.replaceChildren(...INITS.map(([id, label, mount, icon]) => {
|
|
488
|
-
const n = S.badges && S.badges[id];
|
|
489
|
-
return el('button', { type: 'button', 'aria-pressed': String(S.init === id), 'data-c3-init': id, onclick: () => showInit(id) }, c3ic(icon, 'sm'), label, n ? [' ', el('span', { class: 'bdg', 'data-c3-init-badge': id }, String(n))] : null);
|
|
490
|
-
}));
|
|
491
|
-
}
|
|
492
|
-
/* The roadmap read answers { pending } while the repository is still being read. Ask again until it has rows, and say so meanwhile. */
|
|
493
|
-
async function loadInitRows() {
|
|
494
|
-
for (let n = 0; n < 40; n++) {
|
|
495
|
-
const r = await pmGet('/api/initiatives?ws=' + encodeURIComponent(S.ws) + '&view=roadmap');
|
|
496
|
-
if (r && r.horizons) {
|
|
497
|
-
S.initRows = { rows: [...(r.horizons.now || []), ...(r.horizons.next || []), ...(r.horizons.later || []), ...(r.dormant || [])], sig: r.sig || null, strategy: r.strategy || null };
|
|
498
|
-
S.initNote.hidden = true;
|
|
499
|
-
return;
|
|
500
|
-
}
|
|
501
|
-
if (!(r && r.pending)) {
|
|
502
|
-
S.initRows = { rows: [], sig: null };
|
|
503
|
-
S.initNote.hidden = false;
|
|
504
|
-
S.initNote.textContent = 'The initiatives could not be read' + (r && r.error ? ': ' + r.error : '.');
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
S.initNote.hidden = false; S.initNote.textContent = 'Reading the repository';
|
|
508
|
-
await new Promise((res) => setTimeout(res, 4000));
|
|
509
|
-
}
|
|
510
|
-
S.initRows = { rows: [], sig: null };
|
|
511
|
-
S.initNote.textContent = 'The repository is taking long to read. Reload in a moment for the initiatives.';
|
|
512
|
-
}
|
|
513
|
-
function showInit(id) {
|
|
514
|
-
S.init = id; drawInitSwitch();
|
|
515
|
-
const ws = S.workspaces.find((w) => w.id === S.ws) || {};
|
|
516
|
-
for (const [k, , mount] of INITS) {
|
|
517
|
-
if (!mount) continue;
|
|
518
|
-
S.initHosts[k].hidden = k !== id;
|
|
519
|
-
if (k === id && !S.mounted['init-' + k] && S.initRows) {
|
|
520
|
-
S.mounted['init-' + k] = true;
|
|
521
|
-
const ctx = { ...initiativesViewCtx({ repo: ws.root || null, ws: S.ws, rows: S.initRows.rows, sig: S.initRows.sig, openInitiative: () => location.assign('/classic.html'), setState: () => {} }), strategy: S.initRows.strategy, plan: planNode(sc(OWN.has(k) ? 'init-' + k : 'init')), links: crossLinks((t) => showTab(t, true)) };
|
|
522
|
-
if (k === 'lifecycle' || OWN.has(k)) mount(S.initHosts[k], Object.freeze(ctx));
|
|
523
|
-
else { const view = el('div'); S.initHosts[k].replaceChildren(ctx.plan, view); mount(view, Object.freeze({ ...ctx, plan: null })); }
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
450
|
/* Reports: the headline deck on top (the Plan deck of the Reports scope), then the three tabs (Dashboards, Reports, Charts; c3-reports.js).
|
|
528
451
|
* The Reports deck always names its scope, so real mode gets the engine's report signals and never the Briefing's cards (console3-deck-flow-fixes). */
|
|
529
|
-
function repTab(pane) { mountReports(pane, { ws: S.ws, pmGet, deck: (h) => h.append(planNode('rep')) }); }
|
|
452
|
+
function repTab(pane) { return import('./c3-reports.js').then((m) => m.mountReports(pane, { ws: S.ws, pmGet, deck: (h) => h.append(planNode('rep')) })); }
|
|
453
|
+
/* Reports opens on a view (Drafts, a dashboard). Before the tab has loaded, c3-reports.js keeps the view and opens on it. */
|
|
454
|
+
const reportsView = (v) => import('./c3-reports.js').then((m) => m.showReportsView(v));
|
|
530
455
|
|
|
531
|
-
|
|
456
|
+
/* Each tab's code loads the first time the tab is shown (c3-desktop-light-load): the page starts with the Briefing's modules only.
|
|
457
|
+
* The Work tab's module (pm-work-view.js) is already loaded for the console's shared helpers. */
|
|
458
|
+
const BUILD = { brief: briefTab,
|
|
459
|
+
cal: (p) => import('./calendar-views.js').then((m) => m.mountCalendarViews(p, { view: 'week', page: true, scope: sc('cal'), deck: planNode('cal', { bare: true }) })),
|
|
460
|
+
work: (p) => { mountPmWork(p, { go: (t) => showTab(t, true), ws: S.ws }); },
|
|
461
|
+
init: (p) => import('./c3-initiatives.js').then((m) => m.mountInitiatives(p, { ws: S.ws, workspaces: S.workspaces, demo: DEMO, showTab })),
|
|
462
|
+
rep: repTab };
|
|
463
|
+
/* A tab whose code could not load says so in its pane, and the next press tries again. */
|
|
464
|
+
function build(id) {
|
|
465
|
+
S.mounted[id] = Promise.resolve().then(() => BUILD[id](S.panes[id])).catch((e) => {
|
|
466
|
+
delete S.mounted[id];
|
|
467
|
+
S.panes[id].replaceChildren(el('p', { class: 'dim mono c3s2', role: 'status', 'data-c3pg': 'tab-error' }, 'This page could not load: ' + String((e && e.message) || e) + '. Open the tab again to retry.'));
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
/* Settings loads on its first opening (c3-settings.js). `then(fn)` runs fn with the mounted Settings. */
|
|
471
|
+
function settingsReady() {
|
|
472
|
+
if (!S.settingsLoad) {
|
|
473
|
+
S.settingsCtx = { ws: S.ws, workspaces: S.workspaces, theme: S.theme, themePref: S.themePref, setTheme, goTab: (t, v) => { showTab(t, true); if (t === 'rep' && v) reportsView(v); }, section: q.get('section') || '', root: S.root };
|
|
474
|
+
S.settingsLoad = import('./c3-settings.js').then((m) => { S.settings = m.mountSettings(S.panes.settings, S.settingsCtx); return S.settings; });
|
|
475
|
+
S.settingsLoad.catch((e) => {
|
|
476
|
+
S.settingsLoad = null;
|
|
477
|
+
S.panes.settings.replaceChildren(el('p', { class: 'dim mono c3s2', role: 'status', 'data-c3pg': 'tab-error' }, 'Settings could not load: ' + String((e && e.message) || e) + '. Open Settings again to retry.'));
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
return S.settingsLoad;
|
|
481
|
+
}
|
|
532
482
|
|
|
533
483
|
function showTab(id, push) {
|
|
534
484
|
if (!BUILD[id] && id !== 'settings') id = 'brief';
|
|
@@ -543,9 +493,9 @@ function showTab(id, push) {
|
|
|
543
493
|
if (S.gearBtn) S.gearBtn.setAttribute('aria-pressed', String(id === 'settings'));
|
|
544
494
|
if (id === 'settings') {
|
|
545
495
|
S.ttl.replaceChildren(gearIcon('lg'), 'Settings'); document.title = 'Settings · PMOS console';
|
|
546
|
-
if (!S.settings)
|
|
496
|
+
if (!S.settings) settingsReady();
|
|
547
497
|
else S.settings.refresh();
|
|
548
|
-
} else if (!S.mounted[id])
|
|
498
|
+
} else if (!S.mounted[id]) build(id);
|
|
549
499
|
S.root.dataset.tab = id;
|
|
550
500
|
S.dockOpen = S.wide && id === 'brief' && dockPref(); syncDock(); // the dock starts folded (c3-quiet-pages); Ctrl J or the More menu opens it, and the choice is kept
|
|
551
501
|
S.pageView.classList.remove('view'); void S.pageView.offsetWidth; S.pageView.classList.add('view');
|
|
@@ -565,22 +515,27 @@ function briefCount() {
|
|
|
565
515
|
onBriefing((x) => { b = x; upd(); }); onBriefPage((x) => { page = x; upd(); });
|
|
566
516
|
loadBriefing(); if (S.tab !== 'brief') loadBriefPage();
|
|
567
517
|
}
|
|
518
|
+
/* The Briefing's first read has answered (or 4 s have passed), and the browser has a free moment. */
|
|
519
|
+
function afterBriefing() {
|
|
520
|
+
const idle = () => new Promise((res) => (typeof requestIdleCallback === 'function' ? requestIdleCallback(() => res(), { timeout: 1500 }) : setTimeout(res, 200)));
|
|
521
|
+
return Promise.race([loadBriefing(), new Promise((res) => setTimeout(res, 4000))]).then(idle, idle);
|
|
522
|
+
}
|
|
568
523
|
async function askCount() {
|
|
569
524
|
if (S.demo) { setBdg(S.initBdg, 4); return; }
|
|
570
|
-
const j = await pmGet('/api/needs?ws=' + encodeURIComponent(S.ws)).catch(() => null);
|
|
525
|
+
const j = await pmGet('/api/needs?ws=' + encodeURIComponent(S.ws) + '&kind=triage').catch(() => null); // only the triage rows: the badge counts them
|
|
571
526
|
if (!j || !Array.isArray(j.needs)) return;
|
|
572
527
|
setBdg(S.initBdg, j.needs.filter((r) => r && r.kind === 'triage').length);
|
|
573
528
|
}
|
|
574
529
|
|
|
575
530
|
async function workCount() {
|
|
576
|
-
const j = await pmGet('/api/pm-items').catch(() => null);
|
|
577
|
-
if (!j || !j.ok || !j.lists) return;
|
|
531
|
+
const j = await pmGet('/api/pm-items?counts=1').catch(() => null); // the counts alone; the Work page reads the lists when it opens
|
|
532
|
+
if (!j || !j.ok || !(j.lists || j.counts)) return;
|
|
578
533
|
if (j.counts && typeof j.counts.inbox === 'number') { // the Work page's own counts: what is to process (inbox) plus what is on today
|
|
579
534
|
const w = j.counts.inbox + (Number(j.counts.today) || 0);
|
|
580
535
|
S.workBdg.textContent = String(w); S.workBdg.hidden = w === 0;
|
|
581
536
|
return;
|
|
582
537
|
}
|
|
583
|
-
const
|
|
538
|
+
const L = j.lists || {}; const n = (L.triage || []).length + (S.demo ? 0 : (L.today || []).length); // the mockup's Work badge counts what is to triage
|
|
584
539
|
S.workBdg.textContent = String(n); S.workBdg.hidden = n === 0;
|
|
585
540
|
}
|
|
586
541
|
|
|
@@ -628,7 +583,8 @@ async function boot() {
|
|
|
628
583
|
if (q.get('studio') && /^[a-z][a-z0-9-]{0,39}$/.test(q.get('studio'))) { if (!start) start = 'rep'; import('./c3-design-studio.js').then((m) => m.openStudio({ ws: S.ws, id: q.get('studio'), name: '' })).catch(() => { /* the studio is not available */ }); }
|
|
629
584
|
if (!S.userNav) showTab(start || 'brief', false); // a press during the read above (the gear, a tab) wins over the start page
|
|
630
585
|
new ResizeObserver(moveInd).observe(S.tabNav);
|
|
631
|
-
|
|
586
|
+
briefCount(); S.dock.start(); S.dockStarted = true;
|
|
587
|
+
afterBriefing().then(() => { workCount(); askCount(); }); // the Work and Initiatives counts wait until the Briefing has its own reads
|
|
632
588
|
setInterval(() => { S.sub.textContent = dateLine(); }, 30000);
|
|
633
589
|
}
|
|
634
590
|
|
|
@@ -173,6 +173,17 @@
|
|
|
173
173
|
"B1 with nothing stored, status says absent",
|
|
174
174
|
"B5 status never carries the key or its length"
|
|
175
175
|
]
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"id": "STD-REFRESH",
|
|
179
|
+
"file": "workspace/proactive.test.js",
|
|
180
|
+
"run": "data-refresh-settings",
|
|
181
|
+
"ref": "planning/prd/data-refresh-settings-prd.md R3 (the PM said Build on 2026-10-14)",
|
|
182
|
+
"reason": "R3 moves the morning brief's schedule from schedules.json to Settings › Data refresh. The brief set no longer writes a schedule; it turns the brief source on, daily at 06:45. H8 now asserts that through GET /api/refresh, and I7 accepts the CLI's new 'already on in Settings, Data refresh' line.",
|
|
183
|
+
"names": [
|
|
184
|
+
"H8 one press creates the daily 06:45 morning-brief schedule once; a second press adds nothing",
|
|
185
|
+
"I7 `brief schedule` reports that the schedule already exists"
|
|
186
|
+
]
|
|
176
187
|
}
|
|
177
188
|
],
|
|
178
189
|
"entries": [
|
package/workspace/server.js
CHANGED
|
@@ -79,6 +79,9 @@ const validateLib = require('./lib/validate.js');
|
|
|
79
79
|
const pageAgent = require('./lib/page-agent.js'); // page-starts-agent — the fixed prompts a page press may start
|
|
80
80
|
const scanRunLib = require('./lib/scan-run.js'); // horizon-scan — the watch list suggest run and the scheduled scans, in the 60-second tick
|
|
81
81
|
const reportFinderLib = require('./lib/report-finder.js'); // report-workspace — the weekly look for the reports the PM already sends
|
|
82
|
+
const refreshLib = require('./lib/refresh.js'); // data-refresh-settings — the sources, their schedules and the refresh log
|
|
83
|
+
const refreshRunLib = require('./lib/refresh-run.js'); // data-refresh-settings — the engine bound to this server (created by the tick below)
|
|
84
|
+
let refreshRun = null;
|
|
82
85
|
const driverRunLib = require('./lib/driver-run.js'); // initiative-driver — wakes, the unblock sweep and the runs, in the 60-second tick
|
|
83
86
|
const headlessLib = require('./lib/headless.js'); // workspace-headless-jobs — which jobs run as `claude -p`, and with which tools
|
|
84
87
|
const liveNotes = require('./lib/live-notes.js'); // jev-live-notes — reads and appends a repository's live-notes ledger; no network
|
|
@@ -1945,7 +1948,7 @@ let kitBusy = null; // workspace-kit-update: the one kit or app update
|
|
|
1945
1948
|
const autoSync = require('./lib/auto-sync').createAutoSync({
|
|
1946
1949
|
roots: () => [...new Set([...workspaces.values()].map((w) => w.root))],
|
|
1947
1950
|
busy: syncing,
|
|
1948
|
-
onDone: (root) => boardCache.invalidate(root),
|
|
1951
|
+
onDone: (root, out) => { boardCache.invalidate(root); try { if (refreshRun) refreshRun.codeRun('sync', { trigger: 'change', result: out && out.ok ? 'done' : 'failed', got: out && out.ok ? (out.message || 'synced') : null, error: out && !out.ok ? out.message || 'the sync failed' : null }); } catch { /* the log is not the sync's business */ } },
|
|
1949
1952
|
label: `${os.hostname()} (auto-sync)`,
|
|
1950
1953
|
});
|
|
1951
1954
|
// workspace-data-freshness (R2 to R4): the facts cache is kept in the runtime state folder, and ONE change detector per
|
|
@@ -1958,7 +1961,13 @@ const freshness = require('./lib/freshness.js').create({
|
|
|
1958
1961
|
workspaces: () => [...workspaces.values()],
|
|
1959
1962
|
schedules: () => readSchedules(),
|
|
1960
1963
|
factsStatus: (root) => factsAsync.status(root),
|
|
1961
|
-
onChange: (w) => {
|
|
1964
|
+
onChange: (w) => {
|
|
1965
|
+
// data-refresh-settings: the repository source is the rebuild on change; switched off in Data refresh, a change rebuilds nothing.
|
|
1966
|
+
if (refreshRun && !refreshRun.jobGate('repo').ok) return;
|
|
1967
|
+
boardCache.board(w.root, { refreshNow: true }); factsAsync.refresh(w.root);
|
|
1968
|
+
if (refreshRun) refreshRun.codeRun('repo', { trigger: 'change', ws: w.id, got: 'a change in the repository' });
|
|
1969
|
+
},
|
|
1970
|
+
scheduleOf: (skill) => (refreshRun && refreshRun.managedSkill(skill) ? refreshRun.scheduleOfSkill(skill) : undefined),
|
|
1962
1971
|
pollMs: Math.max(250, Number(process.env.PMOS_FRESH_POLL_MS) || 10000),
|
|
1963
1972
|
stableMs: Math.max(0, Number(process.env.PMOS_FRESH_STABLE_MS) || 3000),
|
|
1964
1973
|
});
|
|
@@ -2621,7 +2630,62 @@ const coreDocsTick = () => {
|
|
|
2621
2630
|
proSettings, inQuiet: (now, quiet) => proactiveLib.inQuietHours(now, quiet), isLead: (w) => isPlanNeedsTree(w) }, Number.isFinite(t) ? { now: t } : {});
|
|
2622
2631
|
} catch (e) { warnWriteFail('core documents tick', e); }
|
|
2623
2632
|
};
|
|
2624
|
-
|
|
2633
|
+
// data-refresh-settings: the refresh engine. It starts the skill sources that are due (calendar, Jira, work assigned to you, tracker,
|
|
2634
|
+
// the morning brief), writes the refresh log, and gates the job ticks below: a job that is off, or whose input is still running, waits.
|
|
2635
|
+
const REFRESH_BOOT = Date.now();
|
|
2636
|
+
refreshRun = refreshRunLib.create({
|
|
2637
|
+
stateDir: STATE,
|
|
2638
|
+
now: () => { const t = process.env.PMOS_REFRESH_NOW ? Date.parse(process.env.PMOS_REFRESH_NOW) : NaN; return Number.isFinite(t) ? t + (Date.now() - REFRESH_BOOT) : Date.now(); },
|
|
2639
|
+
homeWs: () => { const h = homeWorkspace(); return h ? h.id : null; },
|
|
2640
|
+
proactive: () => proSettings(),
|
|
2641
|
+
schedules: { read: readSchedules, write: writeSchedules },
|
|
2642
|
+
startSkill: (wsId, skill, name, origin) => startSkillSession(wsId, skill, name, origin),
|
|
2643
|
+
sessionAlive: (id) => { const x = sessions.get(String(id || '')); return !!x && x.state !== 'exited'; },
|
|
2644
|
+
freshRows: (wsIdArg) => { const w = getWs(wsIdArg || ''); return w ? freshness.sources({ id: w.id, root: w.root, repoStateDir: (() => { try { return stateDirFor(w); } catch { return null; } })() }) : []; },
|
|
2645
|
+
copyPaths: (id, wsIdArg) => {
|
|
2646
|
+
const w = getWs(wsIdArg || '');
|
|
2647
|
+
if (id === 'mywork') return [path.join(STATE, 'my-work', 'read.json')];
|
|
2648
|
+
if (id === 'jira' && w) { let d = null; try { d = stateDirFor(w); } catch { d = null; } return d ? [path.join(d, 'tracker', 'jira')] : []; }
|
|
2649
|
+
return [];
|
|
2650
|
+
},
|
|
2651
|
+
tell: ({ source, label, error, ws }) => {
|
|
2652
|
+
const w = getWs(ws || '') || homeWorkspace(); const nf = w ? needsFileFor(w) : null; if (!nf) return;
|
|
2653
|
+
let needs = []; try { needs = JSON.parse(fs.readFileSync(nf, 'utf8')); } catch { needs = []; }
|
|
2654
|
+
needs.push({ run_id: `refresh-${source}-${Date.now()}`, kind: 'refresh', initiative_id: label, question: `${label} could not refresh: ${String(error).slice(0, 300)} See Settings, Data refresh.` });
|
|
2655
|
+
fs.writeFileSync(nf, JSON.stringify(needs)); invalidateNeedsAgg(w.id);
|
|
2656
|
+
},
|
|
2657
|
+
setDayCheckEvery: (every) => {
|
|
2658
|
+
let raw = {}; try { raw = JSON.parse(fs.readFileSync(PRO_SETTINGS(), 'utf8')); } catch { raw = {}; }
|
|
2659
|
+
const cur = proSettings();
|
|
2660
|
+
const n = proactiveLib.normalizeSettings({ ...raw, dayCheck: { ...cur.dayCheck, every: Number(every) } });
|
|
2661
|
+
if (!n.ok) throw new Error(n.error);
|
|
2662
|
+
fs.mkdirSync(STATE, { recursive: true }); fs.writeFileSync(PRO_SETTINGS(), JSON.stringify(n.settings) + '\n');
|
|
2663
|
+
},
|
|
2664
|
+
setRepoWatch: (on) => { for (const w of workspaces.values()) freshness.setEnabled(w.root, on); },
|
|
2665
|
+
jobs: {
|
|
2666
|
+
daycheck: () => {
|
|
2667
|
+
const date = briefLib.localDate(Date.now()); const run = readDayRun(date);
|
|
2668
|
+
if (!run) return { ok: false, error: 'There is no plan for today yet. Press Prepare my day first; the day check reads that plan.' };
|
|
2669
|
+
const c = startDayCheck(run, 'pressed', Date.now());
|
|
2670
|
+
if (!c.ok) return { ok: false, error: c.error };
|
|
2671
|
+
writeDayRun(c.run); return { ok: true, started: 'session', session: c.session };
|
|
2672
|
+
},
|
|
2673
|
+
scan: (wsIdArg) => { const w = getWs(wsIdArg || '') || homeWorkspace(); if (!w) return { ok: false, error: 'no workspace' }; const r = scanner.startNow(w, {}, { by: 'pm' }); return r.ok ? { ok: true } : { ok: false, status: r.status, error: r.error }; },
|
|
2674
|
+
coredocs: () => { let m; try { m = require('./lib/routes-core-docs.js'); } catch { return { ok: false, error: 'core documents are not installed' }; }
|
|
2675
|
+
m.tick({ getWs, stateDirFor, workspaceList: () => [...workspaces.values()], runtimeDir: STATE, selfRoot: EMBEDDED_SELF ? SELF_ROOT : null, initiativeIndex,
|
|
2676
|
+
spawnSession, stopSession: (id) => { const x = sessions.get(String(id || '')); if (x && x.child) stopHeadless(x); }, invalidateNeedsAgg,
|
|
2677
|
+
proSettings, inQuiet: () => false, isLead: (w) => isPlanNeedsTree(w) }, { force: true });
|
|
2678
|
+
return { ok: true }; },
|
|
2679
|
+
driver: () => { const r = driver.tick({ sweepNow: true }); return r && r.off ? { ok: false, error: 'The initiative driver is switched off on this computer.' } : { ok: true }; },
|
|
2680
|
+
finder: () => reportFinder.runNow(),
|
|
2681
|
+
signals: () => { const t0 = Date.now(); const r = proactiveScan({ manual: true }); refreshRun.codeRun('signals', { trigger: 'pressed', took_ms: Date.now() - t0, got: r && r.ok ? `${r.appended || 0} new signals` : null, result: r && r.ok ? 'done' : 'failed', error: r && r.ok ? null : (r && r.error) || 'the scan failed' }); return { ok: true, started: 'done' }; },
|
|
2682
|
+
repo: () => { for (const w of workspaces.values()) { boardCache.board(w.root, { refreshNow: true }); factsAsync.refresh(w.root, { force: true }); } refreshRun.codeRun('repo', { trigger: 'pressed', got: 'board and initiative facts rebuilt' }); return { ok: true, started: 'rebuild' }; },
|
|
2683
|
+
sync: () => { autoSync.tick().then((out) => { const ran = (out || []).filter(Boolean); refreshRun.codeRun('sync', { trigger: 'pressed', result: ran.every((x) => x.ok) ? 'done' : 'failed', got: ran.length ? `${ran.length} repositories synced` : 'nothing to sync', error: ran.filter((x) => !x.ok).map((x) => x.message).join('; ') || null }); }).catch(() => {}); return { ok: true, started: 'sync' }; },
|
|
2684
|
+
},
|
|
2685
|
+
warn: (what, e) => warnWriteFail(what, e),
|
|
2686
|
+
});
|
|
2687
|
+
const gated = (id, fn) => () => { let g = { ok: true }; try { g = refreshRun.jobGate(id); } catch { g = { ok: true }; } if (g.ok) fn(); };
|
|
2688
|
+
try { const _tick = setInterval(() => { checkDueSchedules(); proactiveTick(); try { prepareTick({ manual: false }); } catch { /* the next tick tries again */ } try { dayTick(); } catch (e) { warnWriteFail('day run tick', e); } try { refreshRun.tick(); } catch (e) { warnWriteFail('refresh tick', e); } gated('sync', () => autoSync.tick().catch(() => {}))(); dashboardScheduleTick(); gated('driver', driverTick)(); gated('scan', scanTick)(); gated('finder', reportFinderTick)(); gated('coredocs', coreDocsTick)(); reportDesignTick(); }, TICK_MS); if (_tick.unref) _tick.unref(); } catch {}
|
|
2625
2689
|
|
|
2626
2690
|
// ── Sessions: real PTYs, owned by the server, with disk-backed scrollback + metadata. ──
|
|
2627
2691
|
/** @type {Map<string, {id,ws,name,agent,cwd,state,note,created,pty,buf,cols,rows,clients:Set}>} */
|
|
@@ -3408,6 +3472,7 @@ function attachPty(s, { seed = false } = {}) {
|
|
|
3408
3472
|
: `exited (code ${exitCode})`;
|
|
3409
3473
|
appendBuf(s, `\r\n\x1b[38;5;244m[${s.note}]\x1b[0m\r\n`);
|
|
3410
3474
|
persist(s); pushSessions();
|
|
3475
|
+
noteSessionEnd(s);
|
|
3411
3476
|
});
|
|
3412
3477
|
return p;
|
|
3413
3478
|
}
|
|
@@ -3583,6 +3648,7 @@ function startHeadless(base, workdir, kind, launchArgs, env, initialInput, addDi
|
|
|
3583
3648
|
persist(s); pushSessions();
|
|
3584
3649
|
// page-starts-agent: a caller that started this run from the page reads its result here (a proposal, never a write).
|
|
3585
3650
|
if (typeof onDone === 'function') { try { onDone({ result: r, code, signal, timedOut, stopped: !!s.stoppedByPm, note: s.note }); } catch (e) { warnWriteFail('headless result', e); } }
|
|
3651
|
+
noteSessionEnd(s);
|
|
3586
3652
|
});
|
|
3587
3653
|
child.stdin.on('error', () => {});
|
|
3588
3654
|
child.stdin.end(prompt);
|
|
@@ -3603,7 +3669,15 @@ function killHeadless(child, sig) {
|
|
|
3603
3669
|
try { process.kill(-child.pid, sig); } catch { try { child.kill(sig); } catch {} }
|
|
3604
3670
|
}
|
|
3605
3671
|
|
|
3606
|
-
|
|
3672
|
+
// data-refresh-settings: every session passes the refresh engine on its way out, so a background refresh writes its start line
|
|
3673
|
+
// (lib/refresh-run.js started: only an origin it knows is logged). The end is noted where a session exits (noteSessionEnd).
|
|
3674
|
+
function spawnSession(opts) {
|
|
3675
|
+
const s = spawnSessionInner(opts);
|
|
3676
|
+
try { if (s && refreshRun) refreshRun.started(s); } catch (e) { warnWriteFail('refresh log (start)', e); }
|
|
3677
|
+
return s;
|
|
3678
|
+
}
|
|
3679
|
+
function noteSessionEnd(s) { try { if (refreshRun) refreshRun.ended(s); } catch (e) { warnWriteFail('refresh log (end)', e); } }
|
|
3680
|
+
function spawnSessionInner({ name, agent, cwd, ws, origin, initialInput, profile, initiative, modelAsk, planItem, workType, autoDelete, launchFlags, addDirs, onDone, scratch, extraEnv, writePaths }) {
|
|
3607
3681
|
const id = nextSessionId();
|
|
3608
3682
|
/* pm-os-absorb Run 1 — every AGENT session gets a token, `<id>.<8 hex>`, in its environment as PMOS_SESSION, and
|
|
3609
3683
|
* PMOS_BIN, this workspace's own bin/pmos.js. `pmos item` run inside it is then attributed `agent:<token>` (the token
|
|
@@ -3790,6 +3864,7 @@ function spawnSession({ name, agent, cwd, ws, origin, initialInput, profile, ini
|
|
|
3790
3864
|
: `exited (code ${exitCode})`;
|
|
3791
3865
|
appendBuf(s, `\r\n\x1b[38;5;244m[${s.note}]\x1b[0m\r\n`);
|
|
3792
3866
|
persist(s); pushSessions();
|
|
3867
|
+
noteSessionEnd(s);
|
|
3793
3868
|
});
|
|
3794
3869
|
feed(s);
|
|
3795
3870
|
persist(s); pushSessions(); pushWorkspaces();
|
|
@@ -4345,7 +4420,7 @@ function dayTick() {
|
|
|
4345
4420
|
if (a.ok) run = a.run;
|
|
4346
4421
|
run = startDayTasks(run, now);
|
|
4347
4422
|
const due = dayRunLib.checkDue(run, { settings: proSettings(), now, events: brief ? brief.calendar || [] : [] });
|
|
4348
|
-
if (due.due) { const c = startDayCheck(run, due.reason, now); if (c.ok) run = c.run; }
|
|
4423
|
+
if (due.due && refreshRun.jobGate('daycheck').ok) { const c = startDayCheck(run, due.reason, now); if (c.ok) run = c.run; }
|
|
4349
4424
|
run.checks.next = due.next;
|
|
4350
4425
|
if (JSON.stringify(run) !== before) writeDayRun(run);
|
|
4351
4426
|
}
|
|
@@ -4364,13 +4439,15 @@ function myWorkRead(wsId) {
|
|
|
4364
4439
|
return { ok: true, session: s.id };
|
|
4365
4440
|
}
|
|
4366
4441
|
/** Refresh now (workspace-data-freshness R7): start skill `skillId` in a session of repository `wsId`, as myWorkRead does. */
|
|
4367
|
-
function startSkillSession(wsId, skillId, name) {
|
|
4442
|
+
function startSkillSession(wsId, skillId, name, origin) {
|
|
4368
4443
|
const w = getWs(wsId);
|
|
4369
4444
|
if (!w) return { ok: false, status: 400, error: 'no workspace' };
|
|
4370
4445
|
const task = taskById(skillId, w.root);
|
|
4371
4446
|
if (!task || task.kind !== 'agent' || !task.prompt) return { ok: false, status: 409, error: `the ${skillId} skill is not installed for this repository` };
|
|
4372
4447
|
const prompt = skillStore.retargetFiling(task.prompt, filingContract(w));
|
|
4373
|
-
|
|
4448
|
+
// data-refresh-settings: a refresh of a source runs as `refresh:<source>`, so the refresh log records it whoever pressed it.
|
|
4449
|
+
const src = refreshLib.SKILL_OF.get(skillId);
|
|
4450
|
+
const s = spawnSession({ name, agent: task.agent || 'claude', ws: w.id, origin: origin || (src ? 'refresh:' + src : 'source-refresh'), initialInput: prompt + '\r',
|
|
4374
4451
|
modelAsk: { scheduleTier: null, skillTier: task.tier || null, skill: task.skill } });
|
|
4375
4452
|
if (!s || s.state === 'exited') return { ok: false, status: 500, error: (s && s.note) || 'the refresh session could not start' };
|
|
4376
4453
|
return { ok: true, session: s.id };
|
|
@@ -4593,7 +4670,12 @@ function proactiveTick() {
|
|
|
4593
4670
|
const now = Date.now();
|
|
4594
4671
|
if (now - proLastScan < PRO_SCAN_EVERY_MS) return;
|
|
4595
4672
|
proLastScan = now;
|
|
4596
|
-
|
|
4673
|
+
if (refreshRun.jobGate('signals').ok) {
|
|
4674
|
+
try {
|
|
4675
|
+
const r = proactiveScan({ manual: false });
|
|
4676
|
+
if (r && !r.skipped) refreshRun.codeRun('signals', { trigger: 'schedule', took_ms: Date.now() - now, got: `${r.appended || 0} new signals` });
|
|
4677
|
+
} catch (e) { warnWriteFail('proactive scan', e); refreshRun.codeRun('signals', { trigger: 'schedule', result: 'failed', error: String((e && e.message) || e) }); }
|
|
4678
|
+
}
|
|
4597
4679
|
try { learnWeekly({ manual: false }); } catch (e) { warnWriteFail('proactive weekly review', e); }
|
|
4598
4680
|
}
|
|
4599
4681
|
|
|
@@ -6662,6 +6744,8 @@ async function handleRequest(req, res) {
|
|
|
6662
6744
|
const list = readSchedules(); const made = []; const kept = [];
|
|
6663
6745
|
for (const def of set) {
|
|
6664
6746
|
if (!taskById(def.skill, w.root)) { kept.push({ skill: def.skill, why: 'the skill is not installed' }); continue; }
|
|
6747
|
+
// data-refresh-settings (R3): a refresh skill is scheduled in Settings › Data refresh. The set turns its source on there.
|
|
6748
|
+
if (refreshRun.managedSkill(def.skill)) { const turned = refreshRun.ensureOn(refreshRun.managedSkill(def.skill), w.id); kept.push({ skill: def.skill, why: turned ? 'turned on in Settings, Data refresh' : 'already on in Settings, Data refresh', refresh: refreshRun.managedSkill(def.skill) }); continue; }
|
|
6665
6749
|
if (list.some((x) => x.ws === w.id && x.skill === def.skill)) { kept.push({ skill: def.skill, why: 'already scheduled' }); continue; }
|
|
6666
6750
|
const norm = scheduleLogic.normalizeSchedule({ ...def, task: def.skill });
|
|
6667
6751
|
if (!norm.ok) { kept.push({ skill: def.skill, why: norm.error }); continue; }
|
|
@@ -6971,8 +7055,12 @@ async function handleRequest(req, res) {
|
|
|
6971
7055
|
? { count: strict.empty ? 0 : strict.data.items.length, error: null }
|
|
6972
7056
|
: { count: null, error: strict.error } });
|
|
6973
7057
|
}
|
|
7058
|
+
// ?kind=a,b keeps only those kinds (the console's Initiatives badge counts triage; the Briefing's agent rows read three kinds):
|
|
7059
|
+
// the full queue is large and most readers want a few rows of it (c3-desktop-light-load).
|
|
7060
|
+
const kinds = String(url.searchParams.get('kind') || '').split(',').map((k) => k.trim()).filter(Boolean);
|
|
7061
|
+
const queue = strict.ok ? strict.all : readNeeds(w);
|
|
6974
7062
|
return json(res, {
|
|
6975
|
-
needs:
|
|
7063
|
+
needs: kinds.length && Array.isArray(queue) ? queue.filter((n) => n && kinds.includes(n.kind)) : queue,
|
|
6976
7064
|
state: strict.ok ? { ok: true } : { ok: false, error: strict.error },
|
|
6977
7065
|
by_ws: needsByWorkspace(),
|
|
6978
7066
|
});
|
|
@@ -8380,9 +8468,11 @@ async function handleRequest(req, res) {
|
|
|
8380
8468
|
if (!taskById('morning-brief', w.root)) return json(res, { ok: false, error: 'the morning-brief skill is not installed for this repository' }, 409);
|
|
8381
8469
|
const norm = scheduleLogic.normalizeSchedule({ ...SEED_SETS.brief[0], task: 'morning-brief' });
|
|
8382
8470
|
if (!norm.ok) return json(res, { ok: false, error: norm.error }, 500);
|
|
8383
|
-
|
|
8384
|
-
sched
|
|
8471
|
+
// data-refresh-settings (R3): the brief's schedule lives in Settings › Data refresh. The entry kept here only carries this run.
|
|
8472
|
+
sched = { id: 'sc-' + Date.now().toString(36) + 'g', ws: w.id, ...norm.schedule, profile: null, lastRun: null, lastStatus: null, enabled: false, endedReason: 'Moved to Settings › Data refresh' };
|
|
8473
|
+
sched.nextRun = null;
|
|
8385
8474
|
const list = readSchedules(); list.unshift(sched); writeSchedules(list); seeded = true;
|
|
8475
|
+
try { refreshRun.ensureOn('brief', w.id); } catch (e) { warnWriteFail('refresh settings (brief)', e); }
|
|
8386
8476
|
}
|
|
8387
8477
|
const result = fireSchedule(sched);
|
|
8388
8478
|
// A session that could not start is recorded as already ended, so the page says why instead of waiting on a read.
|
|
@@ -8630,6 +8720,8 @@ async function handleRequest(req, res) {
|
|
|
8630
8720
|
// A brief for a day the PM pressed "Prepare my day": its agent priorities become tasks and start (G13), and a merge
|
|
8631
8721
|
// closes the check that sent it (G22).
|
|
8632
8722
|
try { dayAfterBrief(out.date, merged); } catch (e) { warnWriteFail('day run after the brief', e); }
|
|
8723
|
+
// data-refresh-settings: a full brief ends the morning brief's refresh run (its session stays open after it submits).
|
|
8724
|
+
if (body.merge !== true) { try { refreshRun.briefDone(`${(out.items || []).length} items`); } catch (e) { warnWriteFail('refresh log (brief)', e); } }
|
|
8633
8725
|
return json(res, { ok: true, brief: out, ...(merged ? { merged } : {}), ...(remembered ? { remembered } : {}), ...(memoryError ? { memory_error: memoryError } : {}) });
|
|
8634
8726
|
}
|
|
8635
8727
|
if (req.method === 'GET') {
|
|
@@ -9078,7 +9170,9 @@ async function handleRequest(req, res) {
|
|
|
9078
9170
|
// c3-initiative-boards: the PMOS checkout this server is embedded in, whose default board keeps its derived ladder
|
|
9079
9171
|
selfRoot: EMBEDDED_SELF ? SELF_ROOT : null,
|
|
9080
9172
|
// horizon-scan-portfolio: Scan now, Stop and an approved research proposal start and end runs in the scanner; scanClock fixes its clock in the suites
|
|
9081
|
-
scanner, scanClock
|
|
9173
|
+
scanner, scanClock,
|
|
9174
|
+
// data-refresh-settings: Settings › Data refresh (lib/routes-refresh.js)
|
|
9175
|
+
refreshRun })) return;
|
|
9082
9176
|
res.writeHead(404); res.end('not found');
|
|
9083
9177
|
}
|
|
9084
9178
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Initiative c3-desktop-light-load: the desktop console loads the Briefing's code and reads only, each tab's code on its first opening, the folded dock reads only the runs, the badges read counts, the pulse reads the gantt rows alone, no read twice on open.",
|
|
3
|
+
"modules": ["light-load.test.js"],
|
|
4
|
+
"panels": ["c3-light-load-test.js"],
|
|
5
|
+
"seconds": { "light-load.test.js": 15, "c3-light-load-test.js": 60 }
|
|
6
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "initiative data-refresh-settings: the refresh engine (lib/refresh.js) and its routes on a real server (lib/routes-refresh.js, lib/refresh-run.js).",
|
|
3
|
+
"modules": ["refresh.test.js", "refresh-routes.test.js"],
|
|
4
|
+
"seconds": { "refresh.test.js": 2, "refresh-routes.test.js": 25 }
|
|
5
|
+
}
|