create-pmos 0.241.0 → 0.243.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +2 -2
- 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/payload/state/log.md +1 -1
- package/workspace/c3-report-design-real-test.js +13 -2
- package/workspace/c3-report-design-test.js +25 -3
- package/workspace/c3-report-structure-real-test.js +38 -1
- package/workspace/c3-report-structure-test.js +21 -6
- package/workspace/guide/console-calendar.md +15 -1
- package/workspace/guide/notes.md +26 -1
- package/workspace/guide/report-structure.md +3 -3
- package/workspace/guide/reports-page.md +3 -1
- package/workspace/guide/reports-set-up.md +5 -1
- package/workspace/lib/headless.js +7 -2
- package/workspace/lib/meeting-material.js +151 -0
- package/workspace/lib/meeting-replay.js +184 -0
- package/workspace/lib/notes-screen.js +128 -8
- package/workspace/lib/report-structure.js +49 -5
- package/workspace/lib/route-modules.js +2 -0
- package/workspace/lib/routes-meeting-focus.js +5 -3
- package/workspace/lib/routes-meeting-material.js +143 -0
- package/workspace/lib/routes-meeting-replay.js +75 -0
- package/workspace/lib/routes-notes.js +37 -4
- package/workspace/lib/routes-report-setups.js +16 -2
- package/workspace/lib/routes-report-structure.js +176 -5
- package/workspace/lib/routes-reports.js +10 -0
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-audience-link.js +21 -0
- package/workspace/public/pm/c3-audience.js +46 -0
- package/workspace/public/pm/c3-boot.js +2 -1
- package/workspace/public/pm/c3-calendar.js +15 -3
- package/workspace/public/pm/c3-demo-material.js +58 -0
- package/workspace/public/pm/c3-demo.js +5 -1
- package/workspace/public/pm/c3-focus.js +83 -16
- package/workspace/public/pm/c3-listen.css +10 -0
- package/workspace/public/pm/c3-material.css +22 -0
- package/workspace/public/pm/c3-material.js +321 -0
- package/workspace/public/pm/c3-notes.js +12 -4
- package/workspace/public/pm/c3-replay.css +133 -0
- package/workspace/public/pm/c3-replay.js +429 -0
- package/workspace/public/pm/c3-report-deck.css +5 -0
- package/workspace/public/pm/c3-report-deck.js +41 -5
- package/workspace/public/pm/c3-report-setup.css +5 -0
- package/workspace/public/pm/c3-report-setup.js +20 -2
- package/workspace/public/pm/c3-report-store.js +24 -2
- package/workspace/public/pm/c3-report-structure.css +4 -0
- package/workspace/public/pm/c3-report-structure.js +26 -8
- package/workspace/public/pm/c3-shell.js +2 -0
- package/workspace/public/pm/dest-notes.js +4 -2
- package/workspace/public/pm/dest-reports.js +10 -1
- package/workspace/public/pm/listen-sheet.js +94 -0
- package/workspace/public/pm/listen-sources.js +82 -0
- package/workspace/public/pm/notes-aside.js +62 -0
- package/workspace/public/pm/notes-screen-capture.js +23 -10
- package/workspace/public/pm/notes-screen-view.js +18 -5
- package/workspace/public/pm/notes-shots.js +55 -0
- package/workspace/public/pm/report-view.js +23 -1
- package/workspace/public/pm/reports-copy.js +5 -0
- package/workspace/public/pm/shell-recording.js +10 -2
- package/workspace/public/pm/view-notes.js +48 -8
- package/workspace/server.js +8 -1
- package/workspace/skills/report-structure-pull.skill +45 -0
- package/workspace/test-lists/meeting-material.json +6 -0
- package/workspace/test-lists/meeting-replay.json +6 -0
- package/workspace/test-lists/mm-capture.json +15 -0
- package/workspace/test-lists/report-structure-gaps.json +5 -0
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* GET /api/report-structure/check?ws&id&source → { ok, check }
|
|
13
13
|
* POST /api/report-structure/dry { ws, id, n? } → { ok, running: true } starts ONE report-structure-dry job
|
|
14
14
|
* GET /api/report-structure/dry?ws&id&run → { ok, dry }
|
|
15
|
+
* POST /api/report-structure/pull { ws, id, source } → { ok, running: true } Test pull: ONE report-structure-pull job reads ONE source; never a turn, never a version
|
|
16
|
+
* GET /api/report-structure/pull?ws&id&source → { ok, pull, running } the last pull of the source (structure/pull/<source>.json)
|
|
15
17
|
* GET /api/charts?ws → { ok, charts: [chart + { users }] }
|
|
16
18
|
* GET /api/charts/one?ws&id → { ok, chart }
|
|
17
19
|
*
|
|
@@ -48,12 +50,15 @@ const headless = require('./headless.js');
|
|
|
48
50
|
const text = (v) => (typeof v === 'string' ? v : '');
|
|
49
51
|
const bad = (status, error) => ({ status, body: { ok: false, error } });
|
|
50
52
|
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
51
|
-
const POSTS = ['turn', 'accept', 'revert', 'source', 'remove', 'check', 'dry'];
|
|
53
|
+
const POSTS = ['turn', 'accept', 'revert', 'source', 'remove', 'check', 'dry', 'pull'];
|
|
52
54
|
const HOLD_MS = 4 * 3600 * 1000;
|
|
53
55
|
const CHECK_MS = 10 * 60 * 1000;
|
|
56
|
+
/** How long a check session may take. PMOS_REPORT_CHECK_MS shortens it for the suites. */
|
|
57
|
+
const checkMs = () => (Number(process.env.PMOS_REPORT_CHECK_MS) > 0 ? Number(process.env.PMOS_REPORT_CHECK_MS) : CHECK_MS);
|
|
54
58
|
const STEP_FLAGS_FALLBACK = [{ flag: '--chrome' }, { flag: '--allowedTools', value: 'mcp__claude-in-chrome' }];
|
|
55
59
|
|
|
56
60
|
const jobs = new Map(); // `${root}\0${id}` → { id, kind, since, turn, pending, sessionId, base, n }
|
|
61
|
+
const pullJobs = new Map(); // `${root}\0${id}\0${source}` → { run, since, pending, sessionId, n } (a Test pull never blocks, and is never blocked by, a turn)
|
|
57
62
|
const checkJobs = new Map(); // `${root}\0${id}\0${source}` → { run, since, pending, sessionId, kind }
|
|
58
63
|
let lastCtx = null;
|
|
59
64
|
const keyOf = (root, id) => `${path.resolve(String(root))}\u0000${id}`;
|
|
@@ -77,7 +82,7 @@ function runningOf(root, id) {
|
|
|
77
82
|
const j = jobs.get(keyOf(root, id));
|
|
78
83
|
return j ? { since: j.since, turn: j.turn, kind: j.kind } : null;
|
|
79
84
|
}
|
|
80
|
-
const answerOf = (root, id) => S.structureAnswer(root, id, { running: runningOf(root, id) });
|
|
85
|
+
const answerOf = (root, id) => S.structureAnswer(root, id, { running: runningOf(root, id), pulling: pullingOf(root, id) });
|
|
81
86
|
const storeIds = (root) => new Set(CS.stateOf(root).keys());
|
|
82
87
|
|
|
83
88
|
// ── the job ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
@@ -184,6 +189,87 @@ function finish(w, id, job, r) {
|
|
|
184
189
|
try { fs.unlinkSync(job.pending); } catch { /* not written */ }
|
|
185
190
|
}
|
|
186
191
|
|
|
192
|
+
|
|
193
|
+
// ── Test pull: one source, read straight into its row ─────────────────────────────────────────────────────
|
|
194
|
+
// A pull is NOT a turn: no line in the conversation, no version, and a running turn does not block it. The only event is `pull`; the result is a file.
|
|
195
|
+
const pullKey = (root, id, source) => `${keyOf(root, id)}\u0000${source}`;
|
|
196
|
+
function pullingOf(root, id) {
|
|
197
|
+
const pre = `${keyOf(root, id)}\u0000`;
|
|
198
|
+
return [...pullJobs.keys()].filter((k) => k.startsWith(pre)).map((k) => k.slice(pre.length));
|
|
199
|
+
}
|
|
200
|
+
function pullPrompt(skill, { root, id, setup, src, n, pending }) {
|
|
201
|
+
const p = S.paths(root, id);
|
|
202
|
+
const lines = [`- Report: ${setup.name} (${id})`, `- Today: ${new Date().toISOString().slice(0, 10)}`, `- Schedule: ${setup.cadence}`, `- Repository: ${p.abs}`,
|
|
203
|
+
`- Source: ${src.id} (${src.kind}): ${String(src.name).replace(/\s+/g, ' ')}`, `- Version: S${n} (${path.join(p.versions, `S${n}.json`)})`, `- Chart store: ${CS.paths(root).file}`, `- Output path: ${pending}`];
|
|
204
|
+
return `${skill}\n\nThis job:\n${lines.join('\n')}\n\nWrite the JSON to the output path, then print the JSON result. Send nothing.\r`;
|
|
205
|
+
}
|
|
206
|
+
/** A source Test pull can read: not a PMOS source, and not a Chrome or computer source (those have Check it works). */
|
|
207
|
+
const pullable = (src) => src.group === 'outside' && src.kind !== 'chrome' && src.kind !== 'computer';
|
|
208
|
+
function startPull(ctx, w, id, sourceId) {
|
|
209
|
+
const root = w.root;
|
|
210
|
+
const st = S.stateOf(root, id);
|
|
211
|
+
const hit = sourceOf(root, id, sourceId);
|
|
212
|
+
if (!hit || st.current == null) return { ok: false, status: 404, error: 'no such source in this version' };
|
|
213
|
+
if (!pullable(hit.src)) return { ok: false, status: 400, error: 'Test pull reads connectors, links and uploads. PMOS sources open in PMOS, and a Chrome or computer source has Check it works.' };
|
|
214
|
+
if (hit.src.state !== 'reached') return { ok: false, status: 409, error: 'Connect this source first, then press Test pull.' };
|
|
215
|
+
const key = pullKey(root, id, sourceId);
|
|
216
|
+
if (pullJobs.has(key)) return { ok: false, status: 409, error: 'PMOS is already reading this source. Wait for the answer.' };
|
|
217
|
+
if (typeof ctx.spawnSession !== 'function') return { ok: false, status: 409, error: 'this workspace cannot start background runs' };
|
|
218
|
+
const origin = `report-structure-pull:${id}:${sourceId}`;
|
|
219
|
+
if (headless.kindFor({ agent: 'claude', origin }) !== 'report-structure-pull') return { ok: false, status: 409, error: 'Background runs are switched off here (PMOS_HEADLESS=off), so PMOS cannot read the source.' };
|
|
220
|
+
const skill = typeof ctx.skillPrompt === 'function' ? ctx.skillPrompt('report-structure-pull', w) : null;
|
|
221
|
+
if (!skill) return { ok: false, status: 500, error: 'the report-structure-pull skill is not installed' };
|
|
222
|
+
const setup = setupOf(root, id) || { id, name: id, cadence: id };
|
|
223
|
+
const p = S.paths(root, id);
|
|
224
|
+
const run = S.newRun();
|
|
225
|
+
const pending = path.join(p.pullPending, `${run}.json`);
|
|
226
|
+
try { fs.mkdirSync(p.pullPending, { recursive: true }); } catch (e) { return { ok: false, status: 500, error: 'could not make the pending folder: ' + String((e && e.code) || e) }; }
|
|
227
|
+
const job = { id: run, since: new Date().toISOString(), pending, sessionId: null, n: st.current, source: sourceId };
|
|
228
|
+
pullJobs.set(key, job);
|
|
229
|
+
let s = null;
|
|
230
|
+
try {
|
|
231
|
+
s = ctx.spawnSession({ name: 'Report structure · test pull', agent: 'claude', ws: w.id, origin, cwd: p.rel,
|
|
232
|
+
initialInput: pullPrompt(skill, { root, id, setup, src: hit.src, n: st.current, pending }), writePaths: [pending], onDone: (r) => finishPull(w, id, job, r) });
|
|
233
|
+
} catch (e) { s = { state: 'exited', note: String((e && e.message) || e) }; }
|
|
234
|
+
if (!s || s.state === 'exited' || s.headless !== 'report-structure-pull') {
|
|
235
|
+
pullJobs.delete(key);
|
|
236
|
+
if (s && s.headless !== 'report-structure-pull' && s.state !== 'exited' && typeof ctx.stopSession === 'function') { try { ctx.stopSession(s.id); } catch { /* best effort */ } }
|
|
237
|
+
return { ok: false, status: 409, error: 'PMOS could not start the read: ' + ((s && s.note) || 'no background run') };
|
|
238
|
+
}
|
|
239
|
+
job.sessionId = s.id;
|
|
240
|
+
return { ok: true, running: true };
|
|
241
|
+
}
|
|
242
|
+
function finishPull(w, id, job, r) {
|
|
243
|
+
const root = w.root;
|
|
244
|
+
const key = pullKey(root, id, job.source);
|
|
245
|
+
if (pullJobs.get(key) === job) pullJobs.delete(key);
|
|
246
|
+
// Whatever happens, the source row gets an answer: a result file, or a "returned nothing" file with the reason. The plan and the conversation stay as they were.
|
|
247
|
+
const land = (pull) => {
|
|
248
|
+
try {
|
|
249
|
+
S.writePull(root, id, { ...pull, at: new Date().toISOString(), run: job.id });
|
|
250
|
+
S.append(root, id, { ev: 'pull', source: job.source, n: job.n, ok: !!pull.ok });
|
|
251
|
+
} catch { /* the folder is gone */ }
|
|
252
|
+
};
|
|
253
|
+
const nothing = (reason) => land({ source: job.source, n: job.n, period: '', reached: false, ok: false, reason, values: [], charts: 0, got: 0 });
|
|
254
|
+
try {
|
|
255
|
+
const res = (r && r.result) || {};
|
|
256
|
+
const ok = !!(r && !r.timedOut && !r.stopped && r.code === 0 && res.ok && !res.isError);
|
|
257
|
+
const out = resultJson(res.text);
|
|
258
|
+
let raw = null; try { raw = fs.readFileSync(job.pending, 'utf8'); } catch { raw = null; }
|
|
259
|
+
let data = null; if (raw) { try { data = JSON.parse(raw); } catch { data = null; } }
|
|
260
|
+
if (!data && out && isObj(out.pull)) data = out.pull;
|
|
261
|
+
if (!ok && !data) nothing(r && r.timedOut ? 'The read took too long and was stopped.' : r && r.stopped ? 'The read was stopped.' : 'The read failed' + (res.text ? ': ' + String(res.text).replace(/\s+/g, ' ').slice(0, 200) : '.'));
|
|
262
|
+
else if (!data) nothing('The read ended without a result.');
|
|
263
|
+
else {
|
|
264
|
+
const plan = S.readVersion(root, id, job.n);
|
|
265
|
+
const v = plan ? S.validatePull(data, plan, job.source) : { ok: false, error: 'the version is gone' };
|
|
266
|
+
if (!v.ok) nothing('The result could not be used: ' + v.error + '. Nothing was saved in the plan.');
|
|
267
|
+
else land(v.pull);
|
|
268
|
+
}
|
|
269
|
+
} catch (e) { nothing('The result could not be read: ' + String((e && e.message) || e).slice(0, 160)); }
|
|
270
|
+
try { fs.unlinkSync(job.pending); } catch { /* not written */ }
|
|
271
|
+
}
|
|
272
|
+
|
|
187
273
|
/** A turn that has no answer and no job in this server (the workspace restarted mid-run) gets an error line. */
|
|
188
274
|
function settleStale(root, id) {
|
|
189
275
|
if (jobs.has(keyOf(root, id))) return;
|
|
@@ -343,7 +429,7 @@ function settleChecks(ctx, w, id) {
|
|
|
343
429
|
const steps = v.steps.map((s) => (s.id === 'mac' ? base.find((b) => b.id === 'mac') || s : s)); // the server's own step wins
|
|
344
430
|
finishCheck(root, id, sourceId, job.kind, steps, { endSession: ctx && ctx.endSession, job });
|
|
345
431
|
} else finishCheck(root, id, sourceId, job.kind, failSteps(job.kind, 'read', `The check answered something PMOS could not use: ${v.error}`, base), { endSession: ctx && ctx.endSession, job });
|
|
346
|
-
} else if (now - job.since >
|
|
432
|
+
} else if (now - job.since > checkMs()) {
|
|
347
433
|
finishCheck(root, id, sourceId, job.kind, failSteps(job.kind, 'session', 'The check session did not answer in 10 minutes. Check again.', base), { endSession: ctx && ctx.endSession, job });
|
|
348
434
|
}
|
|
349
435
|
}
|
|
@@ -456,6 +542,79 @@ function sweep(ctx) {
|
|
|
456
542
|
return out;
|
|
457
543
|
}
|
|
458
544
|
|
|
545
|
+
// ── Draft it now, for a report with a saved structure ───────────────────────────────────────────────────
|
|
546
|
+
// A press on Draft it now (or Draft now on the report) starts the session the schedule would start, through the same gate, so the draft follows the
|
|
547
|
+
// structure. `drafting.json` marks a draft that was started; it ends when a draft file lands in the report's folder, or when the session is gone.
|
|
548
|
+
const draftingFile = (root, id) => path.join(S.paths(root, id).dir, 'drafting.json');
|
|
549
|
+
function readDrafting(root, id) { try { const o = JSON.parse(fs.readFileSync(draftingFile(root, id), 'utf8')); return isObj(o) ? o : null; } catch { return null; } }
|
|
550
|
+
/** The schedule that drafts a report (skill report-template, args = the id), or null. */
|
|
551
|
+
function draftScheduleOf(ctx, w, id) {
|
|
552
|
+
try { return ctx.scheduleStore.read().find((x) => x && x.skill === 'report-template' && x.args === id && (!x.ws || x.ws === w.id)) || null; } catch { return null; }
|
|
553
|
+
}
|
|
554
|
+
/** server.js calls this when the report-template session of a structured report starts. */
|
|
555
|
+
function markDrafting(root, id, { sched, session, saved } = {}) {
|
|
556
|
+
try {
|
|
557
|
+
fs.mkdirSync(S.paths(root, id).dir, { recursive: true });
|
|
558
|
+
fs.writeFileSync(draftingFile(root, id), JSON.stringify({ since: new Date().toISOString(), sched: sched || null, session: session || null, saved: saved == null ? null : saved }) + '\n');
|
|
559
|
+
} catch { /* the row shows no state; the draft still runs */ }
|
|
560
|
+
}
|
|
561
|
+
/** A draft file (.md) in the report's folder written since `since`: { slot, path } | null. */
|
|
562
|
+
function landedSince(root, id, since) {
|
|
563
|
+
try {
|
|
564
|
+
const p = S.paths(root, id);
|
|
565
|
+
const hit = fs.readdirSync(p.folder).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).map((f) => ({ f, t: fs.statSync(path.join(p.folder, f)).mtimeMs }))
|
|
566
|
+
.filter((x) => x.t >= Date.parse(since) - 1000).sort((a, b) => b.t - a.t)[0];
|
|
567
|
+
return hit ? { slot: hit.f.replace(/\.md$/, ''), path: `${p.rel}/${hit.f}` } : null;
|
|
568
|
+
} catch { return null; }
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* What a row shows for a report's draft: null, or { state: 'drafting' | 'held' | 'ended', since, saved, sources?, session?, reason? }.
|
|
572
|
+
* A held draft waits for a Chrome or computer check; an ended one lost its session before a draft landed.
|
|
573
|
+
*/
|
|
574
|
+
function draftState(ctx, w, id) {
|
|
575
|
+
try {
|
|
576
|
+
const root = w.root;
|
|
577
|
+
const hold = readHold(root, id);
|
|
578
|
+
if (hold) {
|
|
579
|
+
const sp = savedPlan(root, id);
|
|
580
|
+
const names = (Array.isArray(hold.sources) ? hold.sources : []).map((sid) => ((sp && sp.plan.sources.find((x) => x.id === sid)) || { name: sid }).name);
|
|
581
|
+
return { state: 'held', since: hold.since, saved: hold.saved == null ? null : hold.saved, sources: names, expires: new Date(Date.parse(hold.since) + holdMs()).toISOString() };
|
|
582
|
+
}
|
|
583
|
+
const m = readDrafting(root, id);
|
|
584
|
+
if (!m) return null;
|
|
585
|
+
if (m.ended) return { state: 'ended', since: m.since, saved: m.saved, reason: m.reason || 'The drafting session ended before a draft was written.' };
|
|
586
|
+
if (landedSince(root, id, m.since)) { try { fs.unlinkSync(draftingFile(root, id)); } catch { /* gone */ } return null; }
|
|
587
|
+
const alive = m.session && typeof ctx.sessionAlive === 'function' ? ctx.sessionAlive(m.session) : true;
|
|
588
|
+
if (!alive) {
|
|
589
|
+
const o = { ...m, ended: true, reason: 'The drafting session ended before a draft was written. Press Draft it now to try again.' };
|
|
590
|
+
try { fs.writeFileSync(draftingFile(root, id), JSON.stringify(o) + '\n'); } catch { /* shown again next time */ }
|
|
591
|
+
return { state: 'ended', since: m.since, saved: m.saved, reason: o.reason };
|
|
592
|
+
}
|
|
593
|
+
return { state: 'drafting', since: m.since, saved: m.saved, session: m.session || null };
|
|
594
|
+
} catch { return null; }
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Draft it now. → { structured: false } for a report with no saved structure (the caller drafts as it always did),
|
|
598
|
+
* { structured: true, status, error } when it cannot start, or { structured: true, drafting: true, held, state }.
|
|
599
|
+
*/
|
|
600
|
+
function draftNow(ctx, w, id) {
|
|
601
|
+
const root = w.root;
|
|
602
|
+
const sp = savedPlan(root, id);
|
|
603
|
+
if (!sp) return { structured: false };
|
|
604
|
+
const sched = draftScheduleOf(ctx, w, id);
|
|
605
|
+
if (!sched) return { structured: true, status: 409, error: 'This report has no draft schedule. Set it up again, then press Draft it now.' };
|
|
606
|
+
const cur = draftState(ctx, w, id);
|
|
607
|
+
if (cur && (cur.state === 'drafting' || cur.state === 'held')) {
|
|
608
|
+
return { structured: true, status: 409, error: cur.state === 'held' ? 'A draft is waiting for a check of ' + (cur.sources || []).join(', ') + '.' : 'A draft is being made. Wait for it to land.' };
|
|
609
|
+
}
|
|
610
|
+
try { fs.unlinkSync(draftingFile(root, id)); } catch { /* none */ }
|
|
611
|
+
const gate = beforeDraft(ctx, w, id, sched);
|
|
612
|
+
if (gate.hold) return { structured: true, drafting: true, held: true, state: draftState(ctx, w, id) };
|
|
613
|
+
if (typeof ctx.fireDraft !== 'function') return { structured: true, status: 500, error: 'The server cannot start a draft.' };
|
|
614
|
+
try { ctx.fireDraft(sched.id, { id, ws: w.id }); } catch (e) { return { structured: true, status: 500, error: 'The draft did not start: ' + (e && e.message ? e.message : 'unknown') }; }
|
|
615
|
+
return { structured: true, drafting: true, held: false, state: draftState(ctx, w, id) };
|
|
616
|
+
}
|
|
617
|
+
|
|
459
618
|
// ── the handler ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
460
619
|
async function handle(req, res, url, ctx) {
|
|
461
620
|
lastCtx = ctx;
|
|
@@ -497,6 +656,12 @@ async function handle(req, res, url, ctx) {
|
|
|
497
656
|
const c = S.readCheck(w.root, id, sid);
|
|
498
657
|
return send(c ? { status: 200, body: { ok: true, check: c } } : bad(404, 'this source has not been checked'));
|
|
499
658
|
}
|
|
659
|
+
if (sub === 'pull') {
|
|
660
|
+
const sid = text(url.searchParams.get('source'));
|
|
661
|
+
if (!S.SLUG_RE.test(sid)) return send(bad(400, 'source must be a source id'));
|
|
662
|
+
const pl = S.readPull(w.root, id, sid);
|
|
663
|
+
return send(pl ? { status: 200, body: { ok: true, pull: pl, running: pullJobs.has(pullKey(w.root, id, sid)) } } : bad(404, 'this source has not been pulled'));
|
|
664
|
+
}
|
|
500
665
|
if (sub === 'dry') {
|
|
501
666
|
const run = text(url.searchParams.get('run'));
|
|
502
667
|
if (!S.RUN_RE.test(run)) return send(bad(400, 'run must be a run id'));
|
|
@@ -537,6 +702,12 @@ async function handle(req, res, url, ctx) {
|
|
|
537
702
|
if (!s.ok) { S.append(root, id, { ev: 'error', text: s.error, mode: 'turn' }); return fail(s); }
|
|
538
703
|
return send({ status: 200, body: { ok: true, running: true } });
|
|
539
704
|
}
|
|
705
|
+
if (sub === 'pull') {
|
|
706
|
+
const sid = text(body.source);
|
|
707
|
+
if (!S.SLUG_RE.test(sid)) return send(bad(400, 'source must be a source id'));
|
|
708
|
+
const r = startPull(ctx, w, id, sid);
|
|
709
|
+
return r.ok ? send({ status: 200, body: { ok: true, running: true } }) : fail(r);
|
|
710
|
+
}
|
|
540
711
|
if (sub === 'dry') {
|
|
541
712
|
if (busy()) return send(bad(409, BUSY));
|
|
542
713
|
const st = S.stateOf(root, id);
|
|
@@ -615,5 +786,5 @@ async function handle(req, res, url, ctx) {
|
|
|
615
786
|
return send(bad(404, 'no such route'));
|
|
616
787
|
}
|
|
617
788
|
|
|
618
|
-
module.exports = { prefixes: ['/api/report-structure', '/api/charts'], handle, sweep, beforeDraft, draftBlock, startCheck, settleChecks, readHold, savedPlan, computerRule, resultJson, promptFor,
|
|
619
|
-
_jobs: jobs, _checks: checkJobs };
|
|
789
|
+
module.exports = { prefixes: ['/api/report-structure', '/api/charts'], handle, sweep, beforeDraft, draftBlock, draftNow, draftState, markDrafting, startCheck, settleChecks, readHold, savedPlan, computerRule, resultJson, promptFor,
|
|
790
|
+
_jobs: jobs, _checks: checkJobs, _pulls: pullJobs };
|
|
@@ -118,6 +118,16 @@ async function handle(req, res, url, ctx) {
|
|
|
118
118
|
const failed = (r) => { ctx.json(res, { ok: false, error: r.error || 'refused', index: r.index == null ? null : r.index }, r.status || 400); return true; };
|
|
119
119
|
|
|
120
120
|
if (p === '/api/report/draft') {
|
|
121
|
+
// A first Draft now of a report with a saved structure is made by the session the schedule would start, so it follows the structure
|
|
122
|
+
// (report-structure-gaps). A Redraft, a draft at a named slot and a report with no saved structure keep the instant draft below.
|
|
123
|
+
if (body.redraft !== true && !cs.slot && typeof ctx.structureDraft === 'function') {
|
|
124
|
+
const sd = ctx.structureDraft(w, cs.cadence);
|
|
125
|
+
if (sd.structured) {
|
|
126
|
+
if (sd.status) return refuse(ctx, res, sd.status, sd.error);
|
|
127
|
+
ctx.json(res, { ok: true, ws: w.id, cadence: cs.cadence, drafting: true, held: !!sd.held, state: sd.state || null });
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
121
131
|
// Draft now, or Redraft. A Redraft keeps every note, byte for byte, by the initiative id it was written under.
|
|
122
132
|
const draft = RD.draftOf(root, { cadence: cs.cadence, slot: cs.slot || undefined });
|
|
123
133
|
if (!draft || draft.ok === false) return failed(draft || { status: 500, error: 'the drafter answered nothing' });
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.243.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.243.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/* pm/c3-audience-link.js — the audience window of a PMOS deck (initiative meeting-material-replay-gaps).
|
|
2
|
+
*
|
|
3
|
+
* audienceChannelName(ws, path) the BroadcastChannel both windows share
|
|
4
|
+
* openAudienceWindow({ ws, path, name, start }) → { win, ch } | null (the browser blocked the window)
|
|
5
|
+
*
|
|
6
|
+
* The window is /c3.html?audience=1&…, which c3-boot.js hands to c3-audience.js. It shows the slides only. The presenter view and the window tell
|
|
7
|
+
* each other the slide through the channel: { t: 'slide', i } both ways, { t: 'hello' } from a window that has just opened, { t: 'close' } from
|
|
8
|
+
* the presenter view when it ends. The channel carries a slide number and nothing else, and it exists only between pages of this origin. */
|
|
9
|
+
export const audienceChannelName = (ws, path) => 'pmos-audience:' + String(ws || '') + ':' + String(path || '');
|
|
10
|
+
|
|
11
|
+
export function openAudienceWindow({ ws, path, name, start } = {}) {
|
|
12
|
+
if (typeof window === 'undefined' || typeof window.open !== 'function') return null;
|
|
13
|
+
const q = new URLSearchParams({ audience: '1', ws: String(ws || ''), path: String(path || ''), name: String(name || 'Deck').slice(0, 120), start: String(Math.max(0, Number(start) || 0)) });
|
|
14
|
+
try { if (new URLSearchParams(location.search).get('demo') === '1') q.set('demo', '1'); } catch { /* no location */ }
|
|
15
|
+
let win = null;
|
|
16
|
+
try { win = window.open('/c3.html?' + q.toString(), 'pmos-audience', 'popup=yes,width=1280,height=720'); } catch { win = null; }
|
|
17
|
+
if (!win || win.closed) return null;
|
|
18
|
+
let ch = null;
|
|
19
|
+
try { ch = typeof BroadcastChannel === 'function' ? new BroadcastChannel(audienceChannelName(ws, path)) : null; } catch { ch = null; }
|
|
20
|
+
return { win, ch };
|
|
21
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/* pm/c3-audience.js — the audience window of a PMOS deck (initiative meeting-material-replay-gaps). c3-boot.js loads it alone for
|
|
2
|
+
* /c3.html?audience=1&ws=…&path=…&name=…&start=N, opened by c3-audience-link.js.
|
|
3
|
+
*
|
|
4
|
+
* It reads the deck through the routes the presenter view already uses and shows the slides only, full window: no speaker notes, no toolbar. It
|
|
5
|
+
* follows the presenter's slide through a BroadcastChannel and tells the presenter when a key here moves it. Escape, or the presenter ending
|
|
6
|
+
* the session, closes the window. Nothing is written and nothing is sent. */
|
|
7
|
+
import { el } from './dom.js';
|
|
8
|
+
import { createDeck } from './c3-report-deck.js';
|
|
9
|
+
import { audienceChannelName } from './c3-audience-link.js';
|
|
10
|
+
import { ensureC3Css } from './pm-work-view.js';
|
|
11
|
+
|
|
12
|
+
const q = new URLSearchParams(location.search);
|
|
13
|
+
const WS = q.get('ws') || '';
|
|
14
|
+
const PATH = q.get('path') || '';
|
|
15
|
+
const NAME = (q.get('name') || 'Deck').slice(0, 120);
|
|
16
|
+
const START = Math.max(0, parseInt(q.get('start') || '0', 10) || 0);
|
|
17
|
+
|
|
18
|
+
function say(text) {
|
|
19
|
+
document.body.append(el('div', { class: 'c3 aud-msg', 'data-aud': 'message' }, el('p', null, text)));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function runAudiencePage() {
|
|
23
|
+
document.title = 'PMOS · Audience view · ' + NAME;
|
|
24
|
+
document.body.classList.add('c3-aud-win');
|
|
25
|
+
ensureC3Css();
|
|
26
|
+
// Demo mode (?demo=1, the mockup's sample content) answers the reads of this window as it does for the page that opened it.
|
|
27
|
+
if (q.get('demo') === '1') { try { const m = await import('./c3-demo.js'); m.installDemo(); if (window.__c3demoReady) await window.__c3demoReady; } catch { /* the reads then go to the server */ } }
|
|
28
|
+
if (!PATH) { say('This window needs a deck. Open Audience view from the deck again.'); return; }
|
|
29
|
+
let ch = null;
|
|
30
|
+
try { ch = typeof BroadcastChannel === 'function' ? new BroadcastChannel(audienceChannelName(WS, PATH)) : null; } catch { ch = null; }
|
|
31
|
+
const deck = createDeck({ ws: WS, path: PATH, name: NAME, cycle: '', standalone: true, onSlide: (i) => { if (ch) ch.postMessage({ t: 'slide', i }); } });
|
|
32
|
+
const t0 = Date.now();
|
|
33
|
+
while (!deck.state.loaded && Date.now() - t0 < 10000) await new Promise((r) => setTimeout(r, 60));
|
|
34
|
+
const S = deck.state;
|
|
35
|
+
if (!S.loaded || !S.cyc || !S.cyc.designed || !S.R || S.R.format !== 'deck') { say('This report has no designed deck yet, so there are no slides to show.'); return; }
|
|
36
|
+
deck.present('aud', START);
|
|
37
|
+
if (ch) {
|
|
38
|
+
ch.onmessage = (e) => {
|
|
39
|
+
const m = e && e.data;
|
|
40
|
+
if (!m || typeof m !== 'object') return;
|
|
41
|
+
if (m.t === 'slide' && Number.isInteger(m.i)) deck.goTo(m.i, { quiet: true });
|
|
42
|
+
else if (m.t === 'close') window.close();
|
|
43
|
+
};
|
|
44
|
+
ch.postMessage({ t: 'hello' });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const narrow = typeof matchMedia === 'function' && matchMedia('(max-width:640px)').matches;
|
|
7
7
|
let desk = false;
|
|
8
8
|
try { desk = localStorage.getItem('pmos.c3.testDesk') === 'desk'; } catch { /* no storage */ }
|
|
9
|
-
if (new URLSearchParams(location.search).get('
|
|
9
|
+
if (new URLSearchParams(location.search).get('audience') === '1') import('./c3-audience.js').then((m) => m.runAudiencePage()); // the audience window of a PMOS deck: slides only, in a window of its own
|
|
10
|
+
else if (new URLSearchParams(location.search).get('capture') === '1') import('./c3-capture.js'); // the PMOS Chrome extension's popup page: a small page on its own, at any width
|
|
10
11
|
else if (narrow && !desk) import('./c3-phone-boot.js');
|
|
11
12
|
else import('./c3-shell.js');
|
|
@@ -26,6 +26,7 @@ import { pmPost } from './http.js';
|
|
|
26
26
|
import { notesView, openNote, noteOfEvent, primeNotes } from './c3-notes.js'; // Calendar › Notes (initiative c3-notes-place)
|
|
27
27
|
import { onNotes } from './notes-links.js';
|
|
28
28
|
import { openSheet } from './c3-sheet.js';
|
|
29
|
+
import { materialSection } from './c3-material.js'; // Material on the meeting card (initiative meeting-material-replay)
|
|
29
30
|
|
|
30
31
|
const LAYERS = [
|
|
31
32
|
['meetings', 'Meetings'],
|
|
@@ -669,6 +670,15 @@ const focusBtn = (e, date, ui) => button({ label: [c3ic('target', 'sm'), 'Focus'
|
|
|
669
670
|
attrs: { 'data-cal3': 'focus', title: 'Open the meeting focus page: the clock, the topics, the material and PMOS beside the conversation' },
|
|
670
671
|
onClick: () => { if (ui) { ui.st.sel = null; closeMeetingSheet(ui.r); ui.redraw(); } import('./c3-focus.js').then((m) => m.openFocus({ event: e, date })).catch(() => toast({ text: 'The focus page could not load.', tone: 'error' })); } });
|
|
671
672
|
|
|
673
|
+
/* "Replay": the full-screen replay of a meeting that has ended and has a recorded note (c3-replay.js); it closes the popup first, as Focus does */
|
|
674
|
+
const replayBtn = (e, ui) => {
|
|
675
|
+
const n = noteOfEvent(e);
|
|
676
|
+
if (!n || n.private || n.state === 'recording' || n.state === 'paused' || !(n.segments > 0 || (n.words || 0) > 0)) return null;
|
|
677
|
+
return button({ label: [c3ic('play', 'sm'), 'Replay'], variant: 'ghost', size: 'sm',
|
|
678
|
+
attrs: { 'data-cal3': 'replay', title: 'Open the meeting replay: the topics, the key points, the screen and the conversation along the audio' },
|
|
679
|
+
onClick: () => { if (ui) { ui.st.sel = null; closeMeetingSheet(ui.r); ui.redraw(); } import('./c3-replay.js').then((m) => m.openReplay({ note: n.id })).catch(() => toast({ text: 'The replay could not load.', tone: 'error' })); } });
|
|
680
|
+
};
|
|
681
|
+
|
|
672
682
|
function meetingDetail(ui, hit, inSheet) {
|
|
673
683
|
const { M, h } = ui; const e = hit.e;
|
|
674
684
|
const s = h.timeOf(e.at); const f = s === null ? null : h.endH(e);
|
|
@@ -692,7 +702,7 @@ function meetingDetail(ui, hit, inSheet) {
|
|
|
692
702
|
if (!M.ctx) body = [fold('info', 'The calendar sent no meeting context, so PMOS shows nothing for this meeting.', { 'data-cal3': 'no-context' })];
|
|
693
703
|
else if (past && (!rec || rec.held === false)) body = [fold('moon', 'Nothing held for this meeting: no note, decisions or linked items yet.', { 'data-cal3': 'nothing-held' })];
|
|
694
704
|
else if (past) body = [list(['decision', 'action'], 'What came out of it', 'out', 'No decision or action is recorded.'), list(['moved'], 'What moved after it', 'moved', 'Nothing moved within 48 hours that refers to it.')];
|
|
695
|
-
else body = upcomingBody(ui, rec, items);
|
|
705
|
+
else body = upcomingBody(ui, rec, items, e, hit.date);
|
|
696
706
|
const b = !past && rec && rec.brief ? rec.brief : null;
|
|
697
707
|
const lv = !past && rec ? rec.level || null : null;
|
|
698
708
|
const iniId = lv && lv.kind !== 'initiative' ? null : typeof e.initiative === 'string' && !(lv && lv.by === 'pm') ? e.initiative : b && b.initiative ? b.initiative.id : null;
|
|
@@ -707,7 +717,7 @@ function meetingDetail(ui, hit, inSheet) {
|
|
|
707
717
|
// the popup: the sheet's header carries the title and Close; at Half and wider the note sits beside what came out of it
|
|
708
718
|
const side = noteCard(ui, e);
|
|
709
719
|
return [
|
|
710
|
-
el('div', { class: 'cal3-dt' }, el('span', { class: 'tag' + (past ? '' : ' cal3-up') }, past ? 'past' : 'upcoming'), !past && s !== null && hit.date === ui.d.today ? focusBtn(e, hit.date, ui) : null),
|
|
720
|
+
el('div', { class: 'cal3-dt' }, el('span', { class: 'tag' + (past ? '' : ' cal3-up') }, past ? 'past' : 'upcoming'), !past && s !== null && hit.date === ui.d.today ? focusBtn(e, hit.date, ui) : null, past ? replayBtn(e, ui) : null),
|
|
711
721
|
el('div', { class: 'dim mono cal3-when' }, h.dayLabel(hit.date) + (s === null ? ', all day' : ' · ' + h.hhmm(s) + ' to ' + h.hhmm(f))),
|
|
712
722
|
chipsRow(),
|
|
713
723
|
past || !M.ctx ? null : levelRow(ui, e, lv),
|
|
@@ -827,7 +837,7 @@ function openBoardTab(ui, board, tab) {
|
|
|
827
837
|
ui.h.gotoTab('init');
|
|
828
838
|
}
|
|
829
839
|
|
|
830
|
-
function upcomingBody(ui, rec, items) {
|
|
840
|
+
function upcomingBody(ui, rec, items, e, date) {
|
|
831
841
|
const { h } = ui;
|
|
832
842
|
const b = rec && rec.brief ? rec.brief : null;
|
|
833
843
|
const lv = rec && rec.level ? rec.level : null;
|
|
@@ -857,6 +867,8 @@ function upcomingBody(ui, rec, items) {
|
|
|
857
867
|
el('span', { class: 'tag' + (x.done ? ' cal3-up' : '') }, CTXWORD[x.kind] || x.kind), el('span', { class: 'cal3-ct' }, x.text),
|
|
858
868
|
x.at ? el('span', { class: 'dim mono cal3-at' }, h.dayLabel(x.at) + (h.timeOf(x.at) !== null ? ' ' + h.hhmm(h.timeOf(x.at)) : '')) : null,
|
|
859
869
|
x.ref && REF[x.ref.type] ? refLink(ui, x.ref, { title: x.text }) : null))])));
|
|
870
|
+
if (e) out.push(materialSection({ event: e, date, level: lv, brief: b,
|
|
871
|
+
back: () => { if (!ui.st.sel) { ui.st.sel = { type: 'event', id: e.id }; ui.redraw(); } } }));
|
|
860
872
|
if (b && ((b.links || []).length || (b.refs || []).length)) out.push(el('div', { class: 'chips2', 'data-cal3': 'meeting-links' },
|
|
861
873
|
...(b.links || []).map((l) => el('a', { class: 'chip2', href: l.url, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), l.label)),
|
|
862
874
|
...(b.refs || []).map((r) => r.link ? el('a', { class: 'chip2', href: r.link, target: '_blank', rel: 'noopener noreferrer' }, c3ic('out', 'sm'), r.label, r.source ? el('span', { class: 'dim' }, ' ' + r.source) : null)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* pm/c3-demo-material.js — DEMO answers for the material of a meeting (initiative meeting-material-replay, part 1) on /c3.html?demo=1.
|
|
2
|
+
* c3-demo.js passes GET and POST /api/meeting-material and GET /api/meeting-material/suggest here, and adds the material of the meeting to the
|
|
3
|
+
* answer of GET /api/meeting-focus. The data is the approved mockup's: PMOS suggests the Partner API decision deck, the weekly report and a
|
|
4
|
+
* policy document; a search finds three more. The store lives in the page: a reload forgets it and nothing reaches the server. Real mode never loads this file. */
|
|
5
|
+
|
|
6
|
+
const rows = []; // { key, scope, event, item, topic, present, removed? } in the order they were added; the last of an item wins
|
|
7
|
+
const TOPICS = ['Context and goal', 'Options: soft vs hard limits', 'Partner impact: top 20 traffic', 'Partner comms and timing', 'Decide and next steps']
|
|
8
|
+
.map((text) => ({ id: text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''), text }));
|
|
9
|
+
const WS = '881665c933';
|
|
10
|
+
const SUGGESTED = [
|
|
11
|
+
{ kind: 'pmos-deck', ref: 'planning/reports/rs1/2026-10-21.md', title: 'Partner API rate limits: decision deck', meta: 'Reports · deck · draft', why: 'Your deck, edited Fri 9 Oct', present: true, ws: WS },
|
|
12
|
+
{ kind: 'report', ref: 'planning/reports/rs3/2026-10-14.md', title: 'Partner API weekly report · cycle 41', meta: 'Reports · document · issued', why: 'The report this review reads each week', present: false, ws: WS },
|
|
13
|
+
{ kind: 'pmos-doc', ref: 'planning/prd/partner-agreement-prd.md', title: 'Partner agreement v3: fair use (section 4.2)', meta: 'IDPF-07 documents', why: 'Named in the last note: "check the agreement"', present: false, ws: WS },
|
|
14
|
+
];
|
|
15
|
+
const FIND = [
|
|
16
|
+
{ kind: 'pmos-deck', ref: 'planning/reports/rs1/2026-10-07.md', title: 'Payments 1.5 release review deck', meta: 'Reports · deck · issued', present: true, ws: WS },
|
|
17
|
+
{ kind: 'report', ref: 'planning/reports/rs3/2026-10-02.md', title: 'Monthly product review · September', meta: 'Reports · document · issued', present: false, ws: WS },
|
|
18
|
+
{ kind: 'pmos-doc', ref: 'planning/prd/idpf-07-charter.md', title: 'IDPF-07 charter', meta: 'IDPF-07 documents', present: false, ws: WS },
|
|
19
|
+
];
|
|
20
|
+
const norm = (t) => String(t || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
21
|
+
const keyOf = (e) => (e && e.series ? 's:' + e.series : 't:' + norm(e && e.title));
|
|
22
|
+
const idOf = (r) => [r.key, r.scope === 'series' ? '' : r.event || '', r.item.kind, r.item.ref || r.item.url].join('|');
|
|
23
|
+
const classify = (u) => {
|
|
24
|
+
const s = String(u || '').trim(); let x; try { x = new URL(/^[a-z][a-z0-9+.-]*:/i.test(s) ? s : 'https://' + s); } catch { return null; }
|
|
25
|
+
if (x.protocol !== 'https:') return null;
|
|
26
|
+
if (x.hostname === 'docs.google.com' && /^\/presentation\/d\//.test(x.pathname)) return { kind: 'slides', url: x.href };
|
|
27
|
+
if (x.hostname === 'docs.google.com' && /^\/document\/d\//.test(x.pathname)) return { kind: 'gdoc', url: x.href };
|
|
28
|
+
if (/\.atlassian\.net$/.test(x.hostname) && x.pathname.startsWith('/wiki')) return { kind: 'conf', url: x.href };
|
|
29
|
+
return { kind: 'link', url: x.href };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** The items of one meeting, in the shape of GET /api/meeting-material. event: { id?, title, series? } */
|
|
33
|
+
export function materialFor(event) {
|
|
34
|
+
const key = keyOf(event); const held = new Map();
|
|
35
|
+
for (const r of rows) { const id = idOf(r); if (r.removed) held.delete(id); else held.set(id, r); }
|
|
36
|
+
return [...held.values()].filter((r) => r.key === key && (r.scope === 'series' || r.event === String(event.id || ''))).map((r) => ({
|
|
37
|
+
id: Math.abs([...idOf(r)].reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 7)).toString(16), kind: r.item.kind, ref: r.item.ref || null, url: r.item.url || null, title: r.item.title,
|
|
38
|
+
ws: r.item.ws || null, topic: r.topic || null, present: r.present === true, scope: r.scope, from: 'you', by: 'pm:wawan', at: null }));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function materialGet(path, url) {
|
|
42
|
+
const q = new URL(url, location.origin).searchParams;
|
|
43
|
+
if (path === '/api/meeting-material/suggest') {
|
|
44
|
+
const t = (q.get('q') || '').toLowerCase();
|
|
45
|
+
return { ok: true, suggested: SUGGESTED, found: t.length >= 2 ? FIND.filter((x) => x.title.toLowerCase().includes(t)) : [] };
|
|
46
|
+
}
|
|
47
|
+
const event = { id: q.get('event') || '', title: q.get('title') || '', series: q.get('series') || '' };
|
|
48
|
+
return { ok: true, key: keyOf(event), items: materialFor(event), ...(q.get('topics') ? { topics: TOPICS } : {}) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function materialPost(b) {
|
|
52
|
+
const e = b.event || {}; const it = b.item || {};
|
|
53
|
+
const item = it.ref ? { kind: it.kind, ref: it.ref, title: it.title || it.ref, ws: it.ws } : (() => { const c = classify(it.url); return c ? { ...c, title: it.title || c.url } : null; })();
|
|
54
|
+
if (!item) return { ok: false, error: 'a link must be an https address' };
|
|
55
|
+
const scope = b.scope === 'series' ? 'series' : 'meeting';
|
|
56
|
+
rows.push({ key: keyOf(e), scope, event: scope === 'meeting' ? String(e.id || '') : '', item, topic: b.topic || null, present: b.op !== 'remove' && b.present === true, removed: b.op === 'remove' });
|
|
57
|
+
return { ok: true, items: materialFor(e) };
|
|
58
|
+
}
|
|
@@ -12,6 +12,7 @@ import { el } from './dom.js';
|
|
|
12
12
|
import * as D from './c3-demo-data.js';
|
|
13
13
|
import { installInit } from './c3-demo-init.js';
|
|
14
14
|
import { focusGet, focusPost } from './c3-demo-focus.js'; // Meeting focus: the mockup's Partner API review (GET /api/meeting-focus, its ask poll, and the topic and ask POSTs)
|
|
15
|
+
import { materialGet, materialPost, materialFor } from './c3-demo-material.js'; // Meeting material: GET and POST /api/meeting-material, its suggestions, and the material in the Focus read
|
|
15
16
|
import { installBoards } from './c3-demo-boards.js'; // Initiatives boards: answers every /api/boards GET and POST (installed outermost, in installDemo)
|
|
16
17
|
import { installCoreDocs } from './c3-demo-coredocs.js'; // Core documents of each level: GET /api/core-docs and its POSTs (installed outermost, after installBoards)
|
|
17
18
|
import './c3-demo-cal-ix.js'; // Calendar and the ribbon: sets window.__c3calix (calendar-views.js reads it in demo only)
|
|
@@ -53,7 +54,9 @@ function getRoute(path, url) {
|
|
|
53
54
|
switch (path) {
|
|
54
55
|
case '/api/briefing': return D.briefing(url);
|
|
55
56
|
case '/api/calendar-views': return withCalendarPage(D.calendarViews(url));
|
|
56
|
-
case '/api/meeting-focus':
|
|
57
|
+
case '/api/meeting-focus': { const o = focusGet(path, url); return o && o.ok ? { ...o, material: [...materialFor({ id: q.get('event') || '', title: q.get('title') || '' }), ...materialFor(o.event)] } : o; } // what was asked for, and the sample meeting the demo answers with
|
|
58
|
+
case '/api/meeting-focus/ask': case '/api/meeting-focus/now': return focusGet(path, url);
|
|
59
|
+
case '/api/meeting-material': case '/api/meeting-material/suggest': return materialGet(path, url);
|
|
57
60
|
case '/api/pm-items': return D.pmItems();
|
|
58
61
|
case '/api/runs': return D.runs();
|
|
59
62
|
case '/api/releases': return D.releases();
|
|
@@ -78,6 +81,7 @@ function postRoute(path, body) {
|
|
|
78
81
|
let b = {};
|
|
79
82
|
try { b = body ? JSON.parse(body) : {}; } catch { b = {}; }
|
|
80
83
|
if (path.startsWith('/api/meeting-focus/')) return focusPost(path, body);
|
|
84
|
+
if (path === '/api/meeting-material') return materialPost(b);
|
|
81
85
|
if (path === '/api/pm-items/add') return { ok: true, item: { id: 'item-demo-new' + (++added), title: String(b.line || '').trim() || 'New item', due: null, priority: null, waiting_on: null, job: null, steps: [], created_at: D.NOW_ISO + 'Z' } };
|
|
82
86
|
if (path === '/api/pm-items/remove') return { ok: true, undo_ms: 8000 };
|
|
83
87
|
if (path === '/api/calendar-links') return (window.__c3links = (window.__c3links || []).concat([b])), { ok: true, link: b.kind === 'none' ? null : { kind: b.kind, ws: b.ws || null, board: b.board || null, initiative: b.initiative || null, title: b.title || null, by: 'pm:wawan' } }; // nothing is saved
|