create-pmos 0.534.0 → 0.536.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 +27 -35
- 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/briefing-premium-sources-test.js +14 -16
- package/workspace/changelog.json +26 -0
- package/workspace/ev-everywhere-test.js +2 -2
- package/workspace/guide/data-freshness.md +10 -4
- package/workspace/guide/reports.md +11 -1
- package/workspace/lib/refresh-run.js +7 -2
- package/workspace/lib/report-engine.js +27 -6
- package/workspace/lib/report-library.js +1 -1
- package/workspace/lib/report-orders.js +218 -0
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-report-flow.js +1 -1
- package/workspace/lib/routes-report-orders.js +161 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/premium-briefing-depth-test.js +4 -1
- package/workspace/public/pm/cozy-ev.css +11 -2
- package/workspace/public/pm/cozy-evmodel.js +77 -0
- package/workspace/public/pm/cozy-settings.js +42 -27
- package/workspace/public/pm/cozy-vw-rp.css +40 -0
- package/workspace/public/pm/premium-report-read.js +90 -9
- package/workspace/public/pm/report-orders-ui.js +115 -0
- package/workspace/skills/report-write.skill +1 -1
- package/workspace/test-lists/report-orders.json +11 -0
|
@@ -39,6 +39,7 @@ const MODULES = Object.freeze([
|
|
|
39
39
|
'routes-capture', // Run 3 (3C): ⌘K capture
|
|
40
40
|
'routes-board', // Run 4 (4D): the cross-initiative Board, Release
|
|
41
41
|
'routes-reports', // Run 5 (5C): drafts, Issue, notes
|
|
42
|
+
'routes-report-orders', // batch 27 item 16: apply an order to a report draft, Undo, my saved orders (canvas TpA, TpD, TpE)
|
|
42
43
|
'routes-report-flow', // batch 22 item 14: the report building flow (template versions, the report's own session, review)
|
|
43
44
|
'routes-notes', // meeting-notes Run 1: the Notes view (typed notes, dictation, transcripts, summaries)
|
|
44
45
|
'routes-my-work', // initiative my-work: the ranked list of work assigned to the PM, the brief, Read again
|
|
@@ -307,4 +307,4 @@ async function handle(req, res, url, ctx) {
|
|
|
307
307
|
return r.ok ? send({ status: 200, body: r }) : fail(r);
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
-
module.exports = { prefixes, handle, stateOf, askPrompt, draftWords };
|
|
310
|
+
module.exports = { prefixes, handle, stateOf, askPrompt, draftWords, ask };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* routes-report-orders.js — apply an order to a report draft, Undo, and my saved orders (batch 27 item 16; main canvas TpA, TpD, TpE;
|
|
3
|
+
* lib/report-orders.js). The app says "order", never "template".
|
|
4
|
+
*
|
|
5
|
+
* GET /api/report-orders?ws&path → { ok, id, slot, issued, engine, now, pick, next, orders, slides, undo } what the draft's order is,
|
|
6
|
+
* the ✦ (pick), every order with what it adds and drops, my orders first, my slides, and the Undo
|
|
7
|
+
* GET /api/report-orders/preview?ws&order → text/html, the order drawn with placeholder words (one slide per part), sanitized
|
|
8
|
+
* POST /api/report-orders/apply { ws, path, order } → { ok, redraft: { session, started|passed } } | { ok, next: { id, name } }
|
|
9
|
+
* A draft: the draft is copied aside (Undo, one day), the brief takes the order (lib/report-engine.js make),
|
|
10
|
+
* and the report's own session is asked to write it again in that order, keeping its readings. An agreed outline:
|
|
11
|
+
* nothing changes today; the order is used from the next cycle. An issued cycle is refused 409 (it is frozen).
|
|
12
|
+
* POST /api/report-orders/undo { ws, path } → { ok } the draft as it was before the redraft (its day only)
|
|
13
|
+
* POST /api/report-orders/save { ws, path, name? } → { ok, id } the draft's parts and layouts as my order (never its numbers or words)
|
|
14
|
+
* POST /api/report-orders/slide { ws, path, n } → { ok, id, name } slide n (from 0) of the draft's deck as my slide: its layout and part
|
|
15
|
+
* POST /api/report-orders/rename { ws, id, name } → { ok }
|
|
16
|
+
* POST /api/report-orders/remove { ws, id } → { ok }
|
|
17
|
+
*
|
|
18
|
+
* MY ORDERS are the PM's own: they live in the per-user runtime directory (ctx.runtimeDir), not in any repository, and no other person reads them.
|
|
19
|
+
* WHO WRITES. Every POST passes pm-guard requirePagePress; a request with an agent session header is refused first (403). Nothing here sends
|
|
20
|
+
* anything outward; the redraft runs in the report's own session (lib/routes-report-flow.js ask), which never issues.
|
|
21
|
+
*/
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const guard = require('./pm-guard.js');
|
|
25
|
+
const RO = require('./report-orders.js');
|
|
26
|
+
const RE = require('./report-engine.js');
|
|
27
|
+
const RL = require('./report-library.js');
|
|
28
|
+
const issues = require('./issues.js');
|
|
29
|
+
|
|
30
|
+
const prefixes = ['/api/report-orders'];
|
|
31
|
+
const POSTS = ['apply', 'undo', 'save', 'slide', 'rename', 'remove'];
|
|
32
|
+
const text = (v) => (typeof v === 'string' ? v : '');
|
|
33
|
+
const bad = (status, error) => ({ status, body: { ok: false, error } });
|
|
34
|
+
const PATH_RE = /(?:^|\/)reports\/([a-z][a-z0-9-]{0,39})\/(\d{4}-\d{2}-\d{2})\.md$/;
|
|
35
|
+
function wsOf(ctx, id) { const w = ctx.getWs(id || null); return w && (!id || w.id === id) ? w : null; }
|
|
36
|
+
const mineDir = (ctx) => ctx.runtimeDir || require('./state-root.js').defaultRoot();
|
|
37
|
+
const readJson = (f) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; } };
|
|
38
|
+
|
|
39
|
+
/** The draft a path names: { id, slot, P, issued } | { error }. The path must be a report document that exists. */
|
|
40
|
+
function draftOf(w, rel) {
|
|
41
|
+
const m = PATH_RE.exec(text(rel));
|
|
42
|
+
if (!m || text(rel).split('/').includes('..')) return { error: bad(400, 'path must be a report draft') };
|
|
43
|
+
let P; try { P = RE.paths(w.root, m[1], m[2]); } catch (e) { return { error: bad(400, e.message) }; }
|
|
44
|
+
if (!fs.existsSync(P.md.abs)) return { error: bad(404, 'no such report') };
|
|
45
|
+
let iss = false; try { iss = issues.isIssued(issues.readIssues(w.root), m[1], m[2]); } catch { iss = false; }
|
|
46
|
+
return { id: m[1], slot: m[2], P, issued: iss };
|
|
47
|
+
}
|
|
48
|
+
function setupName(w, id) { try { const RS = require('./report-setups.js'); const s = RS.fold(RS.readLines(RS.ledgerFile(w.root))).setups.get(id); return (s && s.name) || ''; } catch { return ''; } }
|
|
49
|
+
function reportName(w, id, brief) { return setupName(w, id) || (brief && brief.title) || id; }
|
|
50
|
+
/** A saved order's first name: the recurring report's ("Product weekly order"), else the order it began as ("Weekly Review, my way"), else
|
|
51
|
+
* "My order". Never an ad hoc report's title: that is its question, a sentence with dates in it. */
|
|
52
|
+
function savedName(w, id, brief) { const s = setupName(w, id); if (s) return s + ' order'; return brief && brief.template && brief.template.name ? brief.template.name.replace(/, my way$/, '') + ', my way' : 'My order'; }
|
|
53
|
+
const card = (o, now, mine, extra = {}) => ({ id: o.id, name: o.name, sub: RO.subOf(o.id, mine), n: o.outline.length, parts: o.outline, ...RO.diff(now.parts, o.outline), ...extra });
|
|
54
|
+
/** The whole state of one draft's order. */
|
|
55
|
+
function stateOf(ctx, w, d) {
|
|
56
|
+
const brief = readJson(d.P.brief.abs), report = readJson(d.P.report.abs);
|
|
57
|
+
const now = RO.nowOf(brief, report);
|
|
58
|
+
const my = RO.fold(RO.readLines(mineDir(ctx)));
|
|
59
|
+
const pk = d.issued ? null : RO.pick(brief, report);
|
|
60
|
+
const orders = my.orders.map((o) => card(o, now, my.orders, { mine: true }))
|
|
61
|
+
.concat(RL.TEMPLATES.map((t) => card({ id: t.id, name: t.name, outline: t.outline }, now, my.orders)));
|
|
62
|
+
return { ok: true, id: d.id, slot: d.slot, issued: d.issued, engine: !!report, name: reportName(w, d.id, brief),
|
|
63
|
+
now, pick: pk, next: RO.nextOf(w.root, d.id), orders, slides: my.slides.map((s) => ({ id: s.id, name: s.name, part: s.outline[0].part, layout: s.outline[0].layout, at: s.at })),
|
|
64
|
+
undo: RO.undoOf(w.root, d.id, d.slot, { issued: d.issued }) };
|
|
65
|
+
}
|
|
66
|
+
/** The words the report's session gets for a redraft in an order. */
|
|
67
|
+
function redraftWords(id, slot, rel, order) {
|
|
68
|
+
return `Write this draft again in the "${order.name}" order (${rel}, slot ${slot}). This change IS a redraft, of this slot only. `
|
|
69
|
+
+ `The brief (${slot}.brief.json) now carries the order as its template: follow skills/report-write.skill from step 3, with the order's parts as the starting outline, `
|
|
70
|
+
+ `each part in its slide layout and retitled as a finding; add or drop a part when the findings say so. Keep every reading and every finding already filed: `
|
|
71
|
+
+ `run \`pmos report make ${id} --slot ${slot}\` only when a source is missing. Then run \`pmos report write ${id} --slot ${slot} --from <file>\`.`;
|
|
72
|
+
}
|
|
73
|
+
/** The deck slide n as one saved part: its layout, and a part name that is never a finding. */
|
|
74
|
+
function slidePart(d, n) {
|
|
75
|
+
const deck = RE.engineDeckOf(d.P.md.abs, null);
|
|
76
|
+
if (!deck) return { error: bad(409, 'this draft was written before the report engine: its slides have no layouts to save') };
|
|
77
|
+
const s = deck.slides[n];
|
|
78
|
+
if (!s) return { error: bad(404, 'no such slide') };
|
|
79
|
+
const fixed = { title: ['Title', 'overview'], answer: ['The answer', s.layout], beyond: ['Beyond the question', 'agenda'], ask: ['Asks', 'decisions'], sources: ['Sources', 'sources'] };
|
|
80
|
+
if (fixed[s.kind]) return { part: fixed[s.kind][0], layout: fixed[s.kind][1] || 'statement' };
|
|
81
|
+
const report = readJson(d.P.report.abs), brief = readJson(d.P.brief.abs);
|
|
82
|
+
const parts = RO.partsOf(report, brief);
|
|
83
|
+
// the section slides come after the title and answer slides, in the report's order (a long text section may take several slides)
|
|
84
|
+
const k = deck.slides.slice(0, n + 1).filter((x) => x.kind === 'section' && !/\(continued\)$/.test(x.title)).length - 1;
|
|
85
|
+
const p = parts[k] || { part: RO.nameOfLayout(s.layout), layout: s.layout };
|
|
86
|
+
return { part: p.part, layout: p.layout };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function handle(req, res, url, ctx) {
|
|
90
|
+
const p = url.pathname;
|
|
91
|
+
const send = (r) => { ctx.json(res, r.body, r.status); return true; };
|
|
92
|
+
const sub = p.replace(/^\/api\/report-orders\/?/, '');
|
|
93
|
+
if (req.method === 'GET') {
|
|
94
|
+
const w = wsOf(ctx, url.searchParams.get('ws'));
|
|
95
|
+
if (!w) return send(bad(404, 'no such workspace'));
|
|
96
|
+
if (sub === 'preview') {
|
|
97
|
+
const o = RO.orderOf(text(url.searchParams.get('order')), RO.fold(RO.readLines(mineDir(ctx))).orders);
|
|
98
|
+
if (!o) return send(bad(404, 'no such order'));
|
|
99
|
+
const html = require('./report-design.js').sanitize(require('./report-slides.js').deckHtml(RO.previewDeck(o)));
|
|
100
|
+
res.writeHead && res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' });
|
|
101
|
+
if (res.end) res.end(html); else { res.status = 200; res.body = html; }
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (sub !== '') return send(bad(404, 'no such route'));
|
|
105
|
+
const d = draftOf(w, url.searchParams.get('path'));
|
|
106
|
+
if (d.error) return send(d.error);
|
|
107
|
+
return send({ status: 200, body: stateOf(ctx, w, d) });
|
|
108
|
+
}
|
|
109
|
+
if (!POSTS.includes(sub)) return send(bad(404, 'no such route'));
|
|
110
|
+
if (req.method !== 'POST') return send(bad(405, 'POST only'));
|
|
111
|
+
if (req.headers && req.headers['x-pmos-session']) return send(bad(403, 'a session cannot change a report: the PM presses on the page'));
|
|
112
|
+
const read = await ctx.readJson(req);
|
|
113
|
+
if (!read.ok) return send(bad(400, 'the body is not JSON'));
|
|
114
|
+
const body = read.body || {};
|
|
115
|
+
const g = guard.requirePagePress(req, body);
|
|
116
|
+
if (!g.ok) return send(bad(g.status, g.error));
|
|
117
|
+
const w = wsOf(ctx, text(body.ws));
|
|
118
|
+
if (!w) return send(bad(404, 'no such workspace'));
|
|
119
|
+
const dir = mineDir(ctx);
|
|
120
|
+
|
|
121
|
+
if (sub === 'rename') { const r = RO.rename(dir, text(body.id), text(body.name)); return send(r.ok ? { status: 200, body: { ok: true } } : bad(r.status, r.error)); }
|
|
122
|
+
if (sub === 'remove') { const r = RO.remove(dir, text(body.id)); return send(r.ok ? { status: 200, body: { ok: true } } : bad(r.status, r.error)); }
|
|
123
|
+
|
|
124
|
+
const d = draftOf(w, body.path);
|
|
125
|
+
if (d.error) return send(d.error);
|
|
126
|
+
if (sub === 'save') {
|
|
127
|
+
const report = readJson(d.P.report.abs), brief = readJson(d.P.brief.abs);
|
|
128
|
+
if (!report) return send(bad(409, 'this draft was written before the report engine: its slides have no layouts to save'));
|
|
129
|
+
const outline = [{ part: 'Title', layout: 'overview' }].concat(RO.partsOf(report, brief), [{ part: 'Sources', layout: 'sources' }]);
|
|
130
|
+
const r = RO.save(dir, { kind: 'order', name: text(body.name) || savedName(w, d.id, brief), outline, from: { report: d.id, slot: d.slot } });
|
|
131
|
+
return send(r.ok ? { status: 200, body: { ok: true, id: r.id } } : bad(r.status, r.error));
|
|
132
|
+
}
|
|
133
|
+
if (sub === 'slide') {
|
|
134
|
+
const n = Number(body.n);
|
|
135
|
+
if (!Number.isInteger(n) || n < 0) return send(bad(400, 'n is the slide, from 0'));
|
|
136
|
+
const sp = slidePart(d, n);
|
|
137
|
+
if (sp.error) return send(sp.error);
|
|
138
|
+
const r = RO.save(dir, { kind: 'slide', name: sp.part, outline: [sp], from: { report: d.id, slot: d.slot } });
|
|
139
|
+
return send(r.ok ? { status: 200, body: { ok: true, id: r.id, name: sp.part, layout: sp.layout } } : bad(r.status, r.error));
|
|
140
|
+
}
|
|
141
|
+
if (d.issued) return send(bad(409, `the ${d.slot} report is issued, so it is frozen. The next cycle starts from its order`));
|
|
142
|
+
if (sub === 'undo') { const r = RO.undo(w.root, d.id, d.slot); return send(r.ok ? { status: 200, body: { ok: true, order: r.order } } : bad(r.status, r.error)); }
|
|
143
|
+
|
|
144
|
+
// apply
|
|
145
|
+
const order = RO.orderOf(text(body.order), RO.fold(RO.readLines(dir)).orders);
|
|
146
|
+
if (!order) return send(bad(404, 'no such order'));
|
|
147
|
+
const brief = readJson(d.P.brief.abs);
|
|
148
|
+
if (brief && Array.isArray(brief.agreed_outline) && brief.agreed_outline.length) {
|
|
149
|
+
RO.setNext(w.root, d.id, order, d.slot, { by: g.actor });
|
|
150
|
+
return send({ status: 200, body: { ok: true, next: { id: order.id, name: order.name } } });
|
|
151
|
+
}
|
|
152
|
+
RO.snapshot(w.root, d.id, d.slot, { order });
|
|
153
|
+
const m = RE.make(w.root, d.id, { slot: d.slot, template: RL.templateOf(order.id) ? order.id : order });
|
|
154
|
+
if (!m.ok) { RO.undo(w.root, d.id, d.slot); return send(bad(m.status || 400, m.error)); }
|
|
155
|
+
const rel = d.P.md.rel;
|
|
156
|
+
const r = require('./routes-report-flow.js').ask(ctx, w, d.id, redraftWords(d.id, d.slot, rel, order), rel);
|
|
157
|
+
if (!r.ok) { RO.undo(w.root, d.id, d.slot); return send(bad(r.status || 409, r.error)); }
|
|
158
|
+
return send({ status: 200, body: { ok: true, redraft: { session: r.session, started: !!r.started, passed: !!r.passed }, order: { id: order.id, name: order.name } } });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { prefixes, handle, stateOf, redraftWords };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.536.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.536.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@anthropic-ai/sdk": "^1.3.0",
|
package/workspace/package.json
CHANGED
|
@@ -254,7 +254,10 @@ const GANTT = [
|
|
|
254
254
|
await until(() => p.evaluate(() => document.querySelectorAll('#evSettings:not([hidden]) .ev-fr[data-src]').length > 0), 20000);
|
|
255
255
|
const ft = await p.evaluate(() => document.querySelector('#evSettings .ev-st-main').textContent);
|
|
256
256
|
const srcRows = await p.evaluate(() => document.querySelectorAll('#evSettings .ev-fr[data-src]').length);
|
|
257
|
-
|
|
257
|
+
// batch 28 item 1 (CnA): "never posts" is the (i) button's note; press it and the note says so
|
|
258
|
+
await p.click('#evSettings [data-ev-info]');
|
|
259
|
+
const note = await until(() => p.evaluate(() => (document.querySelector('#evSettings #evConInfo') || {}).textContent || ''), 5000);
|
|
260
|
+
ok('F1 Settings › Connectors lists the workspace\'s sources with when each was last read, and (i) says it never posts', srcRows >= 3 && /read/.test(ft) && /never posts/.test(note || '') && !/up to date/i.test(ft), ft + ' | ' + note);
|
|
258
261
|
await shot('src-1280-light');
|
|
259
262
|
const one = await p.evaluate(() => { const b = document.querySelector('#evSettings .ev-fr [data-ev-read]'); return b ? b.dataset.evRead : null; });
|
|
260
263
|
if (one) {
|
|
@@ -292,12 +292,21 @@
|
|
|
292
292
|
/* Settings › Connectors: each source's last read (feedback batch 16 item 5, FmA3Settings; was the Sources card), and the amber box on top */
|
|
293
293
|
.ev-st-main > .ev-st-warn { margin-bottom: 12px; color: var(--cz-ink); }
|
|
294
294
|
.ev-st-warn .ev-ib-card { background: var(--cz-card); }
|
|
295
|
-
.ev-st .ev-reads { margin-bottom:
|
|
295
|
+
.ev-st .ev-reads { margin-bottom: 0; }
|
|
296
296
|
.ev-st .ev-fr { align-items: center; gap: 10px; }
|
|
297
297
|
.ev-st .ev-fr .ev-late { color: var(--cz-risk); font-weight: var(--fw-strong); }
|
|
298
298
|
.ev-st .ev-ib-sp { width: 32px; flex: none; }
|
|
299
299
|
.ev-st-nav .ev-mi .ev-dot.risk { margin-left: 6px; }
|
|
300
|
-
|
|
300
|
+
/* design A (batch 28 item 1, canvas CnA): one list, trouble first; the dot sits in a gutter so every name lines up */
|
|
301
|
+
.ev-st .ev-fr { min-height: 60px; box-sizing: border-box; padding: 10px 0; }
|
|
302
|
+
.ev-st .ev-fr .ev-gut { width: 8px; flex: none; display: flex; justify-content: center; }
|
|
303
|
+
.ev-st .ev-fr .ev-why { margin-top: 2px; overflow-wrap: anywhere; }
|
|
304
|
+
.ev-st .ev-fr .ev-why.ev-late { font-weight: inherit; }
|
|
305
|
+
.ev-st .ev-fr .ev-tx > span { overflow-wrap: anywhere; }
|
|
306
|
+
.ev-st .ev-info { margin: 0 0 8px; }
|
|
307
|
+
.ev-st .ev-fold { border-top: 1px solid var(--cz-hair); border-bottom: 0; }
|
|
308
|
+
.ev-st .ev-con-more { padding-left: 16px; border-left: 2px solid var(--cz-hair); margin-bottom: 12px; }
|
|
309
|
+
@media (max-width: 760px) { .ev-st .ev-fr .ev-ib, .ev-st .ev-fr .ev-ib-sp { width: 44px; height: 44px; } }
|
|
301
310
|
|
|
302
311
|
/* New for you's item Viewers (batch 18 items 2 and 2b, canvas OvA5Overlap, OvA6Given): the summary, then the two initiatives, the pick and ONE primary */
|
|
303
312
|
.nv { display: flex; flex-direction: column; gap: 10px; }
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
* bannerOf(sources, now) → { id, name, title: 'Reconnect Jira.', body, text, action } | null the one source the PM must act on
|
|
8
8
|
* (self-heal, feedback batch 26 item 7): null while Claude repairs a source by itself, or after it did
|
|
9
9
|
* sourceRows(sources, now) → [{ id, name, sub, when, risk, act: 'read' | 'connect' | '' }] the Sources table (EvSources)
|
|
10
|
+
* connectorRows(sources, health, now) → [{ id, name, trouble, why, when, act }] Settings › Connectors, design A (CnA): trouble first
|
|
11
|
+
* issueRow(issue) → { src, name, why } a harness-health issue split into a row's name and reason
|
|
12
|
+
* clockOf(iso, now) → '09:15' today · 'yesterday' · '3 Nov'
|
|
10
13
|
* partsOf(id) → what may be out of date when that source is old ('Board and the Jira-linked initiatives')
|
|
11
14
|
* fabLabel(here, open, stale, news) → 'Open the menu. You are in Home. 3 new for you. Jira 5 days old' (the button says both dots, FmGrill 13)
|
|
12
15
|
* SEARCH_KINDS · searchGroups(results, { phone }) → [{ key, label, rows }] EvSearch's groups, in its order
|
|
@@ -71,6 +74,80 @@ export function sourceRows(sources, now = Date.now()) {
|
|
|
71
74
|
});
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
/* Settings › Connectors, design A (batch 28 item 1, canvas CnA): one list, trouble first. Each source says its status once, on its own
|
|
78
|
+
* row. A trouble row has the amber dot, one reason and one action; a quiet row has its name and its time. Claude's failed background
|
|
79
|
+
* work and the repository's Git sync failure are rows too: an issue about a source (harness-health `src`) is that source's reason,
|
|
80
|
+
* and any other issue is a row of its own. */
|
|
81
|
+
const HEALTH_SRC = { calendar: 'calendar', jira: 'jira', mywork: 'my-work', tracker: 'tracker', tools: 'tools', sync: 'repository' };
|
|
82
|
+
// lib/harness-health.js words each issue as one sentence that starts with the work's label (refresh SOURCES); a row splits it into
|
|
83
|
+
// its name and its reason here, so the server's sentence stays as the bell and the logs know it
|
|
84
|
+
const HEALTH_NAME = { calendar: 'Calendar', jira: 'Jira tickets and sprints', mywork: 'Work assigned to you', tracker: 'Tracker files', tools: 'Your tools', brief: 'Morning briefing' };
|
|
85
|
+
const cap = (t) => t.replace(/^./, (c) => c.toUpperCase());
|
|
86
|
+
/** { src, name, why } of a harness-health issue ({ key, text }). */
|
|
87
|
+
export function issueRow(i) {
|
|
88
|
+
const key = String((i && i.key) || ''), text = String((i && i.text) || '');
|
|
89
|
+
const m = /^(failed|missed):(.+)$/.exec(key);
|
|
90
|
+
if (m) { const name = HEALTH_NAME[m[2]] || m[2], why = text.startsWith(name + ' ') ? cap(text.slice(name.length + 1)) : text; return { src: m[2], name, why }; }
|
|
91
|
+
if (key === 'sync') { const g = /^Git sync is failing: (.+?) was not found\./.exec(text); return { src: 'sync', name: 'Repository', why: g ? 'Git cannot find ' + g[1] + '. Check the address or your access.' : text }; }
|
|
92
|
+
if (key === 'brief') return { src: 'brief', name: 'Morning brief', why: cap(text.replace(/^Morning brief /, '')) };
|
|
93
|
+
if (key === 'tick') return { src: '', name: 'Background checks', why: text.replace(/^Background checks /, '').replace(/^./, (c) => c.toUpperCase()) };
|
|
94
|
+
if (key === 'signedout') return { src: '', name: 'Claude', why: text.replace(/^Claude /, '').replace(/^./, (c) => c.toUpperCase()) };
|
|
95
|
+
if (key.startsWith('unsent:')) { const k = text.indexOf(' is idle: '); return k > 0 ? { src: '', name: text.slice(0, k), why: 'Idle: ' + text.slice(k + 10) } : { src: '', name: 'A session', why: text }; }
|
|
96
|
+
return { src: '', name: 'Background work', why: text };
|
|
97
|
+
}
|
|
98
|
+
const two_ = (n) => String(n).padStart(2, '0');
|
|
99
|
+
/** A clock time today ("09:15"), else the day ("yesterday", "3 Nov"). */
|
|
100
|
+
export function clockOf(iso, now = Date.now()) {
|
|
101
|
+
const d = new Date(iso || ''); if (Number.isNaN(d.getTime())) return '';
|
|
102
|
+
const n = new Date(now), day = (x) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime();
|
|
103
|
+
const diff = Math.round((day(n) - day(d)) / 86400000);
|
|
104
|
+
if (diff <= 0) return two_(d.getHours()) + ':' + two_(d.getMinutes());
|
|
105
|
+
if (diff === 1) return 'yesterday';
|
|
106
|
+
return d.getDate() + ' ' + ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()];
|
|
107
|
+
}
|
|
108
|
+
const short = (a) => a.replace(/ hours? ago$/, ' h ago');
|
|
109
|
+
/**
|
|
110
|
+
* connectorRows(sources, health, now) → [{ id, name, trouble, why, when, act }] trouble rows first (needs you, then failed, then
|
|
111
|
+
* the other issues), then the quiet rows in the server's order.
|
|
112
|
+
* why the one reason (trouble), or "Checked 10:42 · same since 09:15" on a quiet row a measured read applies to, else ''
|
|
113
|
+
* when the time on a quiet row ('3 h ago', 'Watching', 'Not connected', 'Reading now'); '' when `why` carries the time
|
|
114
|
+
* act { kind: 'read' | 'open' | 'sec', label, href?, sec? } | null the one icon action
|
|
115
|
+
*/
|
|
116
|
+
export function connectorRows(sources, health, now = Date.now()) {
|
|
117
|
+
const issues = (health && (health.level === 'warn' || health.level === 'bad') && health.issues) || [];
|
|
118
|
+
const bySrc = new Map(), alone = [];
|
|
119
|
+
for (const x of issues) { const i = { key: x.key, ...issueRow(x) }, id = HEALTH_SRC[i.src]; if (id && !bySrc.has(id)) bySrc.set(id, i); else if (!id) alone.push(i); }
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const s of (sources || []).filter(Boolean)) {
|
|
122
|
+
const name = sourceName(s), h = s.heal, iss = bySrc.get(s.id);
|
|
123
|
+
const read = { kind: 'read', label: 'Read ' + name + ' again' };
|
|
124
|
+
if (h && h.state === 'needs-you' && h.action) {
|
|
125
|
+
const a = h.action, dur = s.as_of ? ago(s.as_of, now).replace(/ ago$/, '') : '';
|
|
126
|
+
const why = [s.as_of ? 'Not read for ' + dur + '.' : 'Never read.', h.tried || (h.why ? 'The read ' + h.why + '.' : ''), a.read ? '' : a.how].filter(Boolean).join(' ');
|
|
127
|
+
out.push({ id: s.id, name, trouble: 1, why, when: '', act: a.read ? { ...read, label: a.label } : a.href ? { kind: 'open', label: a.label, href: a.href } : { kind: 'sec', label: a.label, sec: a.sec || 'claude' } });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (s.state === 'failed' || iss) {
|
|
131
|
+
const why = iss ? iss.why : 'Could not be read.';
|
|
132
|
+
const act = s.id === 'repository' ? (iss ? { kind: 'sec', label: 'Check the repository address', sec: 'workspaces' } : null) : read;
|
|
133
|
+
out.push({ id: s.id, name, trouble: 2, why, when: '', act });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const mend = h && h.state === 'repairing' && (s.state === 'stale' || s.state === 'never');
|
|
137
|
+
const sch = s.schedule || {};
|
|
138
|
+
if (s.state === 'fresh' && sch.same_since && sch.checked_at) {
|
|
139
|
+
out.push({ id: s.id, name, trouble: 0, why: 'Checked ' + clockOf(sch.checked_at, now) + ' · same since ' + clockOf(sch.same_since, now), when: '', act: null });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const when = s.state === 'off' ? 'Not connected' : s.state === 'building' ? 'Reading now' : mend ? 'Reading again' : s.state === 'never' ? 'Never read'
|
|
143
|
+
: s.id === 'repository' && s.state === 'fresh' ? 'Watching' : short(ago(s.as_of, now)) || 'Never read';
|
|
144
|
+
const behind = (s.state === 'stale' || s.state === 'never') && s.id !== 'repository' && !mend;
|
|
145
|
+
out.push({ id: s.id, name, trouble: 0, why: '', when, act: behind ? read : null });
|
|
146
|
+
}
|
|
147
|
+
for (const i of alone) out.push({ id: 'issue:' + i.key, name: i.name || 'Background work', trouble: 3, why: i.why, when: '', act: i.key === 'signedout' ? { kind: 'sec', label: 'Open Settings › Claude', sec: 'claude' } : null });
|
|
148
|
+
return out.map((r, k) => ({ r, k })).sort((a, b) => (a.r.trouble || 9) - (b.r.trouble || 9) || a.k - b.k).map((x) => x.r);
|
|
149
|
+
}
|
|
150
|
+
|
|
74
151
|
export function fabLabel(here, open, stale, news) {
|
|
75
152
|
if (open) return 'Close the menu';
|
|
76
153
|
return 'Open the menu. You are in ' + (PLACE[here] || 'PMOS') + (news > 0 ? '. ' + news + ' new for you' : '') + (stale ? '. ' + (stale.text || 'A source is stale') : '');
|
|
@@ -12,10 +12,12 @@
|
|
|
12
12
|
* › Dress for the day (adler.dress.*, canvas AsSet1, batch 18 item 8): the master switch, where you are, one switch per group,
|
|
13
13
|
* "More festivals" with one switch per day, big sports and your colours, exam season, your days, Wear now (pm/cozy-seasons.js).
|
|
14
14
|
* openSettings('dress') opens it straight away (the › in Adler's tap popover, AsSet2Tap).
|
|
15
|
-
* Connectors
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Connectors design A (batch 28 item 1, canvas CnA): one list, trouble first (GET /api/sources, pm/cozy-evmodel.js connectorRows). Each
|
|
16
|
+
* source says its status once, on its own row: a trouble row has the amber dot, one reason and one icon action (↻ POST
|
|
17
|
+
* /api/sources/refresh, or the place to fix it); a quiet row has its name and time, and "Checked 10:42 · same since 09:15"
|
|
18
|
+
* where a measured read applies (SrSee R3). Claude's failed background work and the Git sync failure are rows too. ↻ in the
|
|
19
|
+
* header reads every source (POST /api/sources/refresh-all); (i) says what PMOS reads and that it never posts. The switches
|
|
20
|
+
* (what PMOS reads on its own, the connectors your sessions have; GET and POST /api/proactive/settings) fold into one row.
|
|
19
21
|
* Workspaces the repositories PMOS works in (GET and POST /api/workspaces, POST /api/workspaces/forget; the folder is never touched)
|
|
20
22
|
* Workflows a link to its own page, /premium-workflows.html (pm/wf-page.js, D99): the stages each kind of initiative goes through
|
|
21
23
|
* Updates the versions and Check now, Update, Restart to update (GET /api/kit/status, POST /api/kit/check | update | upgrade-app); opening it
|
|
@@ -25,7 +27,7 @@
|
|
|
25
27
|
* Adler's moments (batch 17 item 4-B, AdM4): one face per section, where Claude speaks. Updates: glasses while it checks or installs,
|
|
26
28
|
* holding the card when a version is ready, calm when up to date. General: holding the update card, else the Jira promise (only after
|
|
27
29
|
* Commit). A failure in the status line takes the honest face, and then the section's face steps back (one Adler per screen).
|
|
28
|
-
* The card at the bottom of General says when an update is ready. While a source needs you (lib/source-heal.js), an amber box at the top of every section names
|
|
30
|
+
* The card at the bottom of General says when an update is ready. While a source needs you (lib/source-heal.js), an amber box at the top of every other section names
|
|
29
31
|
* it with its one action and what Claude tried, and the place menu opens Settings at Connectors. Every write is the PM's press (pmPost carries the launch nonce).
|
|
30
32
|
* PMOS reads the sources and never posts to them.
|
|
31
33
|
* openSettings(section?) */
|
|
@@ -33,7 +35,7 @@ import { mountClaudeSetup } from './cozy-claude-setup.js';
|
|
|
33
35
|
import * as JL from './cozy-jl.js';
|
|
34
36
|
import { pmGet, pmPost, pressFetch } from './http.js';
|
|
35
37
|
import { esc, svg, ICON, toast, wsParam, wsQ } from './cozy-evui.js';
|
|
36
|
-
import {
|
|
38
|
+
import { connectorRows, bannerOf } from './cozy-evmodel.js';
|
|
37
39
|
import * as AD from './cozy-adler.js';
|
|
38
40
|
import * as SE from './cozy-seasons.js';
|
|
39
41
|
import { weeksOf, lineOf, storiesOf } from './upd-weeks.js';
|
|
@@ -47,7 +49,7 @@ const SECTIONS = [['general', 'General'], ['claude', 'Claude'], ['checks', 'Clau
|
|
|
47
49
|
const PAGE_LINKS = { workflows: '/premium-workflows.html' }; // a section that is a page of its own: the row goes there
|
|
48
50
|
const SRC = { calendar: 'Calendar', wispr: 'Wispr Flow', mail: 'Mail', slack: 'Slack', jira: 'Jira', confluence: 'Confluence', github: 'GitHub', drive: 'Google Drive', figma: 'Figma', website: 'Your websites' };
|
|
49
51
|
const VIEWER_KEY = 'pmos.cz.viewer';
|
|
50
|
-
const T = { el: null, sec: 'general', ws: null, st: {}, src: null, con: {}, conn: [], set: null, prods: null, msg: '', busy: false, phase: '', say: '', back: null, fresh: null, reading: '', note: {},
|
|
52
|
+
const T = { el: null, sec: 'general', ws: null, st: {}, src: null, con: {}, conn: [], set: null, prods: null, msg: '', busy: false, phase: '', say: '', back: null, fresh: null, reading: '', note: {}, info: false, fold: false, dress: false, more: false, bday: false, dset: {}, wkOpen: new Set(), wkMore: new Set(), arc: false };
|
|
51
53
|
const cmpVer = (a, b) => { const x = String(a || '0').split('-')[0].split('.').map(Number), y = String(b || '0').split('-')[0].split('.').map(Number); for (let i = 0; i < 3; i++) if ((x[i] || 0) !== (y[i] || 0)) return (x[i] || 0) < (y[i] || 0) ? -1 : 1; return 0; };
|
|
52
54
|
const home = () => (T.ws && T.ws[0] ? T.st[T.ws[0].id] : null);
|
|
53
55
|
const latest = () => { const s = home(); return (s && (s.latest || (s.lastCheck && s.lastCheck.latest))) || null; };
|
|
@@ -168,22 +170,24 @@ function dressSec() {
|
|
|
168
170
|
const firstLine = () => { const c = changes().find((e) => e.v === latest()); const p = c && c.prs && c.prs[0]; return p ? (p.t || p.s || '') : ''; };
|
|
169
171
|
/* each source's last read (FmA3Settings): the name, "read 09:41", the amber dot and ↻ on a stale or failed one */
|
|
170
172
|
const RD = 'M20 12a8 8 0 1 1-2.3-5.7M20 4v5h-5';
|
|
173
|
+
const INFO_I = 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18M12 11v5M12 8h.01';
|
|
171
174
|
const readBtn = (id, label, cls = 'ev-ib') => `<button type="button" class="${cls}" data-ev-read="${esc(id)}" aria-label="${esc(label)}" title="${esc(label)}"${T.reading ? ' disabled' : ''}>${svg(RD, 18)}</button>`;
|
|
172
175
|
function readsHtml() {
|
|
173
176
|
const s = T.fresh;
|
|
174
177
|
if (!s) return '<p class="ev-small ev-mut">Reading when each source was last read.</p>';
|
|
175
178
|
if (!s.ok) return `<p class="ev-small ev-mut">When each source was last read could not be read${s.error ? ': ' + esc(s.error) : ''}.</p>`;
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
179
|
+
const act = (x) => {
|
|
180
|
+
const a = x.act;
|
|
181
|
+
if (!a) return '<span class="ev-ib-sp" aria-hidden="true"></span>';
|
|
182
|
+
if (a.kind === 'read') return readBtn(x.id, a.label);
|
|
183
|
+
const at = a.kind === 'open' ? `href="${esc(a.href)}" target="_blank" rel="noopener"` : `href="#${esc(a.sec)}" data-ev-sec="${esc(a.sec)}"`;
|
|
184
|
+
return `<a class="ev-ib" ${at} aria-label="${esc(a.label)}" title="${esc(a.label)}">${svg(ICON.open, 18)}</a>`;
|
|
185
|
+
};
|
|
186
|
+
// one list, trouble first (CnA): the amber dot, the one reason and the one action on a trouble row; the name and the time on a quiet one
|
|
187
|
+
const rows = connectorRows(s.sources, s.health).map((x) => `<div class="ev-trow ev-fr${x.trouble ? ' ev-fr-t' : ''}" data-src="${esc(x.id)}"><span class="ev-gut">${x.trouble ? '<i class="ev-dot risk" aria-hidden="true"></i>' : ''}</span>`
|
|
188
|
+
+ `<div class="ev-tx ev-grow">${x.trouble ? `<b>${esc(x.name)}</b>` : `<span>${esc(x.name)}</span>`}${x.why ? `<p class="ev-small ev-why ${x.trouble ? 'ev-late' : 'ev-mut'}">${x.trouble ? '<span class="ev-vh">Needs a look: </span>' : ''}${esc(x.why)}</p>` : ''}${T.note[x.id] ? `<p class="ev-small" role="status">${esc(T.note[x.id])}</p>` : ''}</div>`
|
|
189
|
+
+ `${x.when ? `<span class="ev-small ev-mut">${esc(x.when)}</span>` : ''}${act(x)}</div>`).join('');
|
|
190
|
+
return `<div class="ev-reads">${rows}</div>`;
|
|
187
191
|
}
|
|
188
192
|
/** The amber box at the top of Settings, only when the PM must act on a source (self-heal, feedback batch 26 item 7). While Claude
|
|
189
193
|
* reads a source again by itself, or after it did, there is no box. The box names the one action and links to the exact place:
|
|
@@ -196,11 +200,19 @@ function staleHtml() {
|
|
|
196
200
|
: `<a class="ev-ib ev-ib-card" href="${esc(a.href || '#' + a.sec)}"${a.href ? ' target="_blank" rel="noopener"' : ` data-ev-sec="${esc(a.sec)}"`} aria-label="${esc(a.label)}" title="${esc(a.label)}">${svg(ICON.open, 18)}</a>`;
|
|
197
201
|
return `<div class="ev-warn ev-line ev-st-warn" id="srcWarn" data-cause="${esc(b.cause || '')}"><i class="ev-dot risk" aria-hidden="true"></i><p class="ev-grow"><b>${esc(b.title)}</b> ${esc(b.body)}${a.read ? '' : ' ' + esc(a.how)}</p>${act}</div>`;
|
|
198
202
|
}
|
|
203
|
+
const INFO = 'PMOS reads these sources and never posts to them. A switch chooses only what PMOS reads on its own; your Claude sessions can use every connector.';
|
|
199
204
|
function connectors() {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
205
|
+
const info = T.info ? `<p class="ev-small ev-mut ev-info" id="evConInfo">${esc(INFO)}</p>` : '';
|
|
206
|
+
if (T.src === null) return info + readsHtml() + '<p class="ev-small ev-mut">Reading the connectors.</p>';
|
|
207
|
+
const brief = Object.keys(SRC).filter((k) => k in T.src).map((k) => [SRC[k], trow(SRC[k], T.src[k] ? 'PMOS reads it on its own' : 'PMOS does not read it on its own', sw(T.src[k], `data-ev-src="${k}"`, SRC[k]))]);
|
|
208
|
+
const more = T.conn.filter((c) => !c.source).map((c) => [c.label, trow(c.label, T.con[c.name] ? 'On. Your sessions can use it' : 'Your sessions can use it', sw(T.con[c.name], `data-ev-conn="${esc(c.name)}"`, c.label))]);
|
|
209
|
+
const all = [...brief, ...more];
|
|
210
|
+
if (!all.length) return info + readsHtml();
|
|
211
|
+
// the other connectors fold to one row (CnA): how many, and the first three names
|
|
212
|
+
const names = all.map((x) => x[0]), sub = names.length > 3 ? names.slice(0, 3).join(', ') + ' and ' + (names.length - 3) + (names.length - 3 === 1 ? ' other' : ' others') : names.join(', ');
|
|
213
|
+
const fold = `<div class="ev-trow ev-fr ev-fold"><span class="ev-gut"></span><div class="ev-tx ev-grow"><span>${all.length} more in your Claude session</span><p class="ev-small ev-mut">${esc(sub)}</p></div>`
|
|
214
|
+
+ `<button type="button" class="ev-ib" data-ev-fold aria-expanded="${!!T.fold}" aria-controls="evConMore" aria-label="${T.fold ? 'Hide the other connectors' : 'Open the other connectors'}" title="${T.fold ? 'Hide the other connectors' : 'Open the other connectors'}">${svg(T.fold ? ICON.down : CHEV, 18)}</button></div>`;
|
|
215
|
+
return info + readsHtml() + fold + (T.fold ? `<div class="ev-con-more" id="evConMore">${all.map((x) => x[1]).join('')}</div>` : '');
|
|
204
216
|
}
|
|
205
217
|
function workspaces() {
|
|
206
218
|
if (T.ws === null) return '<p class="ev-small ev-mut">Reading the repositories.</p>';
|
|
@@ -323,12 +335,14 @@ function draw() {
|
|
|
323
335
|
const el = T.el; if (!el || el.hidden) return;
|
|
324
336
|
// a redraw replaces the nodes: keep focus on the same control (by its data-ev-* key), else on the open section, never on the body
|
|
325
337
|
const ae = document.activeElement, had = ae && el.contains(ae), key = had ? [...ae.attributes].find((a) => a.name.startsWith('data-ev-')) : null;
|
|
326
|
-
const n = {
|
|
338
|
+
const n = { workspaces: T.ws ? T.ws.filter((x) => !x.linked).length : '', updates: ready() ? '<span class="ev-pill y">1</span>' : '' };
|
|
327
339
|
const stale = T.fresh && T.fresh.ok ? bannerOf(T.fresh.sources) : null;
|
|
328
340
|
el.querySelector('.ev-st-nav').innerHTML = '<h2 class="ev-h2">Settings</h2>' + SECTIONS.map(([k, l]) => PAGE_LINKS[k] ? `<a href="${PAGE_LINKS[k]}" class="ev-mi">${esc(l)}</a>` : `<a href="#${k}" class="ev-mi" data-ev-sec="${k}"${T.sec === k ? ' aria-current="page"' : ''}>${esc(l)}${k === 'connectors' && stale ? '<i class="ev-dot risk" aria-label="A source needs you" role="img"></i>' : ''}${n[k] !== undefined && n[k] !== '' ? `<span class="ev-k">${typeof n[k] === 'number' ? n[k] : n[k]}</span>` : ''}</a>`).join('');
|
|
329
341
|
const label = (SECTIONS.find((x) => x[0] === T.sec) || SECTIONS[0])[1];
|
|
330
|
-
const all = T.sec === 'connectors' ? `<button type="button" class="ev-ib" data-ev-
|
|
331
|
-
|
|
342
|
+
const all = T.sec === 'connectors' ? `<button type="button" class="ev-ib" data-ev-info aria-expanded="${!!T.info}" aria-controls="evConInfo" aria-label="What PMOS reads and what it never does" title="What PMOS reads and what it never does">${svg(INFO_I, 18)}</button>`
|
|
343
|
+
+ `<button type="button" class="ev-ib" data-ev-readall aria-label="Read every source again" title="Read all again"${T.reading ? ' disabled' : ''}>${svg(RD, 18)}</button>` : '';
|
|
344
|
+
// the amber box sits on top of the other sections only: on Connectors the trouble row says it (say it once, CnA)
|
|
345
|
+
el.querySelector('.ev-st-main').innerHTML = (T.sec === 'connectors' ? '' : staleHtml()) + `<div class="ev-top"><h3 class="ev-h2" id="evStTitle">${esc(label)}</h3>${all}<button type="button" class="ev-ib" data-ev-close aria-label="Close settings" title="Close settings">${svg(ICON.close, 18)}</button></div>`
|
|
332
346
|
+ BODY[T.sec]() + `<p role="status"${T.msg && AD.moodOfText(T.msg) === 'honest' ? ' class="ev-say honest"' : ''}>${T.msg && AD.moodOfText(T.msg) === 'honest' ? AD.adler('honest', 24, { label: false }) : ''}${esc(T.msg)}</p>`;
|
|
333
347
|
if (disposeClaudeSetup) { disposeClaudeSetup(); disposeClaudeSetup = null; }
|
|
334
348
|
const setup = el.querySelector('[data-claude-setup]');
|
|
@@ -372,6 +386,8 @@ async function press(e) {
|
|
|
372
386
|
if ('evClose' in t.dataset) { closeSettings(); return; }
|
|
373
387
|
if (t.dataset.evWk) { const k = t.dataset.evWk; if (T.wkOpen.has(k)) T.wkOpen.delete(k); else T.wkOpen.add(k); draw(); return; }
|
|
374
388
|
if ('evArc' in t.dataset) { T.arc = !T.arc; draw(); return; }
|
|
389
|
+
if ('evInfo' in t.dataset) { T.info = !T.info; draw(); return; }
|
|
390
|
+
if ('evFold' in t.dataset) { T.fold = !T.fold; draw(); return; }
|
|
375
391
|
if (t.dataset.evMore) { T.wkMore.add(t.dataset.evMore); draw(); return; }
|
|
376
392
|
if (t.dataset.evGo) { T.sec = t.dataset.evGo; draw(); return; }
|
|
377
393
|
if (t.dataset.evSeg === 'theme') { const M = await import('./cozy-nav.js'); M.setTheme(t.dataset.v); draw(); return; }
|
|
@@ -480,7 +496,6 @@ async function waitRestart(from, log) {
|
|
|
480
496
|
}
|
|
481
497
|
/** GET /api/sources: when each source was last read. The place menu's amber dot follows (the event pm:sources). */
|
|
482
498
|
async function readFresh() {
|
|
483
|
-
if (!T.lines) T.lines = await import('./home-more.js').catch(() => null);
|
|
484
499
|
const r = await pmGet('/api/sources' + wsQ());
|
|
485
500
|
T.fresh = r && r.ok !== false ? r : { ok: false, error: (r && r.error) || 'the server did not answer' };
|
|
486
501
|
if (T.fresh.ok) window.dispatchEvent(new CustomEvent('pm:sources', { detail: T.fresh }));
|
|
@@ -525,7 +540,7 @@ export function openSettings(section) {
|
|
|
525
540
|
const el = mount();
|
|
526
541
|
if (section === 'dress') { T.sec = 'adler'; T.dress = true; } else if (section && BODY[section]) { T.sec = section; T.dress = false; }
|
|
527
542
|
else if (el.hidden) { T.sec = 'general'; T.dress = false; } // a fresh open with no section opens on General (R2), not on the section of the last visit
|
|
528
|
-
if (el.hidden) { const ae = document.activeElement; T.back = ae && ae !== document.body ? ae : null; T.ws = null; T.src = null; T.msg = ''; T.phase = ''; T.auto = false; T.wkOpen = new Set(); T.wkMore = new Set(); T.arc = false; T.fresh = null; T.note = {}; T.dset = {}; T.cl = null; T.gr = null; T.np = null; JL.reset(); }
|
|
543
|
+
if (el.hidden) { const ae = document.activeElement; T.back = ae && ae !== document.body ? ae : null; T.ws = null; T.src = null; T.msg = ''; T.phase = ''; T.auto = false; T.wkOpen = new Set(); T.wkMore = new Set(); T.arc = false; T.fresh = null; T.note = {}; T.info = false; T.fold = false; T.dset = {}; T.cl = null; T.gr = null; T.np = null; JL.reset(); }
|
|
529
544
|
el.hidden = false; document.getElementById('evStScrim').hidden = false; el.setAttribute('data-esc-layer', '');
|
|
530
545
|
draw();
|
|
531
546
|
const f = el.querySelector('[aria-current="page"]'); if (f) f.focus();
|
|
@@ -310,3 +310,43 @@ button.rp-cc { cursor: pointer; }
|
|
|
310
310
|
.rf-lk .rf-fix small { color: var(--cz-ink3); font-size: var(--fs-meta); line-height: var(--lh-meta); }
|
|
311
311
|
.rf-tplx { padding: 10px 12px; border: 1px solid var(--cz-hair); border-radius: 14px; background: var(--cz-bluebg); }
|
|
312
312
|
.rf-hv em.rf-rest { color: var(--cz-ink2); }
|
|
313
|
+
|
|
314
|
+
/* ── the orders of a draft (batch 27 item 16; canvas TpA, TpD, TpE; report-orders-ui.js) ── */
|
|
315
|
+
.ro-row { align-items: center; }
|
|
316
|
+
.ro-row .ro-ib.rp-tl { width: 30px; height: 30px; }
|
|
317
|
+
.ro-next { color: var(--cz-ink3); }
|
|
318
|
+
.ro-ass { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; padding: 10px 12px; border-radius: 14px; border: 1px solid var(--cz-blue); background: var(--cz-card); font-size: var(--fs-body); line-height: var(--lh-body); }
|
|
319
|
+
.ro-ass p { margin: 0; }
|
|
320
|
+
.ro-st { color: var(--cz-blueink); font-weight: var(--fw-strong); }
|
|
321
|
+
.vz .ro-note { margin: 0; font-size: var(--fs-meta); line-height: var(--lh-meta); color: var(--cz-ink3); }
|
|
322
|
+
.ro-pts { display: flex; flex-wrap: wrap; gap: 4px; margin: 0; }
|
|
323
|
+
.ro-pt { display: inline-flex; padding: 2px 8px; border-radius: 8px; border: 1px solid var(--cz-hair); background: var(--cz-well); color: var(--cz-ink2); font-size: var(--fs-badge); line-height: var(--lh-badge); }
|
|
324
|
+
.ro-pt.add, .ro-pt.on { border-color: var(--cz-blue); color: var(--cz-blueink); font-weight: var(--fw-strong); background: var(--cz-card); }
|
|
325
|
+
.ro-pt.cut { border-style: dashed; color: var(--cz-ink3); text-decoration: line-through; background: none; }
|
|
326
|
+
.ro-acts { display: flex; justify-content: flex-end; align-items: center; gap: 6px; }
|
|
327
|
+
.ro-acts .rp-pri { margin: 0; width: 36px; height: 36px; }
|
|
328
|
+
.ro-gal { display: flex; flex-direction: column; gap: 10px; }
|
|
329
|
+
.ro-gh { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
|
330
|
+
.ro-gh small { display: block; color: var(--cz-ink3); font-size: var(--fs-meta); line-height: var(--lh-meta); }
|
|
331
|
+
.ro-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 10px; }
|
|
332
|
+
.ro-card { display: flex; flex-direction: column; gap: 6px; text-align: left; padding: 10px; border-radius: 14px; border: 1px solid var(--cz-hair); background: var(--cz-card); color: var(--cz-ink); cursor: pointer; font: inherit; }
|
|
333
|
+
.ro-card:hover { background: var(--cz-well); }
|
|
334
|
+
.ro-card:focus-visible, .ro-sl:focus-visible { outline: 2px solid var(--cz-blue); outline-offset: 2px; }
|
|
335
|
+
.ro-card.now { border-color: var(--cz-blue); }
|
|
336
|
+
.ro-card small, .ro-sl small { color: var(--cz-ink3); font-size: var(--fs-meta); line-height: var(--lh-meta); }
|
|
337
|
+
.ro-cn { display: flex; align-items: center; gap: 6px; font-size: var(--fs-row); line-height: var(--lh-row); }
|
|
338
|
+
.ro-tag { display: inline-flex; padding: 1px 8px; border-radius: 999px; border: 1px solid var(--cz-hair); font-size: var(--fs-badge); line-height: var(--lh-badge); color: var(--cz-ink2); font-weight: var(--fw-strong); }
|
|
339
|
+
.ro-tag.ro-star { color: var(--cz-blueink); border-color: var(--cz-blue); }
|
|
340
|
+
.ro-pics { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 4px; }
|
|
341
|
+
.ro-pic { display: block; position: relative; aspect-ratio: 16 / 9; border-radius: 4px; overflow: hidden; background: var(--cz-well); box-shadow: inset 0 0 0 1px var(--cz-hair); }
|
|
342
|
+
.ro-slides { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
343
|
+
.ro-sl { display: flex; flex-direction: column; gap: 4px; width: 120px; padding: 0; border: 0; background: none; cursor: pointer; text-align: left; font: inherit; }
|
|
344
|
+
.ro-sl .ro-pics { grid-template-columns: 1fr; }
|
|
345
|
+
.ro-big { position: relative; aspect-ratio: 16 / 9; }
|
|
346
|
+
.ro-big .ro-pic { position: absolute; inset: 0; }
|
|
347
|
+
.ro-ren { display: flex; align-items: center; gap: 6px; }
|
|
348
|
+
.ro-ren input { flex: 1; min-width: 0; height: 34px; padding: 0 10px; border-radius: 10px; border: 1px solid var(--cz-hair); background: var(--cz-card); color: var(--cz-ink); font: inherit; }
|
|
349
|
+
.ro-ren .rp-pri { margin: 0; }
|
|
350
|
+
@media (max-width: 560px) { .ro-grid { grid-template-columns: 1fr; } }
|
|
351
|
+
.ro-ib.rp-pri { background: var(--cz-blue); color: var(--cz-onsolid); border-radius: 999px; }
|
|
352
|
+
.ro-ib.rp-pri:hover { background: var(--cz-blue); filter: brightness(1.08); color: var(--cz-onsolid); }
|