From 187b1e1a3ad54672fb15daed58e01878e3abcb8a Mon Sep 17 00:00:00 2001
From: GeiserX <9169332+GeiserX@users.noreply.github.com>
Date: Fri, 10 Jul 2026 14:46:19 +0200
Subject: [PATCH 1/3] =?UTF-8?q?fix:=20address=20code-audit=20findings=20?=
=?UTF-8?q?=E2=80=94=20perf,=20error-handling,=20consistency,=20CI=20(wave?=
=?UTF-8?q?=201)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
From a full-codebase multi-perspective audit. Wave 1 = the safe, high-value,
mechanical fixes (concurrency refactors + large refactors deferred).
Performance:
- store: add idx_earnings_platform_created on earnings(platform, created_at) —
the fastest-growing table had zero indexes; ListDailyBalances/ListLatestEarnings/
PurgeOldData full-scanned it under a single SQLite connection.
- app: GetAppState fetches ListLatestEarnings/ListDeployments once and threads
them into computeEarningsSummary/notifications (was 3x/2x per call).
Error handling:
- store: RecordEvent no longer swallows its INSERT error (it sinks all health
events; a silent failure masked crash-loops) — logs on failure.
- native: updatePID logs a registry-persist failure, like persistRunning.
- frontend: renderSettings/renderFleet/removeFleetDevice/hydrateWizardForm and
clipboard copy now try/catch their awaited bound calls (were silent dead tabs
/ empty forms / unhandled rejections).
Consistency / correctness:
- runtime: unify the two divergent memory parsers into one canonical
parseMemoryBytes (fractional + overflow-guarded); the native path now shares
the Docker grammar, removing the fail-open-on-fractional divergence. Deletes
the duplicate parseMemBytes + its test.
- frontend: escapeHtml all slug interpolations consistently.
Test:
- fix flaky TestNativeSupervisorStopsAfterMaxRestarts (5s deadline -> the repo's
waitFor 15s poll; ~4s consumed by real fork/exec under -race intermittently
reddened CI).
CI / security:
- pin go-version 1.26.5 in both workflows (closes crypto/tls GO-2026-5856,
reachable via Docker Logs).
- desktop-release: add persist-credentials:false; fix the Windows-signing `if`
(referenced step-level env, always false, so signing never ran).
Verified: go build ./... + GOOS=windows build + go vet ./... + go test -race
./... + frontend tsc && vite build all green.
---
.github/workflows/ci.yml | 4 +-
.github/workflows/desktop-release.yml | 8 ++-
app.go | 25 ++++----
app_test.go | 10 ++--
frontend/src/main.ts | 63 ++++++++++++++-------
internal/runtime/native.go | 8 ++-
internal/runtime/native_test.go | 11 +++-
internal/runtime/resource_limits.go | 52 -----------------
internal/runtime/resource_limits_linux.go | 2 +-
internal/runtime/resource_limits_test.go | 34 -----------
internal/runtime/resource_limits_windows.go | 2 +-
internal/runtime/runtime.go | 19 ++++++-
internal/runtime/runtime_test.go | 57 ++++++++++++-------
internal/store/store.go | 7 ++-
metrics.go | 7 ++-
15 files changed, 155 insertions(+), 154 deletions(-)
delete mode 100644 internal/runtime/resource_limits.go
delete mode 100644 internal/runtime/resource_limits_test.go
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 860f27b..5646192 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -22,7 +22,7 @@ jobs:
persist-credentials: false
- uses: actions/setup-go@v6
with:
- go-version: '1.26'
+ go-version: '1.26.5'
cache: true
- name: Build
run: go build ./...
@@ -53,7 +53,7 @@ jobs:
persist-credentials: false
- uses: actions/setup-go@v6
with:
- go-version: '1.26'
+ go-version: '1.26.5'
cache: true
- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest
diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml
index 3dc4ed1..74f58d6 100644
--- a/.github/workflows/desktop-release.yml
+++ b/.github/workflows/desktop-release.yml
@@ -23,10 +23,12 @@ jobs:
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v7
+ with:
+ persist-credentials: false
- uses: actions/setup-go@v6
with:
- go-version: '1.26.x'
+ go-version: '1.26.5'
cache: true
- uses: actions/setup-node@v6
@@ -58,7 +60,7 @@ jobs:
run: go run github.com/wailsapp/wails/v2/cmd/wails@v2.12.0 build -clean
- name: Sign Windows binaries
- if: runner.os == 'Windows' && env.WINDOWS_SIGN_CERT != ''
+ if: runner.os == 'Windows' && secrets.WINDOWS_SIGN_CERT != ''
env:
WINDOWS_SIGN_CERT: ${{ secrets.WINDOWS_SIGN_CERT }}
WINDOWS_SIGN_CERT_PASSWORD: ${{ secrets.WINDOWS_SIGN_CERT_PASSWORD }}
@@ -110,6 +112,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
+ with:
+ persist-credentials: false
- uses: actions/download-artifact@v8
with:
diff --git a/app.go b/app.go
index 58e6f47..9553ccf 100644
--- a/app.go
+++ b/app.go
@@ -332,16 +332,21 @@ func (a *App) GetAppState() (AppState, error) {
// can proceed (and message honestly) on Docker OR native. Available keeps its
// Docker-only meaning; this is purely additive.
runtimeStatus.NativeAvailable = a.services.HasNativeRuntime()
+ // Fetch these once and thread them into the helpers below instead of letting
+ // each helper re-run its own full-table scan (they were each queried up to 3x
+ // per GetAppState call before this).
+ deployments := a.store.ListDeployments()
+ earnings := a.store.ListLatestEarnings()
return AppState{
Config: a.cfg.Config(),
Runtime: runtimeStatus,
Services: a.catalog.ListVisible(),
- Deployments: a.store.ListDeployments(),
- Earnings: a.store.ListLatestEarnings(),
+ Deployments: deployments,
+ Earnings: earnings,
Guides: runtime.InstallGuides(),
- Notifications: a.notifications(runtimeStatus),
+ Notifications: a.notifications(runtimeStatus, earnings, deployments),
Currencies: supportedCurrencies(),
- Summary: a.computeEarningsSummary(),
+ Summary: a.computeEarningsSummary(earnings),
Health: a.store.HealthScores(7),
ServiceDetails: a.store.ListServiceDetails(),
}, nil
@@ -353,7 +358,7 @@ func (a *App) GetEarningsSummary() (EarningsSummary, error) {
if err := a.ready(); err != nil {
return EarningsSummary{}, err
}
- return a.computeEarningsSummary(), nil
+ return a.computeEarningsSummary(a.store.ListLatestEarnings()), nil
}
// computeEarningsSummary converts the per-service CUMULATIVE daily balances into
@@ -362,7 +367,7 @@ func (a *App) GetEarningsSummary() (EarningsSummary, error) {
// points (e.g. GRASS) are surfaced separately and never summed. It is
// stale-graceful: missing rates simply drop a service from the total and set
// RatesStale, rather than erroring.
-func (a *App) computeEarningsSummary() EarningsSummary {
+func (a *App) computeEarningsSummary(earnings []store.EarningsRecord) EarningsSummary {
disp := "USD"
if a.cfg != nil {
if c := a.cfg.Config().DisplayCurrency; c != "" {
@@ -537,7 +542,7 @@ func (a *App) computeEarningsSummary() EarningsSummary {
// Breakdown = every service's latest record, INCLUDING error rows so the UI
// keeps a "needs attention" chip.
- for _, rec := range a.store.ListLatestEarnings() {
+ for _, rec := range earnings {
se := ServiceEarning{
Platform: rec.Platform,
Name: rec.Platform,
@@ -1197,7 +1202,7 @@ func (a *App) ManagedRuntimePlan() runtime.ManagedRuntimePlan {
return runtime.ManagedRuntimeRoadmap()
}
-func (a *App) notifications(status runtime.Status) []Notification {
+func (a *App) notifications(status runtime.Status, earnings []store.EarningsRecord, deployments []store.Deployment) []Notification {
var items []Notification
// Only warn that the runtime is offline when NEITHER Docker nor the always-on
// native runtime can run anything. With native available the app works without
@@ -1205,12 +1210,12 @@ func (a *App) notifications(status runtime.Status) []Notification {
if !status.Available && !status.NativeAvailable {
items = append(items, Notification{Level: "warning", Title: "Runtime offline", Message: status.Message})
}
- for _, record := range a.store.ListLatestEarnings() {
+ for _, record := range earnings {
if record.Error != "" {
items = append(items, Notification{Level: "error", Title: record.Platform + " collector", Message: record.Error})
}
}
- if len(a.store.ListDeployments()) == 0 {
+ if len(deployments) == 0 {
items = append(items, Notification{Level: "info", Title: "No services deployed", Message: "Use the setup wizard or register a mobile device to start tracking earnings."})
}
return items
diff --git a/app_test.go b/app_test.go
index e8099f7..4e38c73 100644
--- a/app_test.go
+++ b/app_test.go
@@ -95,7 +95,7 @@ func TestComputeEarningsSummary(t *testing.T) {
}
app := &App{cfg: cfg, store: st, catalog: cat, exchange: svc, ctx: context.Background()}
- sum := app.computeEarningsSummary()
+ sum := app.computeEarningsSummary(app.store.ListLatestEarnings())
// Total = honeygain latest (2.00 USD) + mysterium latest (8 MYST * 0.25 = 2.00 USD).
// GRASS is excluded (non-convertible) and the error row has no successful daily
@@ -266,7 +266,7 @@ func TestEarningsSummaryFirstObservationContributesZero(t *testing.T) {
now := time.Now().UTC()
seedEarnings(t, st, store.EarningsRecord{Platform: "honeygain", Balance: 50.0, Currency: "USD", CreatedAt: atUTC(now, 10)})
- sum := app.computeEarningsSummary()
+ sum := app.computeEarningsSummary(app.store.ListLatestEarnings())
if !approxEq(sum.Total, 50.0) {
t.Fatalf("Total = %v, want 50.00 (latest cumulative still shows in Total)", sum.Total)
}
@@ -296,7 +296,7 @@ func TestEarningsSummaryCarryForwardAcrossGap(t *testing.T) {
store.EarningsRecord{Platform: "honeygain", Balance: 5.0, Currency: "USD", CreatedAt: atUTC(now, 10)},
)
- sum := app.computeEarningsSummary()
+ sum := app.computeEarningsSummary(app.store.ListLatestEarnings())
if !approxEq(sum.Today, 2.0) {
t.Fatalf("Today = %v, want 2.00 (5.00 today minus 3.00 carried across the gap)", sum.Today)
}
@@ -328,7 +328,7 @@ func TestEarningsSummaryMonthWindow(t *testing.T) {
store.EarningsRecord{Platform: "honeygain", Balance: 8.0, Currency: "USD", CreatedAt: atUTC(now, 8)},
)
- sum := app.computeEarningsSummary()
+ sum := app.computeEarningsSummary(app.store.ListLatestEarnings())
if !approxEq(sum.Month, 5.0) {
t.Fatalf("Month = %v, want 5.00 (8.00 today minus 3.00 at the month baseline)", sum.Month)
}
@@ -359,7 +359,7 @@ func TestEarningsSummaryPointsClassificationDuringOutage(t *testing.T) {
store.EarningsRecord{Platform: "mysterium", Balance: 8.0, Currency: "MYST", CreatedAt: atUTC(now, 10)},
)
- sum := app.computeEarningsSummary()
+ sum := app.computeEarningsSummary(app.store.ListLatestEarnings())
if !approxEq(sum.Total, 0) {
t.Fatalf("Total = %v, want 0 (MYST is unpriced during the outage -> excluded)", sum.Total)
}
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index c32e406..6e1800e 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -486,7 +486,7 @@ function renderCatalogCard(service: Service, deployments: Deployment[]) {
${escapeHtml(service.shortDescription || service.description)}
- ${service.manualOnly && signupUrl ? `` : ``}
+ ${service.manualOnly && signupUrl ? `` : ``}
${signupUrl ? `` : ""}
@@ -494,7 +494,13 @@ function renderCatalogCard(service: Service, deployments: Deployment[]) {
}
async function renderSettings(current: AppState) {
- const settings = await GetSettingsState();
+ let settings: SettingsState;
+ try {
+ settings = await GetSettingsState();
+ } catch (error) {
+ showErrorToast({scope: "settings", error: String(error)});
+ return;
+ }
const total = totalBalance(current);
// Background-helper state is derived live from the OS (not a stored preference):
// installed = a login agent is registered, running = the service manager reports it
@@ -674,7 +680,13 @@ function envInputName(key: string) {
}
async function renderFleet(current: AppState) {
- const fleet = await GetFleetState();
+ let fleet: FleetState;
+ try {
+ fleet = await GetFleetState();
+ } catch (error) {
+ showErrorToast({scope: "fleet", error: String(error)});
+ return;
+ }
const total = totalBalance(current);
root.innerHTML = `
${titlebar()}
@@ -744,8 +756,12 @@ async function renderFleet(current: AppState) {
});
document.querySelectorAll("[data-copy]").forEach((button) => {
button.addEventListener("click", async () => {
- await navigator.clipboard.writeText(button.dataset.copy || "");
- button.textContent = "Copied";
+ try {
+ await navigator.clipboard.writeText(button.dataset.copy || "");
+ button.textContent = "Copied";
+ } catch (error) {
+ showErrorToast({scope: "clipboard", error: String(error)});
+ }
});
});
}
@@ -785,8 +801,12 @@ async function addFleetDevice() {
async function removeFleetDevice(id: number) {
if (!confirm("Remove this fleet device from CashPilot Desktop?")) return;
- await RemoveFleetDevice(id);
- render();
+ try {
+ await RemoveFleetDevice(id);
+ render();
+ } catch (error) {
+ showErrorToast({scope: "fleet", error: String(error)});
+ }
}
function totalBalance(current: AppState) {
@@ -1011,12 +1031,12 @@ function renderServicesTable(services: Service[], deployments: Deployment[], ear
${deployment.memoryMb.toFixed(0)} MB |
-
-
+
+
${deployment.status === "running"
- ? ``
- : ``}
-
+ ? ``
+ : ``}
+
|
@@ -1188,7 +1208,7 @@ function renderWizardServices(services: Service[]) {
Choose providers to set up. Earnings vary by location, hardware, uptime, and demand, so CashPilot does not promise a fixed amount.
${services.map((service) => `
-