diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 48a6f0fd..c8d0214c 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -163,6 +163,18 @@ const routes: RouteRecordRaw[] = [ descriptionKey: 'admin.dashboard.description' } }, + { + path: '/admin/ops', + name: 'AdminOps', + component: () => import('@/views/admin/ops/OpsDashboard.vue'), + meta: { + requiresAuth: true, + requiresAdmin: true, + title: 'Ops Monitoring', + titleKey: 'admin.ops.title', + descriptionKey: 'admin.ops.description' + } + }, { path: '/admin/users', name: 'AdminUsers', diff --git a/frontend/src/stores/adminSettings.ts b/frontend/src/stores/adminSettings.ts new file mode 100644 index 00000000..460cc92b --- /dev/null +++ b/frontend/src/stores/adminSettings.ts @@ -0,0 +1,130 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { adminAPI } from '@/api' + +export const useAdminSettingsStore = defineStore('adminSettings', () => { + const loaded = ref(false) + const loading = ref(false) + + const readCachedBool = (key: string, defaultValue: boolean): boolean => { + try { + const raw = localStorage.getItem(key) + if (raw === 'true') return true + if (raw === 'false') return false + } catch { + // ignore localStorage failures + } + return defaultValue + } + + const writeCachedBool = (key: string, value: boolean) => { + try { + localStorage.setItem(key, value ? 'true' : 'false') + } catch { + // ignore localStorage failures + } + } + + const readCachedString = (key: string, defaultValue: string): string => { + try { + const raw = localStorage.getItem(key) + if (typeof raw === 'string' && raw.length > 0) return raw + } catch { + // ignore localStorage failures + } + return defaultValue + } + + const writeCachedString = (key: string, value: string) => { + try { + localStorage.setItem(key, value) + } catch { + // ignore localStorage failures + } + } + + // Default open, but honor cached value to reduce UI flicker on first paint. + const opsMonitoringEnabled = ref(readCachedBool('ops_monitoring_enabled_cached', true)) + const opsRealtimeMonitoringEnabled = ref(readCachedBool('ops_realtime_monitoring_enabled_cached', true)) + const opsQueryModeDefault = ref(readCachedString('ops_query_mode_default_cached', 'auto')) + + async function fetch(force = false): Promise { + if (loaded.value && !force) return + if (loading.value) return + + loading.value = true + try { + const settings = await adminAPI.settings.getSettings() + opsMonitoringEnabled.value = settings.ops_monitoring_enabled ?? true + writeCachedBool('ops_monitoring_enabled_cached', opsMonitoringEnabled.value) + + opsRealtimeMonitoringEnabled.value = settings.ops_realtime_monitoring_enabled ?? true + writeCachedBool('ops_realtime_monitoring_enabled_cached', opsRealtimeMonitoringEnabled.value) + + opsQueryModeDefault.value = settings.ops_query_mode_default || 'auto' + writeCachedString('ops_query_mode_default_cached', opsQueryModeDefault.value) + + loaded.value = true + } catch (err) { + // Keep cached/default value: do not "flip" the UI based on a transient fetch failure. + loaded.value = true + console.error('[adminSettings] Failed to fetch settings:', err) + } finally { + loading.value = false + } + } + + function setOpsMonitoringEnabledLocal(value: boolean) { + opsMonitoringEnabled.value = value + writeCachedBool('ops_monitoring_enabled_cached', value) + loaded.value = true + } + + function setOpsRealtimeMonitoringEnabledLocal(value: boolean) { + opsRealtimeMonitoringEnabled.value = value + writeCachedBool('ops_realtime_monitoring_enabled_cached', value) + loaded.value = true + } + + function setOpsQueryModeDefaultLocal(value: string) { + opsQueryModeDefault.value = value || 'auto' + writeCachedString('ops_query_mode_default_cached', opsQueryModeDefault.value) + loaded.value = true + } + + // Keep UI consistent if we learn that ops is disabled via feature-gated 404s. + // (event is dispatched from the axios interceptor) + let eventHandlerCleanup: (() => void) | null = null + + function initializeEventListeners() { + if (eventHandlerCleanup) return + + try { + const handler = () => { + setOpsMonitoringEnabledLocal(false) + } + window.addEventListener('ops-monitoring-disabled', handler) + eventHandlerCleanup = () => { + window.removeEventListener('ops-monitoring-disabled', handler) + } + } catch { + // ignore window access failures (SSR) + } + } + + if (typeof window !== 'undefined') { + initializeEventListeners() + } + + return { + loaded, + loading, + opsMonitoringEnabled, + opsRealtimeMonitoringEnabled, + opsQueryModeDefault, + fetch, + setOpsMonitoringEnabledLocal, + setOpsRealtimeMonitoringEnabledLocal, + setOpsQueryModeDefaultLocal + } +}) diff --git a/frontend/src/stores/index.ts b/frontend/src/stores/index.ts index 0e4caef0..05c18e7e 100644 --- a/frontend/src/stores/index.ts +++ b/frontend/src/stores/index.ts @@ -5,6 +5,7 @@ export { useAuthStore } from './auth' export { useAppStore } from './app' +export { useAdminSettingsStore } from './adminSettings' export { useSubscriptionStore } from './subscriptions' export { useOnboardingStore } from './onboarding'