Skip to content

Commit dece7af

Browse files
committed
Polish app stability and security
1 parent 3479177 commit dece7af

9 files changed

Lines changed: 434 additions & 45 deletions

File tree

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,21 @@ RigScope is local-first. It does not send telemetry anywhere. The server binds t
5050

5151
Some identifiers such as MAC addresses are partially masked in the UI. Exported reports are intended for local use; review them before sharing publicly.
5252

53+
## Community Score Sync
54+
55+
The Community tab works offline by default. `Save / Sync Profile` stores only a reduced public score card: setup name, owner label, RigScore, CPU/GPU/RAM/storage summary, OS, board, and benchmark numbers. It does not publish raw hardware inventory.
56+
57+
Optional GitHub-backed sync can be enabled on the local server:
58+
59+
```powershell
60+
$env:RIGSCOPE_COMMUNITY_FEED_URL="https://gist.githubusercontent.com/<user>/<gist>/raw/rigscope-community.json"
61+
$env:RIGSCOPE_GITHUB_GIST_ID="<gist-id>"
62+
$env:RIGSCOPE_GITHUB_TOKEN="<fine-grained-token>"
63+
npm start
64+
```
65+
66+
`RIGSCOPE_COMMUNITY_FEED_URL` reads a public JSON leaderboard from GitHub raw/gist. `RIGSCOPE_GITHUB_GIST_ID` plus `RIGSCOPE_GITHUB_TOKEN` enables publishing through the backend only; tokens are never accepted by or stored in the browser UI.
67+
5368
## Current Sections
5469

5570
- Overview

electron/main.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ const { app, BrowserWindow, shell } = require("electron");
22
const { startServer } = require("../server");
33

44
const APP_URL = "http://127.0.0.1:8787";
5+
let mainWindow = null;
56

67
function createWindow() {
8+
if (mainWindow && !mainWindow.isDestroyed()) {
9+
if (mainWindow.isMinimized()) mainWindow.restore();
10+
mainWindow.focus();
11+
return mainWindow;
12+
}
713
const win = new BrowserWindow({
814
width: 1440,
915
height: 980,
@@ -18,23 +24,41 @@ function createWindow() {
1824
sandbox: true
1925
}
2026
});
27+
mainWindow = win;
2128

2229
win.webContents.setWindowOpenHandler(({ url }) => {
23-
shell.openExternal(url);
30+
if (/^https?:\/\//i.test(url)) shell.openExternal(url);
2431
return { action: "deny" };
2532
});
2633

34+
win.on("closed", () => {
35+
if (mainWindow === win) mainWindow = null;
36+
});
37+
2738
win.loadURL(APP_URL);
39+
return win;
2840
}
2941

30-
app.whenReady().then(() => {
31-
startServer();
42+
const gotSingleInstanceLock = app.requestSingleInstanceLock();
43+
if (!gotSingleInstanceLock) {
44+
app.quit();
45+
} else {
46+
app.on("second-instance", () => {
47+
createWindow();
48+
});
49+
50+
app.whenReady().then(async () => {
51+
await startServer();
3252
createWindow();
3353

3454
app.on("activate", () => {
3555
if (BrowserWindow.getAllWindows().length === 0) createWindow();
3656
});
37-
});
57+
}).catch((error) => {
58+
console.error(error);
59+
app.quit();
60+
});
61+
}
3862

3963
app.on("window-all-closed", () => {
4064
if (process.platform !== "darwin") app.quit();

native-runners.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,24 @@ function getStatus(reason = "status") {
119119
};
120120
}
121121

122+
function isChildAlive(child) {
123+
if (!child || !child.pid || child.exitCode !== null || child.signalCode !== null) return false;
124+
try {
125+
process.kill(child.pid, 0);
126+
return true;
127+
} catch {
128+
return false;
129+
}
130+
}
131+
122132
function startProfile(options = {}) {
123133
if (options.acknowledgement !== ACK) {
124134
throw new Error(`Native stress launch requires acknowledgement "${ACK}".`);
125135
}
136+
if (nativeRunner.active && !isChildAlive(nativeRunner.child)) {
137+
nativeRunner.active = false;
138+
nativeRunner.child = null;
139+
}
126140
if (nativeRunner.active) {
127141
throw new Error(`Native runner is already active: ${nativeRunner.profileId}.`);
128142
}

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "rigscope",
3-
"version": "0.3.6",
3+
"version": "0.3.7",
44
"private": true,
55
"description": "Cross-platform local hardware inventory, diagnostics, benchmark, and stress-test dashboard.",
66
"main": "electron/main.js",

public/app.js

Lines changed: 96 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ const state = {
2424
result: null
2525
},
2626
selectedSetup: "local",
27-
savedProfile: null
27+
savedProfile: null,
28+
community: { profiles: [], status: "offline", mode: "local", publishing: "local-only" }
2829
};
2930

3031
const demoSetups = [
@@ -68,13 +69,46 @@ const clamp = (n, min = 0, max = 100) => Math.max(min, Math.min(max, Number(n) |
6869
const pct = (n) => `${Math.round(clamp(n))}%`;
6970
const mb = (n) => `${Math.round(Number(n) || 0)} MB`;
7071
const gb = (n) => `${Number(n || 0).toFixed(1)} GB`;
71-
const esc = (value) => String(value ?? "-").replace(/[&<>"']/g, (ch) => ({
72+
const isMissing = (value) => value === null || value === undefined || value === "" || value === "null" || value === "undefined";
73+
const prettyValue = (value, fallback = "-") => {
74+
if (isMissing(value)) return fallback;
75+
if (typeof value === "number") return Number.isFinite(value) ? String(value) : fallback;
76+
if (typeof value === "boolean") return value ? "yes" : "no";
77+
if (Array.isArray(value)) {
78+
const text = value.map((item) => prettyValue(item, "")).filter(Boolean).join(", ");
79+
return text || fallback;
80+
}
81+
if (typeof value === "object") {
82+
const preferred = value.name ?? value.label ?? value.caption ?? value.value ?? value.status ?? value.id;
83+
if (!isMissing(preferred)) return prettyValue(preferred, fallback);
84+
try {
85+
return JSON.stringify(value).replace(/[{}"]/g, "").slice(0, 180) || fallback;
86+
} catch {
87+
return fallback;
88+
}
89+
}
90+
const text = String(value).trim();
91+
if (!text || text === "[object Object]") return fallback;
92+
return text;
93+
};
94+
const esc = (value) => prettyValue(value).replace(/[&<>"']/g, (ch) => ({
7295
"&": "&amp;",
7396
"<": "&lt;",
7497
">": "&gt;",
7598
"\"": "&quot;",
7699
"'": "&#39;"
77100
}[ch]));
101+
const safeEsc = (value, fallback = "-") => esc(prettyValue(value, fallback));
102+
103+
async function parseApiError(response) {
104+
const text = await response.text();
105+
try {
106+
const payload = JSON.parse(text);
107+
return prettyValue(payload.error || payload.message || text, "не удалось");
108+
} catch {
109+
return prettyValue(text, "не удалось");
110+
}
111+
}
78112

79113
function setBar(id, value) {
80114
const el = $(id);
@@ -412,7 +446,7 @@ async function startCpuStress(durationSec) {
412446
headers: { "Content-Type": "application/json" },
413447
body: JSON.stringify({ cpu: true, memory: false, durationSec, workers: threads })
414448
});
415-
if (!response.ok) throw new Error(await response.text());
449+
if (!response.ok) throw new Error(await parseApiError(response));
416450
const result = await response.json();
417451
const status = result.started?.cpu || {};
418452
state.stress.serverCpu = true;
@@ -427,7 +461,7 @@ async function startServerStress(options) {
427461
headers: { "Content-Type": "application/json" },
428462
body: JSON.stringify({ ...options, workers: threads })
429463
});
430-
if (!response.ok) throw new Error(await response.text());
464+
if (!response.ok) throw new Error(await parseApiError(response));
431465
const result = await response.json();
432466
if (result.started?.cpu) {
433467
state.stress.serverCpu = true;
@@ -717,17 +751,17 @@ function localProfile() {
717751
const score = calculateRigScore().score || state.savedProfile?.score || 0;
718752
return {
719753
id: "local",
720-
name: `${inv.gpu?.name || "Local"} Rig`,
721-
owner: inv.system?.user || "you",
754+
name: `${prettyValue(inv.gpu?.name, "Local")} Rig`,
755+
owner: prettyValue(inv.system?.user, "you"),
722756
score,
723-
cpu: inv.cpu?.name || "-",
724-
gpu: inv.gpu?.name || "-",
725-
memory: `${inv.memory?.totalGb || "-"} GB`,
757+
cpu: prettyValue(inv.cpu?.name),
758+
gpu: prettyValue(inv.gpu?.name),
759+
memory: `${prettyValue(inv.memory?.totalGb)} GB`,
726760
storage: `${inv.physicalDisks?.length || 0} drives`,
727-
board: inv.board?.product || "-",
728-
os: inv.os?.caption || "-",
761+
board: prettyValue(inv.board?.product),
762+
os: prettyValue(inv.os?.caption),
729763
bench: {
730-
cpu: state.bench.cpu?.score || "-",
764+
cpu: prettyValue(state.bench.cpu?.score),
731765
memory: state.bench.memory ? `${state.bench.memory.gbps} GB/s` : "-",
732766
gpu: state.bench.gpu ? `${state.bench.gpu.fps} fps` : "-",
733767
sensors: state.bench.sensors ? sensorLine(state.bench.sensors) : "-"
@@ -738,15 +772,25 @@ function localProfile() {
738772

739773
function setupProfiles() {
740774
const saved = state.savedProfile ? [state.savedProfile] : [];
741-
return [localProfile(), ...saved.filter((p) => p.id !== "local-saved"), ...demoSetups];
775+
const remote = state.community?.profiles || [];
776+
const seen = new Set();
777+
return [localProfile(), ...saved.filter((p) => p.id !== "local-saved"), ...remote, ...demoSetups]
778+
.filter((profile) => {
779+
const id = prettyValue(profile.id, profile.name || "setup");
780+
if (seen.has(id)) return false;
781+
seen.add(id);
782+
return true;
783+
});
742784
}
743785

744786
function renderCommunity() {
745787
if (!state.snapshot) return;
746788
const profiles = setupProfiles().sort((a, b) => Number(b.score || 0) - Number(a.score || 0));
747789
const local = localProfile();
748790
$("localSetupName").textContent = local.name;
749-
$("localSetupMeta").textContent = local.score ? `RigScore ${local.score} · ${local.cpu}` : `${local.cpu} · run Lab tests for a public score`;
791+
$("localSetupMeta").textContent = local.score
792+
? `RigScore ${local.score} · ${local.cpu} · sync ${state.community?.status || "local"}`
793+
: `${local.cpu} · run Lab tests for a public score`;
750794
$("setupCards").innerHTML = profiles.map((profile) => `
751795
<button class="setup-card ${state.selectedSetup === profile.id ? "active" : ""}" data-setup-id="${esc(profile.id)}">
752796
<span>${esc(profile.owner)}</span>
@@ -782,9 +826,23 @@ function renderCommunity() {
782826
]);
783827
}
784828

785-
function saveLocalProfile() {
829+
async function saveLocalProfile() {
786830
state.savedProfile = { ...localProfile(), id: "local-saved", name: "Saved Local Snapshot" };
787831
localStorage.setItem("rigscope.profile", JSON.stringify(state.savedProfile));
832+
try {
833+
const response = await fetch("/api/community/profile", {
834+
method: "POST",
835+
headers: { "Content-Type": "application/json" },
836+
body: JSON.stringify(state.savedProfile)
837+
});
838+
if (!response.ok) throw new Error(await parseApiError(response));
839+
const result = await response.json();
840+
state.community.status = result.github || result.status || "saved locally";
841+
await loadCommunity();
842+
} catch (error) {
843+
state.community.status = "не удалось синхронизировать";
844+
console.error(error);
845+
}
788846
renderCommunity();
789847
}
790848

@@ -1143,11 +1201,11 @@ function update(snapshot) {
11431201
async function refresh() {
11441202
try {
11451203
const response = await fetch("/api/snapshot", { cache: "no-store" });
1146-
if (!response.ok) throw new Error(await response.text());
1204+
if (!response.ok) throw new Error(await parseApiError(response));
11471205
update(await response.json());
11481206
$("livePulse").style.background = "var(--green)";
11491207
} catch (error) {
1150-
$("updatedAt").textContent = "telemetry error";
1208+
$("updatedAt").textContent = "не удалось загрузить телеметрию";
11511209
$("livePulse").style.background = "var(--red)";
11521210
console.error(error);
11531211
}
@@ -1156,7 +1214,7 @@ async function refresh() {
11561214
async function loadToolkit() {
11571215
try {
11581216
const response = await fetch("/api/toolkit", { cache: "no-store" });
1159-
if (!response.ok) throw new Error(await response.text());
1217+
if (!response.ok) throw new Error(await parseApiError(response));
11601218
state.toolkit = await response.json();
11611219
if (state.snapshot) {
11621220
renderToolkit();
@@ -1171,7 +1229,7 @@ async function loadToolkit() {
11711229
async function loadNativeRunners() {
11721230
try {
11731231
const response = await fetch("/api/native-runners", { cache: "no-store" });
1174-
if (!response.ok) throw new Error(await response.text());
1232+
if (!response.ok) throw new Error(await parseApiError(response));
11751233
const payload = await response.json();
11761234
state.nativeRunners = payload;
11771235
state.nativeRunnerStatus = payload.status;
@@ -1182,6 +1240,19 @@ async function loadNativeRunners() {
11821240
}
11831241
}
11841242

1243+
async function loadCommunity() {
1244+
try {
1245+
const response = await fetch("/api/community", { cache: "no-store" });
1246+
if (!response.ok) throw new Error(await parseApiError(response));
1247+
state.community = await response.json();
1248+
if (state.snapshot) renderCommunity();
1249+
} catch (error) {
1250+
state.community = { profiles: [], status: "не удалось загрузить", mode: "local", publishing: "local-only" };
1251+
if (state.snapshot) renderCommunity();
1252+
console.error(error);
1253+
}
1254+
}
1255+
11851256
async function pollNativeRunner() {
11861257
try {
11871258
const response = await fetch("/api/native-runners/status", { cache: "no-store" });
@@ -1216,7 +1287,7 @@ async function startNativeRunner() {
12161287
acknowledgement
12171288
})
12181289
});
1219-
if (!response.ok) throw new Error(await response.text());
1290+
if (!response.ok) throw new Error(await parseApiError(response));
12201291
state.nativeRunnerStatus = await response.json();
12211292
renderNativeRunners();
12221293
if (!state.nativeRunnerTimer) state.nativeRunnerTimer = setInterval(pollNativeRunner, 1500);
@@ -1229,7 +1300,7 @@ async function startNativeRunner() {
12291300
async function stopNativeRunner() {
12301301
try {
12311302
const response = await fetch("/api/native-runners/stop", { method: "POST", cache: "no-store" });
1232-
if (!response.ok) throw new Error(await response.text());
1303+
if (!response.ok) throw new Error(await parseApiError(response));
12331304
state.nativeRunnerStatus = await response.json();
12341305
renderNativeRunners();
12351306
} catch (error) {
@@ -1257,14 +1328,14 @@ async function runServerBench(type, url, buttonId, statusId, runningText) {
12571328
$(statusId).textContent = runningText;
12581329
try {
12591330
const response = await fetch(url, { method: "POST", cache: "no-store" });
1260-
if (!response.ok) throw new Error(await response.text());
1331+
if (!response.ok) throw new Error(await parseApiError(response));
12611332
state.bench[type] = await response.json();
12621333
if (state.snapshot) {
12631334
renderSuite(state.snapshot);
12641335
renderLab(state.snapshot);
12651336
}
12661337
} catch (error) {
1267-
$(statusId).textContent = "Benchmark error";
1338+
$(statusId).textContent = `Не удалось: ${prettyValue(error.message, "ошибка теста")}`;
12681339
console.error(error);
12691340
} finally {
12701341
button.disabled = false;
@@ -1347,5 +1418,7 @@ try {
13471418
refresh();
13481419
loadToolkit();
13491420
loadNativeRunners();
1421+
loadCommunity();
13501422
setView(location.hash.slice(1) || "overview");
13511423
setInterval(refresh, 7000);
1424+
setInterval(loadCommunity, 60000);

public/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ <h2 id="machineName">System scan</h2>
264264
<span id="localSetupMeta">Run Lab tests to publish a stronger profile</span>
265265
</div>
266266
<div class="community-actions">
267-
<button id="saveSetupButton" class="command-button">Save Local Profile</button>
267+
<button id="saveSetupButton" class="command-button">Save / Sync Profile</button>
268268
<button id="exportSetupButton" class="command-button">Export Profile</button>
269269
</div>
270270
</div>

0 commit comments

Comments
 (0)