create-pmos 0.210.0 → 0.212.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/state/discoveries.md +1 -1
- package/workspace/c3-light-load-test.js +110 -0
- package/workspace/c3-settings-test.js +53 -2
- package/workspace/guide/briefing.md +7 -1
- package/workspace/guide/data-freshness.md +35 -8
- package/workspace/guide/index.md +1 -1
- package/workspace/lib/driver-run.js +4 -3
- package/workspace/lib/freshness.js +2 -0
- package/workspace/lib/refresh-run.js +300 -0
- package/workspace/lib/refresh.js +464 -0
- package/workspace/lib/report-finder.js +22 -1
- package/workspace/lib/route-modules.js +1 -0
- package/workspace/lib/routes-calendar-views.js +10 -2
- package/workspace/lib/routes-pm-items.js +2 -0
- package/workspace/lib/routes-refresh.js +93 -0
- package/workspace/lib/settings-store.js +0 -2
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/c3-brief-pulse.js +4 -4
- package/workspace/public/pm/c3-calpop.js +11 -3
- package/workspace/public/pm/c3-dock.js +9 -0
- package/workspace/public/pm/c3-driver.js +1 -1
- package/workspace/public/pm/c3-look.js +27 -0
- package/workspace/public/pm/c3-plan.js +3 -2
- package/workspace/public/pm/c3-quiet.css +6 -1
- package/workspace/public/pm/c3-report-store.js +2 -1
- package/workspace/public/pm/c3-scan-setup.js +3 -7
- package/workspace/public/pm/c3-scan-words.js +7 -0
- package/workspace/public/pm/c3-scan.js +6 -4
- package/workspace/public/pm/c3-settings.css +11 -0
- package/workspace/public/pm/c3-settings.js +236 -27
- package/workspace/public/pm/c3-shell.js +56 -100
- package/workspace/replaced-tests.json +11 -0
- package/workspace/server.js +106 -12
- package/workspace/test-lists/c3-desktop-light-load.json +6 -0
- package/workspace/test-lists/data-refresh-settings.json +5 -0
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* about you GET and POST /api/proactive/settings {name, about:{role, work, duties}} (the Welcome page writes the same)
|
|
17
17
|
* running sessions GET /api/sessions calendar import POST /api/sources/refresh {source:'calendar'}
|
|
18
18
|
* versions GET /api/kit/status · POST /api/kit/check, /api/kit/update, /api/kit/upgrade-app (then GET /api/sessions until the new version)
|
|
19
|
+
* data refresh GET /api/refresh?ws · POST /api/refresh/source, /rules, /run, /run-all · GET /api/refresh/log, /log.csv
|
|
19
20
|
* Every other setting is in the settings store: GET /api/settings and POST /api/settings (lib/settings-store.js).
|
|
20
21
|
* A board's own workflow is edited on the board; here are only the templates new boards start from.
|
|
21
22
|
* Nothing here asks for or shows a key or a password. */
|
|
@@ -27,6 +28,9 @@ import { sessionDefaults, setSessionDefaults } from './dock.js';
|
|
|
27
28
|
import { toast } from './c3-toast.js';
|
|
28
29
|
import { dayPartOf } from './c3-daypart.js';
|
|
29
30
|
import { finderBlock } from './c3-report-store.js';
|
|
31
|
+
import { gearIcon, applyLook } from './c3-look.js';
|
|
32
|
+
|
|
33
|
+
export { gearIcon, applyLook };
|
|
30
34
|
|
|
31
35
|
const cx = (...c) => c.filter(Boolean).join(' ');
|
|
32
36
|
|
|
@@ -35,6 +39,7 @@ export const SECTIONS = Object.freeze([
|
|
|
35
39
|
['workspaces', 'Workspaces', 'repo', 'A workspace is one repository PMOS works in. Settings marked Workspace apply to the one picked at the top of this page.'],
|
|
36
40
|
['boards', 'Boards and workflows', 'cols', 'The templates and defaults new boards start from. A board\'s own workflow is edited on the board, in Initiatives.'],
|
|
37
41
|
['connectors', 'Connectors', 'org', 'PMOS keeps no passwords or keys for these. Your Claude session reaches each tool through the connectors of its profile.'],
|
|
42
|
+
['refresh', 'Data refresh', 'undo', 'What PMOS reads and writes on a schedule: pick what refreshes, when and how often, and see when each one last ran.'],
|
|
38
43
|
['recording', 'Recording', 'mic', 'How meetings are transcribed. This is the one place to check when a transcript looks wrong.'],
|
|
39
44
|
['time', 'Day and time', 'sun', 'The Briefing follows your clock. These settings say what your day looks like.'],
|
|
40
45
|
['sharing', 'Sharing', 'mail', 'Who reports go to, and how. Each report\'s schedule is set in Reports, under Set up.'],
|
|
@@ -52,9 +57,7 @@ export const LABELS = Object.freeze({
|
|
|
52
57
|
'board.template': ['Defaults', 'Template for new boards', 'A new board copies this template. You can change its workflow on the board afterwards.', { product: 'Product', project: 'Project', simple: 'Simple' }],
|
|
53
58
|
'board.rules': ['Defaults', 'Order rules on boards', 'You move cards between stages. When a card skips a step, such as Build before a spec exists, the card shows a warning (decision D3).', { warn: 'Warn on the card', off: 'Off' }],
|
|
54
59
|
'conn.cal.window': ['Google Calendar', 'Import window', 'How far back and ahead your session imports events.', { '7-14': 'Past 7 days, next 14', '14-30': 'Past 14 days, next 30', '30-60': 'Past 30 days, next 60' }],
|
|
55
|
-
'conn.cal.refresh': ['Google Calendar', 'Refresh', 'When your session imports events again.', { open: 'When I open Calendar', hour: 'Every hour', manual: 'Only when I import' }],
|
|
56
60
|
'conn.cal.private': ['Google Calendar', 'Private events', 'Events marked private in Google Calendar.', { busy: 'Show as busy', hide: 'Hide', title: 'Show titles' }],
|
|
57
|
-
'conn.jira.refresh': ['Jira', 'Refresh ticket status', 'Jira owns ticket status. PMOS reads it back on this schedule.', { open: 'When I open a board', 15: 'Every 15 min', manual: 'Only when I sync' }],
|
|
58
61
|
'conn.wispr.wait': ['Wispr Flow', 'Wait before fetching', 'Wispr Flow needs a few minutes to finish its notes after a meeting.', { 2: '2 minutes', 5: '5 minutes', 10: '10 minutes' }],
|
|
59
62
|
'conn.sender': ['Sending', 'Sender for shared dashboards', 'Your session sends through this connector. Until you pick one, a scheduled share is a ready draft.', { none: 'None', slack: 'Slack', gmail: 'Gmail drafts' }],
|
|
60
63
|
'rec.sources': ['Live transcript', 'Listen to', 'The meeting is the sound from your call app. The mic is you.', { both: 'Mic and meeting', mic: 'Mic only', meeting: 'Meeting only' }],
|
|
@@ -123,15 +126,6 @@ export function rowsMatching(q) {
|
|
|
123
126
|
const STORE_SECTION = { ws: 'workspaces', board: 'boards', conn: 'connectors', rec: 'recording', time: 'time', share: 'sharing', sess: 'sessions', look: 'appearance', upd: 'updates' };
|
|
124
127
|
function sectionOfStore(k) { return STORE_SECTION[k.split('.')[0]] || 'workspaces'; }
|
|
125
128
|
|
|
126
|
-
/** A gear, drawn here: the console's sprite has none. */
|
|
127
|
-
export function gearIcon(cls) {
|
|
128
|
-
const NS = 'http://www.w3.org/2000/svg';
|
|
129
|
-
const s = document.createElementNS(NS, 'svg');
|
|
130
|
-
s.setAttribute('class', 'ic' + (cls ? ' ' + cls : '')); s.setAttribute('aria-hidden', 'true'); s.setAttribute('viewBox', '0 0 24 24');
|
|
131
|
-
s.innerHTML = '<circle cx="12" cy="12" r="3"/><path d="M19 12a7 7 0 0 0-.1-1.2l2-1.6-2-3.4-2.4 1a7 7 0 0 0-2-1.2L14 3h-4l-.5 2.6a7 7 0 0 0-2 1.2l-2.4-1-2 3.4 2 1.6a7 7 0 0 0 0 2.4l-2 1.6 2 3.4 2.4-1a7 7 0 0 0 2 1.2L10 21h4l.5-2.6a7 7 0 0 0 2-1.2l2.4 1 2-3.4-2-1.6c.1-.4.1-.8.1-1.2"/>';
|
|
132
|
-
return s;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
129
|
function ensureCss() {
|
|
136
130
|
if (typeof document === 'undefined' || document.querySelector('link[data-c3-settings]')) return;
|
|
137
131
|
const l = document.createElement('link'); l.rel = 'stylesheet'; l.href = new URL('./c3-settings.css', import.meta.url).href; l.setAttribute('data-c3-settings', ''); document.head.appendChild(l);
|
|
@@ -146,24 +140,16 @@ const bt = (attrs, ...kids) => {
|
|
|
146
140
|
|
|
147
141
|
const ZONES = ['Asia/Jakarta', 'Asia/Makassar', 'Asia/Jayapura', 'Asia/Singapore', 'Asia/Kuala_Lumpur', 'Asia/Bangkok', 'Asia/Tokyo', 'Asia/Kolkata', 'Australia/Sydney', 'Europe/London', 'Europe/Berlin', 'America/New_York', 'America/Los_Angeles', 'UTC'];
|
|
148
142
|
/* Choices the mockup draws as a select even though they are short. */
|
|
149
|
-
const AS_SELECT = new Set(['conn.cal.window', 'conn.
|
|
143
|
+
const AS_SELECT = new Set(['conn.cal.window', 'conn.wispr.wait']);
|
|
150
144
|
const qs = (o) => Object.entries(o).filter(([, v]) => v != null && v !== '').map(([k, v]) => k + '=' + encodeURIComponent(v)).join('&');
|
|
151
145
|
|
|
152
|
-
/** Apply the settings the console itself draws: the start tab is read by the shell; tint and the daylight strip are attributes on the root. */
|
|
153
|
-
export function applyLook(root, settings) {
|
|
154
|
-
if (!root || !settings) return;
|
|
155
|
-
ensureCss();
|
|
156
|
-
const v = (k) => { const s = settings.find((x) => x.key === k); return s ? s.value : undefined; };
|
|
157
|
-
root.setAttribute('data-c3-tint', v('time.tint') === false ? 'off' : 'on');
|
|
158
|
-
root.setAttribute('data-c3-strip', v('time.strip') === false ? 'off' : 'on');
|
|
159
|
-
}
|
|
160
|
-
|
|
161
146
|
export function mountSettings(host, ctx = {}) {
|
|
162
147
|
ensureCss();
|
|
163
148
|
const M = {
|
|
164
149
|
sec: SECTIONS.some((s) => s[0] === ctx.section) ? ctx.section : 'about', q: '', ws: ctx.ws || '', data: null, err: {},
|
|
165
150
|
engine: null, people: null, consent: null, online: null, profiles: null, typesafe: null, kits: {}, kitBusy: null, app: { phase: null, error: '' }, about: null, running: null,
|
|
166
151
|
tplEdit: null, wsEdit: null, wsDrop: null, addPath: '',
|
|
152
|
+
rf: null, rfLog: null, rfSig: '', rfFilter: 'all', rfEdit: null, rfAsk: null, rfErr: {}, rfAllBusy: false, rfLoading: false, logSource: '', logResult: '',
|
|
167
153
|
};
|
|
168
154
|
const root = el('div', { class: 'brief c3set', 'data-c3set': '' });
|
|
169
155
|
const nav = el('ul', { class: 'list', role: 'list', 'data-c3set-nav': '' });
|
|
@@ -196,7 +182,7 @@ export function mountSettings(host, ctx = {}) {
|
|
|
196
182
|
}
|
|
197
183
|
async function loadEngine() { const r = await pmGet('/api/notes/engine?' + qs({ ws: M.ws })); M.engine = r && r.ok ? r : { error: (r && r.error) || 'not read' }; draw(); }
|
|
198
184
|
async function loadWs() {
|
|
199
|
-
M.people = null; M.consent = null; draw();
|
|
185
|
+
M.people = null; M.consent = null; M.rf = null; M.rfLog = null; M.rfEdit = null; M.rfAsk = null; draw();
|
|
200
186
|
await Promise.all([loadStore(), loadEngine(),
|
|
201
187
|
M.ws ? pmGet('/api/people?' + qs({ ws: M.ws })).then((r) => { M.people = r && r.ok !== false && r.settings ? { on: !!r.settings.calendar_names } : { error: (r && r.error) || 'not read' }; }) : null,
|
|
202
188
|
M.ws ? pmGet('/api/validate/repo?' + qs({ ws: M.ws })).then((r) => { M.consent = r && r.ok !== false ? { on: !!r.consent } : { error: (r && r.error) || 'not read' }; }) : null]);
|
|
@@ -278,7 +264,7 @@ export function mountSettings(host, ctx = {}) {
|
|
|
278
264
|
el('div', { class: 'nb' },
|
|
279
265
|
forId ? el('label', { class: 'tt', for: forId }, label) : el('span', { class: 'tt' }, label),
|
|
280
266
|
scope || changed ? el('div', { class: 'chips' }, scope ? scopeTag(scope) : null, changed ? el('span', { class: 'st-dot', title: 'Changed from the default' }) : null) : null,
|
|
281
|
-
h ? help(h) : null,
|
|
267
|
+
h ? help(...[].concat(h)) : null,
|
|
282
268
|
env ? el('div', { class: 'fold w', 'data-c3set-env': env.env }, c3ic('alert', 'sm'), el('span', null, el('b', { class: 'mono' }, env.env + '=' + env.value), ' is set where PMOS starts. ' + env.effect + ' Remove it from your shell profile, then restart PMOS.')) : null,
|
|
283
269
|
lock ? fold('lock', lock) : null,
|
|
284
270
|
note ? help(note) : null,
|
|
@@ -418,6 +404,7 @@ export function mountSettings(host, ctx = {}) {
|
|
|
418
404
|
const profs = (M.profiles && M.profiles.profiles) || [];
|
|
419
405
|
return select(id, 'Default profile', [['', 'None'], ...profs.map((x) => [x.id, x.label && x.label !== x.id ? x.label + ' (' + x.id + ')' : x.id])], profileNow().id || '', chooseProfile);
|
|
420
406
|
};
|
|
407
|
+
const refreshLink = (key, label, h) => frame({ key, label, help: h, control: secLink('refresh', 'Data refresh') });
|
|
421
408
|
function connectorsSec() {
|
|
422
409
|
const { id, x } = profileNow();
|
|
423
410
|
const mcp = x ? x.mcp : [];
|
|
@@ -438,13 +425,13 @@ export function mountSettings(host, ctx = {}) {
|
|
|
438
425
|
help('Profiles are listed in ', secLink('sessions', 'Claude sessions'), '.')], { ic: 'user', attrs: { 'data-c3set-profile': '' } }),
|
|
439
426
|
sect('Google Calendar', [list([
|
|
440
427
|
notYet('conn.cal.list', 'Calendars to import', 'Pick which of your calendars the import reads.', 'Choosing calendars comes later. Today the import reads the calendars your session can see.', 'computer'),
|
|
441
|
-
storeRow('conn.cal.window'),
|
|
428
|
+
storeRow('conn.cal.window'), refreshLink('conn.cal.refresh', 'Refresh schedule', 'When the calendar is read again is set in Data refresh.'), storeRow('conn.cal.private'),
|
|
442
429
|
M.ws ? extRow('people.names', people && !people.error ? swBtn(people.on, EXT['people.names'][2], async () => { const r = await pmPost('/api/people/decide', { ws: M.ws, act: 'setting', calendar_names: !people.on }); if (r && r.ok !== false) { people.on = !people.on; toast('Saved: attendee names'); } else M.err['people.names'] = (r && r.error) || 'Not saved.'; draw(); }) : el('span', { class: 'dim' }, people ? people.error : 'Reading'), { scope: 'ws' }) : null]),
|
|
443
430
|
el('div', { class: 'iacts' }, btn('Import now', { ic: 'undo', disabled: !M.ws || !has(/calendar/i), onclick: async () => { const r = await pmPost('/api/sources/refresh', { ws: M.ws, source: 'calendar' }); toast(r && r.ok ? 'Import started in your Claude session. Events appear in Calendar when it finishes.' : ((r && r.error) || 'The import did not start.')); } }),
|
|
444
431
|
el('span', { class: 'dim st-help' }, via(has(/calendar/i), 'calendar') + ' PMOS never edits your calendar.'))], { ic: 'cal', right: reach(has(/calendar/i)) }),
|
|
445
432
|
sect('Jira', [list([notYet('jira.map', 'Jira project for each board', 'A board backed by a Jira project mirrors its sprints and reads ticket status from it, with each Jira status mapped to a board stage.', 'Mapping boards and statuses comes with the Initiatives update.', 'board')]),
|
|
446
433
|
help(via(has(/atlassian|jira/i), 'Jira'))], { ic: 'org', right: reach(has(/atlassian|jira/i)) }),
|
|
447
|
-
group('Jira behaviour', [
|
|
434
|
+
group('Jira behaviour', [refreshLink('conn.jira.refresh', 'Refresh ticket status', 'When Jira is read again is set in Data refresh.'),
|
|
448
435
|
lockedRow('jira.push', 'Creating or updating tickets', 'PMOS drafts tickets from the PRD. Nothing reaches Jira until you select it (D88).', 'You approve every push', 'ws'),
|
|
449
436
|
lockedRow('jira.sprints', 'Sprints and burndown', 'A board backed by Jira shows its sprints. Native sprints are only for boards with no tracker (D5).', 'Mirrored from Jira, read-only', 'board')], 'org'),
|
|
450
437
|
sect('Wispr Flow', [list([w ? extRow('notes.wispr', swBtn(w.wispr !== false, EXT['notes.wispr'][2], () => notesSwitch({ wispr: w.wispr === false }, 'notes.wispr', { wispr: w.wispr !== false }), { disabled: !!override('notes.wispr') }), { scope: 'computer' }) : null, storeRow('conn.wispr.wait')]),
|
|
@@ -706,7 +693,229 @@ export function mountSettings(host, ctx = {}) {
|
|
|
706
693
|
return [sect('PMOS app', [list([appRow])], { ic: 'cloud', attrs: { 'data-c3set-app': '' } }), sect('Kit in each workspace', [list(rows)], { ic: 'repo', count: rows.length }), groupOf('updates', 'When', 'cloud')];
|
|
707
694
|
}
|
|
708
695
|
|
|
709
|
-
|
|
696
|
+
/* ── Data refresh: every source PMOS reads or writes on a schedule (PRD data-refresh-settings R9).
|
|
697
|
+
GET /api/refresh?ws · POST /api/refresh/source, /rules, /run, /run-all · GET /api/refresh/log, /log.csv. One home: Connectors links here. ── */
|
|
698
|
+
const RF_GROUPS = [['tools', 'From your tools', 'org', 'Read through your Claude session. PMOS holds no keys for these.'], ['pmos', 'Written by PMOS', 'spark', 'Background runs that write cards, documents and proposals.'], ['change', 'Kept in step on every change', 'repo', 'No schedule: these follow your files as they change.']];
|
|
699
|
+
const RF_ST = { recent: ['Recent', 'ok'], old: ['Old', 'warn'], failed: ['Failed', 'warn'], running: ['Refreshing now', 'acc'], waiting: ['Waiting', 'acc'], off: ['Off', ''], never: ['Not yet', ''] };
|
|
700
|
+
const RF_KIND = { manual: 'When I press', every: 'Every few minutes', daily: 'Daily', weekdays: 'Working days', weekly: 'Weekly' };
|
|
701
|
+
const RF_DAYS = [['Mon', 'Monday'], ['Tue', 'Tuesday'], ['Wed', 'Wednesday'], ['Thu', 'Thursday'], ['Fri', 'Friday'], ['Sat', 'Saturday'], ['Sun', 'Sunday']];
|
|
702
|
+
const RF_EVERY = { 10: '10 min', 15: '15 min', 30: '30 min', 60: 'hour', 120: '2 hours', 240: '4 hours' };
|
|
703
|
+
const RF_HOURS = []; for (let h = 5; h <= 22; h++) for (const m of ['00', '30']) RF_HOURS.push(String(h).padStart(2, '0') + ':' + m);
|
|
704
|
+
const RF_DOW = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const RF_MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
705
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
706
|
+
/** Local time: HH:MM for today, "Mon 06:30" within a week, "Sun 11 Oct 06:30" beyond. */
|
|
707
|
+
function fmtAt(iso) {
|
|
708
|
+
const d = iso ? new Date(iso) : null; if (!d || isNaN(d)) return '';
|
|
709
|
+
const t = pad2(d.getHours()) + ':' + pad2(d.getMinutes()); const n = new Date();
|
|
710
|
+
if (d.toDateString() === n.toDateString()) return t;
|
|
711
|
+
if (Math.abs(d - n) < 6 * 864e5) return RF_DOW[d.getDay()] + ' ' + t;
|
|
712
|
+
return RF_DOW[d.getDay()] + ' ' + d.getDate() + ' ' + RF_MON[d.getMonth()] + ' ' + t;
|
|
713
|
+
}
|
|
714
|
+
const fmtTook = (ms) => { if (ms == null) return ''; const s = Math.max(1, Math.round(ms / 1000)); return s < 60 ? s + ' s' : Math.floor(s / 60) + ' min' + (s % 60 ? ' ' + (s % 60) + ' s' : ''); };
|
|
715
|
+
const rfRows = () => (M.rf && M.rf.rows) || [];
|
|
716
|
+
const rfRow = (id) => rfRows().find((x) => x.id === id);
|
|
717
|
+
const rfName = (id) => { const x = rfRow(id); return x ? x.label : id; };
|
|
718
|
+
const rfNames = (ids) => (ids || []).map(rfName).join(', ');
|
|
719
|
+
const needsLook = (r) => r.on && (r.state === 'failed' || r.state === 'old');
|
|
720
|
+
const sig = (s) => JSON.stringify(s);
|
|
721
|
+
|
|
722
|
+
async function loadRf(force) {
|
|
723
|
+
const [r, lg] = await Promise.all([pmGet('/api/refresh?' + qs({ ws: M.ws })), pmGet('/api/refresh/log?' + qs({ source: M.logSource, result: M.logResult, limit: 50 }))]);
|
|
724
|
+
const next = r && r.ok ? r : { error: (r && r.error) || 'The refresh schedule could not be read.' };
|
|
725
|
+
const log = lg && lg.ok ? { total: lg.total, runs: lg.runs || [] } : { error: (lg && lg.error) || 'The log could not be read.' };
|
|
726
|
+
const s = sig([next, log]); const same = s === M.rfSig; M.rfSig = s; M.rf = next; M.rfLog = log;
|
|
727
|
+
const a = document.activeElement;
|
|
728
|
+
const busy = !force && (M.rfEdit || M.rfAsk || (a && body.contains(a) && a.tagName === 'SELECT'));
|
|
729
|
+
if (force || (!same && !busy)) draw();
|
|
730
|
+
}
|
|
731
|
+
/* Poll while the section shows; stop when another section, a search or another page is on screen. */
|
|
732
|
+
let rfTimer = null;
|
|
733
|
+
function rfWatch() {
|
|
734
|
+
const on = M.sec === 'refresh' && !M.q;
|
|
735
|
+
if (rfTimer && (!on || !root.isConnected)) { clearInterval(rfTimer); rfTimer = null; }
|
|
736
|
+
if (on && !rfTimer) rfTimer = setInterval(() => { if (M.sec === 'refresh' && !M.q && root.isConnected && root.offsetParent) loadRf(false); }, 15000);
|
|
737
|
+
if (on && !M.rf && !M.rfLoading) { M.rfLoading = true; loadRf(true).finally(() => { M.rfLoading = false; }); }
|
|
738
|
+
}
|
|
739
|
+
const rfAfter = async () => { await loadRf(true); };
|
|
740
|
+
|
|
741
|
+
async function rfSetOn(r, on) {
|
|
742
|
+
M.rfAsk = null;
|
|
743
|
+
const res = await pmPost('/api/refresh/source', { id: r.id, on });
|
|
744
|
+
if (!res || !res.ok) { M.rfErr[r.id] = (res && res.error) || 'Not saved.'; draw(); return; }
|
|
745
|
+
delete M.rfErr[r.id]; await rfAfter();
|
|
746
|
+
toast((on ? 'Turned on: ' : 'Turned off: ') + r.label, async () => { await pmPost('/api/refresh/source', { id: r.id, on: !on }); await rfAfter(); });
|
|
747
|
+
}
|
|
748
|
+
async function rfSaveSchedule(r, sch, lead) {
|
|
749
|
+
const old = r.schedule;
|
|
750
|
+
const res = await pmPost('/api/refresh/source', { id: r.id, schedule: sch });
|
|
751
|
+
if (!res || !res.ok) { const msg = (res && res.error) || 'Not saved.'; if (M.rfEdit && M.rfEdit.id === r.id) M.rfEdit.err = msg; else M.rfErr[r.id] = msg; draw(); return false; }
|
|
752
|
+
M.rfEdit = null; delete M.rfErr[r.id]; await rfAfter();
|
|
753
|
+
const nr = rfRow(r.id);
|
|
754
|
+
toast(lead + ': ' + r.label + (nr ? ', ' + nr.schedule_text : ''), old ? async () => { await pmPost('/api/refresh/source', { id: r.id, schedule: old }); await rfAfter(); } : undefined);
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
async function rfRunNow(r, withInputs) {
|
|
758
|
+
const body = { id: r.id, ws: M.ws }; if (withInputs) body.with_inputs = true;
|
|
759
|
+
const res = await pmPost('/api/refresh/run', body);
|
|
760
|
+
if (!res || !res.ok) { toast((res && res.error) || r.label + ' did not start.'); await rfAfter(); return; }
|
|
761
|
+
await rfAfter();
|
|
762
|
+
toast(res.waiting && res.waiting.length ? r.label + ' starts when ' + rfNames(res.waiting) + ' has finished.' : 'Refreshing ' + r.label + '. You can leave this page.');
|
|
763
|
+
}
|
|
764
|
+
async function rfRunAll() {
|
|
765
|
+
M.rfAllBusy = true; draw();
|
|
766
|
+
const res = await pmPost('/api/refresh/run-all', { ws: M.ws });
|
|
767
|
+
M.rfAllBusy = false; await rfAfter();
|
|
768
|
+
if (!res || !res.ok) { toast((res && res.error) || 'Refresh all did not start.'); return; }
|
|
769
|
+
const bad = (res.failed || []).length;
|
|
770
|
+
toast('Refreshing ' + (res.started || []).length + ' sources.' + (bad ? ' ' + bad + ' did not start: ' + res.failed.map((f) => rfName(f.id) + ' (' + f.error + ')').join(', ') + '.' : ''));
|
|
771
|
+
}
|
|
772
|
+
async function rfSaveRule(key, value, label) {
|
|
773
|
+
const old = M.rf.rules[key];
|
|
774
|
+
const res = await pmPost('/api/refresh/rules', { [key]: value });
|
|
775
|
+
if (!res || !res.ok) { M.err['rf.' + key] = (res && res.error) || 'Not saved.'; draw(); return; }
|
|
776
|
+
delete M.err['rf.' + key]; if (res.rules) M.rf.rules = res.rules; await rfAfter();
|
|
777
|
+
toast('Saved: ' + label, async () => { await pmPost('/api/refresh/rules', { [key]: old }); await rfAfter(); });
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/* the editor's draft is cleaned to the shape the API takes */
|
|
781
|
+
const cleanSchedule = (d) => d.kind === 'manual' ? { kind: 'manual' }
|
|
782
|
+
: d.kind === 'every' ? { kind: 'every', every: String(d.every), work: !!d.work }
|
|
783
|
+
: d.kind === 'weekly' ? { kind: 'weekly', day: d.day, times: [d.times[0]] } : { kind: d.kind, times: d.times.slice() };
|
|
784
|
+
function openEditor(r) {
|
|
785
|
+
const s = r.schedule || r.default || { kind: r.kinds[0] };
|
|
786
|
+
M.rfEdit = { id: r.id, err: '', d: { kind: s.kind, times: (s.times || []).slice(), day: s.day || 'Mon', every: String(s.every || (r.every || [])[0] || '60'), work: !!s.work } };
|
|
787
|
+
draw();
|
|
788
|
+
}
|
|
789
|
+
function setKind(r, k) {
|
|
790
|
+
const d = M.rfEdit.d; d.kind = k;
|
|
791
|
+
if (k === 'every' && !(r.every || []).includes(d.every)) d.every = (r.every || ['60'])[0];
|
|
792
|
+
if (['daily', 'weekdays', 'weekly'].includes(k) && !d.times.length) d.times = ['09:00'];
|
|
793
|
+
if (k === 'weekly') d.times = d.times.slice(0, 1);
|
|
794
|
+
M.rfEdit.err = ''; draw();
|
|
795
|
+
}
|
|
796
|
+
const timeOpts = (v) => (RF_HOURS.includes(v) ? RF_HOURS : [...RF_HOURS, v].sort()).map((h) => [h, h]);
|
|
797
|
+
|
|
798
|
+
/** One run of the log: when, which source, the result, how long and what it got. */
|
|
799
|
+
function runLine(run, withSource) {
|
|
800
|
+
const res = run.result; const bad = res === 'failed' || res === 'interrupted';
|
|
801
|
+
const tone = res === 'done' ? 'ok' : bad ? 'warn' : res === 'skipped' ? '' : 'acc';
|
|
802
|
+
const label = res === 'done' ? 'Done' : res === 'failed' ? 'Failed' : res === 'skipped' ? 'Skipped' : res === 'interrupted' ? 'Interrupted' : 'Running';
|
|
803
|
+
return el('li', { class: 'nrow', 'data-c3set-rfrun': run.run || '' },
|
|
804
|
+
el('span', { class: cx('ki', bad && 't-warn') }, c3ic(bad ? 'alert' : !res ? 'wait' : 'check', 'sm')),
|
|
805
|
+
el('div', { class: 'nb' }, el('span', { class: 'tt' }, (withSource ? rfName(run.source) + ' · ' : '') + fmtAt(run.start_at)),
|
|
806
|
+
el('div', { class: 'chips' }, tag(label, tone), run.took_ms != null ? chip('took ' + fmtTook(run.took_ms)) : null, run.trigger ? chip(run.trigger) : null, run.got ? chip(run.got) : null, run.note ? chip(run.note) : null),
|
|
807
|
+
run.error ? fold('alert', run.error, 'w') : null));
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function rfEditor(r) {
|
|
811
|
+
const E = M.rfEdit; const d = E.d; const win = (M.rf && M.rf.window) || { start: '09:00', end: '18:00' };
|
|
812
|
+
const rows = [];
|
|
813
|
+
if (r.kinds.length > 1) rows.push(frame({ key: 'rf.kind', label: 'How often', control: seg('How often', r.kinds.map((k) => [k, RF_KIND[k] || k]), d.kind, (k) => setKind(r, k)) }));
|
|
814
|
+
if (d.kind === 'every') rows.push(frame({ key: 'rf.every', label: 'Every', forId: 'c3set-rf-every', control: select('c3set-rf-every', 'How often', (r.every || []).map((v) => [v, RF_EVERY[v] || v + ' min']), d.every, (v) => { d.every = v; draw(); }) }));
|
|
815
|
+
if (d.kind === 'daily' || d.kind === 'weekdays') {
|
|
816
|
+
rows.push(frame({ key: 'rf.times', label: 'At', help: d.kind === 'weekdays' ? 'Monday to Friday.' : '',
|
|
817
|
+
control: [...d.times.flatMap((t, i) => [select('c3set-rf-time-' + i, 'Time ' + (i + 1), timeOpts(t), t, (v) => { d.times[i] = v; d.times = [...new Set(d.times)].sort(); draw(); }),
|
|
818
|
+
d.times.length > 1 ? bt({ type: 'button', class: 'ib sm', 'aria-label': 'Remove ' + t, onclick: () => { d.times.splice(i, 1); draw(); } }, c3ic('x', 'sm')) : null]),
|
|
819
|
+
d.times.length < 4 ? btn('Add a time', { variant: 'ghost', ic: 'plus', attrs: { 'data-c3set-rf-addtime': '' }, onclick: () => { const last = d.times[d.times.length - 1]; const h = Math.min(22, +last.slice(0, 2) + 4); let t = pad2(h) + ':' + last.slice(3); for (const o of RF_HOURS) { if (!d.times.includes(t)) break; t = o; } if (!d.times.includes(t)) d.times = [...d.times, t].sort(); draw(); } }) : null] }));
|
|
820
|
+
}
|
|
821
|
+
if (d.kind === 'weekly') rows.push(frame({ key: 'rf.day', label: 'On', control: [select('c3set-rf-day', 'Day', RF_DAYS, d.day, (v) => { d.day = v; draw(); }), select('c3set-rf-time-0', 'Time', timeOpts(d.times[0]), d.times[0], (v) => { d.times = [v]; draw(); })] }));
|
|
822
|
+
if (d.kind === 'every') rows.push(frame({ key: 'rf.work', label: 'Only in working hours', help: [win.start + ' to ' + win.end + ', set in ', secLink('time', 'Day and time'), '. Outside them this waits for the next slot.'],
|
|
823
|
+
control: swBtn(d.work, 'Only in working hours', () => { d.work = !d.work; draw(); }) }));
|
|
824
|
+
const hist = (r.runs || []).slice(0, 5).map((x) => runLine(x, false));
|
|
825
|
+
const isDef = !r.default || sig(r.schedule) === sig(r.default);
|
|
826
|
+
return el('div', { class: 'rf-ed', 'data-c3set-rf-editor': r.id },
|
|
827
|
+
list(rows),
|
|
828
|
+
el('div', { class: 'mk-lbl dim st-help' }, 'Last runs'),
|
|
829
|
+
hist.length ? list(hist) : help('Nothing has run yet.'),
|
|
830
|
+
E.err ? el('div', { class: 'fold w', role: 'alert', 'data-c3set-rf-err': '' }, c3ic('alert', 'sm'), el('span', null, E.err)) : null,
|
|
831
|
+
el('div', { class: 'iacts' }, btn('Save', { variant: 'primary', attrs: { 'data-c3set-rf-save': r.id }, onclick: () => rfSaveSchedule(r, cleanSchedule(d), 'Saved') }),
|
|
832
|
+
btn('Cancel', { variant: 'ghost', onclick: () => { M.rfEdit = null; draw(); } }),
|
|
833
|
+
isDef ? null : btn('Back to default', { variant: 'ghost', ic: 'undo', attrs: { 'data-c3set-rf-default': r.id }, onclick: () => rfSaveSchedule(r, r.default, 'Back to the default') })));
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function rfSourceRow(r) {
|
|
837
|
+
const [lab, tone] = RF_ST[r.state] || ['', ''];
|
|
838
|
+
const open = !!(M.rfEdit && M.rfEdit.id === r.id); const asking = M.rfAsk === r.id; const running = r.state === 'running';
|
|
839
|
+
const l = r.last;
|
|
840
|
+
const how = r.how === 'tool' ? 'Your Claude session reads it' + (profileNow().id ? ' through profile ' + profileNow().id : '') : r.how === 'bg' ? 'A background Claude run writes it' : 'Code on this computer, no model';
|
|
841
|
+
const lastRes = !l ? 'It has not run yet.' : running || !l.result ? 'It is running now.' : l.result === 'failed' ? 'Last result: failed.' : l.result === 'skipped' ? 'The last run was skipped' + (l.note ? ': ' + l.note : '') + '.'
|
|
842
|
+
: l.result === 'interrupted' ? 'The last run was interrupted.' : 'Last result: ' + (l.got || 'done') + (l.took_ms != null ? ', took ' + fmtTook(l.took_ms) : '') + '.';
|
|
843
|
+
const lastChip = running && l ? 'Started ' + fmtAt(l.start_at) : l ? 'Last refreshed ' + fmtAt(l.at || l.start_at) : 'Never refreshed';
|
|
844
|
+
const nextChip = r.state === 'waiting' ? 'Waiting for ' + rfNames(r.waiting_for) : r.on && r.next_at ? 'Next ' + fmtAt(r.next_at) : null;
|
|
845
|
+
const relation = [r.uses.length ? 'Uses: ' + rfNames(r.uses) + '.' : null, r.used_by.length ? 'Used by: ' + rfNames(r.used_by) + '.' : null].filter(Boolean).join(' ');
|
|
846
|
+
const ask = asking ? el('div', { class: 'fold w', role: 'alert', 'data-c3set-rf-ask': r.id }, c3ic('alert', 'sm'),
|
|
847
|
+
el('span', null, 'If you turn off ' + r.label + ': ' + r.impact + (r.used_by.length ? ' These use it: ' + rfNames(r.used_by) + '.' : ''))) : null;
|
|
848
|
+
const nb = [
|
|
849
|
+
el('span', { class: 'tt' }, el('span', { class: cx('ki', r.on && r.state === 'failed' && 't-warn') }, c3ic(r.ic, 'sm')), r.label),
|
|
850
|
+
el('div', { class: 'chips' }, tag(lab, tone), chip(lastChip), chip(r.schedule_text), nextChip ? chip(nextChip) : null),
|
|
851
|
+
help(r.what + ' ' + how + '. ' + lastRes),
|
|
852
|
+
relation ? help(relation) : null,
|
|
853
|
+
r.set_in ? help('Set in ' + r.set_in + '.') : null,
|
|
854
|
+
r.on && r.state === 'failed' ? fold('alert', (l && l.error) || 'The last run failed.', 'w', r.group === 'tools' ? btn('Open Connectors', { variant: 'ghost', ic: 'next', onclick: () => goSec('connectors') }) : null) : null,
|
|
855
|
+
r.on && r.state === 'old' && l && l.note ? fold('clock', l.note) : null,
|
|
856
|
+
r.built_on.length ? el('div', { class: 'fold', 'data-c3set-rf-builton': r.id }, c3ic('clock', 'sm'),
|
|
857
|
+
el('span', null, 'This builds on older data: ' + r.built_on.map((b) => b.label + (b.state && b.state !== 'recent' ? ' (' + (RF_ST[b.state] || [b.state])[0].toLowerCase() + ')' : '')).join(', ') + '.'),
|
|
858
|
+
btn('Refresh inputs first', { variant: 'ghost', ic: 'undo', disabled: !r.on || running, attrs: { 'data-c3set-rf-inputs': r.id }, onclick: () => rfRunNow(r, true) })) : null,
|
|
859
|
+
ask,
|
|
860
|
+
M.rfErr[r.id] ? el('div', { class: 'fold w', role: 'alert' }, c3ic('alert', 'sm'), el('span', null, M.rfErr[r.id])) : null,
|
|
861
|
+
open ? rfEditor(r) : null];
|
|
862
|
+
const acts = asking
|
|
863
|
+
? [btn('Turn off', { variant: 'primary', attrs: { 'data-c3set-rf-off-go': r.id }, onclick: () => rfSetOn(r, false) }), btn('Keep it on', { variant: 'ghost', onclick: () => { M.rfAsk = null; draw(); } })]
|
|
864
|
+
: [swBtn(r.on, (r.on ? 'Turn off ' : 'Turn on ') + r.label, () => { if (r.on) { M.rfAsk = r.id; draw(); } else rfSetOn(r, true); }, { 'data-c3set-rf-on': r.id }),
|
|
865
|
+
btn(running ? 'Refreshing' : 'Refresh now', { ic: 'undo', disabled: !r.on || running, attrs: { 'data-c3set-rf-now': r.id }, onclick: () => rfRunNow(r, false) }),
|
|
866
|
+
r.kinds.length ? btn(open ? 'Close' : 'Schedule', { variant: 'ghost', ic: open ? 'fold' : 'clock', attrs: { 'data-c3set-rf-open': r.id }, onclick: () => { if (open) M.rfEdit = null; else openEditor(r); draw(); } }) : null];
|
|
867
|
+
return el('li', { class: 'nrow st-row rf-row', 'data-c3set-rf': r.id }, el('div', { class: 'nb' }, ...nb.filter(Boolean)), el('div', { class: 'acts' }, ...acts.filter(Boolean)));
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function rfRules() {
|
|
871
|
+
const R = M.rf.rules; const win = M.rf.window; const q = M.rf.quiet;
|
|
872
|
+
const rule = (key, label, h, control) => frame({ key: 'rf.' + key, label, help: h, scope: 'computer', control, err: M.err['rf.' + key] });
|
|
873
|
+
return sect('Rules for every refresh', [list([
|
|
874
|
+
rule('catchup', 'Catch up after PMOS was closed', 'When PMOS starts and a refresh was missed while it was closed, it runs once right away instead of waiting for the next slot.', swBtn(R.catchup, 'Catch up after PMOS was closed', () => rfSaveRule('catchup', !R.catchup, 'catch up'))),
|
|
875
|
+
rule('retry', 'When a refresh fails', 'Failures show on the source here and in the line under the page title.', select('c3set-rf-retry', 'When a refresh fails', [['15', 'Try again once after 15 min'], ['60', 'Try again once after an hour'], ['no', 'Wait for the next slot']], R.retry, (v) => rfSaveRule('retry', v, 'when a refresh fails'))),
|
|
876
|
+
rule('tell', 'Tell me about a failed refresh', 'A card on the Briefing names the source and what to do.', seg('Tell me about a failed refresh', [['brief', 'On the Briefing'], ['here', 'Only here']], R.tell, (v) => rfSaveRule('tell', v, 'tell me about a failed refresh'))),
|
|
877
|
+
rule('max', 'Background runs at once', 'Runs that use Claude wait in line beyond this number.', select('c3set-rf-max', 'Background runs at once', [['1', '1'], ['2', '2'], ['3', '3'], ['5', '5']], R.max, (v) => rfSaveRule('max', v, 'background runs at once'))),
|
|
878
|
+
rule('keep', 'Keep the log for', 'Older lines of the refresh log are deleted. The export holds every line that is kept.', select('c3set-rf-keep', 'Keep the log for', [['30', '30 days'], ['90', '90 days'], ['365', 'A year'], ['forever', 'Forever']], R.keep, (v) => rfSaveRule('keep', v, 'keep the log'))),
|
|
879
|
+
frame({ key: 'rf.quiet', label: 'Quiet hours', scope: 'computer', help: 'No scheduled refresh starts in quiet hours. A refresh you press still runs. Quiet hours are set in the proactive settings, not here.', control: chip(q ? q.start + ' to ' + q.end : 'None') }),
|
|
880
|
+
frame({ key: 'rf.window', label: 'Background working hours', scope: 'computer', help: ['Background runs that follow working hours use these. They are set in ', secLink('time', 'Day and time'), '.'], control: chip(win ? win.start + ' to ' + win.end : '') })])], { ic: 'clock', attrs: { 'data-c3set-rf-rules': '' } });
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function rfLogCard() {
|
|
884
|
+
const L = M.rfLog || {};
|
|
885
|
+
const f = { source: M.logSource, result: M.logResult };
|
|
886
|
+
const href = '/api/refresh/log.csv' + (qs(f) ? '?' + qs(f) : '');
|
|
887
|
+
const setF = (k, v) => { if (k === 'source') M.logSource = v; else M.logResult = v; loadRf(true); };
|
|
888
|
+
const filters = el('div', { class: 'iacts' },
|
|
889
|
+
select('c3set-rf-log-source', 'Source', [['', 'All sources'], ...rfRows().map((x) => [x.id, x.label])], M.logSource, (v) => setF('source', v)),
|
|
890
|
+
select('c3set-rf-log-result', 'Result', [['', 'Any result'], ['done', 'Done'], ['failed', 'Failed'], ['skipped', 'Skipped'], ['interrupted', 'Interrupted']], M.logResult, (v) => setF('result', v)),
|
|
891
|
+
el('a', { class: 'chip2 lk', href, download: 'pmos-refresh-log.csv', 'data-c3set-rf-export': '' }, c3ic('out', 'sm'), 'Export'));
|
|
892
|
+
const runs = L.runs || [];
|
|
893
|
+
return sect('Refresh log', [filters, L.error ? fold('alert', L.error, 'w') : runs.length ? list(runs.map((x) => runLine(x, true))) : fold(null, M.logSource || M.logResult ? 'No run matches.' : 'Nothing has been refreshed yet.'),
|
|
894
|
+
L.total > runs.length ? help('Showing the newest ' + runs.length + ' of ' + L.total + '. Export has every line.') : null], { ic: 'clock', count: L.total != null ? L.total : null, attrs: { 'data-c3set-rf-log': '' } });
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function refreshSec() {
|
|
898
|
+
const D = M.rf;
|
|
899
|
+
if (!D) return [fold(null, 'Reading the refresh schedule.')];
|
|
900
|
+
if (D.error) return [fold('alert', D.error, 'w')];
|
|
901
|
+
const rows = D.rows || [];
|
|
902
|
+
const on = rows.filter((r) => r.on); const bad = rows.filter(needsLook); const run = rows.filter((r) => r.state === 'running');
|
|
903
|
+
const next = on.filter((r) => r.next_at).sort((a, b) => new Date(a.next_at) - new Date(b.next_at))[0];
|
|
904
|
+
const sum = on.length + ' of ' + rows.length + ' on · ' + on.filter((r) => r.state === 'recent').length + ' recent' + (bad.length ? ' · ' + bad.length + ' need a look' : '') + (run.length ? ' · ' + run.length + ' refreshing now' : '');
|
|
905
|
+
const top = sect('Overview', [el('div', { class: 'st-now', 'data-c3set-rf-sum': '' }, sum),
|
|
906
|
+
help((next ? 'Next: ' + next.label + ' at ' + fmtAt(next.next_at) + '. ' : '') + 'PMOS refreshes only while it is running on this computer. The same states show in the line under the page title.'),
|
|
907
|
+
el('div', { class: 'iacts' }, seg('Show', [['all', 'All'], ['needs', 'Needs a look' + (bad.length ? ' (' + bad.length + ')' : '')], ['off', 'Off']], M.rfFilter, (v) => { M.rfFilter = v; draw(); }),
|
|
908
|
+
btn(M.rfAllBusy ? 'Starting' : 'Refresh all now', { ic: 'undo', disabled: M.rfAllBusy || !on.length, attrs: { 'data-c3set-rf-all': '' }, onclick: rfRunAll }))],
|
|
909
|
+
{ ic: 'undo', right: tag(bad.length ? bad.length + ' need a look' : 'All recent', bad.length ? 'warn' : 'ok') });
|
|
910
|
+
const pick = (r) => M.rfFilter === 'all' || (M.rfFilter === 'needs' && needsLook(r)) || (M.rfFilter === 'off' && !r.on);
|
|
911
|
+
const cards = RF_GROUPS.map(([g, t, ic, intro]) => {
|
|
912
|
+
const gr = rows.filter((r) => r.group === g && pick(r));
|
|
913
|
+
return gr.length ? sect(t, [help(intro), list(gr.map(rfSourceRow))], { ic, count: gr.length, attrs: { 'data-c3set-rf-card': g } }) : null;
|
|
914
|
+
});
|
|
915
|
+
return [top, ...(cards.some(Boolean) ? cards : [fold(null, M.rfFilter === 'off' ? 'Every source is on.' : 'Nothing needs a look. Every source refreshed on time.')]), rfRules(), rfLogCard()];
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const BUILD = { about: aboutSec, workspaces: workspacesSec, boards: boardsSec, connectors: () => [...connectorsSec(), finderBlock(M.ws)], refresh: refreshSec, recording: recordingSec, time: timeSec, sharing: sharingSec, access: accessSec, sessions: sessionsSec, privacy: privacySec, appearance: appearanceSec, updates: updatesSec };
|
|
710
919
|
|
|
711
920
|
function searchView() {
|
|
712
921
|
const hits = rowsMatching(M.q);
|
|
@@ -759,7 +968,7 @@ export function mountSettings(host, ctx = {}) {
|
|
|
759
968
|
// never rebuild under a field the PM is typing in
|
|
760
969
|
const a = document.activeElement;
|
|
761
970
|
if (a && body.contains(a) && ((a.tagName === 'INPUT' && a.type === 'text') || a.tagName === 'TEXTAREA')) { if (!draw.pending) { draw.pending = true; a.addEventListener('blur', drawAfterBlur, { once: true }); } return; }
|
|
762
|
-
paintNav();
|
|
971
|
+
paintNav(); rfWatch();
|
|
763
972
|
if (M.q) { body.replaceChildren(...searchView().filter(Boolean)); return; }
|
|
764
973
|
const [id, title, ic, intro] = SECTIONS.find((s) => s[0] === M.sec);
|
|
765
974
|
const needsStore = id !== 'about';
|