Improve operational UX, recovery flows, and action safety

Implement the approved UX review across Fleet, Betas, Library, deployments, and Settings. Add reviewed promotion and reconciliation plans, atomic queued-removal cancellation, recoverable beta drafts and launch inputs, accessible controls, and mobile improvements.

Validated with 473 tests, a clean typecheck, production build, and isolated desktop/mobile browser walkthroughs. Include the review and implementation coverage notes.
This commit is contained in:
2026-09-19 21:34:34 -04:00
parent 940f81e6ca
commit 8ac420fe61
83 changed files with 2574 additions and 1190 deletions
+27
View File
@@ -0,0 +1,27 @@
import { requirementKinds, type BetaPlan } from './beta-plan.js';
export const betaDraftKey = 'electric-cherry:beta-draft:v1';
export interface BetaDraft {
version: 1; savedAt: string; step: number;
picks: Array<{ scriptId: number | null; versionId: number | null }>;
servers: string[]; testPlan: BetaPlan;
label?: string;
}
/** Validate draft structure, preserving incomplete numeric fields for correction. */
export function readBetaDraft(raw: string | null): BetaDraft | null {
if (!raw) return null;
try {
const d = JSON.parse(raw);
if (d?.version !== 1 || !Array.isArray(d.picks) || !d.picks.length || d.picks.length > 100
|| !d.picks.every((p: BetaDraft['picks'][number]) => p && [p.scriptId, p.versionId].every(v => v === null || Number.isSafeInteger(v) && v! > 0))
|| !Array.isArray(d.servers) || !d.servers.every((s: unknown) => typeof s === 'string')
|| d.testPlan?.version !== 1 || !Array.isArray(d.testPlan.requirements) || d.testPlan.requirements.length > 30
|| !d.testPlan.requirements.every((r: BetaPlan['requirements'][number]) => r && requirementKinds.includes(r.kind) && typeof r.id === 'string' && /^[a-zA-Z0-9_-]{1,64}$/.test(r.id) && typeof r.label === 'string')) return null;
return { ...d, step: Math.max(0, Math.min(3, Number(d.step) || 0)) };
} catch { return null; }
}
export function compatibleBetaServers(selected: Iterable<string>, candidates: readonly string[]): { kept: Set<string>; removed: string[] } {
const allowed = new Set(candidates), names = [...selected];
return { kept: new Set(names.filter(name => allowed.has(name))), removed: names.filter(name => !allowed.has(name)) };
}