create-pmos 0.210.0 → 0.211.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pmos",
3
- "version": "0.210.0",
3
+ "version": "0.211.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": {
@@ -1,3 +1,3 @@
1
- kernel-source: pmos@97b9da7
1
+ kernel-source: pmos@d5e2b69
2
2
  assembled: 2026-10-14
3
3
  mode: vendor
@@ -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-14T01:55:32Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T02:17:42Z"}
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-14T01:55:32Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T02:17:42Z"}
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-14T01:55:32Z"}
5
+ generated: {by: process:create-pmos, at: "2026-10-14T02:17:42Z"}
6
6
  status: stable
7
7
  ---
8
8
 
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ /* c3-light-load-test.js — the desktop console's light first load in a browser (initiative c3-desktop-light-load, PRD R1 to R6).
3
+ * Real mode at 1440 wide on a scratch server: what the Briefing loads and reads on open, then that each tab, Settings, the
4
+ * calendar popup and the dock still load when they are first opened. light-load.test.js checks the module graph and the routes. */
5
+ 'use strict';
6
+ const path = require('path');
7
+ const { spawn } = require('child_process');
8
+ const { findBrowser } = require('./lib/find-browser');
9
+ const T = require('./test-run').configure({ name: 'c3-light-load' });
10
+
11
+ const BASE = T.BASE;
12
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
13
+ const until = async (fn, ms = 15000, step = 150) => { const t0 = Date.now(); for (;;) { const v = await fn(); if (v) return v; if (Date.now() - t0 > ms) return v; await sleep(step); } };
14
+ let pass = 0; let fail = 0;
15
+ const ok = (name, cond, extra = '') => { if (cond) pass++; else fail++; console.log(`${cond ? '✓' : '✗'} ${name}${!cond && extra !== '' ? ' — ' + extra : ''}`); };
16
+ let srv = null; let browser = null;
17
+ const finish = async () => {
18
+ if (browser) await browser.close().catch(() => {});
19
+ if (srv) { try { srv.kill('SIGKILL'); } catch { /* gone */ } }
20
+ T.killTmux();
21
+ console.log(`\n${pass}/${pass + fail} c3-light-load assertions passed`);
22
+ process.exit(fail ? 1 : 0);
23
+ };
24
+ process.on('unhandledRejection', (e) => { console.error(e); fail++; finish(); });
25
+
26
+ (async () => {
27
+ const exe = findBrowser();
28
+ if (!exe) { ok('a Chrome or Chromium is on this machine', false); return finish(); }
29
+ const { chromium } = require('playwright-core');
30
+ srv = spawn(process.execPath, [path.join(__dirname, 'server.js')], { cwd: __dirname, stdio: 'ignore', env: T.env({ PMOS_WS_PORT: String(T.PORT) }) });
31
+ await T.waitForServer(BASE);
32
+ browser = await chromium.launch({ executablePath: exe });
33
+ const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
34
+ await ctx.addInitScript(() => { try { localStorage.removeItem('pmos.c3.dock'); } catch { /* none */ } });
35
+ const page = await ctx.newPage();
36
+ const errors = []; const gets = []; const scripts = new Set();
37
+ page.on('pageerror', (e) => errors.push(String((e && e.message) || e)));
38
+ page.on('request', (r) => {
39
+ const u = new URL(r.url());
40
+ if (u.pathname.startsWith('/api/') && r.method() === 'GET') gets.push({ path: u.pathname, q: u.search, at: Date.now() });
41
+ if (r.resourceType() === 'script') scripts.add(u.pathname.split('/').pop());
42
+ });
43
+ await page.goto(BASE + '/');
44
+ ok('the Briefing draws', !!(await until(() => page.evaluate(() => !!document.querySelector('[data-c3pg="pulse"]') && !!document.querySelector('[data-c3pg="deck-host"]')))));
45
+ await sleep(8000); // the deferred badge reads and the dock's first read
46
+ const t0 = gets.length ? gets[0].at : Date.now();
47
+ const first = gets.filter((g) => g.at - t0 < 12000);
48
+ const paths = first.map((g) => g.path);
49
+
50
+ /* R1: the code of the other tabs is not loaded */
51
+ ok('R1: the desktop console loads at most 55 script files on open', scripts.size <= 55, scripts.size + ' files');
52
+ for (const m of ['calendar-views.js', 'c3-initiatives.js', 'c3-reports.js', 'c3-settings.js', 'c3-phone.js', 'c3-doc-sheet.js', 'view-notes.js'])
53
+ ok('R1: ' + m + ' is not loaded on open', !scripts.has(m));
54
+ /* R2: the folded dock reads only the runs */
55
+ ok('R2: the dock is folded', await page.evaluate(() => document.querySelector('[data-c3pg="dock"]').hidden));
56
+ for (const p of ['/api/sessions', '/api/git/worktrees', '/api/sessions/transcript', '/api/scratchpad'])
57
+ ok('R2: the folded dock does not read ' + p, !paths.includes(p));
58
+ ok('R2: the runs are read for the agent button', paths.includes('/api/runs'));
59
+ /* R3, R4: the badges read counts and the needs are read by kind */
60
+ const needs = first.filter((g) => g.path === '/api/needs');
61
+ ok('R4: every needs read names its kinds', needs.length > 0 && needs.every((g) => /[?&]kind=/.test(g.q)), JSON.stringify(needs.map((g) => g.q)));
62
+ const items = first.filter((g) => g.path === '/api/pm-items');
63
+ ok('R3: the Work badge reads the counts alone', items.length === 1 && /counts=1/.test(items[0].q), JSON.stringify(items.map((g) => g.q)));
64
+ /* R5: the pulse reads the gantt rows alone */
65
+ const cal = first.filter((g) => g.path === '/api/calendar-views');
66
+ ok('R5: the pulse reads the quarter once, rows only', cal.length === 1 && /only=gantt/.test(cal[0].q), JSON.stringify(cal.map((g) => g.q)));
67
+ /* R6: no read twice on open (the briefing's two reads have different questions) */
68
+ const seen = new Map(); for (const g of first) { const k = g.path + g.q; seen.set(k, (seen.get(k) || 0) + 1); }
69
+ const twice = [...seen.entries()].filter(([k, n]) => n > 1 && !k.startsWith('/api/runs') && !k.startsWith('/api/sources'));
70
+ ok('R6: no read is made twice on open', twice.length === 0, JSON.stringify(twice));
71
+ ok('R6: the scan and the report cards read once each', ['/api/scan/portfolio', '/api/report-setups', '/api/report-documents'].every((p) => first.filter((g) => g.path === p).length === 1));
72
+ ok('the badges show their numbers', await page.evaluate(() => ['work-count', 'init-count', 'brief-count'].every((k) => !!document.querySelector('[data-c3pg="' + k + '"]'))));
73
+
74
+ /* each place still opens */
75
+ await page.click('[data-c3-tab="cal"]');
76
+ ok('Calendar: its code loads on the first press', !!(await until(() => scripts.has('calendar-views.js'))));
77
+ ok('Calendar: the page draws', !!(await until(() => page.evaluate(() => { const p = document.querySelector('[data-c3-pane="cal"]'); return !!p && !p.hidden && p.children.length > 0 && !p.querySelector('[data-c3pg="tab-error"]'); }))));
78
+ await page.click('[data-c3-tab="init"]');
79
+ ok('Initiatives: its code loads and the page draws', !!(await until(() => scripts.has('c3-initiatives.js') && page.evaluate(() => { const p = document.querySelector('[data-c3-pane="init"]'); return !!p && !p.hidden && p.children.length > 0 && !p.querySelector('[data-c3pg="tab-error"]'); }))));
80
+ await page.click('[data-c3-tab="rep"]');
81
+ ok('Reports: its code loads and the page draws', !!(await until(() => scripts.has('c3-reports.js') && page.evaluate(() => !!document.querySelector('[data-c3-pane="rep"] [data-rp3]')))));
82
+ await page.click('[data-c3pg="settings"]');
83
+ ok('Settings: its code loads and the page draws', !!(await until(() => scripts.has('c3-settings.js') && page.evaluate(() => { const p = document.querySelector('[data-c3-pane="settings"]'); return !!p && !p.hidden && p.children.length > 0 && !p.querySelector('[data-c3pg="tab-error"]'); }))));
84
+ await page.click('[data-c3-tab="brief"]');
85
+ const before = gets.length;
86
+ await page.keyboard.press('Control+j');
87
+ ok('Dock: opening it reads the sessions and the working trees', !!(await until(() => gets.slice(before).some((g) => g.path === '/api/sessions') && gets.slice(before).some((g) => g.path === '/api/git/worktrees'))));
88
+ ok('Dock: the tree draws', !!(await until(() => page.evaluate(() => { const t = document.querySelector('[data-c3pg="dock-tree"]'); return !!t && !document.querySelector('[data-c3pg="dock"]').hidden && !/Reading the repository/.test(t.textContent); }))));
89
+ await page.keyboard.press('Control+j');
90
+ await page.evaluate(() => { if (document.activeElement && document.activeElement.blur) document.activeElement.blur(); });
91
+ await page.keyboard.press('c'); // the key C opens the calendar popup (the header button may be in the quiet layout's overflow)
92
+ ok('Calendar popup: the calendar draws in it', !!(await until(() => page.evaluate(() => { const h = document.querySelector('[data-c3pg="calpop-host"]'); return !!h && !h.querySelector('[data-c3pg="calpop-loading"]') && h.children.length > 0; }))));
93
+ await page.keyboard.press('Escape');
94
+ ok('no page error', errors.length === 0, errors.join(' | '));
95
+ await ctx.close();
96
+
97
+ /* a narrow window gets the phone companion, its code loaded then */
98
+ const pctx = await browser.newContext({ viewport: { width: 1000, height: 800 } });
99
+ const pp = await pctx.newPage(); const perr = [];
100
+ pp.on('pageerror', (e) => perr.push(String((e && e.message) || e)));
101
+ await pp.goto(BASE + '/');
102
+ await until(() => pp.evaluate(() => !!document.querySelector('[data-c3pg="pulse"]')));
103
+ await pp.setViewportSize({ width: 400, height: 800 });
104
+ ok('Phone: narrowing the desktop console mounts the companion', !!(await until(() => pp.evaluate(() => !!document.querySelector('[data-c3ph]')))));
105
+ await pp.setViewportSize({ width: 1000, height: 800 });
106
+ ok('Phone: widening it again removes the companion', !!(await until(() => pp.evaluate(() => !document.querySelector('[data-c3ph]') && !document.getElementById('c3-root').classList.contains('c3-phone-on')))));
107
+ ok('Phone: no page error', perr.length === 0, perr.join(' | '));
108
+ await pctx.close();
109
+ finish();
110
+ })();
@@ -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 (/): the headline cards, Today, Needs you, Portfolio pulse, and what Done, Not yet and Drop do to an item."
5
- timestamp: 2026-10-12
5
+ timestamp: 2026-10-14
6
6
  ---
7
7
 
8
8
  # The Briefing: today's brief
@@ -13,6 +13,12 @@ Review is in the Work page, in its Review lens. Handled, What I could stop and D
13
13
 
14
14
  The Briefing shows only what PMOS already holds. When PMOS holds nothing for a section, the section says so.
15
15
 
16
+ ## What loads when you open the console
17
+
18
+ When you open the console, it loads only what the Briefing needs. The other pages load the first time you open them: Calendar, Initiatives, Reports, Settings and the calendar popup. The first opening of a page can take a moment longer than the next ones. When a page cannot load, it says so. Open it again to try again.
19
+
20
+ The dock is folded when the console opens. While it is folded, PMOS reads only how many agent runs need you, for the badge on the agent button. When you open the dock (Ctrl J), PMOS reads the sessions and the branches. The counts on the Work and Initiatives tabs come after the Briefing has its own data.
21
+
16
22
  ## Headline cards
17
23
 
18
24
  The cards at the top are the signals that pass the relevance bar. Turn a card over to read its case.
@@ -4,6 +4,7 @@
4
4
  * → { ok, view, range, cells, unit, gantt, strategy, events_source, today,
5
5
  * layers, layers_total, layers_note, free, context, context_total, (lib/calendar-layers.js)
6
6
  * link_boards } every board a meeting can be linked to: [{ ws, id, name }]
7
+ * &only=gantt → { ok, view, range, gantt, no_plan, strategy, today } the rows with a plan date or a Jira read, and how many have neither
7
8
  * POST /api/calendar-links → { ok, link } what a meeting is about (lib/calendar-links.js, below)
8
9
  * tidy, tidy_total, tidy_note } (lib/calendar-tidy.js: suggestions in words, no write)
9
10
  *
@@ -39,7 +40,7 @@ function handle(query, deps = {}) {
39
40
  const inis = facts.inis || [];
40
41
  const range = model.rangeFor(view, date);
41
42
  let events = []; let source = 'none';
42
- if (typeof deps.readEvents === 'function') { events = deps.readEvents(range) || []; source = 'calendar-memory'; }
43
+ if (typeof deps.readEvents === 'function' && !(query && query.only === 'gantt')) { events = deps.readEvents(range) || []; source = 'calendar-memory'; }
43
44
  const krs = typeof deps.readKrs === 'function' ? deps.readKrs() : [];
44
45
  const b = model.bucketEvents(events, view, date);
45
46
  const nowDay = deps.today || today();
@@ -50,6 +51,12 @@ function handle(query, deps = {}) {
50
51
  if (jira && typeof jira.get === 'function') for (const g of gantt) { const p = jira.get(g.id); if (p) g.jira = p; }
51
52
  }
52
53
  const strategy = view === 'quarter' || view === 'year' ? model.strategyLens(inis, krs, nowDay) : null;
54
+ // only=gantt: for the Briefing's Portfolio pulse (c3-plan.js readQuarter). No events, layers or tidy, and only the rows a reader can
55
+ // draw: a plan date or a Jira read. The rest are counted in no_plan (a large repository has hundreds of them).
56
+ if (query && query.only === 'gantt') {
57
+ const rows = gantt.filter((g) => g.status !== 'no plan' || g.jira);
58
+ return { status: 200, body: { ok: true, view, range, gantt: rows, no_plan: gantt.length - rows.length, strategy, today: nowDay } };
59
+ }
53
60
  const dated = typeof deps.readDated === 'function' ? (deps.readDated(range) || {}) : {
54
61
  unavailable: { plan_items: 'no reader is wired', releases: 'no reader is wired', reports: 'no reader is wired', own: 'no reader is wired' } };
55
62
  // the initiatives come from the facts this handler already holds, so every caller gets their promised dates
@@ -228,7 +235,8 @@ module.exports = {
228
235
  let facts;
229
236
  try { facts = await require('./facts-async.js').readFactsAsync(root); }
230
237
  catch (e) { ctx.json(res, { ok: false, error: 'the calendar could not read the repository: ' + (e && e.message ? e.message : String(e)) }, 500); return true; }
231
- const out = handle({ view: url.searchParams.get('view') || undefined, date: url.searchParams.get('date') || undefined }, {
238
+ const only = url.searchParams.get('only') || undefined;
239
+ const out = handle({ view: url.searchParams.get('view') || undefined, date: url.searchParams.get('date') || undefined, only }, {
232
240
  readFacts: () => facts,
233
241
  readEvents: () => require('./calendar-events.js').readAll(ctx.runtimeDir),
234
242
  readLinks: () => require('./calendar-links.js').read(ctx.runtimeDir),
@@ -7,6 +7,7 @@
7
7
  * review, review_days: 7 open items untouched 7 days, most stale first
8
8
  * focus: { suggestions, accepted, dismissed } at most 3 suggestions: { key, title, why, item_ids, job }; dismissed = the keys marked not today
9
9
  * counts: { inbox, today, upcoming, waiting, someday, review } }
10
+ * GET /api/pm-items?counts=1 → { ok, today, counts } the counts alone (the console's Work tab badge)
10
11
  * POST /api/pm-items/add { line } → { ok, item } one line: the parser (lib/pm-items.js) reads the parts
11
12
  * POST /api/pm-items/edit { id, fields }→ { ok, item } fields: title, due, priority, waiting_on, job,
12
13
  * defer, flagged, resolves, kind, person, evidence, outcome
@@ -343,6 +344,7 @@ module.exports = {
343
344
  if (p === '/api/pm-items' && req.method === 'GET') {
344
345
  const c = prepare(ctx);
345
346
  const v = view(c.itemLines, c.jobLines, c.now, c.stepLines, c.focusLines, proofSources(ctx, c.itemLines), await assignedAsync(ctx, c.now));
347
+ if (url.searchParams.get('counts') === '1') { ctx.json(res, { ok: v.ok, today: v.today, counts: v.counts }); return true; }
346
348
  v.resolves_titles = resolvesTitles(v.items, ctx, url);
347
349
  // an item the PM added: the agent can draft a first version of it (c3-work-agent amendment 11)
348
350
  const runs = assigned.recentRuns(ctx.readDayRun, c.now.getTime());
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.210.0",
3
+ "version": "0.211.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pmos-workspace",
9
- "version": "0.210.0",
9
+ "version": "0.211.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmos-workspace",
3
- "version": "0.210.0",
3
+ "version": "0.211.0",
4
4
  "private": true,
5
5
  "description": "PMOS Terminal Workspace — local companion hosting real PTYs beside the library and gates (D62)",
6
6
  "license": "MIT",
@@ -76,11 +76,11 @@ export function pulse(S) {
76
76
  el('div', { class: 'pulsehead' },
77
77
  el('h3', null, c3ic('init', 'sm'), 'Portfolio pulse'), attn), body, foot);
78
78
  const note = (t, ic) => el('div', { class: 'fold' }, c3ic(ic || 'moon', 'sm'), t);
79
- const draw = (rows, err) => {
79
+ const draw = (rows, err, unplanned) => { // unplanned: rows the server left out because they have no plan (only=gantt)
80
80
  if (err) { body.replaceChildren(el('p', { class: 'dim mono c3s2', role: 'status' }, 'The initiatives could not be read: ' + err)); return; }
81
- if (!rows.length) { body.replaceChildren(note('No initiative has a promised date yet, so there is nothing to compare with the forecast.')); return; }
81
+ if (!rows.length && !unplanned) { body.replaceChildren(note('No initiative has a promised date yet, so there is nothing to compare with the forecast.')); return; }
82
82
  const shown = (g) => g.pulse || g.status !== 'no plan' || !!jiraOf(g); // a row without a plan shows when its Jira epic has progress
83
- const noPlan = rows.filter((g) => !shown(g)).length;
83
+ const noPlan = rows.filter((g) => !shown(g)).length + (Number(unplanned) || 0);
84
84
  rows = rows.filter(shown);
85
85
  if (!rows.length) { body.replaceChildren(note('No initiative has a promised date or a Jira epic read yet, so there is nothing to show.'), ...(noPlan ? [el('p', { class: 'dim mono c3s2' }, noPlan + ' initiatives have no plan date.')] : [])); return; }
86
86
  const pr = rows.map(pulseRow);
@@ -100,7 +100,7 @@ export function pulse(S) {
100
100
  body.replaceChildren(...[bar, el('div', null, ...pr.slice(0, 8).map(rowEl)), pr.length > 8 ? el('div', { class: 'dim mono c3s2' }, '+' + (pr.length - 8) + ' more in the Calendar tab') : null, noPlan ? el('div', { class: 'dim mono c3s2' }, noPlan + ' more have no plan date.') : null].filter(Boolean));
101
101
  };
102
102
  readQuarter().then((j) => {
103
- if (!j || j.ok === false) draw([], (j && j.error) || 'unreadable'); else { S.today = j.today || ''; draw(j.gantt || []); }
103
+ if (!j || j.ok === false) draw([], (j && j.error) || 'unreadable'); else { S.today = j.today || ''; draw(j.gantt || [], null, j.no_plan); }
104
104
  }).catch((e) => draw([], String((e && e.message) || e)));
105
105
  return box;
106
106
  }
@@ -14,7 +14,9 @@
14
14
  */
15
15
  import { el } from './dom.js';
16
16
  import { ensureC3Css, c3ic, c3ib, c3Phone } from './pm-work-view.js';
17
- import { mountCalendarViews, disposeCalendarViews } from './calendar-views.js';
17
+ /* The calendar's code (calendar-views.js and the notes it draws) loads on the first opening, not with the page (c3-desktop-light-load). */
18
+ let views = null;
19
+ const loadViews = () => views || (views = import('./calendar-views.js').catch((e) => { views = null; throw e; }));
18
20
 
19
21
  const ZOOMS = ['day', 'week', 'month', 'quarter', 'year'];
20
22
  const KEY = 'pmos.c3.calpop.zoom';
@@ -45,7 +47,7 @@ export function closeCalendarPopup(back = true) {
45
47
  document.removeEventListener('keydown', c.onKey, true);
46
48
  if (c.obs) c.obs.disconnect();
47
49
  if (c.stopPhone) c.stopPhone();
48
- try { disposeCalendarViews(c.host); } catch { /* already gone */ }
50
+ if (views) views.then((m) => m.disposeCalendarViews(c.host)).catch(() => { /* already gone */ });
49
51
  c.wrap.remove();
50
52
  if (back && c.opener && c.opener.isConnected && c.opener.focus) { try { c.opener.focus({ preventScroll: true }); } catch { /* not focusable any more */ } }
51
53
  }
@@ -73,7 +75,13 @@ export function openCalendarPopup(opener) {
73
75
  document.addEventListener('keydown', onKey, true);
74
76
  document.body.appendChild(wrap);
75
77
  cur = { wrap, host, opener: from, onKey, stopPhone, obs: null };
76
- mountCalendarViews(host, { view: zoom, page: true, overlay: true }); // the same page grid as the Calendar tab, 36px an hour, no deck
78
+ host.append(el('p', { class: 'dim mono c3s2', role: 'status', 'data-c3pg': 'calpop-loading' }, 'Opening the calendar'));
79
+ const mine = cur;
80
+ loadViews().then((m) => {
81
+ if (cur !== mine) return; // closed before the code arrived
82
+ host.replaceChildren();
83
+ m.mountCalendarViews(host, { view: zoom, page: true, overlay: true }); // the same page grid as the Calendar tab, 36px an hour, no deck
84
+ }, (e) => { if (cur === mine) host.replaceChildren(el('p', { class: 'dim mono c3s2', role: 'status' }, 'The calendar could not load: ' + String((e && e.message) || e))); });
77
85
  /* remember the zoom as it changes, so a reload of the page also keeps it */
78
86
  cur.obs = new MutationObserver(() => { const z = readZoom(host); if (z) { zoom = z; store.set(z); } });
79
87
  cur.obs.observe(host, { subtree: true, childList: true, attributes: true, attributeFilter: ['aria-pressed'] });
@@ -148,8 +148,17 @@ export function createDock(opts) {
148
148
  }
149
149
 
150
150
 
151
+ /* A folded dock reads only the runs, for the needs-you count on the header's agent button; the sessions, the working trees and the
152
+ * selected session's output are read when the dock is open (c3-desktop-light-load). Opening it calls refresh() again. */
151
153
  async function refresh() {
152
154
  const seq = ++S.seq; const ws = S.ws;
155
+ if (side.hidden) {
156
+ const rn = await source().getRuns().catch(() => null);
157
+ if (seq !== S.seq) return;
158
+ S.need = needsYou(rn); agentBdg.textContent = String(S.need); agentBdg.hidden = !S.need;
159
+ if (o.onCount) o.onCount(null, S.need);
160
+ return;
161
+ }
153
162
  const [ss, wt, rn] = await Promise.all([pmGet('/api/sessions'), ws ? pmGet('/api/git/worktrees?ws=' + encodeURIComponent(ws)) : Promise.resolve(null), source().getRuns().catch(() => null)]);
154
163
  if (seq !== S.seq) return;
155
164
  S.need = needsYou(rn); agentBdg.textContent = String(S.need); agentBdg.hidden = !S.need;
@@ -279,7 +279,7 @@ export function agentSetting(state, { onPick } = {}) {
279
279
  /** The agent's rows of GET /api/needs: "Agent has 3 drafts on <title>", "<title> is waiting on you", and "2 portfolio drafts to review" (core
280
280
  * documents, kind core-doc-drafts: it opens that scope's Docs view). [] when the read fails. */
281
281
  export async function loadDriverNeeds() {
282
- const r = await pmGet('/api/needs');
282
+ const r = await pmGet('/api/needs?kind=driver-drafts,driver-blocked,core-doc-drafts'); // these three kinds only, not the whole queue
283
283
  return r && Array.isArray(r.needs) ? r.needs.filter((n) => n && (n.kind === 'driver-drafts' || n.kind === 'driver-blocked' || n.kind === 'core-doc-drafts')) : [];
284
284
  }
285
285
  /** One li.nrow for a driver row (the mockup's two rows). `go(row)` opens the initiative or the Work page. */
@@ -0,0 +1,27 @@
1
+ /* pm/c3-look.js — the two parts of Settings the console needs before Settings is opened (c3-desktop-light-load): the gear icon of the
2
+ * header and applyLook, which sets the time-of-day tint and the daylight strip from the saved settings. They live here so the
3
+ * shell can start without loading c3-settings.js; c3-settings.js re-exports both. */
4
+
5
+ /* c3-settings.css holds the tint and strip rules. The same link attribute as c3-settings.js, so the sheet is added once. */
6
+ function ensureCss() {
7
+ if (typeof document === 'undefined' || document.querySelector('link[data-c3-settings]')) return;
8
+ const l = document.createElement('link'); l.rel = 'stylesheet'; l.href = new URL('./c3-settings.css', import.meta.url).href; l.setAttribute('data-c3-settings', ''); document.head.appendChild(l);
9
+ }
10
+
11
+ /** A gear, drawn here: the console's sprite has none. */
12
+ export function gearIcon(cls) {
13
+ const NS = 'http://www.w3.org/2000/svg';
14
+ const s = document.createElementNS(NS, 'svg');
15
+ s.setAttribute('class', 'ic' + (cls ? ' ' + cls : '')); s.setAttribute('aria-hidden', 'true'); s.setAttribute('viewBox', '0 0 24 24');
16
+ s.innerHTML = '<circle cx="12" cy="12" r="3"/><path d="M19 12a7 7 0 0 0-.1-1.2l2-1.6-2-3.4-2.4 1a7 7 0 0 0-2-1.2L14 3h-4l-.5 2.6a7 7 0 0 0-2 1.2l-2.4-1-2 3.4 2 1.6a7 7 0 0 0 0 2.4l-2 1.6 2 3.4 2.4-1a7 7 0 0 0 2 1.2L10 21h4l.5-2.6a7 7 0 0 0 2-1.2l2.4 1 2-3.4-2-1.6c.1-.4.1-.8.1-1.2"/>';
17
+ return s;
18
+ }
19
+
20
+ /** Apply the settings the console itself draws: the start tab is read by the shell; tint and the daylight strip are attributes on the root. */
21
+ export function applyLook(root, settings) {
22
+ if (!root || !settings) return;
23
+ ensureCss();
24
+ const v = (k) => { const s = settings.find((x) => x.key === k); return s ? s.value : undefined; };
25
+ root.setAttribute('data-c3-tint', v('time.tint') === false ? 'off' : 'on');
26
+ root.setAttribute('data-c3-strip', v('time.strip') === false ? 'off' : 'on');
27
+ }
@@ -83,7 +83,7 @@ let quarter = null;
83
83
  export function readQuarter() {
84
84
  if (!quarter) {
85
85
  const d = new Date(); const p2 = (n) => String(n).padStart(2, '0');
86
- quarter = pmGet('/api/calendar-views?view=quarter&date=' + d.getFullYear() + '-' + p2(d.getMonth() + 1) + '-' + p2(d.getDate()));
86
+ quarter = pmGet('/api/calendar-views?view=quarter&only=gantt&date=' + d.getFullYear() + '-' + p2(d.getMonth() + 1) + '-' + p2(d.getDate())); // the rows alone: no events or layers
87
87
  }
88
88
  return quarter;
89
89
  }
@@ -99,7 +99,8 @@ export function portfolioCard() {
99
99
  const dlab = (v) => { const m = mock && /^\d{4}-(\d{2})-(\d{2})$/.exec(v || ''); return m ? Number(m[2]) + ' ' + MON[Number(m[1]) - 1] : v || ''; }; // the mockup's dlab: 24 Oct
100
100
  readQuarter().then((j) => {
101
101
  if (!j || j.ok === false) { body.replaceChildren(el('div', { class: 'fold w' }, c3ic('alert', 'sm'), 'The initiatives could not be read.')); return; }
102
- const all = j.gantt || []; const rows = all.filter((g) => g.status !== 'no plan'); mock = rows.some((g) => g.pulse);
102
+ // only=gantt leaves the rows with no plan out and counts them in no_plan; they still count in the line under the table
103
+ const all = (j.gantt || []).concat(Array.from({ length: Number(j.no_plan) || 0 }, () => ({ status: 'no plan' }))); const rows = all.filter((g) => g.status !== 'no plan'); mock = rows.some((g) => g.pulse);
103
104
  if (!rows.length) { body.replaceChildren(el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'No initiative has a promised date yet.'), all.length ? el('p', { class: 'dim mono', style: 'font-size:var(--fs-2)' }, all.length + ' initiatives have no plan date.') : null); return; }
104
105
  body.replaceChildren(el('table', { class: 'tb', style: (mock ? 'width:auto;' : 'width:100%;') + 'border-collapse:collapse;font-size:var(--fs-2)' },
105
106
  el('thead', null, el('tr', null, el('th', { style: 'text-align:left' }), el('th', { class: 'mono', style: 'font-size:var(--fs-1);padding:0 8px' }, 'PROMISED'), el('th', { class: 'mono', style: 'font-size:var(--fs-1);padding:0 8px' }, 'FORECAST'), el('th'))),
@@ -193,7 +193,8 @@ export function reportFoundSection(S) {
193
193
  };
194
194
  onReports(fill);
195
195
  const go = () => loadReports((S && S.ws) || RS.ws).then(fill, () => {});
196
- onBriefing(() => { go(); });
196
+ let first = true; // the first briefing comes with the page, while this card's own first read below is still out; a later one reads again
197
+ onBriefing(() => { if (first) { first = false; return; } go(); });
197
198
  go();
198
199
  return host;
199
200
  }
@@ -18,7 +18,9 @@ import { openSheet } from './c3-sheet.js';
18
18
  import { toast } from './c3-toast.js';
19
19
  import { TYPES, TYPE_KEYS, KINDS, typeName, getPortfolio, getBoards, getDismissed, postWatch, postSetting, postNow, postStop, scanNowWords, scanChanged, onScanChange } from './c3-scan-api.js';
20
20
  import { openScanDoc } from './c3-scan-next.js';
21
- import { pastLabel, nextLabel, scheduleLine, ensureScanCss } from './c3-scan-words.js';
21
+ import { pastLabel, nextLabel, scheduleLine, ensureScanCss, latestReports } from './c3-scan-words.js';
22
+
23
+ export { latestReports };
22
24
 
23
25
  const KIND_ICON = { product: 'org', domain: 'report', approach: 'bolt' };
24
26
  const kindTag = (k) => el('span', { class: 'tag' + (k === 'product' ? ' acc' : '') }, k);
@@ -29,12 +31,6 @@ const waitingOf = (d, report) => (report ? ((d.reports.find((r) => r.id === repo
29
31
  const blocked = (d) => !!d && !d.manual.ok && d.manual.why !== 'running';
30
32
  /** "2 of 3 manual scans left today", or the reason Scan now is off, in the server's words. */
31
33
  const leftLine = (d) => (d.manual.ok || d.manual.why === 'running' ? d.manual.left + ' of ' + d.manual.limit + ' manual scans left today.' : scanNowWords({ why: d.manual.why, error: d.manual.error, limit: d.manual.limit }));
32
- /** The reports of the latest scan (the ones that are not a research follow-up), newest first as the server lists them. */
33
- export function latestReports(d) {
34
- const top = d.reports.filter((r) => !r.parent && r.written);
35
- const scan = (d.last && d.last.scan) || (top[0] && top[0].scan) || '';
36
- return scan ? top.filter((r) => r.scan === scan) : top.slice(0, 4);
37
- }
38
34
  const SCHED_NOTE = scheduleLine;
39
35
 
40
36
  /** A check box with a title and a line of help, drawn by the field primitive. attrs go on the input. */
@@ -32,3 +32,10 @@ export function nextLabel(iso, now = new Date()) {
32
32
  }
33
33
  export const scheduleLine = (c) => (c === 'daily' ? 'Every working day, 09:00' : c === 'weekly' ? 'Every Monday, 09:00' : 'Off. No scan runs by itself');
34
34
  export const sameDayAs = sameDay;
35
+
36
+ /** The reports of the latest scan (the ones that are not a research follow-up), newest first as the server lists them. */
37
+ export function latestReports(d) {
38
+ const top = d.reports.filter((r) => !r.parent && r.written);
39
+ const scan = (d.last && d.last.scan) || (top[0] && top[0].scan) || '';
40
+ return scan ? top.filter((r) => r.scan === scan) : top.slice(0, 4);
41
+ }
@@ -11,10 +11,11 @@ import { el } from './dom.js';
11
11
  import { icon, pbtn, lk, chip, plural, sect, put, chgl, ago } from './c3-ini-ui.js';
12
12
  import { onBriefing } from './shell-ticker.js';
13
13
  import { getPortfolio, getBoards, onScanChange } from './c3-scan-api.js';
14
- import { openScanDoc } from './c3-scan-next.js';
15
- import { openScanPicker, latestReports } from './c3-scan-setup.js';
16
- import { ensureScanCss, pastLabel, sameDayAs } from './c3-scan-words.js';
14
+ import { ensureScanCss, pastLabel, sameDayAs, latestReports } from './c3-scan-words.js';
17
15
  export { ensureScanCss, pastLabel };
16
+ /* The scan's sheets (a report, the picker) load on a press, not with the Briefing (c3-desktop-light-load). */
17
+ const openScanDoc = (...a) => import('./c3-scan-next.js').then((m) => m.openScanDoc(...a));
18
+ const openScanPicker = (...a) => import('./c3-scan-setup.js').then((m) => m.openScanPicker(...a));
18
19
 
19
20
  /* ── Triage: an ask the scan found ── */
20
21
  export const isScanAsk = (a) => !!(a && a.source === 'scan' && a.found);
@@ -84,7 +85,8 @@ export function scanSection(S) {
84
85
  if (g === sig) return; sig = g;
85
86
  put(host, cardOf(d, { ws: S.ws, boardCount: boards.length, showReports: () => import('./c3-brief.js').then((m) => { if (m.hooks && m.hooks.go) m.hooks.go({ action: 'gorep' }); }) }));
86
87
  }, () => {});
87
- onBriefing(() => { fill(); });
88
+ let first = true; // the first briefing comes with the page, while this card's own first read below is still out; a later one reads again
89
+ onBriefing(() => { if (first) { first = false; return; } fill(); });
88
90
  const off = onScanChange(() => { if (!host.isConnected) { off(); return; } fill(); });
89
91
  fill();
90
92
  return host;
@@ -27,6 +27,9 @@ import { sessionDefaults, setSessionDefaults } from './dock.js';
27
27
  import { toast } from './c3-toast.js';
28
28
  import { dayPartOf } from './c3-daypart.js';
29
29
  import { finderBlock } from './c3-report-store.js';
30
+ import { gearIcon, applyLook } from './c3-look.js';
31
+
32
+ export { gearIcon, applyLook };
30
33
 
31
34
  const cx = (...c) => c.filter(Boolean).join(' ');
32
35
 
@@ -123,15 +126,6 @@ export function rowsMatching(q) {
123
126
  const STORE_SECTION = { ws: 'workspaces', board: 'boards', conn: 'connectors', rec: 'recording', time: 'time', share: 'sharing', sess: 'sessions', look: 'appearance', upd: 'updates' };
124
127
  function sectionOfStore(k) { return STORE_SECTION[k.split('.')[0]] || 'workspaces'; }
125
128
 
126
- /** A gear, drawn here: the console's sprite has none. */
127
- export function gearIcon(cls) {
128
- const NS = 'http://www.w3.org/2000/svg';
129
- const s = document.createElementNS(NS, 'svg');
130
- s.setAttribute('class', 'ic' + (cls ? ' ' + cls : '')); s.setAttribute('aria-hidden', 'true'); s.setAttribute('viewBox', '0 0 24 24');
131
- s.innerHTML = '<circle cx="12" cy="12" r="3"/><path d="M19 12a7 7 0 0 0-.1-1.2l2-1.6-2-3.4-2.4 1a7 7 0 0 0-2-1.2L14 3h-4l-.5 2.6a7 7 0 0 0-2 1.2l-2.4-1-2 3.4 2 1.6a7 7 0 0 0 0 2.4l-2 1.6 2 3.4 2.4-1a7 7 0 0 0 2 1.2L10 21h4l.5-2.6a7 7 0 0 0 2-1.2l2.4 1 2-3.4-2-1.6c.1-.4.1-.8.1-1.2"/>';
132
- return s;
133
- }
134
-
135
129
  function ensureCss() {
136
130
  if (typeof document === 'undefined' || document.querySelector('link[data-c3-settings]')) return;
137
131
  const l = document.createElement('link'); l.rel = 'stylesheet'; l.href = new URL('./c3-settings.css', import.meta.url).href; l.setAttribute('data-c3-settings', ''); document.head.appendChild(l);
@@ -149,15 +143,6 @@ const ZONES = ['Asia/Jakarta', 'Asia/Makassar', 'Asia/Jayapura', 'Asia/Singapore
149
143
  const AS_SELECT = new Set(['conn.cal.window', 'conn.cal.refresh', 'conn.jira.refresh', 'conn.wispr.wait']);
150
144
  const qs = (o) => Object.entries(o).filter(([, v]) => v != null && v !== '').map(([k, v]) => k + '=' + encodeURIComponent(v)).join('&');
151
145
 
152
- /** Apply the settings the console itself draws: the start tab is read by the shell; tint and the daylight strip are attributes on the root. */
153
- export function applyLook(root, settings) {
154
- if (!root || !settings) return;
155
- ensureCss();
156
- const v = (k) => { const s = settings.find((x) => x.key === k); return s ? s.value : undefined; };
157
- root.setAttribute('data-c3-tint', v('time.tint') === false ? 'off' : 'on');
158
- root.setAttribute('data-c3-strip', v('time.strip') === false ? 'off' : 'on');
159
- }
160
-
161
146
  export function mountSettings(host, ctx = {}) {
162
147
  ensureCss();
163
148
  const M = {
@@ -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, dueCard, crossLinks, newInitLine } from './c3-plan.js';
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 { mountInitiatives } from './c3-initiatives.js';
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: {}, init: 'lifecycle', initRows: null, dockOpen: true, wide: true };
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(store);
140
+ const want = phoneWanted();
158
141
  if (want && !S.phone) {
159
142
  S.root.classList.add('c3-phone-on');
160
- S.phone = mountPhone(S.root, { ws: S.ws, demo: S.demo });
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 && S.settings) S.settings.show(d.section);
323
- if (d.tab === 'rep' && d.view) showReportsView(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) showReportsView(t.view); return; }
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: () => { showTab('init', true); const b = S.initSeg && S.initSeg.querySelector('[data-c3-init="documents"]'); if (b) b.click(); } });
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
- const BUILD = { brief: briefTab, cal: (p) => mountCalendarViews(p, { view: 'week', page: true, scope: sc('cal'), deck: planNode('cal', { bare: true }) }), work: (p) => { mountPmWork(p, { go: (t) => showTab(t, true), ws: S.ws }); }, init: (p) => mountInitiatives(p, { ws: S.ws, workspaces: S.workspaces, demo: DEMO, showTab }), rep: repTab };
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) { 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) showReportsView(v); }, section: q.get('section') || '', root: S.root }; S.settings = mountSettings(S.panes.settings, S.settingsCtx); }
496
+ if (!S.settings) settingsReady();
547
497
  else S.settings.refresh();
548
- } else if (!S.mounted[id]) { S.mounted[id] = true; BUILD[id](S.panes[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 n = (j.lists.triage || []).length + (S.demo ? 0 : (j.lists.today || []).length); // the mockup's Work badge counts what is to triage
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
- workCount(); briefCount(); askCount(); S.dock.start();
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
 
@@ -6971,8 +6971,12 @@ async function handleRequest(req, res) {
6971
6971
  ? { count: strict.empty ? 0 : strict.data.items.length, error: null }
6972
6972
  : { count: null, error: strict.error } });
6973
6973
  }
6974
+ // ?kind=a,b keeps only those kinds (the console's Initiatives badge counts triage; the Briefing's agent rows read three kinds):
6975
+ // the full queue is large and most readers want a few rows of it (c3-desktop-light-load).
6976
+ const kinds = String(url.searchParams.get('kind') || '').split(',').map((k) => k.trim()).filter(Boolean);
6977
+ const queue = strict.ok ? strict.all : readNeeds(w);
6974
6978
  return json(res, {
6975
- needs: strict.ok ? strict.all : readNeeds(w),
6979
+ needs: kinds.length && Array.isArray(queue) ? queue.filter((n) => n && kinds.includes(n.kind)) : queue,
6976
6980
  state: strict.ok ? { ok: true } : { ok: false, error: strict.error },
6977
6981
  by_ws: needsByWorkspace(),
6978
6982
  });
@@ -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
+ }