mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-02 15:09:26 +00:00
cc2da99d6f
* fix(fleet): resolve stuck update states and improve update UX The fleet node update flow had several bugs: the in-memory update tracker never cleared terminal states (timeout, failed, completed), leaving nodes permanently stuck with no way to retry or dismiss. The Recheck button only re-fetched stale state without clearing it, and the POST trigger rejected retries with 409 even after timeout. Backend fixes: - Add DELETE endpoints (single node + batch) to clear tracker entries - Fix 409 race: detect expired timeouts and clear terminal states before re-triggering - Populate error messages in the tracker for timeouts and failures - Include error field in the update-status API response - Auto-expire completed entries after 60 seconds Frontend fixes: - Add retry (RotateCcw) and dismiss (X) buttons on failed/timed-out badges - Show error details via animated cursor hover (CursorFollow pattern) - Recheck button now batch-clears all terminal states before fetching - Recheck shows loading spinner and disables while checking - Extract NodeCardProps interface for readability * fix(fleet): detect update completion via process start time Remote nodes that cannot report their version (e.g. older builds) caused updates to always time out because completion detection relied solely on version comparison. The gateway now tracks the remote node's process start time from /api/meta and detects container restarts by comparing it across polls. Also extracts a createTracker() factory to eliminate repeated object construction across 5 call sites. * docs: add troubleshooting for first-update timeout on old nodes Adds a new troubleshooting entry explaining why the first remote update on nodes running pre-v0.40.0 always times out (neither version nor process start time can be detected). Documents the fix: dismiss, recheck, and confirm the node updated. Also adds a screenshot of the timed-out state with retry/dismiss buttons to the remote updates feature page. * fix(fleet): detect update completion via offline detection and error reporting The update completion detection relied on version change and process start time, both of which fail on nodes running older Sencho versions that report "unknown" and lack the startedAt field. This caused every update to time out after 5 minutes. Add three-signal detection: version change, process restart (startedAt), and offline/online detection (node went unreachable during update and came back). Also add a 90-second early failure heuristic for when the remote image pull fails silently, and surface pull errors from SelfUpdateService via /api/meta so the gateway can report them immediately. * fix(deps): bump vite to 8.0.5 to resolve high severity vulnerabilities Fixes GHSA-4w7w-66w2-5vf9, GHSA-v2wj-q39q-566r, GHSA-p9ff-h696-f583. * fix(deps): bump vite in backend lockfile to resolve audit failures Vitest pulls in vite as a transitive dependency. Bumps to 8.0.5.
101 lines
3.3 KiB
TypeScript
101 lines
3.3 KiB
TypeScript
import { execSync, exec } from 'child_process';
|
|
import DockerController from './DockerController';
|
|
import { disableCapability } from './CapabilityRegistry';
|
|
|
|
interface ComposeContext {
|
|
workingDir: string;
|
|
configFiles: string;
|
|
serviceName: string;
|
|
}
|
|
|
|
class SelfUpdateService {
|
|
private static instance: SelfUpdateService;
|
|
private canSelfUpdate = false;
|
|
private composeContext: ComposeContext | null = null;
|
|
private lastUpdateError: string | null = null;
|
|
|
|
public static getInstance(): SelfUpdateService {
|
|
if (!SelfUpdateService.instance) {
|
|
SelfUpdateService.instance = new SelfUpdateService();
|
|
}
|
|
return SelfUpdateService.instance;
|
|
}
|
|
|
|
async initialize(): Promise<void> {
|
|
const hostname = process.env.HOSTNAME;
|
|
if (!hostname) {
|
|
console.log('[SelfUpdate] HOSTNAME not set — self-update unavailable (not running in Docker?)');
|
|
disableCapability('self-update');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const docker = DockerController.getInstance().getDocker();
|
|
const container = docker.getContainer(hostname);
|
|
const info = await container.inspect();
|
|
const labels = info.Config?.Labels ?? {};
|
|
|
|
const workingDir = labels['com.docker.compose.project.working_dir'];
|
|
const configFiles = labels['com.docker.compose.project.config_files'];
|
|
const serviceName = labels['com.docker.compose.service'];
|
|
|
|
if (!workingDir || !configFiles || !serviceName) {
|
|
console.log('[SelfUpdate] Container lacks Docker Compose labels — self-update unavailable');
|
|
disableCapability('self-update');
|
|
return;
|
|
}
|
|
|
|
this.composeContext = { workingDir, configFiles, serviceName };
|
|
this.canSelfUpdate = true;
|
|
console.log(`[SelfUpdate] Ready — service="${serviceName}" in ${workingDir}`);
|
|
} catch (error) {
|
|
console.log('[SelfUpdate] Could not inspect own container — self-update unavailable:', (error as Error).message);
|
|
disableCapability('self-update');
|
|
}
|
|
}
|
|
|
|
isAvailable(): boolean {
|
|
return this.canSelfUpdate;
|
|
}
|
|
|
|
/** Returns the error message from the last failed update attempt, or null. */
|
|
getLastError(): string | null {
|
|
return this.lastUpdateError;
|
|
}
|
|
|
|
/** Clears the stored update error (call after reading it). */
|
|
clearLastError(): void {
|
|
this.lastUpdateError = null;
|
|
}
|
|
|
|
triggerUpdate(): void {
|
|
if (!this.composeContext) return;
|
|
const { workingDir, configFiles, serviceName } = this.composeContext;
|
|
const env = { ...process.env, PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' };
|
|
this.lastUpdateError = null;
|
|
|
|
console.log(`[SelfUpdate] Pulling latest image for ${serviceName}...`);
|
|
try {
|
|
execSync(`docker compose -f ${configFiles} pull ${serviceName}`, {
|
|
cwd: workingDir,
|
|
env,
|
|
stdio: 'pipe',
|
|
timeout: 300_000, // 5 min max for pull
|
|
});
|
|
} catch (error) {
|
|
this.lastUpdateError = (error as Error).message;
|
|
console.error('[SelfUpdate] Pull failed:', this.lastUpdateError);
|
|
return;
|
|
}
|
|
|
|
console.log(`[SelfUpdate] Recreating container for ${serviceName}... (last breath)`);
|
|
exec(`docker compose -f ${configFiles} up -d --force-recreate ${serviceName}`, {
|
|
cwd: workingDir,
|
|
env,
|
|
});
|
|
// Process will be killed by Docker during recreate — no code runs after this
|
|
}
|
|
}
|
|
|
|
export default SelfUpdateService;
|