instar 1.3.1643 → 1.3.1644
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/native/InstarWedgeCanary.app/Contents/MacOS/InstarWedgeCanary +0 -0
- package/dist/native/instar-desktop-host +0 -0
- package/package.json +1 -1
- 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/side-effects/console-refresh-keeps-place.eli16.md +13 -0
- package/upgrades/side-effects/console-refresh-keeps-place.md +78 -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.1644",
|
|
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": "d838f2be89c7527b0f0864e96d4caa6b96c98d1e6a0abde421d0ef686a3c2c5f",
|
|
3
3
|
"registrySha256": "e9eb5bb7f56ce6f81c5f80210c1ec7999d4b8c7d2ff1ab5cfd97a269172ce2ac",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1644"
|
|
5
5
|
}
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-11-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-11-04T03:09:47.613Z",
|
|
5
|
+
"instarVersion": "1.3.1644",
|
|
6
6
|
"entryCount": 206,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -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.1644",
|
|
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": "d838f2be89c7527b0f0864e96d4caa6b96c98d1e6a0abde421d0ef686a3c2c5f",
|
|
3
3
|
"registrySha256": "e9eb5bb7f56ce6f81c5f80210c1ec7999d4b8c7d2ff1ab5cfd97a269172ce2ac",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1644"
|
|
5
5
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The console's 30-second refresh no longer moves the page. The shared repaint step in `dashboard/console/ui.js` now keeps the scroll position, any section the operator opened or closed, inner scroll positions and focus, restoring them before the browser draws. A link to a section scrolls once per arrival. A loading placeholder only shows before a page's first real content, so Promises and any page showing an error no longer blank themselves on refresh. The refresh also waits while a field is focused or holds typed text, or text is selected, and two refreshes can no longer overlap. In the legacy dashboard, a missed Integrated-Being read no longer blanks the table.
|
|
9
|
+
|
|
10
|
+
## What to Tell Your User
|
|
11
|
+
|
|
12
|
+
The dashboard no longer jumps back up the page or closes what you opened when it refreshes. Your place stays put and only the numbers change.
|
|
13
|
+
|
|
14
|
+
## Summary of New Capabilities
|
|
15
|
+
|
|
16
|
+
- Dashboard UX floor F12: a refresh keeps the operator's place, enforced by a test over every console page.
|
|
17
|
+
|
|
18
|
+
## Evidence
|
|
19
|
+
|
|
20
|
+
Measured in a real browser at 1280x728 against this agent's live data, before and after, on all 23 console routes and all 30 legacy tabs. Before: Permission slips 1795 to 169, Passwords and keys 698 to 165, Under the hood 4640 to 1785, Maturation 2930 to 483, Plans (in the way) 4451 to 1559, Costs 1161 to 959, Allowed 2937 to 2827, Machines 98 to 0, Promises 60 to 0 with a blank. After: all of them hold position with every fold still open. On the real 30-second timer, Development with a section link moved 339 to 419 before and held at 491 after. Legacy tabs showed no scroll movement; only Integrated-Being lost content on a poll, and that is fixed. `tests/unit/dashboard-refresh-keeps-place.test.ts` fails when a page is reverted to a placeholder or a raw scroll (checked by mutation).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# A dashboard refresh no longer moves the page
|
|
2
|
+
|
|
3
|
+
**What this changes, in plain words.** Every console page rebuilds itself every 30 seconds to show fresh data. Rebuilding threw away things the person looking at the page had set up. Sections they had opened snapped shut, which made the page shorter, which pulled them up the page. A link to a section scrolled to it again on every refresh, which is the jump the operator reported on the Development page. One page (Promises) blanked itself to a loading message every time, and pages showing an error did the same.
|
|
4
|
+
|
|
5
|
+
Now the shared repaint step notes where the person is (scroll position, open sections, inner scroll positions, which control has focus), rebuilds, and puts all of it back before the browser draws anything. A person watching sees the numbers change and nothing else. A link scrolls once when you arrive. A loading placeholder only ever appears before the first real content. And a refresh waits while someone is typing or has text selected.
|
|
6
|
+
|
|
7
|
+
**How it works without touching each page's logic.** Pages already called one shared function to fill the page, so the place-keeping lives there. Sections are matched by what they say, with numbers ignored, so "3 jobs" becoming "4 jobs" is still the same section. Pages that remember their own open sections simply already agree, and nothing is clicked.
|
|
8
|
+
|
|
9
|
+
**What keeps it from coming back.** A test finds every registered console page, renders each twice the way the real refresh does, and fails if one shows a placeholder or scrolls. It also fails if a page repaints or scrolls around the shared helpers. It carries a count of pages, so if it stops finding them it fails loudly, and it proves on deliberately bad pages that it catches each fault.
|
|
10
|
+
|
|
11
|
+
**What did not change.** No server route, no stored setting, no change to what any page shows or what any button does. Only browser files changed.
|
|
12
|
+
|
|
13
|
+
**If it is wrong.** Revert the commit. There is no stored state or migration. The likely failure is a section reopening that should have stayed shut, or a refresh skipped while a field holds typed text, and neither loses anything.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Side-Effects Review — A dashboard refresh keeps the operator's place
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `console-refresh-keeps-place`
|
|
4
|
+
**Date:** `2026-11-03`
|
|
5
|
+
**Author:** `Echo`
|
|
6
|
+
**Second-pass reviewer:** `not required`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Tier 1: browser-only files, 20 existing files plus a new test, half of it tests; one review round. `dashboard/console/ui.js` gains the place-keeping in `setKids`, plus `firstPaint`, `arrivalScroll`, `newArrival` and `refreshHeld`. `console.js` route() opens an arrival and holds or serializes the refresh tick. Fourteen pages swap their skeleton guard for `firstPaint`, and seven swap a raw scroll for `arrivalScroll`. `ideas.js` and `maturation-board.js` stop bypassing `setKids`. Legacy `index.html` keeps Integrated-Being rows on a missed read. A first version restored every fold and undid tab taps on Development; the existing Development test caught it, and now only folds the operator moved away from how the page drew them are restored.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
One small decision: `refreshHeld` skips a background repaint while a focused or typed field, an `data-interaction-open` marker, or selected text exists on the page. It constrains only the page's own repaint, not information flow, and the next tick tries again.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Over-block
|
|
19
|
+
|
|
20
|
+
A field holding typed text that the operator walked away from holds that page's refresh until it is cleared or they navigate. The page then shows older numbers, with no indicator. Accepted: F9 already chooses keeping typed text over fresh data, and a pre-filled field (value equal to its default) does not hold. Selected text holds the same way.
|
|
21
|
+
|
|
22
|
+
## 2. Under-block
|
|
23
|
+
|
|
24
|
+
A fold whose wording changes beyond numbers loses its match and returns to its default. A page that shrinks for real still moves the viewport (Home moved 1158 to 1119 when a row above went away), which is correct. A failed read on a page still replaces content with its "can't tell" line, without a jump above what the shorter page allows.
|
|
25
|
+
|
|
26
|
+
## 3. Level-of-abstraction fit
|
|
27
|
+
|
|
28
|
+
It belongs in `setKids`, the one call nearly every page already uses to fill `<main>`, so pages need no state of their own. Only `<main>` carries the place; nested repaints are plain replaces so a sub-part does not fight its page.
|
|
29
|
+
|
|
30
|
+
## 4. Signal vs authority compliance
|
|
31
|
+
|
|
32
|
+
Reference: `docs/signal-vs-authority.md`. Display behavior only, with no authority over messages, sessions or actions. The floor test is a CI check on source and rendered pages.
|
|
33
|
+
|
|
34
|
+
## 4b. Judgment-point check (Judgment Within Floors standard)
|
|
35
|
+
|
|
36
|
+
No competing-signals decision point. `refreshHeld` is an F9 floor over directly observed DOM state, not a judgment between live signals.
|
|
37
|
+
|
|
38
|
+
## 5. Interactions
|
|
39
|
+
|
|
40
|
+
- PR 2680 edits the same Development lines and drops the repeated scroll; this PR is rebased on it after it lands and replaces that site with `arrivalScroll`. Its `foldFromHash` stays.
|
|
41
|
+
- A restored `button[aria-expanded]` fold is re-opened through its own click handler, so page state and lazy reads stay right. A lazy fold re-reads on each repaint, which is what an open fold on a refreshed page should do.
|
|
42
|
+
- The Sessions and Files pages keep their own early returns and only repaint their list halves, so they are unaffected.
|
|
43
|
+
- The shared Maturation view also runs on the standalone page, whose `<main>` is covered the same way.
|
|
44
|
+
|
|
45
|
+
## 6. External surfaces
|
|
46
|
+
|
|
47
|
+
Console and legacy dashboard behavior only. No route, message, config or stored state changes.
|
|
48
|
+
|
|
49
|
+
## 6b. Operator-surface quality (Operator-Surface Quality standard)
|
|
50
|
+
|
|
51
|
+
1. **Leads with the primary action?** Unchanged order; this keeps the operator where they are and never scrolls them away from an action. The first-screen rule is the next PR.
|
|
52
|
+
2. **Zero raw internals as primary content?** No content changes.
|
|
53
|
+
3. **Destructive actions de-emphasized?** No control changes.
|
|
54
|
+
4. **Plain language + phone width?** No wording changes. The refresh was run at 390x844 on Permission slips and held position.
|
|
55
|
+
|
|
56
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
57
|
+
|
|
58
|
+
Machine-local by design: per-browser view state in page memory. Nothing to replicate.
|
|
59
|
+
|
|
60
|
+
## 8. Rollback cost
|
|
61
|
+
|
|
62
|
+
Revert the commit and release. Browser files only, with no state or migration.
|
|
63
|
+
|
|
64
|
+
## Conclusion
|
|
65
|
+
|
|
66
|
+
Clear to ship. The riskiest part, fold restore fighting page-owned state, was found by an existing test and is pinned by a new one.
|
|
67
|
+
|
|
68
|
+
## Second-pass review (if required)
|
|
69
|
+
|
|
70
|
+
Not required: no messaging, session lifecycle, gate or sentinel surface.
|
|
71
|
+
|
|
72
|
+
## Evidence pointers
|
|
73
|
+
|
|
74
|
+
`tests/unit/dashboard-refresh-keeps-place.test.ts` with a mutation check (a raw skeleton and a raw scroll each fail it). Browser before and after numbers are in `upgrades/next/console-refresh-keeps-place.md`. The dashboard PIN was never read or used; the browser used the agent's bearer token.
|
|
75
|
+
|
|
76
|
+
## Class-Closure Declaration (display-only mirror)
|
|
77
|
+
|
|
78
|
+
No agent-authored-artifact defect — not applicable.
|