// frontend/src/modals/bellcloud/devices/ResetStatsModal.jsx // QA-to-shipping reset — clears test data accumulated on the bench (bell // fires, playback, reboots, debug logs) across both the console's own // database and the physical device itself, category by category. // // Two categories (device_boot_data, device_logs_clear) fire real MQTT // commands and need the device online; the rest are console-database-only // and always run regardless of connectivity. See backend's // POST /devices/{id}/reset-stats for the DB/Firestore side — the two // device-side commands are sent directly here via sendMqttCommand, reusing // the existing ack/toast infrastructure rather than duplicating a // wait-for-device-reply mechanism in the backend. import { useState } from 'react' import Modal from '@/components/ui/Modal' import Button from '@/components/ui/Button' import PressHoldButton from '@/components/ui/PressHoldButton' import StatusBadge from '@/components/ui/StatusBadge' import Icon from '@/components/ui/Icon' import api from '@/lib/api' import { useToast } from '@/components/ui/Toast' // ─── Category definitions ────────────────────────────────────────────────── // `key` matches ResetStatsRequest's field names on the backend for the // Postgres/Firestore categories. `firmware` categories are sent as MQTT // commands directly from this modal instead. const CATEGORIES = [ { group: 'Device (firmware)', items: [ { key: 'device_boot_data', firmware: 'telemetry.reset_boot_data', label: 'Boot count & history', description: 'Lifetime boot counter and on-device SD boot log.', defaultChecked: true }, { key: 'device_logs_clear', firmware: 'logs.clear', label: 'SD debug logs', description: 'Day-rotated debug log files stored on the device SD card.', defaultChecked: true }, ], }, { group: 'Bell & Playback (console)', items: [ { key: 'bell_stats', label: 'Bell strike stats', description: 'Lifetime hammer strikes and per-bell strike counts.', defaultChecked: true }, { key: 'playback_stats', label: 'Playback stats', description: 'Lifetime melody playback count and warning count.', defaultChecked: true }, ], }, { group: 'Console History', items: [ { key: 'device_logs', label: 'Device logs', description: 'Console-stored debug/info log history for this device.', defaultChecked: true }, { key: 'heartbeats', label: 'Heartbeats', description: 'RSSI/uptime/free-heap history — also clears RSSI & heap graphs.', defaultChecked: true }, { key: 'commands', label: 'Command history', description: 'Sent-command log shown in the Control tab.', defaultChecked: true }, { key: 'boot_events', label: 'Boot event history', description: 'Console-side boot/crash timeline (separate from the on-device log above).', defaultChecked: true }, { key: 'alert_history', label: 'Alert history', description: 'Past alert transitions (warnings/critical/failed).', defaultChecked: true }, { key: 'ping_samples', label: 'Ping samples', description: 'Latency history shown in the ping-latency graph.', defaultChecked: true }, { key: 'diagnostics_reports', label: 'Diagnostics reports', description: 'CPU temp/WiFi/OTA/stack/bell-heat history from the 5-min metrics report.', defaultChecked: true }, { key: 'reports', label: 'Device reports', description: 'History of critical board-initiated events (e.g. bell overload) from control/reports.', defaultChecked: true }, ], }, { group: 'Advanced', items: [ { key: 'current_alerts', label: 'Current active alerts', description: 'Clears LIVE alert state — if the device genuinely has an active fault right now, this hides it from the console until its next state change. Only enable if you are sure the device is idle/healthy.', defaultChecked: false, danger: true }, ], }, ] const ALL_KEYS = CATEGORIES.flatMap(g => g.items.map(i => i.key)) function defaultSelection() { const sel = {} for (const g of CATEGORIES) for (const i of g.items) sel[i.key] = i.defaultChecked return sel } // ─── Checkbox row ─────────────────────────────────────────────────────────── function CategoryRow({ item, checked, onChange, disabled }) { return ( ) } // ─── ResetStatsModal ──────────────────────────────────────────────────────── export default function ResetStatsModal({ open, onClose, deviceId, isOnline, sendMqttCommand }) { const { toast } = useToast() const [selection, setSelection] = useState(defaultSelection) const [running, setRunning] = useState(false) const [results, setResults] = useState(null) // { [key]: { ok, detail } } once run completes const toggle = (key) => setSelection(prev => ({ ...prev, [key]: !prev[key] })) const toggleAll = (value) => { const sel = {} for (const key of ALL_KEYS) sel[key] = value setSelection(sel) } const anySelected = ALL_KEYS.some(k => selection[k]) const firmwareItems = CATEGORIES[0].items const backendKeys = ALL_KEYS.filter(k => !firmwareItems.some(f => f.key === k)) const handleClose = () => { if (running) return setResults(null) onClose() } const runReset = async () => { setRunning(true) const outcome = {} // Firmware commands — only attempt if online; skip cleanly if not. for (const item of firmwareItems) { if (!selection[item.key]) continue if (!isOnline) { outcome[item.key] = { ok: false, detail: 'Device offline — skipped' } continue } try { await sendMqttCommand(item.firmware, {}) outcome[item.key] = { ok: true, detail: 'Reset on device' } } catch (err) { outcome[item.key] = { ok: false, detail: err.message || 'Command failed' } } } // Backend (Postgres + Firestore) — always attempted regardless of connectivity. const backendBody = {} for (const key of backendKeys) backendBody[key] = !!selection[key] const anyBackend = backendKeys.some(k => selection[k]) if (anyBackend) { try { const res = await api.post(`/devices/${deviceId}/reset-stats`, backendBody) for (const key of backendKeys) { if (!selection[key]) continue const val = res[key] outcome[key] = { ok: val !== null && val !== undefined, detail: typeof val === 'number' ? `${val} row(s) cleared` : (val ? 'Cleared' : 'No change') } } } catch (err) { for (const key of backendKeys) { if (selection[key]) outcome[key] = { ok: false, detail: err.message || 'Failed' } } } } setResults(outcome) setRunning(false) const failed = Object.values(outcome).filter(o => !o.ok).length if (failed === 0) { toast.success('Stats Reset', 'Selected categories were cleared successfully.') } else { toast.warning('Partially Completed', `${failed} categor${failed === 1 ? 'y' : 'ies'} could not be reset — see details below.`) } } return (
{!results && ( <>
This clears QA/bench test data before shipping to a customer. It does not affect network settings, bell/clock configuration, melodies, warranty records, or customer assignment. This action cannot be undone.
{!isOnline && (
Device is offline — console history will still be cleared, but on-device resets (boot count, SD logs) will be skipped.
)}
{CATEGORIES.map(group => (
{group.group}
{group.items.map(item => ( ))}
))}
{running ? 'Resetting…' : 'Hold to Reset Selected'}
)} {results && ( <>
{CATEGORIES.flatMap(g => g.items).filter(item => selection[item.key]).map(item => { const r = results[item.key] return (
{item.label}
{r?.detail} {r?.ok ? 'Done' : 'Skipped'}
) })}
)}
) }