create-pmos 0.186.0 → 0.188.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/scripts/workflow-state.js +2 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/c3-ib-triage-backlog-test.js +20 -13
- package/workspace/c3-ini-life-test.js +167 -0
- package/workspace/c3-initiatives-test.js +6 -6
- package/workspace/c3-phone-test.js +67 -16
- package/workspace/c3-quiet-test.js +2 -1
- package/workspace/guide/initiative-boards.md +37 -2
- package/workspace/guide/my-work.md +1 -0
- package/workspace/guide/phone-companion.md +19 -4
- package/workspace/lib/day-run.js +4 -3
- package/workspace/lib/engine/boards.js +73 -18
- package/workspace/lib/engine/workflow-state.js +2 -1
- package/workspace/lib/initiative-rows.js +2 -2
- package/workspace/lib/my-work.js +2 -1
- package/workspace/lib/routes-boards.js +119 -6
- package/workspace/lib/routes-meeting-focus.js +19 -7
- package/workspace/lib/vendor/scripts/workflow-state.js +2 -1
- package/workspace/lib/work-assigned.js +1 -1
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-demo-boards.js +82 -17
- package/workspace/public/pm/c3-demo-focus.js +39 -1
- package/workspace/public/pm/c3-focus.css +53 -0
- package/workspace/public/pm/c3-focus.js +188 -44
- package/workspace/public/pm/c3-ib-initiatives.js +127 -60
- package/workspace/public/pm/c3-ib-items.js +2 -2
- package/workspace/public/pm/c3-ini-life.css +32 -0
- package/workspace/public/pm/c3-ini-life.js +379 -0
- package/workspace/public/pm/c3-ini-ui.js +12 -7
- package/workspace/public/pm/c3-initiatives.js +14 -4
- package/workspace/public/pm/c3-phone.css +3 -1
- package/workspace/public/pm/c3-phone.js +28 -8
- package/workspace/public/pm/console3.css +3 -0
- package/workspace/public/pm/pm-work-view.js +11 -5
- package/workspace/public/pm/run-card.js +12 -3
- package/workspace/public/pm/sessions-panel.js +2 -2
- package/workspace/server.js +25 -3
- package/workspace/test-lists/c3-ini-life.json +9 -0
|
@@ -196,7 +196,17 @@ export function createStore() {
|
|
|
196
196
|
'GRO-02': { problem: R({ text: 'Partner admins only log in when something breaks, so they miss new features and usage trends.' }, -1) },
|
|
197
197
|
'OFF-01': { launch: R({ items: [{ id: 'l1', text: 'Three bids compared', done: true }, { id: 'l2', text: 'Legal review of the contract', done: false }, { id: 'l3', text: 'Contract signed', done: false }] }, -3), learnings: R({ items: [{ id: 'n1', text: 'Ask for fixed-price bids first: two of three changed their price after the site visit.', date: D(-6) }] }, -6) },
|
|
198
198
|
};
|
|
199
|
-
|
|
199
|
+
/* c3-initiative-lifecycle: a likely duplicate, one on hold, one cancelled, two archived (one merged into IDPF-07) */
|
|
200
|
+
const L = (id, title, stage, owner, anchor, horizon, status, s, e, progress, extra) => Object.assign(I(id, 'idpf', title, stage, owner, anchor, horizon, status, s, e, progress, 'feature', false), { featured: false }, extra || {});
|
|
201
|
+
rows.push(
|
|
202
|
+
L('IDPF-151', 'Guided self-serve setup for new workspaces', 'Discover', 'Lina Kusuma', 'KR 2.1 Activation to 45%', 'next', 'on track', -3, 60, 0.05, { dupOf: 'IDPF-12' }),
|
|
203
|
+
L('IDPF-152', 'SAML SSO for enterprise trials', 'Plan', 'You', 'KR 3.2 Net revenue retention 115%', 'now', 'on hold', -30, 40, 0.3, { was: 'on track', stopped: { reason: 'Waiting for the Q1 budget decision', at: at(-11, 10), back: D(19) } }),
|
|
204
|
+
L('IDPF-153', 'Sunset the v1 SDK', 'Build & Deliver', 'You', 'KR 1.3 Partner uptime 99.9%', 'now', 'cancelled', -50, 10, 0.4, { was: 'at risk', stopped: { reason: 'Not worth it now: three partners still on v1', at: at(-6, 10), back: null } }),
|
|
205
|
+
L('IDPF-154', 'Refresh the marketing site footer', 'Discover', '', 'KR 2.3 WAU partners +20%', 'later', 'on track', -60, -20, 0, { archived: { at: at(-2, 10), by: 'You', reason: 'Not related to this board', into: null, moved: { epics: [], tasks: [] } } }),
|
|
206
|
+
L('IDPF-155', 'Throttle bursty partner API calls', 'Discover', 'You', 'KR 1.3 Partner uptime 99.9%', 'now', 'on track', -20, 30, 0.05, { archived: { at: at(-4, 10), by: 'You', reason: 'Merged into Partner API rate limits', into: 'IDPF-07', moved: { epics: [], tasks: [] } } }),
|
|
207
|
+
);
|
|
208
|
+
tasks.push({ id: 'task-demo-l1', board: 'idpf', initiative: 'IDPF-151', title: 'Sketch the setup checklist', owner: 'Lina Kusuma', state: 'To do', start: D(1), due: D(8), epic: null, kind: 'Task', history: [] });
|
|
209
|
+
return { boards, rows, asks, releases, events, jira, tasks, records, seq: 0, notDup: [] };
|
|
200
210
|
}
|
|
201
211
|
|
|
202
212
|
/* ── reads ── */
|
|
@@ -205,16 +215,22 @@ const wfOf = (S, r) => r.workflow || boardOf(S, r.board).workflow;
|
|
|
205
215
|
const catIn = (wf, stage) => ((wf.find((s) => s.name === stage) || {}).cat) || 'Plan';
|
|
206
216
|
const catOfRow = (S, r) => catIn(wfOf(S, r), r.stage);
|
|
207
217
|
const tplEdited = (b) => !INI_TEMPLATES[b.template] || !sameFlow(tplStages(b.template), b.workflow);
|
|
208
|
-
const
|
|
218
|
+
const ACTIVE = ['on track', 'at risk', 'late'];
|
|
219
|
+
const isOpen = (r) => ACTIVE.includes(r.status) && !r.archived; // on hold, done, cancelled and archived are not open
|
|
220
|
+
const live = (r) => !r.archived;
|
|
221
|
+
const mergedFrom = (S, id) => S.rows.filter((x) => x.archived && x.archived.into === id).map((x) => x.id);
|
|
209
222
|
const nextRelease = (S, bid) => S.releases.filter((x) => x.board === bid && x.date >= TODAY).sort((a, c) => (a.date < c.date ? -1 : 1))[0] || null;
|
|
210
223
|
const openAsks = (S, bid) => S.asks.filter((a) => a.board === bid && a.state === 'open').length;
|
|
211
224
|
function rowOut(S, r) {
|
|
212
225
|
const wf = wfOf(S, r);
|
|
213
226
|
return { id: r.id, title: r.title, lane: r.lane, stage: r.stage, cat: catIn(wf, r.stage), owner: r.owner || null, anchor: r.anchor, horizon: r.horizon, status: r.status, start: r.start, end: r.end, progress: r.progress,
|
|
214
|
-
warnings: r.warnings.slice(), template: r.template || null, workflow: wf.map((s) => ({ name: s.name, cat: s.cat })), epics: (r.epics || []).slice(), epicsFrom: r.epics ? 'pmos' : null
|
|
227
|
+
warnings: r.warnings.slice(), template: r.template || null, workflow: wf.map((s) => ({ name: s.name, cat: s.cat })), epics: (r.epics || []).slice(), epicsFrom: r.epics ? 'pmos' : null,
|
|
228
|
+
lifecycle: r.stopped ? (r.status === 'on hold' ? 'on-hold' : 'cancelled') : null, stopped: r.stopped ? { ...r.stopped } : null,
|
|
229
|
+
archived: r.archived ? { ...r.archived, intoTitle: r.archived.into ? (S.rows.find((x) => x.id === r.archived.into) || {}).title || r.archived.into : undefined } : null,
|
|
230
|
+
mergedFrom: mergedFrom(S, r.id), dupOf: r.dupOf && !r.archived && ACTIVE.includes(r.status) && !(S.notDup || []).includes(r.id + '|' + r.dupOf) ? r.dupOf : null };
|
|
215
231
|
}
|
|
216
232
|
function boardOut(S, b) {
|
|
217
|
-
const list = S.rows.filter((r) => r.board === b.id);
|
|
233
|
+
const list = S.rows.filter((r) => r.board === b.id && live(r));
|
|
218
234
|
const byCat = Object.fromEntries(CATS.map((c) => [c, 0]));
|
|
219
235
|
for (const r of list) byCat[catOfRow(S, r)]++;
|
|
220
236
|
const rel = nextRelease(S, b.id);
|
|
@@ -224,24 +240,28 @@ function boardOut(S, b) {
|
|
|
224
240
|
asks: openAsks(S, b.id), nextRelease: rel ? { id: rel.id, name: rel.name, at: rel.date, date: rel.date, status: rel.status } : null };
|
|
225
241
|
}
|
|
226
242
|
const H = { now: 0, next: 1, later: 2 };
|
|
227
|
-
const SS = { late: 0, 'at risk': 1, 'on track': 2,
|
|
243
|
+
const SS = { late: 0, 'at risk': 1, 'on track': 2, 'on hold': 3, done: 4, cancelled: 5 };
|
|
228
244
|
/** The saved view and the search, as the server applies them (the base the facets count). */
|
|
229
245
|
function viewRows(S, b, p) {
|
|
230
246
|
const q = String(p.q || '').trim().toLowerCase();
|
|
231
|
-
const
|
|
247
|
+
const archive = p.archived === '1';
|
|
248
|
+
const view = archive ? 'all' : (p.view || 'now');
|
|
249
|
+
const given = !!p.status; // a status filter overrides the view's own status rule
|
|
250
|
+
const act = (r) => given || isOpen(r);
|
|
232
251
|
return S.rows.filter((r) => {
|
|
233
|
-
if (r.board !== b.id) return false;
|
|
234
|
-
if (view === 'now' && (!['now', 'next'].includes(r.horizon) || !
|
|
235
|
-
if (view === 'mine' && (r.owner !== 'You' || !
|
|
236
|
-
if (view === 'risk' && !['at risk', 'late'].includes(r.status)) return false;
|
|
237
|
-
if (view === 'noowner' && (r.owner || !
|
|
252
|
+
if (r.board !== b.id || !!r.archived !== archive) return false;
|
|
253
|
+
if (view === 'now' && (!['now', 'next'].includes(r.horizon) || !act(r))) return false;
|
|
254
|
+
if (view === 'mine' && (r.owner !== 'You' || !act(r))) return false;
|
|
255
|
+
if (view === 'risk' && !given && !['at risk', 'late'].includes(r.status)) return false;
|
|
256
|
+
if (view === 'noowner' && (r.owner || !act(r))) return false;
|
|
238
257
|
if (q && !(r.id + ' ' + r.title + ' ' + r.anchor + ' ' + r.owner).toLowerCase().includes(q)) return false;
|
|
239
258
|
return true;
|
|
240
259
|
});
|
|
241
260
|
}
|
|
242
261
|
/** Then each named filter (a value or a comma list; 'none' for an empty owner or anchor), as scripts/boards.js filterRows does. */
|
|
262
|
+
const asksCancelled = (p) => p.archived === '1' || String(p.status || '').split(',').map((x) => x.trim()).includes('cancelled');
|
|
243
263
|
function filterRows(S, b, p) {
|
|
244
|
-
return viewRows(S, b, p).filter((r) => {
|
|
264
|
+
return viewRows(S, b, p).filter((r) => asksCancelled(p) || r.status !== 'cancelled').filter((r) => {
|
|
245
265
|
const has = (want, v) => !want || String(want).split(',').map((x) => x.trim().toLowerCase()).includes(String(v || 'none').toLowerCase());
|
|
246
266
|
return has(p.stage, r.stage) && has(p.cat, catOfRow(S, r)) && has(p.status, r.status) && has(p.owner, r.owner) && has(p.horizon, r.horizon) && has(p.anchor, r.anchor);
|
|
247
267
|
});
|
|
@@ -305,14 +325,14 @@ const okb = (body) => ({ status: 200, body: { ok: true, ...body } });
|
|
|
305
325
|
/** One request. Returns { status, body } or undefined when the path is not a boards route. */
|
|
306
326
|
export function handle(S, method, path, p, body) {
|
|
307
327
|
if (path.startsWith('/api/stage-tools')) return handleTools(S, method, path, p, body || {});
|
|
308
|
-
if (!path.startsWith('/api/boards')) return handleMore(S, method, path, p);
|
|
328
|
+
if (!path.startsWith('/api/boards') && path !== '/api/lifecycle') return handleMore(S, method, path, p);
|
|
309
329
|
const findBoard = (ws, id) => { const b = S.boards.find((x) => x.id === id); return b && (!ws || ws === b.ws) ? b : null; };
|
|
310
330
|
if (method === 'GET') {
|
|
311
331
|
if (path === '/api/boards') return okb({ today: TODAY, boards: S.boards.map((b) => boardOut(S, b)) });
|
|
312
332
|
const b = findBoard(p.ws, p.board);
|
|
313
333
|
if (!b && path !== '/api/boards/asks') return fail(404, 'No board "' + (p.board || '') + '" in this workspace.');
|
|
314
334
|
if (path === '/api/boards/board') {
|
|
315
|
-
const list = S.rows.filter((r) => r.board === b.id);
|
|
335
|
+
const list = S.rows.filter((r) => r.board === b.id && live(r));
|
|
316
336
|
const byStage = {};
|
|
317
337
|
for (const r of list) byStage[r.stage] = (byStage[r.stage] || 0) + 1;
|
|
318
338
|
const rel = nextRelease(S, b.id);
|
|
@@ -324,7 +344,9 @@ export function handle(S, method, path, p, body) {
|
|
|
324
344
|
const list = sortRows(S, filterRows(S, b, p), p.sort);
|
|
325
345
|
const base0 = viewRows(S, b, p);
|
|
326
346
|
const offset = Math.max(0, Number(p.offset) || 0); const limit = Math.min(100, Math.max(1, Number(p.limit) || 25));
|
|
327
|
-
return okb({ view: p.view || 'now', total: list.length, offset, limit, rows: list.slice(offset, offset + limit).map((r) => rowOut(S, r)),
|
|
347
|
+
return okb({ view: p.archived === '1' ? 'all' : p.view || 'now', total: list.length, offset, limit, rows: list.slice(offset, offset + limit).map((r) => rowOut(S, r)),
|
|
348
|
+
cancelledHidden: asksCancelled(p) ? 0 : S.rows.filter((r) => r.board === b.id && live(r) && r.status === 'cancelled').length,
|
|
349
|
+
archived: p.archived === '1' ? list.length : S.rows.filter((r) => r.board === b.id && r.archived).length,
|
|
328
350
|
facets: { stage: tally(base0, (r) => r.stage), status: tally(base0, (r) => r.status), owner: tally(base0, (r) => r.owner || 'none'), horizon: tally(base0, (r) => r.horizon),
|
|
329
351
|
anchor: tally(base0, (r) => r.anchor || 'none'), cat: tally(base0, (r) => catOfRow(S, r)) } });
|
|
330
352
|
}
|
|
@@ -348,7 +370,7 @@ export function handle(S, method, path, p, body) {
|
|
|
348
370
|
return okb({ board: b.id, anchors: [...krs, ...health], error: null });
|
|
349
371
|
}
|
|
350
372
|
if (path === '/api/boards/items') {
|
|
351
|
-
const list = S.rows.filter((r) => r.board === b.id);
|
|
373
|
+
const list = S.rows.filter((r) => r.board === b.id && live(r));
|
|
352
374
|
return okb({ board: b.id, build: b.build, jira: b.jira, items: itemsOf(S, b),
|
|
353
375
|
initiatives: list.map((r) => { const o = rowOut(S, r); return { id: o.id, title: o.title, stage: o.stage, cat: o.cat, status: o.status, owner: o.owner, horizon: o.horizon, start: o.start, end: o.end, progress: o.progress, template: o.template, featured: r.featured }; }) });
|
|
354
376
|
}
|
|
@@ -369,6 +391,49 @@ export function handle(S, method, path, p, body) {
|
|
|
369
391
|
}
|
|
370
392
|
/* POST */
|
|
371
393
|
const ws = body.ws;
|
|
394
|
+
if (path === '/api/lifecycle') {
|
|
395
|
+
// D70 in the demo: on-hold, cancelled or active (the server writes one lifecycle.jsonl line; here the row changes in memory)
|
|
396
|
+
const r = S.rows.find((x) => x.id === body.initiative_id);
|
|
397
|
+
if (!r) return fail(404, "no initiative '" + (body.initiative_id || '') + "' in demo");
|
|
398
|
+
if (!['on-hold', 'cancelled', 'closed', 'active'].includes(body.state)) return fail(400, 'state must be one of closed, on-hold, cancelled, active');
|
|
399
|
+
if (body.back != null && (body.state !== 'on-hold' || !/^\d{4}-\d{2}-\d{2}$/.test(String(body.back)))) return fail(400, 'back is a day (YYYY-MM-DD) and only goes with on-hold');
|
|
400
|
+
if (!r.stopped) r.was = r.status;
|
|
401
|
+
if (body.state === 'active') { r.status = r.was || 'on track'; r.stopped = null; }
|
|
402
|
+
else { r.status = body.state === 'on-hold' ? 'on hold' : body.state === 'closed' ? 'done' : 'cancelled'; r.stopped = { reason: String(body.reason || ''), at: at(0, 8), back: body.back || null }; }
|
|
403
|
+
S.events.push({ at: at(0, 8), by: 'You', kind: 'lifecycle', id: r.id, state: body.state, reason: body.reason || '' });
|
|
404
|
+
return okb({ recorded: { initiative_id: r.id, state: body.state } });
|
|
405
|
+
}
|
|
406
|
+
if (path === '/api/boards/archive' || path === '/api/boards/merge' || path === '/api/boards/notdup') {
|
|
407
|
+
const bb = findBoard(ws, body.board);
|
|
408
|
+
if (!bb) return fail(404, 'No board "' + (body.board || '') + '" in this workspace.');
|
|
409
|
+
const on = (id) => S.rows.find((x) => x.id === id && x.board === bb.id);
|
|
410
|
+
const r = on(body.id);
|
|
411
|
+
if (!r) return fail(404, 'no initiative ' + (body.id || '') + ' on this board');
|
|
412
|
+
if (path === '/api/boards/notdup') { S.notDup.push(r.id + '|' + body.of); return okb({}); }
|
|
413
|
+
if (path === '/api/boards/archive') {
|
|
414
|
+
if (typeof body.archived !== 'boolean') return fail(400, 'archived is true (archive) or false (restore)');
|
|
415
|
+
if (body.archived && r.archived) return fail(409, r.id + ' is archived already');
|
|
416
|
+
if (!body.archived && !r.archived) return fail(409, r.id + ' is not archived');
|
|
417
|
+
if (!body.archived && r.archived.into) {
|
|
418
|
+
const t = on(r.archived.into); const mv = r.archived.moved || { epics: [], tasks: [] };
|
|
419
|
+
if (t && mv.epics.length) { t.epics = (t.epics || []).filter((k) => !mv.epics.includes(k)); r.epics = [...new Set([...(r.epics || []), ...mv.epics])]; }
|
|
420
|
+
for (const tk of S.tasks) if (mv.tasks.includes(tk.id) && tk.initiative === r.archived.into) tk.initiative = r.id;
|
|
421
|
+
}
|
|
422
|
+
r.archived = body.archived ? { at: at(0, 8), by: 'You', reason: String(body.reason || ''), into: null, moved: { epics: [], tasks: [] } } : null;
|
|
423
|
+
S.events.push({ at: at(0, 8), by: 'You', kind: body.archived ? 'archive' : 'restore', board: bb.id, id: r.id });
|
|
424
|
+
return okb({ row: rowOut(S, r) });
|
|
425
|
+
}
|
|
426
|
+
const t = on(body.into);
|
|
427
|
+
if (body.id === body.into) return fail(400, 'an initiative cannot be merged into itself');
|
|
428
|
+
if (!t) return fail(404, 'no initiative ' + (body.into || '') + ' on this board (an initiative merges into one on the same board)');
|
|
429
|
+
if (r.archived) return fail(409, r.id + ' is archived already');
|
|
430
|
+
const epics = (body.epics || []).filter((k) => (r.epics || []).includes(k)); const tks = (body.tasks || []).filter((id) => S.tasks.some((x) => x.id === id && x.initiative === r.id));
|
|
431
|
+
if (epics.length) { t.epics = [...new Set([...(t.epics || []), ...epics])]; r.epics = (r.epics || []).filter((k) => !epics.includes(k)); }
|
|
432
|
+
for (const tk of S.tasks) if (tks.includes(tk.id)) tk.initiative = t.id;
|
|
433
|
+
r.archived = { at: at(0, 8), by: 'You', reason: 'Merged into ' + t.title, into: t.id, moved: { epics, tasks: tks } };
|
|
434
|
+
S.events.push({ at: at(0, 8), by: 'You', kind: 'merge', board: bb.id, id: r.id, into: t.id });
|
|
435
|
+
return okb({ row: rowOut(S, r), into: rowOut(S, t) });
|
|
436
|
+
}
|
|
372
437
|
if (path === '/api/boards/create') {
|
|
373
438
|
const name = String(body.name || '').trim();
|
|
374
439
|
if (!name) return fail(400, 'A board needs a name.');
|
|
@@ -667,7 +732,7 @@ export function installBoards() {
|
|
|
667
732
|
const raw = typeof input === 'string' ? input : input instanceof URL ? input.href : input && input.url;
|
|
668
733
|
const method = String((init && init.method) || (input && input.method) || 'GET').toUpperCase();
|
|
669
734
|
const u = new URL(raw, location.origin);
|
|
670
|
-
if (u.origin === location.origin && (u.pathname.startsWith('/api/boards') || u.pathname.startsWith('/api/stage-tools') || u.searchParams.has('board') || u.pathname.startsWith('/api/doc'))) {
|
|
735
|
+
if (u.origin === location.origin && (u.pathname.startsWith('/api/boards') || u.pathname === '/api/lifecycle' || u.pathname.startsWith('/api/stage-tools') || u.searchParams.has('board') || u.pathname.startsWith('/api/doc'))) {
|
|
671
736
|
let b = {};
|
|
672
737
|
if (method !== 'GET') { try { b = init && init.body ? JSON.parse(init.body) : {}; } catch { b = {}; } }
|
|
673
738
|
const out = handle(S, method, u.pathname, Object.fromEntries(u.searchParams), b);
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* context chips, the room's last lines, three PMOS notes and two it is holding, the captured items and a Google Slides deck.
|
|
5
5
|
* Whatever meeting the page asks for, the demo answers with this one. GET /api/meeting-focus/now (the compact page of the Chrome extension) names it too;
|
|
6
6
|
* the Meet code NONE_CODE answers "no meeting fits" with three of the day's meetings as choices, so that path can be walked. A correction (set, add, park) and a question change the answer
|
|
7
|
-
* for the rest of the page's life; nothing is saved and nothing reaches the server. Real mode never loads this file.
|
|
7
|
+
* for the rest of the page's life; nothing is saved and nothing reaches the server. Real mode never loads this file.
|
|
8
|
+
* A read by note alone (the phone's focus for a recording that is not on the calendar) answers with a corridor talk: Budi asks about the SSO copy.
|
|
9
|
+
* focusNote is the live note the phone's Note sheet shows (the shape of GET /api/notes/one). */
|
|
8
10
|
|
|
9
11
|
const DAY = '2026-10-08';
|
|
10
12
|
const at = (hm) => Date.parse(DAY + 'T' + hm + ':00');
|
|
@@ -108,9 +110,45 @@ export function focusGet(path, url) {
|
|
|
108
110
|
const a = S.asks.find((x) => x.id === q.get('id'));
|
|
109
111
|
return a ? { ok: true, id: a.id, state: 'answered' } : { ok: false, status: 404, error: 'no such question' };
|
|
110
112
|
}
|
|
113
|
+
const q = new URL(url, location.origin).searchParams;
|
|
114
|
+
if (!q.get('event') && q.get('note')) return talkView(q.get('note'));
|
|
111
115
|
return view();
|
|
112
116
|
}
|
|
113
117
|
|
|
118
|
+
/* a recording that is not on the calendar: started 40 seconds ago, no topics, no brief, no stage */
|
|
119
|
+
const TALK_AT = NOW - 40000;
|
|
120
|
+
const TALK_TAIL = [['Budi', 12000, 'Does the SSO copy need legal to look at it first?'], ['You', 26000, 'I think so. Let me check.']]
|
|
121
|
+
.map(([who, t, text]) => ({ who, at: new Date(TALK_AT + t).toISOString(), t, text, source: who === 'You' ? 'mic' : 'meeting' }));
|
|
122
|
+
function talkView(noteId) {
|
|
123
|
+
const id = String(noteId);
|
|
124
|
+
const notes = [{ id: 't0', at: TALK_AT + 30000, cat: 'Answer', heard: '"Does the SSO copy need legal to look at it first?" Budi', body: 'Yes. Since the 2 Oct policy change, legal reviews all login copy. The last review took 3 working days.',
|
|
125
|
+
why: 'Budi asked, and the beta date depends on it.', src: 'the legal policy page in Confluence', show: 'now' }].concat(S.extra);
|
|
126
|
+
return {
|
|
127
|
+
ok: true, now_ms: NOW, date: DAY, talk: true,
|
|
128
|
+
event: { id: 'note:' + id, title: 'Conversation', at: new Date(TALK_AT).toISOString(), end: null, attendees: [], initiative: null, purpose: null, links: [] },
|
|
129
|
+
brief: null, note: { id, title: 'Conversation', state: 'recording', recorded_from: new Date(TALK_AT).toISOString(), lines: 2, sessions: [{ from: new Date(TALK_AT).toISOString(), to: null }] },
|
|
130
|
+
topics: [], current: null, runs: [], tail: TALK_TAIL, captured: { decisions: [], actions: [], questions: [] },
|
|
131
|
+
gist: 'Budi is asking whether the SSO copy needs a legal check before the Onboarding v2 beta.',
|
|
132
|
+
chips: [{ label: 'SSO copy', sub: 'last edited by Budi, Fri 9 Oct', detail: 'The sign-in screen copy, 4 strings. Last edited by Budi on Fri 9 Oct. Not yet reviewed by legal.', from: 'IDPF-12 in Jira' },
|
|
133
|
+
{ label: 'Onboarding v2 beta', sub: 'in 18 days', detail: 'Beta on 30 Oct, 18 days away. At risk: QA is one tester short, and a scope cut waits on you today.', from: 'IDPF-12 and your decisions' }],
|
|
134
|
+
notes, asks: S.asks.map((a) => ({ ...a })),
|
|
135
|
+
companion: { on: true, running: false, started: false, reason: 'too soon', last_run_at: NOW - 10000, error: null },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The live note of the focus page's recording, for the phone's Note sheet: the shape of GET /api/notes/one, with live bullets. */
|
|
140
|
+
export function focusNote(id, data) {
|
|
141
|
+
const talk = !!(data && data.talk);
|
|
142
|
+
const tail = talk ? TALK_TAIL : TAIL;
|
|
143
|
+
const speakers = { mic: 'You' };
|
|
144
|
+
for (const x of tail) if (x.who !== 'You') speakers['meeting:' + x.who] = x.who;
|
|
145
|
+
return { ok: true, note: { id, title: talk ? 'Conversation' : EVENT.title, state: 'recording', speakers,
|
|
146
|
+
transcript: tail.map((x) => ({ t: x.t, at: x.at, source: x.who === 'You' ? 'mic' : 'meeting:' + x.who, text: x.text })),
|
|
147
|
+
bullets: talk ? [{ id: 0, cat: 'question', text: 'Does the SSO copy need a legal check before the beta?' }]
|
|
148
|
+
: [{ id: 0, cat: 'decision', text: 'Option B needs 30 days notice; the partner agreement allows it' }, { id: 1, cat: 'action', text: 'Chen sends the per-partner numbers to Lina' },
|
|
149
|
+
{ id: 2, cat: 'question', text: 'Can a partner buy a temporary higher tier during the grace window?' }] } };
|
|
150
|
+
}
|
|
151
|
+
|
|
114
152
|
/** POST /api/meeting-focus/topic and POST /api/meeting-focus/ask: they change the demo's answer and nothing else. */
|
|
115
153
|
export function focusPost(path, body) {
|
|
116
154
|
let b = {};
|
|
@@ -198,3 +198,56 @@
|
|
|
198
198
|
.c3focus.fc-pop:not([data-fc-state="ok"]) .fc-cv, .c3focus.fc-pop:not([data-fc-state="ok"]) .fc-foot { display: none; }
|
|
199
199
|
.c3focus.fc-pop.fc-pill .fc-popbody { display: none; }
|
|
200
200
|
.c3focus.fc-pop.fc-pill .fc-page { padding: 6px 10px; justify-content: center; }
|
|
201
|
+
|
|
202
|
+
/* phone mode (initiative c3-phone-companion, mockup version 5): the full page in one column. A head with Back to your day, the title and the
|
|
203
|
+
* clock, the bar of the talk; the now line and the chips that open sheets; What's being said; the conversation; Ask PMOS quietly and Stop at the foot. */
|
|
204
|
+
.c3focus.fc-phone { overflow: hidden; }
|
|
205
|
+
.c3focus.fc-phone .fc-page { display: flex; flex-direction: column; gap: 8px; height: 100%; max-width: none; margin: 0; padding: 8px 12px 0; }
|
|
206
|
+
.c3focus.fc-phone .fc-phd { display: grid; gap: 6px; flex: 0 0 auto; }
|
|
207
|
+
.c3focus.fc-phone .fc-phid { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 8px; align-items: center; }
|
|
208
|
+
.c3focus.fc-phone .fc-phid .ib { width: 36px; height: 36px; min-width: 0; padding: 0; }
|
|
209
|
+
.c3focus.fc-phone .fc-tt { gap: 0; }
|
|
210
|
+
.c3focus.fc-phone .fc-tt h1 { font-size: var(--fs-3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
211
|
+
.c3focus.fc-phone .fc-sub { font-size: var(--fs-1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
212
|
+
.c3focus.fc-phone .fc-phclk { display: grid; justify-items: end; }
|
|
213
|
+
.c3focus.fc-phone .fc-phclk b { font-size: var(--fs-3); font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
214
|
+
.c3focus.fc-phone .fc-phclk .fc-recd b { color: var(--destructive); }
|
|
215
|
+
.c3focus.fc-phone .fc-phclk .dim { font-size: var(--fs-1); font-variant-numeric: tabular-nums; }
|
|
216
|
+
.c3focus.fc-phone .fc-phnow { display: grid; gap: 6px; flex: 0 0 auto; min-width: 0; }
|
|
217
|
+
.c3focus.fc-phone .fc-nowt { flex-wrap: nowrap; }
|
|
218
|
+
.c3focus.fc-phone .fc-nowt b { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
219
|
+
.c3focus.fc-phone .fc-nowt .ki, .c3focus.fc-phone .fc-nowt .mono { flex: none; }
|
|
220
|
+
.c3focus.fc-phone .fc-phchips { display: flex; gap: 6px; flex-wrap: wrap; row-gap: 4px; }
|
|
221
|
+
.c3focus.fc-phone .fc-phchips .chip2 { flex: none; font-size: var(--fs-2); min-height: 30px; }
|
|
222
|
+
.c3focus.fc-phone .fc-bandwrap { flex: 0 0 auto; }
|
|
223
|
+
.c3focus.fc-phone .fc-gist { padding: 8px 10px; }
|
|
224
|
+
.c3focus.fc-phone .fc-gist p { font-size: var(--fs-2); }
|
|
225
|
+
.c3focus.fc-phone .fc-gl { display: block; margin: 0 0 2px; }
|
|
226
|
+
.c3focus.fc-phone .fc-cxs { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
|
|
227
|
+
.c3focus.fc-phone .fc-cx { flex: none; max-width: 80%; }
|
|
228
|
+
.c3focus.fc-phone .fc-cv { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; max-height: none; overflow: auto; }
|
|
229
|
+
.c3focus.fc-phone .fc-tk { padding: 6px 10px; }
|
|
230
|
+
.c3focus.fc-phone .fc-s2 { overflow: visible; padding: 6px 8px; }
|
|
231
|
+
.c3focus.fc-phone .fc-x { display: grid; opacity: 1; }
|
|
232
|
+
.c3focus.fc-phone .fc-acts2 button { min-height: 36px; }
|
|
233
|
+
.c3focus.fc-phone .fc-phft { display: flex; gap: 8px; align-items: center; flex: 0 0 auto; margin: 0 -12px; padding: 8px 12px calc(8px + env(safe-area-inset-bottom, 0px)); border-top: 1px solid var(--border); background: var(--background); }
|
|
234
|
+
.c3focus.fc-phone .fc-in { flex: 1 1 auto; min-width: 0; padding: 0; border: 0; }
|
|
235
|
+
.c3focus.fc-phone .fc-in input { height: 40px; font-size: 16px; }
|
|
236
|
+
.c3focus.fc-phone .fc-phact { flex: none; display: flex; }
|
|
237
|
+
.c3focus.fc-phone .fc-phact button { min-height: 40px; }
|
|
238
|
+
.c3focus.fc-phone:not([data-fc-state="ok"]) .fc-cv, .c3focus.fc-phone:not([data-fc-state="ok"]) .fc-phft { display: none; }
|
|
239
|
+
[data-fc="stop-sheet"] .fc-p, [data-fc="note-sheet"] .fc-p { margin: 0 0 var(--sp-2); font-size: var(--fs-3); line-height: 1.5; }
|
|
240
|
+
[data-fc="stop-sheet"] .iacts { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
241
|
+
[data-fc="note-sheet"] { display: grid; gap: var(--sp-2); }
|
|
242
|
+
[data-fc="note-sheet"] .fc-said { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; font-size: var(--fs-2); line-height: 1.5; }
|
|
243
|
+
[data-fc="note-sheet"] .fc-lbl { font-size: var(--fs-1); padding-bottom: 4px; }
|
|
244
|
+
[data-fc="note-sheet"] .fc-note { margin: 0; font-size: var(--fs-1); }
|
|
245
|
+
/* the stage, when the phone opens it in a sheet (outside .c3focus) */
|
|
246
|
+
[data-shell="sheet"] [data-fc="stage"] .fc-frame { margin-top: var(--sp-2); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: var(--muted); aspect-ratio: 16 / 9; width: 100%; }
|
|
247
|
+
[data-shell="sheet"] [data-fc="stage"] .fc-frame iframe { display: block; width: 100%; height: 100%; border: 0; background: var(--card); }
|
|
248
|
+
[data-shell="sheet"] [data-fc="stage"] .fc-strip { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: var(--sp-2) 0 0; }
|
|
249
|
+
[data-shell="sheet"] [data-fc="stage"] .fc-src { display: inline-flex; align-items: center; gap: 4px; font-size: var(--fs-1); margin: 0; }
|
|
250
|
+
[data-shell="sheet"] [data-fc="stage"] .fc-grow { flex: 1 1 auto; }
|
|
251
|
+
[data-shell="sheet"] [data-fc="stage"] .fc-card { display: flex; gap: var(--sp-2); align-items: center; margin-top: var(--sp-2); padding: var(--sp-2); border: 1px solid var(--border); border-radius: 8px; background: var(--card); flex-wrap: wrap; }
|
|
252
|
+
[data-shell="sheet"] [data-fc="stage"] .fc-card .nb { display: grid; gap: 2px; flex: 1 1 160px; min-width: 0; overflow-wrap: anywhere; }
|
|
253
|
+
[data-shell="sheet"] [data-fc="stage"] .seg { flex-wrap: wrap; }
|