create-pmos 0.154.0 → 0.156.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/README.md +18 -1
- package/bin/cockpit-install.js +10 -5
- package/install.ps1 +171 -0
- package/install.sh +184 -0
- package/package.json +3 -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/bin/pmos.js +1 -0
- package/workspace/doctor.js +4 -2
- package/workspace/guide/document-sheet.md +18 -3
- package/workspace/guide/launcher.md +2 -2
- package/workspace/lib/launcher-cli.js +2 -2
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-demo-boards.js +4 -3
- package/workspace/public/pm/c3-demo-init.js +16 -0
- package/workspace/public/pm/c3-doc-sheet.js +328 -21
- package/workspace/public/pm/c3-docs.js +2 -1
- package/workspace/public/pm/c3-ib-docs.js +3 -1
- package/workspace/public/pm/c3-settings.js +1 -1
- package/workspace/public/pm/console3.css +83 -0
- package/workspace/public/pm/frame-copy.js +1 -1
- package/workspace/server.js +3 -3
- package/workspace/test-lists/c3-doc-sheet-v2.json +6 -0
|
@@ -316,6 +316,22 @@ POST['/api/doc-sheet/reject'] = (b) => {
|
|
|
316
316
|
};
|
|
317
317
|
POST['/api/doc-sheet/why'] = (b) => { const R = revsOf(b.path); const r = R && R.find((x) => x.rev === Number(b.rev)); if (!r) return { ok: false, error: 'no such revision' }; if (!String(b.why || '').trim()) return { ok: false, error: 'a why is required' }; r.why_added = b.why.trim(); r.why_added_at = '2026-10-08T08:12:00.000Z'; return { ok: true, rev: r.rev }; };
|
|
318
318
|
POST['/api/doc-sheet/mark-current'] = (b) => { if (!String(b.why || '').trim()) return { ok: false, error: 'a why is required' }; DEMO_EVENTS.push({ path: b.path, kind: 'marked', at: '2026-10-08T08:12:00.000Z', why: b.why.trim(), by: 'pm' }); return { ok: true, at: '2026-10-08T08:12:00.000Z' }; };
|
|
319
|
+
/* the copies in other tools (GET /api/docs, lib/doc-sync.js signals; initiative c3-doc-sheet-v2): Checkout v2 PRD has a Confluence copy that is behind
|
|
320
|
+
* and a Google Doc that PMOS is ahead of; the drop-off analysis has a Confluence copy in step. Check marks them read now; sync and link are kept in memory. */
|
|
321
|
+
const pOf = (id) => DFILES.find((f) => f.id === id).path;
|
|
322
|
+
const COPIES = {
|
|
323
|
+
[pOf('p1')]: [{ kind: 'confluence', label: 'Confluence', target: 'acme.atlassian.net/wiki/spaces/PM/pages/48213/Checkout+v2+PRD', url: null, title: 'Checkout v2 PRD', signal: 'behind', basis: 'text', words: 'Confluence has a change PMOS does not have: Dana edited the Scope section on Thu 8 Oct.', checked_at: '2026-10-08T07:40:00.000Z', checked_by: 'agent', remote_modified_at: '2026-10-08T06:55:00.000Z', remote_modified_by: 'Dana Ruiz', linked_at: '2026-09-25T10:00:00.000Z', syncing: null, sync_failed: null },
|
|
324
|
+
{ kind: 'google', label: 'Google Docs', target: 'docs.google.com/document/d/1Xc2Checkout', url: null, title: 'Checkout v2 PRD (shared with Design)', signal: 'ahead', basis: 'text', words: 'PMOS has changes the Google Doc does not have: the done criteria and the audit risk.', checked_at: '2026-10-08T07:40:00.000Z', checked_by: 'agent', remote_modified_at: '2026-10-05T16:20:00.000Z', remote_modified_by: 'Wawan', linked_at: '2026-10-01T09:00:00.000Z', syncing: null, sync_failed: null }],
|
|
325
|
+
[pOf('u1')]: [{ kind: 'confluence', label: 'Confluence', target: 'acme.atlassian.net/wiki/spaces/PM/pages/48390', url: null, title: 'Address drop-off analysis', signal: 'same', basis: 'text', words: 'Both have the same text.', checked_at: '2026-10-08T07:40:00.000Z', checked_by: 'agent', remote_modified_at: '2026-10-06T11:00:00.000Z', remote_modified_by: 'Wawan', linked_at: '2026-10-06T11:05:00.000Z', syncing: null, sync_failed: null }] };
|
|
326
|
+
GET['/api/docs'] = () => ({ ok: true, docs: Object.entries(COPIES).map(([path, links]) => ({ path, title: (DFILES.find((f) => f.path === path) || {}).title || path, links: links.map((l) => ({ ...l })), attention: links.some((l) => l.signal !== 'same') })), summary: {}, open: null, session: null, connectors: {} });
|
|
327
|
+
POST['/api/docs/check'] = () => { for (const L of Object.values(COPIES)) for (const l of L) { l.checked_at = '2026-10-08T08:12:00.000Z'; l.checked_by = 'agent'; } return { ok: true, session: 'demo-check' }; };
|
|
328
|
+
POST['/api/docs/sync'] = (b) => { const l = (COPIES[b.path] || []).find((x) => x.kind === b.kind); if (!l) return { ok: false, error: b.path + ' has no such copy' }; if (!['pmos', 'copy'].includes(b.use)) return { ok: false, error: 'say which version to use: pmos or copy' }; l.syncing = { run: null, use: b.use, started_at: '2026-10-08T08:12:00.000Z' }; return { ok: true, session: 'demo-sync' }; };
|
|
329
|
+
POST['/api/docs/link'] = (b) => {
|
|
330
|
+
if (!['google', 'confluence'].includes(b.kind) || !String(b.target || '').trim()) return { ok: false, error: 'not a Google Docs or Confluence address' };
|
|
331
|
+
const L = COPIES[b.path] || (COPIES[b.path] = []); if (L.some((x) => x.kind === b.kind)) return { ok: false, error: 'that document already has a ' + b.kind + ' copy' };
|
|
332
|
+
L.push({ kind: b.kind, label: b.kind === 'google' ? 'Google Docs' : 'Confluence', target: b.target, url: null, title: '', signal: 'unchecked', basis: null, words: 'No check has read this copy yet.', checked_at: null, checked_by: null, remote_modified_at: null, remote_modified_by: null, linked_at: '2026-10-08T08:12:00.000Z', syncing: null, sync_failed: null });
|
|
333
|
+
return { ok: true };
|
|
334
|
+
};
|
|
319
335
|
POST['/api/doc-sheet/change-note'] = (b) => ({ ok: true, note: 'A change note for the owners of ' + (b.downstream || []).join(', ') });
|
|
320
336
|
|
|
321
337
|
/* health: the six checks (the mockup's docHealth, line 4059). Checkout v2 PRD is recomputed from its text; the others answer the fixed N of M. */
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/* pm/c3-doc-sheet.js — the DOCUMENT SHEET of /c3.html (Initiatives › Documents, an initiative's Docs, Reports). The right-hand sheet from
|
|
2
2
|
* c3-sheet.js, up to 780px, a bottom sheet on a phone. Initiative c3-doc-sheet-rich: the approved mockup (planning/prd/c3-doc-sheet-rich-prd.md).
|
|
3
3
|
*
|
|
4
|
-
* openDocSheet({ ws, doc, mode, prompt, relations(doc, openOther), onChanged, generated, secExtra }) doc = { path, name, ty, v, st, ini, iniName }
|
|
4
|
+
* openDocSheet({ ws, doc, mode, prompt, relations(doc, openOther), onChanged, generated, secExtra, library(), openGraph(doc) }) doc = { path, name, ty, v, st, ini, iniName }
|
|
5
5
|
*
|
|
6
6
|
* Six modes (the segment under the header; the keys are kept for the callers):
|
|
7
7
|
* read Read the markdown rendered (pm/c3-md.js): the front matter as properties, each section with who changed it last (from the
|
|
8
|
-
* revisions), "Show who wrote what", the Markdown source with a copy button,
|
|
9
|
-
*
|
|
8
|
+
* revisions), "Show who wrote what", the Markdown source with a copy button, the stale banner, and the open proposals
|
|
9
|
+
* pinned to the sections they change, with "Show change" for the diff in place. The OUTLINE (v2) beside it: the headings
|
|
10
|
+
* nested by level, the section in view marked as the page scrolls, groups that fold, a filter past 12 headings, a dot on
|
|
11
|
+
* stale and proposal sections; Hide is kept in this browser (localStorage); under 900px it is a button over the text.
|
|
10
12
|
* edit Edit a rich-text editor of the whole document (toolbar, Rich | Markdown over the same draft), the front matter as fields; a
|
|
11
13
|
* changed section is marked; saving needs a reason (POST /api/doc-sheet/edit). A section nobody touched is written back
|
|
12
14
|
* exactly as it was, so opening and saving without a change writes nothing.
|
|
@@ -18,13 +20,20 @@
|
|
|
18
20
|
* change that has none (POST /api/doc-sheet/why).
|
|
19
21
|
* health Health POST /api/doc-health/check: the checks, N of M; a failing Fresh check lists the sources that changed, and "Mark as still
|
|
20
22
|
* current" records a reason (POST /api/doc-sheet/mark-current).
|
|
21
|
-
* links Relations the
|
|
23
|
+
* links Relations the graph around this document (v2): its sources on the left, its initiative and goals on top, the documents that
|
|
24
|
+
* use it and its copies on the right, a drift link dashed; a node opens that document. It reads `library()` from the
|
|
25
|
+
* caller, else GET /api/docs/library; the caller's `relations` list, when given, is kept below the graph.
|
|
26
|
+
* COPIES (v2): GET /api/docs gives the document's Confluence and Google Doc links with their signal (lib/doc-sync.js): a chip per copy in
|
|
27
|
+
* the header, and the "Copies in other tools" panel: Check now (POST /api/docs/check), Open in the tool, Bring their change in as a
|
|
28
|
+
* proposal (the ask run reads the copy; PMOS keeps hashes, not the copy's text, so the proposal is the difference), Update the copy to
|
|
29
|
+
* this version (five seconds to undo, then POST /api/docs/sync with use: pmos), and Link a copy (POST /api/docs/link).
|
|
22
30
|
* Nothing is invented: a part with no data says so. The demo (?demo=1) answers the routes from fixtures.
|
|
23
31
|
*
|
|
24
32
|
* Two options for a report, which is a document with no stored file yet (report-families-view.js):
|
|
25
33
|
* generated = { text, why, health } Read shows the generated text; Edit and Proposals say why they need a stored document; Health shows `health`.
|
|
26
34
|
* secExtra(name) a node under each section in Read: the report's claims and where they come from.
|
|
27
|
-
* Pure parts (Node-testable): splitDoc, joinDoc, changedSections, wordDiffTokens, byName, dayName, parseFront, setFront, composeSections
|
|
35
|
+
* Pure parts (Node-testable): splitDoc, joinDoc, changedSections, wordDiffTokens, byName, dayName, parseFront, setFront, composeSections,
|
|
36
|
+
* copyKindOf, citesIn, outlineOf, hoodOf. */
|
|
28
37
|
import { el } from './dom.js';
|
|
29
38
|
import { pmGet, pmPost } from './http.js';
|
|
30
39
|
import { c3ic } from './pm-work-view.js';
|
|
@@ -183,10 +192,63 @@ const md = (text, extra) => el('div', { class: 'ds-md' + (extra ? ' ' + extra :
|
|
|
183
192
|
const btn = (cls, attrs, kids, onClick) => c3btn(cls, attrs, kids, onClick);
|
|
184
193
|
const headLevel = (s) => (/^(#+)/.exec(s.prefix || '') || ['', ''])[1].length;
|
|
185
194
|
|
|
186
|
-
|
|
195
|
+
/* ── v2 (c3-doc-sheet-v2): the copies in other tools, the outline, the graph ── */
|
|
196
|
+
/* a copy's state (lib/doc-sync.js signal): [label, icon, class, sentence, chip word] */
|
|
197
|
+
const SIG = {
|
|
198
|
+
same: ['In step', 'check', 'ds-good', 'Both have the same text.', 'in step'],
|
|
199
|
+
ahead: ['PMOS is ahead', 'up', 'ds-acc', 'PMOS has changes the copy does not have.', 'PMOS ahead'],
|
|
200
|
+
behind: ['Behind', 'alert', 'ds-warn', 'The copy has changes PMOS does not have.', 'behind'],
|
|
201
|
+
diverged: ['Both changed', 'alert', 'ds-bad', 'Both changed since they last matched.', 'both changed'],
|
|
202
|
+
unchecked: ['Not checked', 'info', '', 'No check has read this copy yet.', 'not checked'],
|
|
203
|
+
unreadable: ['Cannot open', 'alert', 'ds-bad', 'The last check could not open the copy.', 'cannot open'],
|
|
204
|
+
'missing-remote': ['Not found', 'alert', 'ds-bad', 'The copy is not there any more.', 'not found'],
|
|
205
|
+
'missing-repo': ['Not in PMOS', 'alert', 'ds-bad', 'This file is not in the repository any more.', 'not in PMOS'] };
|
|
206
|
+
const SYNCING = ['Updating', 'clock', 'ds-acc', 'A session is updating this copy.', 'updating'];
|
|
207
|
+
const KLABEL = { google: 'Google Doc', confluence: 'Confluence' };
|
|
208
|
+
const sigOf = (l) => (l.syncing ? SYNCING : SIG[l.signal] || SIG.unchecked);
|
|
209
|
+
const labelOf = (l) => KLABEL[l.kind] || l.label || l.kind;
|
|
210
|
+
/** Which tool a pasted address names (the server checks it again): 'confluence', 'google' or null. */
|
|
211
|
+
export function copyKindOf(target) {
|
|
212
|
+
const t = String(target || '');
|
|
213
|
+
if (/atlassian|confluence|\/wiki\/|pageId=/i.test(t)) return 'confluence';
|
|
214
|
+
if (/google|docs\.|drive\./i.test(t)) return 'google';
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
/** The names of the sections that cite a path (or its file name). */
|
|
218
|
+
export function citesIn(secs, p) {
|
|
219
|
+
const b = String(p).split('/').pop();
|
|
220
|
+
return secs.filter((s) => s.name !== 'Front matter' && (s.text.includes(p) || (b && s.text.includes(b)))).map((s) => s.name);
|
|
221
|
+
}
|
|
222
|
+
/** The outline: the named sections, nested from the top level found (1 to 3). A single title heading on top is left out. */
|
|
223
|
+
export function outlineOf(secs) {
|
|
224
|
+
let named = secs.filter((s) => s.prefix && headLevel(s) > 0);
|
|
225
|
+
if (named.length && headLevel(named[0]) === 1 && named.filter((s) => headLevel(s) === 1).length === 1) named = named.slice(1);
|
|
226
|
+
if (!named.length) return [];
|
|
227
|
+
const base = Math.min(...named.map(headLevel));
|
|
228
|
+
return named.map((s) => ({ s, lv: Math.min(3, headLevel(s) - base + 1) }));
|
|
229
|
+
}
|
|
230
|
+
/** The neighbourhood of a document in a library response: { me, src, out } with each edge's state (the Docs page's rule when it has none). */
|
|
231
|
+
export function hoodOf(lib, path) {
|
|
232
|
+
if (!lib || !Array.isArray(lib.files)) return null;
|
|
233
|
+
const me = lib.files.findIndex((f) => f.path === path);
|
|
234
|
+
const kinds = lib.kinds || []; const src = []; const out = [];
|
|
235
|
+
const st = (a, b, k, s) => s || (['link', 'cites', 'anchor'].includes(kinds[k]) && a.date && b.date && a.date > b.date ? 'drift' : 'ok');
|
|
236
|
+
if (me >= 0) for (const [a, b, k, s] of lib.edges || []) {
|
|
237
|
+
if (b === me && a !== me && lib.files[a]) src.push({ f: lib.files[a], kind: kinds[k] || '', st: st(lib.files[a], lib.files[me], k, s) });
|
|
238
|
+
else if (a === me && b !== me && lib.files[b]) out.push({ f: lib.files[b], kind: kinds[k] || '', st: st(lib.files[me], lib.files[b], k, s) });
|
|
239
|
+
}
|
|
240
|
+
return { me: me >= 0 ? lib.files[me] : null, src, out };
|
|
241
|
+
}
|
|
242
|
+
const OL_KEY = 'pmos.c3.docOutline';
|
|
243
|
+
const olGet = () => { try { return localStorage.getItem(OL_KEY) !== 'off'; } catch { return true; } };
|
|
244
|
+
const olSet = (on) => { try { localStorage.setItem(OL_KEY, on ? 'on' : 'off'); } catch { /* no storage here */ } };
|
|
245
|
+
const ESTATE = { ok: 'in step', drift: 'out of step', conflict: 'conflict', missing: 'missing', ahead: 'PMOS ahead' };
|
|
246
|
+
|
|
247
|
+
export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, onChanged, generated, secExtra, library, openGraph }) {
|
|
187
248
|
const S = { mode: MODE[mode] || mode, prompt, text: null, err: '', revs: null, texts: {}, health: null, hAsk: false, busy: false, changed: false,
|
|
188
249
|
meta: null, metaFor: null, props: null, propsErr: '', log: null, logErr: '', asking: null, closed: false, who: false, src: false, preview: {},
|
|
189
|
-
pick: {}, rejFor: null, rejWhy: '', whyFor: null, whyText: '', staleWhy: null, logF: 'all', cmp: null, cmpNode: null, flashRev: null, ed: null, leave: null
|
|
250
|
+
pick: {}, rejFor: null, rejWhy: '', whyFor: null, whyText: '', staleWhy: null, logF: 'all', cmp: null, cmpNode: null, flashRev: null, ed: null, leave: null,
|
|
251
|
+
copies: null, copiesErr: '', syncOpen: false, linking: false, linkUrl: '', pend: {}, checkRun: null, lib: undefined, ol: olGet(), olOpen: false, olq: '', olShut: {}, cur: null };
|
|
190
252
|
const demo = isDemo();
|
|
191
253
|
const base = { ws, path: doc.path };
|
|
192
254
|
let ui = null;
|
|
@@ -237,7 +299,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
237
299
|
const reloadAll = async () => {
|
|
238
300
|
S.revs = null; S.meta = null; S.metaFor = null; S.texts = {}; S.health = null; S.hAsk = false; S.log = null;
|
|
239
301
|
await loadText(); draw();
|
|
240
|
-
await Promise.all([loadRevs(), loadProps(), loadHealth()]); loadMeta(); if (S.mode === 'hist') { await loadLog(); } draw();
|
|
302
|
+
await Promise.all([loadRevs(), loadProps(), loadHealth(), loadCopies()]); loadMeta(); if (S.mode === 'hist') { await loadLog(); } draw();
|
|
241
303
|
};
|
|
242
304
|
|
|
243
305
|
/* ── drawing helpers ── */
|
|
@@ -266,7 +328,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
266
328
|
return el('div', { class: 'chips2 ds-meta', 'data-doc-meta': '' },
|
|
267
329
|
el('span', { class: 'chip2 ' + st[1] }, c3ic(st[2], 'sm'), st[0]), el('span', { class: 'chip2' }, doc.ty || 'Document'), doc.v ? el('span', { class: 'chip2 mono' }, doc.v) : null,
|
|
268
330
|
h && h.total ? btn('chip2 lk ' + hcls(h), { 'data-doc-hchip': '' }, [c3ic('shield', 'sm'), h.passed + ' of ' + h.total + ' checks'], () => go('health')) : null,
|
|
269
|
-
el('span', { class: 'chip2' }, c3ic('init', 'sm'), doc.iniName || doc.ini || 'Portfolio'),
|
|
331
|
+
el('span', { class: 'chip2' }, c3ic('init', 'sm'), doc.iniName || doc.ini || 'Portfolio'), copyChips(),
|
|
270
332
|
last && !generated ? btn('chip2 lk', { 'data-doc-last': '' }, [c3ic(isAgentRev(last) ? 'spark' : 'user', 'sm'), 'Last change: ', el('b', null, whoOf(last) + ','), ' ' + dayName(last.at)], () => go('hist')) : null);
|
|
271
333
|
}
|
|
272
334
|
function seg() {
|
|
@@ -285,6 +347,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
285
347
|
if (m === 'hist') { S.cmp = null; loadLog().then(() => { draw(); if (S.flashRev != null) { goEl('[data-log-rev="' + S.flashRev + '"]'); S.flashRev = null; } }); }
|
|
286
348
|
if (m === 'ask' && S.props === null) loadProps().then(draw);
|
|
287
349
|
if (m === 'read') loadMeta();
|
|
350
|
+
if (m === 'links') loadLib();
|
|
288
351
|
draw();
|
|
289
352
|
const sb = ui && ui.body.closest('.sb'); if (sb) sb.scrollTop = 0;
|
|
290
353
|
}
|
|
@@ -294,7 +357,7 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
294
357
|
const f = fresh(); if (!f || generated) return null;
|
|
295
358
|
const secs = S.text ? splitDoc(S.text) : [];
|
|
296
359
|
const srcs = Array.isArray(f.sources) ? f.sources : [];
|
|
297
|
-
const cites = (p) =>
|
|
360
|
+
const cites = (p) => citesIn(secs, p);
|
|
298
361
|
const op = openProps();
|
|
299
362
|
if (S.staleWhy !== null) {
|
|
300
363
|
const inp = el('input', { type: 'text', id: 'dstalewhy', 'data-doc-stalewhy': '', placeholder: 'For example: the launch date is not agreed yet', 'aria-label': 'Why it is still current', value: S.staleWhy, onInput: (e) => { S.staleWhy = e.target.value; } });
|
|
@@ -318,6 +381,243 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
318
381
|
await loadHealth(); draw(); say('Marked as current. The reason is in the edit log.');
|
|
319
382
|
}
|
|
320
383
|
|
|
384
|
+
/* ── v2: the copies in other tools (GET /api/docs: the links and their signals, lib/doc-sync.js) ── */
|
|
385
|
+
const loadCopies = async () => {
|
|
386
|
+
if (generated) { S.copies = []; return; }
|
|
387
|
+
const j = await pmGet('/api/docs?' + q({ ws })).catch(() => null);
|
|
388
|
+
if (!j || !j.ok) { S.copies = S.copies || []; S.copiesErr = (j && j.error) || 'The copies could not be read.'; return; }
|
|
389
|
+
const d = (j.docs || []).find((x) => x.path === doc.path);
|
|
390
|
+
S.copies = d ? d.links || [] : []; S.copiesErr = ''; S.checkRun = j.session || j.open || null;
|
|
391
|
+
};
|
|
392
|
+
const copyChips = () => {
|
|
393
|
+
if (generated || S.copies == null) return null;
|
|
394
|
+
if (!S.copies.length) return btn('chip2 lk', { 'data-doc-copychip': '', 'aria-expanded': String(S.syncOpen) }, [c3ic('link', 'sm'), 'Link a Confluence or Google Doc copy'], () => toggleCopies());
|
|
395
|
+
return S.copies.map((l) => { const g = sigOf(l); return btn('chip2 lk ' + g[2], { 'data-doc-copychip': l.kind, 'aria-expanded': String(S.syncOpen), title: l.words || g[3] }, [c3ic(g[1], 'sm'), labelOf(l) + ': ' + g[4]], () => toggleCopies()); });
|
|
396
|
+
};
|
|
397
|
+
function toggleCopies() {
|
|
398
|
+
S.syncOpen = !S.syncOpen;
|
|
399
|
+
if (S.syncOpen && S.mode === 'edit') { go('read'); return; }
|
|
400
|
+
draw(); const sb = ui && ui.body.closest('.sb'); if (sb && S.syncOpen) sb.scrollTop = 0;
|
|
401
|
+
}
|
|
402
|
+
function copiesPanel() {
|
|
403
|
+
if (!S.syncOpen || generated) return null;
|
|
404
|
+
const L = S.copies || [];
|
|
405
|
+
const rows = L.map((l) => {
|
|
406
|
+
const g = sigOf(l); const name = labelOf(l); const pend = S.pend[l.kind];
|
|
407
|
+
const acts = [btn('ds-btn', { 'data-copy-check': l.kind, disabled: !!S.checkRun, 'aria-disabled': S.checkRun ? 'true' : null }, [c3ic(S.checkRun ? 'clock' : 'undo', 'sm'), S.checkRun ? 'Checking' : 'Check now'], () => checkNow()),
|
|
408
|
+
l.url ? el('a', { class: 'ds-btn', href: l.url, target: '_blank', rel: 'noopener', 'data-copy-open': l.kind }, c3ic('out', 'sm'), 'Open in ' + name) : null];
|
|
409
|
+
if (!l.syncing && (l.signal === 'behind' || l.signal === 'diverged')) acts.push(btn('ds-btn ag', { 'data-copy-take': l.kind }, [c3ic('spark', 'sm'), 'Bring their change in as a proposal'], () => takeCopy(l)));
|
|
410
|
+
if (!l.syncing && l.signal === 'ahead') acts.push(pend ? btn('ds-btn', { 'data-copy-undo': l.kind }, [c3ic('undo', 'sm'), 'Undo the update'], () => undoPush(l))
|
|
411
|
+
: btn('ds-btn pri', { 'data-copy-push': l.kind }, [c3ic('up', 'sm'), 'Update the ' + name + ' to this version'], () => pushCopy(l)));
|
|
412
|
+
const changed = l.remote_modified_at ? 'changed ' + whenOf(l.remote_modified_at) + (l.remote_modified_by ? ' by ' + l.remote_modified_by : '') : '';
|
|
413
|
+
return el('li', { 'data-copy': l.kind },
|
|
414
|
+
el('div', { class: 'l1' }, c3ic('doc', 'sm'), el('b', null, l.title || name), el('span', { class: 'tag ' + g[2], 'data-copy-state': l.syncing ? 'syncing' : l.signal || 'unchecked' }, c3ic(g[1], 'sm'), g[0])),
|
|
415
|
+
el('div', { class: 'ds-mono' }, [name, l.target, changed].filter(Boolean).join(' · ')),
|
|
416
|
+
el('p', null, l.words || g[3]),
|
|
417
|
+
el('div', { class: 'ds-note' }, l.checked_at ? 'Last checked ' + whenOf(l.checked_at) + (l.checked_by ? ' by ' + byName(l.checked_by) : '') + '.' : 'Not checked yet.'),
|
|
418
|
+
l.syncing ? el('div', { class: 'fold', role: 'status', 'data-copy-syncing': l.kind }, c3ic('clock', 'sm'), 'A session is updating ' + (l.syncing.use === 'copy' ? 'PMOS from the ' + name : 'the ' + name + ' to this version') + (l.syncing.started_at ? '. It started ' + whenOf(l.syncing.started_at) + '.' : '.')) : null,
|
|
419
|
+
l.sync_failed ? el('div', { class: 'fold w', role: 'alert', 'data-copy-failed': l.kind }, c3ic('alert', 'sm'), 'The last update did not finish' + (l.sync_failed.reason ? ': ' + l.sync_failed.reason : '.')) : null,
|
|
420
|
+
pend ? el('div', { class: 'fold', role: 'status', 'data-copy-pending': l.kind }, c3ic('clock', 'sm'), 'The update starts in five seconds. Undo stops it.') : null,
|
|
421
|
+
el('div', { class: 'ds-acts' }, acts));
|
|
422
|
+
});
|
|
423
|
+
const linkRow = S.linking ? el('div', { class: 'ds-whyadd ds-linkadd' },
|
|
424
|
+
el('input', { type: 'text', id: 'dcopyurl', 'data-copy-url': '', placeholder: 'Paste a Confluence page or Google Doc link', 'aria-label': 'Address of the copy', value: S.linkUrl, onInput: (e) => { S.linkUrl = e.target.value; } }),
|
|
425
|
+
btn('ds-btn pri', { 'data-copy-linkok': '' }, 'Link', () => linkCopy()), btn('ds-btn', {}, 'Cancel', () => { S.linking = false; draw(); })) : null;
|
|
426
|
+
return el('section', { class: 'ds-sync', 'aria-label': 'Copies in other tools', 'data-doc-copies': '' },
|
|
427
|
+
el('div', { class: 'hd' }, el('b', null, 'Copies in other tools'), el('span', { class: 'ds-acts' },
|
|
428
|
+
btn('ds-btn', { 'data-copy-link': '' }, [c3ic('link', 'sm'), L.length ? 'Link another copy' : 'Link a copy'], () => { S.linking = true; S.linkUrl = ''; draw(); const i = ui.body.querySelector('#dcopyurl'); if (i) i.focus(); }),
|
|
429
|
+
btn('ds-btn', { 'aria-label': 'Close the copies', 'data-copy-close': '' }, c3ic('x', 'sm'), () => { S.syncOpen = false; draw(); }))),
|
|
430
|
+
S.copiesErr ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), S.copiesErr) : null, linkRow,
|
|
431
|
+
L.length ? el('ul', null, rows) : el('p', { class: 'ds-note', 'data-copy-none': '' }, 'This document has no copy in Confluence or Google Docs yet. Link one to see whether it is in step.'),
|
|
432
|
+
el('p', { class: 'ds-note' }, 'PMOS never overwrites this document from a copy: a change in the copy comes in as a proposal you accept by section. Updating a copy starts one agent session, and you have five seconds to undo.'));
|
|
433
|
+
}
|
|
434
|
+
async function checkNow() {
|
|
435
|
+
const r = await pmPost('/api/docs/check', { ws }).catch(() => null);
|
|
436
|
+
if (!r || !r.ok) { say((r && r.error) || 'The check did not start.'); return; }
|
|
437
|
+
S.checkRun = { id: r.session }; draw();
|
|
438
|
+
say('Checking the copies. An agent session reads them with your connectors; the states change when it finishes.');
|
|
439
|
+
for (let i = 0; i < 120 && !S.closed && S.checkRun; i++) {
|
|
440
|
+
await new Promise((ok) => setTimeout(ok, 5000));
|
|
441
|
+
if (S.closed) return;
|
|
442
|
+
await loadCopies(); drawBg();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/* a change in the copy comes in as a proposal: the read-only ask run (#637) reads the copy and drafts it by section */
|
|
446
|
+
function takeCopy(l) {
|
|
447
|
+
S.prompt = 'Read the ' + labelOf(l) + ' copy of this document (' + l.target + ') and propose its changes here, section by section. Keep the PMOS text where only PMOS changed.';
|
|
448
|
+
S.syncOpen = false; go('ask');
|
|
449
|
+
if (demo) askDemo(); else askReal();
|
|
450
|
+
}
|
|
451
|
+
function pushCopy(l) {
|
|
452
|
+
const name = labelOf(l);
|
|
453
|
+
S.pend[l.kind] = setTimeout(async () => {
|
|
454
|
+
delete S.pend[l.kind];
|
|
455
|
+
const r = await pmPost('/api/docs/sync', { ws, path: doc.path, kind: l.kind, use: 'pmos' }).catch(() => null);
|
|
456
|
+
if (!r || !r.ok) say((r && r.error) || 'The update did not start.');
|
|
457
|
+
else say('Updating the ' + name + '. A session writes it with your connectors.');
|
|
458
|
+
await loadCopies(); drawBg();
|
|
459
|
+
}, 5000);
|
|
460
|
+
draw();
|
|
461
|
+
}
|
|
462
|
+
function undoPush(l) { clearTimeout(S.pend[l.kind]); delete S.pend[l.kind]; draw(); say('Stopped. The ' + labelOf(l) + ' is not updated.'); }
|
|
463
|
+
async function linkCopy() {
|
|
464
|
+
const t = String(S.linkUrl || '').trim(); const kind = copyKindOf(t);
|
|
465
|
+
if (!kind) { say('Paste a Confluence page or Google Doc link.'); const i = ui.body.querySelector('#dcopyurl'); if (i) i.focus(); return; }
|
|
466
|
+
const r = await pmPost('/api/docs/link', { ws, path: doc.path, kind, target: t }).catch(() => null);
|
|
467
|
+
if (!r || !r.ok) { say((r && r.error) || 'The copy was not linked.'); return; }
|
|
468
|
+
S.linking = false; S.linkUrl = ''; await loadCopies(); draw(); say('Linked the ' + KLABEL[kind] + ' copy. Check now reads it.');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* ── v2: the outline (Read) ── */
|
|
472
|
+
const staleNames = () => {
|
|
473
|
+
const f = fresh(); if (!f || !S.text) return new Set();
|
|
474
|
+
const secs = splitDoc(S.text); const out = new Set();
|
|
475
|
+
(Array.isArray(f.sources) ? f.sources : []).forEach((x) => citesIn(secs, x.path).forEach((n) => out.add(n)));
|
|
476
|
+
return out;
|
|
477
|
+
};
|
|
478
|
+
function outlineNav(items) {
|
|
479
|
+
const qq = S.olq.trim().toLowerCase(); const stale = staleNames(); const prop = new Set(openProps().flatMap((p) => p.sections.map((c) => c.name)));
|
|
480
|
+
const rows = []; let hideUnder = null;
|
|
481
|
+
items.forEach((it, k) => {
|
|
482
|
+
const { s, lv } = it;
|
|
483
|
+
if (hideUnder !== null && lv > hideUnder) return;
|
|
484
|
+
hideUnder = null;
|
|
485
|
+
if (qq && !s.name.toLowerCase().includes(qq)) return;
|
|
486
|
+
let n = 0; for (let j = k + 1; j < items.length && items[j].lv > lv; j++) n++;
|
|
487
|
+
const shut = !qq && !!S.olShut[s.i]; if (shut) hideUnder = lv;
|
|
488
|
+
const on = S.cur === s.i;
|
|
489
|
+
rows.push(el('li', { class: 'l' + lv + (on ? ' on' : ''), 'data-ol-i': String(s.i) },
|
|
490
|
+
n && !qq ? btn('tw', { 'aria-label': (shut ? 'Show' : 'Hide') + ' the ' + plural(n, 'heading') + ' under ' + s.name, 'aria-expanded': String(!shut), 'data-ol-fold': String(s.i) }, c3ic('next', 'sm'), () => { S.olShut[s.i] = !shut; redrawOl(); }) : el('span', { class: 'tw' }),
|
|
491
|
+
btn('go', { 'data-ol-go': String(s.i), 'aria-current': on ? 'location' : null }, [el('span', null, s.name),
|
|
492
|
+
stale.has(s.name) ? el('i', { class: 'mdot w', title: 'A source changed after this section', 'data-ol-stale': '' }) : null,
|
|
493
|
+
prop.has(s.name) ? el('i', { class: 'mdot p', title: 'The agent proposes a change here', 'data-ol-prop': '' }) : null], () => jumpTo(s.i))));
|
|
494
|
+
});
|
|
495
|
+
return el('nav', { class: 'ds-ol', 'aria-label': 'Outline', 'data-doc-outline': '' },
|
|
496
|
+
el('div', { class: 'hd' }, el('b', null, 'Outline'), el('span', { class: 'ds-mono' }, plural(items.length, 'heading'))),
|
|
497
|
+
items.length > 12 ? el('input', { type: 'text', id: 'dolq', 'data-ol-find': '', placeholder: 'Find a heading', 'aria-label': 'Find a heading', value: S.olq, onInput: (e) => { S.olq = e.target.value; redrawOl(); } }) : null,
|
|
498
|
+
el('ul', null, rows.length ? rows : el('li', { class: 'none ds-note' }, 'No heading matches.')),
|
|
499
|
+
stale.size || prop.size ? el('div', { class: 'key ds-note' }, stale.size ? el('span', null, el('i', { class: 'mdot w' }), 'stale') : null, prop.size ? el('span', null, el('i', { class: 'mdot p' }), 'proposal') : null) : null);
|
|
500
|
+
}
|
|
501
|
+
function redrawOl() {
|
|
502
|
+
const n = ui && ui.body.querySelector('.ds-ol'); if (!n || S.text == null) return;
|
|
503
|
+
const f = document.activeElement && document.activeElement.id === 'dolq' ? document.activeElement.selectionStart : null;
|
|
504
|
+
n.replaceWith(outlineNav(outlineOf(splitDoc(S.text)))); placeOl();
|
|
505
|
+
if (f != null) { const i = ui.body.querySelector('#dolq'); if (i) { i.focus(); try { i.setSelectionRange(f, f); } catch { /* not a text field */ } } }
|
|
506
|
+
}
|
|
507
|
+
/* where the sticky header ends: a section scrolled to lands below it, and the outline sticks under it */
|
|
508
|
+
/* (a sticky element sticks inside the scroller's padding: its edge from the scroller's top adds the padding) */
|
|
509
|
+
const stickBottom = () => { const t = ui && ui.body.querySelector('.ds-top'); return t ? (parseFloat(getComputedStyle(t).top) || 0) + t.offsetHeight : 0; };
|
|
510
|
+
const topEdge = () => { const sb = ui && ui.body.closest('.sb'); return stickBottom() + (sb ? parseFloat(getComputedStyle(sb).paddingTop) || 0 : 0); };
|
|
511
|
+
function placeOl() {
|
|
512
|
+
const nav = ui && ui.body.querySelector('.ds-ol'); const sb = ui && ui.body.closest('.sb'); if (!nav || !sb) return;
|
|
513
|
+
const h = stickBottom() + 8;
|
|
514
|
+
nav.style.setProperty('--in-ds-oltop', h + 'px'); nav.style.setProperty('--in-ds-olmax', Math.max(160, sb.clientHeight - h - 16) + 'px');
|
|
515
|
+
}
|
|
516
|
+
function jumpTo(i) {
|
|
517
|
+
const sb = ui && ui.body.closest('.sb'); const n = ui && ui.body.querySelector('[data-dsec-i="' + i + '"]'); if (!sb || !n) return;
|
|
518
|
+
sb.scrollTop += n.getBoundingClientRect().top - sb.getBoundingClientRect().top - topEdge() - 8;
|
|
519
|
+
n.classList.remove('flash'); void n.offsetWidth; n.classList.add('flash');
|
|
520
|
+
S.cur = i; S.pinCur = i; S.olOpen = false; const lay = ui.body.querySelector('.ds-rlay'); if (lay) lay.classList.remove('olopen');
|
|
521
|
+
const b = ui.body.querySelector('[data-ol-btn]'); if (b) b.setAttribute('aria-expanded', 'false');
|
|
522
|
+
markCur();
|
|
523
|
+
}
|
|
524
|
+
function trackCur() {
|
|
525
|
+
if (S.mode !== 'read' || !ui) return;
|
|
526
|
+
const sb = ui.body.closest('.sb'); const nav = ui.body.querySelector('.ds-ol'); if (!sb || !nav) return;
|
|
527
|
+
const ids = new Set([...nav.querySelectorAll('[data-ol-i]')].map((x) => x.getAttribute('data-ol-i')));
|
|
528
|
+
const lim = sb.getBoundingClientRect().top + topEdge() + 24; let cur = null;
|
|
529
|
+
for (const s of ui.body.querySelectorAll('.ds-rmain [data-dsec-i]')) {
|
|
530
|
+
const k = s.getAttribute('data-dsec-i');
|
|
531
|
+
if (s.getBoundingClientRect().top <= lim) { if (ids.has(k)) cur = +k; } else break;
|
|
532
|
+
}
|
|
533
|
+
if (cur == null) { const f = nav.querySelector('[data-ol-i]'); cur = f ? +f.getAttribute('data-ol-i') : null; }
|
|
534
|
+
/* a section pressed near the end cannot scroll to the top: it stays the marked one while it is on screen */
|
|
535
|
+
if (S.pinCur != null) {
|
|
536
|
+
const p = ui.body.querySelector('[data-dsec-i="' + S.pinCur + '"]'); const r = p && p.getBoundingClientRect(); const b = sb.getBoundingClientRect();
|
|
537
|
+
if (r && r.top >= b.top + topEdge() - 8 && r.top < b.bottom - 40) cur = S.pinCur; else S.pinCur = null;
|
|
538
|
+
}
|
|
539
|
+
if (cur !== S.cur) { S.cur = cur; markCur(); }
|
|
540
|
+
}
|
|
541
|
+
function markCur() {
|
|
542
|
+
const nav = ui && ui.body.querySelector('.ds-ol'); if (!nav) return;
|
|
543
|
+
nav.querySelectorAll('[data-ol-i]').forEach((li) => { const on = +li.getAttribute('data-ol-i') === S.cur; li.classList.toggle('on', on); const g = li.querySelector('[data-ol-go]'); if (g) { if (on) g.setAttribute('aria-current', 'location'); else g.removeAttribute('aria-current'); } });
|
|
544
|
+
const secs = S.text ? splitDoc(S.text) : []; const c = secs.find((s) => s.i === S.cur);
|
|
545
|
+
const ob = ui.body.querySelector('[data-ol-btn] b'); if (ob) ob.textContent = c ? c.name : '';
|
|
546
|
+
const on = nav.querySelector('li.on');
|
|
547
|
+
if (on && nav.scrollHeight > nav.clientHeight) { const t = on.offsetTop - nav.offsetTop; if (t < nav.scrollTop || t > nav.scrollTop + nav.clientHeight - 30) nav.scrollTop = t - nav.clientHeight / 3; }
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/* ── v2: Relations, the graph around this document (from the caller's library, else GET /api/docs/library) ── */
|
|
551
|
+
const loadLib = async () => {
|
|
552
|
+
if (S.lib !== undefined) return;
|
|
553
|
+
S.lib = null;
|
|
554
|
+
const j = await (library ? Promise.resolve().then(library) : pmGet('/api/docs/library?' + q({ ws }))).catch(() => null);
|
|
555
|
+
S.lib = j && j.ok ? j : { err: (j && j.error) || 'The document graph could not be read.' };
|
|
556
|
+
drawBg();
|
|
557
|
+
};
|
|
558
|
+
const catName = (c) => { const k = ((S.lib && S.lib.categories) || []).find((x) => x.key === c); return k ? k.name : c || 'Document'; };
|
|
559
|
+
const fileDoc = (f) => ({ path: f.path, name: f.title || f.path, ty: f.ty || catName(f.category), v: f.v || '', st: STATE[f.st] ? f.st : 'current', ini: f.initiative || '',
|
|
560
|
+
iniName: (S.lib && S.lib.initiatives && S.lib.initiatives[f.initiative] && (S.lib.initiatives[f.initiative].short || S.lib.initiatives[f.initiative].title)) || f.initiative || '' });
|
|
561
|
+
function graphNodes() {
|
|
562
|
+
const H = hoodOf(S.lib && !S.lib.err ? S.lib : null, doc.path) || { src: [], out: [] };
|
|
563
|
+
const isGoal = (f) => f.category === 'goal' || f.node === 'Goal';
|
|
564
|
+
const nd = (e, side) => { const f = e.f; const file = !f.node && f.st !== 'node' && f.st !== 'missing';
|
|
565
|
+
return { side, name: f.title || f.path, sub: [...new Set([e.kind, f.node || (f.date ? dayName(f.date) : '')].filter(Boolean).map((x) => x.toLowerCase() === String(e.kind).toLowerCase() ? e.kind : x))].join(' · '), st: e.st, path: f.path, open: file ? fileDoc(f) : null }; };
|
|
566
|
+
const top = H.src.filter((e) => isGoal(e.f)).map((e) => nd(e, 'top'));
|
|
567
|
+
if (doc.ini || doc.iniName) top.unshift({ side: 'top', name: doc.iniName || doc.ini, sub: 'initiative', st: 'ok', init: true });
|
|
568
|
+
const left = H.src.filter((e) => !isGoal(e.f)).map((e) => nd(e, 'left'));
|
|
569
|
+
const right = H.out.map((e) => nd(e, 'right')).concat((S.copies || []).map((l) => ({ side: 'right', name: labelOf(l) + ' copy', sub: sigOf(l)[4], url: l.url, kind: l.kind,
|
|
570
|
+
st: l.signal === 'behind' || l.signal === 'diverged' ? 'drift' : l.signal === 'ahead' ? 'ahead' : l.signal === 'same' ? 'ok' : 'missing' })));
|
|
571
|
+
return { top, left, right };
|
|
572
|
+
}
|
|
573
|
+
function graphEl(G) {
|
|
574
|
+
const CAP = 6;
|
|
575
|
+
const node = (n) => {
|
|
576
|
+
const kids = [el('b', null, n.name), el('small', null, n.sub)];
|
|
577
|
+
const a = { 'data-gn': n.side, 'data-gn-st': n.st, 'data-gn-copy': n.kind || null };
|
|
578
|
+
if (n.url) return el('a', { class: 'ds-gn ' + n.st, href: n.url, target: '_blank', rel: 'noopener', ...a }, ...kids);
|
|
579
|
+
if (n.open) return btn('ds-gn ' + n.st, { 'data-gn-path': n.path, title: 'Open ' + n.name, ...a }, kids, () => S.open(n.open));
|
|
580
|
+
return el('div', { class: 'ds-gn x ' + n.st, ...a }, ...kids);
|
|
581
|
+
};
|
|
582
|
+
const col = (list) => [list.slice(0, CAP).map(node), list.length > CAP ? el('div', { class: 'ds-note ds-gmore' }, '+' + (list.length - CAP) + ' more, listed below') : null];
|
|
583
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('class', 'ds-gsvg'); svg.setAttribute('aria-hidden', 'true');
|
|
584
|
+
const wrap = el('div', { class: 'ds-graph', role: 'group', 'aria-label': 'This document, its sources on the left, its initiative above, the documents that use it and its copies on the right', 'data-doc-graph': '' }, svg,
|
|
585
|
+
G.top.length ? el('div', { class: 'ds-gtop' }, col(G.top)) : null,
|
|
586
|
+
el('div', { class: 'ds-gmid' }, el('div', { class: 'ds-gcol' }, G.left.length ? col(G.left) : el('div', { class: 'ds-note' }, 'No source recorded')),
|
|
587
|
+
el('div', { class: 'ds-gn me', 'data-gn': 'me' }, el('b', null, doc.name), el('small', null, 'this document' + (doc.v ? ' · ' + doc.v : ''))),
|
|
588
|
+
el('div', { class: 'ds-gcol' }, G.right.length ? col(G.right) : el('div', { class: 'ds-note' }, 'Nothing uses it yet'))),
|
|
589
|
+
el('div', { class: 'key ds-note' }, el('span', null, el('i', { class: 'ln ok' }), 'in step'), el('span', null, el('i', { class: 'ln drift' }), 'out of step'), el('span', null, el('i', { class: 'ln ahead' }), 'PMOS ahead of the copy'),
|
|
590
|
+
openGraph ? btn('chip2 lk', { 'data-doc-fullgraph': '' }, [c3ic('tree', 'sm'), 'Open the full graph in Docs'], () => { ui.close(); openGraph(doc); }) : null));
|
|
591
|
+
if (typeof ResizeObserver === 'function') new ResizeObserver(() => drawEdges(wrap)).observe(wrap);
|
|
592
|
+
return wrap;
|
|
593
|
+
}
|
|
594
|
+
function drawEdges(wrap) {
|
|
595
|
+
const svg = wrap.querySelector('.ds-gsvg'); const me = wrap.querySelector('[data-gn="me"]'); if (!svg || !me || !wrap.isConnected) return;
|
|
596
|
+
const W = wrap.getBoundingClientRect(); const M = me.getBoundingClientRect();
|
|
597
|
+
svg.setAttribute('viewBox', '0 0 ' + Math.round(W.width) + ' ' + Math.round(W.height)); svg.setAttribute('width', String(Math.round(W.width))); svg.setAttribute('height', String(Math.round(W.height)));
|
|
598
|
+
const paths = [...wrap.querySelectorAll('[data-gn]:not([data-gn="me"])')].map((n) => {
|
|
599
|
+
const R = n.getBoundingClientRect(); const side = n.getAttribute('data-gn');
|
|
600
|
+
const [fx, fy, tx, ty] = side === 'left' ? [R.right, R.top + R.height / 2, M.left, M.top + M.height / 2] : side === 'right' ? [M.right, M.top + M.height / 2, R.left, R.top + R.height / 2] : [R.left + R.width / 2, R.bottom, M.left + M.width / 2, M.top];
|
|
601
|
+
const x1 = fx - W.left; const y1 = fy - W.top; const x2 = tx - W.left; const y2 = ty - W.top;
|
|
602
|
+
const d = side === 'top' ? 'M' + x1 + ' ' + y1 + ' C ' + x1 + ' ' + (y1 + y2) / 2 + ', ' + x2 + ' ' + (y1 + y2) / 2 + ', ' + x2 + ' ' + y2 : 'M' + x1 + ' ' + y1 + ' C ' + (x1 + x2) / 2 + ' ' + y1 + ', ' + (x1 + x2) / 2 + ' ' + y2 + ', ' + x2 + ' ' + y2;
|
|
603
|
+
const p = document.createElementNS('http://www.w3.org/2000/svg', 'path'); p.setAttribute('d', d); p.setAttribute('class', 'e ' + (n.getAttribute('data-gn-st') || 'ok')); return p;
|
|
604
|
+
});
|
|
605
|
+
svg.replaceChildren(...paths);
|
|
606
|
+
}
|
|
607
|
+
function linksBody() {
|
|
608
|
+
if (S.lib === undefined) loadLib();
|
|
609
|
+
if (S.lib == null) return el('div', { class: 'fold', role: 'status' }, c3ic('clock', 'sm'), 'Reading the document graph.');
|
|
610
|
+
const G = graphNodes(); const any = G.left.length || G.right.length || G.top.some((n) => !n.init);
|
|
611
|
+
const rel = (h, list) => list.length ? [el('h3', { class: 'ds-h' }, h), el('ul', { class: 'ds-rel' }, list.map((n) => el('li', { 'data-rel': n.path || n.kind || n.name },
|
|
612
|
+
c3ic(n.url ? 'out' : n.init ? 'init' : 'doc', 'sm'),
|
|
613
|
+
n.open ? btn('ds-rlk', { 'data-rel-open': n.path }, [n.name, el('small', null, n.sub)], () => S.open(n.open)) : n.url ? el('a', { class: 'ds-rlk', href: n.url, target: '_blank', rel: 'noopener' }, n.name, el('small', null, n.sub)) : el('span', { class: 'ds-rlk' }, n.name, el('small', null, n.sub)),
|
|
614
|
+
n.st && n.st !== 'ok' ? el('span', { class: 'tag ' + (n.st === 'ahead' ? 'ds-acc' : n.st === 'drift' ? 'ds-warn' : 'ds-bad') }, ESTATE[n.st] || n.st) : el('span'))))] : null;
|
|
615
|
+
return el('div', { class: 'ds' },
|
|
616
|
+
S.lib.err ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), S.lib.err) : null,
|
|
617
|
+
any ? graphEl(G) : el('div', { class: 'fold', 'data-doc-norel': '' }, c3ic('info', 'sm'), 'No relation is recorded for this document.'),
|
|
618
|
+
relations ? relations(doc, (d) => { if (S.open) S.open(d); }) : [rel('Sources', G.left), rel('Goals and initiative', G.top.filter((n) => !n.init)), rel('Used by and copies', G.right)]);
|
|
619
|
+
}
|
|
620
|
+
|
|
321
621
|
/* ── Read ── */
|
|
322
622
|
function propsBlock(secs, editing) {
|
|
323
623
|
const s0 = secs.find((s) => s.prefix === ''); const fm = s0 && parseFront(s0.text);
|
|
@@ -334,11 +634,14 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
334
634
|
if (S.text == null) return S.err ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), S.err) : el('div', { class: 'fold', role: 'status' }, c3ic('clock', 'sm'), 'Reading the document.');
|
|
335
635
|
const secs = splitDoc(S.text);
|
|
336
636
|
if (!S.text.trim()) return el('div', { class: 'fold' }, c3ic('moon', 'sm'), 'The document is empty.');
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
637
|
+
const items = outlineOf(secs); const olOn = items.length >= 2 && !S.src;
|
|
638
|
+
if (S.cur == null && items.length) S.cur = items[0].s.i;
|
|
639
|
+
const curName = (items.find((x) => x.s.i === S.cur) || items[0] || { s: { name: '' } }).s.name;
|
|
640
|
+
const olCtl = olOn ? el('span', { class: 'ds-acts' },
|
|
641
|
+
btn('ds-btn ds-olbtn', { 'aria-expanded': String(S.olOpen), 'data-ol-btn': '' }, [c3ic('ul', 'sm'), 'Outline · ', el('b', null, curName)], () => { S.olOpen = !S.olOpen; const lay = ui.body.querySelector('.ds-rlay'); if (lay) lay.classList.toggle('olopen', S.olOpen); const b = ui.body.querySelector('[data-ol-btn]'); if (b) b.setAttribute('aria-expanded', String(S.olOpen)); }),
|
|
642
|
+
btn('ds-btn ds-olhide', { 'aria-pressed': String(!S.ol), 'data-ol-hide': '' }, [c3ic('ul', 'sm'), S.ol ? 'Hide the outline' : 'Show the outline'], () => { S.ol = !S.ol; olSet(S.ol); draw(); })) : el('span');
|
|
340
643
|
const tg = (id, label, on, set) => el('label', { class: 'ds-tg' }, el('input', { type: 'checkbox', checked: on, 'data-doc-toggle': id, onChange: (e) => { set(e.target.checked); draw(); } }), label);
|
|
341
|
-
const bar = el('div', { class: 'ds-rbar' },
|
|
644
|
+
const bar = el('div', { class: 'ds-rbar' }, olCtl, el('span', { class: 'ds-acts' },
|
|
342
645
|
generated ? null : tg('who', 'Show who wrote what', S.who, (v) => { S.who = v; }), tg('src', 'Markdown source', S.src, (v) => { S.src = v; })));
|
|
343
646
|
const legend = S.who ? el('div', { class: 'ds-legend' }, el('span', null, el('i', { class: 'h' }), 'Written by a person'), el('span', null, el('i', { class: 'a' }), 'Written by the agent, accepted by a person')) : null;
|
|
344
647
|
if (S.src) {
|
|
@@ -368,8 +671,9 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
368
671
|
});
|
|
369
672
|
const adds = openProps().flatMap((p) => p.sections.filter((c) => c.kind === 'added' && S.preview[p.id]).map((c) => el('div', { class: 'ds-sec a prev', 'data-dsec-new': c.name },
|
|
370
673
|
el('div', { class: 'ds-pin' }, el('span', null, c3ic('spark', 'sm'), 'The proposal adds a section: ' + c.name)), el('div', { class: 'ds-sec-h' }, el('h2', null, c.name)), md(diffMd(null, c.after), 'ds-body'))));
|
|
371
|
-
|
|
372
|
-
|
|
674
|
+
const main = [generated ? el('div', { class: 'fold', role: 'note', 'data-doc-generated': '' }, c3ic('info', 'sm'), generated.why) : null, staleBanner(), propsBlock(secs, false), cells, adds];
|
|
675
|
+
return el('div', { class: 'ds' + (S.who ? ' who' : '') }, bar, legend,
|
|
676
|
+
olOn ? el('div', { class: 'ds-rlay' + (S.ol ? '' : ' nool') + (S.olOpen ? ' olopen' : '') }, outlineNav(items), el('div', { class: 'ds-rmain' }, main)) : el('div', { class: 'ds-rmain' }, main));
|
|
373
677
|
}
|
|
374
678
|
const short = (t) => { const s = String(t || '').replace(/\s+/g, ' ').trim(); return s.length > 90 ? s.slice(0, 88) + '…' : s; };
|
|
375
679
|
|
|
@@ -709,8 +1013,6 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
709
1013
|
el('p', { class: 'ds-note' }, 'Health answers one question: can I trust this document without rereading it? The checks run again every time the text changes.'));
|
|
710
1014
|
}
|
|
711
1015
|
|
|
712
|
-
const linksBody = () => (relations ? relations(doc, (d) => { if (S.open) S.open(d); }) : el('div', { class: 'fold' }, c3ic('info', 'sm'), 'No relation is recorded for this document.'));
|
|
713
|
-
|
|
714
1016
|
/* a load that finished in the background must not redraw under a hand that is typing */
|
|
715
1017
|
const drawBg = () => { const a = document.activeElement; if (ui && a && ui.body.contains(a) && a.closest('input,textarea,[contenteditable=true]')) { refreshTop(); return; } draw(); };
|
|
716
1018
|
const top = () => el('div', { class: 'ds-top' }, head(), seg(), S.mode === 'edit' && S.ed ? toolbar() : null,
|
|
@@ -723,21 +1025,26 @@ export function openDocSheet({ ws, doc, mode = 'read', prompt = '', relations, o
|
|
|
723
1025
|
const body = S.mode === 'read' ? readBody() : S.mode === 'edit' ? editBody() : S.mode === 'ask' ? propsBody() : S.mode === 'hist' ? logBody() : S.mode === 'health' ? healthBody() : linksBody();
|
|
724
1026
|
const keep = ui.body.querySelector('textarea:focus,input[type=text]:focus');
|
|
725
1027
|
const had = keep ? { id: keep.id, s: keep.selectionStart } : null;
|
|
726
|
-
ui.body.replaceChildren(el('div', { class: 'dpn ds' }, top(), body));
|
|
1028
|
+
ui.body.replaceChildren(el('div', { class: 'dpn ds' }, top(), copiesPanel(), body));
|
|
1029
|
+
const sh = ui.body.closest('.sheet'); if (sh) sh.classList.toggle('dwx', S.mode === 'read' && S.ol && !!ui.body.querySelector('.ds-ol'));
|
|
1030
|
+
const sb = ui.body.closest('.sb');
|
|
1031
|
+
if (sb && !sb.__dsTrack) { sb.__dsTrack = true; sb.addEventListener('scroll', trackCur, { passive: true }); }
|
|
1032
|
+
if (S.mode === 'read') { placeOl(); trackCur(); }
|
|
1033
|
+
if (S.mode === 'links') { const g = ui.body.querySelector('.ds-graph'); if (g) requestAnimationFrame(() => drawEdges(g)); }
|
|
727
1034
|
if (had && had.id) { const n = ui.body.querySelector('#' + had.id); if (n) { n.focus(); try { n.setSelectionRange(had.s, had.s); } catch { /* not a text field */ } } }
|
|
728
1035
|
if (S.mode === 'ask' && S.openProp) { const id = S.openProp; S.openProp = null; goEl('[data-doc-prop-card="' + id + '"]'); }
|
|
729
1036
|
}
|
|
730
1037
|
|
|
731
1038
|
document.addEventListener('selectionchange', keepRange);
|
|
732
1039
|
ui = openSheet({ title: doc.name, icon: 'doc', dw: true, body: el('div', { class: 'dpn' }), onClose: () => { S.closed = true; document.removeEventListener('selectionchange', keepRange); if (S.changed && onChanged) onChanged(); } });
|
|
733
|
-
S.open = (d) => { ui.close(); if (S.openOther) S.openOther(d); };
|
|
1040
|
+
S.open = (d) => { ui.close(); if (S.openOther) S.openOther(d); else openDocSheet({ ws, doc: d, library, openGraph }); };
|
|
734
1041
|
ui.setOpener = (fn) => { S.openOther = fn; };
|
|
735
1042
|
draw();
|
|
736
1043
|
(async () => {
|
|
737
1044
|
await loadText();
|
|
738
1045
|
if (S.mode === 'edit') startEdit();
|
|
739
1046
|
drawBg();
|
|
740
|
-
await Promise.all([loadRevs(), loadProps(), loadHealth()]);
|
|
1047
|
+
await Promise.all([loadRevs(), loadProps(), loadHealth(), loadCopies()]);
|
|
741
1048
|
if (S.mode === 'hist') await loadLog();
|
|
742
1049
|
drawBg(); loadMeta();
|
|
743
1050
|
})();
|
|
@@ -58,7 +58,8 @@ export async function badge(ctx) {
|
|
|
58
58
|
function openDoc(V, d, ask) {
|
|
59
59
|
if (!d || d.st === 'missing' || d.node) return;
|
|
60
60
|
const ui = openDocSheet({ ws: V.ctx.ws, doc: { path: d.path, name: d.name, ty: d.ty, v: d.v, st: d.st, ini: d.ini, iniName: iniName(V, d.ini) }, mode: ask ? 'ask' : 'read', prompt: ask || '',
|
|
61
|
-
relations: (doc, openOther) => relationsOf(V, d, openOther),
|
|
61
|
+
relations: (doc, openOther) => relationsOf(V, d, openOther), library: () => V.lib, openGraph: () => { V.view = 'graph'; V.sel = d.i; V.render(); },
|
|
62
|
+
onChanged: () => { delete V.health[d.path]; V.asking = false; load(V); } });
|
|
62
63
|
ui.setOpener((o) => openDoc(V, o));
|
|
63
64
|
}
|
|
64
65
|
const iniName = (V, id) => { const i = V.lib && V.lib.initiatives && V.lib.initiatives[id]; return i ? i.short || i.title : id || ''; };
|
|
@@ -30,7 +30,9 @@ export async function loadDocs(ctx, idSet) {
|
|
|
30
30
|
async function openDoc(ctx, d, titles) {
|
|
31
31
|
try {
|
|
32
32
|
const { openDocSheet } = await import('./c3-doc-sheet.js');
|
|
33
|
-
openDocSheet({ ws: ctx.ws, doc: { path: d.path, name: d.name, ty: d.ty, v: '', st: d.st, ini: d.ini, iniName: titles.get(d.ini) || d.ini }
|
|
33
|
+
const ui = openDocSheet({ ws: ctx.ws, doc: { path: d.path, name: d.name, ty: d.ty, v: '', st: d.st, ini: d.ini, iniName: titles.get(d.ini) || d.ini },
|
|
34
|
+
library: () => ctx.api.get('/api/docs/library', { board: ctx.board.id }) });
|
|
35
|
+
ui.setOpener((o) => openDoc(ctx, { path: o.path, name: o.name, ty: o.ty, st: o.st, ini: o.ini }, titles));
|
|
34
36
|
} catch { ctx.toast('The document sheet could not open. The file is ' + d.path + '.'); }
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -546,7 +546,7 @@ export function mountSettings(host, ctx = {}) {
|
|
|
546
546
|
const values = () => Object.fromEntries(F.map(([k]) => [k, inputs[k].value.trim()]));
|
|
547
547
|
const running = o.on && o.running && o.running.running;
|
|
548
548
|
return [group('Online', [extRow('online.on', swBtn(o.on, 'Online', () => post({ ...values(), on: !o.on }, o.on ? 'Online is off' : 'Online is on'), { disabled: !o.canChange }), { scope: 'computer',
|
|
549
|
-
note: !o.canChange ? 'Only the computer that runs PMOS can change this.' : !o.cloudflared ? 'cloudflared is not installed. Install it
|
|
549
|
+
note: !o.canChange ? 'Only the computer that runs PMOS can change this.' : !o.cloudflared ? 'cloudflared is not installed. Install it (Mac: brew install cloudflared; Windows: winget install Cloudflare.cloudflared), then switch Online on.' : null })], 'phone'),
|
|
550
550
|
running ? fold('check', 'Tunnel running · cloudflared found' + (o.lastRefusal ? '' : ' · no refused sign-in'), 'a') : null,
|
|
551
551
|
sect('Tunnel and sign-in', [list(fieldRows), o.canChange ? el('div', { class: 'iacts' }, btn('Save', { variant: 'primary', onclick: () => post(values()) })) : null,
|
|
552
552
|
o.lastRefusal ? help('Last refused sign-in: ' + o.lastRefusal.reason) : null], { ic: 'phone' }),
|