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/boot.js +140 -113
- package/dist/boot.js.map +1 -1
- package/dist/cli.js +140 -113
- package/dist/cli.js.map +1 -1
- package/dist/server.js +140 -113
- 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.js +4 -6
- package/public/agent-profile.js +10 -12
- package/public/app-updater.css +318 -0
- package/public/app-updater.js +247 -0
- package/public/app.js +66 -21
- package/public/home.html +1 -1
- package/public/i18n.js +60 -0
- package/public/index.html +168 -32
- package/public/mention-picker.js +527 -0
- package/public/new-agent.js +10 -5
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/dist/version.js.map
CHANGED
|
@@ -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.
|
|
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
package/public/agent-overlay.js
CHANGED
|
@@ -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 = {
|
package/public/agent-profile.js
CHANGED
|
@@ -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
|
+
})();
|