@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
|
@@ -13,6 +13,42 @@
|
|
|
13
13
|
<link rel="icon" href="/control/mark.svg" type="image/svg+xml">
|
|
14
14
|
<!-- iOS "Add to Home Screen" ignores SVG icons: without this, a saved Control Web shortcut is a grey letter. -->
|
|
15
15
|
<link rel="apple-touch-icon" href="/control/apple-touch-icon.png">
|
|
16
|
+
<!-- 7.0.28: the sign-in card and the first paint must look right before, or without, the 10 stylesheets below
|
|
17
|
+
(Miles 19:28: "Connect this browser" with no styling, the mark twice at full size, when the phone opened the page
|
|
18
|
+
before it could reach the Mac). These rules are copies of app.css, word for word (a test compares them), so the
|
|
19
|
+
stylesheets that follow change nothing when they arrive; only the tokens it needs, and the dark set also follows
|
|
20
|
+
the system when theme.js has not run. theme.js retries a stylesheet that failed. -->
|
|
21
|
+
<style id="critical-css">
|
|
22
|
+
:root { --page: #f5f0e6; --ink: #1f1712; --sub: #66635e; --panel: #ffffff; --panel-border: #e3d9c6; --accent: #89662d; --on-accent: #ffffff; --btn-bg: #ffffff; --btn-border: #a08a66; --grey: #5c5a55; --display: 'Fraunces', 'Iowan Old Style', Georgia, serif; --body: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif; --gutter: 16px; --radius: 12px; --safe-top: env(safe-area-inset-top, 0px); --safe-bottom: env(safe-area-inset-bottom, 0px); }
|
|
23
|
+
:root[data-theme='dark'] { --page: #17120e; --ink: #efe7dc; --sub: #b3a796; --panel: #221b15; --panel-border: #3a2f25; --accent: #d9b06a; --on-accent: #17120e; --btn-bg: #221b15; --btn-border: #7a6650; --grey: #a39887; }
|
|
24
|
+
@media (prefers-color-scheme: dark) { :root:not([data-theme]) { --page: #17120e; --ink: #efe7dc; --sub: #b3a796; --panel: #221b15; --panel-border: #3a2f25; --accent: #d9b06a; --on-accent: #17120e; --btn-bg: #221b15; --btn-border: #7a6650; --grey: #a39887; } }
|
|
25
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
26
|
+
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; background: var(--page); }
|
|
27
|
+
body { margin: 0; min-height: 100dvh; background: var(--page); color: var(--ink); font: 400 16px/1.45 var(--body); -webkit-font-smoothing: antialiased; -webkit-tap-highlight-color: transparent; }
|
|
28
|
+
button, input, select, textarea { font: inherit; color: inherit; }
|
|
29
|
+
button { cursor: pointer; background: none; border: 0; padding: 0; text-align: inherit; }
|
|
30
|
+
.hidden { display: none !important; }
|
|
31
|
+
.mark { width: 28px; height: 28px; color: var(--ink); flex: none; }
|
|
32
|
+
.first-paint { display: none; }
|
|
33
|
+
html.fp-wait .first-paint { position: fixed; inset: 0; z-index: 70; display: grid; place-items: center; background: var(--page); color: var(--ink); pointer-events: none; animation: fp-failsafe 1ms linear 25s forwards; }
|
|
34
|
+
.first-paint .mark { width: 44px; height: 44px; }
|
|
35
|
+
@keyframes fp-failsafe { to { visibility: hidden; } }
|
|
36
|
+
.noscript-note { margin: 0; min-height: 100dvh; display: grid; place-items: center; padding: 24px var(--gutter); font-size: 17px; color: var(--ink); text-align: center; }
|
|
37
|
+
.auth-gate { min-height: 100dvh; display: grid; place-items: center; padding: calc(var(--safe-top) + 24px) var(--gutter) calc(var(--safe-bottom) + 24px); }
|
|
38
|
+
.auth-card { width: 100%; max-width: 420px; }
|
|
39
|
+
.auth-card .mark { width: 44px; height: 44px; margin-bottom: 22px; }
|
|
40
|
+
.auth-card h1 { font: 500 40px/1.05 var(--display); margin: 0 0 10px; letter-spacing: -0.01em; }
|
|
41
|
+
.auth-card p { color: var(--sub); margin: 0 0 24px; }
|
|
42
|
+
.auth-card label { display: block; font-weight: 600; font-size: 15px; margin-bottom: 8px; }
|
|
43
|
+
.auth-card .btn { margin-top: 12px; }
|
|
44
|
+
.auth-foot { font-size: 14px; color: var(--sub); margin-top: 18px; }
|
|
45
|
+
.auth-offline p { color: var(--sub); }
|
|
46
|
+
.field { width: 100%; min-height: 48px; padding: 12px 14px; border-radius: var(--radius); border: 1px solid var(--panel-border); background: var(--panel); font-size: 16px; /* 16px keeps iOS Safari from zooming on focus */ }
|
|
47
|
+
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 0 18px; border-radius: var(--radius); border: 1px solid var(--btn-border); background: var(--btn-bg); font-weight: 600; font-size: 15px; white-space: nowrap; transition: transform 120ms ease; }
|
|
48
|
+
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
|
49
|
+
.btn.block { width: 100%; }
|
|
50
|
+
.link-btn { color: var(--accent); font-weight: 600; font-size: 15px; min-height: 44px; display: inline-flex; align-items: center; }
|
|
51
|
+
</style>
|
|
16
52
|
<link rel="stylesheet" href="/control/app.css">
|
|
17
53
|
<link rel="stylesheet" href="/control/control.css">
|
|
18
54
|
<link rel="stylesheet" href="/control/panes/speakers.css">
|
|
@@ -64,16 +100,16 @@
|
|
|
64
100
|
<!-- 7.0.19: the first paint. Shows at once, before any script or answer from the Mac: the mark on the page's own
|
|
65
101
|
background, true whichever comes next (the sign-in card or the app). Out of the layout (fixed), so nothing moves
|
|
66
102
|
when it goes. app.js removes it; a CSS failsafe hides it after 25 s if the scripts never run. -->
|
|
67
|
-
<div id="first-paint" class="first-paint" aria-hidden="true"><svg class="mark"><use href="#i-mark"/></svg></div>
|
|
103
|
+
<div id="first-paint" class="first-paint" aria-hidden="true"><svg class="mark" width="44" height="44"><use href="#i-mark"/></svg></div>
|
|
68
104
|
<noscript><p class="noscript-note">COS Control Web needs JavaScript. Turn it on for this site, then reload.</p></noscript>
|
|
69
105
|
|
|
70
106
|
<main id="auth-gate" class="auth-gate hidden" aria-labelledby="auth-title">
|
|
71
107
|
<div class="auth-card">
|
|
72
|
-
<svg class="mark" aria-hidden="true"><use href="#i-mark"/></svg>
|
|
108
|
+
<svg class="mark" width="44" height="44" aria-hidden="true"><use href="#i-mark"/></svg>
|
|
73
109
|
<h1 id="auth-title">Connect this browser</h1>
|
|
74
110
|
<p id="auth-intro">COS runs on your Mac. Sign in once. You stay signed in while you use COS here, for up to 30 days.</p>
|
|
75
111
|
<div id="auth-offline" class="auth-offline hidden" role="status">
|
|
76
|
-
<p id="auth-offline-text">Your Mac may be asleep, or this device may be offline or off Tailscale. This page checks again every 30 seconds.</p>
|
|
112
|
+
<p id="auth-offline-text">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.</p>
|
|
77
113
|
<button type="button" class="btn primary block" id="auth-retry">Try again</button>
|
|
78
114
|
</div>
|
|
79
115
|
<form id="auth-form">
|
|
@@ -98,7 +134,7 @@
|
|
|
98
134
|
|
|
99
135
|
<div id="app-shell" class="app-shell hidden">
|
|
100
136
|
<nav class="rail" aria-label="Sections">
|
|
101
|
-
<div class="rail-head"><svg class="mark" aria-hidden="true"><use href="#i-mark"/></svg><span class="rail-brand">Control</span></div>
|
|
137
|
+
<div class="rail-head"><svg class="mark" width="28" height="28" aria-hidden="true"><use href="#i-mark"/></svg><span class="rail-brand">Control</span></div>
|
|
102
138
|
<button type="button" class="rail-btn rail-today" data-pane="today" aria-label="Today"><svg class="icon"><use href="#i-today"/></svg><span class="rail-text"><span class="rail-label">Today</span><span class="rail-story" data-story="today"></span></span><span class="badge-dot hidden" data-badge="today"></span></button>
|
|
103
139
|
<div class="rail-sep"></div>
|
|
104
140
|
<button type="button" class="rail-btn" data-pane="messages" aria-label="Messages"><svg class="icon"><use href="#i-messages"/></svg><span class="rail-text"><span class="rail-label">Messages</span><span class="rail-story" data-story="messages"></span></span><span class="badge-dot hidden" data-badge="messages"></span></button>
|
|
@@ -112,6 +148,9 @@
|
|
|
112
148
|
<div class="rail-spacer"></div>
|
|
113
149
|
<button type="button" class="rail-settings" data-pane="settings" aria-label="Settings"><svg class="icon"><use href="#i-settings"/></svg><span class="rail-label">Settings</span></button>
|
|
114
150
|
<p class="server-line" data-server-line><span class="dot ok" data-host-dot></span><span class="server-text" data-server-text>Server connected</span></p>
|
|
151
|
+
<!-- 7.0.29: the folded Duo's side rail only (app.css, html.short-screen): the top bar's Back, at thumb height. Shown
|
|
152
|
+
where the top bar shows Back today, on a reader, and it calls the same goBack. Never drawn from 600 px. -->
|
|
153
|
+
<button type="button" class="rail-back" id="rail-back" aria-label="Back" hidden><svg class="icon"><use href="#i-back"/></svg></button>
|
|
115
154
|
</nav>
|
|
116
155
|
|
|
117
156
|
<div class="main-col">
|
|
@@ -127,7 +166,7 @@
|
|
|
127
166
|
<div class="topbar-left">
|
|
128
167
|
<button type="button" id="topbar-back" class="topbar-back hidden"><svg class="icon"><use href="#i-back"/></svg><span id="topbar-back-label">Back</span></button>
|
|
129
168
|
<button type="button" id="section-btn" class="section-btn" aria-haspopup="dialog" aria-controls="section-menu">
|
|
130
|
-
<svg class="mark" aria-hidden="true"><use href="#i-mark"/></svg><span id="section-name">Today</span><span class="caret" aria-hidden="true"></span>
|
|
169
|
+
<svg class="mark" width="28" height="28" aria-hidden="true"><use href="#i-mark"/></svg><span id="section-name">Today</span><span class="caret" aria-hidden="true"></span>
|
|
131
170
|
</button>
|
|
132
171
|
</div>
|
|
133
172
|
<button type="button" class="icon-btn" data-open-ask aria-label="Ask COS or find a session"><svg class="icon"><use href="#i-ask"/></svg></button>
|
|
@@ -25,6 +25,32 @@
|
|
|
25
25
|
if (media && media.addEventListener) media.addEventListener('change', function () { if (read() === 'system') apply('system'); });
|
|
26
26
|
})();
|
|
27
27
|
|
|
28
|
+
/* 7.0.29 (Miles 21:42): "for shorter screens like the duo we should use the side rail when it's folded"; tall phones keep
|
|
29
|
+
the top menu. The test is the SCREEN's shape, never the window's: a screen whose short side is under 600 and whose long
|
|
30
|
+
side is under 1.6 times its short side is short (html.short-screen). app.css draws the side rail only with this class
|
|
31
|
+
AND a page under 600 px wide on a touch screen. Missing or odd numbers give no class: no rail, the layout as before.
|
|
32
|
+
MEASURED: his phone's own device reports (2026-11-10 and 11-11) give screen.width 466 and screen.height 678 in rows laid
|
|
33
|
+
out 449, 548 and 1119 px wide at visualViewport.scale 0.85, and the canary (11-11 21:56) read screen.width 466 in a
|
|
34
|
+
Safari tab, folded, at scale 1. 678 / 466 is 1.46. Every iPhone's published screen is 1.78 (SE, 8 Plus) or 2.16 and up.
|
|
35
|
+
ASSUMED, not measured: which of those rows were the Home Screen app and which a tab, that other zoom settings report
|
|
36
|
+
the same, and that folding or opening never changes the numbers. So this one function runs at load (before the first
|
|
37
|
+
paint, so the rail never moves the page on load) and again on every resize and orientation change, and the page's
|
|
38
|
+
device report and a 'rail' report on each change carry screen.width and screen.height, so his phone settles it. */
|
|
39
|
+
(function () {
|
|
40
|
+
function shortScreen() {
|
|
41
|
+
var de = document.documentElement, on = false;
|
|
42
|
+
try {
|
|
43
|
+
var s = window.screen, a = s ? Number(s.width) : 0, b = s ? Number(s.height) : 0;
|
|
44
|
+
if (a > 0 && b > 0 && isFinite(a) && isFinite(b)) { var lo = Math.min(a, b), hi = Math.max(a, b); on = lo < 600 && hi / lo < 1.6; }
|
|
45
|
+
} catch (_e) { on = false; }
|
|
46
|
+
try { if (de && de.classList) { if (on) de.classList.add('short-screen'); else de.classList.remove('short-screen'); } } catch (_e) { /* no class: no rail */ }
|
|
47
|
+
return on;
|
|
48
|
+
}
|
|
49
|
+
window.CosShortScreen = shortScreen;
|
|
50
|
+
shortScreen();
|
|
51
|
+
if (typeof window.addEventListener === 'function') { window.addEventListener('resize', shortScreen); window.addEventListener('orientationchange', shortScreen); }
|
|
52
|
+
})();
|
|
53
|
+
|
|
28
54
|
/* The app's scripts (index.html #boot-scripts), loaded one frame after the page is drawn so the first paint never waits
|
|
29
55
|
for them. Same files, same order (async=false keeps insertion order), and app.js's boot runs only once every one of
|
|
30
56
|
them has run, as when they were in the page: window.CosBoot.after(boot). The frame wait has a 50 ms fallback, because
|
|
@@ -40,10 +66,16 @@
|
|
|
40
66
|
list = Array.isArray(list) ? list.filter(function (u) { return typeof u === 'string' && u.indexOf('/control/') === 0; }) : [];
|
|
41
67
|
var left = list.length;
|
|
42
68
|
if (!left) { settle(); return; }
|
|
69
|
+
// 7.0.28: after a reload that the restyle check below made because a script had failed, ask for every script with a
|
|
70
|
+
// fresh query, so none comes from the browser's memory of the failed request (WebKit kept the failure across the
|
|
71
|
+
// reload). Read once.
|
|
72
|
+
var bust = '';
|
|
73
|
+
try { bust = sessionStorage.getItem('cos_web_bust') || ''; sessionStorage.removeItem('cos_web_bust'); } catch (_e) { bust = ''; }
|
|
43
74
|
list.forEach(function (src) {
|
|
44
75
|
var el = document.createElement('script');
|
|
45
|
-
el.src = src; el.async = false;
|
|
46
|
-
el.onload =
|
|
76
|
+
el.src = /^\d{1,15}$/.test(bust) ? src + (src.indexOf('?') >= 0 ? '&' : '?') + 'r=' + bust : src; el.async = false;
|
|
77
|
+
el.onload = function () { if (--left === 0) settle(); };
|
|
78
|
+
el.onerror = function () { if (window.CosRestyle) window.CosRestyle.scriptFailed(src); if (--left === 0) settle(); };
|
|
47
79
|
document.body.appendChild(el);
|
|
48
80
|
});
|
|
49
81
|
}
|
|
@@ -54,3 +86,138 @@
|
|
|
54
86
|
}
|
|
55
87
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', afterFrame); else afterFrame();
|
|
56
88
|
})();
|
|
89
|
+
|
|
90
|
+
/* 7.0.28 (Miles 19:28, "Connect this browser" with no styling): the page's own files must not stay missing. When the
|
|
91
|
+
phone opens the page before it can reach the Mac (the tunnel still coming up, or the page drawn from the browser's
|
|
92
|
+
copy), the HTML draws but a stylesheet or a script can fail, and nothing asked for it again. index.html's inline
|
|
93
|
+
critical CSS keeps the sign-in card right meanwhile; this brings the rest back. While anything failed, it asks the
|
|
94
|
+
Mac for the failed file after 1, 2, 4 and 8 s, then every 30 s, only while the page is in front. When the Mac
|
|
95
|
+
answers: a failed script reloads the page (app.js cannot be re-run in place); failed stylesheets are asked for again
|
|
96
|
+
in place. A file that keeps failing never loops: at most 2 reloads in 5 minutes. What happened is kept for app.js's numbers-only 'restyle' row (window.CosRestyle.last, and sessionStorage
|
|
97
|
+
across the reload).
|
|
98
|
+
7.0.29 (Q1 FIX 1 and 2, the lead's R1 and R2): the page never reloads by itself over his words. While any text box
|
|
99
|
+
holds words, focus is in a text box, or a send is in flight (app.js's window.CosBusy; when app.js is the script that
|
|
100
|
+
failed, the boxes alone), it shows one line with a Reload button instead, and only his tap reloads. The cap fails
|
|
101
|
+
closed: if the reload count cannot be read, or does not read back after writing (storage blocked), it gives up and
|
|
102
|
+
shows the same line rather than reloading. */
|
|
103
|
+
(function () {
|
|
104
|
+
var STEPS = [1000, 2000, 4000, 8000], SETTLED_MS = 30000, KEY = 'cos_web_restyle', RELOADS_KEY = 'cos_web_restyle_reloads';
|
|
105
|
+
var RELOAD_MAX = 2, RELOAD_WINDOW_MS = 5 * 60000;
|
|
106
|
+
var R = { scripts: [], sheets: 0, tries: 0, t0: 0, timer: null, done: false, last: null };
|
|
107
|
+
// Every real browser has these; a harness that runs this file without them gets no restyle check, not a throw that
|
|
108
|
+
// would stop the theme and the loader above from being tested.
|
|
109
|
+
if (typeof window.addEventListener !== 'function' || typeof document.querySelectorAll !== 'function' || typeof document.addEventListener !== 'function') return;
|
|
110
|
+
function missing() {
|
|
111
|
+
return [].filter.call(document.querySelectorAll('link[rel="stylesheet"]'), function (l) {
|
|
112
|
+
// A sheet that failed to load throws on read in WebKit (SecurityError); count it as missing.
|
|
113
|
+
try { return !l.sheet || !l.sheet.cssRules || l.sheet.cssRules.length === 0; } catch (_e) { return true; }
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function bust(href) { return href + (href.indexOf('?') >= 0 ? '&' : '?') + 'r=' + Date.now(); }
|
|
117
|
+
function record(how) {
|
|
118
|
+
R.last = { how: how, sheets: R.sheets, scripts: R.scripts.length, tries: R.tries, ms: R.t0 ? Date.now() - R.t0 : 0 };
|
|
119
|
+
try { window.dispatchEvent(new CustomEvent('cos-restyle', { detail: R.last })); } catch (_e) {}
|
|
120
|
+
}
|
|
121
|
+
// What would be lost by a reload: words in a text box he can see, focus in one, or a send on its way (app.js).
|
|
122
|
+
function busy() {
|
|
123
|
+
var out = { words: false, focus: false, sending: false };
|
|
124
|
+
try {
|
|
125
|
+
[].forEach.call(document.querySelectorAll('textarea, input, [contenteditable]'), function (el) {
|
|
126
|
+
if (el.getAttribute && el.getAttribute('aria-hidden') === 'true') return; // app.js's hidden measuring copy
|
|
127
|
+
if (el.tagName === 'INPUT' && !/^(|text|search|email|url|tel|password|number)$/i.test(el.getAttribute('type') || '')) return;
|
|
128
|
+
var v = el.isContentEditable ? el.textContent : el.value;
|
|
129
|
+
if (v && String(v).trim()) out.words = true;
|
|
130
|
+
});
|
|
131
|
+
var a = document.activeElement;
|
|
132
|
+
out.focus = !!(a && (a.tagName === 'TEXTAREA' || (a.tagName === 'INPUT' && /^(|text|search|email|url|tel|password|number)$/i.test(a.getAttribute('type') || '')) || a.isContentEditable));
|
|
133
|
+
} catch (_e) {}
|
|
134
|
+
try { out.sending = typeof window.CosBusy === 'function' && !!window.CosBusy(); } catch (_e) {}
|
|
135
|
+
out.any = out.words || out.focus || out.sending;
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
// The one line, in place of a reload. Inline styles with the theme's own colours where they exist, so it reads right
|
|
139
|
+
// on a page whose stylesheet failed too. At the top: a fixed bar near the bottom fights the phone browser's own bar.
|
|
140
|
+
// On the folded Duo it stops at the side rail (--rail-w is set only while that rail shows).
|
|
141
|
+
function offer(b) {
|
|
142
|
+
try { offerLine(b); } catch (_e) { /* no line, and still no reload by itself */ }
|
|
143
|
+
}
|
|
144
|
+
function offerLine(b) {
|
|
145
|
+
var line = document.getElementById('cos-reload-line');
|
|
146
|
+
if (!line) {
|
|
147
|
+
line = document.createElement('div');
|
|
148
|
+
line.id = 'cos-reload-line'; line.setAttribute('role', 'status');
|
|
149
|
+
line.style.cssText = 'position:fixed;left:12px;right:calc(12px + var(--rail-w, 0px));top:calc(env(safe-area-inset-top, 0px) + 8px);z-index:90;display:flex;align-items:center;gap:12px;' +
|
|
150
|
+
'padding:10px 10px 10px 14px;border-radius:12px;border:1px solid var(--panel-border, #a08a66);background:var(--panel, #ffffff);color:var(--ink, #1f1712);' +
|
|
151
|
+
'font:400 15px/1.4 -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;max-width:560px;margin:0 auto;';
|
|
152
|
+
var p = document.createElement('p'); p.style.cssText = 'margin:0;flex:1 1 auto;min-width:0;'; p.id = 'cos-reload-words';
|
|
153
|
+
var btn = document.createElement('button'); btn.type = 'button'; btn.textContent = 'Reload';
|
|
154
|
+
btn.style.cssText = 'flex:none;min-height:44px;padding:0 16px;border-radius:10px;border:0;background:var(--accent, #89662d);color:var(--on-accent, #ffffff);font-family:inherit;font-size:15px;font-weight:600;line-height:1;';
|
|
155
|
+
btn.addEventListener('click', function () { reloadNow(); });
|
|
156
|
+
line.appendChild(p); line.appendChild(btn);
|
|
157
|
+
(document.body || document.documentElement).appendChild(line);
|
|
158
|
+
}
|
|
159
|
+
var words = document.getElementById('cos-reload-words');
|
|
160
|
+
if (words) words.textContent = 'Part of this page did not load.' + (b && b.words ? ' Send or copy what you have typed first. A reload clears it.' : '');
|
|
161
|
+
}
|
|
162
|
+
function reloadNow() {
|
|
163
|
+
try { sessionStorage.setItem(KEY, JSON.stringify(R.last)); sessionStorage.setItem('cos_web_bust', String(Date.now())); } catch (_e) {}
|
|
164
|
+
location.reload();
|
|
165
|
+
}
|
|
166
|
+
function recover() {
|
|
167
|
+
if (R.scripts.length) {
|
|
168
|
+
// Never by itself over his words (R1): the line, and only his tap reloads.
|
|
169
|
+
var b = busy();
|
|
170
|
+
if (b.any) { R.done = true; record('offered'); offer(b); return; }
|
|
171
|
+
// At most 2 reloads in 5 minutes: a file that keeps failing must never become a reload loop. Fails closed (R2): a
|
|
172
|
+
// count that cannot be read, or that does not read back after writing, means no reload by itself.
|
|
173
|
+
var recent = null, back = null;
|
|
174
|
+
try { recent = JSON.parse(sessionStorage.getItem(RELOADS_KEY) || '[]').filter(function (t) { return Date.now() - t < RELOAD_WINDOW_MS; }); } catch (_e) { recent = null; }
|
|
175
|
+
if (!recent || recent.length >= RELOAD_MAX) { R.done = true; record('gave-up'); offer(b); return; }
|
|
176
|
+
var now = Date.now();
|
|
177
|
+
try { recent.push(now); sessionStorage.setItem(RELOADS_KEY, JSON.stringify(recent)); back = JSON.parse(sessionStorage.getItem(RELOADS_KEY) || 'null'); } catch (_e) { back = null; }
|
|
178
|
+
if (!back || back.length !== recent.length || back[back.length - 1] !== now) { R.done = true; record('gave-up'); offer(b); return; }
|
|
179
|
+
record('reload');
|
|
180
|
+
reloadNow();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
var gone = missing();
|
|
184
|
+
gone.forEach(function (old) {
|
|
185
|
+
var el = document.createElement('link');
|
|
186
|
+
el.rel = 'stylesheet'; el.href = bust(old.getAttribute('href') || old.href);
|
|
187
|
+
el.onload = function () { if (old.parentNode) old.parentNode.removeChild(old); };
|
|
188
|
+
old.parentNode.insertBefore(el, old.nextSibling);
|
|
189
|
+
});
|
|
190
|
+
R.done = true; record('sheets');
|
|
191
|
+
}
|
|
192
|
+
function schedule() {
|
|
193
|
+
if (R.timer || R.done || document.hidden) return;
|
|
194
|
+
var ms = R.tries < STEPS.length ? STEPS[R.tries] : SETTLED_MS;
|
|
195
|
+
R.timer = setTimeout(check, ms);
|
|
196
|
+
}
|
|
197
|
+
function check() {
|
|
198
|
+
R.timer = null;
|
|
199
|
+
if (R.done || document.hidden) return;
|
|
200
|
+
R.tries++;
|
|
201
|
+
// Ask for the file that failed (a script first): it answering is what matters, and a file that is not there (404)
|
|
202
|
+
// never counts as the Mac being back.
|
|
203
|
+
var gone = missing(), url = R.scripts[0] || (gone[0] && (gone[0].getAttribute('href') || gone[0].href)) || '/control/app.css';
|
|
204
|
+
var probe = window.fetch ? window.fetch(bust(url), { cache: 'no-store', credentials: 'same-origin' }) : Promise.reject(new Error('no fetch'));
|
|
205
|
+
probe.then(function (r) { if (r && r.ok) recover(); else schedule(); }, function () { schedule(); });
|
|
206
|
+
}
|
|
207
|
+
function start() {
|
|
208
|
+
if (R.done || R.t0) return;
|
|
209
|
+
R.sheets = missing().length;
|
|
210
|
+
if (!R.sheets && !R.scripts.length) return;
|
|
211
|
+
R.t0 = Date.now();
|
|
212
|
+
schedule();
|
|
213
|
+
}
|
|
214
|
+
window.CosRestyle = {
|
|
215
|
+
scriptFailed: function (src) { if (R.scripts.indexOf(src) < 0) R.scripts.push(src); if (R.t0) schedule(); else start(); },
|
|
216
|
+
get last() { return R.last; },
|
|
217
|
+
// After a reload that this caused: what the page that reloaded had seen, read once.
|
|
218
|
+
takeSaved: function () { try { var v = sessionStorage.getItem(KEY); sessionStorage.removeItem(KEY); return v ? JSON.parse(v) : null; } catch (_e) { return null; } }
|
|
219
|
+
};
|
|
220
|
+
document.addEventListener('visibilitychange', function () { if (!document.hidden && R.t0 && !R.done && !R.timer) check(); });
|
|
221
|
+
// Every <link> in the head has finished (loaded or failed) by the time the page's load event fires.
|
|
222
|
+
if (document.readyState === 'complete') start(); else window.addEventListener('load', start);
|
|
223
|
+
})();
|