create-pmos 0.242.0 → 0.244.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 +2 -2
- package/workspace/guide/notes.md +5 -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 +16 -4
- package/workspace/lib/meeting-focus.js +6 -4
- package/workspace/lib/meeting-replay.js +15 -1
- package/workspace/lib/report-structure.js +49 -5
- package/workspace/lib/routes-meeting-focus.js +1 -0
- 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-material.js +11 -1
- package/workspace/public/pm/c3-replay.css +6 -0
- package/workspace/public/pm/c3-replay.js +19 -3
- 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-reports.js +10 -1
- package/workspace/public/pm/report-view.js +23 -1
- package/workspace/public/pm/reports-copy.js +5 -0
- package/workspace/server.js +8 -1
- package/workspace/skills/report-structure-pull.skill +45 -0
- package/workspace/test-lists/report-structure-gaps.json +5 -0
|
@@ -36,7 +36,7 @@ const crypto = require('crypto');
|
|
|
36
36
|
const RD = require('./report-draft.js');
|
|
37
37
|
const CS = require('./charts-store.js');
|
|
38
38
|
|
|
39
|
-
const EVENTS = Object.freeze(['describe', 'turn', 'version', 'accept', 'revert', 'source', 'remove', 'dry', 'check', 'error']);
|
|
39
|
+
const EVENTS = Object.freeze(['describe', 'turn', 'version', 'accept', 'revert', 'source', 'remove', 'dry', 'pull', 'check', 'error']);
|
|
40
40
|
const PMOS_KINDS = Object.freeze(['pmos-level', 'dashboard', 'chart', 'document', 'past-report']);
|
|
41
41
|
const OUTSIDE_KINDS = Object.freeze(['connector', 'link', 'upload', 'person', 'chrome', 'computer']);
|
|
42
42
|
const KINDS = Object.freeze([...PMOS_KINDS, ...OUTSIDE_KINDS]);
|
|
@@ -79,7 +79,7 @@ function paths(root, id) {
|
|
|
79
79
|
const folder = path.join(abs, rel);
|
|
80
80
|
const dir = path.join(folder, 'structure');
|
|
81
81
|
return { abs, rel, folder, dir, relDir: `${rel}/structure`, events: path.join(dir, 'structure.jsonl'), versions: path.join(dir, 'versions'), pending: path.join(dir, 'pending'),
|
|
82
|
-
dry: path.join(dir, 'dry'), checks: path.join(dir, 'checks'), checksPending: path.join(dir, 'checks', 'pending'), brief: path.join(dir, 'brief.md'), briefRel: `${rel}/structure/brief.md` };
|
|
82
|
+
dry: path.join(dir, 'dry'), pull: path.join(dir, 'pull'), pullPending: path.join(dir, 'pull', 'pending'), checks: path.join(dir, 'checks'), checksPending: path.join(dir, 'checks', 'pending'), brief: path.join(dir, 'brief.md'), briefRel: `${rel}/structure/brief.md` };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// ── the one writer and the fold ───────────────────────────────────────────────────────────────────────────
|
|
@@ -440,6 +440,50 @@ function landVersion(root, id, plan, { turn, answer, questions, options, from, b
|
|
|
440
440
|
return { ok: true, n, plan: v.plan };
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
// ── test pull (report-structure-gaps): ONE source read for the last complete period ──────────────────────────
|
|
444
|
+
/** What a pull job wrote for one source of a plan. → { ok, pull } | refusal. `reached` false is a valid answer: the source returned nothing. */
|
|
445
|
+
function validatePull(data, plan, sourceId) {
|
|
446
|
+
if (!isObj(data)) return refuse(422, 'the pull result must be a JSON object');
|
|
447
|
+
const src = listOf(plan && plan.sources).find((s) => s.id === sourceId);
|
|
448
|
+
if (!src) return refuse(422, 'the source is not in the plan');
|
|
449
|
+
const chartIds = new Set(listOf(plan.charts).filter((c) => c.source === sourceId).map((c) => c.id));
|
|
450
|
+
if (str(data.source) !== sourceId) return refuse(422, 'source must be the source that was pulled');
|
|
451
|
+
if (!Number.isInteger(data.n) || data.n < 1) return refuse(422, 'n must be the version number');
|
|
452
|
+
const per = badText(data.period, 'period', { max: 40 });
|
|
453
|
+
if (per || !data.period.trim()) return refuse(422, per || 'period is required');
|
|
454
|
+
if (typeof data.reached !== 'boolean') return refuse(422, 'reached must be true or false');
|
|
455
|
+
const rs = badText(data.reason === undefined ? '' : data.reason, 'reason', { max: 300, allowEmpty: true });
|
|
456
|
+
if (rs) return refuse(422, rs);
|
|
457
|
+
const values = data.values === undefined ? [] : data.values;
|
|
458
|
+
if (!Array.isArray(values) || values.length > LIMITS.charts) return refuse(422, `values is a list of at most ${LIMITS.charts}`);
|
|
459
|
+
const out = [];
|
|
460
|
+
for (const [i, v] of values.entries()) {
|
|
461
|
+
const w = `values[${i}]`;
|
|
462
|
+
if (!isObj(v)) return refuse(422, `${w} must be an object`);
|
|
463
|
+
if (!chartIds.has(str(v.chart))) return refuse(422, `${w}.chart ${JSON.stringify(str(v.chart)).slice(0, 40)} is not a chart of the plan that uses this source`);
|
|
464
|
+
const c = {};
|
|
465
|
+
for (const k of ['value', 'was', 'delta', 'note']) { const r = textField(v, k, w); if (r.error) return refuse(422, r.error); c[k] = r.v; }
|
|
466
|
+
if (v.flag !== undefined && typeof v.flag !== 'boolean') return refuse(422, `${w}.flag must be true or false`);
|
|
467
|
+
out.push({ chart: v.chart, value: c.value, was: c.was, delta: c.delta, flag: v.flag === true, note: c.note });
|
|
468
|
+
}
|
|
469
|
+
const reached = data.reached === true;
|
|
470
|
+
if (!reached && out.length) return refuse(422, 'a source that was not reached has no values');
|
|
471
|
+
const got = out.filter((v) => v.value).length;
|
|
472
|
+
const reason = str(data.reason).trim() || (reached && !got ? 'The source answered, but no chart returned a number.' : '');
|
|
473
|
+
return { ok: true, pull: { source: sourceId, n: data.n, period: data.period.trim(), reached, ok: reached && got > 0, reason, values: out, charts: chartIds.size, got } };
|
|
474
|
+
}
|
|
475
|
+
function writePull(root, id, pull) {
|
|
476
|
+
if (!SLUG_RE.test(String(pull.source))) throw new Error('report-structure: bad source id');
|
|
477
|
+
writeAtomic(path.join(paths(root, id).pull, `${pull.source}.json`), JSON.stringify(pull, null, 2) + '\n');
|
|
478
|
+
}
|
|
479
|
+
function readPull(root, id, source) { return SLUG_RE.test(String(source)) ? readJson(path.join(paths(root, id).pull, `${source}.json`)) : null; }
|
|
480
|
+
function readPulls(root, id) {
|
|
481
|
+
const out = {};
|
|
482
|
+
let names = []; try { names = fs.readdirSync(paths(root, id).pull); } catch { return out; }
|
|
483
|
+
for (const f of names) { const m = /^([a-z0-9][a-z0-9-]*)\.json$/.exec(f); if (m && SLUG_RE.test(m[1])) { const o = readPull(root, id, m[1]); if (o) out[m[1]] = o; } }
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
|
|
443
487
|
// ── the GET answer ────────────────────────────────────────────────────────────────────────────────────────
|
|
444
488
|
/** Chrome and computer sources show what their last check found: a passing check is Reached, a failing one Needs you. */
|
|
445
489
|
function overlay(plan, checks) {
|
|
@@ -450,7 +494,7 @@ function overlay(plan, checks) {
|
|
|
450
494
|
return { ...s, state: c.ok ? 'reached' : 'needs-you' };
|
|
451
495
|
}) };
|
|
452
496
|
}
|
|
453
|
-
function structureAnswer(root, id, { running } = {}) {
|
|
497
|
+
function structureAnswer(root, id, { running, pulling } = {}) {
|
|
454
498
|
const st = stateOf(root, id);
|
|
455
499
|
const checks = readChecks(root, id);
|
|
456
500
|
const versions = [...st.versions.values()].sort((a, b) => a.n - b.n).map((v) => ({ n: v.n, at: v.at, turn: v.turn }));
|
|
@@ -458,7 +502,7 @@ function structureAnswer(root, id, { running } = {}) {
|
|
|
458
502
|
running: !!running, job: running ? { since: running.since, turn: running.turn, kind: running.kind } : null,
|
|
459
503
|
current: st.current, saved: st.saved ? st.saved.n : null, versions, log: st.log,
|
|
460
504
|
plan: st.current != null ? overlay(readVersion(root, id, st.current), checks) : null,
|
|
461
|
-
dry: st.dry, checks, errors: st.errors.map((e) => ({ text: e.text, at: e.at })),
|
|
505
|
+
dry: st.dry, checks, pulls: readPulls(root, id), pulling: Array.isArray(pulling) ? pulling : [], errors: st.errors.map((e) => ({ text: e.text, at: e.at })),
|
|
462
506
|
};
|
|
463
507
|
}
|
|
464
508
|
/** What the setups GET carries for one report: { saved, at, charts, sections } of the accepted version, else null. */
|
|
@@ -512,4 +556,4 @@ function writeBrief(root, id, setup, { now } = {}) {
|
|
|
512
556
|
|
|
513
557
|
module.exports = { EVENTS, KINDS, PMOS_KINDS, OUTSIDE_KINDS, STATES, FIXED, LIMITS, CHECK_STEPS, CHECK_STATES, CADENCE_RE, SLUG_RE, RUN_RE,
|
|
514
558
|
paths, append, readEvents, fold, stateOf, nextN, badText, validatePlan, validateSource, validateDry, validateCheck, cleanQuestions, cleanOptions, addSourceTo, removeFrom,
|
|
515
|
-
landVersion, readVersion, rewriteVersion, writeDry, readDry, writeCheck, readCheck, readChecks, writeBrief, structureAnswer, savedOf, overlay, defaultState, newRun };
|
|
559
|
+
landVersion, readVersion, rewriteVersion, writeDry, readDry, validatePull, writePull, readPull, readPulls, writeCheck, readCheck, readChecks, writeBrief, structureAnswer, savedOf, overlay, defaultState, newRun };
|
|
@@ -129,6 +129,7 @@ function settleRun(ctx, key, done, now) {
|
|
|
129
129
|
const res = done && done.result;
|
|
130
130
|
if (done && done.stopped) return append(ctx.runtimeDir, key + '.jsonl', { k: 'fail', at, error: 'The run was stopped before it answered.' });
|
|
131
131
|
if (done && done.timedOut) return append(ctx.runtimeDir, key + '.jsonl', { k: 'fail', at, error: 'The companion took too long and was stopped.' });
|
|
132
|
+
if (res && headless.signedOut(res.text)) return append(ctx.runtimeDir, key + '.jsonl', { k: 'fail', at, auth: true, error: headless.SIGNED_OUT_WHY });
|
|
132
133
|
if (!res || !res.ok || res.isError) return append(ctx.runtimeDir, key + '.jsonl', { k: 'fail', at, error: 'The companion did not answer (' + ((done && done.note) || 'no result') + ').' });
|
|
133
134
|
const parsed = F.parseCompanion(res.text);
|
|
134
135
|
// a malformed answer is dropped, not shown
|
|
@@ -120,10 +120,16 @@ function documentsOf(ctx, w) {
|
|
|
120
120
|
return { docs, reports: r.reports };
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/** The state of a report's structure draft, else null (report-structure-gaps). */
|
|
124
|
+
function draftStateOf(ctx, w, s) {
|
|
125
|
+
if (!ctx || typeof ctx.structureDraftState !== 'function' || s.source !== 'work') return null;
|
|
126
|
+
try { return ctx.structureDraftState(w, s.id) || null; } catch { return null; }
|
|
127
|
+
}
|
|
128
|
+
|
|
123
129
|
/** The saved structure of a report (initiative report-structure): { saved, at, charts, sections }, else null. */
|
|
124
130
|
function structureOf(root, id) { try { return require('./report-structure.js').savedOf(root, id); } catch { return null; } }
|
|
125
131
|
|
|
126
|
-
function setupOut(s, now, docsOf, idx, root) {
|
|
132
|
+
function setupOut(s, now, docsOf, idx, root, draft) {
|
|
127
133
|
const mine = (docsOf.reports || []).filter((x) => x.cadence === s.id);
|
|
128
134
|
const issued = mine.filter((x) => x.issued).map((x) => x.slot).sort().pop() || null;
|
|
129
135
|
const latest = mine.slice().sort((a, b) => (a.slot < b.slot ? 1 : -1))[0] || null;
|
|
@@ -134,6 +140,7 @@ function setupOut(s, now, docsOf, idx, root) {
|
|
|
134
140
|
next_draft: next, last_issued: issued, status: s.status, cycles: mine.length,
|
|
135
141
|
copy: copyOf(latest ? linkFor(idx, latest.path, s.output) : null, docSync.copyKindOf(s.output)),
|
|
136
142
|
structure: structureOf(root, s.id),
|
|
143
|
+
draft: draft || null, // report-structure-gaps: { state: 'drafting' | 'held' | 'ended', since, … } while a draft that follows the saved structure is being made
|
|
137
144
|
};
|
|
138
145
|
}
|
|
139
146
|
|
|
@@ -162,7 +169,7 @@ function listAnswer(ctx, w, now) {
|
|
|
162
169
|
const docsOf = documentsOf(ctx, w);
|
|
163
170
|
const idx = copyIndex(w.root);
|
|
164
171
|
return {
|
|
165
|
-
setups: [...st.setups.values()].map((s) => setupOut(s, new Date(now), docsOf, idx, w.root)),
|
|
172
|
+
setups: [...st.setups.values()].map((s) => setupOut(s, new Date(now), docsOf, idx, w.root, draftStateOf(ctx, w, s))),
|
|
166
173
|
findings: RS.openFindings(st).map((f) => ({ id: f.id, name: f.name, why: f.why, evidence: f.evidence, confidence: f.confidence, proposal: f.proposal })),
|
|
167
174
|
asks: [...st.asks.values()].map((a) => ({ id: a.id, text: a.text, state: a.state, changed: !!a.changed, proposal: a.proposal, error: a.error })),
|
|
168
175
|
finder: finderOut(ctx, st, now),
|
|
@@ -360,6 +367,13 @@ async function handle(req, res, url, ctx) {
|
|
|
360
367
|
if (s.source !== 'work') return send(bad(400, 'a dashboard report is drafted from its dashboard on its day'));
|
|
361
368
|
let next = null; try { next = RS.nextDraftAt(s.schedule, s.last_run, new Date()); } catch { next = null; }
|
|
362
369
|
const day = next ? `${next.getFullYear()}-${String(next.getMonth() + 1).padStart(2, '0')}-${String(next.getDate()).padStart(2, '0')}` : null; // the local day, as setUp names a slot
|
|
370
|
+
// A report with a saved structure is drafted by the session the schedule would start, so the draft follows the structure (report-structure-gaps).
|
|
371
|
+
// One with no saved structure keeps the instant draft below.
|
|
372
|
+
const sd = typeof ctx.structureDraft === 'function' ? ctx.structureDraft(w, s.id) : { structured: false };
|
|
373
|
+
if (sd.structured) {
|
|
374
|
+
if (sd.status) return send(bad(sd.status, sd.error));
|
|
375
|
+
return send({ status: 200, body: { ok: true, drafting: true, held: !!sd.held, state: sd.state || null, id: s.id } });
|
|
376
|
+
}
|
|
363
377
|
const d = draftCycle(root, s.id, day, g.actor);
|
|
364
378
|
if (d.error) return send(bad(500, 'the draft was not written: ' + d.error));
|
|
365
379
|
if (ctx.invalidateNeedsAgg) ctx.invalidateNeedsAgg();
|
|
@@ -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.244.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.244.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');
|
|
@@ -18,6 +18,7 @@ import { ensureComponentsCss } from './primitives.js';
|
|
|
18
18
|
import { icon, pbtn, sect, put, lk, ib, list, empty } from './c3-ini-ui.js';
|
|
19
19
|
import { openSheet, closeSheet } from './c3-sheet.js';
|
|
20
20
|
import { toast } from './c3-toast.js';
|
|
21
|
+
import { openAudienceWindow } from './c3-audience-link.js';
|
|
21
22
|
|
|
22
23
|
/** What each kind of item is called, and which icon it takes. `deck`: PMOS presents it itself. `present`: it can be presented at all. */
|
|
23
24
|
export const KIND = {
|
|
@@ -145,7 +146,16 @@ export function presentItem(item) {
|
|
|
145
146
|
if (item.kind === 'pmos-deck') return showDeck(item, 'pres');
|
|
146
147
|
return openItem(item);
|
|
147
148
|
}
|
|
148
|
-
|
|
149
|
+
/** Audience view: a PMOS deck opens in a window of its own, slides only (initiative meeting-material-replay-gaps). The window opens at once, on the press, because a browser
|
|
150
|
+
* blocks a window that opens after a wait. When it is blocked a notice says so and offers the full-screen view in this window. */
|
|
151
|
+
export function audienceItem(item) {
|
|
152
|
+
if (item && item.kind === 'pmos-deck') {
|
|
153
|
+
if (openAudienceWindow({ ws: item.ws || wsParam(), path: item.ref, name: item.title || 'Deck' })) return;
|
|
154
|
+
toast({ text: 'The audience window was blocked. Allow pop-ups for this page, or use the full-screen view here.', actionLabel: 'Full-screen view here', timeout: 10000, onAction: () => showDeck(item, 'aud') });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
return openItem(item);
|
|
158
|
+
}
|
|
149
159
|
|
|
150
160
|
/* ── the Material part of the meeting card ────────────────────────────────────────────────────────────────────────── */
|
|
151
161
|
|
|
@@ -125,3 +125,9 @@
|
|
|
125
125
|
.c3replay .rpl-lb { min-height: 44px; }
|
|
126
126
|
}
|
|
127
127
|
@media (prefers-reduced-motion: reduce) { .c3replay .rpl-now { transition: none; } }
|
|
128
|
+
|
|
129
|
+
/* meeting-material-replay-gaps: where a recording was off between two starts */
|
|
130
|
+
.c3replay .rpl-gap { position: absolute; top: 0; bottom: 0; left: var(--fc-left, 0%); width: var(--fc-width, 0%); min-width: 0; min-height: 0; height: auto; padding: 0; margin: 0; box-sizing: border-box; border: 0; border-left: 1px dashed var(--muted-foreground); border-right: 1px dashed var(--muted-foreground); background: repeating-linear-gradient(135deg, transparent 0 4px, var(--border) 4px 6px); cursor: pointer; }
|
|
131
|
+
.c3replay .rpl-gap:focus-visible { outline: 2px solid color-mix(in oklab, var(--ring) 50%, var(--foreground)); outline-offset: 2px; }
|
|
132
|
+
.c3replay .rpl-gapnote { display: flex; gap: 8px; align-items: center; font-size: var(--fs-2); }
|
|
133
|
+
.c3replay .rpl-gapkey { display: inline-block; width: 22px; height: 10px; flex: none; border: 1px dashed var(--muted-foreground); background: repeating-linear-gradient(135deg, transparent 0 3px, var(--border) 3px 5px); }
|