instar 1.3.402 → 1.3.403

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.
@@ -2692,6 +2692,7 @@
2692
2692
  <button class="tab" data-tab="threadline" onclick="switchTab('threadline')">Threadline</button>
2693
2693
  <button class="tab" data-tab="evidence" onclick="switchTab('evidence')">Evidence</button>
2694
2694
  <button class="tab" data-tab="process-health" onclick="switchTab('process-health')">Process Health</button>
2695
+ <button class="tab" data-tab="subscriptions" onclick="switchTab('subscriptions')">Subscriptions</button>
2695
2696
  <button class="tab" data-tab="preferences-learning" onclick="switchTab('preferences-learning')">Preferences</button>
2696
2697
  <button class="tab" data-tab="machines" onclick="switchTab('machines')">Machines</button>
2697
2698
  <button class="tab" data-tab="mandates" onclick="switchTab('mandates')">Mandates</button>
@@ -3243,6 +3244,44 @@
3243
3244
  </details>
3244
3245
  </div>
3245
3246
 
3247
+ <!-- subscriptions tab (Subscription & Auth Standard P2.2 — live quota + pending logins) -->
3248
+ <style>
3249
+ .sub-account { border:1px solid var(--border,#2a2a2a); border-radius:10px; padding:14px 16px; margin-bottom:12px; }
3250
+ .sub-account-head { display:flex; justify-content:space-between; align-items:baseline; gap:12px; }
3251
+ .sub-account-nick { font-weight:600; font-size:15px; }
3252
+ .sub-account-status { font-size:12px; opacity:.75; }
3253
+ .sub-account-meta { font-size:12px; opacity:.6; margin:2px 0 10px; }
3254
+ .sub-account-noquota { font-size:12px; opacity:.6; }
3255
+ .sub-quota { margin:8px 0; }
3256
+ .sub-quota-head { display:flex; justify-content:space-between; font-size:12px; margin-bottom:4px; }
3257
+ .sub-quota-label { opacity:.8; }
3258
+ .sub-quota-pct { opacity:.65; }
3259
+ .sub-quota-track { height:8px; border-radius:4px; background:var(--border,#2a2a2a); overflow:hidden; }
3260
+ .sub-quota-fill { height:100%; background:linear-gradient(90deg,#4caf80,#3a9); }
3261
+ .sub-pending { border:1px solid var(--border,#2a2a2a); border-radius:10px; padding:12px 16px; margin-bottom:10px; }
3262
+ .sub-pending-head { display:flex; justify-content:space-between; align-items:baseline; gap:12px; }
3263
+ .sub-pending-label { font-weight:600; }
3264
+ .sub-pending-ttl { font-size:12px; opacity:.7; }
3265
+ .sub-pending-code { font-family:monospace; font-size:14px; margin-top:6px; }
3266
+ .sub-pending-url { font-size:12px; opacity:.7; word-break:break-all; margin-top:4px; }
3267
+ .sub-pending-reissue { font-size:11px; opacity:.55; margin-top:4px; }
3268
+ .sub-empty, .sub-disabled { font-size:13px; opacity:.7; padding:8px 0; }
3269
+ </style>
3270
+ <div id="subscriptionsPanel" class="ph-root" style="display:none;flex-direction:column;padding:28px;gap:24px;overflow-y:auto">
3271
+ <h2 class="ph-title">Subscriptions</h2>
3272
+ <p class="ph-intro">Your subscription accounts and how much of each is left before it resets — plus any logins waiting for you to approve on your phone.</p>
3273
+ <section class="ph-section">
3274
+ <h3 class="ph-h">Accounts</h3>
3275
+ <p class="ph-h-sub">Each account’s live usage (5-hour and weekly) and when it resets.</p>
3276
+ <div id="subAccounts"></div>
3277
+ </section>
3278
+ <section class="ph-section">
3279
+ <h3 class="ph-h">Pending logins</h3>
3280
+ <p class="ph-h-sub">Logins waiting for approval — open the link on your phone and confirm. An expired code is re-issued automatically.</p>
3281
+ <div id="subPending"></div>
3282
+ </section>
3283
+ </div>
3284
+
3246
3285
  <!-- mandates tab (coordination-mandate spec, decision 2A — the operator's PIN-gated surface) -->
3247
3286
  <div id="mandatesPanel" class="ph-root" style="display:none;flex-direction:column;padding:28px;gap:24px;overflow-y:auto">
3248
3287
  <div style="display:flex;justify-content:space-between;align-items:baseline">
@@ -4650,6 +4689,13 @@
4650
4689
  onActivate: () => { if (typeof startProcessHealth === 'function') startProcessHealth(); },
4651
4690
  onDeactivate: () => { if (typeof stopProcessHealth === 'function') stopProcessHealth(); },
4652
4691
  },
4692
+ {
4693
+ id: 'subscriptions',
4694
+ panels: ['subscriptionsPanel'],
4695
+ display: ['flex'],
4696
+ onActivate: () => { if (typeof startSubscriptions === 'function') startSubscriptions(); },
4697
+ onDeactivate: () => { if (typeof stopSubscriptions === 'function') stopSubscriptions(); },
4698
+ },
4653
4699
  {
4654
4700
  id: 'preferences-learning',
4655
4701
  panels: ['preferencesLearningPanel'],
@@ -8602,6 +8648,34 @@
8602
8648
  if (__phController) __phController.stop();
8603
8649
  }
8604
8650
 
8651
+ // ── Subscriptions tab (Subscription & Auth Standard P2.2) ──
8652
+ // Same lazy dynamic-import pattern as Process Health: the controller lives in
8653
+ // this (classic) scope so it can read `token`; the module ships the pure
8654
+ // renderers + the polling controller.
8655
+ let __subController = null;
8656
+ let __subModule = null;
8657
+ async function startSubscriptions() {
8658
+ if (!__subModule) {
8659
+ try { __subModule = await import('/dashboard/subscriptions.js'); }
8660
+ catch (e) { console.error('[subscriptions] module load failed', e); return; }
8661
+ }
8662
+ if (!__subController) {
8663
+ const els = {
8664
+ accounts: document.getElementById('subAccounts'),
8665
+ pending: document.getElementById('subPending'),
8666
+ };
8667
+ const fetchImpl = (url, opts = {}) => fetch(url, {
8668
+ headers: { 'Authorization': `Bearer ${token}`, ...(opts.headers || {}) },
8669
+ signal: opts.signal,
8670
+ });
8671
+ __subController = __subModule.createController({ doc: document, els, fetchImpl });
8672
+ }
8673
+ __subController.start();
8674
+ }
8675
+ function stopSubscriptions() {
8676
+ if (__subController) __subController.stop();
8677
+ }
8678
+
8605
8679
  // ── Mandates tab (coordination-mandate spec, decision 2A) ──
8606
8680
  // Same lazy dynamic-import pattern as Process Health. The PIN typed in this
8607
8681
  // tab is sent once per action and never stored anywhere.
@@ -0,0 +1,264 @@
1
+ // Subscriptions tab — a read surface for the multi-account Subscription & Auth
2
+ // pool: per-account live quota bars (5h / weekly + reset countdown), status, and
3
+ // the Pending Logins panel (device codes / verification URLs awaiting approval,
4
+ // with TTL). Spec: docs/specs/_drafts/subscription-auth-standard-master-spec.md.
5
+ //
6
+ // Browser-native ESM (no build step; served at /dashboard/subscriptions.js and
7
+ // loaded by index.html via <script type="module">). The pure functions are
8
+ // exported so the 3-tier jsdom tests exercise the SHIPPED code, not a copy; the
9
+ // controller is attached to window.Subscriptions so index.html drives start/stop
10
+ // on tab activation.
11
+ //
12
+ // Load-bearing safety contract (mirrors the Process Health tab §4.6): every
13
+ // dynamic value flows through sanitizeForDisplay before the DOM; all DOM writes
14
+ // are textContent only (never innerHTML); the only dynamic ATTRIBUTE written is a
15
+ // quota-bar width, set from a clamped NUMBER (0–100) — never a string from data.
16
+ // No verification URL is ever rendered as a live href (defense-in-depth): it is
17
+ // shown as sanitized TEXT for the operator to copy.
18
+
19
+ const CAPS = { label: 64, code: 48, url: 320, summary: 240 };
20
+
21
+ // Structural presentation-glyph class (NFKC-fold THEN strip), identical to the
22
+ // Process Health tab: \p{So} + arrows + geometric + box-drawing + dingbats +
23
+ // variation-selectors + bullet/middot — so a confusable can't impersonate chrome.
24
+ const CHROME_GLYPH_RE = new RegExp(
25
+ '[\\p{So}\\u2190-\\u21FF\\u25A0-\\u25FF\\u2500-\\u257F\\u2700-\\u27BF\\uFE00-\\uFE0F\\u2022\\u00B7\\u2027\\u2043]',
26
+ 'gu',
27
+ );
28
+ const CONTROL_RE = new RegExp('[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F\\u007F-\\u009F]', 'g');
29
+ const BIDI_RE = new RegExp('[\\u202A-\\u202E\\u2066-\\u2069]', 'g');
30
+
31
+ /** Sanitize a dynamic value before it touches the DOM (see contract above). */
32
+ export function sanitizeForDisplay(value, fieldKind = 'summary') {
33
+ let s = value == null ? '' : String(value);
34
+ s = s.normalize('NFKC');
35
+ s = s.replace(CONTROL_RE, '');
36
+ s = s.replace(BIDI_RE, '');
37
+ s = s.replace(/\n{2,}/g, '\n').replace(/[ \t]{5,}/g, ' ');
38
+ s = s.replace(CHROME_GLYPH_RE, '');
39
+ s = capGraphemes(s, CAPS[fieldKind] ?? CAPS.summary);
40
+ return s;
41
+ }
42
+
43
+ function capGraphemes(s, max) {
44
+ if (s.length <= max) return s;
45
+ if (typeof Intl !== 'undefined' && Intl.Segmenter) {
46
+ const arr = Array.from(new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(s), (x) => x.segment);
47
+ if (arr.length <= max) return s;
48
+ return arr.slice(0, max - 1).join('') + '…';
49
+ }
50
+ let cut = max - 1;
51
+ const c = s.charCodeAt(cut - 1);
52
+ if (c >= 0xd800 && c <= 0xdbff) cut -= 1;
53
+ return s.slice(0, cut) + '…';
54
+ }
55
+
56
+ /** Clamp a utilization value to an integer 0–100 (the only dynamic attribute). */
57
+ export function clampPct(value) {
58
+ const n = Number(value);
59
+ if (!Number.isFinite(n)) return 0;
60
+ return Math.max(0, Math.min(100, Math.round(n)));
61
+ }
62
+
63
+ const STATUS_WORDS = {
64
+ active: 'Active',
65
+ warming: 'Warming up',
66
+ 'rate-limited': 'At its limit',
67
+ 'needs-reauth': 'Needs sign-in',
68
+ disabled: 'Disabled',
69
+ };
70
+ export function friendlyStatus(status) {
71
+ return STATUS_WORDS[typeof status === 'string' ? status : ''] || 'Unknown';
72
+ }
73
+
74
+ const PROVIDER_WORDS = { anthropic: 'Claude', openai: 'Codex', 'github-copilot': 'Copilot', google: 'Gemini' };
75
+ export function friendlyProvider(provider) {
76
+ return PROVIDER_WORDS[typeof provider === 'string' ? provider : ''] || sanitizeForDisplay(provider, 'label');
77
+ }
78
+
79
+ /** Human countdown to a reset/expiry instant: "resets in 2h 15m" / "expired". */
80
+ export function countdown(iso, now = Date.now(), { expiredWord = 'expired' } = {}) {
81
+ const t = typeof iso === 'string' ? Date.parse(iso) : NaN;
82
+ if (Number.isNaN(t)) return '';
83
+ const sec = Math.floor((t - now) / 1000);
84
+ if (sec <= 0) return expiredWord;
85
+ const hr = Math.floor(sec / 3600);
86
+ const min = Math.floor((sec % 3600) / 60);
87
+ if (hr >= 24) { const d = Math.floor(hr / 24); return `${d}d ${hr % 24}h`; }
88
+ if (hr >= 1) return `${hr}h ${min}m`;
89
+ if (min >= 1) return `${min}m`;
90
+ return `${sec}s`;
91
+ }
92
+
93
+ // ── DOM helpers (textContent ONLY — never innerHTML) ────────────────────────
94
+ function el(doc, tag, cls, text) {
95
+ const node = doc.createElement(tag);
96
+ if (cls) node.setAttribute('class', cls); // static literal
97
+ if (text != null) node.textContent = text; // dynamic text → textContent ONLY
98
+ return node;
99
+ }
100
+
101
+ /** A labelled quota bar. `pct` is clamped to a 0–100 NUMBER before it reaches the
102
+ * only dynamic attribute (style width); the percent text is also from that number. */
103
+ export function quotaBar(doc, label, pct, resetIso, now = Date.now()) {
104
+ const wrap = el(doc, 'div', 'sub-quota');
105
+ const used = clampPct(pct);
106
+ const head = el(doc, 'div', 'sub-quota-head');
107
+ head.appendChild(el(doc, 'span', 'sub-quota-label', sanitizeForDisplay(label, 'label')));
108
+ const resetTxt = resetIso ? countdown(resetIso, now, { expiredWord: 'resetting' }) : '';
109
+ head.appendChild(el(doc, 'span', 'sub-quota-pct', `${used}% used${resetTxt ? ` · resets in ${resetTxt}` : ''}`));
110
+ wrap.appendChild(head);
111
+ const track = el(doc, 'div', 'sub-quota-track');
112
+ const fill = el(doc, 'div', 'sub-quota-fill');
113
+ fill.style.width = `${used}%`; // safe: `used` is a clamped integer
114
+ track.appendChild(fill);
115
+ wrap.appendChild(track);
116
+ return wrap;
117
+ }
118
+
119
+ /** Per-account rows: nickname, status, provider·framework, 5h + weekly quota bars. */
120
+ export function renderAccounts(doc, target, accounts, now = Date.now()) {
121
+ if (!target) return;
122
+ target.replaceChildren();
123
+ if (!Array.isArray(accounts) || accounts.length === 0) {
124
+ target.appendChild(el(doc, 'div', 'sub-empty', 'No subscription accounts enrolled yet.'));
125
+ return;
126
+ }
127
+ for (const a of accounts) {
128
+ const card = el(doc, 'div', 'sub-account');
129
+ const head = el(doc, 'div', 'sub-account-head');
130
+ head.appendChild(el(doc, 'span', 'sub-account-nick', sanitizeForDisplay(a && a.nickname, 'label')));
131
+ head.appendChild(el(doc, 'span', 'sub-account-status', friendlyStatus(a && a.status)));
132
+ card.appendChild(head);
133
+ card.appendChild(el(doc, 'div', 'sub-account-meta',
134
+ `${friendlyProvider(a && a.provider)} · ${sanitizeForDisplay(a && a.framework, 'label')}`));
135
+ const q = (a && a.lastQuota) || null;
136
+ if (q && (q.fiveHour || q.sevenDay)) {
137
+ if (q.fiveHour) card.appendChild(quotaBar(doc, '5-hour', q.fiveHour.utilizationPct, q.fiveHour.resetsAt, now));
138
+ if (q.sevenDay) card.appendChild(quotaBar(doc, 'Weekly', q.sevenDay.utilizationPct, q.sevenDay.resetsAt, now));
139
+ } else {
140
+ card.appendChild(el(doc, 'div', 'sub-account-noquota', 'No quota reading yet.'));
141
+ }
142
+ target.appendChild(card);
143
+ }
144
+ }
145
+
146
+ /** Pending Logins panel: device code / verification URL (as TEXT) + TTL + reissues. */
147
+ export function renderPendingLogins(doc, target, logins, now = Date.now()) {
148
+ if (!target) return;
149
+ target.replaceChildren();
150
+ if (!Array.isArray(logins) || logins.length === 0) {
151
+ target.appendChild(el(doc, 'div', 'sub-empty', 'No logins waiting for approval.'));
152
+ return;
153
+ }
154
+ for (const l of logins) {
155
+ const row = el(doc, 'div', 'sub-pending');
156
+ const head = el(doc, 'div', 'sub-pending-head');
157
+ head.appendChild(el(doc, 'span', 'sub-pending-label', sanitizeForDisplay(l && l.label, 'label')));
158
+ const ttl = l && l.ttlExpiresAt ? countdown(l.ttlExpiresAt, now) : '';
159
+ head.appendChild(el(doc, 'span', 'sub-pending-ttl', ttl ? `expires in ${ttl}` : 'expired'));
160
+ row.appendChild(head);
161
+ if (l && l.userCode) {
162
+ row.appendChild(el(doc, 'div', 'sub-pending-code', `Code: ${sanitizeForDisplay(l.userCode, 'code')}`));
163
+ }
164
+ // Verification URL shown as TEXT for the operator to copy — never a live href.
165
+ row.appendChild(el(doc, 'div', 'sub-pending-url', sanitizeForDisplay(l && l.verificationUrl, 'url')));
166
+ const rc = l && Number(l.reissueCount);
167
+ if (Number.isFinite(rc) && rc > 0) {
168
+ row.appendChild(el(doc, 'div', 'sub-pending-reissue', `Re-issued ${rc} time${rc === 1 ? '' : 's'}`));
169
+ }
170
+ target.appendChild(row);
171
+ }
172
+ }
173
+
174
+ export function renderDisabled(doc, els) {
175
+ if (els && els.accounts) {
176
+ els.accounts.replaceChildren(
177
+ el(doc, 'div', 'sub-disabled', 'The subscription pool isn’t set up yet. Enroll an account to get started.'),
178
+ );
179
+ }
180
+ if (els && els.pending) els.pending.replaceChildren();
181
+ }
182
+
183
+ // ── Controller (fetch /subscription-pool + /pending-logins, render) ─────────
184
+ const URLS = {
185
+ accounts: '/subscription-pool',
186
+ pending: '/subscription-pool/pending-logins',
187
+ };
188
+
189
+ export function createController(opts) {
190
+ const {
191
+ doc,
192
+ els = {},
193
+ fetchImpl,
194
+ now = () => Date.now(),
195
+ cadenceMs = 30_000,
196
+ schedule = (fn, ms) => setTimeout(fn, ms),
197
+ cancel = (id) => clearTimeout(id),
198
+ } = opts;
199
+
200
+ const state = { timerId: null, active: false, inFlight: null };
201
+
202
+ async function fetchJson(url, controller) {
203
+ const resp = await fetchImpl(url, { signal: controller.signal });
204
+ if (!resp.ok) throw new Error(`${url} ${resp.status}`);
205
+ return resp.json();
206
+ }
207
+
208
+ async function tick() {
209
+ if (!state.active) return;
210
+ if (state.inFlight) { try { state.inFlight.abort(); } catch { /* superseded */ } }
211
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : { signal: undefined, abort() {} };
212
+ state.inFlight = controller;
213
+ let accountsBody, pendingBody;
214
+ try {
215
+ [accountsBody, pendingBody] = await Promise.all([
216
+ fetchJson(URLS.accounts, controller),
217
+ fetchJson(URLS.pending, controller),
218
+ ]);
219
+ } catch {
220
+ if (controller.signal && controller.signal.aborted) return;
221
+ state.inFlight = null;
222
+ reschedule();
223
+ return;
224
+ }
225
+ if (controller.signal && controller.signal.aborted) return;
226
+ state.inFlight = null;
227
+ // Feature dark → both routes answer { enabled:false }. Show the friendly copy.
228
+ if (accountsBody && accountsBody.enabled === false && pendingBody && pendingBody.enabled === false) {
229
+ renderDisabled(doc, els);
230
+ reschedule();
231
+ return;
232
+ }
233
+ render(accountsBody, pendingBody);
234
+ reschedule();
235
+ }
236
+
237
+ function render(accountsBody, pendingBody) {
238
+ const accounts = accountsBody && Array.isArray(accountsBody.accounts) ? accountsBody.accounts : [];
239
+ const logins = pendingBody && Array.isArray(pendingBody.logins) ? pendingBody.logins : [];
240
+ renderAccounts(doc, els.accounts, accounts, now());
241
+ renderPendingLogins(doc, els.pending, logins, now());
242
+ }
243
+
244
+ function reschedule() {
245
+ if (!state.active) return;
246
+ if (state.timerId != null) cancel(state.timerId);
247
+ state.timerId = schedule(() => { void tick(); }, cadenceMs);
248
+ }
249
+
250
+ function start() { if (state.active) return; state.active = true; void tick(); }
251
+ function stop() {
252
+ state.active = false;
253
+ if (state.timerId != null) { cancel(state.timerId); state.timerId = null; }
254
+ if (state.inFlight) { try { state.inFlight.abort(); } catch { /* ignore */ } state.inFlight = null; }
255
+ }
256
+ function onVisible() { if (!state.active) start(); }
257
+ function onHidden() { stop(); }
258
+
259
+ return { start, stop, onVisible, onHidden, tick, render, _state: state };
260
+ }
261
+
262
+ if (typeof window !== 'undefined') {
263
+ window.Subscriptions = { createController, sanitizeForDisplay, renderAccounts, renderPendingLogins };
264
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.402",
3
+ "version": "1.3.403",
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-06-07T08:26:35.260Z",
5
- "instarVersion": "1.3.402",
4
+ "generatedAt": "2026-06-07T08:45:36.629Z",
5
+ "instarVersion": "1.3.403",
6
6
  "entryCount": 199,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,50 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: minor -->
5
+
6
+ ## What Changed
7
+
8
+ Added a **Subscriptions** tab to the dashboard — the visual surface for the
9
+ multi-account Subscription & Auth pool (P1.1–P2.1). It shows, per account: the
10
+ nickname, a friendly status, and live quota bars for the 5-hour and weekly
11
+ windows with a "resets in …" countdown. A **Pending Logins** panel below lists
12
+ any in-flight enrollment — the device code / verification URL (shown as copyable
13
+ text, never a live link) with its TTL countdown and how many times it has been
14
+ re-issued.
15
+
16
+ The tab is a self-contained browser-native ESM module (`dashboard/subscriptions.js`)
17
+ served statically, registered in `index.html` exactly like the Process Health and
18
+ Preferences tabs (lazy dynamic-import, start/stop on tab activation, polled every
19
+ 30s). It consumes the existing `GET /subscription-pool` and
20
+ `GET /subscription-pool/pending-logins` routes — no new server routes, no `src/`
21
+ change. When the pool isn't set up the routes answer `{ enabled:false }` and the
22
+ tab shows a friendly "not set up yet" message (never an error).
23
+
24
+ It carries the same load-bearing display-safety contract as the Process Health
25
+ tab: every dynamic value is sanitized (NFKC fold + control/bidi/chrome-glyph
26
+ strip + grapheme cap) before the DOM, all writes are `textContent` only, and the
27
+ only dynamic attribute (a quota-bar width) comes from a clamped 0–100 integer.
28
+
29
+ Coverage: 26 tests across three tiers — render unit tests (sanitize/clamp/
30
+ countdown/renderers + XSS-survives-as-inert-text), a controller integration test
31
+ (jsdom + injected fetch/timers: render, feature-dark, XSS through the controller,
32
+ fetch-failure resilience, visibility gating), and an e2e feature-alive test that
33
+ boots a real server with the production routes and drives the shipped controller
34
+ against it (accounts + pending logins render; feature-off → friendly copy).
35
+
36
+ ## What to Tell Your User
37
+
38
+ There's a new **Subscriptions** tab on your dashboard. It shows each of your
39
+ subscription accounts, how much of each is left (5-hour and weekly) and when it
40
+ resets, and any logins still waiting for you to approve on your phone — with the
41
+ code and a countdown. Open it from any device with your dashboard PIN.
42
+
43
+ ## Summary of New Capabilities
44
+
45
+ - **Subscriptions dashboard tab** — per-account live quota bars (5h + weekly) with
46
+ reset countdowns + friendly status, on any device behind the dashboard PIN.
47
+ - **Pending Logins panel** — in-flight enrollment codes / verification URLs (as
48
+ copyable text) with TTL countdown + re-issue count.
49
+ - **Mobile-responsive + safe** — sanitized, `textContent`-only rendering; no live
50
+ links; reuses the Process Health tab's hardened display contract.