viberoom 0.5.3 → 0.5.4
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/hub.js +9 -0
- package/dist/main.js +10 -0
- package/dist/server.js +23 -0
- package/dist/update.js +95 -0
- package/package.json +1 -1
- package/ui/app.css +9 -1
- package/ui/app.js +83 -0
package/dist/hub.js
CHANGED
|
@@ -113,6 +113,7 @@ export class Hub extends EventEmitter {
|
|
|
113
113
|
diagrams: { preset: "pop", primary: null },
|
|
114
114
|
editor: { ...DEFAULT_EDITOR_SETTINGS },
|
|
115
115
|
appearance: { ...DEFAULT_APPEARANCE },
|
|
116
|
+
checkForUpdates: true,
|
|
116
117
|
roomDefaults: {},
|
|
117
118
|
vendorPresets: {},
|
|
118
119
|
};
|
|
@@ -165,6 +166,8 @@ export class Hub extends EventEmitter {
|
|
|
165
166
|
next.profileCompleted = patch.profileCompleted === true || patch.profileCompleted === "true";
|
|
166
167
|
if (patch.agentSkillsNeedApproval !== undefined)
|
|
167
168
|
next.agentSkillsNeedApproval = patch.agentSkillsNeedApproval === true || patch.agentSkillsNeedApproval === "true";
|
|
169
|
+
if (patch.checkForUpdates !== undefined)
|
|
170
|
+
next.checkForUpdates = patch.checkForUpdates === true || patch.checkForUpdates === "true";
|
|
168
171
|
if (patch.diagrams !== undefined && typeof patch.diagrams === "object" && patch.diagrams) {
|
|
169
172
|
const d = patch.diagrams;
|
|
170
173
|
const preset = String(d.preset ?? next.diagrams?.preset ?? "pop");
|
|
@@ -432,9 +435,15 @@ export class Hub extends EventEmitter {
|
|
|
432
435
|
else
|
|
433
436
|
this.log.info(`removed room ${id}`);
|
|
434
437
|
}
|
|
438
|
+
update = null;
|
|
439
|
+
setUpdate(update) {
|
|
440
|
+
this.update = update;
|
|
441
|
+
this.emit("event", { type: "update", update });
|
|
442
|
+
}
|
|
435
443
|
snapshot() {
|
|
436
444
|
return {
|
|
437
445
|
settings: this.settings,
|
|
446
|
+
update: this.update,
|
|
438
447
|
recipes: listRecipes().map(({ build: _b, ...r }) => r),
|
|
439
448
|
skills: this.skills.list(),
|
|
440
449
|
roomDefaults: { ...DEFAULT_ROOM_SETTINGS, ...this.settings.roomDefaults },
|
package/dist/main.js
CHANGED
|
@@ -86,6 +86,7 @@ Options
|
|
|
86
86
|
--browser open the default browser instead of a Chromium app window
|
|
87
87
|
`);
|
|
88
88
|
}
|
|
89
|
+
import { checkForUpdate } from "./update.js";
|
|
89
90
|
function buildInfo() {
|
|
90
91
|
const pkg = JSON.parse(readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"));
|
|
91
92
|
const built = statSync(fileURLToPath(import.meta.url)).mtime;
|
|
@@ -251,6 +252,15 @@ async function runHub(options, log, info) {
|
|
|
251
252
|
}
|
|
252
253
|
}
|
|
253
254
|
hub.setHubUrl(server.url);
|
|
255
|
+
if (hub.settings.checkForUpdates) {
|
|
256
|
+
void checkForUpdate(hub.dataDir, info.version).then((update) => {
|
|
257
|
+
hub.setUpdate(update);
|
|
258
|
+
if (update.available)
|
|
259
|
+
log.info(`viberoom ${update.latest} is available (this is ${update.current})`);
|
|
260
|
+
else if (update.error)
|
|
261
|
+
log.warn(`update check failed: ${update.error}`);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
254
264
|
if (background)
|
|
255
265
|
writePidFile(options.dataDir, { pid: process.pid, port: options.port, build: info.build, startedAt: Date.now() });
|
|
256
266
|
log.info(`viberoom ${info.version} (build ${info.build}) is open at ${server.url} (data: ${hub.dataDir}; rooms: ${[...hub.rooms.values()].map((r) => r.name).join(", ")})`);
|
package/dist/server.js
CHANGED
|
@@ -40,6 +40,7 @@ const STATIC_FILES = {
|
|
|
40
40
|
"/vendor/mermaid.min.js": { file: "mermaid/dist/mermaid.min.js", type: "text/javascript; charset=utf-8", dir: "node_modules" },
|
|
41
41
|
"/vendor/marked.umd.js": { file: "marked/lib/marked.umd.js", type: "text/javascript; charset=utf-8", dir: "node_modules" },
|
|
42
42
|
};
|
|
43
|
+
import { checkForUpdate, installUpdate, restartWithNewBuild, runsFromSourceCheckout } from "./update.js";
|
|
43
44
|
export function startServer(hub, port, log, info, onShutdownRequest) {
|
|
44
45
|
const uiDir = fileURLToPath(new URL("../ui/", import.meta.url));
|
|
45
46
|
const assetsDir = fileURLToPath(new URL("../assets/", import.meta.url));
|
|
@@ -178,6 +179,28 @@ export function startServer(hub, port, log, info, onShutdownRequest) {
|
|
|
178
179
|
sendJson(res, 200, snapshot());
|
|
179
180
|
return;
|
|
180
181
|
}
|
|
182
|
+
if (req.method === "GET" && path === "/api/update") {
|
|
183
|
+
if (url.searchParams.get("check") === "1")
|
|
184
|
+
hub.setUpdate(await checkForUpdate(hub.dataDir, info.version, { force: true }));
|
|
185
|
+
sendJson(res, 200, hub.update ?? { current: info.version, latest: null, available: false, checkedAt: null, error: null });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (req.method === "POST" && path === "/api/update/install") {
|
|
189
|
+
const mainUrl = new URL("./main.js", import.meta.url).href;
|
|
190
|
+
if (runsFromSourceCheckout(mainUrl))
|
|
191
|
+
throw new Error("this viberoom runs from a source checkout; update it with git pull and npm run update");
|
|
192
|
+
const latest = hub.update?.available ? hub.update.latest : null;
|
|
193
|
+
if (!latest)
|
|
194
|
+
throw new Error("no newer version is known; check for updates first");
|
|
195
|
+
log.info(`installing viberoom ${latest} (npm install -g)`);
|
|
196
|
+
const result = await installUpdate(latest);
|
|
197
|
+
if (!result.ok)
|
|
198
|
+
throw new Error(`npm install failed: ${result.output.slice(-600) || "no output"}`);
|
|
199
|
+
log.info(`viberoom ${latest} installed; starting the new build, which replaces this hub`);
|
|
200
|
+
sendJson(res, 200, { ok: true, version: latest });
|
|
201
|
+
setTimeout(() => restartWithNewBuild(mainUrl, port, hub.dataDir), 300);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
181
204
|
if (req.method === "GET" && path === "/api/version") {
|
|
182
205
|
sendJson(res, 200, info);
|
|
183
206
|
return;
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
export const REGISTRY_URL = "https://registry.npmjs.org/viberoom/latest";
|
|
7
|
+
export const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
const CHECK_FILE = "update-check.json";
|
|
9
|
+
export function parseVersion(v) {
|
|
10
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?$/.exec(v.trim());
|
|
11
|
+
if (!m)
|
|
12
|
+
return null;
|
|
13
|
+
return { parts: [Number(m[1]), Number(m[2]), Number(m[3])], prerelease: !!m[4] };
|
|
14
|
+
}
|
|
15
|
+
export function compareVersions(a, b) {
|
|
16
|
+
const pa = parseVersion(a);
|
|
17
|
+
const pb = parseVersion(b);
|
|
18
|
+
if (!pa || !pb)
|
|
19
|
+
return 0;
|
|
20
|
+
for (let i = 0; i < 3; i++)
|
|
21
|
+
if (pa.parts[i] !== pb.parts[i])
|
|
22
|
+
return pa.parts[i] - pb.parts[i];
|
|
23
|
+
if (pa.prerelease !== pb.prerelease)
|
|
24
|
+
return pa.prerelease ? -1 : 1;
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
export function readCheckRecord(dataDir) {
|
|
28
|
+
const file = join(dataDir, CHECK_FILE);
|
|
29
|
+
if (!existsSync(file))
|
|
30
|
+
return null;
|
|
31
|
+
try {
|
|
32
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
33
|
+
if (typeof raw.checkedAt !== "string")
|
|
34
|
+
return null;
|
|
35
|
+
return { checkedAt: raw.checkedAt, latest: typeof raw.latest === "string" ? raw.latest : null, error: typeof raw.error === "string" ? raw.error : null };
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function toInfo(current, record) {
|
|
42
|
+
const latest = record?.latest ?? null;
|
|
43
|
+
return { current, latest, available: latest !== null && compareVersions(latest, current) > 0, checkedAt: record?.checkedAt ?? null, error: record?.error ?? null };
|
|
44
|
+
}
|
|
45
|
+
export async function checkForUpdate(dataDir, current, options = {}) {
|
|
46
|
+
const now = options.now ?? Date.now;
|
|
47
|
+
const previous = readCheckRecord(dataDir);
|
|
48
|
+
if (!options.force && previous && now() - Date.parse(previous.checkedAt) < CHECK_INTERVAL_MS)
|
|
49
|
+
return toInfo(current, previous);
|
|
50
|
+
const doFetch = options.fetchImpl ?? fetch;
|
|
51
|
+
let record;
|
|
52
|
+
try {
|
|
53
|
+
const res = await doFetch(REGISTRY_URL, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(options.timeoutMs ?? 6000) });
|
|
54
|
+
if (!res.ok)
|
|
55
|
+
throw new Error(`registry answered ${res.status}`);
|
|
56
|
+
const body = (await res.json());
|
|
57
|
+
if (typeof body.version !== "string" || !parseVersion(body.version))
|
|
58
|
+
throw new Error("registry answer had no version");
|
|
59
|
+
record = { checkedAt: new Date(now()).toISOString(), latest: body.version, error: null };
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
63
|
+
record = { checkedAt: new Date(now()).toISOString(), latest: previous?.latest ?? null, error: message.replace(/\s+/g, " ").slice(0, 200) };
|
|
64
|
+
}
|
|
65
|
+
writeFileSync(join(dataDir, CHECK_FILE), JSON.stringify(record, null, 2));
|
|
66
|
+
return toInfo(current, record);
|
|
67
|
+
}
|
|
68
|
+
export function runsFromSourceCheckout(mainModuleUrl) {
|
|
69
|
+
const path = decodeURIComponent(new URL(mainModuleUrl).pathname);
|
|
70
|
+
return !/\/node_modules\/viberoom\//.test(path);
|
|
71
|
+
}
|
|
72
|
+
export function installCommandLine(version) {
|
|
73
|
+
if (!parseVersion(version))
|
|
74
|
+
throw new Error(`not a version: ${version}`);
|
|
75
|
+
return `npm install -g viberoom@${version} --no-audit --no-fund`;
|
|
76
|
+
}
|
|
77
|
+
export function installUpdate(version) {
|
|
78
|
+
const line = installCommandLine(version);
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
const child = process.platform === "win32" ? spawn(line, { shell: true, windowsHide: true }) : spawn("npm", line.split(" ").slice(1));
|
|
81
|
+
let output = "";
|
|
82
|
+
const collect = (chunk) => {
|
|
83
|
+
output = (output + chunk.toString()).slice(-4000);
|
|
84
|
+
};
|
|
85
|
+
child.stdout?.on("data", collect);
|
|
86
|
+
child.stderr?.on("data", collect);
|
|
87
|
+
child.on("error", (error) => resolve({ ok: false, output: `${output}\n${error.message}`.trim() }));
|
|
88
|
+
child.on("close", (code) => resolve({ ok: code === 0, output: output.trim() }));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export function restartWithNewBuild(mainModuleUrl, port, dataDir) {
|
|
92
|
+
const main = fileURLToPath(mainModuleUrl);
|
|
93
|
+
const child = spawn(process.execPath, [main, "start", "--port", String(port), "--data-dir", dataDir, "--no-open"], { detached: true, stdio: "ignore", windowsHide: true });
|
|
94
|
+
child.unref();
|
|
95
|
+
}
|
package/package.json
CHANGED
package/ui/app.css
CHANGED
|
@@ -44,7 +44,15 @@
|
|
|
44
44
|
.rail-toggle { width: 44px; height: 24px; margin-top: 8px; border-radius: 8px; border: 0; background: transparent; color: var(--faint); display: grid; place-content: center; padding: 0; align-self: center; transition: background var(--t-fast), color var(--t-fast); }
|
|
45
45
|
.rail-toggle .i { width: 16px; height: 16px; }
|
|
46
46
|
.rail-toggle:hover { background: #fff; color: var(--primary); }
|
|
47
|
-
.rail-foot { margin-top: auto; display: flex; flex-direction: column; gap: 0; align-items: center; }
|
|
47
|
+
.rail-foot { position: relative; margin-top: auto; display: flex; flex-direction: column; gap: 0; align-items: center; }
|
|
48
|
+
.update-pop { position: absolute; left: 12px; bottom: calc(100% - 2px); z-index: 30; display: flex; gap: 6px; align-items: flex-start; width: 250px; padding: 10px 8px 10px 14px; background: #fff; border-radius: 18px 18px 18px 6px; box-shadow: 0 10px 28px -12px rgba(28, 27, 51, 0.45), var(--shadow-tile); font-size: 12.5px; font-weight: 600; color: var(--ink-2); animation: bubble-in 0.45s var(--ease-out); }
|
|
49
|
+
.update-pop::after { content: ""; position: absolute; left: 14px; bottom: -6px; width: 12px; height: 12px; background: #fff; border-radius: 0 0 3px 0; transform: rotate(45deg); box-shadow: 3px 3px 4px -3px rgba(28, 27, 51, 0.25); }
|
|
50
|
+
.update-pop .up-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
51
|
+
.update-pop .up-text { line-height: 1.4; }
|
|
52
|
+
.update-pop .up-text b { color: var(--ink); }
|
|
53
|
+
.update-pop .up-go { align-self: flex-start; }
|
|
54
|
+
.update-pop .up-side { flex: none; display: flex; flex-direction: column; gap: 2px; }
|
|
55
|
+
.update-pop .icon-btn.sm { width: 26px; height: 26px; }
|
|
48
56
|
.shell.rail-open .rail-foot { align-items: stretch; padding: 0 14px; }
|
|
49
57
|
.rail-item { position: relative; display: flex; align-items: center; justify-content: center; gap: 10px; width: 44px; height: 44px; padding: 0; border: 0; background: transparent; border-radius: 14px; color: var(--muted); font-weight: 800; font-size: 13px; white-space: nowrap; transition: background var(--t-fast), color var(--t-fast), box-shadow var(--t-fast); }
|
|
50
58
|
.shell.rail-open .rail-item { width: auto; justify-content: flex-start; padding: 0 12px 0 0; }
|
package/ui/app.js
CHANGED
|
@@ -823,6 +823,51 @@
|
|
|
823
823
|
maybeOfferReconnect();
|
|
824
824
|
}
|
|
825
825
|
|
|
826
|
+
function renderUpdatePop() {
|
|
827
|
+
const old = $("#update-pop");
|
|
828
|
+
const u = state.update;
|
|
829
|
+
const show = u && u.available && u.latest && recall("updateDismissed") !== u.latest;
|
|
830
|
+
if (!show) {
|
|
831
|
+
if (old && !old.dataset.busy) old.remove();
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
if (old && old.dataset.version === u.latest) return;
|
|
835
|
+
if (old) old.remove();
|
|
836
|
+
const pop = document.createElement("div");
|
|
837
|
+
pop.id = "update-pop";
|
|
838
|
+
pop.className = "update-pop";
|
|
839
|
+
pop.dataset.version = u.latest;
|
|
840
|
+
pop.innerHTML = `<div class="up-main"><div class="up-text"><b>viberoom ${esc(u.latest)}</b> is out. You have ${esc(u.current)}.</div><button type="button" class="btn sm primary up-go">Update now and restart</button></div>
|
|
841
|
+
<div class="up-side"><button type="button" class="icon-btn sm up-x" title="Not now">${ic("close")}</button><button type="button" class="icon-btn sm up-settings" title="Update settings">${ic("settings")}</button></div>`;
|
|
842
|
+
pop.querySelector(".up-x").addEventListener("click", () => {
|
|
843
|
+
remember("updateDismissed", u.latest);
|
|
844
|
+
pop.remove();
|
|
845
|
+
});
|
|
846
|
+
pop.querySelector(".up-settings").addEventListener("click", () => setView("settings"));
|
|
847
|
+
pop.querySelector(".up-go").addEventListener("click", () => installUpdate(pop, u.latest));
|
|
848
|
+
els.rail.querySelector(".rail-foot").appendChild(pop);
|
|
849
|
+
}
|
|
850
|
+
async function installUpdate(pop, version) {
|
|
851
|
+
const go = pop.querySelector(".up-go");
|
|
852
|
+
const text = pop.querySelector(".up-text");
|
|
853
|
+
pop.dataset.busy = "1";
|
|
854
|
+
go.disabled = true;
|
|
855
|
+
go.classList.add("loading");
|
|
856
|
+
text.innerHTML = `Installing <b>viberoom ${esc(version)}</b>… this takes a moment.`;
|
|
857
|
+
try {
|
|
858
|
+
await post("/api/update/install", {});
|
|
859
|
+
go.classList.remove("loading");
|
|
860
|
+
text.innerHTML = `<b>viberoom ${esc(version)}</b> is installed. Restarting…`;
|
|
861
|
+
go.hidden = true;
|
|
862
|
+
} catch (e) {
|
|
863
|
+
delete pop.dataset.busy;
|
|
864
|
+
go.classList.remove("loading");
|
|
865
|
+
go.disabled = false;
|
|
866
|
+
go.textContent = "Try again";
|
|
867
|
+
text.innerHTML = `<span class="error">${esc(e.message || String(e))}</span>`;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
826
871
|
function renderRail() {
|
|
827
872
|
els.rail.querySelectorAll(".rail-item[data-nav]").forEach((b) => {
|
|
828
873
|
const nav = b.dataset.nav;
|
|
@@ -2204,6 +2249,12 @@
|
|
|
2204
2249
|
</div>
|
|
2205
2250
|
</div>
|
|
2206
2251
|
<div>
|
|
2252
|
+
<div class="section" id="sp-update">
|
|
2253
|
+
${sectionTitle("refresh", "Updates")}
|
|
2254
|
+
<label class="switch"><span class="label">Check for updates once a day<span class="hint">At start, one request to the npm registry for the latest viberoom version; nothing else leaves this machine. A newer version shows as a bubble over your avatar.</span></span><input type="checkbox" id="sp-updates" ${s.checkForUpdates !== false ? "checked" : ""}></label>
|
|
2255
|
+
<p class="hint" id="sp-update-status">${updateStatusText()}</p>
|
|
2256
|
+
<button type="button" class="btn sm" id="sp-update-check">Check now</button>
|
|
2257
|
+
</div>
|
|
2207
2258
|
<div class="section">
|
|
2208
2259
|
${sectionTitle("spark", "Vibemates on this machine")}
|
|
2209
2260
|
${machine || '<p class="hint">No supported vibemate is installed yet.</p>'}
|
|
@@ -2288,6 +2339,20 @@
|
|
|
2288
2339
|
});
|
|
2289
2340
|
$("#sp-font").addEventListener("change", () => (sample.style.fontFamily = FONTS.text[$("#sp-font").value].stack));
|
|
2290
2341
|
$("#sp-mono").addEventListener("change", () => sample.querySelectorAll("code").forEach((c) => (c.style.fontFamily = FONTS.mono[$("#sp-mono").value].stack)));
|
|
2342
|
+
$("#sp-update-check").addEventListener("click", async () => {
|
|
2343
|
+
const b = $("#sp-update-check");
|
|
2344
|
+
b.disabled = true;
|
|
2345
|
+
b.classList.add("loading");
|
|
2346
|
+
try {
|
|
2347
|
+
state.update = await get("/api/update?check=1");
|
|
2348
|
+
$("#sp-update-status").textContent = updateStatusText();
|
|
2349
|
+
renderUpdatePop();
|
|
2350
|
+
} catch (e) {
|
|
2351
|
+
showError(e);
|
|
2352
|
+
}
|
|
2353
|
+
b.disabled = false;
|
|
2354
|
+
b.classList.remove("loading");
|
|
2355
|
+
});
|
|
2291
2356
|
bindSave($("#sp-form"), $("#sp-save"), async () => {
|
|
2292
2357
|
const vendorPresets = {};
|
|
2293
2358
|
els.pageInner.querySelectorAll("input[data-vendor]").forEach((inp) => {
|
|
@@ -2297,6 +2362,7 @@
|
|
|
2297
2362
|
await post("/api/settings", {
|
|
2298
2363
|
bypassPermissionsByDefault: $("#sp-bypass").checked,
|
|
2299
2364
|
agentSkillsNeedApproval: $("#sp-skill-approval").checked,
|
|
2365
|
+
checkForUpdates: $("#sp-updates").checked,
|
|
2300
2366
|
diagrams: { preset: $("#sp-diagram-preset").value, primary: $("#sp-diagram-custom").checked ? $("#sp-diagram-color").value : null },
|
|
2301
2367
|
editor: { mode: $("#sp-editor-mode").value, command: $("#sp-editor-cmd").value },
|
|
2302
2368
|
appearance: { chatFontSize: Number($("#sp-chat-fs").value), font: $("#sp-font").value, mono: $("#sp-mono").value },
|
|
@@ -2314,6 +2380,16 @@
|
|
|
2314
2380
|
});
|
|
2315
2381
|
}
|
|
2316
2382
|
|
|
2383
|
+
function updateStatusText() {
|
|
2384
|
+
const u = state.update;
|
|
2385
|
+
const v = state.version ? state.version.version : "?";
|
|
2386
|
+
if (!u || !u.checkedAt) return `This is viberoom ${v}; not checked yet.`;
|
|
2387
|
+
const when = new Date(u.checkedAt).toLocaleString();
|
|
2388
|
+
if (u.available) return `viberoom ${u.latest} is available (this is ${u.current}); checked ${when}.`;
|
|
2389
|
+
if (u.error) return `Could not reach the registry (${u.error}); checked ${when}.`;
|
|
2390
|
+
return `This is viberoom ${u.current}, the latest; checked ${when}.`;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2317
2393
|
function skillBadges(sk) {
|
|
2318
2394
|
const out = [];
|
|
2319
2395
|
if (sk.userInvocable === false) out.push('<span class="badge">vibemate only</span>');
|
|
@@ -3163,6 +3239,8 @@
|
|
|
3163
3239
|
function loadSnapshot(snapshot) {
|
|
3164
3240
|
state.settings = snapshot.settings;
|
|
3165
3241
|
applyAppearance();
|
|
3242
|
+
state.update = snapshot.update || null;
|
|
3243
|
+
renderUpdatePop();
|
|
3166
3244
|
state.version = snapshot.version || null;
|
|
3167
3245
|
state.skills = snapshot.skills || [];
|
|
3168
3246
|
state.recipes = snapshot.recipes || [];
|
|
@@ -3346,6 +3424,11 @@
|
|
|
3346
3424
|
if (state.detailsOpen && state.selection.kind === "me" && !editingInDetails()) renderDetails();
|
|
3347
3425
|
if (state.view === "room") renderSideRoom();
|
|
3348
3426
|
});
|
|
3427
|
+
es.addEventListener("update", (e) => {
|
|
3428
|
+
state.update = JSON.parse(e.data).update;
|
|
3429
|
+
renderUpdatePop();
|
|
3430
|
+
if (state.view === "settings" && !editingInDetails()) renderSettingsPage();
|
|
3431
|
+
});
|
|
3349
3432
|
es.addEventListener("reset", () => location.href = "/");
|
|
3350
3433
|
}
|
|
3351
3434
|
function releaseStream() {
|