@gotcos/glasses-server 7.0.27 → 7.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/docs/web-key-verifier-contract.md +1 -1
- package/package.json +1 -1
- package/server/index.ts +29 -6
- package/server/lib/tailnet-self-name.ts +57 -10
- package/server/lib/web-key-signer-app.ts +25 -6
- package/server/lib/web-key-signer-tls.ts +790 -0
- package/server/routes/web-key.ts +31 -2
- package/server/web-control/.manifest.json +4 -4
- package/server/web-control/app.css +54 -0
- package/server/web-control/app.js +317 -36
- package/server/web-control/index.html +44 -5
- package/server/web-control/theme.js +169 -2
|
@@ -245,9 +245,55 @@
|
|
|
245
245
|
clearTimeout(toastTimer); toastTimer = setTimeout(function () { el.classList.add('hidden'); }, 3200);
|
|
246
246
|
}
|
|
247
247
|
function setStatus(el, text, isError) { if (!el) return; el.textContent = text || ''; el.classList.toggle('error', !!isError); }
|
|
248
|
-
|
|
248
|
+
// 7.0.29 (lead F2): an empty session reply box is as tall as its placeholder, so a placeholder that wraps (the rail's
|
|
249
|
+
// width at 382 px wraps "Message this session (not an answer)") is never cut off. Only #session-input; where the
|
|
250
|
+
// placeholder fits on one line the height is the same as before.
|
|
251
|
+
function autoGrow(ta) {
|
|
252
|
+
ta.style.height = 'auto';
|
|
253
|
+
var h = ta.scrollHeight;
|
|
254
|
+
if (!ta.value && ta.id === 'session-input' && ta.placeholder) h = Math.max(h, placeholderHeight(ta, ta.placeholder));
|
|
255
|
+
ta.style.height = Math.min(h, 140) + 'px';
|
|
256
|
+
}
|
|
257
|
+
// 7.0.29 (lead F2): with the rail at 382 px, "Message this session (not an answer)" wraps; with the keyboard up the box
|
|
258
|
+
// is capped under two lines, so the second line would be cut. Where the long words would wrap in the box's width, the
|
|
259
|
+
// box says the short form instead; checked again when the width changes (railSync). Elsewhere the words are as before.
|
|
260
|
+
function sxPlaceholderFit(box) {
|
|
261
|
+
if (!box || !box.setAttribute || !box.phLong) return;
|
|
262
|
+
var ph = box.phLong;
|
|
263
|
+
if (box.phShort) { var one = placeholderHeight(box, 'M'), two = placeholderHeight(box, box.phLong); if (one > 0 && two > one + 2) ph = box.phShort; }
|
|
264
|
+
if (!box.getAttribute || box.getAttribute('placeholder') !== ph) box.setAttribute('placeholder', ph);
|
|
265
|
+
if (!box.value && box.style) autoGrow(box);
|
|
266
|
+
}
|
|
267
|
+
// The placeholder's height in a hidden copy of the box: same width, font, padding and line height. The box's own value
|
|
268
|
+
// is never touched (setting it would drop its undo history and move the caret).
|
|
269
|
+
var PH_PROBE = null;
|
|
270
|
+
function placeholderHeight(ta, text) {
|
|
271
|
+
try {
|
|
272
|
+
if (typeof getComputedStyle !== 'function' || !document.body || !ta.getBoundingClientRect) return 0;
|
|
273
|
+
var cs = getComputedStyle(ta), w = ta.getBoundingClientRect().width;
|
|
274
|
+
if (!(w > 0)) return 0;
|
|
275
|
+
var c = PH_PROBE;
|
|
276
|
+
if (!c) { c = PH_PROBE = document.createElement('textarea'); c.setAttribute('aria-hidden', 'true'); c.tabIndex = -1; c.rows = 1; }
|
|
277
|
+
c.style.cssText = 'position:absolute;left:-9999px;top:0;visibility:hidden;height:auto;min-height:0;max-height:none;overflow:hidden;resize:none;border:0;' +
|
|
278
|
+
'box-sizing:' + cs.boxSizing + ';width:' + w + 'px;padding:' + cs.paddingTop + ' ' + cs.paddingRight + ' ' + cs.paddingBottom + ' ' + cs.paddingLeft + ';' +
|
|
279
|
+
'font:' + cs.font + ';letter-spacing:' + cs.letterSpacing + ';line-height:' + cs.lineHeight + ';white-space:' + cs.whiteSpace + ';word-break:' + cs.wordBreak + ';';
|
|
280
|
+
c.value = text;
|
|
281
|
+
if (c.parentNode !== document.body) document.body.appendChild(c);
|
|
282
|
+
return c.scrollHeight;
|
|
283
|
+
} catch (_e) { return 0; }
|
|
284
|
+
}
|
|
249
285
|
|
|
250
286
|
// ── API ─────────────────────────────────────────────────
|
|
287
|
+
// 7.0.29 R1 (Q1 FIX 1): a write on its way to the Mac (a send, a save). theme.js's restyle check asks window.CosBusy
|
|
288
|
+
// before it reloads the page, so a reload never lands on a message in flight.
|
|
289
|
+
var WRITES = { n: 0 };
|
|
290
|
+
function writeStart(method) {
|
|
291
|
+
if (!method || method === 'GET') return function () {};
|
|
292
|
+
WRITES.n++;
|
|
293
|
+
var once = false;
|
|
294
|
+
return function () { if (!once) { once = true; WRITES.n = Math.max(0, WRITES.n - 1); } };
|
|
295
|
+
}
|
|
296
|
+
if (typeof window === 'object' && window) window.CosBusy = function () { return WRITES.n > 0 || !!(typeof state === 'object' && state && state.reading && state.reading.sending); };
|
|
251
297
|
function api(path, opts) {
|
|
252
298
|
var o = opts || {};
|
|
253
299
|
var headers = { 'X-COS-Web': '1' };
|
|
@@ -260,7 +306,7 @@
|
|
|
260
306
|
var limit = o.timeoutMs != null ? o.timeoutMs : (method === 'GET' ? API_READ_TIMEOUT_MS : 0);
|
|
261
307
|
var ctrl = limit && window.AbortController ? new AbortController() : null;
|
|
262
308
|
var timer = ctrl ? setTimeout(function () { ctrl.abort(); }, limit) : null;
|
|
263
|
-
var sentAt = Date.now();
|
|
309
|
+
var sentAt = Date.now(), writeDone = typeof writeStart === 'function' ? writeStart(method) : function () {};
|
|
264
310
|
return fetch(path, { method: method, headers: headers, credentials: 'same-origin', body: body, signal: ctrl ? ctrl.signal : undefined }).then(function (res) {
|
|
265
311
|
// Blob bodies keep their deadline after headers: a stalled download must release its queue slot.
|
|
266
312
|
if (timer && o.responseType !== 'blob') clearTimeout(timer);
|
|
@@ -288,9 +334,11 @@
|
|
|
288
334
|
});
|
|
289
335
|
}).then(function (value) {
|
|
290
336
|
if (timer) clearTimeout(timer);
|
|
337
|
+
writeDone();
|
|
291
338
|
return value;
|
|
292
339
|
}, function (err) {
|
|
293
340
|
if (timer) clearTimeout(timer);
|
|
341
|
+
writeDone();
|
|
294
342
|
if (err && err.name === 'AbortError' && ctrl && ctrl.signal.aborted) {
|
|
295
343
|
// 6.99.32 (QA 6.99.30 canary W1): a read the Mac never answered in time means it is not answering. A write has
|
|
296
344
|
// no deadline unless its caller set one, and a slow save is not a Mac that went away, so only reads say so.
|
|
@@ -529,6 +577,56 @@
|
|
|
529
577
|
// Comet): height is its scarce axis, so it gets the phone Ask sheet and the 44 px tap floor (app.css), as folded does.
|
|
530
578
|
var SHORT_TOUCH_MQ = '(pointer: coarse) and (max-height: 720px)';
|
|
531
579
|
function isPhone() { return !window.matchMedia('(min-width: 600px)').matches || window.matchMedia(SHORT_TOUCH_MQ).matches; }
|
|
580
|
+
// 7.0.29: the folded Duo's side rail (app.css "the folded Duo's side rail"). theme.js decides from the screen's shape
|
|
581
|
+
// (html.short-screen); this media query is app.css's own, so the two never disagree. When it is on, the rail is the one
|
|
582
|
+
// way to switch panes: the top bar's section button keeps the name and loses its menu (inert), and its Back moves to
|
|
583
|
+
// the rail's foot (#rail-back, the same goBack).
|
|
584
|
+
var SIDE_RAIL_MQ = '(max-width: 599px) and (pointer: coarse)';
|
|
585
|
+
function sideRail() {
|
|
586
|
+
var de = document.documentElement;
|
|
587
|
+
return !!(de && de.classList && de.classList.contains('short-screen') && window.matchMedia && window.matchMedia(SIDE_RAIL_MQ).matches);
|
|
588
|
+
}
|
|
589
|
+
// 44 points on the phone. Safari's Page Zoom lays the page out wider and draws it at visualViewport.scale (his Duo: 449 px
|
|
590
|
+
// at 0.85), so 44 px would be 37 points: the rail's buttons are ceil(44 / scale) px: 44 at 100 percent, 52 at 85, 88 at 50 (the lowest Page Zoom). Read at boot and when the
|
|
591
|
+
// layout width changes (rotation), never on a pinch: a pinch must not resize the rail.
|
|
592
|
+
var RAIL = { hit: 0, cw: -1, hooked: false, on: null };
|
|
593
|
+
function railHitSync() {
|
|
594
|
+
var de = document.documentElement, cw = de ? de.clientWidth : 0;
|
|
595
|
+
if (RAIL.hit && cw === RAIL.cw) return RAIL.hit;
|
|
596
|
+
var vv = window.visualViewport, s = vv && typeof vv.scale === 'number' ? vv.scale : 1;
|
|
597
|
+
if (!(s >= 0.5 && s <= 1)) s = 1;
|
|
598
|
+
RAIL.hit = Math.ceil(44 / s - 0.01); // s is 0.5 to 1 here, so 44 to 88
|
|
599
|
+
RAIL.cw = cw;
|
|
600
|
+
if (de && de.style) de.style.setProperty('--rail-hit', RAIL.hit + 'px');
|
|
601
|
+
return RAIL.hit;
|
|
602
|
+
}
|
|
603
|
+
function railSync() {
|
|
604
|
+
var on = false;
|
|
605
|
+
try {
|
|
606
|
+
// theme.js's one screen test first (its own resize listener ran too; this keeps the order certain), then the page.
|
|
607
|
+
if (typeof window.CosShortScreen === 'function') window.CosShortScreen();
|
|
608
|
+
on = sideRail();
|
|
609
|
+
var btn = typeof $ === 'function' ? $('section-btn') : null;
|
|
610
|
+
if (btn) {
|
|
611
|
+
btn.inert = on;
|
|
612
|
+
if (on) btn.removeAttribute('aria-haspopup'); else btn.setAttribute('aria-haspopup', 'dialog');
|
|
613
|
+
}
|
|
614
|
+
if (on) { railHitSync(); closeMenu(); }
|
|
615
|
+
// The rail changes the reply box's width: an empty box re-fits its placeholder (autoGrow, F2).
|
|
616
|
+
var si = typeof $ === 'function' ? $('session-input') : null; if (si && typeof sxPlaceholderFit === 'function' && si.phLong) sxPlaceholderFit(si); else if (si && !si.value && typeof autoGrow === 'function') autoGrow(si);
|
|
617
|
+
// A change after boot (a fold, a rotation, a new screen) is reported once, with the screen's own numbers.
|
|
618
|
+
if (RAIL.on !== null && RAIL.on !== on && typeof layoutCrumb === 'function') { var sc = window.screen || {}; layoutCrumb('rail', { sw: sc.width, sh: sc.height, rail: on, railHit: RAIL.hit }); }
|
|
619
|
+
RAIL.on = on;
|
|
620
|
+
if (!RAIL.hooked && typeof window.matchMedia === 'function' && typeof window.addEventListener === 'function') {
|
|
621
|
+
RAIL.hooked = true;
|
|
622
|
+
var mq = window.matchMedia(SIDE_RAIL_MQ);
|
|
623
|
+
if (mq && mq.addEventListener) mq.addEventListener('change', railSync); else if (mq && mq.addListener) mq.addListener(railSync);
|
|
624
|
+
window.addEventListener('resize', function () { railSync(); });
|
|
625
|
+
window.addEventListener('orientationchange', function () { railSync(); });
|
|
626
|
+
}
|
|
627
|
+
} catch (_e) { /* a harness without a page: no rail */ }
|
|
628
|
+
return on;
|
|
629
|
+
}
|
|
532
630
|
/** A keyboard and a fine pointer are the way in (a Mac, a PC): Return sends. Touch screens type a new line. */
|
|
533
631
|
function keyboardFirst() { return !!(window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches); }
|
|
534
632
|
// Return in a text box sends only where a keyboard and a pointer are the way in (cos-chat.js keyboardFirst): never on
|
|
@@ -563,6 +661,8 @@
|
|
|
563
661
|
|
|
564
662
|
// Top bar (phone): the section switcher at a list, a back button on a reader.
|
|
565
663
|
$('topbar-back').classList.toggle('hidden', !reading);
|
|
664
|
+
var railBack = $('rail-back');
|
|
665
|
+
if (railBack) railBack.hidden = !reading;
|
|
566
666
|
$('section-btn').classList.toggle('hidden', reading);
|
|
567
667
|
$('topbar-back-label').textContent = TITLES[r.pane] || 'Back';
|
|
568
668
|
$('section-name').textContent = TITLES[r.pane] || 'Today';
|
|
@@ -624,7 +724,7 @@
|
|
|
624
724
|
else if (y > lastY + 4 && isPhone()) bar.classList.add('tucked');
|
|
625
725
|
lastY = y;
|
|
626
726
|
}
|
|
627
|
-
function openMenu() { $('section-menu').classList.remove('hidden'); var cur = document.querySelector('#section-menu [aria-current="page"]') || document.querySelector('#section-menu [data-goto]'); if (cur) cur.focus(); }
|
|
727
|
+
function openMenu() { if (sideRail()) return; $('section-menu').classList.remove('hidden'); var cur = document.querySelector('#section-menu [aria-current="page"]') || document.querySelector('#section-menu [data-goto]'); if (cur) cur.focus(); }
|
|
628
728
|
function closeMenu() { $('section-menu').classList.add('hidden'); }
|
|
629
729
|
|
|
630
730
|
// ── auth ────────────────────────────────────────────────
|
|
@@ -2068,7 +2168,11 @@
|
|
|
2068
2168
|
var qs1 = sessionQs(), open = (!!html && !te) || !!(qs1 && ((qs1.itemsFor && qs1.itemsFor(r.id).length) || (qs1.atMacFor && qs1.atMacFor(r.id).length)));
|
|
2069
2169
|
var box = $('session-input');
|
|
2070
2170
|
// 7.0.18: here a reply IS the answer (sent with the contract's attribution), so the box says so.
|
|
2071
|
-
if (box && box.setAttribute)
|
|
2171
|
+
if (box && box.setAttribute) {
|
|
2172
|
+
box.phLong = te ? 'Or write your own answer' : open ? 'Message this session (not an answer)' : 'Continue this session…';
|
|
2173
|
+
box.phShort = open && !te ? 'Message (not an answer)' : '';
|
|
2174
|
+
if (typeof sxPlaceholderFit === 'function') sxPlaceholderFit(box); else box.setAttribute('placeholder', box.phLong);
|
|
2175
|
+
}
|
|
2072
2176
|
}
|
|
2073
2177
|
// ── end at the Mac's prompt
|
|
2074
2178
|
|
|
@@ -2759,6 +2863,8 @@
|
|
|
2759
2863
|
var touched = function () { SX_PIN.userAt = Date.now(); };
|
|
2760
2864
|
['wheel', 'touchstart', 'pointerdown', 'keydown'].forEach(function (t) { body.addEventListener(t, touched, { passive: true }); });
|
|
2761
2865
|
body.addEventListener('scroll', sxGoQuestionPaint, { passive: true });
|
|
2866
|
+
// 7.0.29 ADD 3: reaching the end shows him what is there.
|
|
2867
|
+
body.addEventListener('scroll', function () { if (typeof sxMarkShown === 'function' && state.reading) sxMarkShown(state.reading); }, { passive: true });
|
|
2762
2868
|
body.addEventListener('scroll', function () {
|
|
2763
2869
|
if (SX_PIN.body !== body || Date.now() - SX_PIN.userAt > SX_PIN_USER_MS) return;
|
|
2764
2870
|
if (Math.abs(body.scrollTop - SX_PIN.put) > 4 && body.scrollTop < body.scrollHeight - body.clientHeight - 4) sxUnpin();
|
|
@@ -2907,12 +3013,25 @@
|
|
|
2907
3013
|
// Earlier this month as compact rows, folded shut with a count. Days are counted back from today (2 to 6 days ago is
|
|
2908
3014
|
// Earlier this week, 7 to 13 Last week, 14 to 30 Earlier this month), so no group is empty only because of the
|
|
2909
3015
|
// calendar.
|
|
2910
|
-
var SX_GROUPS = [['needs', 'Needs you', 'state'], ['working', 'Working now', 'state'], ['pinned', 'Pinned', ''], ['today', 'Today', ''],
|
|
3016
|
+
var SX_GROUPS = [['needs', 'Needs you', 'state'], ['working', 'Working now', 'state'], ['pinned', 'Pinned', 'pinned'], ['today', 'Today', ''],
|
|
2911
3017
|
['yesterday', 'Yesterday', ''], ['week', 'Earlier this week', ''], ['lastweek', 'Last week', 'compact'], ['month', 'Earlier this month', 'compact']];
|
|
2912
3018
|
var SX_DAY_MS = 86400000, SX_REST_MS = 2000, SX_EMPTY_HOLD_MS = 5000;
|
|
2913
3019
|
// place: each row's group as last drawn at rest; seen: when Needs you and Working now last had rows; open: unfolded
|
|
2914
3020
|
// compact groups; older: /older's short rows (olderOk false: an older server, 404).
|
|
2915
3021
|
var SXL = { place: null, down: false, touchAt: 0, scrollAt: 0, timer: 0, seen: {}, open: {}, older: [], olderN: null, olderOk: null, olderNext: null, olderAt: 0, olderBusy: false };
|
|
3022
|
+
// 7.0.29 ADD 1 (Miles 23:53: thirteen pinned rows sat between Working now and Today): Pinned folds like a folder, with
|
|
3023
|
+
// its count, and this browser remembers the choice (localStorage). Open by default, so no list changes until he folds
|
|
3024
|
+
// it. Placement is unchanged: sxGroupFor tests state before pin, so a pinned session that is running is drawn in
|
|
3025
|
+
// Working now, one that waits or failed in Needs you, folded or not, and it goes back under Pinned when idle.
|
|
3026
|
+
var SX_PIN_FOLD_KEY = 'cos_sx_pinned_folded';
|
|
3027
|
+
function sxPinnedOpen() {
|
|
3028
|
+
if (typeof SXL.pinOpen !== 'boolean') { var v = null; try { v = typeof localStorage === 'object' && localStorage ? localStorage.getItem(SX_PIN_FOLD_KEY) : null; } catch (_e) { v = null; } SXL.pinOpen = v !== '1'; }
|
|
3029
|
+
return SXL.pinOpen;
|
|
3030
|
+
}
|
|
3031
|
+
function sxPinnedSet(open) {
|
|
3032
|
+
SXL.pinOpen = !!open;
|
|
3033
|
+
try { if (typeof localStorage === 'object' && localStorage) { if (open) localStorage.removeItem(SX_PIN_FOLD_KEY); else localStorage.setItem(SX_PIN_FOLD_KEY, '1'); } } catch (_e) { /* storage off: this visit only */ }
|
|
3034
|
+
}
|
|
2916
3035
|
function sxRowKey(s) { var k = splitSessionId(s.provider, s.session_id || s.id); return k.provider + '|' + String(k.id).toLowerCase(); }
|
|
2917
3036
|
function sxDayStart(t) { var d = new Date(t); d.setHours(0, 0, 0, 0); return d.getTime(); }
|
|
2918
3037
|
function sxSortTime(s) { return SX.clock === 'opened' ? (sxTime(s.created) || sxTime(s.modified)) : sxTime(s.modified); }
|
|
@@ -3017,6 +3136,13 @@
|
|
|
3017
3136
|
return;
|
|
3018
3137
|
}
|
|
3019
3138
|
if (!n) return;
|
|
3139
|
+
if (g[2] === 'pinned') {
|
|
3140
|
+
var popen = sxPinnedOpen();
|
|
3141
|
+
html += '<section class="sx-g" data-g="pinned"><button type="button" class="sx-gh" data-sx="fold" data-g="pinned" aria-expanded="' + popen + '">' +
|
|
3142
|
+
'<span class="fold"><span class="chev" aria-hidden="true"></span>' + g[1] + '</span><span class="n">' + n + '</span></button>' +
|
|
3143
|
+
(popen ? '<div class="sx-rows">' + rows.map(function (s) { return sxRow(s, null, amb, s.form === 'short'); }).join('') + '</div>' : '') + '</section>';
|
|
3144
|
+
return;
|
|
3145
|
+
}
|
|
3020
3146
|
if (g[2] === 'compact') {
|
|
3021
3147
|
var open = !!SXL.open[id];
|
|
3022
3148
|
html += '<section class="sx-g" data-g="' + id + '"><button type="button" class="sx-gh" data-sx="fold" data-g="' + id + '" aria-expanded="' + open + '">' +
|
|
@@ -3188,6 +3314,9 @@
|
|
|
3188
3314
|
(s.cos_pinned ? '<button type="button" role="menuitem" data-sx="org-pin" data-key="' + esc(key) + '">Unpin in COS</button>' : '')
|
|
3189
3315
|
: '<button type="button" role="menuitem" data-sx="org-pin" data-key="' + esc(key) + '">' + (s.cos_pinned ? 'Unpin' : 'Pin') + '</button>') +
|
|
3190
3316
|
'<button type="button" role="menuitem" data-sx="org-move-menu" data-key="' + esc(key) + '">Move to folder</button>' +
|
|
3317
|
+
// 7.0.29 ADD 2 (Miles 23:53: he could not find how to take a session out of a folder; "No folder" sat at the foot
|
|
3318
|
+
// of Move to folder): a row in a folder gets Remove from folder, the same write as No folder (folder_id null).
|
|
3319
|
+
(s.folder_id && SXF.byId[s.folder_id] ? '<button type="button" role="menuitem" data-sx="org-move" data-key="' + esc(key) + '" data-folder="">Remove from folder</button>' : '') +
|
|
3191
3320
|
'<button type="button" role="menuitem" data-sx="org-rename" data-key="' + esc(key) + '">Rename</button>';
|
|
3192
3321
|
}
|
|
3193
3322
|
function sxFolderMenuHtml(id) {
|
|
@@ -3491,6 +3620,7 @@
|
|
|
3491
3620
|
if (act === 'view') { sxViewMenu(false); return setSessionsView(t.getAttribute('data-view')); }
|
|
3492
3621
|
if (act === 'view-menu') return sxViewMenu();
|
|
3493
3622
|
if (act === 'goto-question') { sxToNewest(); sxGoQuestionPaint(); var gq = $('session-questions'), first = gq && gq.querySelector('button, input, [tabindex]'); if (first && first.focus) first.focus({ preventScroll: true }); return; }
|
|
3623
|
+
if (act === 'fold' && t.getAttribute('data-g') === 'pinned') { sxPinnedSet(!sxPinnedOpen()); return renderSessionGroups(); }
|
|
3494
3624
|
if (act === 'fold') { var g = t.getAttribute('data-g'), isF = g.indexOf('f:') === 0; SXL.open[g] = isF ? SXL.open[g] === false : !SXL.open[g]; return renderSessionGroups(); }
|
|
3495
3625
|
if (/^(row-menu|org-|folder-|new-folder|rename-clear|inline-cancel)/.test(act)) { e.stopPropagation(); return sxOrgClick(act, t); }
|
|
3496
3626
|
if (act === 'older-more') return sxLoadOlder(false, true);
|
|
@@ -3507,7 +3637,6 @@
|
|
|
3507
3637
|
if (act === 'clear') { var input = $('sessions-q'); input.value = ''; searchSessions(); input.focus(); return; }
|
|
3508
3638
|
if (act === 'copy-session') return copySessionBrief();
|
|
3509
3639
|
if (act === 'full') { if (SX_SPLIT) SX_SPLIT.toggleFull(); return; }
|
|
3510
|
-
if (act === 'refresh-turns' && rd) { rd.refusal = null; renderVerdict(rd); refreshTurns(rd.provider, rd.id); return; }
|
|
3511
3640
|
if (act === 'retry' && rd) { rd.refusal = null; renderVerdict(rd); return sendSessionTurn(!!rd.retryFork); }
|
|
3512
3641
|
if (act === 'caution-send' && rd) { rd.cautionAck = true; rd.cautionAsk = false; renderVerdict(rd); return sendSessionTurn(false); }
|
|
3513
3642
|
if (act === 'caution-cancel' && rd) { rd.cautionAsk = false; renderVerdict(rd); return; }
|
|
@@ -3803,7 +3932,7 @@
|
|
|
3803
3932
|
var timer = ctrl && o.timeoutMs ? setTimeout(function () { ctrl.abort(); }, o.timeoutMs) : null;
|
|
3804
3933
|
var headers = { 'X-COS-Web': '1' };
|
|
3805
3934
|
if (o.body !== undefined) headers['Content-Type'] = 'application/json';
|
|
3806
|
-
var sentAt = Date.now();
|
|
3935
|
+
var sentAt = Date.now(), writeDone = typeof writeStart === 'function' ? writeStart(o.method || 'GET') : function () {};
|
|
3807
3936
|
return fetch(path, { method: o.method || 'GET', headers: headers, credentials: 'same-origin', body: o.body !== undefined ? JSON.stringify(o.body) : undefined, signal: ctrl ? ctrl.signal : undefined })
|
|
3808
3937
|
.then(function (res) {
|
|
3809
3938
|
sxNoteClock(res, sentAt);
|
|
@@ -3816,7 +3945,7 @@
|
|
|
3816
3945
|
noteServer(false);
|
|
3817
3946
|
throw unreachableError(err);
|
|
3818
3947
|
})
|
|
3819
|
-
.finally(function () { if (timer) clearTimeout(timer); });
|
|
3948
|
+
.finally(function () { if (timer) clearTimeout(timer); writeDone(); });
|
|
3820
3949
|
}
|
|
3821
3950
|
|
|
3822
3951
|
// Per-browser memory for one session: messages held until a reply ends, and the queue keys
|
|
@@ -3925,6 +4054,8 @@
|
|
|
3925
4054
|
// edge says one waits. It sits in the reader, not the dock, because the dock scrolls by itself as a last resort.
|
|
3926
4055
|
'<p class="sx-goq" id="session-goq" hidden><span>A question is waiting at the end.</span><button type="button" class="cx-btn quiet" data-sx="goto-question">Go to the question</button></p>';
|
|
3927
4056
|
readerImagesMount($('session-turns'), r);
|
|
4057
|
+
if (typeof unsentRestore === 'function') unsentRestore(r);
|
|
4058
|
+
var si0 = $('session-input'); if (si0 && !si0.value && typeof autoGrow === 'function') autoGrow(si0);
|
|
3928
4059
|
sxNoteList(r, d);
|
|
3929
4060
|
setLive({});
|
|
3930
4061
|
if (typeof readerMacPaint === 'function') readerMacPaint(r, sxState(d) === 'waiting');
|
|
@@ -3942,6 +4073,8 @@
|
|
|
3942
4073
|
startHeldTimer(state.reading);
|
|
3943
4074
|
}
|
|
3944
4075
|
if (turns.length) requestAnimationFrame(function () { if (typeof sxToNewest === 'function') sxToNewest(); else window.scrollTo(0, document.documentElement.scrollHeight); });
|
|
4076
|
+
// 7.0.29 ADD 3: the newest turns he was just taken to count as shown (this frame runs after the one above).
|
|
4077
|
+
if (turns.length) requestAnimationFrame(function () { if (typeof sxMarkShown === 'function') sxMarkShown(r); });
|
|
3945
4078
|
requestAnimationFrame(function () { if (typeof sxFrameCrumb === 'function') sxFrameCrumb('open'); });
|
|
3946
4079
|
watchSession(provider, id);
|
|
3947
4080
|
}).catch(function (err) {
|
|
@@ -4230,6 +4363,53 @@
|
|
|
4230
4363
|
paintActivity(r);
|
|
4231
4364
|
if (moved) renderVerdict(r);
|
|
4232
4365
|
}
|
|
4366
|
+
// ── 7.0.29 ADD 3 (Miles 00:01 "we have to hit continue to steer the session"; 00:03 "Refresh doesn't seem to do
|
|
4367
|
+
// anything"; the lead's ruling 00:02 and A1 to A3 00:47): the changed-thread stop. The server guard is unchanged; the
|
|
4368
|
+
// page does the acknowledging. Shown, not just drawn (A1): a turn counts as shown only while the reader is at the
|
|
4369
|
+
// newest turn (following it, or scrolled to the end since). Read from the reader's own pin and scroll, no observer per
|
|
4370
|
+
// turn. Only turns with words count; tool steps are not turns here.
|
|
4371
|
+
function sxTextTurns(turns) { return (turns || []).filter(function (t) { return t && String(t.text || t.content || '').trim(); }); }
|
|
4372
|
+
function sxAtEnd() {
|
|
4373
|
+
try {
|
|
4374
|
+
var body = $('session-body');
|
|
4375
|
+
if (body && document.documentElement.classList.contains('sx-frame')) return SX_PIN.body === body || body.scrollTop >= body.scrollHeight - body.clientHeight - 4;
|
|
4376
|
+
var de = document.documentElement;
|
|
4377
|
+
return (window.scrollY || 0) + window.innerHeight >= de.scrollHeight - 4;
|
|
4378
|
+
} catch (_e) { return false; } // a page without these (a test harness): nothing counts as shown, so nothing resends
|
|
4379
|
+
}
|
|
4380
|
+
function sxMarkShown(r) {
|
|
4381
|
+
if (!r || state.reading !== r || !sxAtEnd()) return;
|
|
4382
|
+
r.shown = r.shown || {};
|
|
4383
|
+
sxTextTurns(r.turns).forEach(function (t) { r.shown[sxTurnSig(t)] = 1; });
|
|
4384
|
+
}
|
|
4385
|
+
// At Send: what he had been shown, and whether the session was already waiting on him.
|
|
4386
|
+
function sxPressSeen(r) {
|
|
4387
|
+
sxMarkShown(r);
|
|
4388
|
+
return { shown: Object.assign({}, r.shown || {}), waiting: !!r.waitingNow };
|
|
4389
|
+
}
|
|
4390
|
+
// After a changed-thread refusal: fetch the turns (the refreshTurns request), draw them, and say what is new since Send:
|
|
4391
|
+
// turns with words he had not been shown, or a question or permission now waiting that was not (A2). null: the fetch
|
|
4392
|
+
// failed, so nothing can be said and the box shows.
|
|
4393
|
+
function sxChangedLook(r, seen) {
|
|
4394
|
+
return api(sessionPath(r.provider, r.id, '?turns=' + SX_TURNS + '&cards=1')).then(function (d) {
|
|
4395
|
+
if (state.reading !== r) return null;
|
|
4396
|
+
var turns = Array.isArray(d && d.recent_turns) ? d.recent_turns : [];
|
|
4397
|
+
var box = $('session-turns'); if (box) { sxMergeTurns(box, r.turns || [], turns, d); readerImagesMount(box, r); }
|
|
4398
|
+
r.detail = d; r.turns = turns; r.more = d.recent_turns_more === true;
|
|
4399
|
+
var fresh = sxTextTurns(turns).filter(function (t) { return !seen.shown[sxTurnSig(t)]; });
|
|
4400
|
+
// Waiting by the reader's own rule, or by the detail's own word (a session read as stale may still say it waits):
|
|
4401
|
+
// either way stopping is the safe side.
|
|
4402
|
+
return { fresh: fresh, newWait: (sxState(d) === 'waiting' || (d && d.agent_state === 'waiting')) && !seen.waiting };
|
|
4403
|
+
}, function () { return null; });
|
|
4404
|
+
}
|
|
4405
|
+
// Take him to the first new turn: it is drawn, and the stop is about it.
|
|
4406
|
+
function sxShowFresh(fresh) {
|
|
4407
|
+
if (!fresh || !fresh.length) return;
|
|
4408
|
+
var sig = sxTurnSig(fresh[0]), el = null;
|
|
4409
|
+
document.querySelectorAll('#session-turns .turn[data-sig]').forEach(function (n) { if (!el && n.getAttribute('data-sig') === sig) el = n; });
|
|
4410
|
+
sxUnpin();
|
|
4411
|
+
if (el && el.scrollIntoView) el.scrollIntoView({ block: 'start' }); else sxToNewest();
|
|
4412
|
+
}
|
|
4233
4413
|
function refreshTurns(provider, id) {
|
|
4234
4414
|
var reading = state.reading;
|
|
4235
4415
|
api(sessionPath(provider, id, '?turns=' + SX_TURNS + '&cards=1')).then(function (d) {
|
|
@@ -4239,6 +4419,7 @@
|
|
|
4239
4419
|
var box = $('session-turns'); if (box) { sxMergeTurns(box, r.turns || [], turns, d); readerImagesMount(box, r); }
|
|
4240
4420
|
// The header keeps the subtitle it opened with: a rewrap above the turns would move them.
|
|
4241
4421
|
r.detail = d; r.turns = turns; r.more = d.recent_turns_more === true;
|
|
4422
|
+
sxMarkShown(r);
|
|
4242
4423
|
renderHeld(r);
|
|
4243
4424
|
loadQueue(provider, id);
|
|
4244
4425
|
sxProbeAttach(r);
|
|
@@ -4362,9 +4543,10 @@
|
|
|
4362
4543
|
} else if (r.refusal) {
|
|
4363
4544
|
var f = r.refusal;
|
|
4364
4545
|
html = refusalBox(f.copy, f.supplement,
|
|
4365
|
-
|
|
4546
|
+
// 7.0.29 ADD 3: a changed thread says what each button does. The page has already refreshed, so no Refresh.
|
|
4547
|
+
(f.changed ? '<button type="button" class="cx-btn quiet" data-action="send-anyway">Send it anyway</button>' : '') +
|
|
4366
4548
|
(f.retry ? '<button type="button" class="cx-btn quiet" data-sx="retry">Retry</button>' : '') +
|
|
4367
|
-
(f.fork ? forkBtn : ''));
|
|
4549
|
+
(f.fork ? (f.changed ? '<button type="button" class="cx-btn quiet" data-action="fork-session" title="Runs your message in a copy of this thread. The original is untouched.">Send to a copy</button>' : forkBtn) : ''));
|
|
4368
4550
|
} else if (v && v.attachable === false && /^(native_thread_working|native_target_busy)$/.test(v.reason)) {
|
|
4369
4551
|
// 6.99.29: "Working right now" only while the one state says so.
|
|
4370
4552
|
if (sxParkNoteShows(r)) html = '<div class="sx-park"><p>' + esc(sxParkLine(r)) + '</p>' + (/fork/i.test(v.reasonCopy) ? '<div class="sx-box-actions">' + forkBtn + '</div>' : '') + '</div>';
|
|
@@ -4399,18 +4581,51 @@
|
|
|
4399
4581
|
if (!r.draft || r.draft.text !== text || (r.draft.ids || []).join(',') !== files.join(',')) r.draft = { text: text, key: newTurnKey(), ids: files };
|
|
4400
4582
|
return r.draft;
|
|
4401
4583
|
}
|
|
4402
|
-
//
|
|
4403
|
-
|
|
4584
|
+
// 7.0.29 (lead F3: the rail's Back sits level with Send, so a mis-tap is likely): words typed in a session's reply
|
|
4585
|
+
// box and not sent come back when that session opens again in this tab, after Back, the rail or another pane. Kept in
|
|
4586
|
+
// memory, per session, as he types; dropped when the box is empty or when those words are sent.
|
|
4587
|
+
var SX_UNSENT = {};
|
|
4588
|
+
function unsentKey(provider, id) { return String(provider || '') + '|' + String(id || '').toLowerCase(); }
|
|
4589
|
+
function unsentSave() {
|
|
4590
|
+
var r = state.reading, input = $('session-input');
|
|
4591
|
+
if (!r || !input) return;
|
|
4592
|
+
var k = unsentKey(r.provider, r.id);
|
|
4593
|
+
if (input.value.trim()) SX_UNSENT[k] = input.value; else delete SX_UNSENT[k];
|
|
4594
|
+
}
|
|
4595
|
+
function unsentRestore(r) {
|
|
4596
|
+
var input = $('session-input'), v = r ? SX_UNSENT[unsentKey(r.provider, r.id)] : '';
|
|
4597
|
+
if (input && v && !input.value) { input.value = v; autoGrow(input); }
|
|
4598
|
+
}
|
|
4599
|
+
// 7.0.29 R4 (Q1 Note 1): the saved words of the session the message was sent FROM only, never another session's
|
|
4600
|
+
// words that happen to match. No session named, nothing is dropped.
|
|
4601
|
+
function unsentDrop(text, r) {
|
|
4602
|
+
if (!r || !r.provider || !r.id) return;
|
|
4603
|
+
var k = unsentKey(r.provider, r.id), v = SX_UNSENT[k];
|
|
4604
|
+
if (v == null) return;
|
|
4605
|
+
var t = String(text).trim(), bare = typeof turnEndBare === 'function' ? turnEndBare(text) : t;
|
|
4606
|
+
v = String(v).trim();
|
|
4607
|
+
if (v === t || v === bare) delete SX_UNSENT[k];
|
|
4608
|
+
}
|
|
4609
|
+
// The box is cleared only of the very words that were sent; anything typed since stays. `r` is the session they were
|
|
4610
|
+
// sent from: another session's box is never touched (R4).
|
|
4611
|
+
function clearBoxOf(text, r) {
|
|
4612
|
+
if (typeof unsentDrop === 'function') unsentDrop(text, r || state.reading);
|
|
4613
|
+
if (r && state.reading !== r) return;
|
|
4404
4614
|
var input = $('session-input');
|
|
4405
4615
|
// 7.0.18: an answer to a turn-end ask was sent with the attribution in front; the box holds his words without it.
|
|
4406
4616
|
if (input && (input.value.trim() === String(text).trim() || input.value.trim() === turnEndBare(text))) { input.value = ''; autoGrow(input); }
|
|
4407
4617
|
if (state.reading && state.reading.draft && state.reading.draft.text === text) state.reading.draft = null;
|
|
4408
4618
|
}
|
|
4409
4619
|
// Words go back in the box only if the box is empty; otherwise the status line keeps them. `files` says what became
|
|
4410
|
-
// of the message's files (heldFilesBack).
|
|
4411
|
-
|
|
4620
|
+
// of the message's files (heldFilesBack). 7.0.29 R4: when `r`, the session they came from, is not the one open, they
|
|
4621
|
+
// go to that session's saved words (back in its box when it opens), never into the open session's box.
|
|
4622
|
+
function returnToBox(text, why, files, r) {
|
|
4623
|
+
if (r && r.provider && r.id && state.reading !== r) {
|
|
4624
|
+
if (typeof unsentKey === 'function') { var k = unsentKey(r.provider, r.id), had = SX_UNSENT[k]; SX_UNSENT[k] = had && String(had).trim() ? had + '\n\n' + text : text; }
|
|
4625
|
+
return;
|
|
4626
|
+
}
|
|
4412
4627
|
var input = $('session-input');
|
|
4413
|
-
if (input && !input.value.trim()) { input.value = text; autoGrow(input); sessionStatus(why + ' Your message is back in the box.' + (files || ''), 'error'); }
|
|
4628
|
+
if (input && !input.value.trim()) { input.value = text; autoGrow(input); if (typeof unsentSave === 'function') unsentSave(); sessionStatus(why + ' Your message is back in the box.' + (files || ''), 'error'); }
|
|
4414
4629
|
else sessionStatus(why + ' Not sent: “' + String(text).slice(0, 80) + (text.length > 80 ? '…' : '') + '”' + (files || ''), 'error');
|
|
4415
4630
|
}
|
|
4416
4631
|
// Your words show in the conversation the moment the Mac accepts them; the next refresh of
|
|
@@ -4437,7 +4652,7 @@
|
|
|
4437
4652
|
var READER_VIEW = {
|
|
4438
4653
|
status: function (r, text, kind, actionHtml) { sessionStatus(text, kind, actionHtml); },
|
|
4439
4654
|
pending: function (r, text, note) { showPendingTurn(text, note); },
|
|
4440
|
-
clear: function (r, text) { clearBoxOf(text); },
|
|
4655
|
+
clear: function (r, text) { clearBoxOf(text, r); },
|
|
4441
4656
|
verdict: function (r) { renderVerdict(r); },
|
|
4442
4657
|
hold: function (r, draft) { holdUntilReplyEnds(r, draft); },
|
|
4443
4658
|
watch: function (r, bindingId, key) { watchTurn(bindingId, key, r.provider, r.id); },
|
|
@@ -4552,7 +4767,11 @@
|
|
|
4552
4767
|
if (r.refusal || r.cautionAsk) { r.refusal = null; r.cautionAsk = false; renderVerdict(r); }
|
|
4553
4768
|
// 7.0.18: while a turn-end ask is open, the box's words are his own answer ('Other' in the contract): sent with the
|
|
4554
4769
|
// same attribution as a choice. A fork goes to a copy and is not an answer, so it is sent as written.
|
|
4555
|
-
|
|
4770
|
+
var answering = !!(!fork && r.turnEnd && turnEndOf(readerRowOf(r)));
|
|
4771
|
+
if (answering) text = turnEndPrompt(text);
|
|
4772
|
+
// 7.0.29 ADD 3: what he had been shown when he pressed Send. A turn-end answer never resends by itself; R6 (Q1's nit):
|
|
4773
|
+
// the same test as the answer's prefix (r.turnEnd AND the ask still open), so a message after a closed ask is a message.
|
|
4774
|
+
if (!fork && typeof sxPressSeen === 'function') opts = Object.assign({}, opts || {}, { seen: answering ? null : sxPressSeen(r) });
|
|
4556
4775
|
r.sending = true;
|
|
4557
4776
|
var btns = form.querySelectorAll('button'); btns.forEach(function (b) { b.disabled = true; });
|
|
4558
4777
|
var done = function () { r.sending = false; btns.forEach(function (b) { b.disabled = false; }); paintReaderFiles(); };
|
|
@@ -4633,14 +4852,25 @@
|
|
|
4633
4852
|
// it under the same key and sends it the moment the reply ends.
|
|
4634
4853
|
if (reason === 'native_turn_in_progress') { if (!opts.held) view.hold(r, draft); return 'held'; }
|
|
4635
4854
|
if (reason === 'native_thread_changed' && b.revision) {
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4855
|
+
// The box. Send it anyway always carries the revision of THIS refusal, the newest (A3).
|
|
4856
|
+
var changedBox = function (copy) { r.sendAnyway = b.revision; view.status(r, ''); r.refusal = { copy: copy, changed: true, fork: true }; view.verdict(r); };
|
|
4857
|
+
if (opts.held) { settleHeld(r, opts.held, 'refused', 'This session changed on your Mac while your message waited.'); return 'changed'; }
|
|
4858
|
+
// 7.0.29 ADD 3, the session reader only (opts.seen; Today's panel keeps its own path): never for a held or
|
|
4859
|
+
// queued message, a turn-end answer (seen is null), or a send that already carries a revision, which the one
|
|
4860
|
+
// automatic resend always does. So at most one resend, and a second refusal shows the box.
|
|
4861
|
+
if (opts.seen && !opts.acknowledgedRevision && state.reading === r) {
|
|
4862
|
+
return sxChangedLook(r, opts.seen).then(function (look) {
|
|
4863
|
+
if (look && !look.fresh.length && !look.newWait && state.reading === r) return continueSession(r, text, Object.assign({}, opts, { acknowledgedRevision: b.revision }));
|
|
4864
|
+
changedBox(!look ? 'This session changed on your Mac, and COS could not load what changed. Your message is not sent yet.'
|
|
4865
|
+
: look.fresh.length ? 'A new reply arrived while you were writing. Your message is not sent yet.'
|
|
4866
|
+
: 'The session is now waiting on a question. Your message is not sent yet.');
|
|
4867
|
+
if (look) sxShowFresh(look.fresh);
|
|
4868
|
+
return 'changed';
|
|
4869
|
+
});
|
|
4643
4870
|
}
|
|
4871
|
+
changedBox(opts.seen === undefined ? (b.reasonCopy || 'This session changed on your Mac since you opened it.')
|
|
4872
|
+
: opts.seen === null ? 'This session changed on your Mac. Your answer is not sent yet.'
|
|
4873
|
+
: 'This session changed on your Mac again. Your message is not sent yet.');
|
|
4644
4874
|
return 'changed';
|
|
4645
4875
|
}
|
|
4646
4876
|
if (/^(stale_epoch|binding_expired|binding_not_active|binding_unusable|unknown_binding)$/.test(reason) && !opts.reattached) {
|
|
@@ -4861,7 +5091,7 @@
|
|
|
4861
5091
|
showPendingTurn(draft.text, 'sends after this reply', draft.key);
|
|
4862
5092
|
saveHeld(r);
|
|
4863
5093
|
}
|
|
4864
|
-
clearBoxOf(draft.text);
|
|
5094
|
+
clearBoxOf(draft.text, r);
|
|
4865
5095
|
sessionStatus('COS is still answering. ' + (r.held.length > 1 ? r.held.length + ' messages send' : 'Yours sends') + ' as soon as it finishes. ', null, '<button type="button" class="link-btn" data-action="drop-after">Don’t send</button>');
|
|
4866
5096
|
startHeldTimer(r);
|
|
4867
5097
|
}
|
|
@@ -4894,7 +5124,7 @@
|
|
|
4894
5124
|
} else {
|
|
4895
5125
|
if (bubble) bubble.remove();
|
|
4896
5126
|
// The whole message comes back: its words, and its files where the Mac still keeps them.
|
|
4897
|
-
returnToBox(h.text, how === 'cancelled' ? 'Not sent.' : copy || 'Not sent.', heldFilesBack(r, h));
|
|
5127
|
+
returnToBox(h.text, how === 'cancelled' ? 'Not sent.' : copy || 'Not sent.', heldFilesBack(r, h), r);
|
|
4898
5128
|
}
|
|
4899
5129
|
if (!r.held.some(function (x) { return !x.cancelled; })) clearInterval(r.heldTimer);
|
|
4900
5130
|
}
|
|
@@ -4966,7 +5196,7 @@
|
|
|
4966
5196
|
sessionStatus('Copying the session. This can take up to a minute.');
|
|
4967
5197
|
var settle = function (b) {
|
|
4968
5198
|
if (b.forked === true) {
|
|
4969
|
-
r.forkDraft = null; clearBoxOf(text); readerFilesSent(readerFilesOn() ? readerBox(r) : null, ids);
|
|
5199
|
+
r.forkDraft = null; clearBoxOf(text, r); readerFilesSent(readerFilesOn() ? readerBox(r) : null, ids);
|
|
4970
5200
|
sessionStatus('Forked. The copy has your message and shows in Sessions in a moment.');
|
|
4971
5201
|
setTimeout(function () { if (state.pane === 'sessions') loadSessions(true); }, 4000);
|
|
4972
5202
|
return true;
|
|
@@ -7136,10 +7366,11 @@
|
|
|
7136
7366
|
|
|
7137
7367
|
// ── events (one delegated listener) ─────────────────────
|
|
7138
7368
|
document.addEventListener('click', function (e) {
|
|
7139
|
-
var t = e.target.closest('[data-today-next],[data-next-prep],[data-next-prep-cancel],[data-next-prep-remove],[data-today-needs],[data-today],[data-goto],[data-pane],[data-session],[data-meeting],[data-speakers],[data-action],[data-open-ask],[data-theme-choice],#topbar-back,#section-btn,#sign-out,#copy-link,#ask-close,#auth-switch,#auth-token-show,#auth-retry,#device-code-btn');
|
|
7369
|
+
var t = e.target.closest('[data-today-next],[data-next-prep],[data-next-prep-cancel],[data-next-prep-remove],[data-today-needs],[data-today],[data-goto],[data-pane],[data-session],[data-meeting],[data-speakers],[data-action],[data-open-ask],[data-theme-choice],#topbar-back,#rail-back,#section-btn,#sign-out,#copy-link,#ask-close,#auth-switch,#auth-token-show,#auth-retry,#device-code-btn');
|
|
7140
7370
|
if (!t) { if (e.target.id === 'ask-sheet') closeAsk(); if (e.target.id === 'section-menu') closeMenu(); return; }
|
|
7141
7371
|
var inSheet = !!t.closest('#ask-sheet');
|
|
7142
7372
|
if (t.id === 'topbar-back') return goBack();
|
|
7373
|
+
if (t.id === 'rail-back') return goBack();
|
|
7143
7374
|
if (t.id === 'section-btn') return openMenu();
|
|
7144
7375
|
if (t.id === 'auth-retry') { t.disabled = true; t.textContent = 'Checking'; bootStatus().then(function () { t.disabled = false; t.textContent = 'Try again'; }); return; }
|
|
7145
7376
|
if (t.id === 'auth-switch') {
|
|
@@ -7284,6 +7515,7 @@
|
|
|
7284
7515
|
});
|
|
7285
7516
|
document.addEventListener('input', function (e) {
|
|
7286
7517
|
if (e.target.tagName === 'TEXTAREA') autoGrow(e.target);
|
|
7518
|
+
if (e.target.id === 'session-input' && typeof unsentSave === 'function') unsentSave();
|
|
7287
7519
|
if (e.target.id === 'ask-q') renderAsk();
|
|
7288
7520
|
if (e.target.hasAttribute && e.target.hasAttribute('data-next-prep-steer')) nextPrepInput(e.target);
|
|
7289
7521
|
if (e.target.id === 'sessions-q' && !e.target.value.trim()) renderSessionGroups(state.sessions);
|
|
@@ -7507,6 +7739,47 @@
|
|
|
7507
7739
|
} catch (_e) { return false; }
|
|
7508
7740
|
}
|
|
7509
7741
|
|
|
7742
|
+
// Page errors (lead 08:31, 11/11): Control Web reported none, so "no errors in his rows" proved nothing. A script error
|
|
7743
|
+
// or an unhandled rejection becomes one bounded row, event web.error: what kind it was, the error's class (TypeError),
|
|
7744
|
+
// its code and HTTP status when it has them, the script file's own name, line and column, and the pane. Never the
|
|
7745
|
+
// message, a stack or any text: those can carry what a person typed. At most WEB_ERR_MAX rows a page load, one per
|
|
7746
|
+
// kind, class, code and place.
|
|
7747
|
+
var WEB_ERR_MAX = 10, WEB_ERR = { n: 0, seen: {} };
|
|
7748
|
+
function webErrorRow(kind, err, file, line, col) {
|
|
7749
|
+
try {
|
|
7750
|
+
var name = err && typeof err.name === 'string' ? err.name : '';
|
|
7751
|
+
var cls = /^[A-Za-z][A-Za-z0-9]{0,39}$/.test(name) ? name : (err === null || err === undefined ? 'none' : typeof err === 'object' ? 'object' : typeof err);
|
|
7752
|
+
var code = err && (typeof err.code === 'string' || typeof err.code === 'number') && /^[A-Za-z0-9_.-]{1,40}$/.test(String(err.code)) ? String(err.code) : '';
|
|
7753
|
+
var status = err && typeof err.status === 'number' && isFinite(err.status) ? err.status : null;
|
|
7754
|
+
var src = String(file || '').split(/[?#]/)[0].split('/').pop();
|
|
7755
|
+
src = /^[A-Za-z0-9_.-]{1,40}$/.test(src) ? src : '';
|
|
7756
|
+
var ln = Number(line) > 0 ? Math.floor(Number(line)) : 0, cn = Number(col) > 0 ? Math.floor(Number(col)) : 0;
|
|
7757
|
+
var key = [kind, cls, code, status, src, ln].join('|');
|
|
7758
|
+
if (WEB_ERR.seen[key] || WEB_ERR.n >= WEB_ERR_MAX) return false;
|
|
7759
|
+
WEB_ERR.seen[key] = 1; WEB_ERR.n++;
|
|
7760
|
+
var pane = typeof state === 'object' && state && typeof state.pane === 'string' && /^[a-z-]{1,24}$/.test(state.pane) ? state.pane : '';
|
|
7761
|
+
api('/api/diag/client', { method: 'POST', body: { ts: Date.now(), level: 'error', event: 'web.error',
|
|
7762
|
+
data: { kind: kind, cls: cls, code: code, status: status, src: src, line: ln, col: cn, pane: pane, n: WEB_ERR.n } }, timeoutMs: 5000 }).catch(function () {});
|
|
7763
|
+
return true;
|
|
7764
|
+
} catch (_e) { return false; }
|
|
7765
|
+
}
|
|
7766
|
+
if (window.addEventListener) {
|
|
7767
|
+
// Only script errors: a failed image or file load fires 'error' on its element, which does not reach the window.
|
|
7768
|
+
window.addEventListener('error', function (e) { if (e && (e.error || e.filename)) webErrorRow('error', e.error || null, e.filename, e.lineno, e.colno); });
|
|
7769
|
+
window.addEventListener('unhandledrejection', function (e) { webErrorRow('rejection', e ? e.reason : null, '', 0, 0); });
|
|
7770
|
+
}
|
|
7771
|
+
|
|
7772
|
+
// 7.0.28: when theme.js had to bring back a stylesheet or a script (the page opened before it could reach the Mac), one
|
|
7773
|
+
// numbers-only row says so: how ('sheets', 'reload' or 'gave-up'), how many sheets and scripts had failed, how many
|
|
7774
|
+
// checks it took and how long. A reload's report comes back from sessionStorage once.
|
|
7775
|
+
(function restyleRow() {
|
|
7776
|
+
var R = window.CosRestyle;
|
|
7777
|
+
if (!R) return;
|
|
7778
|
+
var row = function (d) { if (d && typeof d === 'object') layoutCrumb('restyle', { how: String(d.how || '').slice(0, 24), sheets: +d.sheets || 0, scripts: +d.scripts || 0, tries: +d.tries || 0, ms: +d.ms || 0 }); };
|
|
7779
|
+
row(R.takeSaved && R.takeSaved());
|
|
7780
|
+
window.addEventListener('cos-restyle', function (e) { row(e && e.detail); });
|
|
7781
|
+
})();
|
|
7782
|
+
|
|
7510
7783
|
// ── the shared kit for pane modules (panes/*.js) ───────
|
|
7511
7784
|
window.COSWeb = {
|
|
7512
7785
|
$: $, esc: esc, md: md, plain: plain, structureLensAnswer: structureLensAnswer, tableFit: tableFit, api: api, toast: toast, ago: ago, clock: clock, toDate: toDate,
|
|
@@ -7679,7 +7952,7 @@
|
|
|
7679
7952
|
var sc = window.screen || {}, w = lrWide();
|
|
7680
7953
|
layoutCrumb('device', { sw: sc.width, sh: sc.height, sah: sc.availHeight, oh: window.outerHeight, ow: window.outerWidth, dpr: window.devicePixelRatio,
|
|
7681
7954
|
sat: lrProbe('padding-top:env(safe-area-inset-top)', 'paddingTop'), sab: lrProbe('padding-bottom:env(safe-area-inset-bottom)', 'paddingBottom'),
|
|
7682
|
-
anyCoarse: lrMatch('(any-pointer: coarse)'), shortTouch: lrMatch(SHORT_TOUCH_MQ), phone: isPhone(), cw: w.cw, dw: w.dw, scw: w.scw, wide: w.wide });
|
|
7955
|
+
anyCoarse: lrMatch('(any-pointer: coarse)'), shortTouch: lrMatch(SHORT_TOUCH_MQ), rail: typeof sideRail === 'function' ? sideRail() : false, railHit: typeof RAIL === 'object' ? RAIL.hit : 0, phone: isPhone(), cw: w.cw, dw: w.dw, scw: w.scw, wide: w.wide });
|
|
7683
7956
|
}
|
|
7684
7957
|
// The focus line is for touch screens only (QA W3): on a Mac it would only spend the budget.
|
|
7685
7958
|
function lrFocus() {
|
|
@@ -7774,6 +8047,7 @@
|
|
|
7774
8047
|
|
|
7775
8048
|
// Every pane module is a plain script after this one; boot waits for all of them.
|
|
7776
8049
|
function boot() {
|
|
8050
|
+
if (typeof railSync === 'function') railSync(); // 7.0.29: before the shell shows, so the rail never moves the page
|
|
7777
8051
|
// The open-time report's clock (ms since navigation start, -1 where there is none) and the two numbers boot gives it:
|
|
7778
8052
|
// when boot started (every script had run) and the sign-in check's round trip. Kept inside boot, and FIRST_PAINT
|
|
7779
8053
|
// looked up, never assumed, because the harnesses that run boot alone have neither.
|
|
@@ -7822,20 +8096,27 @@
|
|
|
7822
8096
|
return api('/api/web-session/status').then(function (st) { return bootAnswer(st); }, function (err) { return bootFail(err); });
|
|
7823
8097
|
}
|
|
7824
8098
|
function bootAnswer(st) {
|
|
8099
|
+
AUTH_RETRY.n = 0; // the Mac answered: a later not-answering card starts its quick checks again
|
|
7825
8100
|
if (st && st.authenticated) return showShell(st);
|
|
7826
8101
|
// Q1 S2: a browser that was signed in before learns why it is not now (the 30 days, no use in its last 2 days, a sign-out).
|
|
7827
8102
|
showAuth(typeof signinKnown === 'function' && signinKnown() ? signinLapsedCopy() : undefined);
|
|
7828
8103
|
}
|
|
7829
8104
|
function bootFail(err) {
|
|
7830
8105
|
// F23: a Mac that did not answer is not a sign-in problem. Say so, offer Try again, keep the form out of the way.
|
|
7831
|
-
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
|
|
8106
|
+
// showAuth turns the card off first (authOffline(false)); that must not restart the 1, 2, 4, 8 s steps, so only a Mac
|
|
8107
|
+
// that answered (bootAnswer, or an error with a status here) resets them.
|
|
8108
|
+
if (err && err.status) { AUTH_RETRY.n = 0; showAuth(err.message); } else { showAuth(); authOffline(true); }
|
|
8109
|
+
}
|
|
8110
|
+
// Q1 M2: the not-answering card checks again while the page is in front, and stops while it is hidden. 7.0.28 (Miles
|
|
8111
|
+
// 19:28): the first checks come after 1, 2, 4 and 8 s, because a phone that opened the page before its tunnel was up
|
|
8112
|
+
// usually reaches the Mac within seconds; then every 30 s. The steps start over once the Mac has answered.
|
|
8113
|
+
var AUTH_RETRY = { timer: null, ms: 30000, steps: [1000, 2000, 4000, 8000], n: 0 };
|
|
7835
8114
|
function authRetrySchedule(on) {
|
|
7836
8115
|
if (AUTH_RETRY.timer) { clearTimeout(AUTH_RETRY.timer); AUTH_RETRY.timer = null; }
|
|
7837
8116
|
if (!on || document.hidden) return;
|
|
7838
|
-
|
|
8117
|
+
var ms = AUTH_RETRY.n < AUTH_RETRY.steps.length ? AUTH_RETRY.steps[AUTH_RETRY.n] : AUTH_RETRY.ms;
|
|
8118
|
+
AUTH_RETRY.n++;
|
|
8119
|
+
AUTH_RETRY.timer = setTimeout(function () { AUTH_RETRY.timer = null; if (!document.hidden) bootStatus(); }, ms);
|
|
7839
8120
|
}
|
|
7840
8121
|
document.addEventListener('visibilitychange', function () {
|
|
7841
8122
|
var box = $('auth-offline');
|
|
@@ -7853,8 +8134,8 @@
|
|
|
7853
8134
|
if (title) title.textContent = on ? (offline ? 'This device is offline' : 'COS on your Mac is not answering') : 'Connect this browser';
|
|
7854
8135
|
if (intro) intro.classList.toggle('hidden', !!on);
|
|
7855
8136
|
var text = $('auth-offline-text');
|
|
7856
|
-
if (text && on) text.textContent = offline ? 'Connect to the internet and Tailscale. This page checks again every 30 seconds.'
|
|
7857
|
-
: 'Your Mac may be asleep, or this device may be offline or off Tailscale. This page checks again every 30 seconds.';
|
|
8137
|
+
if (text && on) text.textContent = offline ? 'Connect to the internet and Tailscale. This page checks again in a few seconds, then every 30 seconds.'
|
|
8138
|
+
: 'Your Mac may be asleep, or this device may be offline or off Tailscale. This page checks again in a few seconds, then every 30 seconds.';
|
|
7858
8139
|
authRetrySchedule(!!on);
|
|
7859
8140
|
if (on) { var err = $('auth-error'); if (err) { setStatus(err, '', true); err.classList.add('hidden'); } }
|
|
7860
8141
|
}
|