feat(settings): add log retention settings page

Sysadmin/admin-only settings page for configuring how long device log
history is kept before pruning. GET/PUT /api/settings/log-retention,
new LogRetentionSettings page, nav entry, and route.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 18:33:40 +03:00
co-authored by Claude Sonnet 5
parent acd1896788
commit dff1a35560
6 changed files with 171 additions and 0 deletions
+10
View File
@@ -292,6 +292,16 @@ const SETTINGS_ITEMS = [
</svg>
),
},
{
to: '/settings/log-retention',
label: 'Log Retention',
icon: (
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<circle cx="8" cy="8" r="6"/>
<path d="M8 5v3l2 1.5"/>
</svg>
),
},
{
to: '/settings/pages',
label: 'Page Settings',
@@ -0,0 +1,99 @@
// frontend/src/pages/settings/LogRetentionSettings.jsx
import { useState, useEffect } from 'react'
import api from '@/lib/api'
import { useToast } from '@/components/ui/Toast'
import PageHeader from '@/components/ui/PageHeader'
import Card from '@/components/ui/Card'
import Spinner from '@/components/ui/Spinner'
import FormField from '@/components/ui/FormField'
import Button from '@/components/ui/Button'
const PRESETS = [30, 90, 180, 365]
export default function LogRetentionSettings() {
const { toast } = useToast()
const [days, setDays] = useState(180)
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
useEffect(() => {
api.get('/settings/log-retention')
.then(data => setDays(data.days))
.catch(e => toast.danger('Failed to load settings', e.message))
.finally(() => setLoading(false))
}, [])
const handleSave = async () => {
setSaving(true)
try {
const updated = await api.put('/settings/log-retention', { days: Number(days) })
setDays(updated.days)
toast.success('Settings saved', `Device logs are now retained for ${updated.days} days.`)
} catch (e) {
toast.danger('Failed to save', e.message)
} finally {
setSaving(false)
}
}
return (
<div className="page-wrapper page-wrapper--centered" style={{ '--page-content-max-width': 'var(--content-max-width-md)' }}>
<PageHeader
title="Log Retention"
subtitle="Controls how long device MQTT logs (Device Logs, Info, Heartbeats, Commands) are kept before automatic deletion."
/>
{loading ? (
<div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--space-12)' }}>
<Spinner size="lg" />
</div>
) : (
<Card variant="flat">
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-5)' }}>
<FormField
label="Retention period (days)"
name="retention_days"
type="number"
value={days}
onChange={e => setDays(e.target.value)}
hint="Applies globally to every device — there is no per-device override."
inputProps={{ min: 1, max: 3650 }}
/>
<div style={{ display: 'flex', gap: 'var(--space-2)', flexWrap: 'wrap' }}>
{PRESETS.map(p => (
<button
key={p}
type="button"
onClick={() => setDays(p)}
style={{
height: 28,
paddingInline: 'var(--space-3)',
borderRadius: 'var(--radius-md)',
border: `1px solid ${Number(days) === p ? 'var(--color-primary)' : 'var(--color-border-strong)'}`,
background: Number(days) === p ? 'var(--color-primary-subtle)' : 'var(--color-bg-elevated)',
color: Number(days) === p ? 'var(--color-primary)' : 'var(--color-text-secondary)',
fontSize: 'var(--font-size-xs)',
fontWeight: 'var(--font-weight-semibold)',
cursor: 'pointer',
fontFamily: 'var(--font-family-base)',
}}
>
{p === 30 ? '1 month' : p === 90 ? '3 months' : p === 180 ? '6 months' : '1 year'}
</button>
))}
</div>
<div>
<Button variant="primary" loading={saving} onClick={handleSave}>
Save
</Button>
</div>
</div>
</Card>
)}
</div>
)
}
+2
View File
@@ -32,6 +32,7 @@ import StaffDetail from '@/pages/settings/staff/StaffDetail'
import StaffForm from '@/pages/settings/staff/StaffForm'
import PublicFeaturesSettings from '@/pages/settings/PublicFeaturesSettings'
import LogViewerPage from '@/pages/settings/LogViewerPage'
import LogRetentionSettings from '@/pages/settings/LogRetentionSettings'
import AutomationsPage from '@/pages/settings/automations/AutomationsPage'
import ApiReferencePage from '@/pages/engineering/developer/ApiReferencePage'
import CustomerList from '@/pages/crm/customers/CustomerList'
@@ -196,6 +197,7 @@ export default function V2Router() {
<Route path="settings/public-features" element={<RoleGate roles={['sysadmin', 'admin']}><PublicFeaturesSettings /></RoleGate>} />
<Route path="settings/automations" element={<RoleGate roles={['sysadmin', 'admin']}><AutomationsPage /></RoleGate>} />
<Route path="settings/audit-log" element={<RoleGate roles={['sysadmin']}><LogViewerPage /></RoleGate>} />
<Route path="settings/log-retention" element={<RoleGate roles={['sysadmin', 'admin']}><LogRetentionSettings /></RoleGate>} />
{/* Catch-all */}
<Route path="*" element={<Navigate to="/" replace />} />