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 ( +