feat(sync-ui): sync window renderer, snapshots in /tmp/subminer-db-snapshots, docs + changelog

This commit is contained in:
2026-07-11 18:28:21 -07:00
parent 97aaf44b3c
commit 187437b681
22 changed files with 1657 additions and 88 deletions
+5 -1
View File
@@ -33,7 +33,11 @@ test('tick triggers a due auto-sync host with its saved direction', () => {
test('tick skips hosts synced more recently than the interval', () => {
let state = makeState();
state = recordSyncResult(state, 'auto-box', { atMs: 90 * 60_000, status: 'success', detail: null });
state = recordSyncResult(state, 'auto-box', {
atMs: 90 * 60_000,
status: 'success',
detail: null,
});
const triggered: string[] = [];
const scheduler = createSyncAutoScheduler({
readState: () => state,
@@ -119,3 +119,29 @@ test('runSyncLauncher surfaces spawn errors', async () => {
assert.equal(result.ok, false);
assert.match(result.error ?? '', /ENOENT/);
});
test('resolveSyncLauncherCommand prefers the bundled launcher over PATH', async () => {
const { resolveSyncLauncherCommand } = await import('./sync-launcher-client');
const bundled = resolveSyncLauncherCommand({
findCommand: (name: string) =>
name === 'bun' ? '/usr/bin/bun' : name === 'subminer' ? '/home/u/.local/bin/subminer' : null,
resolveResourcePath: () => '/opt/app/resources/launcher/subminer',
existsSync: () => true,
});
assert.deepEqual(bundled.command, ['/usr/bin/bun', '/opt/app/resources/launcher/subminer']);
const fallback = resolveSyncLauncherCommand({
findCommand: (name: string) => (name === 'subminer' ? '/home/u/.local/bin/subminer' : null),
resolveResourcePath: () => '/missing',
existsSync: () => false,
});
assert.deepEqual(fallback.command, ['/home/u/.local/bin/subminer']);
const none = resolveSyncLauncherCommand({
findCommand: () => null,
resolveResourcePath: () => '/missing',
existsSync: () => false,
});
assert.equal(none.command, null);
assert.match(none.error ?? '', /launcher/i);
});
+11 -6
View File
@@ -1,4 +1,5 @@
import { spawn as nodeSpawn } from 'node:child_process';
import fs from 'node:fs';
import { parseSyncProgressLine, type SyncProgressEvent } from '../../shared/sync/sync-events';
import { findCommand } from './command-line-launcher-deps';
import { resolveLauncherResourcePath } from './command-line-launcher';
@@ -28,8 +29,9 @@ export interface SyncLauncherResolution {
error: string | null;
}
// The launcher is a bun script: prefer the PATH-installed `subminer`, fall
// back to running the bundled resource through bun directly.
// The launcher is a bun script. Prefer the bundled launcher resource (it
// always matches this app version's sync protocol) and only fall back to a
// PATH-installed `subminer`, which may be older.
export function resolveSyncLauncherCommand(
deps: {
findCommand?: typeof findCommand;
@@ -38,14 +40,17 @@ export function resolveSyncLauncherCommand(
} = {},
): SyncLauncherResolution {
const find = deps.findCommand ?? findCommand;
const installed = find('subminer', {});
if (installed) return { command: [installed], error: null };
const exists = deps.existsSync ?? fs.existsSync;
const resourcePath = deps.resolveResourcePath
? deps.resolveResourcePath()
: resolveLauncherResourcePath({});
const bunPath = find('bun', {});
if (bunPath && resourcePath) return { command: [bunPath, resourcePath], error: null };
if (bunPath && resourcePath && exists(resourcePath)) {
return { command: [bunPath, resourcePath], error: null };
}
const installed = find('subminer', {});
if (installed) return { command: [installed], error: null };
return {
command: null,
+1 -7
View File
@@ -138,13 +138,7 @@ test('run-sync spawns the launcher, forwards progress, and rejects concurrent ru
force: true,
})) as { started: boolean; runId: number };
assert.equal(start.started, true);
assert.deepEqual(launcherCalls[0]!.args, [
'sync',
'media-box',
'--push',
'--force',
'--json',
]);
assert.deepEqual(launcherCalls[0]!.args, ['sync', 'media-box', '--push', '--force', '--json']);
const second = (await invoke('sync-ui:run-sync', { host: 'other' })) as {
started: boolean;
+2 -6
View File
@@ -213,9 +213,7 @@ export function createSyncUiRuntime(deps: SyncUiRuntimeDeps) {
if (result.started) {
// Remember the host (and the direction used) before the launcher's own
// bookkeeping lands, so it shows up in the UI immediately.
writeState(
upsertSyncHost(readState(), { host, direction: request.direction }, deps.nowMs()),
);
writeState(upsertSyncHost(readState(), { host, direction: request.direction }, deps.nowMs()));
}
return result;
}
@@ -298,9 +296,7 @@ export function createSyncUiRuntime(deps: SyncUiRuntimeDeps) {
const channels = IPC_CHANNELS.request;
deps.ipcMain.handle(channels.syncUiGetSnapshot, () => getSnapshot());
deps.ipcMain.handle(channels.syncUiSaveHost, (_event, update) =>
writeState(
upsertSyncHost(readState(), update as SyncUiHostUpdateRequest, deps.nowMs()),
),
writeState(upsertSyncHost(readState(), update as SyncUiHostUpdateRequest, deps.nowMs())),
);
deps.ipcMain.handle(channels.syncUiRemoveHost, (_event, host) =>
writeState(removeSyncHost(readState(), String(host))),