create-pmos 0.54.1 → 0.55.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.
@@ -0,0 +1,760 @@
1
+ /* pm/today-day.js — the day section at the top of Today (initiative today-goals, PRD G9 to G22).
2
+ *
3
+ * daySection({ ws, setState, openIni }) → the section node, or null in fixture mode
4
+ * daySig(ws) → a string that changes whenever what the section shows changes
5
+ *
6
+ * One read feeds it: GET /api/proactive/day (the run, its stage, the fresh brief with its calendar, items and digests, the
7
+ * goals' plan, the source settings and states, the daytime check). The section draws:
8
+ * - a one-line top bar: the greeting, the four steps of "Prepare my day" (each selects its detail), the last and the
9
+ * coming check with "Check now", Sources, and "Prepare my day";
10
+ * - on the left, the calendar (with a line at the current time), the jobs (needs you, agent working, done), the goals
11
+ * and the mentions with the watched channels, projects and spaces;
12
+ * - on the right, the detail of what is selected: a meeting, a job, a goal, a mention, a step, the sources or a place.
13
+ * The presses go to the routes that own them: the goals route (add, done, drop, reopen, remove), /goals/plan (Prepare my
14
+ * day), /day/decide (approve, revise, run again), /day/check (Check now) and the settings route (sources, levels, the
15
+ * daytime check, websites). Nothing here sends anything outward: the agent sessions do that, after the PM approves.
16
+ *
17
+ * FIRST PAINT. Folded, the section is one control (its toggle), so Today keeps inside its control budget
18
+ * (first-paint-today.test.js). It opens by itself on a day the PM pressed "Prepare my day", and it remembers the PM's own
19
+ * choice in this browser.
20
+ *
21
+ * REFRESH. Like the goals before it, the read repeats every 30 seconds, and every 4 seconds while a run reads or works, a
22
+ * check runs or a press is in flight. A read that changes nothing draws nothing.
23
+ */
24
+
25
+ import { el } from './dom.js';
26
+ import { button, iconButton, badge, toast, row, tabs, field } from './primitives.js';
27
+ import { pmGet, pmPost } from './http.js';
28
+ import { FIXTURE } from './state.js';
29
+ import { COPY } from './today-copy.js';
30
+ import { goalRows, goalAdd, goalDetail, addGoal, goalsSig } from './today-goals.js';
31
+
32
+ const D = COPY.day;
33
+ const SLOW_MS = 30000;
34
+ const FAST_MS = 4000;
35
+ const MAX_REVISIONS = 3;
36
+ const SCOPED = ['slack', 'jira', 'confluence'];
37
+ const NARROW = 860; // below this width of the section the detail goes under the list (components.css @container td-day)
38
+
39
+ /* ── per-viewer conveniences (never state that must persist): the fold and the new items already seen ── */
40
+ const store = {
41
+ get(k) { try { return localStorage.getItem(k); } catch { return null; } },
42
+ set(k, v) { try { localStorage.setItem(k, v); } catch { /* private window: the default applies */ } },
43
+ };
44
+ const OPEN_KEY = 'pmos.today.day.open';
45
+ const SEEN_KEY = 'pmos.today.day.seen';
46
+ let seen = new Set();
47
+ try { seen = new Set(JSON.parse(store.get(SEEN_KEY) || '[]')); } catch { seen = new Set(); }
48
+
49
+ const cache = new Map(); // ws -> { data, error }
50
+ const runs = new Map(); // ws -> the last "Prepare my day" press: { state: 'busy'|'started'|'running'|'failed', at, session, error }
51
+ const ui = {
52
+ open: (() => { const v = store.get(OPEN_KEY); return v === 'true' ? true : v === 'false' ? false : null; })(),
53
+ sel: null, mtab: 'all', openSrc: null, showIgn: {}, showIgnDigests: false,
54
+ revising: new Set(), notes: new Map(), siteDraft: '', placeDraft: {}, busy: new Set(), scroll: false,
55
+ };
56
+ let rerender = () => {};
57
+ let timer = null;
58
+
59
+ /* ── the read ─────────────────────────────────────────────────────────────────────────────────────────────────── */
60
+
61
+ const minuteOf = (v) => { const t = typeof v === 'number' ? v : Date.parse(v); return Number.isFinite(t) ? Math.floor(t / 60000) : null; };
62
+ const dataSig = (d) => (d ? JSON.stringify({ ...d, now: minuteOf(d.now) }) : 'null');
63
+
64
+ async function load(ws) {
65
+ const q = ws ? '?ws=' + encodeURIComponent(ws) : '';
66
+ let out = await pmGet('/api/proactive/day' + q);
67
+ if (out && out.ok === false && out.status === 404) {
68
+ // a workspace server from before the day run: show the goals from the older read, and say so
69
+ const g = await pmGet('/api/proactive/goals' + q);
70
+ out = g && g.ok ? { ok: true, legacy: true, today: g.today, now: null, run: null, stage: null, brief: null, plan: g.plan, max_goals: g.max_goals, settings: null, sources: [], check: null } : g;
71
+ }
72
+ const prev = cache.get(ws);
73
+ const next = out && out.ok ? { data: out, error: null } : { data: prev ? prev.data : null, error: (out && out.error) || D.unreached };
74
+ cache.set(ws, next);
75
+ if (!prev || dataSig(prev.data) !== dataSig(next.data) || prev.error !== next.error) rerender();
76
+ schedule();
77
+ }
78
+
79
+ function busyNow() {
80
+ for (const [ws, c] of cache) {
81
+ const d = c.data;
82
+ if (d && (d.stage === 'reading' || d.stage === 'working' || (d.check && d.check.running))) return true;
83
+ if (d && d.run && (d.run.tasks || []).some((t) => t.status === 'sending')) return true;
84
+ const r = runs.get(ws);
85
+ if (r && r.state === 'busy') return true;
86
+ }
87
+ return false;
88
+ }
89
+
90
+ function schedule() {
91
+ if (typeof window === 'undefined') return;
92
+ if (timer) clearTimeout(timer);
93
+ timer = setTimeout(() => {
94
+ timer = null;
95
+ // a hidden Today reads at the slow pace only: the reads cost the server, and nobody looks
96
+ if (!document.querySelector('[data-today="day"]') && !busyNow()) { schedule(); return; }
97
+ for (const w of cache.keys()) load(w);
98
+ }, busyNow() ? FAST_MS : SLOW_MS);
99
+ }
100
+
101
+ function ensure(ws) {
102
+ if (!cache.has(ws)) { cache.set(ws, { data: null, error: null }); load(ws); }
103
+ }
104
+
105
+ /* ── the model: the read, joined once per draw ────────────────────────────────────────────────────────────────── */
106
+
107
+ const pad = (n) => String(n).padStart(2, '0');
108
+ const hhmm = (v) => { if (v === null || v === undefined || v === '') return ''; const d = new Date(typeof v === 'number' ? v : Date.parse(v)); return Number.isNaN(d.getTime()) ? String(v) : `${pad(d.getHours())}:${pad(d.getMinutes())}`; };
109
+ const isUrl = (s) => typeof s === 'string' && s.length <= 500 && /^https?:\/\/\S+$/i.test(s);
110
+ const srcName = (k) => D.srcName[k] || (k === 'derived' ? 'PMOS' : String(k || ''));
111
+
112
+ function jobOf(item, task, worth) {
113
+ const kind = (item && item.kind) || (task && task.kind) || (item && item.owner === 'pm' ? 'you' : 'approve');
114
+ const status = task ? task.status : kind === 'you' ? 'you' : 'notStarted';
115
+ const group = worth ? 'worth' : ['you', 'review', 'failed'].includes(status) ? 'needs' : ['done', 'sent'].includes(status) ? 'done' : 'working';
116
+ return {
117
+ id: item ? item.id : task.item_id, item, task, kind, status, group, worth,
118
+ title: (item && item.title) || (task && task.title) || '', source: (item && item.source) || (task && task.source) || '',
119
+ link: (item && isUrl(item.link) && item.link) || (task && isUrl(task.link) && task.link) || null,
120
+ goal: (item && item.goal) || (task && task.goal) || null, isNew: !!(item && item.new),
121
+ };
122
+ }
123
+
124
+ function deriveStage(run, brief) {
125
+ if (!run) return null;
126
+ if (!brief) return 'reading';
127
+ const ts = run.tasks || [];
128
+ if (ts.some((t) => ['waiting', 'working', 'sending'].includes(t.status))) return 'working';
129
+ if (ts.some((t) => t.status === 'review')) return 'review';
130
+ return 'done';
131
+ }
132
+
133
+ function model(d) {
134
+ const today = d && d.today;
135
+ const brief = d && d.brief && (!today || d.brief.date === today) ? d.brief : null;
136
+ const plan = (d && d.plan) || null;
137
+ const goals = plan ? plan.goals || [] : [];
138
+ const goalNo = new Map(goals.map((g, i) => [g.id, i + 1]));
139
+ const run = (d && d.run) || null;
140
+ const tasks = run && Array.isArray(run.tasks) ? run.tasks : [];
141
+ const byItem = new Map(tasks.map((t) => [t.item_id, t]));
142
+ const used = new Set();
143
+ const jobs = [];
144
+ const add = (x, worth) => { const t = byItem.get(x.id) || null; if (t) used.add(t.id); jobs.push(jobOf(x, t, worth)); };
145
+ for (const x of (brief && brief.priorities) || []) add(x, false);
146
+ for (const x of (brief && brief.worth_asking) || []) add(x, true);
147
+ for (const t of tasks) if (!used.has(t.id)) jobs.push(jobOf(null, t, false)); // an item a check took off: its task stays
148
+ const now = d && d.now ? (typeof d.now === 'number' ? d.now : Date.parse(d.now)) : Date.now();
149
+ const events = ((brief && brief.calendar) || []).map((e, i) => {
150
+ const s = Date.parse(e.start);
151
+ const f = e.end ? Date.parse(e.end) : s + 30 * 60000;
152
+ return { ...e, id: e.id || 'ev-' + i, s, f, time: hhmm(e.start), ended: Number.isFinite(f) && f <= now };
153
+ }).sort((a, b) => a.s - b.s);
154
+ const settings = (d && d.settings) || null;
155
+ const scopes = (settings && settings.scopes) || {};
156
+ const level = (svc, name) => (scopes[svc] && scopes[svc][name]) || 'search';
157
+ const digests = ((brief && Array.isArray(brief.digests)) ? brief.digests : []).map((g) => ({ ...g, id: g.source + '|' + g.name, level: level(g.source, g.name) }));
158
+ return {
159
+ d, brief, plan, goals, goalNo, run, tasks, jobs, events, now, settings, scopes, level, digests,
160
+ stage: run ? (d.stage || deriveStage(run, brief)) : null,
161
+ mentions: jobs.filter((j) => j.item && j.item.mention),
162
+ sources: (d && Array.isArray(d.sources)) ? d.sources : [],
163
+ check: (d && d.check) || null,
164
+ free: plan && plan.time ? plan.time.free_minutes : null,
165
+ maxGoals: (d && d.max_goals) || 7,
166
+ needs: jobs.filter((j) => j.group === 'needs').length,
167
+ done: jobs.filter((j) => j.group === 'done').length,
168
+ };
169
+ }
170
+
171
+ const stepIndex = (stage) => ({ reading: 0, working: 2, review: 3, done: 4 }[stage] ?? -1);
172
+
173
+ /* ── small parts ──────────────────────────────────────────────────────────────────────────────────────────────── */
174
+
175
+ // a status word: the badge primitive. Health and attention read warning or destructive; everything else stays neutral.
176
+ const TONE = { '': 'muted', ok: 'outline', info: 'outline', goal: 'outline', warn: 'warning', purple: 'warning', bad: 'destructive' };
177
+ function pill(text, tone, attrs) {
178
+ return badge({ text, tone: TONE[tone || ''] || 'muted', attrs });
179
+ }
180
+ function statusPill(j) {
181
+ const s = D.status2;
182
+ const map = {
183
+ notStarted: [s.notStarted, ''], waiting: [s.waiting, ''], working: [s.working, 'info'], sending: [s.sending, 'info'],
184
+ review: [j.kind === 'auto' ? s.reviewAuto : s.review, 'purple'], sent: [s.sent, 'ok'], done: [s.done, 'ok'],
185
+ failed: [s.failed, 'bad'], you: [s.you, 'warn'],
186
+ };
187
+ const [t, tone] = map[j.status] || [j.status, ''];
188
+ return pill(t, tone, { 'data-day-status': j.status });
189
+ }
190
+ /** A link that opens outside the workspace, drawn as a small button. Only http and https addresses are drawn. */
191
+ function extLink(label, href, attrs) {
192
+ if (!isUrl(href)) return null;
193
+ return el('a', { class: 'td-day__a', href, target: '_blank', rel: 'noopener noreferrer', ...(attrs || {}) }, label);
194
+ }
195
+ function panel(name, count, ...kids) {
196
+ return el('section', { class: 'td-day__panel', 'aria-label': D.panels[name], 'data-day-panel': name },
197
+ el('h2', { class: 'td-day__h2' }, D.panels[name], count ? el('span', null, count) : null), ...kids);
198
+ }
199
+ function note(text, attrs) { return el('p', { class: 'td-day__note', ...(attrs || {}) }, text); }
200
+ function list(title, items, cls) {
201
+ if (!items || !items.length) return null;
202
+ return el('div', { class: 'td-day__phase' }, el('h4', null, title), el('ul', { class: 'td-day__chk' + (cls ? ' ' + cls : '') }, ...items.map((x) => el('li', null, x))));
203
+ }
204
+ function dl(pairs) {
205
+ const kids = [];
206
+ for (const [k, v] of pairs) if (v !== null && v !== undefined && v !== '') kids.push(el('dt', null, k), el('dd', null, v));
207
+ return kids.length ? el('dl', { class: 'td-day__ctx' }, ...kids) : null;
208
+ }
209
+
210
+ function greet(now) {
211
+ const h = new Date(now).getHours();
212
+ return h < 12 ? D.hello.morning : h < 18 ? D.hello.afternoon : D.hello.evening;
213
+ }
214
+ function dateLabel(now) {
215
+ try { return new Date(now).toLocaleDateString(undefined, { weekday: 'short', day: 'numeric', month: 'short' }); } catch { return ''; }
216
+ }
217
+
218
+ /* ── presses ──────────────────────────────────────────────────────────────────────────────────────────────────── */
219
+
220
+ async function prepareDay(ws) {
221
+ const cur = runs.get(ws);
222
+ if (cur && cur.state === 'busy') return;
223
+ runs.set(ws, { state: 'busy' });
224
+ rerender(); schedule();
225
+ const r = await pmPost('/api/proactive/goals/plan', { ws });
226
+ const sid = r && (r.session || (r.result && r.result.sessionId));
227
+ if (r && r.ok) {
228
+ runs.set(ws, { state: r.running ? 'running' : 'started', at: Date.now(), session: sid ? { id: sid, name: 'morning-brief' } : null });
229
+ ui.sel = { type: 'step', id: '0' };
230
+ toast({ text: r.running ? D.runRunning : COPY.goals.started });
231
+ } else {
232
+ const error = (r && r.error) || D.unreached;
233
+ runs.set(ws, { state: 'failed', at: Date.now(), error });
234
+ toast({ text: D.runFailed(error), tone: 'destructive' });
235
+ }
236
+ rerender();
237
+ load(ws);
238
+ }
239
+
240
+ async function openSession(session) {
241
+ try { const m = await import('./handoff-box.js'); await m.openSessionTab(null, session); } catch (e) { console.error('[pm] could not open the session:', e); }
242
+ }
243
+
244
+ async function checkNow(ws) {
245
+ ui.busy.add('check'); rerender();
246
+ const r = await pmPost('/api/proactive/day/check', { ws });
247
+ ui.busy.delete('check');
248
+ toast(r && r.ok ? { text: D.checkStarted } : { text: D.checkRefused((r && r.error) || D.unreached), tone: 'destructive' });
249
+ rerender();
250
+ load(ws);
251
+ }
252
+
253
+ async function decide(ws, task, action, note) {
254
+ const body = { task: task.id, action };
255
+ if (note !== undefined) body.note = note;
256
+ ui.busy.add(task.id); rerender();
257
+ const r = await pmPost('/api/proactive/day/decide', body);
258
+ ui.busy.delete(task.id);
259
+ if (r && r.ok) { toast({ text: D.decided[action] }); ui.revising.delete(task.id); ui.notes.delete(task.id); }
260
+ else toast({ text: D.refused((r && r.error) || D.unreached), tone: 'destructive' });
261
+ rerender();
262
+ load(ws);
263
+ }
264
+
265
+ async function setSettings(ws, patch) {
266
+ const r = await pmPost('/api/proactive/settings', patch);
267
+ if (!r || r.ok === false) { toast({ text: D.refused((r && r.error) || D.unreached), tone: 'destructive' }); return false; }
268
+ await load(ws);
269
+ return true;
270
+ }
271
+
272
+ function select(type, id) {
273
+ ui.sel = type ? { type, id: String(id) } : null;
274
+ if (type === 'job' && !seen.has(id)) { seen.add(id); store.set(SEEN_KEY, JSON.stringify([...seen].slice(-200))); }
275
+ ui.scroll = true;
276
+ rerender();
277
+ }
278
+
279
+ /* ── the top bar ──────────────────────────────────────────────────────────────────────────────────────────────── */
280
+
281
+ function topBar(ctx) {
282
+ const { m, ws } = ctx;
283
+ const si = stepIndex(m.stage);
284
+ const steps = el('ol', { class: 'td-day__steps', 'aria-label': D.prepare }, ...D.steps.map((t, i) => {
285
+ const st = si < 0 ? 'todo' : i < si || si === 4 ? 'done' : i === si ? 'now' : 'todo';
286
+ return el('li', null, button({ label: [el('span', { class: 'td-day__tsdot', 'aria-hidden': 'true' }), t], variant: 'ghost', size: 'sm', onClick: () => select('step', i),
287
+ attrs: { class: 'td-day__ts', 'data-step-state': st, 'data-day-step': String(i), 'data-day-sel': 'step:' + i, 'aria-current': ctx.cur('step', String(i)), title: D.stepHint[i] } }));
288
+ }));
289
+ const working = m.tasks.filter((t) => t.status === 'working' || t.status === 'sending').length;
290
+ const status = !m.run ? D.status.idle : m.stage === 'reading' ? D.status.reading : m.stage === 'working' ? D.status.working(working, m.needs)
291
+ : m.stage === 'review' ? D.status.review(m.needs, m.done) : D.status.done(m.done);
292
+ let check = null;
293
+ if (m.run && m.check) {
294
+ const every = m.settings && m.settings.dayCheck && typeof m.settings.dayCheck.every === 'number' ? m.settings.dayCheck.every : m.check.every;
295
+ const running = !!m.check.running || ui.busy.has('check');
296
+ check = every === 0 ? el('span', { class: 'td-day__check', 'data-day': 'check' }, D.checkOff)
297
+ : el('span', { class: 'td-day__check', 'data-day': 'check' }, el('span', null, running ? D.checking : D.checked(hhmm(m.check.last), hhmm(m.check.next))),
298
+ button({ label: D.checkNow, size: 'sm', disabled: running, attrs: { 'data-day': 'check-now' }, onClick: () => checkNow(ws) }));
299
+ }
300
+ const press = runs.get(ws);
301
+ const busy = !!press && press.state === 'busy';
302
+ const bs = m.run && m.run.brief_session;
303
+ const reading = !!bs && !bs.ended_at && m.stage === 'reading';
304
+ const label = busy ? D.starting : reading ? D.stillReading : m.run ? D.again : D.prepare;
305
+ return el('header', { class: 'td-day__bar' },
306
+ (() => { const when = [dateLabel(m.now), m.free !== null && m.free !== undefined ? D.free(m.free) : null].filter(Boolean).join(' · ');
307
+ return el('div', { class: 'td-day__hello', title: greet(m.now) + ' ' + when }, el('b', null, greet(m.now)), ' ', el('span', { class: 'td-day__dim' }, when)); })(),
308
+ steps,
309
+ el('span', { class: 'td-day__status', role: 'status', 'data-day': 'status', title: status }, status),
310
+ check,
311
+ button({ label: D.sources, size: 'sm', attrs: { 'data-day': 'sources', 'data-day-sel': 'sources:all', 'aria-current': ctx.cur('sources', 'all') }, onClick: () => select('sources', 'all') }),
312
+ button({ label, variant: 'primary', size: 'sm', disabled: busy || reading, attrs: { 'data-day': 'prepare', 'data-goals': 'prepare-day', title: D.prepareHint, 'aria-busy': busy ? 'true' : 'false' }, onClick: () => prepareDay(ws) }),
313
+ iconButton({ icon: 'chevron-down', ariaLabel: D.fold, size: 'sm', expanded: true, attrs: { 'data-day': 'toggle' }, onClick: () => fold(false) }));
314
+ }
315
+
316
+ function runLine(ctx) {
317
+ const r = runs.get(ctx.ws);
318
+ if (!r || r.state === 'busy') return null;
319
+ const text = r.state === 'failed' ? D.runFailed(r.error) : r.state === 'running' ? D.runRunning : D.runStarted(hhmm(r.at));
320
+ return el('p', { class: 'td-day__run' + (r.state === 'failed' ? ' td-day__run--failed' : ''), 'data-day': 'run', 'data-goals': 'run', 'data-run': r.state,
321
+ role: r.state === 'failed' ? 'alert' : 'status' }, text);
322
+ }
323
+
324
+ /* ── the left column ──────────────────────────────────────────────────────────────────────────────────────────── */
325
+
326
+ /** A selectable row of a list: the row primitive. `lead` and `trail` are nodes or text; the row carries data-day-sel. */
327
+ function rowBtn(ctx, type, id, { lead, title, meta, trail, dot }, mods) {
328
+ const cur = ctx.cur(type, String(id));
329
+ return el('li', null, row({
330
+ title, meta: meta ? [meta] : null, onOpen: () => select(type, id),
331
+ lead: [dot || null, lead !== undefined && lead !== null && lead !== '' ? el('span', { class: 'td-day__lead' }, lead) : null].filter(Boolean),
332
+ trail: [trail || null].filter(Boolean),
333
+ attrs: { class: 'td-day__r', 'data-day-sel': type + ':' + id, 'data-cur': cur, ...(mods || {}) },
334
+ openAttrs: { 'aria-current': cur },
335
+ }));
336
+ }
337
+
338
+ function eventChip(e) {
339
+ if (e.ended) return e.notes && (e.notes.wispr || e.notes.mail) ? pill(D.pill.notes, 'ok') : pill(D.pill.ended, '');
340
+ if (e.prep) return pill(D.pill.prepare, 'warn');
341
+ if (e.steps && e.steps.before && e.steps.before.length) return pill(D.pill.ready, '');
342
+ return null;
343
+ }
344
+
345
+ function calendar(ctx) {
346
+ const { m } = ctx;
347
+ if (!m.events.length) return panel('calendar', null, note(m.run && m.stage === 'reading' ? D.readingJobs : D.noCalendar));
348
+ const rows = [];
349
+ let nowShown = false;
350
+ const nowRow = () => el('li', { class: 'td-day__nowrow', 'data-day': 'now', 'aria-hidden': 'true' }, D.now(hhmm(m.now)));
351
+ for (const e of m.events) {
352
+ if (!nowShown && e.s > m.now) { rows.push(nowRow()); nowShown = true; }
353
+ const n = e.goal ? m.goalNo.get(e.goal) : null;
354
+ rows.push(rowBtn(ctx, 'event', e.id, { lead: e.time, title: e.title, meta: e.purpose || e.with || '',
355
+ trail: el('span', { class: 'td-day__chips' }, n ? pill(D.pill.goal(n), 'goal') : null, eventChip(e)) },
356
+ { 'data-past': e.ended ? 'true' : null, 'data-goal-tie': n ? 'true' : null }));
357
+ }
358
+ if (!nowShown) rows.push(nowRow());
359
+ return panel('calendar', null, el('ul', { class: 'td-day__rows', 'data-day-list': 'calendar' }, ...rows));
360
+ }
361
+
362
+ function jobRow(ctx, j) {
363
+ const n = j.goal ? ctx.m.goalNo.get(j.goal) : null;
364
+ const sub = j.status === 'failed' && j.task && j.task.error ? j.task.error
365
+ : [j.item && j.item.where, srcName(j.source)].filter(Boolean).join(' · ');
366
+ return rowBtn(ctx, 'job', j.id, { lead: n ? D.pill.goal(n) : '·', title: j.title, meta: sub, trail: statusPill(j),
367
+ dot: j.isNew && !seen.has(j.id) ? el('span', { class: 'td-day__newdot', 'data-day': 'new', role: 'img', 'aria-label': D.newItem }) : null });
368
+ }
369
+
370
+ function jobsPanel(ctx) {
371
+ const { m } = ctx;
372
+ if (!m.jobs.length) return panel('jobs', null, note(m.run && m.stage === 'reading' ? D.readingJobs : D.noJobs, { 'data-day': 'no-jobs' }));
373
+ const groups = ['needs', 'working', 'done', 'worth'].map((g) => [g, m.jobs.filter((j) => j.group === g)]).filter(([, l]) => l.length);
374
+ return panel('jobs', D.jobsCount(m.needs, m.done), ...groups.map(([g, l]) => el('div', { class: 'td-day__group', 'data-day-group': g },
375
+ el('div', { class: 'td-day__gh' }, D.groups[g], pill(String(l.length), '')),
376
+ el('ul', { class: 'td-day__rows' }, ...l.map((j) => jobRow(ctx, j))))));
377
+ }
378
+
379
+ function goalsPanel(ctx) {
380
+ const { m } = ctx;
381
+ return panel('goals', D.goalsCount(m.goals.filter((g) => g.status === 'open').length, m.maxGoals), goalRows(ctx), goalAdd(ctx));
382
+ }
383
+
384
+ function mentionStatus(j) {
385
+ if (j.group === 'done') return pill(D.status2.handled, 'ok');
386
+ if (j.status === 'you') return j.item && j.item.reply_needed ? pill(D.status2.replyNeeded, 'warn') : pill(D.status2.you, 'warn');
387
+ return statusPill(j);
388
+ }
389
+
390
+ function digestRow(ctx, g) {
391
+ const sub = D.digestLine(D.levelWord[g.level] || g.level, (g.decisions || []).length, (g.open || []).length);
392
+ return rowBtn(ctx, 'channel', g.id, { lead: el('span', { class: 'td-day__chip' }, srcName(g.source)), title: g.name, meta: sub });
393
+ }
394
+
395
+ function mentionsPanel(ctx) {
396
+ const { m } = ctx;
397
+ const svcs = [...new Set(m.mentions.map((j) => j.source))];
398
+ if (ui.mtab !== 'all' && !svcs.includes(ui.mtab)) ui.mtab = 'all';
399
+ const need = m.mentions.filter((j) => j.group === 'needs').length;
400
+ const mtabs = m.mentions.length ? tabs({ variant: 'line', role: 'tablist', ariaLabel: D.panels.mentions, value: ui.mtab, attrs: { 'data-day': 'mtabs' },
401
+ items: ['all', ...svcs].map((v) => ({ id: v, label: v === 'all' ? D.all : srcName(v), count: v === 'all' ? m.mentions.length : m.mentions.filter((j) => j.source === v).length })),
402
+ onChange: (v) => { ui.mtab = v; rerender(); } }) : null;
403
+ const shown = m.mentions.filter((j) => ui.mtab === 'all' || j.source === ui.mtab);
404
+ const rows = shown.length ? el('ul', { class: 'td-day__rows', 'data-day-list': 'mentions' }, ...shown.map((j) => {
405
+ const it = j.item;
406
+ return rowBtn(ctx, 'mention', j.id, { lead: el('span', { class: 'td-day__chip' }, srcName(j.source)),
407
+ title: it.about && it.ask ? `${it.about}: ${it.ask}` : it.ask || j.title, meta: D.fromWho(it.who, it.where) || it.why_now || '', trail: mentionStatus(j) });
408
+ })) : note(D.noMentions);
409
+ const live = m.digests.filter((g) => g.level !== 'ignore');
410
+ const ign = m.digests.filter((g) => g.level === 'ignore');
411
+ const dig = m.digests.length ? el('div', { class: 'td-day__group', 'data-day-group': 'watched' },
412
+ el('div', { class: 'td-day__gh' }, D.watched, pill(String(live.length), '')),
413
+ live.length ? el('ul', { class: 'td-day__rows', 'data-day-list': 'digests' }, ...live.map((g) => digestRow(ctx, g))) : null,
414
+ ign.length ? button({ label: ui.showIgnDigests ? D.hideIgnored : D.showIgnored(ign.length), variant: 'ghost', size: 'sm',
415
+ attrs: { 'data-day': 'show-ignored', 'aria-expanded': ui.showIgnDigests ? 'true' : 'false' }, onClick: () => { ui.showIgnDigests = !ui.showIgnDigests; rerender(); } }) : null,
416
+ ign.length && ui.showIgnDigests ? el('ul', { class: 'td-day__rows', 'data-day-list': 'ignored' }, ...ign.map((g) => digestRow(ctx, g))) : null) : null;
417
+ return panel('mentions', m.mentions.length ? D.mentionsCount(m.mentions.length, need) : null, mtabs, rows, dig);
418
+ }
419
+
420
+ /* ── the right column ─────────────────────────────────────────────────────────────────────────────────────────── */
421
+
422
+ function dhead(kind, title) {
423
+ return el('div', { class: 'td-day__dhead' }, el('span', { class: 'td-day__kind' }, kind), title ? el('h3', null, title) : null);
424
+ }
425
+ function relRow(ctx, type, id, label, lead, j) {
426
+ return rowBtn(ctx, type, id, { lead: lead || '', title: label, trail: j ? statusPill(j) : null });
427
+ }
428
+
429
+ function stepItem(st) {
430
+ const who = st && (st.who === 'pm' || st.who === 'agent') ? st.who : null;
431
+ return el('span', { class: 'td-day__stepline' + (st && st.done ? ' td-day__stepline--done' : who === 'pm' ? ' td-day__stepline--you' : '') },
432
+ String((st && st.t) || ''), who ? el('span', { class: 'td-day__who' }, D.who[who]) : null);
433
+ }
434
+
435
+ function eventDetail(ctx, e) {
436
+ const { m } = ctx;
437
+ const n = e.goal ? m.goalNo.get(e.goal) : null;
438
+ const g = n ? m.goals[n - 1] : null;
439
+ const ini = e.initiative && e.initiative.id ? e.initiative : null;
440
+ const steps = e.steps || {};
441
+ const notes = e.notes || {};
442
+ const w = notes.wispr || null;
443
+ const mail = notes.mail || null;
444
+ const mailBlock = mail ? el('div', { class: 'td-day__notes', 'data-day': 'mail' }, el('div', { class: 'td-day__src' }, D.mail, ' · ', mail.from || '', ' ', isUrl(mail.link) ? el('a', { href: mail.link, target: '_blank', rel: 'noopener noreferrer' }, D.openLink) : null),
445
+ el('p', null, el('b', null, (mail.subject || '') + '. '), mail.gist || '')) : null;
446
+ const after = e.ended ? el('div', { class: 'td-day__notes', 'data-day': 'notes' }, el('h4', null, D.notesTitle),
447
+ w ? [el('div', { class: 'td-day__src' }, D.wispr, ' ', isUrl(w.link) ? el('a', { href: w.link, target: '_blank', rel: 'noopener noreferrer' }, D.openLink) : null),
448
+ w.summary ? el('p', null, w.summary) : null,
449
+ list(D.decisions, (w.decisions || []).map(String), 'td-day__chk--ok'),
450
+ list(D.actions, (w.actions || []).map((a) => (typeof a === 'string' ? a : stepItem(a))))] : note(D.noWispr),
451
+ mailBlock || note(D.noMail)) : null;
452
+ const tie = g ? el('p', { class: 'td-day__goalline', 'data-day': 'tie' }, D.countsToward(n, g.text))
453
+ : [note(D.tieHow, { 'data-day': 'tie' }), m.goals.length < m.maxGoals ? el('div', { class: 'td-day__acts' }, button({ label: D.addAsGoal, size: 'sm', attrs: { 'data-day-act': 'add-goal' },
454
+ onClick: async () => { if (await addGoal(ctx, e.title, ini ? ini.id : null)) toast({ text: D.goalAdded }); } })) : null];
455
+ return [
456
+ dhead(D.kind.meeting, e.title),
457
+ el('p', { class: 'td-day__dim td-day__mono' }, `${e.time}${e.end ? '–' + hhmm(e.end) : ''}${e.with ? ' · ' + e.with : ''}${e.ended ? ' · ' + D.ended : ''}`),
458
+ e.purpose ? el('p', null, el('b', null, D.purpose + ': '), e.purpose) : null,
459
+ e.prep ? el('p', null, el('b', null, D.prep + ': '), e.prep) : null,
460
+ ini ? el('div', { class: 'td-day__ini', 'data-day': 'initiative' }, el('span', null, D.initiative + ': ', el('b', { class: 'td-day__mono' }, ini.id), ini.why ? el('span', { class: 'td-day__dim' }, ' (' + D.suggested(ini.why) + ')') : null),
461
+ ctx.openIni ? button({ label: D.openInitiative, size: 'sm', variant: 'ghost', attrs: { 'data-day-act': 'open-initiative' }, onClick: () => ctx.openIni(ini.id) }) : null) : null,
462
+ tie,
463
+ !e.ended ? el('div', { class: 'td-day__acts' }, e.prep ? pill(D.needPrep, 'warn') : steps.before && steps.before.length ? pill(D.nothingToPrep, 'ok') : null) : null,
464
+ after,
465
+ !e.ended ? [list(D.phases.before, (steps.before || []).map(stepItem)), list(D.phases.during, (steps.during || []).map(stepItem)), list(D.phases.after, (steps.after || []).map(stepItem)), mailBlock] : null,
466
+ Array.isArray(e.links) && e.links.length ? el('div', { class: 'td-day__acts', 'data-day': 'links' }, ...e.links.map((l) => extLink((l.label || l.url) + ' ↗', l.url))) : null,
467
+ ];
468
+ }
469
+
470
+ function jobCard(ctx, j) {
471
+ const { m, ws } = ctx;
472
+ const t = j.task;
473
+ const it = j.item || {};
474
+ const n = j.goal ? m.goalNo.get(j.goal) : null;
475
+ const busy = t && ui.busy.has(t.id);
476
+ const revs = t && Array.isArray(t.revisions) ? t.revisions.length : 0;
477
+ const statusNote = { notStarted: D.notTask, waiting: D.waitingNote, working: D.workingNote, sending: D.sendingNote, you: D.youNote }[j.status]
478
+ || (j.status === 'done' && j.kind === 'auto' ? D.autoNote : null);
479
+ const draft = t && t.draft ? el('div', { class: 'td-day__draftbox', 'data-day': 'draft' }, el('h4', null, D.draft),
480
+ t.draft.summary ? el('p', null, t.draft.summary) : null,
481
+ t.draft.body ? el('pre', { class: 'td-day__draft' }, t.draft.body) : null) : null;
482
+ const acts = [];
483
+ const canRevise = t && (j.status === 'review' || j.status === 'done') && revs < MAX_REVISIONS;
484
+ if (t && j.status === 'review' && j.kind === 'approve') acts.push(button({ label: D.approve, variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-day-act': 'approve' }, onClick: () => decide(ws, t, 'approve') }));
485
+ if (t && j.kind === 'auto' && (j.status === 'review' || (j.status === 'done' && !t.decided))) acts.push(button({ label: D.approveAuto, size: 'sm', disabled: busy, attrs: { 'data-day-act': 'approve' }, onClick: () => decide(ws, t, 'approve') }));
486
+ if (canRevise && !ui.revising.has(t.id)) acts.push(button({ label: D.revise, size: 'sm', disabled: busy, attrs: { 'data-day-act': 'revise' }, onClick: () => { ui.revising.add(t.id); rerender(); } }));
487
+ if (t && j.status === 'failed') acts.push(button({ label: D.retry, variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-day-act': 'retry' }, onClick: () => decide(ws, t, 'retry') }));
488
+ acts.push(extLink(D.openConversation, j.link, { 'data-day-link': 'conversation' }));
489
+ if (t && t.draft) acts.push(extLink(D.openDraft, t.draft.link, { 'data-day-link': 'draft' }));
490
+ if (t && t.sent) acts.push(extLink(D.openSent, t.sent.link, { 'data-day-link': 'sent' }));
491
+ if (t && t.session_id) acts.push(button({ label: D.openSession, size: 'sm', variant: 'ghost', attrs: { 'data-day-act': 'session' }, onClick: () => openSession({ id: t.session_id, name: 'day-task' }) }));
492
+ let form = null;
493
+ if (t && ui.revising.has(t.id) && canRevise) {
494
+ const f = field({ label: D.noteLabel, kind: 'textarea', value: ui.notes.get(t.id) || '', placeholder: D.noteHint, hideLabel: true, attrs: { maxlength: '500', 'data-day-input': 'note-' + t.id } });
495
+ const ta = f.control;
496
+ ta.addEventListener('input', () => { ui.notes.set(t.id, ta.value); });
497
+ form = el('div', { class: 'td-day__revise', 'data-day': 'revise' }, f, el('div', { class: 'td-day__acts' },
498
+ button({ label: D.sendNote, variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-day-act': 'send-note' }, onClick: () => {
499
+ const v = (ui.notes.get(t.id) || '').trim();
500
+ if (!v || v.length > 500) { toast({ text: D.noteNeeded, tone: 'destructive' }); return; }
501
+ decide(ws, t, 'revise', v);
502
+ } }),
503
+ button({ label: D.cancel, size: 'sm', variant: 'ghost', attrs: { 'data-day-act': 'cancel' }, onClick: () => { ui.revising.delete(t.id); rerender(); } })));
504
+ }
505
+ const reply = typeof it.reply_needed === 'boolean' ? (it.reply_needed ? D.yes : D.no) : null;
506
+ return el('article', { class: 'td-day__job', 'data-day-job': j.id },
507
+ el('div', { class: 'td-day__jobtop' }, el('span', { class: 'td-day__jobtitle' }, j.title), statusPill(j)),
508
+ el('span', { class: 'td-day__src' }, [srcName(j.source), it.where, n ? D.pill.goal(n) : null].filter(Boolean).join(' · ')),
509
+ dl([[D.where, D.fromWho(it.who, null)], [D.about, it.about || null], [D.why, it.why_now || null], [D.ask, it.ask || null], [D.reply, reply], [D.how, it.how || null]]),
510
+ statusNote ? note(statusNote) : null,
511
+ t && j.status === 'failed' && t.error ? el('p', { class: 'td-day__run td-day__run--failed', role: 'alert' }, D.failedWhy(t.error)) : null,
512
+ j.status === 'sent' && t && ((t.sent && t.sent.at) || (t.decided && t.decided.at)) ? note(D.sentAt(hhmm((t.sent && t.sent.at) || t.decided.at))) : null,
513
+ draft,
514
+ revs ? note(D.revisions(revs, MAX_REVISIONS) + (revs >= MAX_REVISIONS ? ' ' + D.noMoreRevisions : '')) : null,
515
+ el('div', { class: 'td-day__acts' }, ...acts.filter(Boolean)),
516
+ form);
517
+ }
518
+
519
+ function jobDetail(ctx, j) {
520
+ const { m } = ctx;
521
+ const n = j.goal ? m.goalNo.get(j.goal) : null;
522
+ const meets = m.events.filter((e) => e.goal && e.goal === j.goal);
523
+ const mention = j.item && j.item.mention;
524
+ return [
525
+ dhead(n ? D.kind.jobGoal(n) : D.kind.job, null),
526
+ jobCard(ctx, j),
527
+ meets.length || mention ? el('div', { class: 'td-day__rel' }, el('h4', null, D.related), el('ul', { class: 'td-day__rows' },
528
+ ...meets.map((e) => relRow(ctx, 'event', e.id, e.title, e.time)),
529
+ mention ? relRow(ctx, 'mention', j.id, j.item.ask || j.title, srcName(j.source)) : null)) : null,
530
+ ];
531
+ }
532
+
533
+ function mentionDetail(ctx, j) {
534
+ const it = j.item;
535
+ const reply = typeof it.reply_needed === 'boolean' ? (it.reply_needed ? D.yes : D.no) : null;
536
+ return [
537
+ dhead(D.kind.mention(srcName(j.source)), it.about || j.title),
538
+ dl([[D.where, D.fromWho(it.who, it.where)], [D.ask, it.ask || it.why_now || null], [D.reply, reply]]),
539
+ el('div', { class: 'td-day__acts' }, extLink(D.openIn(srcName(j.source)), j.link, { 'data-day-link': 'conversation' }), mentionStatus(j)),
540
+ el('div', { class: 'td-day__rel' }, el('h4', null, D.theJob), el('ul', { class: 'td-day__rows' }, relRow(ctx, 'job', j.id, j.title, '', j))),
541
+ ];
542
+ }
543
+
544
+ function levelSeg(ctx, svc, name) {
545
+ const cur = ctx.m.level(svc, name);
546
+ return tabs({ variant: 'segmented', role: 'radiogroup', ariaLabel: name, value: cur, attrs: { 'data-day-levels': svc + ':' + name },
547
+ items: ['watch', 'search', 'ignore'].map((l) => ({ id: l, label: D.levels[l] })), onChange: (l) => { if (l !== cur) setLevel(ctx, svc, name, l); } });
548
+ }
549
+ function setLevel(ctx, svc, name, lvl) {
550
+ const scopes = { ...(ctx.m.scopes || {}) };
551
+ scopes[svc] = { ...(scopes[svc] || {}), [name]: lvl };
552
+ return setSettings(ctx.ws, { scopes });
553
+ }
554
+
555
+ function channelDetail(ctx, id) {
556
+ const g = ctx.m.digests.find((x) => x.id === id);
557
+ if (!g) return null;
558
+ const sec = (t, l) => list(t, (l || []).map(String), 'td-day__chk--ok');
559
+ return [
560
+ dhead(D.kind.place(srcName(g.source), D.place[g.source] || '', D.levelWord[g.level] || g.level), g.name),
561
+ sec(D.decisions, g.decisions), sec(D.open, g.open), sec(D.forGoals, g.goals),
562
+ el('div', { class: 'td-day__ch' }, el('span', null, D.changeLevel), levelSeg(ctx, g.source, g.name)),
563
+ note(D.levelHelp),
564
+ el('div', { class: 'td-day__acts' }, extLink(D.openIn(srcName(g.source)), g.link, { 'data-day-link': 'place' }),
565
+ button({ label: D.manageSources, size: 'sm', variant: 'ghost', onClick: () => select('sources', 'all') })),
566
+ ];
567
+ }
568
+
569
+ function stepDetail(ctx, i) {
570
+ const { m } = ctx;
571
+ const si = stepIndex(m.stage);
572
+ const head = dhead(D.stepOf(i + 1), D.stepTitle[i]);
573
+ if (!m.run) return [head, note(D.readNone)];
574
+ if (i > si && si !== 4) return [head, note(D.notYet)];
575
+ const jobRows = (l) => (l.length ? el('ul', { class: 'td-day__rows' }, ...l.map((j) => relRow(ctx, 'job', j.id, j.title, '', j))) : null);
576
+ if (i === 0) {
577
+ const bs = m.run.brief_session;
578
+ const items = m.brief ? (m.brief.priorities || []).length + (m.brief.worth_asking || []).length : 0;
579
+ return [head, el('p', null, m.brief ? D.readDone(m.events.length, items) : D.readNow),
580
+ el('ul', { class: 'td-day__chk' }, ...m.sources.map((s) => el('li', { class: s.on && s.state === 'read' ? 'td-day__ok' : '' }, el('span', { class: s.on ? '' : 'td-day__dim' }, `${srcName(s.k)}: ${srcLine(s)}`)))),
581
+ el('div', { class: 'td-day__acts' }, button({ label: D.manageSources, size: 'sm', onClick: () => select('sources', 'all') }),
582
+ bs && bs.id ? button({ label: D.openSession, size: 'sm', variant: 'ghost', attrs: { 'data-day-act': 'session' }, onClick: () => openSession({ id: bs.id, name: 'morning-brief' }) }) : null)];
583
+ }
584
+ if (i === 1) {
585
+ const live = m.jobs.filter((j) => !j.worth);
586
+ return [head, el('p', null, D.madeJobs(live.length, live.filter((j) => j.kind === 'you').length, live.filter((j) => j.kind !== 'you').length)),
587
+ m.goals.length ? el('ul', { class: 'td-day__rows' }, ...m.goals.map((g, k) => relRow(ctx, 'goal', g.id, g.text, String(k + 1)))) : null];
588
+ }
589
+ if (i === 2) {
590
+ const ts = m.jobs.filter((j) => j.task);
591
+ const c = (s) => m.tasks.filter((t) => s.includes(t.status)).length;
592
+ return [head, el('p', null, D.agentsLine(c(['working', 'sending']), c(['waiting']), c(['done', 'sent']))), jobRows(ts)];
593
+ }
594
+ const need = m.jobs.filter((j) => j.group === 'needs');
595
+ return [head, el('p', null, D.approveLine(need.length)), jobRows(need)];
596
+ }
597
+
598
+ function srcLine(s) {
599
+ if (!s.on) return D.srcState.off;
600
+ if (s.state === 'read') return typeof s.got === 'number' ? D.gave(s.got) : D.srcState.read;
601
+ return D.srcState[s.state] || D.srcState.none;
602
+ }
603
+
604
+ function placeControls(ctx, svc) {
605
+ const { m } = ctx;
606
+ const names = [...new Set([...Object.keys(m.scopes[svc] || {}), ...m.digests.filter((g) => g.source === svc).map((g) => g.name)])];
607
+ const row = (name) => el('div', { class: 'td-day__ch', 'data-day-place': svc + ':' + name }, el('span', null, name), levelSeg(ctx, svc, name));
608
+ const live = names.filter((x) => m.level(svc, x) !== 'ignore');
609
+ const ign = names.filter((x) => m.level(svc, x) === 'ignore');
610
+ const input = field({ label: D.placeAdd[svc], value: '', placeholder: D.placeHint, hideLabel: true, attrs: { maxlength: '120', 'data-day-input': 'place-' + svc },
611
+ onCommit: async (raw) => {
612
+ const v = String(raw || '').trim();
613
+ if (!v) return;
614
+ ui.placeDraft[svc] = '';
615
+ if (!(await setLevel(ctx, svc, v, 'watch'))) ui.placeDraft[svc] = v;
616
+ } });
617
+ // made empty and filled after, so Enter still commits text a rebuild put back
618
+ input.control.value = ui.placeDraft[svc] || '';
619
+ input.control.addEventListener('input', () => { ui.placeDraft[svc] = input.control.value; });
620
+ return [
621
+ note(D.readNote[svc]),
622
+ ...live.map(row),
623
+ ign.length ? button({ label: ui.showIgn[svc] ? D.hideIgnored : D.showIgnored(ign.length), variant: 'ghost', size: 'sm',
624
+ attrs: { 'data-day-ignored': svc, 'aria-expanded': ui.showIgn[svc] ? 'true' : 'false' }, onClick: () => { ui.showIgn[svc] = !ui.showIgn[svc]; rerender(); } }) : null,
625
+ ui.showIgn[svc] ? ign.map(row) : null,
626
+ input,
627
+ note(D.levelHelp),
628
+ ];
629
+ }
630
+
631
+ function siteControls(ctx) {
632
+ const sites = (ctx.m.settings && Array.isArray(ctx.m.settings.websites)) ? ctx.m.settings.websites : [];
633
+ const input = field({ label: D.siteAdd, value: '', placeholder: D.siteHint, hideLabel: true, attrs: { maxlength: '500', 'data-day-input': 'site' },
634
+ onCommit: async (raw) => {
635
+ const v = String(raw || '').trim();
636
+ if (!v) return;
637
+ if (!isUrl(v)) { toast({ text: D.siteBad, tone: 'destructive' }); return; }
638
+ if (sites.length >= 10) { toast({ text: D.siteFull, tone: 'destructive' }); return; }
639
+ ui.siteDraft = '';
640
+ if (!(await setSettings(ctx.ws, { websites: [...sites.filter((x) => x !== v), v] }))) ui.siteDraft = v;
641
+ } });
642
+ input.control.value = ui.siteDraft;
643
+ input.control.addEventListener('input', () => { ui.siteDraft = input.control.value; });
644
+ return [
645
+ el('h4', null, D.sites),
646
+ sites.length ? el('ul', { class: 'td-day__sites', 'data-day': 'sites' }, ...sites.map((u) => el('li', null, el('span', { class: 'td-day__mono' }, u),
647
+ button({ label: D.remove, size: 'sm', variant: 'ghost', attrs: { 'data-day-site-remove': u }, onClick: () => setSettings(ctx.ws, { websites: sites.filter((x) => x !== u) }) })))) : null,
648
+ sites.length < 10 ? input : note(D.siteFull),
649
+ ];
650
+ }
651
+
652
+ function sourcesDetail(ctx) {
653
+ const { m, ws } = ctx;
654
+ if (!m.settings) return [dhead(D.kind.settings, D.sources), note(m.d && m.d.legacy ? D.oldServer : D.noSources)];
655
+ const dc = m.settings.dayCheck || { every: 60, start: '09:00', end: '18:00' };
656
+ const log = m.check && Array.isArray(m.check.log) ? m.check.log.slice(-4).reverse() : [];
657
+ const box = el('div', { class: 'td-day__notes', 'data-day': 'day-check' }, el('h4', null, D.dayCheck),
658
+ el('div', { class: 'td-day__ch' }, el('span', null, D.every), tabs({ variant: 'segmented', role: 'radiogroup', ariaLabel: D.every, value: String(dc.every), attrs: { 'data-day': 'every' },
659
+ items: D.everyOpts.map(([v, l]) => ({ id: String(v), label: l })), onChange: (v) => { if (Number(v) !== dc.every) setSettings(ws, { dayCheck: { ...dc, every: Number(v) } }); } })),
660
+ el('div', { class: 'td-day__ch' }, el('span', null, D.hours), el('span', { class: 'td-day__mono' }, `${dc.start}–${dc.end}`)),
661
+ el('div', { class: 'td-day__ch' }, el('span', null, D.beforeMeeting), el('span', { class: 'td-day__dim' }, D.beforeMeetingWhat)),
662
+ el('div', { class: 'td-day__ch' }, el('span', null, D.afterMeeting), el('span', { class: 'td-day__dim' }, D.afterMeetingWhat)),
663
+ note(D.checkHelp),
664
+ log.length ? el('div', { class: 'td-day__log', 'data-day': 'check-log' }, ...log.map((x) => el('div', null, x.found === null || x.found === undefined ? D.checkRunning(hhmm(x.at)) : D.checkLog(hhmm(x.at), x.found || 0, x.resolved || 0)))) : null);
665
+ const rows = m.sources.map((s) => {
666
+ const name = srcName(s.k);
667
+ const open = ui.openSrc === s.k;
668
+ const more = !open ? null : el('div', { class: 'td-day__more' }, note(D.srcWhat[s.k] || ''),
669
+ SCOPED.includes(s.k) ? placeControls(ctx, s.k) : null,
670
+ s.k === 'website' ? siteControls(ctx) : null);
671
+ return el('div', { class: 'td-day__sr', 'data-day-source': s.k },
672
+ el('span', { class: 'td-day__ic', 'aria-hidden': 'true' }, name.slice(0, 2)),
673
+ button({ label: name, variant: 'ghost', size: 'sm', iconEnd: open ? 'chevron-down' : 'chevron-right', attrs: { 'aria-expanded': open ? 'true' : 'false', 'data-day-osrc': s.k }, onClick: () => { ui.openSrc = open ? null : s.k; rerender(); } }),
674
+ el('span', { class: 'td-day__st' }, srcLine(s)),
675
+ field({ kind: 'check', label: D.read, value: !!s.on, attrs: { 'aria-label': D.readSwitch(name), 'data-day-src': s.k },
676
+ onCommit: (v) => setSettings(ws, { briefSources: { [s.k]: !!v } }) }),
677
+ more);
678
+ });
679
+ return [dhead(D.kind.settings, D.sources), note(D.sourcesIntro), box, el('div', { class: 'td-day__slist' }, ...rows)];
680
+ }
681
+
682
+ function defaultSel(m) {
683
+ const j = m.jobs.find((x) => x.group === 'needs');
684
+ if (j) return { type: 'job', id: j.id };
685
+ const e = m.events.find((x) => !x.ended);
686
+ if (e) return { type: 'event', id: e.id };
687
+ return { type: 'step', id: String(Math.max(0, Math.min(3, stepIndex(m.stage)))) };
688
+ }
689
+
690
+ function detail(ctx) {
691
+ const { m } = ctx;
692
+ let sel = ctx.sel;
693
+ let kids = null;
694
+ const find = (s) => {
695
+ if (s.type === 'event') { const e = m.events.find((x) => x.id === s.id); return e ? eventDetail(ctx, e) : null; }
696
+ if (s.type === 'job') { const j = m.jobs.find((x) => x.id === s.id); return j ? jobDetail(ctx, j) : null; }
697
+ if (s.type === 'mention') { const j = m.mentions.find((x) => x.id === s.id); return j ? mentionDetail(ctx, j) : null; }
698
+ if (s.type === 'goal') return goalDetail(ctx, s.id, (type, id, label, lead, j) => relRow(ctx, type, id, label, lead, j));
699
+ if (s.type === 'channel') return channelDetail(ctx, s.id);
700
+ if (s.type === 'step') return stepDetail(ctx, Math.max(0, Math.min(3, Number(s.id) || 0)));
701
+ if (s.type === 'sources') return sourcesDetail(ctx);
702
+ return null;
703
+ };
704
+ kids = find(sel);
705
+ if (!kids) { sel = defaultSel(m); kids = find(sel); }
706
+ return el('aside', { class: 'td-day__detail', 'aria-label': D.panels.detail, 'data-day': 'detail', 'data-day-detail': sel.type, 'data-day-detail-id': sel.id }, kids);
707
+ }
708
+
709
+ /* ── the section ──────────────────────────────────────────────────────────────────────────────────────────────── */
710
+
711
+ function fold(open) {
712
+ ui.open = open;
713
+ store.set(OPEN_KEY, open ? 'true' : 'false');
714
+ rerender();
715
+ }
716
+
717
+ /** The section. `ws` is the repository the day run belongs to; `openIni(id)` opens an initiative of it. */
718
+ export function daySection({ ws, setState, openIni } = {}) {
719
+ if (FIXTURE) return null;
720
+ // like Today's own feeds, a read that lands re-renders only while the section is on the page: a hidden Today must not
721
+ // cause a whole-page render under whatever the PM is doing elsewhere
722
+ if (setState) rerender = () => { if (document.querySelector('[data-today="day"]')) setState({}); };
723
+ ensure(ws);
724
+ const c = cache.get(ws) || {};
725
+ const m = model(c.data);
726
+ const open = ui.open !== null ? ui.open : !!m.run;
727
+ if (!open) {
728
+ return el('section', { class: 'td-day', 'data-today': 'day', 'data-open': 'false', 'aria-label': D.title },
729
+ button({ label: D.toggle(m.needs), variant: 'ghost', iconEnd: 'chevron-right', attrs: { 'data-day': 'toggle', 'aria-expanded': 'false' }, onClick: () => fold(true) }));
730
+ }
731
+ const sel = ui.sel;
732
+ const ctx = {
733
+ ws, m, sel, openIni, reload: () => load(ws), rerender: () => rerender(), select,
734
+ cur: (type, id) => { const s = sel || defaultSel(m); return s.type === type && s.id === String(id) ? 'true' : 'false'; },
735
+ };
736
+ ctx.sel = sel || defaultSel(m);
737
+ if (ui.scroll) {
738
+ ui.scroll = false;
739
+ // on a narrow screen the detail is under the list: bring it into view once it is on the page
740
+ requestAnimationFrame(() => requestAnimationFrame(() => {
741
+ const box = document.querySelector('[data-today="day"] .td-day__cols');
742
+ const d = document.querySelector('[data-today="day"] [data-day="detail"]');
743
+ if (box && d && box.clientWidth < NARROW) d.scrollIntoView({ behavior: 'smooth', block: 'start' });
744
+ }));
745
+ }
746
+ return el('section', { class: 'td-day', 'data-today': 'day', 'data-open': 'true', 'aria-label': D.title },
747
+ topBar(ctx),
748
+ runLine(ctx),
749
+ c.error ? el('p', { class: 'td-day__run td-day__run--failed', role: 'status', 'data-day': 'error' }, D.refused(c.error)) : null,
750
+ m.d && m.d.legacy ? note(D.oldServer) : null,
751
+ el('div', { class: 'td-day__cols' },
752
+ el('div', { class: 'td-day__left' }, calendar(ctx), jobsPanel(ctx), goalsPanel(ctx), mentionsPanel(ctx)),
753
+ detail(ctx)));
754
+ }
755
+
756
+ export function daySig(ws) {
757
+ const c = cache.get(ws) || {};
758
+ return JSON.stringify([ui.open, ui.sel, ui.mtab, ui.openSrc, ui.showIgn, ui.showIgnDigests, [...ui.revising], [...ui.busy], c.error, runs.get(ws) || null, [...seen].length])
759
+ + dataSig(c.data) + goalsSig();
760
+ }