privateboard 0.1.23 → 0.1.25
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/dist/boot.js +1404 -1378
- package/dist/boot.js.map +1 -1
- package/dist/cli.js +1404 -1378
- package/dist/cli.js.map +1 -1
- package/dist/server.js +1370 -1359
- package/dist/server.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
- package/public/agent-overlay.css +82 -0
- package/public/agent-overlay.js +459 -17
- package/public/agent-profile.js +34 -54
- package/public/app-updater.css +318 -0
- package/public/app-updater.js +247 -0
- package/public/app.js +1590 -691
- package/public/home.html +1 -1
- package/public/i18n.js +477 -52
- package/public/icons/floor.png +0 -0
- package/public/index.html +600 -213
- package/public/keys-store.js +112 -1
- package/public/mention-picker.js +573 -0
- package/public/new-agent.js +17 -7
- package/public/onboarding.js +108 -117
- package/public/themes.css +44 -0
- package/public/user-settings.css +503 -3
- package/public/user-settings.js +526 -217
- package/public/voice-replay.css +33 -20
- package/public/voice-replay.js +16 -0
package/public/agent-profile.js
CHANGED
|
@@ -2413,61 +2413,29 @@
|
|
|
2413
2413
|
* route · short label rendered as the right-edge pill. */
|
|
2414
2414
|
function pickerEntries() {
|
|
2415
2415
|
const cache = modelsSnapshot();
|
|
2416
|
-
|
|
2416
|
+
// Multi-SIM credential model · the user has exactly one active
|
|
2417
|
+
// LLM provider at a time, so each reachable model maps to one
|
|
2418
|
+
// pickable row (no carrier fork). `cache.reachable` is filtered
|
|
2419
|
+
// server-side based on `prefs.active_llm_credential_id`, so the
|
|
2420
|
+
// picker naturally collapses to the active provider's family.
|
|
2417
2421
|
if (cache && Array.isArray(cache.reachable) && cache.reachable.length > 0) {
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
// the orchestrator would resolve unpinned. When only ONE
|
|
2428
|
-
// carrier serves the model, we collapse to a single row with
|
|
2429
|
-
// `carrier: null` so the agent uses default routing (and
|
|
2430
|
-
// automatically follows the carrier if the user later
|
|
2431
|
-
// reconfigures keys).
|
|
2432
|
-
const reachableCarriers = [];
|
|
2433
|
-
if (directOk) reachableCarriers.push({ carrier: m.provider, route: "via " + provider + " direct" });
|
|
2434
|
-
if (baiOk) reachableCarriers.push({ carrier: "bai", route: "via B.AI" });
|
|
2435
|
-
if (orOk) reachableCarriers.push({ carrier: "openrouter", route: "via OpenRouter" });
|
|
2436
|
-
if (reachableCarriers.length === 0) continue;
|
|
2437
|
-
if (reachableCarriers.length === 1) {
|
|
2438
|
-
// Only one carrier serves this model · use the bare modelV
|
|
2439
|
-
// id and `carrier: null` so the saved agent record uses
|
|
2440
|
-
// default routing rather than a sticky pin to a carrier the
|
|
2441
|
-
// user might later swap.
|
|
2442
|
-
const only = reachableCarriers[0];
|
|
2443
|
-
out.push({
|
|
2444
|
-
id: m.modelV,
|
|
2445
|
-
v: m.modelV,
|
|
2446
|
-
carrier: null,
|
|
2447
|
-
name: m.displayName,
|
|
2448
|
-
provider,
|
|
2449
|
-
deck: m.deck || "",
|
|
2450
|
-
route: only.carrier === m.provider ? "direct" : only.route,
|
|
2451
|
-
});
|
|
2452
|
-
continue;
|
|
2453
|
-
}
|
|
2454
|
-
// Multi-carrier · one row per carrier · composite id
|
|
2455
|
-
// `${modelV}@${carrier}` distinguishes them in the picker.
|
|
2456
|
-
for (const { carrier, route } of reachableCarriers) {
|
|
2457
|
-
out.push({
|
|
2458
|
-
id: m.modelV + "@" + carrier,
|
|
2459
|
-
v: m.modelV,
|
|
2460
|
-
carrier,
|
|
2461
|
-
name: m.displayName,
|
|
2462
|
-
provider,
|
|
2463
|
-
deck: m.deck || "",
|
|
2464
|
-
route,
|
|
2465
|
-
});
|
|
2466
|
-
}
|
|
2467
|
-
}
|
|
2468
|
-
if (out.length > 0) return out;
|
|
2422
|
+
return cache.reachable.map((m) => ({
|
|
2423
|
+
id: m.modelV,
|
|
2424
|
+
v: m.modelV,
|
|
2425
|
+
carrier: null,
|
|
2426
|
+
name: m.displayName,
|
|
2427
|
+
provider: providerLabel(m.provider),
|
|
2428
|
+
deck: m.deck || "",
|
|
2429
|
+
route: "",
|
|
2430
|
+
}));
|
|
2469
2431
|
}
|
|
2470
|
-
|
|
2432
|
+
// Cache not yet loaded · return an empty list. The renderer will
|
|
2433
|
+
// show whatever stale info the agent's saved modelV resolves to
|
|
2434
|
+
// and re-fetch the cache. We deliberately AVOID falling back to
|
|
2435
|
+
// the hardcoded PROFILE_MODELS catalog here — that would surface
|
|
2436
|
+
// models the user's credential can't reach (e.g. showing Claude
|
|
2437
|
+
// when the active credential is OpenAI direct).
|
|
2438
|
+
return [];
|
|
2471
2439
|
}
|
|
2472
2440
|
|
|
2473
2441
|
/** Look up a single entry by composite id (`${v}@${carrier}` or
|
|
@@ -3092,7 +3060,14 @@
|
|
|
3092
3060
|
* to the provider's billing/pricing page so the user is one click
|
|
3093
3061
|
* from resolving it. */
|
|
3094
3062
|
function openVoicePaidOverlay(opts) {
|
|
3095
|
-
|
|
3063
|
+
// Idempotent · if an overlay is already showing, leave it alone.
|
|
3064
|
+
// Repeated billing-error events (each director's failed TTS in a
|
|
3065
|
+
// single round; replay + live-room hitting the same backend tag)
|
|
3066
|
+
// would otherwise tear down + rebuild the DOM on every call,
|
|
3067
|
+
// producing a visible "flash" as the panel reappears. The user
|
|
3068
|
+
// only needs to see the upgrade prompt ONCE — every subsequent
|
|
3069
|
+
// identical error is the same actionable item.
|
|
3070
|
+
if (document.getElementById("ap-voice-paid-overlay")) return;
|
|
3096
3071
|
const provider = (opts && opts.provider) || "";
|
|
3097
3072
|
const upgradeUrl = (opts && opts.upgradeUrl) || "";
|
|
3098
3073
|
const message = (opts && opts.message) || "";
|
|
@@ -4759,6 +4734,11 @@
|
|
|
4759
4734
|
* events from whichever copy is live. */
|
|
4760
4735
|
window.AgentProfileVoice = {
|
|
4761
4736
|
renderVoiceBlock,
|
|
4737
|
+
// Public hook so the room SSE handler + voice-replay can open
|
|
4738
|
+
// the same upgrade overlay when a TTS billing error fires
|
|
4739
|
+
// mid-room (insufficient balance / paid plan required) instead
|
|
4740
|
+
// of only when the user is on the agent-profile voice picker.
|
|
4741
|
+
openPaidOverlay: openVoicePaidOverlay,
|
|
4762
4742
|
};
|
|
4763
4743
|
|
|
4764
4744
|
document.addEventListener("boardroom:locale", () => {
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/* ─────────────── App auto-update overlay ───────────────
|
|
2
|
+
Consent-driven update flow for the Electron build. Three
|
|
3
|
+
states: prompt (new version available) → downloading
|
|
4
|
+
(progress bar) → ready (restart). Wired from
|
|
5
|
+
public/app-updater.js against window.privateboard.updater.
|
|
6
|
+
|
|
7
|
+
Chrome (classification strip · lime corner brackets ·
|
|
8
|
+
topbar · dashed-rule foot) mirrors voice-onboarding.css
|
|
9
|
+
so the overlay reads as native to the rest of the app's
|
|
10
|
+
modal vocabulary. */
|
|
11
|
+
|
|
12
|
+
.upd-overlay {
|
|
13
|
+
position: fixed;
|
|
14
|
+
inset: 0;
|
|
15
|
+
background: rgba(0, 0, 0, 0.78);
|
|
16
|
+
-webkit-backdrop-filter: blur(4px);
|
|
17
|
+
backdrop-filter: blur(4px);
|
|
18
|
+
z-index: 9500;
|
|
19
|
+
display: none;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: center;
|
|
22
|
+
padding: 24px;
|
|
23
|
+
overflow: hidden;
|
|
24
|
+
font-family: var(--mono, "Inter", system-ui, sans-serif);
|
|
25
|
+
}
|
|
26
|
+
.upd-overlay.open {
|
|
27
|
+
display: flex;
|
|
28
|
+
animation: upd-fade 0.14s ease-out;
|
|
29
|
+
}
|
|
30
|
+
@keyframes upd-fade { from { opacity: 0; } to { opacity: 1; } }
|
|
31
|
+
|
|
32
|
+
.upd-backdrop {
|
|
33
|
+
position: absolute;
|
|
34
|
+
inset: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.upd-modal {
|
|
38
|
+
position: relative;
|
|
39
|
+
width: 100%;
|
|
40
|
+
max-width: 480px;
|
|
41
|
+
background: var(--panel);
|
|
42
|
+
border: 0.5px solid var(--line-strong);
|
|
43
|
+
color: var(--text);
|
|
44
|
+
animation: upd-rise 0.18s ease-out;
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
min-height: 0;
|
|
48
|
+
}
|
|
49
|
+
@keyframes upd-rise {
|
|
50
|
+
from { transform: translateY(10px); opacity: 0; }
|
|
51
|
+
to { transform: translateY(0); opacity: 1; }
|
|
52
|
+
}
|
|
53
|
+
/* Lime corner brackets · same vocabulary as the vonb overlay. */
|
|
54
|
+
.upd-modal::before, .upd-modal::after {
|
|
55
|
+
content: "";
|
|
56
|
+
position: absolute;
|
|
57
|
+
width: 10px;
|
|
58
|
+
height: 10px;
|
|
59
|
+
border: 1.5px solid var(--lime);
|
|
60
|
+
pointer-events: none;
|
|
61
|
+
}
|
|
62
|
+
.upd-modal::before { top: -1px; left: -1px; border-right: none; border-bottom: none; }
|
|
63
|
+
.upd-modal::after { bottom: -1px; right: -1px; border-left: none; border-top: none; }
|
|
64
|
+
|
|
65
|
+
/* ─── Classification strip ─── */
|
|
66
|
+
.upd-classification {
|
|
67
|
+
background: var(--panel-2);
|
|
68
|
+
border-bottom: 0.5px solid var(--line-bright);
|
|
69
|
+
padding: 5px 14px;
|
|
70
|
+
font-size: 8px;
|
|
71
|
+
letter-spacing: 0.22em;
|
|
72
|
+
text-transform: uppercase;
|
|
73
|
+
color: var(--lime);
|
|
74
|
+
font-weight: 700;
|
|
75
|
+
display: flex;
|
|
76
|
+
justify-content: space-between;
|
|
77
|
+
align-items: center;
|
|
78
|
+
}
|
|
79
|
+
.upd-classification .dot { display: inline-block; margin-right: 4px; }
|
|
80
|
+
.upd-classification .right {
|
|
81
|
+
color: var(--text-faint);
|
|
82
|
+
letter-spacing: 0.12em;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ─── Topbar · meta + title + close ─── */
|
|
86
|
+
.upd-head {
|
|
87
|
+
display: grid;
|
|
88
|
+
grid-template-columns: 1fr auto;
|
|
89
|
+
gap: 12px;
|
|
90
|
+
align-items: start;
|
|
91
|
+
padding: 14px 16px 12px;
|
|
92
|
+
border-bottom: 0.5px dashed var(--line-bright);
|
|
93
|
+
}
|
|
94
|
+
.upd-head-text { min-width: 0; }
|
|
95
|
+
.upd-head .meta {
|
|
96
|
+
font-family: var(--mono);
|
|
97
|
+
font-size: 9px;
|
|
98
|
+
color: var(--text-dim);
|
|
99
|
+
text-transform: uppercase;
|
|
100
|
+
letter-spacing: 0.18em;
|
|
101
|
+
margin-bottom: 4px;
|
|
102
|
+
font-weight: 700;
|
|
103
|
+
display: flex;
|
|
104
|
+
gap: 6px;
|
|
105
|
+
align-items: center;
|
|
106
|
+
}
|
|
107
|
+
.upd-head .meta .live {
|
|
108
|
+
color: var(--lime);
|
|
109
|
+
font-weight: 700;
|
|
110
|
+
}
|
|
111
|
+
.upd-head .meta .live::before {
|
|
112
|
+
content: "● ";
|
|
113
|
+
animation: upd-pulse 1.6s ease-in-out infinite;
|
|
114
|
+
}
|
|
115
|
+
@keyframes upd-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }
|
|
116
|
+
.upd-head .title {
|
|
117
|
+
font-size: 16px;
|
|
118
|
+
font-weight: 700;
|
|
119
|
+
color: var(--text);
|
|
120
|
+
letter-spacing: -0.01em;
|
|
121
|
+
line-height: 1.3;
|
|
122
|
+
font-family: var(--font-human, system-ui, sans-serif);
|
|
123
|
+
}
|
|
124
|
+
.upd-head .title::before {
|
|
125
|
+
content: "▸ ";
|
|
126
|
+
color: var(--lime);
|
|
127
|
+
font-family: var(--mono);
|
|
128
|
+
}
|
|
129
|
+
.upd-head .close-btn {
|
|
130
|
+
width: 24px; height: 24px;
|
|
131
|
+
background: transparent;
|
|
132
|
+
border: 0.5px solid var(--line-bright);
|
|
133
|
+
color: var(--text-dim);
|
|
134
|
+
font-size: 12px;
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
font-family: var(--mono);
|
|
137
|
+
border-radius: 3px;
|
|
138
|
+
transition: color 0.12s, border-color 0.12s;
|
|
139
|
+
}
|
|
140
|
+
.upd-head .close-btn:hover {
|
|
141
|
+
border-color: var(--lime);
|
|
142
|
+
color: var(--lime);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* ─── Body ─── */
|
|
146
|
+
.upd-body {
|
|
147
|
+
padding: 16px;
|
|
148
|
+
display: flex;
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
gap: 14px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* Version block · "v0.1.22 → v0.1.23" treatment. */
|
|
154
|
+
.upd-version {
|
|
155
|
+
display: flex;
|
|
156
|
+
align-items: baseline;
|
|
157
|
+
gap: 10px;
|
|
158
|
+
font-family: var(--mono);
|
|
159
|
+
font-size: 12px;
|
|
160
|
+
color: var(--text-dim);
|
|
161
|
+
}
|
|
162
|
+
.upd-version .from { color: var(--text-faint); text-decoration: line-through; }
|
|
163
|
+
.upd-version .arrow { color: var(--lime); }
|
|
164
|
+
.upd-version .to {
|
|
165
|
+
color: var(--lime);
|
|
166
|
+
font-weight: 700;
|
|
167
|
+
letter-spacing: 0.02em;
|
|
168
|
+
font-size: 16px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.upd-deck {
|
|
172
|
+
font-family: var(--font-human, system-ui, sans-serif);
|
|
173
|
+
font-size: 13px;
|
|
174
|
+
line-height: 1.55;
|
|
175
|
+
color: var(--text-soft);
|
|
176
|
+
margin: 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* ─── Progress block (downloading state) ─── */
|
|
180
|
+
.upd-progress-card {
|
|
181
|
+
display: flex;
|
|
182
|
+
flex-direction: column;
|
|
183
|
+
gap: 10px;
|
|
184
|
+
padding: 14px;
|
|
185
|
+
border: 0.5px solid var(--line);
|
|
186
|
+
background: var(--panel-2);
|
|
187
|
+
}
|
|
188
|
+
.upd-progress-head {
|
|
189
|
+
display: flex;
|
|
190
|
+
align-items: baseline;
|
|
191
|
+
justify-content: space-between;
|
|
192
|
+
gap: 12px;
|
|
193
|
+
font-family: var(--mono);
|
|
194
|
+
}
|
|
195
|
+
.upd-progress-pct {
|
|
196
|
+
font-size: 22px;
|
|
197
|
+
font-weight: 700;
|
|
198
|
+
color: var(--lime);
|
|
199
|
+
letter-spacing: -0.01em;
|
|
200
|
+
}
|
|
201
|
+
.upd-progress-rate {
|
|
202
|
+
font-size: 10px;
|
|
203
|
+
letter-spacing: 0.14em;
|
|
204
|
+
text-transform: uppercase;
|
|
205
|
+
color: var(--text-faint);
|
|
206
|
+
}
|
|
207
|
+
.upd-progress-bar {
|
|
208
|
+
height: 4px;
|
|
209
|
+
background: var(--bg);
|
|
210
|
+
border: 0.5px solid var(--line);
|
|
211
|
+
overflow: hidden;
|
|
212
|
+
}
|
|
213
|
+
.upd-progress-bar > span {
|
|
214
|
+
display: block;
|
|
215
|
+
height: 100%;
|
|
216
|
+
background: var(--lime);
|
|
217
|
+
transition: width 0.3s ease-out;
|
|
218
|
+
width: 0%;
|
|
219
|
+
}
|
|
220
|
+
/* Indeterminate sweep · before the first download-progress event
|
|
221
|
+
lands (the .pkg is being negotiated). */
|
|
222
|
+
.upd-progress-bar.indeterminate > span {
|
|
223
|
+
width: 30%;
|
|
224
|
+
animation: upd-sweep 1.2s ease-in-out infinite;
|
|
225
|
+
}
|
|
226
|
+
@keyframes upd-sweep {
|
|
227
|
+
0% { transform: translateX(-100%); }
|
|
228
|
+
100% { transform: translateX(333%); }
|
|
229
|
+
}
|
|
230
|
+
.upd-progress-bytes {
|
|
231
|
+
font-family: var(--mono);
|
|
232
|
+
font-size: 10px;
|
|
233
|
+
color: var(--text-dim);
|
|
234
|
+
letter-spacing: 0.08em;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* ─── Error block ─── */
|
|
238
|
+
.upd-error {
|
|
239
|
+
font-family: var(--mono);
|
|
240
|
+
font-size: 11px;
|
|
241
|
+
line-height: 1.5;
|
|
242
|
+
color: var(--red, #B5706A);
|
|
243
|
+
padding: 12px;
|
|
244
|
+
border: 0.5px solid var(--red, #B5706A);
|
|
245
|
+
background: rgba(181, 112, 106, 0.06);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* ─── Foot · CTAs ─── */
|
|
249
|
+
.upd-foot {
|
|
250
|
+
padding: 12px 16px 16px;
|
|
251
|
+
display: flex;
|
|
252
|
+
justify-content: flex-end;
|
|
253
|
+
align-items: center;
|
|
254
|
+
gap: 10px;
|
|
255
|
+
border-top: 0.5px dashed var(--line-bright);
|
|
256
|
+
}
|
|
257
|
+
.upd-btn {
|
|
258
|
+
padding: 8px 18px;
|
|
259
|
+
background: transparent;
|
|
260
|
+
border: 0.5px solid var(--line-bright);
|
|
261
|
+
color: var(--text-soft);
|
|
262
|
+
font-family: var(--mono);
|
|
263
|
+
font-size: 11px;
|
|
264
|
+
letter-spacing: 0.14em;
|
|
265
|
+
text-transform: uppercase;
|
|
266
|
+
cursor: pointer;
|
|
267
|
+
transition: color 0.12s, border-color 0.12s, background 0.12s, filter 0.12s;
|
|
268
|
+
}
|
|
269
|
+
.upd-btn:hover {
|
|
270
|
+
border-color: var(--lime);
|
|
271
|
+
color: var(--lime);
|
|
272
|
+
}
|
|
273
|
+
.upd-btn.primary {
|
|
274
|
+
background: var(--lime);
|
|
275
|
+
border-color: var(--lime);
|
|
276
|
+
color: var(--bg);
|
|
277
|
+
font-weight: 700;
|
|
278
|
+
}
|
|
279
|
+
.upd-btn.primary:hover {
|
|
280
|
+
filter: brightness(1.06);
|
|
281
|
+
color: var(--bg);
|
|
282
|
+
}
|
|
283
|
+
.upd-btn[disabled] {
|
|
284
|
+
opacity: 0.4;
|
|
285
|
+
cursor: not-allowed;
|
|
286
|
+
}
|
|
287
|
+
.upd-btn[disabled]:hover {
|
|
288
|
+
border-color: var(--line-bright);
|
|
289
|
+
color: var(--text-soft);
|
|
290
|
+
filter: none;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* State-driven visibility. The overlay carries one of
|
|
294
|
+
`state-available` / `state-downloading` / `state-ready` /
|
|
295
|
+
`state-error` and the modal swaps which subtree is shown. */
|
|
296
|
+
.upd-modal .upd-state { display: none; }
|
|
297
|
+
.upd-overlay.state-available .upd-modal .upd-state-available { display: flex; flex-direction: column; gap: 14px; }
|
|
298
|
+
.upd-overlay.state-downloading .upd-modal .upd-state-downloading { display: flex; flex-direction: column; gap: 14px; }
|
|
299
|
+
.upd-overlay.state-ready .upd-modal .upd-state-ready { display: flex; flex-direction: column; gap: 14px; }
|
|
300
|
+
.upd-overlay.state-error .upd-modal .upd-state-error { display: flex; flex-direction: column; gap: 14px; }
|
|
301
|
+
|
|
302
|
+
/* Buttons swap per state too. */
|
|
303
|
+
.upd-modal .upd-foot-state { display: none; }
|
|
304
|
+
.upd-overlay.state-available .upd-foot-state-available { display: contents; }
|
|
305
|
+
.upd-overlay.state-downloading .upd-foot-state-downloading { display: contents; }
|
|
306
|
+
.upd-overlay.state-ready .upd-foot-state-ready { display: contents; }
|
|
307
|
+
.upd-overlay.state-error .upd-foot-state-error { display: contents; }
|
|
308
|
+
|
|
309
|
+
/* Body scroll lock while overlay open. */
|
|
310
|
+
body.upd-locked {
|
|
311
|
+
overflow: hidden;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
@media (max-width: 560px) {
|
|
315
|
+
.upd-overlay { padding: 14px; }
|
|
316
|
+
.upd-head { padding: 12px 14px; }
|
|
317
|
+
.upd-body { padding: 14px; }
|
|
318
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/* ─────────────── App auto-update controller ───────────────
|
|
2
|
+
Renderer side of the Electron auto-updater flow. The
|
|
3
|
+
main process (electron/main.ts) pushes every state
|
|
4
|
+
transition over the `updater:state` IPC channel; the
|
|
5
|
+
preload bridge surfaces it as
|
|
6
|
+
`window.privateboard.updater.{onState,getState,…}`.
|
|
7
|
+
|
|
8
|
+
Lifecycle:
|
|
9
|
+
1. On script load (browser fallback or pre-Electron build),
|
|
10
|
+
the IPC bridge is absent · we no-op.
|
|
11
|
+
2. In Electron, we `getState()` to rehydrate (covers refresh
|
|
12
|
+
/ devtools reload that lands after `update-available`
|
|
13
|
+
already fired) and subscribe via `onState`.
|
|
14
|
+
3. On every non-idle state, the overlay opens (or stays
|
|
15
|
+
open) and paints the matching subtree. The user's
|
|
16
|
+
"Later"/"Hide" button only closes the modal — it does
|
|
17
|
+
NOT cancel the download; clicking the dock icon or
|
|
18
|
+
waiting for the next 4-hour re-check re-opens it.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
(function () {
|
|
22
|
+
"use strict";
|
|
23
|
+
|
|
24
|
+
const bridge = (typeof window !== "undefined" && window.privateboard && window.privateboard.updater) || null;
|
|
25
|
+
if (!bridge) return; // Browser preview / non-Electron build · do nothing.
|
|
26
|
+
|
|
27
|
+
let overlayEl = null;
|
|
28
|
+
let lastState = null;
|
|
29
|
+
let userDismissed = false; // Cleared whenever a NEW state arrives.
|
|
30
|
+
let appVersion = ""; // Resolved once via window.privateboard.getAppVersion().
|
|
31
|
+
|
|
32
|
+
function $(sel, root) { return (root || document).querySelector(sel); }
|
|
33
|
+
|
|
34
|
+
function applyI18n() {
|
|
35
|
+
if (!overlayEl) return;
|
|
36
|
+
const I18n = window.I18n;
|
|
37
|
+
overlayEl.querySelectorAll("[data-i18n]").forEach((el) => {
|
|
38
|
+
const key = el.getAttribute("data-i18n");
|
|
39
|
+
if (!key) return;
|
|
40
|
+
let val = null;
|
|
41
|
+
if (I18n && typeof I18n.t === "function") {
|
|
42
|
+
val = I18n.t(key);
|
|
43
|
+
if (val === key) val = null;
|
|
44
|
+
}
|
|
45
|
+
if (val) el.textContent = val;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function fmtBytes(n) {
|
|
50
|
+
if (!Number.isFinite(n) || n <= 0) return "0 MB";
|
|
51
|
+
const mb = n / (1024 * 1024);
|
|
52
|
+
if (mb < 10) return mb.toFixed(1) + " MB";
|
|
53
|
+
return Math.round(mb) + " MB";
|
|
54
|
+
}
|
|
55
|
+
function fmtRate(bps) {
|
|
56
|
+
if (!Number.isFinite(bps) || bps <= 0) return "—";
|
|
57
|
+
const mb = bps / (1024 * 1024);
|
|
58
|
+
if (mb >= 1) return mb.toFixed(1) + " MB/s";
|
|
59
|
+
const kb = bps / 1024;
|
|
60
|
+
return Math.max(1, Math.round(kb)) + " KB/s";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function currentVersion() {
|
|
64
|
+
// Resolved lazily at init via `window.privateboard.getAppVersion()`.
|
|
65
|
+
// If the IPC roundtrip hasn't landed yet (race against the first
|
|
66
|
+
// `update-available` event), the version delta degrades to just
|
|
67
|
+
// "→ v0.1.23" until the next state event re-paints.
|
|
68
|
+
return appVersion;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function setStateClass(kind) {
|
|
72
|
+
if (!overlayEl) return;
|
|
73
|
+
overlayEl.classList.remove(
|
|
74
|
+
"state-available",
|
|
75
|
+
"state-downloading",
|
|
76
|
+
"state-ready",
|
|
77
|
+
"state-error",
|
|
78
|
+
);
|
|
79
|
+
if (kind === "available" || kind === "downloading" || kind === "ready" || kind === "error") {
|
|
80
|
+
overlayEl.classList.add("state-" + kind);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function openModal() {
|
|
85
|
+
if (!overlayEl) return;
|
|
86
|
+
if (overlayEl.classList.contains("open")) return;
|
|
87
|
+
overlayEl.classList.add("open");
|
|
88
|
+
overlayEl.setAttribute("aria-hidden", "false");
|
|
89
|
+
document.body.classList.add("upd-locked");
|
|
90
|
+
}
|
|
91
|
+
function closeModal() {
|
|
92
|
+
if (!overlayEl) return;
|
|
93
|
+
overlayEl.classList.remove("open");
|
|
94
|
+
overlayEl.setAttribute("aria-hidden", "true");
|
|
95
|
+
document.body.classList.remove("upd-locked");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function paint(state) {
|
|
99
|
+
if (!overlayEl || !state) return;
|
|
100
|
+
const from = currentVersion();
|
|
101
|
+
const to = state.version ? ("v" + state.version) : "";
|
|
102
|
+
overlayEl.querySelectorAll("[data-upd-from-version], [data-upd-from-version-d], [data-upd-from-version-r]").forEach((el) => {
|
|
103
|
+
el.textContent = from ? ("v" + from.replace(/^v/, "")) : "";
|
|
104
|
+
el.style.display = from ? "" : "none";
|
|
105
|
+
});
|
|
106
|
+
overlayEl.querySelectorAll("[data-upd-to-version], [data-upd-to-version-d], [data-upd-to-version-r]").forEach((el) => {
|
|
107
|
+
el.textContent = to;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (state.kind === "downloading") {
|
|
111
|
+
const pct = Math.max(0, Math.min(100, Math.round(state.percent || 0)));
|
|
112
|
+
const pctEl = $("[data-upd-pct]", overlayEl);
|
|
113
|
+
if (pctEl) pctEl.textContent = pct + "%";
|
|
114
|
+
const bar = $("[data-upd-bar]", overlayEl);
|
|
115
|
+
if (bar) {
|
|
116
|
+
bar.classList.remove("indeterminate");
|
|
117
|
+
const span = bar.querySelector("span");
|
|
118
|
+
if (span) span.style.width = pct + "%";
|
|
119
|
+
}
|
|
120
|
+
const bytes = $("[data-upd-bytes]", overlayEl);
|
|
121
|
+
if (bytes) {
|
|
122
|
+
bytes.textContent = fmtBytes(state.transferred) + " / " + fmtBytes(state.total);
|
|
123
|
+
}
|
|
124
|
+
const rate = $("[data-upd-rate]", overlayEl);
|
|
125
|
+
if (rate) rate.textContent = fmtRate(state.bytesPerSecond);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (state.kind === "error") {
|
|
129
|
+
const errEl = $("[data-upd-error-message]", overlayEl);
|
|
130
|
+
if (errEl) errEl.textContent = state.message || "—";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
setStateClass(state.kind);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function shouldAutoOpenFor(state) {
|
|
137
|
+
if (!state) return false;
|
|
138
|
+
if (state.kind === "available") return true; // First prompt on launch.
|
|
139
|
+
if (state.kind === "ready") return true; // Always surface the restart prompt.
|
|
140
|
+
if (state.kind === "downloading") return false; // User asked to hide · don't pop it back.
|
|
141
|
+
if (state.kind === "error") return false; // Errors don't steal focus.
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function applyState(state) {
|
|
146
|
+
if (!state || state.kind === "idle") {
|
|
147
|
+
// Idle (no update / cleared) · keep modal closed.
|
|
148
|
+
lastState = state || { kind: "idle" };
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const isNewKind = !lastState || lastState.kind !== state.kind;
|
|
152
|
+
if (isNewKind) userDismissed = false; // A new transition re-prompts.
|
|
153
|
+
lastState = state;
|
|
154
|
+
paint(state);
|
|
155
|
+
if (overlayEl.classList.contains("open")) {
|
|
156
|
+
// Already open — repaint in place; downloading→ready transitions
|
|
157
|
+
// flow without the modal flickering.
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!userDismissed && shouldAutoOpenFor(state)) openModal();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function wireEvents() {
|
|
164
|
+
overlayEl.addEventListener("click", (e) => {
|
|
165
|
+
const close = e.target.closest("[data-upd-close], [data-upd-dismiss]");
|
|
166
|
+
if (close) {
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
userDismissed = true;
|
|
169
|
+
closeModal();
|
|
170
|
+
bridge.dismiss();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const dl = e.target.closest("[data-upd-download]");
|
|
174
|
+
if (dl) {
|
|
175
|
+
e.preventDefault();
|
|
176
|
+
// Optimistic switch to the downloading state so the user sees
|
|
177
|
+
// the progress card immediately; the first real
|
|
178
|
+
// `download-progress` event will replace the indeterminate
|
|
179
|
+
// sweep with a percentage.
|
|
180
|
+
const v = (lastState && lastState.kind === "available") ? lastState.version : "";
|
|
181
|
+
applyState({ kind: "downloading", version: v, percent: 0, transferred: 0, total: 0, bytesPerSecond: 0 });
|
|
182
|
+
const bar = $("[data-upd-bar]", overlayEl);
|
|
183
|
+
if (bar) bar.classList.add("indeterminate");
|
|
184
|
+
bridge.startDownload();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const inst = e.target.closest("[data-upd-install]");
|
|
188
|
+
if (inst) {
|
|
189
|
+
e.preventDefault();
|
|
190
|
+
// Disable the button to prevent a double-click between the
|
|
191
|
+
// IPC roundtrip and the actual app quit.
|
|
192
|
+
inst.setAttribute("disabled", "true");
|
|
193
|
+
bridge.installNow();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
document.addEventListener("keydown", (e) => {
|
|
198
|
+
if (!overlayEl.classList.contains("open")) return;
|
|
199
|
+
if (e.key !== "Escape") return;
|
|
200
|
+
// Escape never installs · only dismisses. During a download or
|
|
201
|
+
// ready state, this is the same as the "Hide"/"Later" button.
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
userDismissed = true;
|
|
204
|
+
closeModal();
|
|
205
|
+
bridge.dismiss();
|
|
206
|
+
});
|
|
207
|
+
document.addEventListener("boardroom:locale", applyI18n);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function init() {
|
|
211
|
+
overlayEl = document.getElementById("upd-overlay");
|
|
212
|
+
if (!overlayEl) return;
|
|
213
|
+
applyI18n();
|
|
214
|
+
wireEvents();
|
|
215
|
+
// Dev preview · expose state injection so the modal can be auditioned
|
|
216
|
+
// without a packaged build + real GitHub release. From devtools:
|
|
217
|
+
// __updaterDev.show({ kind: "available", version: "0.1.99" })
|
|
218
|
+
// __updaterDev.show({ kind: "downloading", version: "0.1.99",
|
|
219
|
+
// percent: 42, transferred: 5_300_000,
|
|
220
|
+
// total: 12_500_000, bytesPerSecond: 850_000 })
|
|
221
|
+
// __updaterDev.show({ kind: "ready", version: "0.1.99" })
|
|
222
|
+
// __updaterDev.show({ kind: "error", message: "Could not connect" })
|
|
223
|
+
// __updaterDev.close()
|
|
224
|
+
window.__updaterDev = {
|
|
225
|
+
show: (s) => { userDismissed = false; applyState(s); openModal(); },
|
|
226
|
+
close: () => { userDismissed = false; closeModal(); },
|
|
227
|
+
};
|
|
228
|
+
// Resolve the app version once · used for the "v_old → v_new"
|
|
229
|
+
// version delta in the modal header.
|
|
230
|
+
if (typeof window.privateboard.getAppVersion === "function") {
|
|
231
|
+
window.privateboard.getAppVersion().then((v) => {
|
|
232
|
+
appVersion = v || "";
|
|
233
|
+
if (lastState && lastState.kind !== "idle") paint(lastState);
|
|
234
|
+
}).catch(() => {});
|
|
235
|
+
}
|
|
236
|
+
bridge.onState((s) => applyState(s));
|
|
237
|
+
// Re-hydrate · covers the case where update-available already
|
|
238
|
+
// fired before this script's defer-run completed.
|
|
239
|
+
bridge.getState().then((s) => { if (s) applyState(s); }).catch(() => {});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (document.readyState === "loading") {
|
|
243
|
+
document.addEventListener("DOMContentLoaded", init);
|
|
244
|
+
} else {
|
|
245
|
+
init();
|
|
246
|
+
}
|
|
247
|
+
})();
|