mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-07 07:59:42 +02:00
COD-131 sync tab order across devices via server state
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)
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
6f4b2b8a17
commit
8807b3ff6d
+40
-1
@@ -287,6 +287,9 @@ const _SSE_HANDLER_MAP = [
|
||||
|
||||
// Clipboard
|
||||
[SSE_EVENTS.CLIPBOARD_WRITE, '_onClipboardWrite'],
|
||||
|
||||
// Session order (global tab order sync, COD-131)
|
||||
[SSE_EVENTS.SESSION_ORDER_CHANGED, '_onSessionOrderChanged'],
|
||||
];
|
||||
|
||||
|
||||
@@ -2956,6 +2959,13 @@ class CodemanApp {
|
||||
// on another device).
|
||||
try { localStorage.removeItem('codeman-tab-meta'); } catch {}
|
||||
|
||||
// COD-131: server is authoritative for global tab order. Seed localStorage
|
||||
// from the server snapshot (if present) so syncSessionOrder() reconciles
|
||||
// against the cross-device order rather than this device's stale local copy.
|
||||
if (Array.isArray(data.sessionOrder) && data.sessionOrder.length) {
|
||||
try { localStorage.setItem('codeman-session-order', JSON.stringify(data.sessionOrder)); } catch {}
|
||||
}
|
||||
|
||||
// Sync sessionOrder with current sessions (preserve order, add new, remove stale)
|
||||
this.syncSessionOrder();
|
||||
|
||||
@@ -3520,13 +3530,42 @@ class CodemanApp {
|
||||
}
|
||||
}
|
||||
|
||||
// Save session order to localStorage
|
||||
// Save session order to localStorage and (debounced) sync to the server so it
|
||||
// follows the user across devices (COD-131). localStorage stays the offline
|
||||
// fallback; the server is authoritative and echoes back via SSE.
|
||||
saveSessionOrder() {
|
||||
try {
|
||||
localStorage.setItem('codeman-session-order', JSON.stringify(this.sessionOrder));
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
const order = [...this.sessionOrder];
|
||||
this._debouncedCall('saveSessionOrderServer', () => {
|
||||
fetch('/api/session-order', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ order }),
|
||||
}).catch(() => {});
|
||||
}, 400);
|
||||
}
|
||||
|
||||
// COD-131: another device (or our own debounced push) reordered tabs. Adopt the
|
||||
// server order as the new base and reconcile to our currently-open sessions.
|
||||
// Guard against no-op churn so an echo of our own push doesn't flicker the tabs.
|
||||
_onSessionOrderChanged(data) {
|
||||
if (!data || !Array.isArray(data.order)) return;
|
||||
try {
|
||||
localStorage.setItem('codeman-session-order', JSON.stringify(data.order));
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
const before = JSON.stringify(this.sessionOrder);
|
||||
this.syncSessionOrder();
|
||||
// Only re-render when the reconciled order actually changed (avoids flicker
|
||||
// when the broadcast is just an echo of the order we already have).
|
||||
if (JSON.stringify(this.sessionOrder) !== before) {
|
||||
this._fullRenderSessionTabs();
|
||||
}
|
||||
}
|
||||
|
||||
// Set up drag-and-drop handlers on tab elements
|
||||
|
||||
@@ -474,6 +474,9 @@ const SSE_EVENTS = {
|
||||
CASE_LINKED: 'case:linked',
|
||||
CASE_DELETED: 'case:deleted',
|
||||
CASE_ORDER_CHANGED: 'case:order-changed',
|
||||
|
||||
// Session order (global tab order sync)
|
||||
SESSION_ORDER_CHANGED: 'session:orderChanged',
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user