Files
roboco/panel/src/store/ui-store.ts
T
acb4d567d2 fix(panel): settings preferences become real client prefs — no more 422 save, no more theater toggles (#487)
The Settings page PUT four keys (notifications_enabled, sound_enabled,
auto_refresh, refresh_interval) the backend's settings allowlist never
accepted — Save died on the first 422 and had never persisted these
cards. Worse, nothing consumed the prefs anywhere: no auto-refresh timer,
no notification toast, no sound system existed. Pure theater.

- the four prefs move into the persisted UI store (client-only, same
  idiom as theme/sidebar) and the cards apply instantly; the dead server
  plumbing and the global Save button are gone — the backend allowlist
  stays strict and untouched
- AutoRefreshDriver (new): when Auto Refresh is on, ticks the page-refresh
  registry every N seconds — skips while nothing is registered or a
  refresh is in flight; default-off so no background poller starts unasked
- NotificationAlerts (new): toasts each newly-arrived WS notification
  (subject + priority) when notifications are enabled, with an optional
  ~120ms Web-Audio chime — initial backlog on connect never toasts, one
  chime per batch, autoplay blocks never throw
- tests: settings page rewritten store-driven; fake-timer coverage for
  the driver; stream/store/sonner/AudioContext-mocked coverage for alerts

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-12 00:43:43 +02:00

82 lines
2.7 KiB
TypeScript

import { create } from "zustand";
import { persist } from "zustand/middleware";
import type { Team } from "@/types";
interface UIState {
// Sidebar
sidebarOpen: boolean;
sidebarCollapsed: boolean;
// Theme
theme: "light" | "dark" | "system";
// Current context
currentTeam: Team | null;
// A2A live view: xl:+ context pane collapse (conversation-first layout
// design doc §1) — same persisted-preference idiom as sidebar/theme.
a2aContextOpen: boolean;
// Client-only Settings-page prefs (never sent to the backend — the
// server's settings allowlist is transcript_retention_days + feature
// flags only). Same persisted-preference idiom as sidebar/theme.
notificationsEnabled: boolean;
soundEnabled: boolean;
autoRefresh: boolean;
refreshIntervalSeconds: number;
// Actions
toggleSidebar: () => void;
setSidebarCollapsed: (collapsed: boolean) => void;
setTheme: (theme: "light" | "dark" | "system") => void;
setCurrentTeam: (team: Team | null) => void;
toggleA2AContext: () => void;
setNotificationsEnabled: (enabled: boolean) => void;
setSoundEnabled: (enabled: boolean) => void;
setAutoRefresh: (enabled: boolean) => void;
setRefreshIntervalSeconds: (seconds: number) => void;
}
export const useUIStore = create<UIState>()(
persist(
(set) => ({
sidebarOpen: true,
sidebarCollapsed: false,
theme: "system",
currentTeam: null,
a2aContextOpen: true,
notificationsEnabled: true,
soundEnabled: true,
autoRefresh: false, // default-off: never start a background poller unasked
refreshIntervalSeconds: 30,
toggleSidebar: () =>
set((state) => ({ sidebarOpen: !state.sidebarOpen })),
setSidebarCollapsed: (collapsed) => set({ sidebarCollapsed: collapsed }),
setTheme: (theme) => set({ theme }),
setCurrentTeam: (team) => set({ currentTeam: team }),
toggleA2AContext: () =>
set((state) => ({ a2aContextOpen: !state.a2aContextOpen })),
setNotificationsEnabled: (enabled) =>
set({ notificationsEnabled: enabled }),
setSoundEnabled: (enabled) => set({ soundEnabled: enabled }),
setAutoRefresh: (enabled) => set({ autoRefresh: enabled }),
setRefreshIntervalSeconds: (seconds) =>
set({ refreshIntervalSeconds: seconds }),
}),
{
name: "roboco-ui-storage",
partialize: (state) => ({
sidebarCollapsed: state.sidebarCollapsed,
theme: state.theme,
currentTeam: state.currentTeam,
a2aContextOpen: state.a2aContextOpen,
notificationsEnabled: state.notificationsEnabled,
soundEnabled: state.soundEnabled,
autoRefresh: state.autoRefresh,
refreshIntervalSeconds: state.refreshIntervalSeconds,
}),
},
),
);