instar 1.3.623 → 1.3.625

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.
@@ -3393,6 +3393,19 @@
3393
3393
  .sub-pending-url { font-size:12px; opacity:.7; word-break:break-all; margin-top:4px; }
3394
3394
  .sub-pending-reissue { font-size:11px; opacity:.55; margin-top:4px; }
3395
3395
  .sub-empty, .sub-disabled { font-size:13px; opacity:.7; padding:8px 0; }
3396
+ /* account-machine-matrix grid */
3397
+ .sub-matrix { border-collapse:collapse; width:100%; font-size:13px; }
3398
+ .sub-matrix th, .sub-matrix td { border:1px solid var(--border,#2a2a2a); padding:8px 10px; text-align:center; vertical-align:top; }
3399
+ .sub-matrix-acct { text-align:left; font-weight:600; word-break:break-all; }
3400
+ .sub-matrix-mach { font-weight:600; }
3401
+ .sub-matrix-off { opacity:.55; }
3402
+ .sub-matrix-cell.sub-matrix-offline { opacity:.5; }
3403
+ .sub-matrix-glyph { font-size:13px; }
3404
+ .sub-matrix-setup, .sub-matrix-confirm, .sub-matrix-code-submit { margin-top:4px; padding:4px 10px; cursor:pointer; }
3405
+ .sub-matrix-pin, .sub-matrix-code-input { display:block; margin:4px auto; padding:3px 6px; width:90%; box-sizing:border-box; }
3406
+ .sub-matrix-status { font-size:11px; opacity:.8; margin-top:4px; }
3407
+ .sub-matrix-signin { display:inline-block; margin-top:4px; }
3408
+ .sub-matrix-url { font-size:11px; opacity:.7; word-break:break-all; margin-top:4px; }
3396
3409
  </style>
3397
3410
  <div id="subscriptionsPanel" class="ph-root" style="display:none;flex-direction:column;padding:28px;gap:24px;overflow-y:auto">
3398
3411
  <h2 class="ph-title">Subscriptions</h2>
@@ -3401,6 +3414,14 @@
3401
3414
  your subscriptions (ws52-operator-tap-not-text Part A). Leads the panel as the primary
3402
3415
  action when present; silent otherwise. -->
3403
3416
  <div id="subFollowMe"></div>
3417
+ <!-- Account × Machine matrix (account-machine-matrix) — at-a-glance "which account is set up
3418
+ on which machine," with a one-tap "Set up" per empty cell that runs the whole sign-in in
3419
+ the dashboard (PIN → sign-in link → paste the code). Hidden when there's nothing to show. -->
3420
+ <section class="ph-section">
3421
+ <h3 class="ph-h">Accounts on each machine</h3>
3422
+ <p class="ph-h-sub">Which subscription is set up on which machine. Tap “Set up” on an empty cell to add one — you’ll enter your PIN, open the sign-in link, and paste the code right here.</p>
3423
+ <div id="subMatrix"></div>
3424
+ </section>
3404
3425
  <section class="ph-section">
3405
3426
  <h3 class="ph-h">Accounts</h3>
3406
3427
  <p class="ph-h-sub">Each account’s live usage (5-hour and weekly) and when it resets.</p>
@@ -9157,6 +9178,7 @@
9157
9178
  accounts: document.getElementById('subAccounts'),
9158
9179
  pending: document.getElementById('subPending'),
9159
9180
  followMe: document.getElementById('subFollowMe'),
9181
+ matrix: document.getElementById('subMatrix'),
9160
9182
  };
9161
9183
  // Pass opts through (method/body/signal) so the controller can POST the follow-me
9162
9184
  // scan + the PIN-gated issue-for-machine, not just GET.
@@ -271,6 +271,9 @@ export function renderPendingLogins(doc, target, logins, now = Date.now()) {
271
271
  }
272
272
  for (const l of logins) {
273
273
  const row = el(doc, 'div', 'sub-pending');
274
+ // Non-sensitive identifiers for the code-submit handler (never a credential).
275
+ row.setAttribute('data-login-id', sanitizeForDisplay(l && l.id, 'label'));
276
+ if (l && (l.machineId || l.machineNickname)) row.setAttribute('data-machine-id', sanitizeForDisplay(l.machineId, 'label'));
274
277
 
275
278
  // Lead with a plain-language headline naming what to do + where (machine).
276
279
  const machine = sanitizeForDisplay(l && (l.machineNickname || l.machineId), 'label');
@@ -300,6 +303,22 @@ export function renderPendingLogins(doc, target, logins, now = Date.now()) {
300
303
  row.appendChild(el(doc, 'div', 'sub-pending-code', `Code: ${sanitizeForDisplay(l.userCode, 'code')}`));
301
304
  }
302
305
 
306
+ // url-code-paste flow (Claude): after signing in, the provider hands the operator a
307
+ // CODE to paste back. Give them a field for it right here — so it goes straight to the
308
+ // machine doing the login (off-chat), not relayed by hand. (ws52-code-paste-back)
309
+ if (l && l.kind === 'url-code-paste') {
310
+ row.appendChild(el(doc, 'div', 'sub-pending-codehint', 'After you sign in, paste the code the page gives you here:'));
311
+ const input = doc.createElement('input');
312
+ input.setAttribute('type', 'text');
313
+ input.setAttribute('class', 'sub-pending-code-input');
314
+ input.setAttribute('placeholder', 'Paste your sign-in code');
315
+ input.setAttribute('autocomplete', 'off');
316
+ row.appendChild(input);
317
+ const submit = el(doc, 'button', 'sub-pending-code-submit', 'Submit code');
318
+ submit.setAttribute('data-submit-code', '1');
319
+ row.appendChild(submit);
320
+ }
321
+
303
322
  // One short secondary line: the TTL, and the flow notice only if present (trimmed).
304
323
  const ttl = l && l.ttlExpiresAt ? countdown(l.ttlExpiresAt, now) : '';
305
324
  if (ttl) row.appendChild(el(doc, 'div', 'sub-pending-ttl', `Link expires in ${ttl}`));
@@ -311,6 +330,156 @@ export function renderPendingLogins(doc, target, logins, now = Date.now()) {
311
330
  }
312
331
  }
313
332
 
333
+ // ── Account × Machine matrix (account-machine-matrix spec) ─────────────────
334
+ // At-a-glance "which account is set up on which machine," with a one-tap "Set up"
335
+ // per empty cell that runs the whole sign-in IN the dashboard (PIN → mandate →
336
+ // enroll-start → paste the code). Pure renderer so the jsdom tests exercise the
337
+ // SHIPPED grid. Built ENTIRELY from already-shipped pool-scope reads (FD1): the
338
+ // matrix invents no account key — it pivots `(accountId, machineId)` rows.
339
+ //
340
+ // Inputs:
341
+ // poolScope = GET /subscription-pool?scope=pool body
342
+ // { accounts:[{id,email,status,machineId,machineNickname,remote}],
343
+ // pool:{ selfMachineId, failed:[{machineId,error}] } }
344
+ // pendingScope = GET /subscription-pool/pending-logins?scope=pool body
345
+ // { logins:[{id,machineId,...}] } (id === accountId for matrix enrollments)
346
+ // transient = optional client-side last-attempt map keyed `${accountId}::${machineId}`
347
+ // → { state:'held'|'cant-resolve' } (FD6 — known only to the client)
348
+
349
+ /** Pivot the pool-scope + pending-scope bodies into a grid model. Pure + testable. */
350
+ export function buildMatrixModel(poolScope, pendingScope, transient = {}) {
351
+ const accountRows = (poolScope && Array.isArray(poolScope.accounts)) ? poolScope.accounts : [];
352
+ const pendingRows = (pendingScope && Array.isArray(pendingScope.logins)) ? pendingScope.logins : [];
353
+ const failed = (poolScope && poolScope.pool && Array.isArray(poolScope.pool.failed)) ? poolScope.pool.failed : [];
354
+ const selfMachineId = (poolScope && poolScope.pool && poolScope.pool.selfMachineId) || null;
355
+ const offlineMachineIds = new Set(failed.map((f) => f && f.machineId).filter(Boolean));
356
+
357
+ // Columns = union of machines from account rows + failed (offline) machines. A failed
358
+ // machine has NO account rows (pool-scope queries live, codex r3 #1) — so its column is
359
+ // discovered from the failed list and rendered offline (never a fabricated per-account ✓).
360
+ const machines = new Map(); // machineId → { machineId, nickname, offline }
361
+ for (const a of accountRows) {
362
+ const mid = a && a.machineId;
363
+ if (!mid || offlineMachineIds.has(mid)) continue;
364
+ if (!machines.has(mid)) machines.set(mid, { machineId: mid, nickname: (a.machineNickname || mid), offline: false });
365
+ }
366
+ for (const f of failed) {
367
+ const mid = f && f.machineId;
368
+ if (!mid) continue;
369
+ if (!machines.has(mid)) machines.set(mid, { machineId: mid, nickname: mid, offline: true });
370
+ else machines.get(mid).offline = true;
371
+ }
372
+
373
+ // Rows = union of account ids, displayed by email (FD8 — keyed by pool id, shown by email).
374
+ const accounts = new Map(); // accountId → { accountId, email }
375
+ for (const a of accountRows) {
376
+ const id = a && a.id;
377
+ if (!id) continue;
378
+ if (!accounts.has(id)) accounts.set(id, { accountId: id, email: a.email || id });
379
+ else if (!accounts.get(id).email && a.email) accounts.get(id).email = a.email;
380
+ }
381
+ // A pending matrix login can reference an account not yet in any pool row — surface its row too.
382
+ for (const l of pendingRows) {
383
+ const id = l && l.id;
384
+ if (id && !accounts.has(id)) accounts.set(id, { accountId: id, email: id });
385
+ }
386
+
387
+ // (accountId, machineId) → active|needs-reauth, from a CURRENTLY-REACHABLE machine only.
388
+ const cellStatus = new Map();
389
+ for (const a of accountRows) {
390
+ const mid = a && a.machineId;
391
+ if (!a || !a.id || !mid || offlineMachineIds.has(mid)) continue;
392
+ cellStatus.set(`${a.id}::${mid}`, a.status === 'needs-reauth' ? 'needs-reauth' : (a.status === 'active' ? 'active' : 'other'));
393
+ }
394
+ // (accountId, machineId) in-progress, correlated on (login.id === accountId, machineId) (FD6 r3 #2).
395
+ const inProgress = new Set();
396
+ for (const l of pendingRows) {
397
+ if (l && l.id && l.machineId) inProgress.add(`${l.id}::${l.machineId}`);
398
+ }
399
+
400
+ const machineList = Array.from(machines.values());
401
+ const accountList = Array.from(accounts.values());
402
+ const cells = [];
403
+ for (const acct of accountList) {
404
+ const rowCells = [];
405
+ for (const m of machineList) {
406
+ const key = `${acct.accountId}::${m.machineId}`;
407
+ let state;
408
+ if (m.offline) state = 'offline'; // whole column offline (FD6)
409
+ else if (transient[key] && transient[key].state === 'held') state = 'held';
410
+ else if (transient[key] && transient[key].state === 'cant-resolve') state = 'cant-resolve';
411
+ else if (inProgress.has(key)) state = 'in-progress';
412
+ else if (cellStatus.get(key) === 'active') state = 'active';
413
+ else if (cellStatus.get(key) === 'needs-reauth') state = 'needs-reauth';
414
+ else state = 'empty'; // → "Set up" button
415
+ rowCells.push({ accountId: acct.accountId, machineId: m.machineId, state });
416
+ }
417
+ cells.push({ account: acct, cells: rowCells });
418
+ }
419
+ return { machines: machineList, accounts: accountList, rows: cells, selfMachineId };
420
+ }
421
+
422
+ const MATRIX_CELL_GLYPH = {
423
+ active: '✓', 'needs-reauth': '⟳', 'in-progress': '◷', offline: '—', held: '⚠', 'cant-resolve': '✗',
424
+ };
425
+ const MATRIX_CELL_WORD = {
426
+ active: 'Active', 'needs-reauth': 'Needs sign-in', 'in-progress': 'Signing in…',
427
+ offline: 'Machine offline', held: 'Didn’t match — re-try', 'cant-resolve': 'Can’t set up', other: 'Set up',
428
+ };
429
+
430
+ /** Render the account × machine grid. `target` is replaced. Each empty (reachable) cell
431
+ * gets a "Set up" button carrying its (accountId, machineId) as data-* attributes for the
432
+ * controller's delegated tap handler. Offline columns are disabled; no state is fabricated. */
433
+ export function renderAccountMatrix(doc, target, poolScope, pendingScope, transient = {}) {
434
+ if (!target) return;
435
+ target.replaceChildren();
436
+ const model = buildMatrixModel(poolScope, pendingScope, transient);
437
+ if (model.accounts.length === 0 || model.machines.length === 0) {
438
+ target.appendChild(el(doc, 'div', 'sub-empty', 'No accounts or machines to show yet.'));
439
+ return;
440
+ }
441
+ const table = el(doc, 'table', 'sub-matrix');
442
+ // Header row: blank corner + one column per machine.
443
+ const thead = doc.createElement('thead');
444
+ const hr = doc.createElement('tr');
445
+ hr.appendChild(el(doc, 'th', 'sub-matrix-corner', ''));
446
+ for (const m of model.machines) {
447
+ const th = el(doc, 'th', m.offline ? 'sub-matrix-mach sub-matrix-off' : 'sub-matrix-mach',
448
+ sanitizeForDisplay(m.nickname, 'label') + (m.offline ? ' (offline)' : ''));
449
+ hr.appendChild(th);
450
+ }
451
+ thead.appendChild(hr);
452
+ table.appendChild(thead);
453
+
454
+ const tbody = doc.createElement('tbody');
455
+ for (const row of model.rows) {
456
+ const tr = doc.createElement('tr');
457
+ tr.appendChild(el(doc, 'th', 'sub-matrix-acct', sanitizeForDisplay(row.account.email, 'label')));
458
+ for (const c of row.cells) {
459
+ const td = el(doc, 'td', `sub-matrix-cell sub-matrix-${c.state}`);
460
+ if (c.state === 'empty' || c.state === 'held' || c.state === 'cant-resolve') {
461
+ // An actionable cell → a "Set up" button (held/cant-resolve let the operator retry).
462
+ const btn = el(doc, 'button', 'sub-matrix-setup', c.state === 'empty' ? 'Set up' : 'Retry');
463
+ btn.setAttribute('data-matrix-setup', '1');
464
+ btn.setAttribute('data-account-id', sanitizeForDisplay(c.accountId, 'label'));
465
+ btn.setAttribute('data-machine-id', sanitizeForDisplay(c.machineId, 'label'));
466
+ if (c.state !== 'empty') {
467
+ td.appendChild(el(doc, 'div', 'sub-matrix-glyph', `${MATRIX_CELL_GLYPH[c.state]} ${MATRIX_CELL_WORD[c.state]}`));
468
+ }
469
+ td.appendChild(btn);
470
+ } else {
471
+ const word = c.state === 'offline' ? 'unknown' : MATRIX_CELL_WORD[c.state];
472
+ const glyph = MATRIX_CELL_GLYPH[c.state] || '';
473
+ td.appendChild(el(doc, 'span', 'sub-matrix-glyph', `${glyph} ${word}`.trim()));
474
+ }
475
+ tr.appendChild(td);
476
+ }
477
+ tbody.appendChild(tr);
478
+ }
479
+ table.appendChild(tbody);
480
+ target.appendChild(table);
481
+ }
482
+
314
483
  export function renderDisabled(doc, els) {
315
484
  if (els && els.accounts) {
316
485
  els.accounts.replaceChildren(
@@ -327,8 +496,13 @@ const URLS = {
327
496
  // operator's single dashboard (WS5.2 seam #3) — without it the device-code link never appears here.
328
497
  pending: '/subscription-pool/pending-logins?scope=pool',
329
498
  inUse: '/subscription-pool/in-use',
499
+ submitCode: '/subscription-pool/follow-me/submit-code', // POST — paste-back the sign-in code (ws52-code-paste-back)
330
500
  scan: '/subscription-pool/follow-me/scan', // POST — follow-me consent offers (one-tap card)
331
501
  issue: '/mandate/issue-for-machine', // POST (PIN-gated) — Approve issues the mandate
502
+ // account-machine-matrix: pool-scope accounts feed the grid (the SAME read the accounts list
503
+ // uses, with peers merged); start-cell is the PIN-gated "Set up" orchestrator over the chain.
504
+ accountsPool: '/subscription-pool?scope=pool',
505
+ startCell: '/subscription-pool/matrix/start-cell', // POST (PIN-gated) — start a cell's sign-in
332
506
  };
333
507
 
334
508
  export function createController(opts) {
@@ -342,7 +516,10 @@ export function createController(opts) {
342
516
  cancel = (id) => clearTimeout(id),
343
517
  } = opts;
344
518
 
345
- const state = { timerId: null, active: false, inFlight: null, offers: [], approveWired: false };
519
+ // matrixTransient: client-side last-attempt state per `${accountId}::${machineId}` cell (FD6
520
+ // held / cant-resolve are known only to the client from the response it just got).
521
+ const state = { timerId: null, active: false, inFlight: null, offers: [], approveWired: false,
522
+ matrixWired: false, matrixTransient: {}, lastPoolBody: null, lastPendingBody: null };
346
523
 
347
524
  async function fetchJson(url, controller) {
348
525
  const resp = await fetchImpl(url, { signal: controller.signal });
@@ -369,15 +546,17 @@ export function createController(opts) {
369
546
  if (state.inFlight) { try { state.inFlight.abort(); } catch { /* superseded */ } }
370
547
  const controller = typeof AbortController !== 'undefined' ? new AbortController() : { signal: undefined, abort() {} };
371
548
  state.inFlight = controller;
372
- let accountsBody, pendingBody, inUseBody, scanBody;
549
+ let accountsBody, pendingBody, inUseBody, scanBody, poolBody;
373
550
  try {
374
551
  // in-use AND the follow-me scan are best-effort — their failure must not blank the
375
552
  // accounts list, so each is caught independently (in-use → "unknown"; scan → no card).
376
- [accountsBody, pendingBody, inUseBody, scanBody] = await Promise.all([
553
+ // poolBody (scope=pool) feeds the account×machine matrix; best-effort (matrix is hidden if absent).
554
+ [accountsBody, pendingBody, inUseBody, scanBody, poolBody] = await Promise.all([
377
555
  fetchJson(URLS.accounts, controller),
378
556
  fetchJson(URLS.pending, controller),
379
557
  fetchJson(URLS.inUse, controller).catch(() => null),
380
558
  postJson(URLS.scan, {}, controller).then((r) => (r.ok ? r.json : null)).catch(() => null),
559
+ fetchJson(URLS.accountsPool, controller).catch(() => null),
381
560
  ]);
382
561
  } catch {
383
562
  if (controller.signal && controller.signal.aborted) return;
@@ -394,16 +573,25 @@ export function createController(opts) {
394
573
  return;
395
574
  }
396
575
  state.offers = scanBody && Array.isArray(scanBody.offered) ? scanBody.offered : [];
397
- render(accountsBody, pendingBody, inUseBody);
576
+ render(accountsBody, pendingBody, inUseBody, poolBody);
398
577
  reschedule();
399
578
  }
400
579
 
401
- function render(accountsBody, pendingBody, inUseBody) {
580
+ function render(accountsBody, pendingBody, inUseBody, poolBody) {
402
581
  const accounts = accountsBody && Array.isArray(accountsBody.accounts) ? accountsBody.accounts : [];
403
582
  const logins = pendingBody && Array.isArray(pendingBody.logins) ? pendingBody.logins : [];
404
583
  const inUseAccountId = inUseBody && inUseBody.activeAccountId ? inUseBody.activeAccountId : null;
405
584
  renderAccounts(doc, els.accounts, accounts, now(), inUseAccountId);
406
585
  renderPendingLogins(doc, els.pending, logins, now());
586
+ wireCodeSubmit();
587
+ // The account × machine matrix (account-machine-matrix) — built from the pool-scope read +
588
+ // the (already pool-scope) pending logins. Hidden when the pool-scope read is unavailable.
589
+ if (els.matrix) {
590
+ state.lastPoolBody = poolBody || null;
591
+ state.lastPendingBody = pendingBody || null;
592
+ renderAccountMatrix(doc, els.matrix, poolBody, pendingBody, state.matrixTransient);
593
+ wireMatrixSetup();
594
+ }
407
595
  // The one-tap follow-me Approve card(s) — rendered into els.followMe from the scan offers
408
596
  // (ws52-operator-tap-not-text Part A). Silent when there are none. The Approve click is wired
409
597
  // once (delegated) so re-renders never stack listeners.
@@ -456,6 +644,210 @@ export function createController(opts) {
456
644
  s.textContent = text;
457
645
  }
458
646
 
647
+ // Delegated, wired ONCE: a "Submit code" tap on a pending-login row reads the pasted
648
+ // sign-in code and POSTs it (with the login's id + machineId) to the fronting relay,
649
+ // which carries it to the machine doing the login (off-chat). The code is never stored
650
+ // client-side beyond the input; it's cleared on success. (ws52-code-paste-back)
651
+ function wireCodeSubmit() {
652
+ if (state.codeSubmitWired || !els.pending) return;
653
+ state.codeSubmitWired = true;
654
+ els.pending.addEventListener('click', (ev) => {
655
+ const btn = ev.target && ev.target.closest ? ev.target.closest('[data-submit-code]') : null;
656
+ if (!btn || !els.pending.contains(btn)) return;
657
+ const row = btn.closest('.sub-pending');
658
+ if (!row) return;
659
+ const input = row.querySelector('.sub-pending-code-input');
660
+ const code = input ? input.value.trim() : '';
661
+ const id = row.getAttribute('data-login-id');
662
+ const machineId = row.getAttribute('data-machine-id') || undefined;
663
+ if (!code) { setRowStatus(row, 'Paste the code the sign-in page gave you, then tap Submit.'); return; }
664
+ if (!id) { setRowStatus(row, 'Couldn’t identify this login — please refresh.'); return; }
665
+ setRowStatus(row, 'Sending your code…');
666
+ btn.setAttribute('disabled', '1');
667
+ void (async () => {
668
+ try {
669
+ const r = await postJson(URLS.submitCode, { machineId, id, code });
670
+ if (r.ok && r.json && r.json.outcome === 'validated') {
671
+ setRowStatus(row, 'Done — this machine is set up with the account.');
672
+ if (input) input.value = '';
673
+ } else if (r.ok && r.json && r.json.outcome === 'submitted') {
674
+ setRowStatus(row, 'Code sent — finishing sign-in…');
675
+ if (input) input.value = '';
676
+ } else if (r.ok && r.json && r.json.outcome === 'held') {
677
+ setRowStatus(row, 'Signed in, but the account didn’t match what was approved — check with the operator.');
678
+ btn.removeAttribute('disabled');
679
+ } else {
680
+ const msg = (r.json && (r.json.error || r.json.reason)) ? (r.json.error || r.json.reason) : `failed (${r.status})`;
681
+ setRowStatus(row, `Couldn’t submit the code: ${msg}`);
682
+ btn.removeAttribute('disabled');
683
+ }
684
+ } catch (e) {
685
+ setRowStatus(row, 'Couldn’t reach the server — try again.');
686
+ btn.removeAttribute('disabled');
687
+ }
688
+ })();
689
+ });
690
+ }
691
+
692
+ function setRowStatus(row, text) {
693
+ let s = row.querySelector('.sub-pending-status');
694
+ if (!s) { s = el(doc, 'div', 'sub-pending-status', ''); row.appendChild(s); }
695
+ s.textContent = text;
696
+ }
697
+
698
+ // Delegated, wired ONCE: a "Set up" tap on a matrix cell expands an inline PIN input +
699
+ // Confirm. Confirm POSTs the PIN-gated start-cell; on success the cell shows the auth link
700
+ // (operator opens it) + a code input + Submit, which POSTs the SHIPPED submit-code relay.
701
+ // The PIN + code are memory-only (read from the input on tap, cleared after use; never cached).
702
+ function wireMatrixSetup() {
703
+ if (state.matrixWired || !els.matrix) return;
704
+ state.matrixWired = true;
705
+ els.matrix.addEventListener('click', (ev) => {
706
+ const t = ev.target;
707
+ if (!t || typeof t.closest !== 'function') return;
708
+ const setupBtn = t.closest('[data-matrix-setup]');
709
+ if (setupBtn && els.matrix.contains(setupBtn)) { onSetupTap(setupBtn); return; }
710
+ const confirmBtn = t.closest('[data-matrix-confirm]');
711
+ if (confirmBtn && els.matrix.contains(confirmBtn)) { onConfirmTap(confirmBtn); return; }
712
+ const codeBtn = t.closest('[data-matrix-code-submit]');
713
+ if (codeBtn && els.matrix.contains(codeBtn)) { onCodeTap(codeBtn); return; }
714
+ });
715
+ }
716
+
717
+ function matrixCellOf(node) {
718
+ return node && typeof node.closest === 'function' ? node.closest('.sub-matrix-cell') : null;
719
+ }
720
+ function setCellStatus(cell, text) {
721
+ let s = cell.querySelector('.sub-matrix-status');
722
+ if (!s) { s = el(doc, 'div', 'sub-matrix-status', ''); cell.appendChild(s); }
723
+ s.textContent = text;
724
+ }
725
+
726
+ // Expand the cell into a PIN input + Confirm (replacing the "Set up"/"Retry" button).
727
+ function onSetupTap(btn) {
728
+ const cell = matrixCellOf(btn);
729
+ if (!cell) return;
730
+ const accountId = btn.getAttribute('data-account-id');
731
+ const machineId = btn.getAttribute('data-machine-id');
732
+ if (!accountId || !machineId) return;
733
+ btn.remove();
734
+ const pin = doc.createElement('input');
735
+ pin.setAttribute('type', 'password');
736
+ pin.setAttribute('class', 'sub-matrix-pin');
737
+ pin.setAttribute('placeholder', 'Your PIN');
738
+ pin.setAttribute('autocomplete', 'off');
739
+ cell.appendChild(pin);
740
+ const confirm = el(doc, 'button', 'sub-matrix-confirm', 'Confirm');
741
+ confirm.setAttribute('data-matrix-confirm', '1');
742
+ confirm.setAttribute('data-account-id', accountId);
743
+ confirm.setAttribute('data-machine-id', machineId);
744
+ cell.appendChild(confirm);
745
+ }
746
+
747
+ // Confirm → POST the PIN-gated start-cell; render the auth link + code input on success.
748
+ function onConfirmTap(btn) {
749
+ const cell = matrixCellOf(btn);
750
+ if (!cell) return;
751
+ const accountId = btn.getAttribute('data-account-id');
752
+ const machineId = btn.getAttribute('data-machine-id');
753
+ const pinInput = cell.querySelector('.sub-matrix-pin');
754
+ const pin = pinInput ? pinInput.value.trim() : '';
755
+ if (!accountId || !machineId) { setCellStatus(cell, 'Couldn’t prepare this — please refresh.'); return; }
756
+ if (!pin) { setCellStatus(cell, 'Enter your PIN to set this up.'); return; }
757
+ setCellStatus(cell, 'Starting sign-in…');
758
+ btn.setAttribute('disabled', '1');
759
+ void (async () => {
760
+ try {
761
+ const r = await postJson(URLS.startCell, { accountId, machineId, pin });
762
+ if (pinInput) pinInput.value = ''; // PIN is memory-only — clear it immediately
763
+ if (r.ok && r.json && r.json.verificationUrl) {
764
+ renderCellSignIn(cell, accountId, machineId, r.json.verificationUrl, r.json.loginId || accountId);
765
+ } else if (r.status === 409) {
766
+ state.matrixTransient[`${accountId}::${machineId}`] = { state: 'cant-resolve' };
767
+ setCellStatus(cell, 'Can’t set this account up here — its details couldn’t be resolved.');
768
+ btn.removeAttribute('disabled');
769
+ } else {
770
+ const msg = (r.json && (r.json.error || r.json.reason)) ? (r.json.error || r.json.reason) : `failed (${r.status})`;
771
+ setCellStatus(cell, `Couldn’t start: ${msg}`);
772
+ btn.removeAttribute('disabled');
773
+ }
774
+ } catch (e) {
775
+ setCellStatus(cell, 'Couldn’t reach the server — try again.');
776
+ btn.removeAttribute('disabled');
777
+ }
778
+ })();
779
+ }
780
+
781
+ // After start-cell: show the sign-in link (operator opens it) + a code input + Submit.
782
+ function renderCellSignIn(cell, accountId, machineId, verificationUrl, loginId) {
783
+ // Clear the PIN/Confirm UI, keep the status line.
784
+ const pin = cell.querySelector('.sub-matrix-pin'); if (pin) pin.remove();
785
+ const confirm = cell.querySelector('[data-matrix-confirm]'); if (confirm) confirm.remove();
786
+ setCellStatus(cell, 'Open the sign-in link, then paste the code below.');
787
+ const href = trustedLoginUrl(verificationUrl);
788
+ if (href) {
789
+ const a = doc.createElement('a');
790
+ a.setAttribute('href', href);
791
+ a.setAttribute('target', '_blank');
792
+ a.setAttribute('rel', 'noopener noreferrer');
793
+ a.setAttribute('class', 'sub-matrix-signin');
794
+ a.textContent = 'Sign in';
795
+ cell.appendChild(a);
796
+ } else {
797
+ cell.appendChild(el(doc, 'div', 'sub-matrix-url', sanitizeForDisplay(verificationUrl, 'url')));
798
+ }
799
+ const code = doc.createElement('input');
800
+ code.setAttribute('type', 'text');
801
+ code.setAttribute('class', 'sub-matrix-code-input');
802
+ code.setAttribute('placeholder', 'Paste your sign-in code');
803
+ code.setAttribute('autocomplete', 'off');
804
+ cell.appendChild(code);
805
+ const submit = el(doc, 'button', 'sub-matrix-code-submit', 'Submit');
806
+ submit.setAttribute('data-matrix-code-submit', '1');
807
+ submit.setAttribute('data-account-id', accountId);
808
+ submit.setAttribute('data-machine-id', machineId);
809
+ submit.setAttribute('data-login-id', loginId);
810
+ cell.appendChild(submit);
811
+ }
812
+
813
+ // Submit the pasted code via the SHIPPED submit-code relay (unchanged contract).
814
+ function onCodeTap(btn) {
815
+ const cell = matrixCellOf(btn);
816
+ if (!cell) return;
817
+ const accountId = btn.getAttribute('data-account-id');
818
+ const machineId = btn.getAttribute('data-machine-id');
819
+ const loginId = btn.getAttribute('data-login-id') || accountId;
820
+ const input = cell.querySelector('.sub-matrix-code-input');
821
+ const code = input ? input.value.trim() : '';
822
+ if (!code) { setCellStatus(cell, 'Paste the code the sign-in page gave you, then tap Submit.'); return; }
823
+ setCellStatus(cell, 'Sending your code…');
824
+ btn.setAttribute('disabled', '1');
825
+ void (async () => {
826
+ try {
827
+ const r = await postJson(URLS.submitCode, { machineId, id: loginId, code });
828
+ if (input) input.value = ''; // code is memory-only — cleared after use
829
+ const key = `${accountId}::${machineId}`;
830
+ if (r.ok && r.json && r.json.outcome === 'validated') {
831
+ delete state.matrixTransient[key];
832
+ setCellStatus(cell, 'Done — this machine is set up with the account.');
833
+ } else if (r.ok && r.json && r.json.outcome === 'submitted') {
834
+ setCellStatus(cell, 'Code sent — finishing sign-in…');
835
+ } else if (r.ok && r.json && r.json.outcome === 'held') {
836
+ state.matrixTransient[key] = { state: 'held' };
837
+ setCellStatus(cell, 'Signed in, but the account didn’t match what was approved — re-try with the right account.');
838
+ btn.removeAttribute('disabled');
839
+ } else {
840
+ const msg = (r.json && (r.json.error || r.json.reason)) ? (r.json.error || r.json.reason) : `failed (${r.status})`;
841
+ setCellStatus(cell, `Couldn’t submit the code: ${msg}`);
842
+ btn.removeAttribute('disabled');
843
+ }
844
+ } catch (e) {
845
+ setCellStatus(cell, 'Couldn’t reach the server — try again.');
846
+ btn.removeAttribute('disabled');
847
+ }
848
+ })();
849
+ }
850
+
459
851
  function reschedule() {
460
852
  if (!state.active) return;
461
853
  if (state.timerId != null) cancel(state.timerId);
@@ -478,5 +870,6 @@ if (typeof window !== 'undefined') {
478
870
  window.Subscriptions = {
479
871
  createController, sanitizeForDisplay, renderAccounts, renderPendingLogins,
480
872
  renderFollowMeOffers, renderFollowMeApproveCard, buildFollowMeIssuePayload,
873
+ renderAccountMatrix, buildMatrixModel,
481
874
  };
482
875
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAoCH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAwBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAkH7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAy4CD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,IAAI,CA8eN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAk8etE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAoCH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAwBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAkH7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAy4CD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,IAAI,CA8eN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAi8etE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
@@ -9874,7 +9874,7 @@ export async function startServer(options) {
9874
9874
  // a 2nd account never clobbers the 1st.
9875
9875
  const { PendingLoginStore } = await import('../core/PendingLoginStore.js');
9876
9876
  const { EnrollmentWizard } = await import('../core/EnrollmentWizard.js');
9877
- const { FrameworkLoginDriver } = await import('../core/FrameworkLoginDriver.js');
9877
+ const { FrameworkLoginDriver, enrollPaneSessionName } = await import('../core/FrameworkLoginDriver.js');
9878
9878
  const DEFAULT_ENROLL_LOGIN_COMMANDS = {
9879
9879
  'claude-code': 'claude auth login',
9880
9880
  'codex-cli': 'codex login',
@@ -9933,8 +9933,7 @@ export async function startServer(options) {
9933
9933
  const cmd = configHome
9934
9934
  ? `env CLAUDE_CONFIG_DIR=${JSON.stringify(configHome)} ${baseCmd}`
9935
9935
  : baseCmd;
9936
- const slug = (configHome ?? framework).replace(/[^a-zA-Z0-9]+/g, '-').slice(-24);
9937
- const session = `instar-enroll-${framework}-${slug}`;
9936
+ const session = enrollPaneSessionName(framework, configHome);
9938
9937
  try {
9939
9938
  execFileSync(tmuxPath, ['kill-session', '-t', `=${session}`], { stdio: 'ignore' });
9940
9939
  }