feat: rebuild warm operations workbench

This commit is contained in:
Codex
2026-07-30 14:49:33 +08:00
parent 25c853dea8
commit 570edf6bb2
88 changed files with 13413 additions and 2250 deletions
@@ -0,0 +1,91 @@
import type { ScheduledOperationType, ScheduleTargetSelector } from '@multi-simadmin/contracts';
import type { SqliteDatabase } from '../../infrastructure/database/database.js';
export interface ResolvedTarget {
readonly id: string;
readonly revision: number;
}
interface TargetRow {
id: string;
config_revision: number;
}
export interface OperationTargetResolution {
readonly targets: readonly ResolvedTarget[];
readonly targetSnapshot: readonly string[];
readonly unavailableInstanceIds: readonly string[];
}
const operationIds: Record<ScheduledOperationType, string> = {
'restart-service': 'postServiceRestart',
'reboot-system': 'postSystemReboot',
'send-sms': 'postSmsSend',
};
export function resolveTargets(
db: SqliteDatabase,
selector: ScheduleTargetSelector,
): readonly ResolvedTarget[] {
if (selector.mode === 'fixed') {
const placeholders = selector.instanceIds.map(() => '?').join(',');
const rows = db
.prepare(
`SELECT id, config_revision FROM instances
WHERE enabled = 1 AND id IN (${placeholders})`,
)
.all(...selector.instanceIds) as TargetRow[];
const byId = new Map(rows.map((row) => [row.id, row]));
return selector.instanceIds.flatMap((id) => {
const row = byId.get(id);
return row ? [{ id: row.id, revision: row.config_revision }] : [];
});
}
const placeholders = selector.tags.map(() => '?').join(',');
const comparison = selector.match === 'all' ? '= ?' : '> 0';
const parameters: Array<string | number> = [...selector.tags];
if (selector.match === 'all') parameters.push(selector.tags.length);
const rows = db
.prepare(
`SELECT i.id, i.config_revision
FROM instances i
JOIN instance_tags t ON t.instance_id = i.id
WHERE i.enabled = 1 AND t.tag IN (${placeholders})
GROUP BY i.id, i.config_revision
HAVING COUNT(DISTINCT t.tag) ${comparison}
ORDER BY i.id ASC`,
)
.all(...parameters) as TargetRow[];
return rows.map((row) => ({ id: row.id, revision: row.config_revision }));
}
export function resolveOperationTargets(
db: SqliteDatabase,
selector: ScheduleTargetSelector,
operationType: ScheduledOperationType,
): OperationTargetResolution {
const candidates = resolveTargets(db, selector);
if (candidates.length === 0)
return { targets: [], targetSnapshot: [], unavailableInstanceIds: [] };
const placeholders = candidates.map(() => '?').join(',');
const rows = db
.prepare(
`SELECT instance_id, state FROM capabilities
WHERE operation_id = ? AND instance_id IN (${placeholders})`,
)
.all(operationIds[operationType], ...candidates.map((target) => target.id)) as Array<{
instance_id: string;
state: string;
}>;
const states = new Map(rows.map((row) => [row.instance_id, row.state]));
const availableStates = new Set(['supported', 'auth-required', 'degraded']);
return {
targets: candidates.filter((target) => availableStates.has(states.get(target.id) ?? 'unknown')),
targetSnapshot: candidates.map((target) => target.id),
unavailableInstanceIds: candidates
.filter((target) => !availableStates.has(states.get(target.id) ?? 'unknown'))
.map((target) => target.id),
};
}