mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
Tab reordering (drag-and-drop + Ctrl+Shift+{/}) persisted only to
localStorage (codeman-session-order), so each device kept its own private
order. Add server-side persistence so the order follows the user across
devices, live. Takes the issue's recommended default (a): one global order,
server authoritative, localStorage as offline fallback.
- session-order.ts (new, pure + unit-tested): normalizeSessionOrder (coerce
to string[], drop empty/non-string, dedup) and mergeSessionOrder (the
pushing device's order wins; ids the device hadn't loaded fall to the end
in their existing relative order, never dropped — graceful for
closed/remote/parked sessions absent on that device).
- AppState.sessionOrder?: string[]; StateStore get/setSessionOrder + the field
added to buildPartialJson() (the incremental serializer whitelists fields,
so without this the value never reached disk / survived a restart).
- PUT /api/session-order (session-routes): parse -> merge -> persist ->
broadcast session:orderChanged; getLightState() init snapshot now carries
sessionOrder so a fresh load/reconnect restores it.
- SSE event session:orderChanged registered in sse-events.ts + constants.js.
- app.js: handleInit seeds localStorage from the server snapshot before
syncSessionOrder(); saveSessionOrder() also PUTs to the server (debounced
400ms, covers drag + both keyboard moves); _onSessionOrderChanged adopts a
remote order and re-renders (no-op-guarded to avoid echo flicker).
Verified (orchestrator re-ran all gates): tsc 0, lint 0, frontend-syntax +
prettier clean, build ok; session-order + session-order-routes + state-store
56/56. Functional round-trip on an isolated beta: PUT {a,b,c} -> status
snapshot reflects it; merge PUT {c,a} vs {a,b,c} -> {c,a,b} (b preserved at
end); malformed payload rejected with a clean 400; sessionOrder persisted to
state.json and survived a restart.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 79415f2fdfbdf3fbe362a063534e7f84c553eefb)
69 lines
2.5 KiB
TypeScript
69 lines
2.5 KiB
TypeScript
/**
|
|
* @fileoverview Pure helpers for the global session tab-order (COD-131).
|
|
*
|
|
* Tab order (drag-and-drop reorder + Ctrl+Shift+{/}) is persisted server-side
|
|
* so it follows the user across devices. The server is authoritative; the
|
|
* browser's localStorage (`codeman-session-order`) is the offline fallback.
|
|
*
|
|
* These helpers are pure (no IO) so they can be unit-tested in isolation and
|
|
* reused by both the PUT /api/session-order route and the StateStore accessor.
|
|
*
|
|
* - `normalizeSessionOrder` coerces arbitrary input into a clean string[]
|
|
* (non-empty strings only, deduped with first occurrence winning).
|
|
* - `mergeSessionOrder` lets the pushing device's order win, while preserving
|
|
* any server-only ids the pushing device didn't know about — they fall to the
|
|
* END in their existing relative order, never dropped.
|
|
*/
|
|
|
|
/**
|
|
* Coerce arbitrary input into a clean ordered list of session ids:
|
|
* keep only non-empty strings and dedup (first occurrence wins).
|
|
*
|
|
* @param order - unknown input (expected to be a string[], but defensive)
|
|
* @returns a normalized string[] (empty array for non-array / all-junk input)
|
|
*/
|
|
export function normalizeSessionOrder(order: unknown): string[] {
|
|
if (!Array.isArray(order)) {
|
|
return [];
|
|
}
|
|
const seen = new Set<string>();
|
|
const result: string[] = [];
|
|
for (const entry of order) {
|
|
if (typeof entry !== 'string' || entry.length === 0) {
|
|
continue;
|
|
}
|
|
if (seen.has(entry)) {
|
|
continue;
|
|
}
|
|
seen.add(entry);
|
|
result.push(entry);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Merge an incoming order from a pushing device with the existing server order.
|
|
*
|
|
* The incoming order wins; any ids present in `existing` but NOT in `incoming`
|
|
* are appended at the END, preserving their relative order. This is the
|
|
* "server-only ids the pushing device didn't know about fall to the end, never
|
|
* dropped" rule.
|
|
*
|
|
* Both arguments are normalized first, so callers may pass raw input safely.
|
|
*
|
|
* @param incoming - the order the pushing device wants
|
|
* @param existing - the current server-side order
|
|
* @returns the merged, normalized order
|
|
*/
|
|
export function mergeSessionOrder(incoming: string[], existing: string[]): string[] {
|
|
const normalizedIncoming = normalizeSessionOrder(incoming);
|
|
const incomingSet = new Set(normalizedIncoming);
|
|
const merged = [...normalizedIncoming];
|
|
for (const id of normalizeSessionOrder(existing)) {
|
|
if (!incomingSet.has(id)) {
|
|
merged.push(id);
|
|
}
|
|
}
|
|
return merged;
|
|
}
|