Files
electric-cherry/src/lib/config-workbench.ts
T
josh 8ac420fe61 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.
2026-09-19 21:34:34 -04:00

78 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export interface ConfigSource {
id: number | null;
name: string;
version: number | null;
kind: 'baseline' | 'config' | 'map';
draft?: boolean;
}
export interface ConfigAssignment {
value: string;
source: ConfigSource;
line: number;
}
export interface ConfigSetting {
key: string;
value: string;
source: ConfigSource;
history: ConfigAssignment[];
}
export interface ConfigInspection {
text: string;
settings: ConfigSetting[];
sources: ConfigSource[];
warnings: string[];
reason?: string;
}
export interface ConfigServer { name: string; game: string | null; map: string | null; }
/** The same lowercase `set` grammar that the overlay composer accepts. */
export function configLines(text: string) {
return text.split(/\r?\n/).flatMap((line, index) => {
const match = /^(\s*set\s+)(\S+)(\s+)(.*)$/.exec(line);
return match ? [{ index, key: match[2], value: match[4], prefix: match[1] + match[2] + match[3] }] : [];
});
}
export function changeConfigValue(text: string, index: number, value: string): string {
// Keep every separator, comment and unrelated line byte-for-byte intact.
const parts = text.split(/(\r?\n)/);
const entry = configLines(text).find(line => line.index === index);
if (entry) parts[index * 2] = entry.prefix + value.replace(/[\r\n]/g, '');
return parts.join('');
}
/** Add one assignment without changing existing commands or line endings. */
export function addConfigSetting(text: string, key: string, value: string): string {
key = key.trim(); value = value.trim();
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) throw new Error('Use a setting name containing letters, numbers, and underscores.');
if (!value || /[\r\n]/.test(value) || value.startsWith('//')) throw new Error('Enter a value on one line.');
if (configLines(text).some(entry => entry.key.toLowerCase() === key.toLowerCase())) throw new Error('This setting already exists. Find it above to edit its value.');
const newline = text.includes('\r\n') ? '\r\n' : '\n';
const formatted = /\s/.test(value) && !value.startsWith('"') ? JSON.stringify(value) : value;
return text + (text && !text.endsWith('\n') ? newline : '') + `set ${key} ${formatted}` + newline;
}
export function configWarnings(text: string, overlay: boolean): string[] {
const warnings: string[] = [];
const seen = new Map<string, number>();
for (const entry of configLines(text)) {
const key = entry.key.toLowerCase();
if (seen.has(key)) warnings.push(`Line ${entry.index + 1}: ${entry.key} is also assigned on line ${seen.get(key)}.`);
seen.set(key, entry.index + 1);
if (!entry.value.trim() || entry.value.trim().startsWith('//')) warnings.push(`Line ${entry.index + 1}: ${entry.key} has no value.`);
}
if (overlay) text.split(/\r?\n/).forEach((line, index) => {
if (line.trim() && !/^\s*\/\//.test(line) && !/^\s*set\s+\S+\s+/.test(line)) {
warnings.push(`Line ${index + 1} is ignored by overlays. Use set KEY VALUE.`);
}
});
if (/^\s*(?:(?:set|seta)\s+)?sv_maprotation\s+/im.test(text)) {
warnings.push('For recognized maps, the server’s selected map overrides sv_maprotation.');
}
return warnings;
}