instar 1.3.1643 → 1.3.1645
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/dashboard/console/accounts.js +3 -3
- package/dashboard/console/agents.js +2 -2
- package/dashboard/console/allowed.js +3 -4
- package/dashboard/console/console.js +8 -3
- package/dashboard/console/costs.js +2 -2
- package/dashboard/console/development.js +3 -3
- package/dashboard/console/ideas.js +3 -3
- package/dashboard/console/inside.js +4 -9
- package/dashboard/console/learned.js +2 -2
- package/dashboard/console/machines.js +2 -2
- package/dashboard/console/mandates.js +2 -2
- package/dashboard/console/maturation-board.js +6 -7
- package/dashboard/console/messages.js +2 -2
- package/dashboard/console/plans.js +3 -6
- package/dashboard/console/promises.js +3 -6
- package/dashboard/console/schedule.js +3 -6
- package/dashboard/console/secrets.js +2 -2
- package/dashboard/console/ui.js +104 -1
- package/dashboard/index.html +3 -1
- package/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/dist/feedback-factory/execute/FeedbackExecutorService.js +2 -2
- package/dist/feedback-factory/execute/FeedbackExecutorService.js.map +1 -1
- package/dist/feedback-factory/execute/confinementCanary.js +1 -1
- package/dist/feedback-factory/execute/confinementCanary.js.map +1 -1
- package/dist/feedback-factory/execute/executePolicy.d.ts +2 -2
- package/dist/feedback-factory/execute/executePolicy.d.ts.map +1 -1
- package/dist/feedback-factory/execute/executePolicy.js +3 -2
- package/dist/feedback-factory/execute/executePolicy.js.map +1 -1
- package/dist/native/InstarWedgeCanary.app/Contents/MacOS/InstarWedgeCanary +0 -0
- package/dist/native/instar-desktop-host +0 -0
- package/package.json +1 -1
- package/scripts/locked-proof/lib.mjs +6 -6
- package/src/data/builtin-manifest.json +2 -2
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/upgrades/1.3.1644.md +20 -0
- package/upgrades/1.3.1645.md +30 -0
- package/upgrades/side-effects/console-refresh-keeps-place.eli16.md +13 -0
- package/upgrades/side-effects/console-refresh-keeps-place.md +78 -0
- package/upgrades/side-effects/feedback-confined-vitest-direct.md +67 -0
- package/upgrades/side-effects/feedback-origin-test-cache.md +66 -0
- package/upgrades/side-effects/locked-proof-neutral-wording.eli16.md +5 -0
- package/upgrades/side-effects/locked-proof-neutral-wording.md +9 -0
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// repair / retry / cancel POST /subscription-relogin/repair-cell operator session
|
|
8
8
|
|
|
9
9
|
import { buildAccountsPage, frameworkName } from './accounts-model.js';
|
|
10
|
-
import { el, setKids, pageHead, sheet, closeSheet, story, details, note, cantTell, askPin, forgetOperatorSession } from './ui.js';
|
|
10
|
+
import { el, setKids, pageHead, sheet, closeSheet, story, details, note, cantTell, askPin, forgetOperatorSession, firstPaint, arrivalScroll } from './ui.js';
|
|
11
11
|
import { legacyHref } from './nav-model.js';
|
|
12
12
|
import { verifiedDisplayName } from './display-identity.js';
|
|
13
13
|
|
|
@@ -22,7 +22,7 @@ async function postJson(path, body, headers = {}) {
|
|
|
22
22
|
|
|
23
23
|
export async function renderAccounts(main, ctx) {
|
|
24
24
|
const { get } = ctx;
|
|
25
|
-
|
|
25
|
+
firstPaint(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: 'Capacity · Accounts' }), el('h1', { class: 'page-title skeleton', text: 'Loading accounts' })));
|
|
26
26
|
const [pool, relogin, inUse, machines] = await Promise.all([get('/subscription-pool?scope=pool', 8000), get('/subscription-relogin?scope=pool', 8000), get('/subscription-pool/in-use'), get('/pool')]);
|
|
27
27
|
const names = machines.ok && Array.isArray(machines.data && machines.data.machines) ? Object.fromEntries(machines.data.machines.map((m) => [m.machineId, m.nickname])) : {};
|
|
28
28
|
const episodes = relogin.ok && Array.isArray(relogin.data && relogin.data.episodes) ? relogin.data.episodes : [];
|
|
@@ -66,7 +66,7 @@ export async function renderAccounts(main, ctx) {
|
|
|
66
66
|
!relogin.ok ? note(`Couldn't read sign-in repairs: ${relogin.reason}.`) : null,
|
|
67
67
|
needs, list, legacyLink());
|
|
68
68
|
const want = new URLSearchParams((location.hash.split('?')[1] || '')).get('s');
|
|
69
|
-
if (want === 'needs'
|
|
69
|
+
if (want === 'needs') arrivalScroll(needs);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
function legacyLink() {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import {
|
|
15
15
|
buildAgentsPage, threadModel, inviteResultWords, pinResultWords, bearerResultWords, enableWords, bindRequest, bindResultWords, localTime,
|
|
16
16
|
} from './agents-model.js';
|
|
17
|
-
import { el, setKids, pageHead, section, sheet, closeSheet, story, details, note, cantTell, askPin } from './ui.js';
|
|
17
|
+
import { el, setKids, pageHead, section, sheet, closeSheet, story, details, note, cantTell, askPin, firstPaint } from './ui.js';
|
|
18
18
|
import { legacyHref } from './nav-model.js';
|
|
19
19
|
import { verifiedDisplayName, uniqueNamedChoices } from './display-identity.js';
|
|
20
20
|
import { topicNameMap } from './topic-names.js';
|
|
@@ -48,7 +48,7 @@ async function send(path, body, method = 'POST') {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export async function renderAgents(main, ctx) {
|
|
51
|
-
|
|
51
|
+
firstPaint(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: EYEBROW }), el('h1', { class: 'page-title skeleton', text: 'Loading other agents' })));
|
|
52
52
|
const [health, pairingRes, invitesRes, peersRes, convRes, threadsRes, poolRes, topicsRes] = await Promise.all([
|
|
53
53
|
read('/threadline/health'), read('/threadline/pairing'), read('/threadline/pairing/invites'),
|
|
54
54
|
read('/threadline/peers/health?scope=pool', POOL_TIMEOUT_MS), read('/threadline/conversations?scope=mesh'), read('/threadline/observability/threads'), read('/pool'), read('/telegram/topics?scope=pool', POOL_TIMEOUT_MS),
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
buildProfile, buildHistory, buildGuards, buildSuggestions, buildFeatures, flattenFeature, profileChanges,
|
|
9
9
|
walkToEnabled, canTurnOff, canUndoOff, needsConsent, facts, applyText, refusalText, PROFILE_LABEL, PROFILE_SENTENCE, STATE_WORD,
|
|
10
10
|
} from './allowed-model.js';
|
|
11
|
-
import { el, setKids, pageHead, section, sheet, closeSheet, story, details, note, cantTell, actionButton, askPin, operatorSession, forgetOperatorSession } from './ui.js';
|
|
11
|
+
import { el, setKids, pageHead, section, sheet, closeSheet, story, details, note, cantTell, actionButton, askPin, operatorSession, forgetOperatorSession, firstPaint, arrivalScroll } from './ui.js';
|
|
12
12
|
import { legacyHref } from './nav-model.js';
|
|
13
13
|
|
|
14
14
|
const featureErrors = new Map(); // feature id → the server's last refusal, shown beside the row
|
|
@@ -40,7 +40,7 @@ const errWords = (r) => {
|
|
|
40
40
|
|
|
41
41
|
export async function renderAllowed(main, ctx) {
|
|
42
42
|
const { get, post } = ctx;
|
|
43
|
-
|
|
43
|
+
firstPaint(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: 'Settings · What I’m allowed to do' }), el('h1', { class: 'page-title skeleton', text: 'Loading' })));
|
|
44
44
|
const [auto, hist, elev, guardsRes, featRes] = await Promise.all([
|
|
45
45
|
get('/autonomy'), get('/autonomy/history'), get('/autonomy/elevation'), get('/guards?scope=pool', 8000), get('/features'),
|
|
46
46
|
]);
|
|
@@ -66,8 +66,7 @@ export async function renderAllowed(main, ctx) {
|
|
|
66
66
|
|
|
67
67
|
const want = new URLSearchParams((location.hash.split('?')[1] || '')).get('s');
|
|
68
68
|
if (want) {
|
|
69
|
-
|
|
70
|
-
if (sec) sec.scrollIntoView({ block: 'start' });
|
|
69
|
+
arrivalScroll(main.querySelector(`#sec-${want}`));
|
|
71
70
|
}
|
|
72
71
|
}
|
|
73
72
|
|
|
@@ -30,7 +30,7 @@ import { renderSessions } from './sessions.js';
|
|
|
30
30
|
import { renderMaturation, maturationTile } from './maturation-board.js';
|
|
31
31
|
import { renderIdeas, ideasTile } from './ideas.js';
|
|
32
32
|
import { paceCard } from './pace.js';
|
|
33
|
-
import { sheetIsOpen, closeSheet, unlockCheck, machineIssue, MACHINES_PAGE } from './ui.js';
|
|
33
|
+
import { sheetIsOpen, closeSheet, unlockCheck, machineIssue, MACHINES_PAGE, newArrival, refreshHeld } from './ui.js';
|
|
34
34
|
|
|
35
35
|
const TOKEN_KEY = 'instar_token'; // shared with the legacy dashboard
|
|
36
36
|
const THEME_KEY = 'instar_console_theme';
|
|
@@ -639,6 +639,7 @@ const NATIVE = {
|
|
|
639
639
|
function route() {
|
|
640
640
|
if (homeRun) { homeRun.active = false; for (const p of Object.values(homeRun.pending)) if (p && p !== true) clearTimeout(p); homeRun = null; }
|
|
641
641
|
closeSheet();
|
|
642
|
+
newArrival();
|
|
642
643
|
const path = (location.hash.replace(/^#\/?/, '') || 'home').split('?')[0];
|
|
643
644
|
const id = path.split('/')[0];
|
|
644
645
|
const group = GROUPS.find((g) => g.id === id);
|
|
@@ -647,9 +648,13 @@ function route() {
|
|
|
647
648
|
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
|
|
648
649
|
if (NATIVE[path]) {
|
|
649
650
|
NATIVE[path](main, { get, post, getCommitments });
|
|
650
|
-
// Pages refresh in place every 30 s while visible, never over an open sheet
|
|
651
|
+
// Pages refresh in place every 30 s while visible, never over an open sheet or an
|
|
652
|
+
// interaction in progress, and one at a time: a slow read cannot paint over a newer one.
|
|
653
|
+
let refreshing = false;
|
|
651
654
|
refreshTimer = setInterval(() => {
|
|
652
|
-
if (document.visibilityState
|
|
655
|
+
if (refreshing || document.visibilityState !== 'visible' || sheetIsOpen() || refreshHeld(main) || (location.hash.replace(/^#\/?/, '').split('?')[0]) !== path) return;
|
|
656
|
+
refreshing = true;
|
|
657
|
+
Promise.resolve().then(() => NATIVE[path](main, { get, post, getCommitments })).catch(() => { /* @silent-fallback-ok: a failed refresh keeps the page on screen; the next tick tries again */ }).finally(() => { refreshing = false; });
|
|
653
658
|
}, REFRESH_MS);
|
|
654
659
|
} else if (group) renderGroup(main, group); else renderHome(main);
|
|
655
660
|
window.scrollTo(0, 0);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// reaches the DOM through textContent; the model (costs-model.js) decides everything.
|
|
5
5
|
|
|
6
6
|
import { buildCostsPage, WINDOWS, DEFAULT_WINDOW, GRAINS, windowParams, spendQuery, committedLine, basisWord, fmtTokens, fmtUsd, modelWord, doorWord } from './costs-model.js';
|
|
7
|
-
import { el, setKids, pageHead, section, sheet, story, details, note, cantTell } from './ui.js';
|
|
7
|
+
import { el, setKids, pageHead, section, sheet, story, details, note, cantTell, firstPaint } from './ui.js';
|
|
8
8
|
import { legacyHref } from './nav-model.js';
|
|
9
9
|
import { sessionLabel } from './home-model.js';
|
|
10
10
|
|
|
@@ -19,7 +19,7 @@ const view = { window: DEFAULT_WINDOW, grain: 'day' };
|
|
|
19
19
|
|
|
20
20
|
export async function renderCosts(main, ctx) {
|
|
21
21
|
const { get } = ctx;
|
|
22
|
-
|
|
22
|
+
firstPaint(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: EYEBROW }), el('h1', { class: 'page-title skeleton', text: 'Loading what thinking costs' })));
|
|
23
23
|
const w = windowParams(view.window);
|
|
24
24
|
const [spend, caps, status, metrics, insights, tokens, claude, codex, orphans, chains, sessions] = await Promise.all([
|
|
25
25
|
get(`/routing-spend/summary?${spendQuery(view.grain, w.sinceHours)}`), get('/routing-spend/caps'), get('/routing-spend/enable-status'),
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
buildShipping, buildPace, buildFeedback, developmentHeadline, stoppedLead, foldFromHash, maintainerMode, authorityCard, holdWords, verifyWords, rollbackWords,
|
|
14
14
|
readinessAuthorityRequest, triageAuthorityRequest, commitRequest, authorityResultWords, commitResultWords, newDecisionRef, PACE_METRICS, DEV_TOOLS_KEY,
|
|
15
15
|
} from './development-model.js';
|
|
16
|
-
import { el, setKids, pageHead, section, sheet, closeSheet, story, details, note, cantTell, askPin } from './ui.js';
|
|
16
|
+
import { el, setKids, pageHead, section, sheet, closeSheet, story, details, note, cantTell, askPin, firstPaint, arrivalScroll } from './ui.js';
|
|
17
17
|
import { legacyHref } from './nav-model.js';
|
|
18
18
|
|
|
19
19
|
const EYEBROW = 'Work · Development';
|
|
@@ -93,7 +93,7 @@ export async function renderDevelopment(main, ctx) {
|
|
|
93
93
|
// Each navigation gets a new <main>, so no page yet means someone just arrived: a `?s=` link wins over the
|
|
94
94
|
// remembered fold then. A 30-second refresh keeps the page, so it never overrides what the operator opened or closed.
|
|
95
95
|
if (!main.querySelector('.dev-page')) {
|
|
96
|
-
|
|
96
|
+
firstPaint(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: EYEBROW }), el('h1', { class: 'page-title skeleton', text: 'Loading development' })));
|
|
97
97
|
const want = foldFromHash(location.hash);
|
|
98
98
|
if (want) { view.open = want; view.scrollTo = want; }
|
|
99
99
|
}
|
|
@@ -140,7 +140,7 @@ function paint() {
|
|
|
140
140
|
if (view.scrollTo) {
|
|
141
141
|
const sec = main.querySelector(`#sec-${view.scrollTo}`);
|
|
142
142
|
view.scrollTo = null;
|
|
143
|
-
|
|
143
|
+
arrivalScroll(sec);
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Ecosystem ideas page (docs/specs/ecosystem-idea-pipeline.md §9). Advice only: it builds nothing.
|
|
2
2
|
// Every value is written with textContent via el(). Decisions go through the PIN plan and commit.
|
|
3
3
|
|
|
4
|
-
import { el, pageHead, cantTell, askPin, note } from './ui.js';
|
|
4
|
+
import { el, pageHead, cantTell, askPin, note, setKids } from './ui.js';
|
|
5
5
|
import { BANDS, ideasInBand, headline, funnelLine, admissionLine, classWords, projectLine, planRequest } from './ideas-model.js';
|
|
6
6
|
|
|
7
7
|
let probeResult = null;
|
|
@@ -50,7 +50,7 @@ export async function renderIdeas(main, api) {
|
|
|
50
50
|
const refresh = () => renderIdeas(main, api);
|
|
51
51
|
const r = await api.get('/ecosystem-ideas?scope=pool', 12_000);
|
|
52
52
|
if (!r.ok) {
|
|
53
|
-
main
|
|
53
|
+
setKids(main, r.reason === 'switched off on this agent'
|
|
54
54
|
? el('section', {}, el('div', { class: 'eyebrow', text: 'Ideas' }), el('h1', { text: 'The idea review is not switched on for this agent.' }))
|
|
55
55
|
: cantTell(r.reason));
|
|
56
56
|
return;
|
|
@@ -84,7 +84,7 @@ export async function renderIdeas(main, api) {
|
|
|
84
84
|
parts.push(el('div', { class: 'need-card' }, el('span', { class: 'need-title', text: `Suggested: ${p.repo}` }), el('p', { class: 'need-sub', text: p.reason }), add, status));
|
|
85
85
|
}
|
|
86
86
|
if (s.lastTickError) parts.push(note(`The last round had a problem: ${s.lastTickError}`));
|
|
87
|
-
main
|
|
87
|
+
setKids(main, el('div', { class: 'ideas-page' }, parts));
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
export function ideasTile(get) {
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
CHIPS, KINDS, KIND_WORDS, COUNTERPARTIES, COUNTERPARTY_WORDS, STAGE_INFO, LOOKUP_KINDS, SCOPES, ADVISORY, CLAIM_KEYS, REVIEW_KEYS, CLAIMS_PAGE, WINDOW_STEP,
|
|
17
17
|
setPeerNames,
|
|
18
18
|
} from './inside-model.js';
|
|
19
|
-
import { el, setKids, pageHead, sheet, closeSheet, story, details, note, cantTell } from './ui.js';
|
|
19
|
+
import { el, setKids, pageHead, sheet, closeSheet, story, details, note, cantTell, firstPaint, arrivalScroll } from './ui.js';
|
|
20
20
|
|
|
21
21
|
const EYEBROW = 'Settings · Under the hood';
|
|
22
22
|
const SUBTITLE = 'Conversation history, recurring problems, checks on my claims.';
|
|
@@ -24,7 +24,6 @@ const SUBTITLE = 'Conversation history, recurring problems, checks on my claims.
|
|
|
24
24
|
const view = { chip: 'all', kind: null, counterparty: null, limit: WINDOW_STEP, older: [], claimsLimit: CLAIMS_PAGE, open: new Set() };
|
|
25
25
|
let lastProblems = null;
|
|
26
26
|
let lookupNode = null;
|
|
27
|
-
let scrolledFor = null;
|
|
28
27
|
|
|
29
28
|
function bearer() { try { return localStorage.getItem('instar_token') || ''; } catch { return ''; } }
|
|
30
29
|
/** A write that keeps the status and body (the revoke route needs the intent header). */
|
|
@@ -38,7 +37,7 @@ async function send(req) {
|
|
|
38
37
|
|
|
39
38
|
export async function renderInside(main, ctx) {
|
|
40
39
|
const { get } = ctx;
|
|
41
|
-
|
|
40
|
+
firstPaint(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: EYEBROW }), el('h1', { class: 'page-title skeleton', text: 'Loading' })));
|
|
42
41
|
const [recent, stats, sessions, pool, insights, week, all, claimsRes, reviewsRes, pairingRes, ...pages] = await Promise.all([
|
|
43
42
|
get(recentUrl({ limit: view.limit, counterparty: view.counterparty })), get('/shared-state/stats'), get('/shared-state/sessions'), get('/pool'),
|
|
44
43
|
get('/failures/insights?limit=50'), get('/failures/analysis?sinceDays=7'), get('/failures/analysis'),
|
|
@@ -65,14 +64,10 @@ export async function renderInside(main, ctx) {
|
|
|
65
64
|
pageHead({ eyebrow: `${EYEBROW} · How I’m doing inside`, headline: h.text, tone: h.tone, sub: h.sub || SUBTITLE, facts: insideFacts({ activity, problems, claims, reviews }) }),
|
|
66
65
|
el('div', { class: 'page-sections' }, sections)));
|
|
67
66
|
const want = new URLSearchParams(location.hash.split('?')[1] || '').get('s');
|
|
68
|
-
if (want
|
|
67
|
+
if (want) arrivalScroll(main.querySelector(`#sec-${want}`), { open: true });
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
// ── shared bits ───────────────────────────────────────────────────────────
|
|
72
|
-
function openFold(sec) {
|
|
73
|
-
const head = sec.querySelector(':scope > button.list-head');
|
|
74
|
-
if (head && head.getAttribute('aria-expanded') === 'false') head.click();
|
|
75
|
-
}
|
|
76
71
|
/** A collapsible block whose open state survives refreshes. */
|
|
77
72
|
function fold(id, title, count, body, { tone } = {}) {
|
|
78
73
|
const isOpen = view.open.has(id);
|
|
@@ -291,5 +286,5 @@ function lookupBlock(get) {
|
|
|
291
286
|
/** Test seam: forget the remembered view between tests. */
|
|
292
287
|
export function _resetInsideState() {
|
|
293
288
|
view.chip = 'all'; view.kind = null; view.counterparty = null; view.limit = WINDOW_STEP; view.older = []; view.claimsLimit = CLAIMS_PAGE; view.open = new Set();
|
|
294
|
-
lastProblems = null; lookupNode = null;
|
|
289
|
+
lastProblems = null; lookupNode = null;
|
|
295
290
|
}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
buildPreferences, buildCorrections, buildConflicts, machineNames, learnedHeadline, learnedFacts,
|
|
10
10
|
correctionsUrl, kindWords, confidenceWords, correctionStatusWords, ADVISORY_LINE,
|
|
11
11
|
} from './learned-model.js';
|
|
12
|
-
import { el, setKids, pageHead, section, sheet, story, details, note, cantTell } from './ui.js';
|
|
12
|
+
import { el, setKids, pageHead, section, sheet, story, details, note, cantTell, firstPaint } from './ui.js';
|
|
13
13
|
|
|
14
14
|
const PAGE = 20;
|
|
15
15
|
// The last good preferences build, so a failed refresh shows it with its age (stale) instead of nothing.
|
|
@@ -21,7 +21,7 @@ const CORRECTION_KEYS = ['id', 'kind', 'status', 'occurrenceCount', 'detectedAt'
|
|
|
21
21
|
|
|
22
22
|
export async function renderLearned(main, ctx) {
|
|
23
23
|
const { get } = ctx;
|
|
24
|
-
|
|
24
|
+
firstPaint(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: 'Settings · What I’ve learned about you' }), el('h1', { class: 'page-title skeleton', text: 'Loading' })));
|
|
25
25
|
const [prefsRes, firstPage, conflictsRes, poolRes] = await Promise.all([
|
|
26
26
|
get('/preferences/session-context'), get(correctionsUrl(null, PAGE)), get('/state/conflicts'), get('/pool'),
|
|
27
27
|
]);
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
// the legacy tab until their PIN sheet is rebuilt (the plan's first cut).
|
|
4
4
|
|
|
5
5
|
import { buildMachinesPage, ropesFor, healthOf, usageOf, machineCarePool } from './machines-model.js';
|
|
6
|
-
import { el, setKids, pageHead, sheet, closeSheet, story, note, cantTell } from './ui.js';
|
|
6
|
+
import { el, setKids, pageHead, sheet, closeSheet, story, note, cantTell, firstPaint } from './ui.js';
|
|
7
7
|
import { legacyHref } from './nav-model.js';
|
|
8
8
|
|
|
9
9
|
const STATE_DOT = { ok: 'dot live', attn: 'dot attn', bad: 'dot bad', stale: 'dot unknown', unknown: 'dot unknown' };
|
|
10
10
|
|
|
11
11
|
export async function renderMachines(main, ctx) {
|
|
12
12
|
const { get } = ctx;
|
|
13
|
-
|
|
13
|
+
firstPaint(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: 'Capacity · Machines' }), el('h1', { class: 'page-title skeleton', text: 'Loading machines' })));
|
|
14
14
|
const [pool, health, guards, sessions, care] = await Promise.all([get('/pool'), get('/health'), get('/guards'), get('/sessions?scope=pool', 8000), get('/machine-maintenance?scope=pool', 8000)]);
|
|
15
15
|
const live = sessions.ok && Array.isArray(sessions.data && sessions.data.sessions) ? sessions.data.sessions.filter((s) => s.status === 'running' || s.status === 'starting') : [];
|
|
16
16
|
const byMachine = {};
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
issueRequest, revokeRequest, grantRequest, approveRequest, denyRequest, resultWords,
|
|
10
10
|
FLOOR_ACTIONS, GRANT_DURATIONS, AUTHORITIES_TEMPLATE, humanAction, defaultExpiryLocal,
|
|
11
11
|
} from './mandates-model.js';
|
|
12
|
-
import { el, setKids, pageHead, section, sheet, closeSheet, story, details, note, cantTell, askPin } from './ui.js';
|
|
12
|
+
import { el, setKids, pageHead, section, sheet, closeSheet, story, details, note, cantTell, askPin, firstPaint } from './ui.js';
|
|
13
13
|
import { legacyHref } from './nav-model.js';
|
|
14
14
|
import { verifiedDisplayName, uniqueNamedChoices } from './display-identity.js';
|
|
15
15
|
|
|
@@ -29,7 +29,7 @@ async function send(path, body) {
|
|
|
29
29
|
|
|
30
30
|
export async function renderMandates(main, ctx) {
|
|
31
31
|
const { get } = ctx;
|
|
32
|
-
|
|
32
|
+
firstPaint(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: 'People · Permission slips' }), el('h1', { class: 'page-title skeleton', text: 'Loading' })));
|
|
33
33
|
const [slipsRes, auditRes, approvalsRes, usersRes, healthRes, tlHealth, pairing] = await Promise.all([
|
|
34
34
|
get('/mandate'), get(`/mandate/audit?limit=${AUDIT_LIMIT}`), get('/authorization-requests?status=pending'),
|
|
35
35
|
get('/permissions/users'), get('/health'), get('/threadline/health'), get('/threadline/pairing/invites'),
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Layout, top to bottom: one answer sentence · a square per feature (the glance) ·
|
|
6
6
|
// plain groups (Broken, Stuck, …) with one plain line per feature · details on tap.
|
|
7
7
|
|
|
8
|
-
import { el, cantTell } from './ui.js';
|
|
8
|
+
import { el, cantTell, setKids, firstPaint, arrivalScroll } from './ui.js';
|
|
9
9
|
import { maturationPaceNode } from './pace.js';
|
|
10
10
|
import {
|
|
11
11
|
GROUPS, groupOf, groupCounts, headline, subline, parseState, stateQuery, openGroups,
|
|
@@ -21,16 +21,16 @@ let lastBoard = null;
|
|
|
21
21
|
* @param nav { read() → query string, write(query) } — where view state lives in the URL
|
|
22
22
|
*/
|
|
23
23
|
export async function renderMaturation(main, api, nav = consoleNav()) {
|
|
24
|
-
if (!lastBoard) main
|
|
24
|
+
if (!lastBoard) firstPaint(main, el('p', { class: 'mb-loading', text: 'Reading every machine…' }));
|
|
25
25
|
const r = await api.get('/maturation/board?scope=pool', POOL_TIMEOUT_MS);
|
|
26
26
|
if (!r.ok) {
|
|
27
27
|
if (r.reason === 'switched off on this agent') {
|
|
28
|
-
main
|
|
28
|
+
setKids(main, el('section', { class: 'mb-hero' },
|
|
29
29
|
el('div', { class: 'eyebrow', text: 'Feature maturation' }),
|
|
30
30
|
el('h1', { class: 'mb-answer', text: 'The maturation board is not switched on for this agent.' })));
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
|
-
if (!lastBoard) { main
|
|
33
|
+
if (!lastBoard) { setKids(main, cantTell(r.reason)); return; }
|
|
34
34
|
} else {
|
|
35
35
|
lastBoard = r.data;
|
|
36
36
|
}
|
|
@@ -111,13 +111,12 @@ function draw(main, board, nav, pace = null) {
|
|
|
111
111
|
return el('section', { class: 'mb-group' }, head, body);
|
|
112
112
|
}).filter(Boolean);
|
|
113
113
|
|
|
114
|
-
main
|
|
114
|
+
setKids(main, hero, glance, tools,
|
|
115
115
|
sections.length ? el('div', { class: 'mb-groups' }, sections) : el('p', { class: 'mb-sub', text: 'Nothing matches.' }),
|
|
116
116
|
sourcesNote(board));
|
|
117
117
|
|
|
118
118
|
if (st.f) {
|
|
119
|
-
|
|
120
|
-
if (target && typeof target.scrollIntoView === 'function') target.scrollIntoView({ block: 'center' });
|
|
119
|
+
arrivalScroll(main.querySelector('.mb-row[aria-expanded="true"]'), { key: `row:${st.f}`, block: 'center' });
|
|
121
120
|
}
|
|
122
121
|
}
|
|
123
122
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// comes from /health and needs none.
|
|
4
4
|
|
|
5
5
|
import { buildMessagesPage, displayModel, heldHeadline } from './messages-model.js';
|
|
6
|
-
import { el, setKids, pageHead, section, sheet, closeSheet, story, note, cantTell, askPin, operatorSession, forgetOperatorSession } from './ui.js';
|
|
6
|
+
import { el, setKids, pageHead, section, sheet, closeSheet, story, note, cantTell, askPin, operatorSession, forgetOperatorSession, firstPaint } from './ui.js';
|
|
7
7
|
import { legacyHref } from './nav-model.js';
|
|
8
8
|
import { verifiedDisplayName, uniqueNamedChoices } from './display-identity.js';
|
|
9
9
|
import { topicNameMap, isRealTopicName } from './topic-names.js';
|
|
@@ -25,7 +25,7 @@ async function opFetch(path, opts = {}) {
|
|
|
25
25
|
export async function renderMessages(main, ctx) {
|
|
26
26
|
const { get } = ctx;
|
|
27
27
|
const first = !main.querySelector('.msg-page');
|
|
28
|
-
if (first)
|
|
28
|
+
if (first) firstPaint(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: 'People · Messages to you' }), el('h1', { class: 'page-title skeleton', text: 'Loading' })));
|
|
29
29
|
// A locked page is not re-polled against the PIN-gated routes every 30 s: only the
|
|
30
30
|
// first render (and a render after an unlock) asks for the records.
|
|
31
31
|
const locked = !first && !operatorSession() && main.querySelector('.need-card');
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import {
|
|
8
8
|
buildPlansPage, roundsTrack, projectActions, resultWords, refusalWords, driftSentence, linkChips, stepList, blockerStory, reasonWord, statusWord, PROJECT_DETAIL_CAP, OPEN_STATUSES,
|
|
9
9
|
} from './plans-model.js';
|
|
10
|
-
import { el, setKids, pageHead, section, sheet, closeSheet, sheetIsOpen, story, details, note, cantTell } from './ui.js';
|
|
10
|
+
import { el, setKids, pageHead, section, sheet, closeSheet, sheetIsOpen, story, details, note, cantTell, firstPaint, arrivalScroll } from './ui.js';
|
|
11
11
|
import { legacyHref } from './nav-model.js';
|
|
12
12
|
import { topicNameMap } from './topic-names.js';
|
|
13
13
|
import { topicLink } from './promises.js';
|
|
@@ -22,7 +22,7 @@ let last = null; // the last data read, repainted
|
|
|
22
22
|
|
|
23
23
|
export async function renderPlans(main, ctx) {
|
|
24
24
|
const { get, post } = ctx;
|
|
25
|
-
|
|
25
|
+
firstPaint(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: EYEBROW }), el('h1', { class: 'page-title skeleton', text: 'Loading plans' })));
|
|
26
26
|
const [initiatives, digest, projects, blockers, floor, pool, topics] = await Promise.all([
|
|
27
27
|
get('/initiatives'), get('/initiatives/digest'), get('/projects'), get('/blockers'), get('/autonomous/throughput-floor'), get('/pool'), get('/telegram/topics?scope=pool'),
|
|
28
28
|
]);
|
|
@@ -94,10 +94,7 @@ function paint() {
|
|
|
94
94
|
let want = new URLSearchParams(path[1] || '').get('s');
|
|
95
95
|
if (!want && /^work\/plans\/in-the-way/.test(path[0])) want = 'in-the-way';
|
|
96
96
|
if (want) {
|
|
97
|
-
|
|
98
|
-
const head = sec && sec.querySelector('button.list-head[aria-expanded="false"]');
|
|
99
|
-
if (head) head.click();
|
|
100
|
-
if (sec) sec.scrollIntoView({ block: 'start' });
|
|
97
|
+
arrivalScroll(main.querySelector(`#sec-${want}`), { open: true });
|
|
101
98
|
}
|
|
102
99
|
}
|
|
103
100
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { buildPromisesPage, storyOf, backstoryOf, updateQuestionOf, nextStepOf, actionsFor } from './promises-model.js';
|
|
5
5
|
import { isInternalFollowUp } from './home-model.js';
|
|
6
6
|
import { promiseActionLinks } from './promise-actions.js';
|
|
7
|
-
import { el, setKids, pageHead, section, sheet, closeSheet, sheetIsOpen, story, actionButton, note, cantTell } from './ui.js';
|
|
7
|
+
import { el, setKids, pageHead, section, sheet, closeSheet, sheetIsOpen, story, actionButton, note, cantTell, firstPaint, arrivalScroll } from './ui.js';
|
|
8
8
|
import { legacyHref } from './nav-model.js';
|
|
9
9
|
import { verifiedDisplayName } from './display-identity.js';
|
|
10
10
|
|
|
@@ -19,7 +19,7 @@ export async function renderPromises(main, ctx) {
|
|
|
19
19
|
if (main.dataset.promisesLoading === '1') return;
|
|
20
20
|
main.dataset.promisesLoading = '1';
|
|
21
21
|
const { get, post } = ctx;
|
|
22
|
-
|
|
22
|
+
firstPaint(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: 'Work · Promises' }), el('h1', { class: 'page-title skeleton', text: 'Loading promises' })));
|
|
23
23
|
const slow = setTimeout(() => {
|
|
24
24
|
const title = main.querySelector('.page-title.skeleton');
|
|
25
25
|
if (title) title.textContent = 'Still checking promises…';
|
|
@@ -67,10 +67,7 @@ export async function renderPromises(main, ctx) {
|
|
|
67
67
|
// A fact links to its section: open it and scroll there.
|
|
68
68
|
const want = new URLSearchParams((location.hash.split('?')[1] || '')).get('s');
|
|
69
69
|
if (want) {
|
|
70
|
-
|
|
71
|
-
const head = sec && sec.querySelector('button.list-head[aria-expanded="false"]');
|
|
72
|
-
if (head) head.click();
|
|
73
|
-
if (sec) sec.scrollIntoView({ block: 'start' });
|
|
70
|
+
arrivalScroll(main.querySelector(`#sec-${want}`), { open: true });
|
|
74
71
|
}
|
|
75
72
|
// Home links to a specific request. Open its action sheet on arrival, so a
|
|
76
73
|
// tap never strands the operator at the top of a long promise list.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import {
|
|
11
11
|
buildSchedulePage, buildHistory, markText, storySentence, storyLines, jobActions, toggleConfirm, runStartWords, runFollowWords, saveRefusalWords, originWords, scheduleWords, ago, HISTORY_LIMIT, STRIP_LIMIT,
|
|
12
12
|
} from './schedule-model.js';
|
|
13
|
-
import { el, setKids, pageHead, section, sheet, closeSheet, sheetIsOpen, story, details, note, noteLinked, cantTell } from './ui.js';
|
|
13
|
+
import { el, setKids, pageHead, section, sheet, closeSheet, sheetIsOpen, story, details, note, noteLinked, cantTell, firstPaint, arrivalScroll } from './ui.js';
|
|
14
14
|
import { legacyHref } from './nav-model.js';
|
|
15
15
|
import { verifiedDisplayName } from './display-identity.js';
|
|
16
16
|
|
|
@@ -35,7 +35,7 @@ async function send(method, path, body) {
|
|
|
35
35
|
|
|
36
36
|
export async function renderSchedule(main, ctx) {
|
|
37
37
|
const { get } = ctx;
|
|
38
|
-
|
|
38
|
+
firstPaint(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: EYEBROW }), el('h1', { class: 'page-title skeleton', text: 'Loading the schedule' })));
|
|
39
39
|
const [local, pool, machines, history, migration, reconcile] = await Promise.all([
|
|
40
40
|
get('/jobs'), get('/jobs?scope=pool', 8000), get('/pool'), get('/jobs/history?limit=500'), get('/jobs/migration-status'), get('/jobs/reconcile'),
|
|
41
41
|
]);
|
|
@@ -82,10 +82,7 @@ function paint() {
|
|
|
82
82
|
const params = new URLSearchParams(path[1] || '');
|
|
83
83
|
const want = params.get('s');
|
|
84
84
|
if (want) {
|
|
85
|
-
|
|
86
|
-
const head = sec && sec.querySelector('button.list-head[aria-expanded="false"]');
|
|
87
|
-
if (head) head.click();
|
|
88
|
-
if (sec) sec.scrollIntoView({ block: 'start' });
|
|
85
|
+
arrivalScroll(main.querySelector(`#sec-${want}`), { open: true });
|
|
89
86
|
}
|
|
90
87
|
const slug = params.get('job');
|
|
91
88
|
if (slug && !sheetIsOpen()) {
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
topicNameMap, conversationChoices, requestCard, requestBody, testRequestBody, resultWords, shareWords,
|
|
10
10
|
leftWords, TTL_CHOICES,
|
|
11
11
|
} from './secrets-model.js';
|
|
12
|
-
import { el, setKids, pageHead, section, sheet, closeSheet, story, details, note, cantTell } from './ui.js';
|
|
12
|
+
import { el, setKids, pageHead, section, sheet, closeSheet, story, details, note, cantTell, firstPaint } from './ui.js';
|
|
13
13
|
import { legacyHref } from './nav-model.js';
|
|
14
14
|
|
|
15
15
|
const RECORD_KEYS = ['token', 'topicId', 'createdAt', 'expiresAt'];
|
|
@@ -55,7 +55,7 @@ async function share(url, title, status) {
|
|
|
55
55
|
|
|
56
56
|
export async function renderSecrets(main, ctx) {
|
|
57
57
|
const { get } = ctx;
|
|
58
|
-
|
|
58
|
+
firstPaint(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: 'People · Passwords & keys' }), el('h1', { class: 'page-title skeleton', text: 'Loading' })));
|
|
59
59
|
const [pendingRes, ctxRes, syncRes, topicsRes, poolRes] = await Promise.all([
|
|
60
60
|
get('/secrets/pending'), get('/self-knowledge/session-context?full=1'), get('/secrets/sync-status'), get('/telegram/topics?scope=pool'), get('/pool'),
|
|
61
61
|
]);
|
package/dashboard/console/ui.js
CHANGED
|
@@ -205,7 +205,110 @@ export function actionButton({ label, kind = 'primary', confirm, reason, run, do
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
/** replaceChildren that skips null/false (a bare null would render as the text "null"). */
|
|
208
|
-
export function setKids(node, ...kids) {
|
|
208
|
+
export function setKids(node, ...kids) {
|
|
209
|
+
const page = node.tagName === 'MAIN' && node.isConnected && node.childElementCount > 0 && !restoring ? capturePlace(node) : null;
|
|
210
|
+
if (node.tagName === 'MAIN') node.dataset.painted = '1';
|
|
211
|
+
node.replaceChildren(...kids.flat(Infinity).filter((k) => k != null && k !== false));
|
|
212
|
+
if (node.tagName !== 'MAIN' || restoring) return;
|
|
213
|
+
const drawn = foldStates(node);
|
|
214
|
+
if (page) restorePlace(node, page);
|
|
215
|
+
drawnFolds.set(node, drawn);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── A refresh keeps the operator's place (Dashboard UX Standard F12) ───────
|
|
219
|
+
// Every page repaints by replacing what is in <main>. Replacing and restoring in
|
|
220
|
+
// one task means the browser never paints the gap: no flash, no jump. Folds are
|
|
221
|
+
// matched by what they say (numbers ignored, a count going from 3 to 4 is the same
|
|
222
|
+
// fold), so this needs nothing from the page. Only a fold the operator moved away from
|
|
223
|
+
// how the page last drew it is put back; a page that keeps its own fold state (and
|
|
224
|
+
// repaints BECAUSE that state changed) is never fought.
|
|
225
|
+
let restoring = false;
|
|
226
|
+
const drawnFolds = new WeakMap(); // <main> → key → how the page itself last drew each fold
|
|
227
|
+
const foldIsOpen = (n) => (n.tagName === 'DETAILS' ? n.open : n.getAttribute('aria-expanded') === 'true');
|
|
228
|
+
function foldStates(root) {
|
|
229
|
+
const out = new Map();
|
|
230
|
+
for (const [n, key] of placeKeys(root)) if (n.tagName === 'DETAILS' || (n.tagName === 'BUTTON' && n.hasAttribute('aria-expanded'))) out.set(key, foldIsOpen(n));
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
function placeKeys(root) {
|
|
234
|
+
const seen = new Map();
|
|
235
|
+
const keys = new Map();
|
|
236
|
+
for (const n of root.querySelectorAll('*')) {
|
|
237
|
+
const tag = n.tagName;
|
|
238
|
+
const fold = tag === 'DETAILS' || (tag === 'BUTTON' && n.hasAttribute('aria-expanded'));
|
|
239
|
+
const scrolled = n.scrollTop > 0 || n.scrollLeft > 0;
|
|
240
|
+
const entry = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || tag === 'BUTTON' || tag === 'A' || n.hasAttribute('tabindex');
|
|
241
|
+
if (!fold && !scrolled && !entry && n.classList.length === 0) continue;
|
|
242
|
+
const home = n.parentElement && n.parentElement.closest('[id]');
|
|
243
|
+
const label = (tag === 'DETAILS' ? (n.querySelector(':scope > summary') || n).textContent : fold || entry ? (n.getAttribute('aria-label') || n.textContent || n.getAttribute('name') || n.getAttribute('placeholder') || '') : n.className) || '';
|
|
244
|
+
const base = `${tag}|${home && home !== root ? home.id : ''}|${String(label).replace(/\d+/g, '#').trim().slice(0, 80)}`;
|
|
245
|
+
const nth = seen.get(base) || 0;
|
|
246
|
+
seen.set(base, nth + 1);
|
|
247
|
+
keys.set(n, `${base}|${nth}`);
|
|
248
|
+
}
|
|
249
|
+
return keys;
|
|
250
|
+
}
|
|
251
|
+
function capturePlace(root) {
|
|
252
|
+
const place = { x: window.scrollX, y: window.scrollY, folds: new Map(), scrolls: new Map(), focus: null };
|
|
253
|
+
const drawn = drawnFolds.get(root) || new Map();
|
|
254
|
+
for (const [n, key] of placeKeys(root)) {
|
|
255
|
+
if ((n.tagName === 'DETAILS' || (n.tagName === 'BUTTON' && n.hasAttribute('aria-expanded'))) && drawn.has(key) && drawn.get(key) !== foldIsOpen(n)) place.folds.set(key, foldIsOpen(n));
|
|
256
|
+
if (n.scrollTop > 0 || n.scrollLeft > 0) place.scrolls.set(key, [n.scrollLeft, n.scrollTop]);
|
|
257
|
+
if (n === document.activeElement) place.focus = key;
|
|
258
|
+
}
|
|
259
|
+
return place;
|
|
260
|
+
}
|
|
261
|
+
function restorePlace(root, place) {
|
|
262
|
+
restoring = true;
|
|
263
|
+
try {
|
|
264
|
+
for (const [n, key] of placeKeys(root)) {
|
|
265
|
+
if (!place.folds.has(key) || !n.isConnected) continue;
|
|
266
|
+
const was = place.folds.get(key);
|
|
267
|
+
if (foldIsOpen(n) === was) continue;
|
|
268
|
+
if (n.tagName === 'DETAILS') n.open = was;
|
|
269
|
+
else n.click();
|
|
270
|
+
}
|
|
271
|
+
// Fold changes above can add height; positions are restored last, against the final layout.
|
|
272
|
+
for (const [n, key] of placeKeys(root)) {
|
|
273
|
+
const at = place.scrolls.get(key);
|
|
274
|
+
if (at) { n.scrollLeft = at[0]; n.scrollTop = at[1]; }
|
|
275
|
+
if (place.focus === key && typeof n.focus === 'function') n.focus({ preventScroll: true });
|
|
276
|
+
}
|
|
277
|
+
if (window.scrollX !== place.x || window.scrollY !== place.y) window.scrollTo(place.x, place.y);
|
|
278
|
+
} finally { restoring = false; }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The loading placeholder: only ever shown before a page's first real paint, never over real content. */
|
|
282
|
+
export function firstPaint(main, ...kids) {
|
|
283
|
+
if (main.dataset.painted) return;
|
|
284
|
+
main.replaceChildren(...kids.flat(Infinity).filter((k) => k != null && k !== false));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let arrived = new Set();
|
|
288
|
+
/** A new page or link arrived (console route()): its deep-link scroll may run again. */
|
|
289
|
+
export function newArrival() { arrived = new Set(); }
|
|
290
|
+
/** The one way to scroll to a deep-linked section: once per arrival, per key, never on a repaint. */
|
|
291
|
+
export function arrivalScroll(node, { key = 'page', open = false, block = 'start' } = {}) {
|
|
292
|
+
if (!node || arrived.has(key)) return false;
|
|
293
|
+
arrived.add(key);
|
|
294
|
+
if (open) { const head = node.querySelector('button.list-head[aria-expanded="false"]'); if (head) head.click(); }
|
|
295
|
+
if (typeof node.scrollIntoView === 'function') node.scrollIntoView({ block });
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** F9 at the shell: is the operator in the middle of something a repaint would destroy? */
|
|
300
|
+
export function refreshHeld(root) {
|
|
301
|
+
if (!root) return false;
|
|
302
|
+
if (root.querySelector('[data-interaction-open]')) return true;
|
|
303
|
+
const a = document.activeElement;
|
|
304
|
+
if (a && root.contains(a) && (a.tagName === 'TEXTAREA' || a.tagName === 'SELECT' || a.isContentEditable || (a.tagName === 'INPUT' && !/^(button|checkbox|radio|submit|reset|range|color|file)$/.test(a.type)))) return true;
|
|
305
|
+
for (const f of root.querySelectorAll('input, textarea')) {
|
|
306
|
+
if (/^(button|checkbox|radio|submit|reset|range|color|file|hidden)$/.test(f.type || '')) continue;
|
|
307
|
+
if (f.value !== f.defaultValue) return true;
|
|
308
|
+
}
|
|
309
|
+
const sel = typeof window.getSelection === 'function' ? window.getSelection() : null;
|
|
310
|
+
return Boolean(sel && !sel.isCollapsed && sel.anchorNode && root.contains(sel.anchorNode));
|
|
311
|
+
}
|
|
209
312
|
|
|
210
313
|
// ── PIN sheet (00-patterns.md §4: one look, per-action adapters) ───────────
|
|
211
314
|
// adapter 'session': POST /dashboard/unlock → operator-session token, held in page
|
package/dashboard/index.html
CHANGED
|
@@ -10467,9 +10467,11 @@
|
|
|
10467
10467
|
apiFetch('/shared-state/stats').catch(() => null),
|
|
10468
10468
|
]);
|
|
10469
10469
|
const tbody = document.getElementById('ibEntriesTbody');
|
|
10470
|
+
// A read missed by a background refresh keeps the rows on screen (Dashboard UX F12).
|
|
10470
10471
|
if (!recentRes || !recentRes.entries) {
|
|
10471
|
-
tbody.innerHTML = '<tr><td colspan="7" style="padding:14px;color:var(--text-dim);text-align:center">Integrated-Being disabled or unavailable</td></tr>';
|
|
10472
|
+
if (!tbody.dataset.ibRows) tbody.innerHTML = '<tr><td colspan="7" style="padding:14px;color:var(--text-dim);text-align:center">Integrated-Being disabled or unavailable</td></tr>';
|
|
10472
10473
|
} else {
|
|
10474
|
+
tbody.dataset.ibRows = '1';
|
|
10473
10475
|
let entries = recentRes.entries;
|
|
10474
10476
|
if (kind) entries = entries.filter(e => e.kind === kind);
|
|
10475
10477
|
if (entries.length === 0) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "e9eb5bb7f56ce6f81c5f80210c1ec7999d4b8c7d2ff1ab5cfd97a269172ce2ac",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1645",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/audits/phase-b/f10-triage.md",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "0acd00d1676c2247df6020ca0a532c139bd5ad6020cc63f78ef676773d8c1e12",
|
|
3
3
|
"registrySha256": "e9eb5bb7f56ce6f81c5f80210c1ec7999d4b8c7d2ff1ab5cfd97a269172ce2ac",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1645"
|
|
5
5
|
}
|