diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index d8ce2b7..778e826 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,18 +1,24 @@ +import { useScrollToTop } from '@react-navigation/native'; import { useRouter } from 'expo-router'; -import { useEffect } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Alert, ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native'; import { CourseCardSkeleton, Skeleton, AppText as Text } from '@/src/components'; import { sampleCourse } from '@/src/data/sampleCourse'; import { useDynamicFontSize, useAnalytics } from '@/src/hooks'; -import { useAppStore } from '@/src/store'; +import { useUser } from '@/src/store'; import { AnalyticsEvent, ScreenName } from '@/src/utils/trackingEvents'; export default function HomeScreen() { const router = useRouter(); - const { isLoading, setLoading } = useAppStore(); + const [isLoading, setLoading] = useState(false); const { scale } = useDynamicFontSize(); const { trackEvent, trackScreen } = useAnalytics(); + const user = useUser(); + const scrollRef = useRef(null); + useScrollToTop(scrollRef); + + const isDashboardUser = user?.role === 'admin' || user?.role === 'instructor'; useEffect(() => { trackScreen(ScreenName.HOME); @@ -68,6 +74,7 @@ export default function HomeScreen() { return ( {'>'} + + {/* Team Dashboard — visible to admins and instructors */} + {isDashboardUser && ( + { + trackEvent(AnalyticsEvent.BUTTON_CLICK, { button: 'team_dashboard', screen: 'home' }); + router.push('../dashboard'); + }} + accessibilityRole="button" + accessibilityLabel="Team Dashboard" + accessibilityHint="View app health and performance metrics" + > + + ? + + Team Dashboard + App health & metrics + + {'>'} + + + )} ); @@ -259,6 +289,10 @@ const styles = StyleSheet.create({ shadowRadius: 2, elevation: 1, }, + dashboardButton: { + borderColor: '#a5f3fc', + backgroundColor: '#f0fdff', + }, secondaryButtonContent: { flexDirection: 'row', alignItems: 'center', diff --git a/app/dashboard.tsx b/app/dashboard.tsx new file mode 100644 index 0000000..6bdca76 --- /dev/null +++ b/app/dashboard.tsx @@ -0,0 +1,144 @@ +import { MetricsDashboard } from '@/src/components/mobile/MetricsDashboard'; +import { useDashboardMetrics } from '@/src/hooks/useDashboardMetrics'; +import { useMetricsStore } from '@/src/store/metricsStore'; +import type { DashboardRole } from '@/src/store/metricsStore'; +import { useRouter } from 'expo-router'; +import React from 'react'; +import { StyleSheet, TouchableOpacity, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { AppText as Text } from '@/src/components/common/AppText'; + +const ROLE_OPTIONS: { label: string; value: DashboardRole | null }[] = [ + { label: 'My Role', value: null }, + { label: 'Admin', value: 'admin' }, + { label: 'Instructor', value: 'instructor' }, + { label: 'Student', value: 'student' }, +]; + +export default function DashboardScreen() { + const router = useRouter(); + const metrics = useDashboardMetrics(); + const { roleOverride, setRoleOverride, autoRefreshEnabled, setAutoRefresh } = useMetricsStore(); + + return ( + + {/* Top bar */} + + router.back()} style={styles.backButton}> + {'← Back'} + + setAutoRefresh(!autoRefreshEnabled)} + style={[styles.autoRefreshToggle, autoRefreshEnabled && styles.autoRefreshActive]} + > + + {autoRefreshEnabled ? '⏸ Pause' : '▶ Resume'} + + + + + {/* Role selector — lets team members preview other views */} + + View as: + {ROLE_OPTIONS.map((opt) => { + const isActive = roleOverride === opt.value; + return ( + setRoleOverride(opt.value)} + style={[styles.roleChip, isActive && styles.roleChipActive]} + > + + {opt.label} + + + ); + })} + + + + + ); +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + backgroundColor: '#f9fafb', + }, + topBar: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 10, + backgroundColor: '#fff', + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: '#e5e7eb', + }, + backButton: { + paddingVertical: 4, + paddingRight: 12, + }, + backText: { + fontSize: 15, + color: '#2563eb', + fontWeight: '500', + }, + autoRefreshToggle: { + paddingHorizontal: 14, + paddingVertical: 6, + borderRadius: 20, + borderWidth: 1, + borderColor: '#d1d5db', + backgroundColor: '#f9fafb', + }, + autoRefreshActive: { + borderColor: '#16a34a', + backgroundColor: '#f0fdf4', + }, + autoRefreshText: { + fontSize: 12, + fontWeight: '600', + color: '#6b7280', + }, + autoRefreshTextActive: { + color: '#15803d', + }, + roleSelector: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 10, + gap: 8, + backgroundColor: '#fff', + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: '#e5e7eb', + }, + roleSelectorLabel: { + fontSize: 12, + color: '#6b7280', + fontWeight: '600', + marginRight: 2, + }, + roleChip: { + paddingHorizontal: 12, + paddingVertical: 5, + borderRadius: 16, + borderWidth: 1, + borderColor: '#e5e7eb', + backgroundColor: '#f9fafb', + }, + roleChipActive: { + borderColor: '#19c3e6', + backgroundColor: '#e0f7fb', + }, + roleChipText: { + fontSize: 12, + fontWeight: '600', + color: '#6b7280', + }, + roleChipTextActive: { + color: '#0c7a8a', + }, +}); diff --git a/src/components/mobile/MetricsDashboard.tsx b/src/components/mobile/MetricsDashboard.tsx new file mode 100644 index 0000000..b70c4bc --- /dev/null +++ b/src/components/mobile/MetricsDashboard.tsx @@ -0,0 +1,365 @@ +import React, { useCallback } from 'react'; +import { + RefreshControl, + ScrollView, + StyleSheet, + TouchableOpacity, + View, +} from 'react-native'; + +import { AppText as Text } from '../common/AppText'; +import { useMetricsStore } from '../../store/metricsStore'; +import type { DashboardMetrics, DashboardSection, MetricValue } from '../../hooks/useDashboardMetrics'; +import type { DashboardAlert } from '../../store/metricsStore'; + +// ─── Status colours ────────────────────────────────────────────────────────── + +const STATUS_COLOR: Record = { + ok: '#16a34a', + warning: '#d97706', + critical: '#dc2626', + neutral: '#6b7280', +}; + +const ALERT_BG: Record = { + critical: '#fef2f2', + warning: '#fffbeb', + info: '#eff6ff', +}; + +const ALERT_BORDER: Record = { + critical: '#fca5a5', + warning: '#fcd34d', + info: '#93c5fd', +}; + +const ALERT_TEXT: Record = { + critical: '#991b1b', + warning: '#92400e', + info: '#1e40af', +}; + +// ─── Sub-components ────────────────────────────────────────────────────────── + +function AlertBanner({ alert, onDismiss }: { alert: DashboardAlert; onDismiss: (id: string) => void }) { + return ( + + + {alert.severity === 'critical' ? '🚨 ' : alert.severity === 'warning' ? '⚠️ ' : 'ℹ️ '} + {alert.message} + + onDismiss(alert.id)} style={styles.dismissButton}> + + + + ); +} + +function MetricCard({ metric }: { metric: MetricValue }) { + const dotColor = STATUS_COLOR[metric.status] ?? STATUS_COLOR.neutral; + return ( + + + + + {metric.label} + + + {metric.value} + {metric.detail ? ( + + {metric.detail} + + ) : null} + + ); +} + +function SectionBlock({ section }: { section: DashboardSection }) { + const pairs: MetricValue[][] = []; + for (let i = 0; i < section.metrics.length; i += 2) { + pairs.push(section.metrics.slice(i, i + 2)); + } + + return ( + + {section.title} + {pairs.map((pair, i) => ( + + {pair.map((m) => ( + + ))} + {pair.length === 1 && } + + ))} + + ); +} + +// ─── Header bar ───────────────────────────────────────────────────────────── + +function formatAge(ms: number): string { + const seconds = Math.floor((Date.now() - ms) / 1000); + if (seconds < 60) return 'just now'; + const minutes = Math.floor(seconds / 60); + return `${minutes}m ago`; +} + +function HeaderBar({ + role, + lastRefreshedAt, + isAutoRefresh, +}: { + role: string; + lastRefreshedAt: number; + isAutoRefresh: boolean; +}) { + return ( + + + Team Dashboard + + Updated {formatAge(lastRefreshedAt)} + {' '} + + {isAutoRefresh ? '● Live' : '○ Paused'} + + + + + {role.toUpperCase()} + + + ); +} + +// ─── Main component ────────────────────────────────────────────────────────── + +export interface MetricsDashboardProps { + metrics: DashboardMetrics; +} + +export function MetricsDashboard({ metrics }: MetricsDashboardProps) { + const { dismissAlert } = useMetricsStore(); + const [refreshing, setRefreshing] = React.useState(false); + + const onRefresh = useCallback(() => { + setRefreshing(true); + metrics.refresh(); + // Give a brief visual beat before clearing the spinner + setTimeout(() => setRefreshing(false), 600); + }, [metrics]); + + return ( + } + > + + + {/* Alert banners */} + {metrics.alerts.length > 0 && ( + + {metrics.alerts.map((alert) => ( + + ))} + + )} + + {metrics.alerts.length === 0 && ( + + ✓ All metrics within normal range + + )} + + {/* Metric sections */} + {metrics.sections.map((section) => ( + + ))} + + + Pull to refresh • Auto-refresh every 30s + + + ); +} + +// ─── Styles ────────────────────────────────────────────────────────────────── + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f9fafb', + }, + content: { + paddingBottom: 40, + }, + + // Header + headerBar: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + paddingHorizontal: 16, + paddingTop: 20, + paddingBottom: 12, + backgroundColor: '#fff', + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: '#e5e7eb', + }, + headerTitle: { + fontSize: 20, + fontWeight: '700', + color: '#111827', + }, + headerSub: { + fontSize: 12, + color: '#6b7280', + marginTop: 2, + }, + roleBadge: { + backgroundColor: '#e0f2fe', + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 12, + alignSelf: 'flex-start', + }, + roleBadgeText: { + fontSize: 11, + fontWeight: '700', + color: '#0369a1', + letterSpacing: 0.5, + }, + + // Alerts + alertsContainer: { + paddingHorizontal: 16, + paddingTop: 12, + gap: 8, + }, + alertBanner: { + flexDirection: 'row', + alignItems: 'flex-start', + borderWidth: 1, + borderRadius: 10, + padding: 12, + gap: 8, + }, + alertText: { + flex: 1, + fontSize: 13, + fontWeight: '500', + lineHeight: 18, + }, + dismissButton: { + paddingHorizontal: 4, + }, + dismissText: { + fontSize: 14, + fontWeight: '600', + }, + + // Healthy banner + healthyBanner: { + marginHorizontal: 16, + marginTop: 12, + backgroundColor: '#f0fdf4', + borderWidth: 1, + borderColor: '#bbf7d0', + borderRadius: 10, + paddingHorizontal: 14, + paddingVertical: 10, + }, + healthyText: { + fontSize: 13, + color: '#15803d', + fontWeight: '500', + }, + + // Sections + section: { + marginTop: 20, + marginHorizontal: 16, + }, + sectionTitle: { + fontSize: 15, + fontWeight: '700', + color: '#374151', + marginBottom: 10, + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + metricRow: { + flexDirection: 'row', + gap: 10, + marginBottom: 10, + }, + + // Metric cards + metricCard: { + flex: 1, + backgroundColor: '#fff', + borderRadius: 12, + padding: 14, + borderWidth: StyleSheet.hairlineWidth, + borderColor: '#e5e7eb', + shadowColor: '#000', + shadowOffset: { width: 0, height: 1 }, + shadowOpacity: 0.04, + shadowRadius: 2, + elevation: 1, + }, + metricCardSpacer: { + flex: 1, + }, + metricHeader: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + marginBottom: 6, + }, + statusDot: { + width: 8, + height: 8, + borderRadius: 4, + }, + metricLabel: { + fontSize: 11, + fontWeight: '600', + color: '#6b7280', + textTransform: 'uppercase', + letterSpacing: 0.3, + flex: 1, + }, + metricValue: { + fontSize: 22, + fontWeight: '700', + color: '#111827', + marginBottom: 2, + }, + metricDetail: { + fontSize: 11, + color: '#9ca3af', + marginTop: 2, + }, + + // Footer + footer: { + textAlign: 'center', + fontSize: 11, + color: '#9ca3af', + marginTop: 24, + paddingHorizontal: 16, + }, +}); diff --git a/src/components/mobile/index.ts b/src/components/mobile/index.ts index 7f1629f..e350d5b 100644 --- a/src/components/mobile/index.ts +++ b/src/components/mobile/index.ts @@ -24,3 +24,4 @@ export * from './SubscriptionSkeleton'; export * from './VoiceSearch'; export * from './SwipeableRow'; export * from './SwipeableCoordinator'; +export * from './MetricsDashboard'; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index ef79712..e817355 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -30,3 +30,4 @@ export * from './useOptimizedSwipe'; export * from './useOptimizedVideoGestures'; export * from './useDebounce'; +export * from './useDashboardMetrics'; diff --git a/src/hooks/useDashboardMetrics.ts b/src/hooks/useDashboardMetrics.ts new file mode 100644 index 0000000..583652f --- /dev/null +++ b/src/hooks/useDashboardMetrics.ts @@ -0,0 +1,252 @@ +import { useCallback, useEffect, useState } from 'react'; + +import { crashReportingService } from '../services/crashReporting'; +import { useAppStore } from '../store'; +import { useCourseProgressStore } from '../store/courseProgressStore'; +import type { DashboardAlert, DashboardRole } from '../store/metricsStore'; +import { useMetricsStore } from '../store/metricsStore'; +import { useNotificationStore } from '../store/notificationStore'; +import { useQuizStore } from '../store/quizStore'; +import { useSettingsStore } from '../store/settingsStore'; + +export interface MetricValue { + label: string; + value: string | number; + status: 'ok' | 'warning' | 'critical' | 'neutral'; + icon: string; + detail?: string; +} + +export interface DashboardSection { + id: string; + title: string; + metrics: MetricValue[]; +} + +export interface DashboardMetrics { + role: DashboardRole; + lastRefreshedAt: number; + alerts: DashboardAlert[]; + sections: DashboardSection[]; + refresh: () => void; + isAutoRefreshEnabled: boolean; +} + +function resolveRole(userRole: string | undefined, override: DashboardRole | null): DashboardRole { + if (override) return override; + if (userRole === 'admin') return 'admin'; + if (userRole === 'instructor') return 'instructor'; + return 'student'; +} + +export function useDashboardMetrics(): DashboardMetrics { + const user = useAppStore((s) => s.user); + const isAuthenticated = useAppStore((s) => s.isAuthenticated); + + const progressMap = useCourseProgressStore((s) => s.progressMap); + const quizProgress = useQuizStore((s) => s.quizProgress); + + const pushToken = useNotificationStore((s) => s.pushToken); + const isTokenRegistered = useNotificationStore((s) => s.isTokenRegistered); + const unreadCount = useNotificationStore((s) => s.unreadCount); + const notifications = useNotificationStore((s) => s.notifications); + + const analyticsEnabled = useSettingsStore((s) => s.analyticsEnabled); + + const { thresholds, dismissedAlertIds, refreshIntervalMs, autoRefreshEnabled, roleOverride } = + useMetricsStore(); + + const [errorCount, setErrorCount] = useState(() => crashReportingService.getErrorCount()); + const [lastRefreshedAt, setLastRefreshedAt] = useState(Date.now); + + const refresh = useCallback(() => { + setErrorCount(crashReportingService.getErrorCount()); + setLastRefreshedAt(Date.now()); + }, []); + + useEffect(() => { + if (!autoRefreshEnabled) return; + const id = setInterval(refresh, refreshIntervalMs); + return () => clearInterval(id); + }, [autoRefreshEnabled, refresh, refreshIntervalMs]); + + // ─── Derived stats ────────────────────────────────────────────────────────── + const role = resolveRole(user?.role, roleOverride); + + const courseIds = Object.keys(progressMap); + const completedCourses = courseIds.filter( + (id) => progressMap[id].overallProgress >= 100 + ).length; + const inProgressCourses = courseIds.filter( + (id) => progressMap[id].overallProgress > 0 && progressMap[id].overallProgress < 100 + ).length; + + const completedQuizzes = Object.values(quizProgress).filter((q) => q.completed); + const avgQuizScore = + completedQuizzes.length > 0 + ? Math.round( + completedQuizzes.reduce((sum, q) => sum + (q.score ?? 0), 0) / completedQuizzes.length + ) + : 0; + + const recentNotifications = notifications.filter((n) => { + const age = Date.now() - new Date(n.receivedAt).getTime(); + return age < 24 * 60 * 60 * 1000; // last 24 h + }).length; + + // ─── Alerts ───────────────────────────────────────────────────────────────── + const rawAlerts: DashboardAlert[] = []; + + if (errorCount >= thresholds.errorCountCritical) { + rawAlerts.push({ + id: 'err_critical', + severity: 'critical', + message: `${errorCount} unhandled errors this session — investigate immediately`, + timestamp: lastRefreshedAt, + }); + } else if (errorCount >= thresholds.errorCountWarning) { + rawAlerts.push({ + id: 'err_warning', + severity: 'warning', + message: `${errorCount} errors logged this session`, + timestamp: lastRefreshedAt, + }); + } + + if (avgQuizScore > 0 && avgQuizScore < thresholds.avgQuizScoreWarning) { + rawAlerts.push({ + id: 'quiz_score_low', + severity: 'warning', + message: `Average quiz score is low (${avgQuizScore}%) — content review may be needed`, + timestamp: lastRefreshedAt, + }); + } + + if (!isAuthenticated) { + rawAlerts.push({ + id: 'auth_none', + severity: 'info', + message: 'No authenticated user — some metrics are unavailable', + timestamp: lastRefreshedAt, + }); + } + + const alerts = rawAlerts.filter((a) => !dismissedAlertIds.includes(a.id)); + + // ─── Sections ─────────────────────────────────────────────────────────────── + const appHealthSection: DashboardSection = { + id: 'app_health', + title: 'App Health', + metrics: [ + { + label: 'Errors', + value: errorCount, + status: + errorCount >= thresholds.errorCountCritical + ? 'critical' + : errorCount >= thresholds.errorCountWarning + ? 'warning' + : 'ok', + icon: errorCount > 0 ? 'exclamationmark.triangle.fill' : 'checkmark.shield.fill', + detail: `threshold: ${thresholds.errorCountCritical}`, + }, + { + label: 'Session', + value: isAuthenticated ? 'Active' : 'Guest', + status: isAuthenticated ? 'ok' : 'neutral', + icon: 'person.circle', + detail: user?.name ?? 'Not signed in', + }, + { + label: 'Analytics', + value: analyticsEnabled ? 'On' : 'Off', + status: analyticsEnabled ? 'ok' : 'warning', + icon: 'chart.bar.fill', + }, + { + label: 'Push Token', + value: isTokenRegistered ? 'Registered' : 'Pending', + status: isTokenRegistered ? 'ok' : pushToken ? 'warning' : 'neutral', + icon: 'bell.fill', + detail: isTokenRegistered ? 'Device registered' : 'Not yet registered', + }, + ], + }; + + const notificationsSection: DashboardSection = { + id: 'notifications', + title: 'Notifications', + metrics: [ + { + label: 'Unread', + value: unreadCount, + status: unreadCount > 10 ? 'warning' : 'neutral', + icon: 'envelope.badge.fill', + }, + { + label: 'Last 24h', + value: recentNotifications, + status: 'neutral', + icon: 'clock.fill', + detail: `of ${notifications.length} total`, + }, + ], + }; + + const learningSection: DashboardSection = { + id: 'learning', + title: 'Learning Metrics', + metrics: [ + { + label: 'In Progress', + value: inProgressCourses, + status: 'neutral', + icon: 'play.circle.fill', + detail: `${courseIds.length} total courses`, + }, + { + label: 'Completed', + value: completedCourses, + status: completedCourses > 0 ? 'ok' : 'neutral', + icon: 'checkmark.circle.fill', + }, + { + label: 'Quizzes Done', + value: completedQuizzes.length, + status: 'neutral', + icon: 'doc.text.fill', + }, + { + label: 'Avg Score', + value: completedQuizzes.length > 0 ? `${avgQuizScore}%` : 'N/A', + status: + avgQuizScore >= 80 + ? 'ok' + : avgQuizScore >= thresholds.avgQuizScoreWarning + ? 'neutral' + : avgQuizScore > 0 + ? 'warning' + : 'neutral', + icon: 'star.fill', + detail: `from ${completedQuizzes.length} attempt(s)`, + }, + ], + }; + + // Role gates which sections are visible + const sections: DashboardSection[] = + role === 'admin' + ? [appHealthSection, notificationsSection, learningSection] + : role === 'instructor' + ? [notificationsSection, learningSection] + : [learningSection]; + + return { + role, + lastRefreshedAt, + alerts, + sections, + refresh, + isAutoRefreshEnabled: autoRefreshEnabled, + }; +} diff --git a/src/store/index.ts b/src/store/index.ts index c3a24dd..1986563 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -136,3 +136,4 @@ export const useAppStore = create()( export * from './notificationStore'; export * from './courseProgressStore'; export * from './selectors'; +export * from './metricsStore'; diff --git a/src/store/metricsStore.ts b/src/store/metricsStore.ts new file mode 100644 index 0000000..cedd501 --- /dev/null +++ b/src/store/metricsStore.ts @@ -0,0 +1,82 @@ +import { create } from 'zustand'; +import { persist, createJSONStorage } from 'zustand/middleware'; +import AsyncStorage from '@react-native-async-storage/async-storage'; + +export type DashboardRole = 'admin' | 'instructor' | 'student'; +export type AlertSeverity = 'info' | 'warning' | 'critical'; + +export interface AlertThresholds { + errorCountWarning: number; + errorCountCritical: number; + avgQuizScoreWarning: number; +} + +export interface DashboardAlert { + id: string; + severity: AlertSeverity; + message: string; + timestamp: number; +} + +interface MetricsState { + // Configurable alert thresholds + thresholds: AlertThresholds; + // IDs of alerts the user has dismissed in this session + dismissedAlertIds: string[]; + // Polling interval in ms (default 30s) + refreshIntervalMs: number; + // Whether auto-refresh is enabled + autoRefreshEnabled: boolean; + // Role view override (null = use the authenticated user's role) + roleOverride: DashboardRole | null; + + // Actions + setThresholds: (thresholds: Partial) => void; + dismissAlert: (id: string) => void; + clearDismissedAlerts: () => void; + setRefreshInterval: (ms: number) => void; + setAutoRefresh: (enabled: boolean) => void; + setRoleOverride: (role: DashboardRole | null) => void; +} + +const DEFAULT_THRESHOLDS: AlertThresholds = { + errorCountWarning: 2, + errorCountCritical: 5, + avgQuizScoreWarning: 60, +}; + +export const useMetricsStore = create()( + persist( + (set) => ({ + thresholds: DEFAULT_THRESHOLDS, + dismissedAlertIds: [], + refreshIntervalMs: 30_000, + autoRefreshEnabled: true, + roleOverride: null, + + setThresholds: (partial) => + set((s) => ({ thresholds: { ...s.thresholds, ...partial } })), + + dismissAlert: (id) => + set((s) => ({ dismissedAlertIds: [...s.dismissedAlertIds, id] })), + + clearDismissedAlerts: () => set({ dismissedAlertIds: [] }), + + setRefreshInterval: (ms) => set({ refreshIntervalMs: ms }), + + setAutoRefresh: (enabled) => set({ autoRefreshEnabled: enabled }), + + setRoleOverride: (role) => set({ roleOverride: role }), + }), + { + name: 'metrics-dashboard-storage', + storage: createJSONStorage(() => AsyncStorage), + partialize: (state) => ({ + thresholds: state.thresholds, + refreshIntervalMs: state.refreshIntervalMs, + autoRefreshEnabled: state.autoRefreshEnabled, + roleOverride: state.roleOverride, + }), + } + ) +);