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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* routes-refresh.js — Settings › Data refresh (initiative data-refresh-settings, PRD R7, R8).
|
|
3
|
+
*
|
|
4
|
+
* GET /api/refresh?ws=<id> → { ok, ws, rules, quiet, window, rows: [row] } lib/refresh.js rowsOf
|
|
5
|
+
* POST /api/refresh/source { id, on?, schedule? } → { ok, source } on or off, and a skill source's schedule
|
|
6
|
+
* POST /api/refresh/rules { catchup?, retry?, tell?, max?, keep? } → { ok, rules }
|
|
7
|
+
* POST /api/refresh/run { id, ws?, with_inputs? } → { ok, source, started?|waiting?, inputs } Refresh now
|
|
8
|
+
* POST /api/refresh/run-all { ws? } → { ok, started: [id], failed: [{ id, error }] } every source that is on
|
|
9
|
+
* GET /api/refresh/log?source=&result=&limit= → { ok, runs: [run] } newest first
|
|
10
|
+
* GET /api/refresh/log.csv → text/csv, every run kept
|
|
11
|
+
*
|
|
12
|
+
* Every POST passes pm-guard requirePagePress: only the PM's press on the page changes what refreshes or starts a refresh.
|
|
13
|
+
* A wrong value is refused with the reason, and nothing changes. `ctx.refreshRun` is lib/refresh-run.js, bound by server.js.
|
|
14
|
+
*/
|
|
15
|
+
const guard = require('./pm-guard.js');
|
|
16
|
+
const R = require('./refresh.js');
|
|
17
|
+
|
|
18
|
+
const RESULTS = ['done', 'failed', 'skipped', 'interrupted', 'running'];
|
|
19
|
+
|
|
20
|
+
async function handle(req, res, url, ctx) {
|
|
21
|
+
const p = url.pathname;
|
|
22
|
+
const run = ctx.refreshRun;
|
|
23
|
+
if (!run) { ctx.json(res, { ok: false, error: 'the refresh engine is not running' }, 503); return true; }
|
|
24
|
+
|
|
25
|
+
if (p === '/api/refresh' || p === '/api/refresh/log' || p === '/api/refresh/log.csv') {
|
|
26
|
+
if (req.method !== 'GET') { ctx.json(res, { ok: false, error: 'GET only' }, 405); return true; }
|
|
27
|
+
if (p === '/api/refresh') {
|
|
28
|
+
const id = url.searchParams.get('ws') || '';
|
|
29
|
+
const w = ctx.getWs(id);
|
|
30
|
+
if (id && (!w || w.id !== id)) { ctx.json(res, { ok: false, error: 'no such workspace' }, 404); return true; }
|
|
31
|
+
ctx.json(res, { ok: true, ws: w ? w.id : null, ...run.rows(w ? w.id : null) });
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
let runs = run.log().all.slice().reverse().map((r) => ({ ...r, result: r.result || (r.end_at ? 'done' : 'running') }));
|
|
35
|
+
const source = url.searchParams.get('source'); const result = url.searchParams.get('result');
|
|
36
|
+
if (source && !R.BY_ID.has(source)) { ctx.json(res, { ok: false, error: 'source must be one of ' + R.SOURCES.map((x) => x.id).join(', ') }, 400); return true; }
|
|
37
|
+
if (result && !RESULTS.includes(result)) { ctx.json(res, { ok: false, error: 'result must be one of ' + RESULTS.join(', ') }, 400); return true; }
|
|
38
|
+
if (source) runs = runs.filter((r) => r.source === source);
|
|
39
|
+
if (result) runs = runs.filter((r) => r.result === result);
|
|
40
|
+
if (p === '/api/refresh/log.csv') {
|
|
41
|
+
res.writeHead(200, { 'content-type': 'text/csv; charset=utf-8', 'content-disposition': 'attachment; filename="pmos-refresh-log.csv"' });
|
|
42
|
+
res.end(R.toCsv(runs.slice().reverse()));
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
const limit = Math.max(1, Math.min(1000, Number(url.searchParams.get('limit')) || 200));
|
|
46
|
+
ctx.json(res, { ok: true, total: runs.length, runs: runs.slice(0, limit) });
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!['/api/refresh/source', '/api/refresh/rules', '/api/refresh/run', '/api/refresh/run-all'].includes(p)) return false;
|
|
51
|
+
if (req.method !== 'POST') { ctx.json(res, { ok: false, error: 'POST only' }, 405); return true; }
|
|
52
|
+
const got = await ctx.readJson(req);
|
|
53
|
+
if (!got.ok) { ctx.json(res, { ok: false, error: 'the body is not JSON' }, 400); return true; }
|
|
54
|
+
const body = got.body || {};
|
|
55
|
+
const g = guard.requirePagePress(req, body);
|
|
56
|
+
if (!g.ok) { ctx.json(res, { ok: false, error: g.error }, g.status); return true; }
|
|
57
|
+
let ws = null;
|
|
58
|
+
if (body.ws != null) {
|
|
59
|
+
const w = typeof body.ws === 'string' ? ctx.getWs(body.ws) : null;
|
|
60
|
+
if (!w || w.id !== body.ws) { ctx.json(res, { ok: false, error: 'no such workspace' }, 404); return true; }
|
|
61
|
+
ws = w.id;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (p === '/api/refresh/source') {
|
|
65
|
+
const r = run.setSource(String(body.id || ''), { on: body.on, schedule: body.schedule });
|
|
66
|
+
ctx.json(res, r, r.ok ? 200 : 400);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
if (p === '/api/refresh/rules') {
|
|
70
|
+
const patch = { ...body }; delete patch.ws; delete patch.nonce; delete patch.press;
|
|
71
|
+
for (const k of Object.keys(patch)) if (k.startsWith('_')) delete patch[k];
|
|
72
|
+
const r = run.setRules(patch);
|
|
73
|
+
ctx.json(res, r, r.ok ? 200 : 400);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
if (p === '/api/refresh/run') {
|
|
77
|
+
const r = run.runNow(String(body.id || ''), { ws, withInputs: body.with_inputs === true });
|
|
78
|
+
ctx.json(res, r, r.ok ? 200 : r.status || 400);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
// run-all: every source that is on, inputs before the sources that use them (the order of the registry already is).
|
|
82
|
+
const set = run.settings();
|
|
83
|
+
const startedIds = []; const failed = [];
|
|
84
|
+
for (const src of R.SOURCES) {
|
|
85
|
+
if (!set.sources[src.id].on || src.id === 'sync') continue;
|
|
86
|
+
const r = run.runNow(src.id, { ws });
|
|
87
|
+
if (r.ok) startedIds.push(src.id); else failed.push({ id: src.id, error: r.error });
|
|
88
|
+
}
|
|
89
|
+
ctx.json(res, { ok: true, started: startedIds, failed });
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { prefixes: ['/api/refresh'], handle };
|
|
@@ -37,9 +37,7 @@ const DEFS = Object.freeze({
|
|
|
37
37
|
'board.rules': one('boards', 'ws', ['warn', 'off'], 'warn'),
|
|
38
38
|
'board.templates': { section: 'boards', scope: 'computer', type: 'templates', def: TEMPLATES },
|
|
39
39
|
'conn.cal.window': one('connectors', 'computer', ['7-14', '14-30', '30-60'], '14-30'),
|
|
40
|
-
'conn.cal.refresh': one('connectors', 'computer', ['open', 'hour', 'manual'], 'open'),
|
|
41
40
|
'conn.cal.private': one('connectors', 'computer', ['busy', 'hide', 'title'], 'busy'),
|
|
42
|
-
'conn.jira.refresh': one('connectors', 'ws', ['open', '15', 'manual'], '15'),
|
|
43
41
|
'conn.wispr.wait': one('connectors', 'computer', ['2', '5', '10'], '5'),
|
|
44
42
|
'conn.sender': one('connectors', 'ws', ['none', 'slack', 'gmail'], 'none'),
|
|
45
43
|
'rec.sources': one('recording', 'computer', ['both', 'mic', 'meeting'], 'both'),
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.212.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.212.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'))),
|
|
@@ -77,7 +77,12 @@
|
|
|
77
77
|
.c3 .c3top{position:absolute;top:0;right:0;left:auto;align-items:center !important;gap:10px;z-index:3;padding:0 !important;margin:0 !important;min-height:44px;background:transparent}
|
|
78
78
|
.c3 .c3top > div:first-child{display:flex;align-items:center;gap:8px;order:1}
|
|
79
79
|
.c3 .c3top .sub{font-size:var(--fs-1);margin:0}
|
|
80
|
-
.c3 .c3top .fr-fresh.qt-open{position:absolute;top:44px;right:0;background:var(--card);border:1px solid var(--border);border-radius:8px;padding:8px 10px;
|
|
80
|
+
.c3 .c3top .fr-fresh.qt-open{position:absolute;top:44px;right:0;background:var(--card);border:1px solid var(--border);border-radius:8px;padding:8px 10px;box-shadow:0 6px 24px rgb(0 0 0 / .08)}
|
|
81
|
+
/* The open line is a list: one source per row, then the refresh icon with its note. Wrapped inline, a row broke mid-list and left a dot at its end. */
|
|
82
|
+
.c3 .c3top .fr-fresh.qt-open{display:grid !important;grid-template-columns:auto 1fr;align-items:center;gap:6px 8px;width:max-content;max-width:min(360px,calc(100vw - 24px));white-space:nowrap}
|
|
83
|
+
.c3 .c3top .fr-fresh.qt-open > [data-fr-line]{grid-column:1 / -1}
|
|
84
|
+
.c3 .c3top .fr-fresh.qt-open > .fr-fresh__sep{display:none}
|
|
85
|
+
.c3 .c3top .fr-fresh.qt-open > .fr-fresh__note{white-space:normal}
|
|
81
86
|
.c3 nav.c3tabs{margin-top:0 !important;min-height:44px}
|
|
82
87
|
/* Row 2: one context bar per page */
|
|
83
88
|
.c3 .qt-ctx{display:flex !important;align-items:center;gap:10px;flex-wrap:wrap}
|
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
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;
|
|
@@ -72,3 +72,14 @@
|
|
|
72
72
|
/* The effects of two Day and time settings, on the console root. */
|
|
73
73
|
[data-c3-tint="off"][data-daypart]{background-image:none!important}
|
|
74
74
|
[data-c3-strip="off"] .c3pg-daylight{display:none!important}
|
|
75
|
+
/* Data refresh: the source icon sits in the title line; the schedule editor opens under the row. */
|
|
76
|
+
.c3 .c3set .rf-row .tt{display:flex;align-items:center;gap:var(--sp-2)}
|
|
77
|
+
.c3 .c3set .rf-ed{margin-top:var(--sp-3);padding-top:var(--sp-2);border-top:1px solid var(--border)}
|
|
78
|
+
.c3 .c3set .rf-ed .acts select+select,.c3 .c3set .rf-ed .acts select{margin-right:4px}
|
|
79
|
+
.c3 .c3set .rf-ed .mk-lbl{margin:var(--sp-2) 0 2px;text-transform:uppercase}
|
|
80
|
+
.c3 .c3set .rf-row .fold .c3set-b{margin-left:auto}
|
|
81
|
+
@media (max-width:860px){
|
|
82
|
+
.c3 .c3set .list>.nrow.st-row.rf-row{grid-template-columns:minmax(0,1fr)}
|
|
83
|
+
.c3 .c3set .list>.nrow.st-row.rf-row>.acts{justify-content:flex-start}
|
|
84
|
+
}
|
|
85
|
+
.c3.ph .c3set .nrow.st-row .acts{grid-column:auto;justify-content:flex-start}
|