mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
- Redact caseSettings for non-admins and never apply a shared map on a read-only page (CASE-SYNC-001) - Send one case per save and merge it inside mutateSettingsFile, so devices cannot discard each other's cases; rapid saves keep their order (CASE-SYNC-002) - Check HTTP status and the response body, keep the local choice, report an unsynced save and reuse the forbidden-write handling (CASE-SYNC-003) - Let the server decide whether sync is on: refresh the cached switch, discard stale refreshes (CASE-SYNC-004) - Carry shared options through case rename and drop them on delete or unlink - Give the switch an accessible name; add server, client and two-browser tests Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
49 lines
1.9 KiB
TypeScript
49 lines
1.9 KiB
TypeScript
/**
|
|
* @fileoverview Pure helpers for the opt-in shared per-case run options (`caseSettings` in
|
|
* settings.json, written while `syncCaseState` is on). They run inside `mutateSettingsFile()`, so
|
|
* every change is serialised with the file's other writers: two devices saving different cases
|
|
* both land, where a replacement map would let the second discard the first.
|
|
*/
|
|
|
|
export interface CaseRunOptions {
|
|
agentTeams: boolean;
|
|
opusContext1m: boolean;
|
|
}
|
|
|
|
/** Most cases the shared map holds, counted after a merge (the schema bounds one request, not the file). */
|
|
export const MAX_SHARED_CASES = 500;
|
|
|
|
export class SharedCaseLimitError extends Error {
|
|
constructor() {
|
|
super(`Shared case options are limited to ${MAX_SHARED_CASES} cases`);
|
|
this.name = 'SharedCaseLimitError';
|
|
}
|
|
}
|
|
|
|
function asMap(value: unknown): Record<string, CaseRunOptions> {
|
|
return value && typeof value === 'object' && !Array.isArray(value) ? (value as Record<string, CaseRunOptions>) : {};
|
|
}
|
|
|
|
/** The stored map with `incoming` merged in per case. Throws SharedCaseLimitError past the limit. */
|
|
export function mergeCaseSettings(current: unknown, incoming: Record<string, CaseRunOptions>) {
|
|
const merged = { ...asMap(current), ...incoming };
|
|
if (Object.keys(merged).length > MAX_SHARED_CASES) throw new SharedCaseLimitError();
|
|
return merged;
|
|
}
|
|
|
|
/** The map with `from` moved to `to`, or null when there is nothing to move. */
|
|
export function renameCaseSettings(current: unknown, from: string, to: string) {
|
|
const map = asMap(current);
|
|
if (!Object.hasOwn(map, from)) return null;
|
|
const { [from]: moved, ...rest } = map;
|
|
return { ...rest, [to]: moved };
|
|
}
|
|
|
|
/** The map without `name`, or null when it has no entry for it. */
|
|
export function dropCaseSettings(current: unknown, name: string) {
|
|
const map = asMap(current);
|
|
if (!Object.hasOwn(map, name)) return null;
|
|
const { [name]: _gone, ...rest } = map;
|
|
return rest;
|
|
}
|