create-pmos 0.59.3 → 0.60.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/delivery-index.js +85 -7
- package/payload/scripts/workflow-state.js +3 -3
- package/payload/state/discoveries.md +1 -1
- package/workspace/fixtures/initiatives-api/sheet.json +29 -7
- package/workspace/guide/index.md +3 -3
- package/workspace/guide/initiatives-screen.md +25 -1
- package/workspace/guide/layout.md +6 -6
- package/workspace/guide/proactive-engine.md +1 -0
- package/workspace/guide/today.md +23 -1
- package/workspace/lib/calendar-memory.js +257 -0
- package/workspace/lib/console-state.js +1 -1
- package/workspace/lib/doc-category.js +3 -1
- package/workspace/lib/engine/delivery-index.js +85 -7
- package/workspace/lib/engine/workflow-state.js +3 -3
- package/workspace/lib/initiative-files.js +59 -1
- package/workspace/lib/initiative-rows.js +14 -2
- package/workspace/lib/mobile-nav.js +1 -1
- package/workspace/lib/plan-tree.js +44 -5
- package/workspace/lib/proactive-cli.js +16 -1
- package/workspace/lib/routes-initiatives.js +165 -9
- package/workspace/package-lock.json +1 -1
- package/workspace/package.json +1 -1
- package/workspace/public/index.html +1 -1
- package/workspace/public/pm/components.css +70 -0
- package/workspace/public/pm/dest-initiatives.js +7 -1
- package/workspace/public/pm/frame-copy.js +2 -2
- package/workspace/public/pm/initiative-sheet.js +42 -10
- package/workspace/public/pm/initiatives-api.js +15 -0
- package/workspace/public/pm/initiatives-copy.js +13 -0
- package/workspace/public/pm/initiatives-ui.js +14 -0
- package/workspace/public/pm/registry.js +1 -1
- package/workspace/public/pm/today-agenda.js +305 -0
- package/workspace/public/pm/today-copy.js +54 -2
- package/workspace/public/pm/today-day.js +23 -3
- package/workspace/public/pm/today-goals.js +7 -1
- package/workspace/server.js +53 -2
- package/workspace/skills/morning-brief.skill +6 -1
- package/workspace/test-files.json +2 -0
- package/workspace/test-lists/scan-consistency.json +5 -0
|
@@ -26,7 +26,7 @@ import { state, setState } from './state.js';
|
|
|
26
26
|
import { dayOf } from './breakdown-copy.js';
|
|
27
27
|
import { COPY, shortDate } from './initiatives-copy.js';
|
|
28
28
|
import { getSheet, getExport, place, placed, leaveNow, setLifecycle } from './initiatives-api.js';
|
|
29
|
-
import { healthSignal, say, confirmSheet, checkBySheet, horizonLabel } from './initiatives-ui.js';
|
|
29
|
+
import { healthSignal, say, confirmSheet, checkBySheet, horizonLabel, rescanRepo } from './initiatives-ui.js';
|
|
30
30
|
|
|
31
31
|
/** The pre-Run-2 detail pane: the fallback, and the home of the legacy gate buttons that "Open gate" reaches. */
|
|
32
32
|
export function legacyOpen(repo, slug) {
|
|
@@ -72,10 +72,12 @@ export function openSheet(repo, slug) {
|
|
|
72
72
|
const waitingHost = el('div', { class: 'in-sheet__waiting-host' });
|
|
73
73
|
const tabsHost = el('div', { class: 'in-sheet__tabs' });
|
|
74
74
|
const panel = el('div', { class: 'in-sheet__panel', 'data-in-panel': '' });
|
|
75
|
+
// The Breakdown pane mounts into its own host, so the breakdown read from the spec can sit above it (scan-consistency R4).
|
|
76
|
+
const bdHost = el('div', { class: 'in-sheet__bd', 'data-in-bd': '' });
|
|
75
77
|
const s = sheet({
|
|
76
78
|
mode: 'auto', title: slug, sub: '', body: [crumb, stepperHost, waitingHost, tabsHost, panel],
|
|
77
79
|
attrs: { class: 'in-sheet', 'data-in-sheet': 'initiative', 'data-in-id': slug },
|
|
78
|
-
onClose: () => { closed = true; unmountBreakdown(
|
|
80
|
+
onClose: () => { closed = true; unmountBreakdown(bdHost); unmountJira(panel); if (current === s) current = null; },
|
|
79
81
|
});
|
|
80
82
|
current = s;
|
|
81
83
|
|
|
@@ -117,6 +119,7 @@ export function openSheet(repo, slug) {
|
|
|
117
119
|
items.push({ sep: true });
|
|
118
120
|
items.push({ label: COPY.sheet.copyCli, onSelect: () => copyCli() });
|
|
119
121
|
items.push({ label: COPY.sheet.exportBreakdown, onSelect: () => exportBreakdown() });
|
|
122
|
+
items.push({ label: COPY.rescan, onSelect: async () => { const a = await rescanRepo(ws); if (a.ok) reload(); } });
|
|
120
123
|
menu({ anchor, items, ariaLabel: COPY.sheet.menuMore(I.title) });
|
|
121
124
|
}
|
|
122
125
|
|
|
@@ -182,7 +185,24 @@ export function openSheet(repo, slug) {
|
|
|
182
185
|
const steps = data.stepper || [];
|
|
183
186
|
stepperHost.replaceChildren(el('ol', { class: 'in-stepper', 'aria-label': COPY.sheet.stages, 'data-in-stepper': '' }, steps.map((st) => el('li', {
|
|
184
187
|
class: 'in-stepper__step', 'data-state': st.state, 'data-step': st.id, 'aria-current': st.state === 'current' ? 'step' : null,
|
|
185
|
-
|
|
188
|
+
title: st.state === 'skipped' && st.owed ? COPY.sheet.skippedStep(st.owed) : null,
|
|
189
|
+
}, st.state === 'done' ? icon('check') : st.state === 'skipped' ? icon('circle-alert') : null, el('span', null, st.label)))));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The breakdown read from the spec when nothing is planned (read-only; GET /api/initiatives/<id> `derived`). */
|
|
193
|
+
function derivedSection() {
|
|
194
|
+
const d = data.derived;
|
|
195
|
+
if (!d || !Array.isArray(d.items) || !d.items.length) return null;
|
|
196
|
+
const state = COPY.sheet.derivedStates[d.state] || d.state;
|
|
197
|
+
return el('section', { class: 'in-derived', 'data-in-derived': d.state },
|
|
198
|
+
el('h3', { class: 'in-derived__title' }, COPY.sheet.derivedTitle(d.source)),
|
|
199
|
+
el('p', { class: 'in-note' }, COPY.sheet.derivedNote(d.basis)),
|
|
200
|
+
el('div', { class: 'in-sheet__rows', role: 'list' }, d.items.map((it) => row({
|
|
201
|
+
title: (it.ref ? it.ref + ' · ' : '') + it.title, meta: [state], done: d.state === 'done',
|
|
202
|
+
openLabel: COPY.aria.open(it.title),
|
|
203
|
+
onOpen: () => { const src = String(d.source || '').split(',')[0].trim(); if (src) { s.close(); openDoc(slug, src, src.split('/').pop()); } },
|
|
204
|
+
attrs: { 'data-in-derived-item': it.id, 'data-state': d.state },
|
|
205
|
+
}))));
|
|
186
206
|
}
|
|
187
207
|
|
|
188
208
|
function drawWaiting() {
|
|
@@ -224,7 +244,7 @@ export function openSheet(repo, slug) {
|
|
|
224
244
|
items: [{ id: 'breakdown', label: COPY.sheet.breakdown }, { id: 'files', label: COPY.sheet.files }, { id: 'history', label: COPY.sheet.history }, { id: 'jira', label: COPY.sheet.jira }],
|
|
225
245
|
onChange: (id) => {
|
|
226
246
|
if (id === tab) return;
|
|
227
|
-
if (tab === 'breakdown') unmountBreakdown(
|
|
247
|
+
if (tab === 'breakdown') unmountBreakdown(bdHost);
|
|
228
248
|
if (tab === 'jira') unmountJira(panel);
|
|
229
249
|
tab = id;
|
|
230
250
|
drawTabs(); drawPanel();
|
|
@@ -236,15 +256,27 @@ export function openSheet(repo, slug) {
|
|
|
236
256
|
|
|
237
257
|
function drawPanel() {
|
|
238
258
|
if (!data) return;
|
|
239
|
-
if (tab === 'breakdown') {
|
|
259
|
+
if (tab === 'breakdown') {
|
|
260
|
+
const derived = derivedSection();
|
|
261
|
+
panel.replaceChildren(...(derived ? [derived, bdHost] : [bdHost]));
|
|
262
|
+
mountBreakdown(bdHost, { id: tabId, ws, slug }, { embedded: true });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
240
265
|
if (tab === 'jira') { mountJira(panel, { ws, slug }); return; }
|
|
241
266
|
if (tab === 'files') {
|
|
242
267
|
const files = data.files || [];
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
268
|
+
// Grouped by where the tie came from (scan-consistency R3): the record's own paths, the documents the library's
|
|
269
|
+
// index links to it, and the files git changed for it. A `note` row is a fact about the list, not a file.
|
|
270
|
+
const order = ['record', 'knowledge-graph', 'git'];
|
|
271
|
+
const groups = order.map((src) => [src, files.filter((f) => (f.source || 'record') === src)]).filter(([, list]) => list.length);
|
|
272
|
+
const fileRow = (f) => (f.kind === 'note' ? el('p', { class: 'in-note', 'data-in-file-note': '' }, f.label) : row({
|
|
273
|
+
title: f.label, meta: [f.path, f.commits ? COPY.sheet.commits(f.commits) : null, f.shared ? COPY.sheet.shared : null], openLabel: COPY.aria.open(f.label),
|
|
274
|
+
onOpen: () => { s.close(); openDoc(slug, f.path, f.label, null, f.source === 'git' ? { readonly: true } : undefined); },
|
|
275
|
+
attrs: { 'data-in-file': f.kind, 'data-in-file-source': f.source || 'record' }, openAttrs: { 'data-in-file-open': f.kind },
|
|
276
|
+
}));
|
|
277
|
+
panel.replaceChildren(files.length ? el('div', { class: 'in-files' }, groups.map(([src, list]) => el('section', { class: 'in-files__group', 'data-in-files': src },
|
|
278
|
+
el('h3', { class: 'in-files__title' }, COPY.sheet.fileSource[src] || src),
|
|
279
|
+
el('div', { class: 'in-sheet__rows', role: 'list' }, list.map(fileRow))))) : empty({ text: COPY.sheet.noFiles }));
|
|
248
280
|
return;
|
|
249
281
|
}
|
|
250
282
|
const hist = data.history || [];
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* GET /api/initiatives/<id>/export?ws= the breakdown as text
|
|
11
11
|
* POST /api/placements { ws, events: [placed…] } every move, reorder and check-by
|
|
12
12
|
* POST /api/initiatives { ws, title, lane, anchor, intent?, from? } the New-initiative form and a triage card (one body, no events)
|
|
13
|
+
* POST /api/initiatives/rescan { ws } the Re-scan press: read the repository again now (nothing is written)
|
|
13
14
|
*
|
|
14
15
|
* Every GET of /api/initiatives answers 202 { ok: false, pending: true } until the repository's first derivation lands (2C,
|
|
15
16
|
* about 260 ms a record); a read waits and asks again, so a caller sees the answer or a real error, never the 202.
|
|
@@ -47,6 +48,20 @@ export const adopt = (ws, proposal) => pmPost('/api/placements', { ws, events: [
|
|
|
47
48
|
export const drop = (ws, proposal) => pmPost('/api/placements', { ws, events: [{ ev: 'dropped', proposal }] });
|
|
48
49
|
|
|
49
50
|
/** A new initiative: from the form, or from a triaged capture (`from` is the capture's id). It lands in Later. */
|
|
51
|
+
/** Read the repository again now. Answers when the new derivation has landed (the records part of the sig stops reading
|
|
52
|
+
* `~stale`), or { ok: true, slow: true } when it is still running after about three minutes. */
|
|
53
|
+
export async function rescan(ws) {
|
|
54
|
+
const a = await pmPost('/api/initiatives/rescan', { ws });
|
|
55
|
+
if (!a || !a.ok) return a || { ok: false, error: 'no answer' };
|
|
56
|
+
for (let k = 0; k < 120; k++) {
|
|
57
|
+
await sleep(1500);
|
|
58
|
+
const r = await pmGet('/api/initiatives?ws=' + enc(ws) + '&view=roadmap');
|
|
59
|
+
const rec = r && r.sig ? String(r.sig.records || '') : '';
|
|
60
|
+
if (r && r.ok && rec && !rec.endsWith('~stale')) return { ok: true };
|
|
61
|
+
}
|
|
62
|
+
return { ok: true, slow: true };
|
|
63
|
+
}
|
|
64
|
+
|
|
50
65
|
export const createInitiative = (ws, fields) => pmPost('/api/initiatives', { ws, ...fields });
|
|
51
66
|
|
|
52
67
|
export const setLifecycle = (ws, id, state, reason) => pmPost('/api/lifecycle', { ws, initiative_id: id, state, reason: reason || '' });
|
|
@@ -28,6 +28,12 @@ export const COPY = {
|
|
|
28
28
|
roadmap: 'Roadmap',
|
|
29
29
|
list: 'List',
|
|
30
30
|
newInitiative: 'New initiative',
|
|
31
|
+
rescan: 'Re-scan the repository',
|
|
32
|
+
rescanning: 'Re-scanning…',
|
|
33
|
+
rescanHint: 'Read stage, files and breakdown again now',
|
|
34
|
+
rescanDone: (t) => `Read again at ${t}`,
|
|
35
|
+
rescanFailed: (why) => `Could not re-scan: ${why}`,
|
|
36
|
+
rescanSlow: 'The re-scan is still running. The view updates when it ends.',
|
|
31
37
|
reading: 'Reading…',
|
|
32
38
|
readFailed: (why) => `Could not read the initiatives: ${why}`,
|
|
33
39
|
noRepo: 'Add a repository to see its initiatives.',
|
|
@@ -127,6 +133,13 @@ export const COPY = {
|
|
|
127
133
|
openGate: 'Open gate',
|
|
128
134
|
handToAgent: 'Hand to agent',
|
|
129
135
|
noFiles: 'No files yet.',
|
|
136
|
+
fileSource: { record: 'Named by the record', 'knowledge-graph': 'Linked documents', git: 'Changed for it in git' },
|
|
137
|
+
commits: (n) => (n === 1 ? '1 commit' : `${n} commits`),
|
|
138
|
+
shared: 'shared',
|
|
139
|
+
skippedStep: (noun) => `Skipped: ${noun} is still open`,
|
|
140
|
+
derivedTitle: (src) => `From the spec · ${src}`,
|
|
141
|
+
derivedNote: (basis) => `Read from the spec, because the Breakdown has no items yet. Each item shows the state of the whole initiative: ${basis}.`,
|
|
142
|
+
derivedStates: { todo: 'Todo', review: 'Built', done: 'Done' },
|
|
130
143
|
noHistory: 'No history yet.',
|
|
131
144
|
reading: 'Reading…',
|
|
132
145
|
readFailed: (why) => `Could not read the initiative: ${why}`,
|
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
* confirmSheet({…}) a sheet that asks for one press (a PM-only act): an optional choice, a reason, Cancel, Confirm
|
|
7
7
|
* checkBySheet({…}) a sheet with a date and Save or Clear
|
|
8
8
|
* say(out, ok) a toast for the answer of a write: the success text, or the server's refusal
|
|
9
|
+
* rescanRepo(ws) read the repository again now, with a toast at the start and at the end (scan-consistency R6)
|
|
9
10
|
* No native dialog is used anywhere here. Every control is a primitive (primitives.js).
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import { el } from './dom.js';
|
|
13
14
|
import { button, field, signal, sheet, tabs, toast } from './primitives.js';
|
|
14
15
|
import { COPY } from './initiatives-copy.js';
|
|
16
|
+
import { rescan } from './initiatives-api.js';
|
|
15
17
|
|
|
16
18
|
export const horizonLabel = (h) => COPY.horizons[h] || h;
|
|
17
19
|
export const progressText = (row) => (row && row.progress && row.progress.label) || '';
|
|
@@ -24,6 +26,18 @@ export function healthSignal(health) {
|
|
|
24
26
|
return signal({ text: top.text || COPY.healthUnknown, tone, title: top.source || null, attrs: { 'data-health': health.level } });
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
/** Re-scan: read the repository again now. A toast says it started and another says how it ended. Answers the rescan
|
|
30
|
+
* answer; the caller reloads what it shows when `ok`. */
|
|
31
|
+
export async function rescanRepo(ws) {
|
|
32
|
+
toast({ text: COPY.rescanning, timeout: 4000 });
|
|
33
|
+
const a = await rescan(ws);
|
|
34
|
+
const t = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
35
|
+
if (!a || !a.ok) toast({ text: COPY.rescanFailed((a && a.error) || 'no answer'), tone: 'error', timeout: 8000 });
|
|
36
|
+
else toast({ text: a.slow ? COPY.rescanSlow : COPY.rescanDone(t), timeout: 6000 });
|
|
37
|
+
try { if (a && a.ok) window.dispatchEvent(new CustomEvent('pmos:initiatives-changed')); } catch { /* no window */ }
|
|
38
|
+
return a || { ok: false };
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
/** A toast for the answer of a write. A refusal shows the server's own words. */
|
|
28
42
|
export function say(out, okText) {
|
|
29
43
|
if (out && out.ok) { if (okText) toast({ text: okText, timeout: 4000 }); return true; }
|
|
@@ -44,7 +44,7 @@ import { pmGet } from './http.js';
|
|
|
44
44
|
import { empty, ensureComponentsCss } from './primitives.js';
|
|
45
45
|
|
|
46
46
|
export const DESTINATIONS = Object.freeze([
|
|
47
|
-
Object.freeze({ id: 'today', label: '
|
|
47
|
+
Object.freeze({ id: 'today', label: 'Agenda', key: 'g t', module: 'dest-today.js', run: 3 }),
|
|
48
48
|
Object.freeze({ id: 'initiatives', label: 'Initiatives', key: 'g i', module: 'dest-initiatives.js', run: 2 }),
|
|
49
49
|
Object.freeze({ id: 'board', label: 'Board', key: 'g b', module: 'dest-board.js', run: 4 }),
|
|
50
50
|
Object.freeze({ id: 'reports', label: 'Reports', key: 'g r', module: 'dest-reports.js', run: 5 }),
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/* pm/today-agenda.js — the Agenda month in the left column of Today (initiative today-goals, PRD G25 to G27).
|
|
2
|
+
*
|
|
3
|
+
* agendaPanel(ctx, h) → the panel: the month on top, the agenda of the picked day or week under it; null when the
|
|
4
|
+
* server has no agenda route (an older workspace), so the day's calendar list shows instead
|
|
5
|
+
* meetingDetail(ctx, id) → the right column for a remembered meeting: what it means, before it, the last time
|
|
6
|
+
* memoryBlock(id) → the series part of that detail, for a meeting of today drawn from the brief
|
|
7
|
+
* litSeries(sel) → the series key of the selected meeting, so the month marks the days it happens
|
|
8
|
+
* refreshAgenda() · agendaSig() · onAgenda(fn)
|
|
9
|
+
*
|
|
10
|
+
* One read feeds it: GET /api/proactive/agenda?month=YYYY-MM. The server derives every answer (how full, the key
|
|
11
|
+
* meeting, readiness, no notes); this file only draws them. A day cell is a button: it picks that day. A week number
|
|
12
|
+
* picks the week. The lens switch says which answer the cells show; the agenda under the month always shows them all.
|
|
13
|
+
* Nothing here writes: the calendar memory changes only through the morning-brief session's brief.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { el } from './dom.js';
|
|
17
|
+
import { button, iconButton, badge, tabs } from './primitives.js';
|
|
18
|
+
import { pmGet } from './http.js';
|
|
19
|
+
import { COPY } from './today-copy.js';
|
|
20
|
+
|
|
21
|
+
const A = COPY.day.ag;
|
|
22
|
+
const DAY0 = 9 * 60;
|
|
23
|
+
const SPAN = 9 * 60;
|
|
24
|
+
|
|
25
|
+
const cache = new Map(); // month -> { data, error, old }
|
|
26
|
+
const ag = { month: null, pick: null, lens: 'overview', showRoutine: false };
|
|
27
|
+
let redraw = () => {};
|
|
28
|
+
let inflight = new Set();
|
|
29
|
+
|
|
30
|
+
export function onAgenda(fn) { redraw = fn; }
|
|
31
|
+
|
|
32
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
33
|
+
const ymOf = (date) => date.slice(0, 7);
|
|
34
|
+
const shiftMonth = (ym, n) => { const [y, m] = ym.split('-').map(Number); const d = new Date(y, m - 1 + n, 1); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}`; };
|
|
35
|
+
const dateOf = (s) => { const [y, m, d] = s.split('-').map(Number); return new Date(y, m - 1, d); };
|
|
36
|
+
const label = (s, opts) => { try { return dateOf(s).toLocaleDateString(undefined, opts); } catch { return s; } };
|
|
37
|
+
const dayLabel = (s) => label(s, { weekday: 'short', day: 'numeric', month: 'short' });
|
|
38
|
+
/** ISO 8601 week number of a YYYY-MM-DD date. */
|
|
39
|
+
function isoWeek(s) {
|
|
40
|
+
const d = dateOf(s);
|
|
41
|
+
const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));
|
|
42
|
+
const wd = t.getUTCDay() || 7;
|
|
43
|
+
t.setUTCDate(t.getUTCDate() + 4 - wd);
|
|
44
|
+
const y0 = new Date(Date.UTC(t.getUTCFullYear(), 0, 1));
|
|
45
|
+
return Math.ceil(((t - y0) / 86400000 + 1) / 7);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function loadMonth(month) {
|
|
49
|
+
const key = month || 'now';
|
|
50
|
+
if (inflight.has(key)) return;
|
|
51
|
+
inflight.add(key);
|
|
52
|
+
const out = await pmGet('/api/proactive/agenda' + (month ? '?month=' + encodeURIComponent(month) : ''));
|
|
53
|
+
inflight.delete(key);
|
|
54
|
+
const m = out && out.ok ? out.month : month;
|
|
55
|
+
const prev = cache.get(m);
|
|
56
|
+
const next = out && out.ok ? { data: out, error: null, old: false }
|
|
57
|
+
: { data: prev ? prev.data : null, error: (out && out.error) || A.unread, old: !!(out && out.status === 404) };
|
|
58
|
+
if (!ag.month && out && out.ok) ag.month = out.month;
|
|
59
|
+
if (!month && !(out && out.ok)) cache.set('now', next);
|
|
60
|
+
if (m) cache.set(m, next);
|
|
61
|
+
// `now` moves every read: compare it to the minute, so a read that changes nothing draws nothing
|
|
62
|
+
if (!prev || sigOf(prev) !== sigOf(next)) redraw();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const sigOf = (c) => JSON.stringify({ ...c, data: c.data ? { ...c.data, now: String(c.data.now).slice(0, 16) } : null });
|
|
66
|
+
|
|
67
|
+
/** Read the shown month again (the day read calls this, so the month follows the brief and the clock). */
|
|
68
|
+
export function refreshAgenda() { loadMonth(ag.month); }
|
|
69
|
+
|
|
70
|
+
function current() {
|
|
71
|
+
const c = (ag.month && cache.get(ag.month)) || cache.get('now') || null;
|
|
72
|
+
if (!c) { loadMonth(ag.month); return null; }
|
|
73
|
+
return c;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function agendaSig() {
|
|
77
|
+
const c = ag.month ? cache.get(ag.month) : cache.get('now');
|
|
78
|
+
return JSON.stringify([ag, c ? [c.error, c.data && c.data.now ? String(c.data.now).slice(0, 16) : null, c.data && Object.keys(c.data.days || {}).length, c.data && c.data.memory] : null])
|
|
79
|
+
+ (c && c.data ? JSON.stringify(c.data.days).length : 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ── finding a meeting across the months read ─────────────────────────────────────────────────────────────────── */
|
|
83
|
+
|
|
84
|
+
function findEvent(id) {
|
|
85
|
+
for (const c of cache.values()) {
|
|
86
|
+
if (!c.data) continue;
|
|
87
|
+
for (const d of Object.values(c.data.days)) {
|
|
88
|
+
const e = d.events.find((x) => x.id === id);
|
|
89
|
+
if (e) return { e, day: d, data: c.data };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The series key of the selected meeting (a remembered one, or today's from the brief), or null. */
|
|
96
|
+
export function litSeries(sel) {
|
|
97
|
+
if (!sel || (sel.type !== 'meeting' && sel.type !== 'event')) return null;
|
|
98
|
+
const f = findEvent(sel.id);
|
|
99
|
+
return f ? { key: f.e.key, title: f.e.title } : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* ── the month ────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
103
|
+
|
|
104
|
+
function strip(day, lens) {
|
|
105
|
+
const s = el('div', { class: 'td-ag__strip', 'aria-hidden': 'true' });
|
|
106
|
+
const put = (from, mins, cls) => {
|
|
107
|
+
const a = Math.max(0, from - DAY0); const b = Math.min(SPAN, from - DAY0 + mins);
|
|
108
|
+
if (b <= a) return;
|
|
109
|
+
const n = el('span', { class: cls });
|
|
110
|
+
n.style.setProperty('--td-ag-l', (a / SPAN * 100).toFixed(2) + '%');
|
|
111
|
+
n.style.setProperty('--td-ag-w', ((b - a) / SPAN * 100).toFixed(2) + '%');
|
|
112
|
+
s.append(n);
|
|
113
|
+
};
|
|
114
|
+
if (lens === 'free') for (const [a, b] of day.gaps) put(a, b - a, 'td-ag__blk td-ag__blk--free');
|
|
115
|
+
else for (const e of day.events) { const [h, m] = e.time.split(':').map(Number); put(h * 60 + m, e.minutes, 'td-ag__blk' + (e.imp ? ' td-ag__blk--imp' : '')); }
|
|
116
|
+
return s;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readyBadge(day, withWord) {
|
|
120
|
+
if (!day.need) return null;
|
|
121
|
+
return el('span', { class: 'td-ag__ready', 'data-ag-ready': day.ready || 'ok', title: A.readyWord[day.ready || 'ok'] },
|
|
122
|
+
A.readyBadge(day.done, day.need), withWord ? ' ' + A.readyWord[day.ready || 'ok'] : null);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function keyLine(day) {
|
|
126
|
+
if (!day.key) return null;
|
|
127
|
+
const e = day.events.find((x) => x.id === day.key);
|
|
128
|
+
return e ? el('span', { class: 'td-ag__key' }, A.star + ' ' + e.title) : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function cell(data, d, lit) {
|
|
132
|
+
const day = data.days[d];
|
|
133
|
+
const lens = ag.lens;
|
|
134
|
+
const kids = [el('span', { class: 'td-ag__num' }, String(Number(d.slice(8))))];
|
|
135
|
+
if (day.events.length && (lens === 'overview' || lens === 'full' || lens === 'free')) kids.push(strip(day, lens));
|
|
136
|
+
if (lens === 'overview') {
|
|
137
|
+
kids.push(keyLine(day));
|
|
138
|
+
if (day.ready === 'risk' || day.ready === 'warn' || (day.ready === 'ok' && !day.past)) kids.push(readyBadge(day));
|
|
139
|
+
if (day.past && day.no_notes) kids.push(el('span', { class: 'td-ag__nonotes' }, A.noNotes(day.no_notes)));
|
|
140
|
+
} else if (lens === 'full') {
|
|
141
|
+
if (day.busy) kids.push(el('span', { class: 'td-ag__meta' }, A.busy(day.busy)));
|
|
142
|
+
} else if (lens === 'key') {
|
|
143
|
+
kids.push(keyLine(day));
|
|
144
|
+
} else if (lens === 'ready') {
|
|
145
|
+
kids.push(readyBadge(day, false));
|
|
146
|
+
if (day.past && day.no_notes) kids.push(el('span', { class: 'td-ag__nonotes' }, A.noNotes(day.no_notes)));
|
|
147
|
+
} else if (lens === 'free' && !day.weekend) {
|
|
148
|
+
kids.push(el('span', { class: 'td-ag__meta' }, day.gaps.length ? A.freeBlocks(day.gaps.length) : A.noFree));
|
|
149
|
+
}
|
|
150
|
+
const picked = ag.pick && ag.pick.type === 'day' && ag.pick.date === d;
|
|
151
|
+
return button({ label: el('span', { class: 'td-ag__in' }, ...kids.filter(Boolean)), variant: 'ghost', onClick: () => pickDay(d),
|
|
152
|
+
attrs: { class: 'td-ag__cell', 'data-ag-day': d, 'data-out': day.in_month ? null : 'true', 'data-past': day.past ? 'true' : null, 'data-today': day.today ? 'true' : null,
|
|
153
|
+
'data-weekend': day.weekend ? 'true' : null, 'data-lit': lit && lit.has(d) ? 'true' : null, 'aria-pressed': picked ? 'true' : 'false', title: A.dayHint(dayLabel(d), day.key ? (day.events.find((e) => e.id === day.key) || {}).title : null) } });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function pickDay(d) { ag.pick = { type: 'day', date: d }; ag.showRoutine = false; redraw(); }
|
|
157
|
+
function pickWeek(i) { ag.pick = { type: 'week', i }; ag.showRoutine = false; redraw(); }
|
|
158
|
+
function goMonth(n, data) {
|
|
159
|
+
ag.month = n === 0 ? ymOf(data.today) : shiftMonth(ag.month || data.month, n);
|
|
160
|
+
ag.pick = null;
|
|
161
|
+
if (!cache.has(ag.month)) loadMonth(ag.month);
|
|
162
|
+
redraw();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function month(data, lit) {
|
|
166
|
+
const head = el('div', { class: 'td-ag__mhead' },
|
|
167
|
+
iconButton({ icon: 'chevron-left', ariaLabel: A.prev, size: 'sm', attrs: { 'data-ag': 'prev' }, onClick: () => goMonth(-1, data) }),
|
|
168
|
+
el('b', { class: 'td-ag__mlabel', 'data-ag': 'month' }, label(data.month + '-01', { month: 'long', year: 'numeric' })),
|
|
169
|
+
iconButton({ icon: 'chevron-right', ariaLabel: A.later, size: 'sm', attrs: { 'data-ag': 'later' }, onClick: () => goMonth(1, data) }),
|
|
170
|
+
ymOf(data.today) !== data.month ? button({ label: A.thisMonth, size: 'sm', variant: 'ghost', attrs: { 'data-ag': 'this-month' }, onClick: () => goMonth(0, data) }) : null);
|
|
171
|
+
const lens = tabs({ variant: 'segmented', role: 'radiogroup', ariaLabel: A.title, value: ag.lens, attrs: { 'data-ag': 'lens', class: 'td-ag__lens' },
|
|
172
|
+
items: Object.keys(A.lens).map((k) => ({ id: k, label: A.lens[k] })), onChange: (v) => { ag.lens = v; redraw(); } });
|
|
173
|
+
const wd = el('div', { class: 'td-ag__wk td-ag__wk--head', 'aria-hidden': 'true' }, el('span', null, ''),
|
|
174
|
+
...data.weeks[0].map((d) => el('span', { class: 'td-ag__wd' }, label(d, { weekday: 'short' }))));
|
|
175
|
+
const rows = data.weeks.map((w, i) => {
|
|
176
|
+
const n = isoWeek(w[0]);
|
|
177
|
+
const picked = ag.pick && ag.pick.type === 'week' && ag.pick.i === i;
|
|
178
|
+
return el('div', { class: 'td-ag__wk', role: 'group', 'aria-label': A.weekOf(n) },
|
|
179
|
+
button({ label: A.weekNo(n), variant: 'ghost', size: 'sm', onClick: () => pickWeek(i), attrs: { class: 'td-ag__wno', 'data-ag-week': String(n), 'aria-pressed': picked ? 'true' : 'false', title: A.weekHint(n) } }),
|
|
180
|
+
...w.map((d) => cell(data, d, lit)));
|
|
181
|
+
});
|
|
182
|
+
return [head, lens, el('p', { class: 'td-day__note', 'data-ag': 'lens-help' }, A.lensHelp[ag.lens]),
|
|
183
|
+
el('div', { class: 'td-ag__grid', 'data-ag': 'grid', 'data-lens': ag.lens }, wd, ...rows)];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* ── the agenda under the month ───────────────────────────────────────────────────────────────────────────────── */
|
|
187
|
+
|
|
188
|
+
function tiles(day) {
|
|
189
|
+
const key = day.key ? day.events.find((e) => e.id === day.key) : null;
|
|
190
|
+
const t = (k, v, attrs) => el('div', { class: 'td-ag__tile', ...(attrs || {}) }, el('span', { class: 'td-ag__tk' }, A.tiles[k]), el('b', null, v));
|
|
191
|
+
return el('div', { class: 'td-ag__tiles', 'data-ag': 'tiles' },
|
|
192
|
+
t('busy', day.busy ? A.busy(day.busy) : A.noMeetings),
|
|
193
|
+
t('key', key ? key.title : A.noKey),
|
|
194
|
+
t('ready', day.need ? A.readyBadge(day.done, day.need) + ' · ' + A.readyWord[day.ready || 'ok'] : A.noPrep, day.need ? { 'data-ag-ready': day.ready || 'ok' } : null));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function eventRow(h, e) {
|
|
198
|
+
const type = e.src === 'brief' ? 'event' : 'meeting';
|
|
199
|
+
const chips = el('span', { class: 'td-day__chips' },
|
|
200
|
+
e.imp ? badge({ text: A.star, tone: 'outline', attrs: { title: A.means } }) : null,
|
|
201
|
+
e.prep ? el('span', { class: 'td-ag__ready', 'data-ag-ready': e.ready || 'ok' }, A.readyBadge(e.prep_done || 0, e.prep.length)) : null,
|
|
202
|
+
e.ended && !e.notes && (e.imp || e.kind === 'one') ? el('span', { class: 'td-ag__nonotes' }, A.noNotes(1)) : null);
|
|
203
|
+
return h.rowBtn(type, e.id, { lead: e.time, title: e.title, meta: A.kindWord[e.kind] + (e.attendees ? ' · ' + A.people(e.attendees) : ''), trail: chips },
|
|
204
|
+
{ 'data-past': e.ended ? 'true' : null, 'data-ag-event': e.id });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function rowsOf(h, evs) {
|
|
208
|
+
const main = evs.filter((e) => !e.routine);
|
|
209
|
+
const rout = evs.filter((e) => e.routine);
|
|
210
|
+
return [
|
|
211
|
+
main.length ? el('ul', { class: 'td-day__rows', 'data-day-list': 'agenda' }, ...main.map((e) => eventRow(h, e))) : null,
|
|
212
|
+
rout.length ? button({ label: ag.showRoutine ? A.hideRoutine : A.routine(rout.length), variant: 'ghost', size: 'sm',
|
|
213
|
+
attrs: { 'data-ag': 'routine', 'aria-expanded': ag.showRoutine ? 'true' : 'false' }, onClick: () => { ag.showRoutine = !ag.showRoutine; redraw(); } }) : null,
|
|
214
|
+
rout.length && ag.showRoutine ? el('ul', { class: 'td-day__rows', 'data-day-list': 'routine' }, ...rout.map((e) => eventRow(h, e))) : null,
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function agendaOf(data, h) {
|
|
219
|
+
const pick = ag.pick || (data.days[data.today] ? { type: 'day', date: data.today } : { type: 'day', date: data.month + '-01' });
|
|
220
|
+
if (pick.type === 'week') {
|
|
221
|
+
const w = data.weeks[pick.i] || data.weeks[0];
|
|
222
|
+
const all = w.flatMap((d) => data.days[d].events);
|
|
223
|
+
const days = w.filter((d) => data.days[d].events.some((e) => !e.routine));
|
|
224
|
+
const rout = all.filter((e) => e.routine);
|
|
225
|
+
return el('div', { class: 'td-ag__agenda', 'data-ag': 'agenda', 'data-ag-pick': 'week:' + isoWeek(w[0]) },
|
|
226
|
+
el('h3', { class: 'td-ag__ah' }, A.weekOf(isoWeek(w[0])), el('span', { class: 'td-day__dim' }, ' · ' + dayLabel(w[0]) + ' – ' + dayLabel(w[6]))),
|
|
227
|
+
days.length ? days.map((d) => el('div', { class: 'td-ag__wday' }, el('h4', null, dayLabel(d), data.days[d].busy ? el('span', { class: 'td-day__dim' }, ' · ' + A.busy(data.days[d].busy)) : null, readyBadge(data.days[d])),
|
|
228
|
+
el('ul', { class: 'td-day__rows' }, ...data.days[d].events.filter((e) => !e.routine).map((e) => eventRow(h, e))))) : el('p', { class: 'td-day__note' }, A.noMeetings),
|
|
229
|
+
rout.length ? button({ label: ag.showRoutine ? A.hideRoutine : A.routine(rout.length), variant: 'ghost', size: 'sm',
|
|
230
|
+
attrs: { 'data-ag': 'routine', 'aria-expanded': ag.showRoutine ? 'true' : 'false' }, onClick: () => { ag.showRoutine = !ag.showRoutine; redraw(); } }) : null,
|
|
231
|
+
rout.length && ag.showRoutine ? el('ul', { class: 'td-day__rows', 'data-day-list': 'routine' }, ...rout.map((e) => eventRow(h, e))) : null);
|
|
232
|
+
}
|
|
233
|
+
const day = data.days[pick.date];
|
|
234
|
+
if (!day) return null;
|
|
235
|
+
// today keeps the day's calendar from the brief (the line at the current time, the goal ties, the prep chips)
|
|
236
|
+
const todayList = day.today && h.todayRows ? h.todayRows() : null;
|
|
237
|
+
return el('div', { class: 'td-ag__agenda', 'data-ag': 'agenda', 'data-ag-pick': 'day:' + pick.date },
|
|
238
|
+
el('h3', { class: 'td-ag__ah' }, day.today ? A.today + ' · ' + dayLabel(pick.date) : dayLabel(pick.date)),
|
|
239
|
+
tiles(day),
|
|
240
|
+
todayList || (day.events.length ? rowsOf(h, day.events) : el('p', { class: 'td-day__note' }, A.noMeetings)));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** The panel. `h` lends the day section's helpers: { panel, rowBtn(type, id, parts, mods), todayRows(), sel }. */
|
|
244
|
+
export function agendaPanel(ctx, h) {
|
|
245
|
+
const c = current();
|
|
246
|
+
if (!c) return h.panel('agenda', null, el('p', { class: 'td-day__note' }, A.loading));
|
|
247
|
+
if (!c.data) return c.old ? null : h.panel('agenda', null, el('p', { class: 'td-day__note', role: 'status' }, c.error || A.unread));
|
|
248
|
+
const data = c.data;
|
|
249
|
+
const lit = litSeries(h.sel);
|
|
250
|
+
const litDays = lit && data.series[lit.key] ? new Set(data.series[lit.key].dates) : null;
|
|
251
|
+
const empty = !data.memory || !data.memory.events;
|
|
252
|
+
return h.panel('agenda', null,
|
|
253
|
+
...month(data, litDays),
|
|
254
|
+
litDays ? el('p', { class: 'td-day__note', 'data-ag': 'lit' }, A.lit(lit.title, [...litDays].filter((d) => data.days[d]).length)) : null,
|
|
255
|
+
empty ? el('p', { class: 'td-day__note', 'data-ag': 'empty' }, A.empty) : null,
|
|
256
|
+
agendaOf(data, h));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* ── the right column ─────────────────────────────────────────────────────────────────────────────────────────── */
|
|
260
|
+
|
|
261
|
+
function whyLines(e) {
|
|
262
|
+
const out = [];
|
|
263
|
+
if (e.role === 'present') out.push(A.why.present);
|
|
264
|
+
if (e.role === 'decide') out.push(A.why.decide);
|
|
265
|
+
if (e.external) out.push(A.why.external);
|
|
266
|
+
if (e.exec) out.push(A.why.exec);
|
|
267
|
+
if (e.attendees && e.attendees >= 8) out.push(A.why.many(e.attendees));
|
|
268
|
+
if (e.routine) out.push(A.why.routine);
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The series of a meeting: how often it met, what is to come, and the last time, folded. Null when unknown. */
|
|
273
|
+
export function memoryBlock(id) {
|
|
274
|
+
const f = findEvent(id);
|
|
275
|
+
if (!f) return null;
|
|
276
|
+
const s = f.data.series[f.e.key];
|
|
277
|
+
if (!s) return null;
|
|
278
|
+
const last = s.last && s.last.date !== f.day.date ? s.last : null;
|
|
279
|
+
return el('div', { class: 'td-day__notes', 'data-ag': 'memory' },
|
|
280
|
+
el('h4', null, A.means),
|
|
281
|
+
el('p', null, A.kindWord[f.e.kind] + '. ' + (s.count > 1 ? A.series(s.past, s.coming) : A.once)),
|
|
282
|
+
...whyLines(f.e).map((t) => el('p', null, t)),
|
|
283
|
+
last ? el('details', { class: 'td-ag__last', 'data-ag': 'last' }, el('summary', null, A.lastTime(dayLabel(last.date))),
|
|
284
|
+
last.notes ? el('p', null, last.notes) : el('p', { class: 'td-day__note' }, A.noLast),
|
|
285
|
+
last.open && last.open.length ? el('ul', { class: 'td-day__chk' }, ...last.open.map((x) => el('li', null, x))) : null) : null);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function meetingDetail(ctx, id, h) {
|
|
289
|
+
const f = findEvent(id);
|
|
290
|
+
if (!f) return null;
|
|
291
|
+
const e = f.e;
|
|
292
|
+
const end = (() => { const d = new Date(e.end); return `${pad(d.getHours())}:${pad(d.getMinutes())}`; })();
|
|
293
|
+
return [
|
|
294
|
+
h.dhead(A.kindWord[e.kind], e.title),
|
|
295
|
+
el('p', { class: 'td-day__dim td-day__mono' }, `${dayLabel(f.day.date)} · ${e.time}–${end}${e.attendees ? ' · ' + A.people(e.attendees) : ''}`),
|
|
296
|
+
memoryBlock(id),
|
|
297
|
+
e.prep ? el('div', { class: 'td-day__phase', 'data-ag': 'before' }, el('h4', null, A.before),
|
|
298
|
+
el('p', null, A.prepLine(e.prep_done || 0, e.prep.length)),
|
|
299
|
+
el('ul', { class: 'td-day__chk' }, ...e.prep.map((t, i) => el('li', { class: i < (e.prep_done || 0) ? 'td-day__ok' : '' }, t))),
|
|
300
|
+
el('p', { class: e.ready === 'risk' ? 'td-day__warn' : 'td-day__note', 'data-ag-ready': e.ready || 'ok' }, A.readyNote[e.ready || 'ok'])) : null,
|
|
301
|
+
e.ended ? el('div', { class: 'td-day__phase', 'data-ag': 'notes' }, el('h4', null, A.afterNotes),
|
|
302
|
+
e.notes ? el('p', null, e.notes) : el('p', { class: 'td-day__note' }, A.noNotesYet),
|
|
303
|
+
e.open && e.open.length ? [el('h4', null, A.openLines), el('ul', { class: 'td-day__chk' }, ...e.open.map((x) => el('li', null, x)))] : null) : null,
|
|
304
|
+
];
|
|
305
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
export const COPY = {
|
|
12
|
-
title: '
|
|
12
|
+
title: 'Agenda',
|
|
13
13
|
/** "3 need you", "1 needs you". */
|
|
14
14
|
count: (n) => `${n} ${n === 1 ? 'needs' : 'need'} you`,
|
|
15
15
|
countIn: (n, repo) => `${n} ${n === 1 ? 'needs' : 'need'} you in ${repo || 'this repository'}`,
|
|
@@ -128,7 +128,7 @@ export const COPY = {
|
|
|
128
128
|
unreached: 'The workspace did not answer.',
|
|
129
129
|
refused: (why) => `Nothing changed. ${why}`,
|
|
130
130
|
oldServer: 'This workspace server has no day run yet. Only your goals show.',
|
|
131
|
-
panels: { summary: 'Agent results', calendar: 'Calendar', jobs: 'Jobs', goals: 'Goals for today', mentions: 'Mentions since yesterday', waiting: 'Also waiting for you', detail: 'Detail' },
|
|
131
|
+
panels: { summary: 'Agent results', calendar: 'Calendar', agenda: 'Agenda', jobs: 'Jobs', goals: 'Goals for today', mentions: 'Mentions since yesterday', waiting: 'Also waiting for you', detail: 'Detail' },
|
|
132
132
|
summary: (c) => [c.review ? `${c.review} ${c.review === 1 ? 'draft waits' : 'drafts wait'} for you` : null, c.done ? `${c.done} done` : null,
|
|
133
133
|
c.working ? `${c.working} working` : null, c.failed ? `${c.failed} failed` : null].filter(Boolean).join(' · ') || 'No agent job yet.',
|
|
134
134
|
summaryAll: (n) => `All ${n} agent ${n === 1 ? 'job has' : 'jobs have'} a result.`,
|
|
@@ -282,6 +282,58 @@ export const COPY = {
|
|
|
282
282
|
siteFull: 'At most 10 websites.',
|
|
283
283
|
remove: 'Remove',
|
|
284
284
|
noSources: 'The sources could not be read.',
|
|
285
|
+
/** The Agenda month (G25 to G27): the month on top, the agenda of a day or a week under it, the meeting's memory. */
|
|
286
|
+
ag: {
|
|
287
|
+
title: 'Agenda',
|
|
288
|
+
loading: 'Reading the month…',
|
|
289
|
+
prev: 'Earlier month',
|
|
290
|
+
later: 'Later month',
|
|
291
|
+
thisMonth: 'This month',
|
|
292
|
+
weekNo: (n) => `W${n}`,
|
|
293
|
+
weekOf: (n) => `Week ${n}`,
|
|
294
|
+
weekHint: (n) => `Show the agenda of week ${n}`,
|
|
295
|
+
dayHint: (label, key) => `Show the agenda of ${label}${key ? '. Matters most: ' + key : ''}`,
|
|
296
|
+
lens: { overview: 'Overview', full: 'Full', key: 'Key', ready: 'Prep', free: 'Free' },
|
|
297
|
+
lensHelp: {
|
|
298
|
+
overview: 'Each day: how full it is, the meeting that matters and your prep.',
|
|
299
|
+
full: 'The bar is 09:00 to 18:00. Each block is a meeting. Dark blocks are important.',
|
|
300
|
+
key: 'The one meeting of the day that matters, when there is one.',
|
|
301
|
+
ready: 'Prep done out of prep needed. Red: too little free time before the meeting.',
|
|
302
|
+
free: 'Free blocks of 90 minutes or more.',
|
|
303
|
+
},
|
|
304
|
+
busy: (min) => (min >= 60 ? `${Math.floor(min / 60)} h${min % 60 ? ' ' + (min % 60) + ' min' : ''}` : `${min} min`),
|
|
305
|
+
freeBlocks: (n) => `${n} free ${n === 1 ? 'block' : 'blocks'}`,
|
|
306
|
+
noFree: 'No long free block',
|
|
307
|
+
readyBadge: (done, need) => `${done}/${need}`,
|
|
308
|
+
readyWord: { ok: 'prep done', warn: 'prep left', risk: 'prep at risk' },
|
|
309
|
+
noNotes: (n) => `${n} no notes`,
|
|
310
|
+
star: '★',
|
|
311
|
+
lit: (title, n) => `${title}: ${n} ${n === 1 ? 'day' : 'days'} marked on the month.`,
|
|
312
|
+
empty: 'Nothing is remembered yet. Press Prepare my day: the session reads your calendar 90 days back and 31 days ahead.',
|
|
313
|
+
emptyMonth: 'No meetings remembered for this month.',
|
|
314
|
+
unread: 'The month could not be read.',
|
|
315
|
+
tiles: { busy: 'In meetings', key: 'Matters most', ready: 'Prep' },
|
|
316
|
+
noKey: 'Nothing stands out',
|
|
317
|
+
noPrep: 'Nothing to prepare',
|
|
318
|
+
noMeetings: 'No meetings.',
|
|
319
|
+
routine: (n) => `+${n} repeating, nothing for you`,
|
|
320
|
+
hideRoutine: 'Hide the repeating ones',
|
|
321
|
+
kindWord: { one: 'One-to-one', onboard: 'Onboarding', group: 'Group meeting' },
|
|
322
|
+
why: { present: 'You present.', decide: 'You decide.', external: 'People from outside the company join.', exec: 'Leadership joins.', many: (n) => `${n} people join.`, routine: 'It repeats, and nothing in it is for you.' },
|
|
323
|
+
means: 'What it means',
|
|
324
|
+
before: 'Before this meeting',
|
|
325
|
+
prepLine: (done, need) => `${done} of ${need} prep steps done.`,
|
|
326
|
+
readyNote: { ok: 'Your prep is done.', warn: 'Some prep is left, and there is time for it.', risk: 'The free time before it is short for the prep left.' },
|
|
327
|
+
series: (past, coming) => `Held ${past} ${past === 1 ? 'time' : 'times'} so far · ${coming} to come.`,
|
|
328
|
+
once: 'It happens once.',
|
|
329
|
+
lastTime: (date) => `Last time · ${date}`,
|
|
330
|
+
noLast: 'No notes from the last time.',
|
|
331
|
+
afterNotes: 'Notes',
|
|
332
|
+
openLines: 'Still open',
|
|
333
|
+
noNotesYet: 'No notes for this meeting.',
|
|
334
|
+
people: (n) => `${n} people`,
|
|
335
|
+
today: 'Today',
|
|
336
|
+
},
|
|
285
337
|
},
|
|
286
338
|
/** The Acceptance Gate card (D12): the PM's own act on a run. */
|
|
287
339
|
accept: {
|