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.
28 lines
1.7 KiB
TypeScript
28 lines
1.7 KiB
TypeScript
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)) };
|
|
}
|