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.
Files changed (32) hide show
  1. package/dashboard/console/accounts.js +3 -3
  2. package/dashboard/console/agents.js +2 -2
  3. package/dashboard/console/allowed.js +3 -4
  4. package/dashboard/console/console.js +8 -3
  5. package/dashboard/console/costs.js +2 -2
  6. package/dashboard/console/development.js +3 -3
  7. package/dashboard/console/ideas.js +3 -3
  8. package/dashboard/console/inside.js +4 -9
  9. package/dashboard/console/learned.js +2 -2
  10. package/dashboard/console/machines.js +2 -2
  11. package/dashboard/console/mandates.js +2 -2
  12. package/dashboard/console/maturation-board.js +6 -7
  13. package/dashboard/console/messages.js +2 -2
  14. package/dashboard/console/plans.js +3 -6
  15. package/dashboard/console/promises.js +3 -6
  16. package/dashboard/console/schedule.js +3 -6
  17. package/dashboard/console/secrets.js +2 -2
  18. package/dashboard/console/ui.js +104 -1
  19. package/dashboard/index.html +3 -1
  20. package/dist/data/standards-guard-index.json +1 -1
  21. package/dist/data/standards-guard-index.meta.json +2 -2
  22. package/dist/data/standards-registry.meta.json +1 -1
  23. package/dist/native/InstarWedgeCanary.app/Contents/MacOS/InstarWedgeCanary +0 -0
  24. package/dist/native/instar-desktop-host +0 -0
  25. package/package.json +1 -1
  26. package/src/data/builtin-manifest.json +2 -2
  27. package/src/data/standards-guard-index.json +1 -1
  28. package/src/data/standards-guard-index.meta.json +2 -2
  29. package/src/data/standards-registry.meta.json +1 -1
  30. package/upgrades/1.3.1644.md +20 -0
  31. package/upgrades/side-effects/console-refresh-keeps-place.eli16.md +13 -0
  32. 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
- if (!main.querySelector('.acct-list')) setKids(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: 'Capacity · Accounts' }), el('h1', { class: 'page-title skeleton', text: 'Loading accounts' })));
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' && needs) needs.scrollIntoView({ block: 'start' });
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
- if (!main.querySelector('.agents-page')) setKids(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: EYEBROW }), el('h1', { class: 'page-title skeleton', text: 'Loading other agents' })));
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
- if (!main.querySelector('.allowed-page')) setKids(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' })));
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
- const sec = main.querySelector(`#sec-${want}`);
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 === 'visible' && !sheetIsOpen() && (location.hash.replace(/^#\/?/, '').split('?')[0]) === path) NATIVE[path](main, { get, post, getCommitments });
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
- if (!main.querySelector('.costs-page')) setKids(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: EYEBROW }), el('h1', { class: 'page-title skeleton', text: 'Loading what thinking costs' })));
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
- setKids(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: EYEBROW }), el('h1', { class: 'page-title skeleton', text: 'Loading development' })));
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
- if (sec) sec.scrollIntoView({ block: 'start' });
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.replaceChildren(r.reason === 'switched off on this agent'
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.replaceChildren(el('div', { class: 'ideas-page' }, parts));
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
- if (!main.querySelector('.inside-page')) setKids(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: EYEBROW }), el('h1', { class: 'page-title skeleton', text: 'Loading' })));
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 && scrolledFor !== want) { scrolledFor = want; const sec = main.querySelector(`#sec-${want}`); if (sec) { openFold(sec); sec.scrollIntoView({ block: 'start' }); } }
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; scrolledFor = 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
- if (!main.querySelector('.learned-page')) setKids(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' })));
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
- if (!main.querySelector('.machine-cards')) setKids(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: 'Capacity · Machines' }), el('h1', { class: 'page-title skeleton', text: 'Loading machines' })));
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
- if (!main.querySelector('.slips-page')) setKids(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: 'People · Permission slips' }), el('h1', { class: 'page-title skeleton', text: 'Loading' })));
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.replaceChildren(el('p', { class: 'mb-loading', text: 'Reading every machine…' }));
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.replaceChildren(el('section', { class: 'mb-hero' },
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.replaceChildren(cantTell(r.reason)); return; }
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.replaceChildren(hero, glance, tools,
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
- const target = main.querySelector('.mb-row[aria-expanded="true"]');
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) setKids(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: 'People · Messages to you' }), el('h1', { class: 'page-title skeleton', text: 'Loading' })));
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
- if (!main.querySelector('.plans-page')) setKids(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: EYEBROW }), el('h1', { class: 'page-title skeleton', text: 'Loading plans' })));
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
- const sec = main.querySelector(`#sec-${want}`);
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
- setKids(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: 'Work · Promises' }), el('h1', { class: 'page-title skeleton', text: 'Loading promises' })));
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
- const sec = main.querySelector(`#sec-${want}`);
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
- if (!main.querySelector('.schedule-page')) setKids(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: EYEBROW }), el('h1', { class: 'page-title skeleton', text: 'Loading the schedule' })));
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
- const sec = main.querySelector(`#sec-${want}`);
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
- if (!main.querySelector('.secrets-page')) setKids(main, el('section', { class: 'hero page-hero' }, el('div', { class: 'eyebrow', text: 'People · Passwords & keys' }), el('h1', { class: 'page-title skeleton', text: 'Loading' })));
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
  ]);
@@ -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) { node.replaceChildren(...kids.flat(Infinity).filter((k) => k != null && k !== false)); }
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
@@ -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.1643",
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": "7b7c5c131b95619879efffeefa2814a65af619c1ae31a4efd1622e8e16245651",
2
+ "sha256": "d838f2be89c7527b0f0864e96d4caa6b96c98d1e6a0abde421d0ef686a3c2c5f",
3
3
  "registrySha256": "e9eb5bb7f56ce6f81c5f80210c1ec7999d4b8c7d2ff1ab5cfd97a269172ce2ac",
4
- "packageVersion": "1.3.1643"
4
+ "packageVersion": "1.3.1644"
5
5
  }
@@ -2,5 +2,5 @@
2
2
  "sha256": "e9eb5bb7f56ce6f81c5f80210c1ec7999d4b8c7d2ff1ab5cfd97a269172ce2ac",
3
3
  "articleCount": 93,
4
4
  "generatedFrom": "docs/STANDARDS-REGISTRY.md",
5
- "packageVersion": "1.3.1643"
5
+ "packageVersion": "1.3.1644"
6
6
  }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.1643",
3
+ "version": "1.3.1644",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-11-04T02:44:31.654Z",
5
- "instarVersion": "1.3.1643",
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.1643",
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": "7b7c5c131b95619879efffeefa2814a65af619c1ae31a4efd1622e8e16245651",
2
+ "sha256": "d838f2be89c7527b0f0864e96d4caa6b96c98d1e6a0abde421d0ef686a3c2c5f",
3
3
  "registrySha256": "e9eb5bb7f56ce6f81c5f80210c1ec7999d4b8c7d2ff1ab5cfd97a269172ce2ac",
4
- "packageVersion": "1.3.1643"
4
+ "packageVersion": "1.3.1644"
5
5
  }
@@ -2,5 +2,5 @@
2
2
  "sha256": "e9eb5bb7f56ce6f81c5f80210c1ec7999d4b8c7d2ff1ab5cfd97a269172ce2ac",
3
3
  "articleCount": 93,
4
4
  "generatedFrom": "docs/STANDARDS-REGISTRY.md",
5
- "packageVersion": "1.3.1643"
5
+ "packageVersion": "1.3.1644"
6
6
  }
@@ -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.