From dff1a355608e61038f59bc586aad614e557df904 Mon Sep 17 00:00:00 2001 From: bonamin Date: Mon, 21 Sep 2026 18:33:40 +0300 Subject: [PATCH] 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 --- backend/settings/log_retention_models.py | 10 ++ backend/settings/log_retention_service.py | 31 ++++++ backend/settings/router.py | 19 ++++ frontend/src/components/layout/Header.jsx | 10 ++ .../pages/settings/LogRetentionSettings.jsx | 99 +++++++++++++++++++ frontend/src/router/index.jsx | 2 + 6 files changed, 171 insertions(+) create mode 100644 backend/settings/log_retention_models.py create mode 100644 backend/settings/log_retention_service.py create mode 100644 frontend/src/pages/settings/LogRetentionSettings.jsx diff --git a/backend/settings/log_retention_models.py b/backend/settings/log_retention_models.py new file mode 100644 index 0000000..5861a90 --- /dev/null +++ b/backend/settings/log_retention_models.py @@ -0,0 +1,10 @@ +from pydantic import BaseModel +from typing import Optional + + +class LogRetentionSettings(BaseModel): + days: int = 180 + + +class LogRetentionSettingsUpdate(BaseModel): + days: Optional[int] = None diff --git a/backend/settings/log_retention_service.py b/backend/settings/log_retention_service.py new file mode 100644 index 0000000..94cc4a4 --- /dev/null +++ b/backend/settings/log_retention_service.py @@ -0,0 +1,31 @@ +from shared.firebase import get_db +from settings.log_retention_models import LogRetentionSettings, LogRetentionSettingsUpdate + +COLLECTION = "admin_settings" +DOC_ID = "log_retention" + + +def get_log_retention() -> LogRetentionSettings: + """Get log retention settings from Firestore. Creates defaults if not found.""" + db = get_db() + doc = db.collection(COLLECTION).document(DOC_ID).get() + if doc.exists: + return LogRetentionSettings(**doc.to_dict()) + defaults = LogRetentionSettings() + db.collection(COLLECTION).document(DOC_ID).set(defaults.model_dump()) + return defaults + + +def update_log_retention(data: LogRetentionSettingsUpdate) -> LogRetentionSettings: + """Update log retention settings. Only provided fields are updated.""" + db = get_db() + doc_ref = db.collection(COLLECTION).document(DOC_ID) + doc = doc_ref.get() + + existing = doc.to_dict() if doc.exists else LogRetentionSettings().model_dump() + update_data = data.model_dump(exclude_none=True) + existing.update(update_data) + + normalized = LogRetentionSettings(**existing) + doc_ref.set(normalized.model_dump()) + return normalized diff --git a/backend/settings/router.py b/backend/settings/router.py index e68f5b1..1f09ea3 100644 --- a/backend/settings/router.py +++ b/backend/settings/router.py @@ -4,8 +4,10 @@ from auth.dependencies import require_permission, require_roles from auth.models import Role from settings.models import MelodySettings, MelodySettingsUpdate from settings.public_features_models import PublicFeaturesSettings, PublicFeaturesSettingsUpdate +from settings.log_retention_models import LogRetentionSettings, LogRetentionSettingsUpdate from settings import service from settings import public_features_service +from settings import log_retention_service router = APIRouter(prefix="/api/settings", tags=["settings"]) @@ -40,3 +42,20 @@ async def update_public_features( _user: TokenPayload = Depends(require_roles(Role.sysadmin, Role.admin)), ): return public_features_service.update_public_features(body) + + +# ── Log Retention Settings (sysadmin / admin only) ─────────────────────────── + +@router.get("/log-retention", response_model=LogRetentionSettings) +async def get_log_retention( + _user: TokenPayload = Depends(require_roles(Role.sysadmin, Role.admin)), +): + return log_retention_service.get_log_retention() + + +@router.put("/log-retention", response_model=LogRetentionSettings) +async def update_log_retention( + body: LogRetentionSettingsUpdate, + _user: TokenPayload = Depends(require_roles(Role.sysadmin, Role.admin)), +): + return log_retention_service.update_log_retention(body) diff --git a/frontend/src/components/layout/Header.jsx b/frontend/src/components/layout/Header.jsx index 70d9515..7573ce6 100644 --- a/frontend/src/components/layout/Header.jsx +++ b/frontend/src/components/layout/Header.jsx @@ -292,6 +292,16 @@ const SETTINGS_ITEMS = [ ), }, + { + to: '/settings/log-retention', + label: 'Log Retention', + icon: ( + + ), + }, { to: '/settings/pages', label: 'Page Settings', diff --git a/frontend/src/pages/settings/LogRetentionSettings.jsx b/frontend/src/pages/settings/LogRetentionSettings.jsx new file mode 100644 index 0000000..c0f7e22 --- /dev/null +++ b/frontend/src/pages/settings/LogRetentionSettings.jsx @@ -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 ( +
+ + + {loading ? ( +
+ +
+ ) : ( + +
+ setDays(e.target.value)} + hint="Applies globally to every device — there is no per-device override." + inputProps={{ min: 1, max: 3650 }} + /> + +
+ {PRESETS.map(p => ( + + ))} +
+ +
+ +
+
+
+ )} +
+ ) +} diff --git a/frontend/src/router/index.jsx b/frontend/src/router/index.jsx index bcbfd04..e814ade 100644 --- a/frontend/src/router/index.jsx +++ b/frontend/src/router/index.jsx @@ -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() { } /> } /> } /> + } /> {/* Catch-all */} } />