privateboard 0.1.22 → 0.1.24

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/version.d.ts CHANGED
@@ -12,6 +12,6 @@
12
12
  * number ends up surfaced in the user-facing footer or banner. Keep
13
13
  * this file as the canonical source — every callsite reads from here.
14
14
  */
15
- declare const VERSION = "0.1.22";
15
+ declare const VERSION = "0.1.24";
16
16
 
17
17
  export { VERSION };
package/dist/version.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/version.ts
4
- var VERSION = "0.1.22";
4
+ var VERSION = "0.1.24";
5
5
  export {
6
6
  VERSION
7
7
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts"],"sourcesContent":["/**\n * Single source of truth for the app version.\n *\n * Imported by `cli.ts` (CLI banner / `--version`), `server.ts` (the\n * `/health` payload + the `/api/version` endpoint), and bundled into\n * the frontend via the version endpoint. Bump alongside `package.json`\n * on every release — the existing `npm version <patch|minor|major>`\n * + commit pattern updates package.json automatically; this file\n * needs the matching manual bump.\n *\n * If two strings drift (bumped one but not the other), the wrong\n * number ends up surfaced in the user-facing footer or banner. Keep\n * this file as the canonical source — every callsite reads from here.\n */\nexport const VERSION = \"0.1.22\";\n"],"mappings":";;;AAcO,IAAM,UAAU;","names":[]}
1
+ {"version":3,"sources":["../src/version.ts"],"sourcesContent":["/**\n * Single source of truth for the app version.\n *\n * Imported by `cli.ts` (CLI banner / `--version`), `server.ts` (the\n * `/health` payload + the `/api/version` endpoint), and bundled into\n * the frontend via the version endpoint. Bump alongside `package.json`\n * on every release — the existing `npm version <patch|minor|major>`\n * + commit pattern updates package.json automatically; this file\n * needs the matching manual bump.\n *\n * If two strings drift (bumped one but not the other), the wrong\n * number ends up surfaced in the user-facing footer or banner. Keep\n * this file as the canonical source — every callsite reads from here.\n */\nexport const VERSION = \"0.1.24\";\n"],"mappings":";;;AAcO,IAAM,UAAU;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "privateboard",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
4
4
  "description": "PrivateBoard · your private board meeting, on call. Local-first, multi-agent thinking amplifier.",
5
5
  "type": "module",
6
6
  "main": "electron-entry.cjs",
@@ -16,24 +16,22 @@
16
16
  * a key isn't in the table (registry updates lag this map). */
17
17
  const MODEL_LABELS = {
18
18
  "opus-4-7": { name: "Claude Opus 4.7", provider: "Anthropic" },
19
- "opus-4-6": { name: "Claude Opus 4.6", provider: "Anthropic" },
20
19
  "sonnet-4-6": { name: "Claude Sonnet 4.6", provider: "Anthropic" },
21
- "opus-4-6": { name: "Claude Opus 4.6", provider: "Anthropic" },
22
20
  "opus-4-6-fast": { name: "Claude Opus 4.6 Fast", provider: "Anthropic" },
23
21
  "haiku-4-5": { name: "Claude Haiku 4.5", provider: "Anthropic" },
24
22
  "gpt-5-5": { name: "GPT-5.5", provider: "OpenAI" },
25
23
  "gpt-5-4": { name: "GPT-5.4", provider: "OpenAI" },
26
24
  "gpt-5-4-mini": { name: "GPT-5.4 Mini", provider: "OpenAI" },
27
- "gpt-5-5-pro": { name: "GPT-5.5 Pro", provider: "OpenAI" },
28
25
  "codex-5-4": { name: "ChatGPT Codex 5.4", provider: "OpenAI" },
29
26
  "gemini-3-1": { name: "Gemini 3.1 Pro", provider: "Google" },
30
27
  "gemini-3-flash": { name: "Gemini 3 Flash", provider: "Google" },
31
28
  "gemini-3-1-flash": { name: "Gemini 3.1 Flash Lite", provider: "Google" },
32
- "grok-4-3": { name: "Grok 4.3", provider: "xAI" },
33
- "grok-4-1-fast": { name: "Grok 4.1 Fast", provider: "xAI" },
34
- "grok-4-20": { name: "Grok 4.20", provider: "xAI" },
35
29
  "deepseek-v4-pro": { name: "DeepSeek V4 Pro", provider: "DeepSeek" },
36
30
  "deepseek-v4-flash": { name: "DeepSeek Lite", provider: "DeepSeek" },
31
+ "glm-5-1": { name: "GLM 5.1", provider: "Zhipu" },
32
+ "kimi-k2-6": { name: "Kimi K2.6", provider: "Moonshot" },
33
+ "minimax-m2-7": { name: "MiniMax M2.7", provider: "MiniMax" },
34
+ "minimax-m2-5": { name: "MiniMax M2.5", provider: "MiniMax" },
37
35
  };
38
36
 
39
37
  const AGENT_CATALOG = {
@@ -604,9 +604,7 @@
604
604
  // /api/agents record (via window.app.agentsById) and resolves it here.
605
605
  const MODEL_LABELS = {
606
606
  "sonnet-4-6": { name: "Sonnet 4.6", deck: "balanced · default" },
607
- "opus-4-6": { name: "Opus 4.6", deck: "deep reasoning · 1M ctx" },
608
607
  "opus-4-7": { name: "Opus 4.7", deck: "deep reasoning" },
609
- "opus-4-6": { name: "Opus 4.6", deck: "prior-gen flagship" },
610
608
  "opus-4-6-fast": { name: "Opus 4.6 Fast", deck: "faster 4.6 · same intelligence" },
611
609
  "haiku-4-5": { name: "Haiku 4.5", deck: "fast · low-cost" },
612
610
  "gpt-5-5": { name: "GPT-5.5", deck: "flagship · 1M ctx" },
@@ -615,13 +613,13 @@
615
613
  "gemini-3-1": { name: "Gemini 3.1 Pro", deck: "flagship · 1M ctx" },
616
614
  "gemini-3-flash": { name: "Gemini 3 Flash", deck: "frontier flash · 1M ctx" },
617
615
  "gemini-3-1-flash": { name: "Gemini 3.1 Flash Lite", deck: "fast · 1M ctx" },
618
- "grok-4-3": { name: "Grok 4.3", deck: "flagship · 1M ctx" },
619
- "grok-4-1-fast": { name: "Grok 4.1 Fast", deck: "fast · 256k ctx" },
620
- "grok-4-20": { name: "Grok 4.20", deck: "2M ctx · big context" },
621
- "gpt-5-5-pro": { name: "GPT-5.5 Pro", deck: "deep reasoning · 1M ctx" },
622
616
  "codex-5-4": { name: "ChatGPT Codex 5.4", deck: "code · agents" },
623
617
  "deepseek-v4-pro": { name: "DeepSeek V4 Pro", deck: "reasoning · open weights" },
624
618
  "deepseek-v4-flash": { name: "DeepSeek Lite", deck: "V4 Flash · fast · 1M ctx" },
619
+ "glm-5-1": { name: "GLM 5.1", deck: "Zhipu flagship · 200k ctx" },
620
+ "kimi-k2-6": { name: "Kimi K2.6", deck: "Moonshot · long-context" },
621
+ "minimax-m2-7": { name: "MiniMax M2.7", deck: "MiniMax flagship · long-context" },
622
+ "minimax-m2-5": { name: "MiniMax M2.5", deck: "MiniMax prior · long-context" },
625
623
  };
626
624
 
627
625
  function liveModelFor(slug) {
@@ -2340,11 +2338,9 @@
2340
2338
  // Anthropic
2341
2339
  { v: "opus-4-7", name: "Claude Opus 4.7", provider: "Anthropic", deck: "deep reasoning · default" },
2342
2340
  { v: "sonnet-4-6", name: "Claude Sonnet 4.6", provider: "Anthropic", deck: "balanced · 1M ctx" },
2343
- { v: "opus-4-6", name: "Claude Opus 4.6", provider: "Anthropic", deck: "prior-gen flagship" },
2344
2341
  { v: "opus-4-6-fast", name: "Claude Opus 4.6 Fast", provider: "Anthropic", deck: "faster 4.6 · same intelligence" },
2345
2342
  { v: "haiku-4-5", name: "Claude Haiku 4.5", provider: "Anthropic", deck: "fast · low-cost" },
2346
2343
  // OpenAI
2347
- { v: "gpt-5-5-pro", name: "GPT-5.5 Pro", provider: "OpenAI", deck: "flagship · 1M ctx" },
2348
2344
  { v: "gpt-5-5", name: "GPT-5.5", provider: "OpenAI", deck: "1M ctx" },
2349
2345
  { v: "gpt-5-4", name: "GPT-5.4", provider: "OpenAI", deck: "general · 1M ctx" },
2350
2346
  { v: "gpt-5-4-mini", name: "GPT-5.4 Mini", provider: "OpenAI", deck: "fast · 400k ctx" },
@@ -2352,12 +2348,14 @@
2352
2348
  // Google
2353
2349
  { v: "gemini-3-1", name: "Gemini 3.1 Pro", provider: "Google", deck: "multimodal · 1M ctx" },
2354
2350
  { v: "gemini-3-1-flash",name: "Gemini 3.1 Flash", provider: "Google", deck: "fast · 1M ctx" },
2355
- // xAI
2356
- { v: "grok-4-3", name: "Grok 4.3", provider: "xAI", deck: "1M ctx" },
2357
- { v: "grok-4-20", name: "Grok 4.20", provider: "xAI", deck: "2M ctx · big context" },
2358
2351
  // DeepSeek
2359
2352
  { v: "deepseek-v4-pro", name: "DeepSeek V4 Pro", provider: "DeepSeek", deck: "reasoning · open weights" },
2360
- { v: "deepseek-v4-flash", name: "DeepSeek Lite", provider: "DeepSeek", deck: "V4 Flash · fast · 1M ctx" }
2353
+ { v: "deepseek-v4-flash", name: "DeepSeek Lite", provider: "DeepSeek", deck: "V4 Flash · fast · 1M ctx" },
2354
+ // Zhipu · Moonshot · MiniMax (all B.AI routed)
2355
+ { v: "glm-5-1", name: "GLM 5.1", provider: "Zhipu", deck: "Zhipu flagship · 200k ctx" },
2356
+ { v: "kimi-k2-6", name: "Kimi K2.6", provider: "Moonshot", deck: "long-context" },
2357
+ { v: "minimax-m2-7", name: "MiniMax M2.7", provider: "MiniMax", deck: "flagship · long-context" },
2358
+ { v: "minimax-m2-5", name: "MiniMax M2.5", provider: "MiniMax", deck: "prior · long-context" }
2361
2359
  ];
2362
2360
  function modelKey(slug) { return "boardroom.agent.model." + slug; }
2363
2361
 
@@ -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
+ })();