mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-17 14:08:19 +00:00
809bf76c20
Extract duplicated snapshot capture functions from index.ts and SchedulerService.ts into a shared module (snapshot-capture.ts). Fix audit log route patterns that used singular 'snapshot' instead of plural 'snapshots'. Apply design system to FleetSnapshots component (card styling, strokeWidth, font-mono, tabular-nums, ScrollArea). Add safePage pagination, loading toast for creation, and fix the restore dialog race condition with a controlled AlertDialog. Add diagnostic logging gated behind Developer Mode. Add tests for restore endpoint, admin role enforcement, and edge cases. Update docs with troubleshooting section and refresh screenshots.
124 lines
4.1 KiB
TypeScript
124 lines
4.1 KiB
TypeScript
/**
|
|
* Shared snapshot capture functions used by both the REST API (index.ts)
|
|
* and the SchedulerService for fleet-wide snapshot operations.
|
|
*/
|
|
|
|
import { FileSystemService } from '../services/FileSystemService';
|
|
import { isDebugEnabled } from './debug';
|
|
|
|
export interface SnapshotNodeData {
|
|
nodeId: number;
|
|
nodeName: string;
|
|
stacks: Array<{
|
|
stackName: string;
|
|
files: Array<{ filename: string; content: string }>;
|
|
}>;
|
|
}
|
|
|
|
/** Minimal node shape accepted by capture functions. */
|
|
export interface CaptureNode {
|
|
id: number;
|
|
name: string;
|
|
api_url?: string;
|
|
api_token?: string;
|
|
}
|
|
|
|
/**
|
|
* Read compose.yaml and .env files for every stack on a local node.
|
|
* Stacks whose compose file cannot be read are silently skipped.
|
|
*/
|
|
export async function captureLocalNodeFiles(node: CaptureNode): Promise<SnapshotNodeData> {
|
|
const start = Date.now();
|
|
const fsService = FileSystemService.getInstance(node.id);
|
|
const stackNames = await fsService.getStacks();
|
|
const stacks: SnapshotNodeData['stacks'] = [];
|
|
|
|
for (const stackName of stackNames) {
|
|
const files: Array<{ filename: string; content: string }> = [];
|
|
try {
|
|
const composeContent = await fsService.getStackContent(stackName);
|
|
files.push({ filename: 'compose.yaml', content: composeContent });
|
|
} catch (e) {
|
|
console.warn(`[Fleet Snapshot] Could not read compose file for stack "${stackName}", skipping:`, (e as Error).message);
|
|
continue;
|
|
}
|
|
try {
|
|
const envContent = await fsService.getEnvContent(stackName);
|
|
files.push({ filename: '.env', content: envContent });
|
|
} catch {
|
|
// No .env file - that's fine
|
|
}
|
|
stacks.push({ stackName, files });
|
|
}
|
|
|
|
if (isDebugEnabled()) {
|
|
const fileCount = stacks.reduce((sum, s) => sum + s.files.length, 0);
|
|
console.debug(`[Fleet:debug] Local capture "${node.name}": ${stacks.length} stack(s), ${fileCount} file(s) in ${Date.now() - start}ms`);
|
|
}
|
|
|
|
return { nodeId: node.id, nodeName: node.name, stacks };
|
|
}
|
|
|
|
/**
|
|
* Fetch compose.yaml and .env files for every stack on a remote node
|
|
* via the Distributed API proxy. Stacks whose compose file cannot be
|
|
* fetched are silently skipped.
|
|
*/
|
|
export async function captureRemoteNodeFiles(node: CaptureNode): Promise<SnapshotNodeData> {
|
|
if (!node.api_url || !node.api_token) {
|
|
throw new Error('Remote node not configured');
|
|
}
|
|
|
|
const start = Date.now();
|
|
const baseUrl = node.api_url.replace(/\/$/, '');
|
|
const headers = { Authorization: `Bearer ${node.api_token}` };
|
|
|
|
const stacksRes = await fetch(`${baseUrl}/api/stacks`, {
|
|
headers,
|
|
signal: AbortSignal.timeout(15000),
|
|
});
|
|
if (!stacksRes.ok) throw new Error('Failed to fetch stacks from remote node');
|
|
const stackNames = await stacksRes.json() as string[];
|
|
|
|
const stacks: SnapshotNodeData['stacks'] = [];
|
|
|
|
for (const stackName of stackNames) {
|
|
const files: Array<{ filename: string; content: string }> = [];
|
|
try {
|
|
const composeRes = await fetch(`${baseUrl}/api/stacks/${encodeURIComponent(stackName)}`, {
|
|
headers,
|
|
signal: AbortSignal.timeout(15000),
|
|
});
|
|
if (composeRes.ok) {
|
|
const content = await composeRes.text();
|
|
files.push({ filename: 'compose.yaml', content });
|
|
}
|
|
} catch (e) {
|
|
console.warn(`[Fleet Snapshot] Failed to fetch remote compose for stack "${stackName}":`, (e as Error).message);
|
|
continue;
|
|
}
|
|
try {
|
|
const envRes = await fetch(`${baseUrl}/api/stacks/${encodeURIComponent(stackName)}/env`, {
|
|
headers,
|
|
signal: AbortSignal.timeout(15000),
|
|
});
|
|
if (envRes.ok) {
|
|
const content = await envRes.text();
|
|
files.push({ filename: '.env', content });
|
|
}
|
|
} catch {
|
|
// No .env - skip
|
|
}
|
|
if (files.length > 0) {
|
|
stacks.push({ stackName, files });
|
|
}
|
|
}
|
|
|
|
if (isDebugEnabled()) {
|
|
const fileCount = stacks.reduce((sum, s) => sum + s.files.length, 0);
|
|
console.debug(`[Fleet:debug] Remote capture "${node.name}": ${stacks.length} stack(s), ${fileCount} file(s) in ${Date.now() - start}ms`);
|
|
}
|
|
|
|
return { nodeId: node.id, nodeName: node.name, stacks };
|
|
}
|