From 26557f60387a560d51d607e741f0fd9601631f2c Mon Sep 17 00:00:00 2001 From: Matteo Parenti Date: Sat, 14 Feb 2026 18:33:04 +0100 Subject: [PATCH 1/3] Add storgae usage metering and breakdown --- backend/app/api/routes/system.py | 350 ++++++++++++++++++++++++++++ frontend/src/api/client.ts | 41 ++++ frontend/src/i18n/locales/de.ts | 2 + frontend/src/i18n/locales/en.ts | 2 + frontend/src/i18n/locales/fr.ts | 2 + frontend/src/i18n/locales/it.ts | 2 + frontend/src/i18n/locales/ja.ts | 2 + frontend/src/pages/SettingsPage.tsx | 163 ++++++++++++- 8 files changed, 563 insertions(+), 1 deletion(-) diff --git a/backend/app/api/routes/system.py b/backend/app/api/routes/system.py index 82acf445d..d7a17deca 100644 --- a/backend/app/api/routes/system.py +++ b/backend/app/api/routes/system.py @@ -1,6 +1,9 @@ """System information API routes.""" +import asyncio +import os import platform +import time from datetime import datetime from pathlib import Path @@ -23,6 +26,11 @@ from backend.app.services.printer_manager import printer_manager router = APIRouter(prefix="/system", tags=["system"]) +STORAGE_USAGE_CACHE_SECONDS = 300 +_storage_usage_cache: dict | None = None +_storage_usage_cache_ts: float | None = None +_storage_usage_lock = asyncio.Lock() + def get_directory_size(path: Path) -> int: """Calculate total size of a directory in bytes.""" @@ -62,6 +70,337 @@ def format_uptime(seconds: float) -> str: return " ".join(parts) if parts else "< 1m" +def _is_under(path: Path, root: Path) -> bool: + try: + path.resolve().relative_to(root.resolve()) + return True + except ValueError: + return False + + +def _get_database_paths() -> list[Path]: + candidates = [settings.base_dir / "bambuddy.db", settings.base_dir / "bambutrack.db"] + return [path for path in candidates if path.exists()] + + +def _get_database_items() -> list[dict]: + items: list[dict] = [] + for path in _get_database_paths(): + try: + size = path.stat().st_size + except OSError: + continue + items.append( + { + "name": path.name, + "path": str(path), + "bytes": size, + "formatted": format_bytes(size), + } + ) + items.sort(key=lambda item: item["bytes"], reverse=True) + return items + + +def _get_app_dir() -> Path: + return settings.static_dir.parent + + +def _get_data_dirs() -> list[Path]: + return [ + settings.archive_dir, + settings.log_dir, + settings.plate_calibration_dir, + settings.base_dir / "virtual_printer", + settings.base_dir / "firmware", + ] + + +def _get_storage_scan_roots() -> list[Path]: + roots = [] + for path in _get_data_dirs(): + roots.append(path) + return roots + + +def _is_system_path(path: Path) -> bool: + app_dir = _get_app_dir() + if not _is_under(path, app_dir): + return False + return all(not _is_under(path, data_dir) for data_dir in _get_data_dirs()) + + +def _get_storage_rules() -> list[tuple[str, str, callable]]: + base_dir = settings.base_dir + archive_dir = settings.archive_dir + library_dir = archive_dir / "library" + virtual_printer_dir = base_dir / "virtual_printer" + upload_dir = virtual_printer_dir / "uploads" + + db_paths = set(_get_database_paths()) + + return [ + ( + "database", + "Database", + lambda path: path in db_paths, + ), + ( + "library_thumbnails", + "Library Thumbnails", + lambda path: _is_under(path, library_dir / "thumbnails"), + ), + ( + "library_files", + "Library Files", + lambda path: _is_under(path, library_dir / "files"), + ), + ( + "library_other", + "Library Other", + lambda path: _is_under(path, library_dir), + ), + ( + "archive_timelapses", + "Timelapses", + lambda path: _is_under(path, archive_dir) and "timelapse" in path.name.lower(), + ), + ( + "archive_thumbnails", + "Thumbnails", + lambda path: _is_under(path, archive_dir) and path.name.lower().startswith("thumbnail"), + ), + ( + "archive_files", + "Archives", + lambda path: _is_under(path, archive_dir), + ), + ( + "virtual_printer_upload_cache", + "Virtual Printer Upload Cache", + lambda path: _is_under(path, upload_dir / "cache"), + ), + ( + "virtual_printer_uploads", + "Virtual Printer Uploads", + lambda path: _is_under(path, upload_dir), + ), + ( + "virtual_printer_certs", + "Virtual Printer Certs", + lambda path: _is_under(path, virtual_printer_dir / "certs"), + ), + ( + "virtual_printer_other", + "Virtual Printer Other", + lambda path: _is_under(path, virtual_printer_dir), + ), + ( + "downloads", + "Downloads", + lambda path: _is_under(path, base_dir / "firmware"), + ), + ( + "plate_calibration", + "Plate Calibration", + lambda path: _is_under(path, settings.plate_calibration_dir), + ), + ( + "logs", + "Logs", + lambda path: _is_under(path, settings.log_dir), + ), + ] + + +def _classify_file(path: Path, rules: list[tuple[str, str, callable]]) -> tuple[str, str]: + for key, label, matcher in rules: + try: + if matcher(path): + return key, label + except OSError: + continue + return "other_data", "Other" + + +def _format_percentage(part: int, total: int) -> float: + if total <= 0: + return 0.0 + return round((part / total) * 100, 2) + + +def _get_other_bucket(path: Path, base_dir: Path) -> str: + try: + relative = path.resolve().relative_to(base_dir.resolve()) + except ValueError: + return path.parent.name or path.name + + parts = relative.parts + if len(parts) > 1: + return parts[0] + if parts: + return parts[0] + return path.name + + +def _walk_files(roots: list[Path]) -> list[Path]: + files: list[Path] = [] + stack = [root for root in roots if root.exists()] + while stack: + current = stack.pop() + try: + with os.scandir(current) as entries: + for entry in entries: + try: + if entry.is_symlink(): + continue + if entry.is_dir(follow_symlinks=False): + stack.append(Path(entry.path)) + elif entry.is_file(follow_symlinks=False): + files.append(Path(entry.path)) + except OSError: + continue + except OSError: + continue + return files + + +def _scan_storage_usage() -> dict: + base_dir = settings.base_dir + rules = _get_storage_rules() + + roots = _get_storage_scan_roots() + + seen_roots = set() + unique_roots = [] + for root in roots: + resolved = root.resolve() + if resolved not in seen_roots: + seen_roots.add(resolved) + unique_roots.append(root) + + total_bytes = 0 + error_count = 0 + category_sizes: dict[str, dict] = {} + other_breakdown: dict[tuple[str, str], int] = {} + database_items = _get_database_items() + + files = _walk_files(unique_roots) + for file_path in files: + try: + size = file_path.stat().st_size + except OSError: + error_count += 1 + continue + + total_bytes += size + + key, label = _classify_file(file_path, rules) + if key not in category_sizes: + category_sizes[key] = {"key": key, "label": label, "bytes": 0} + category_sizes[key]["bytes"] += size + + if key == "other_data": + bucket = _get_other_bucket(file_path, base_dir) + kind = "system" if _is_system_path(file_path) else "data" + other_breakdown[(bucket, kind)] = other_breakdown.get((bucket, kind), 0) + size + + for item in database_items: + total_bytes += item["bytes"] + key = "database" + label = "Database" + if key not in category_sizes: + category_sizes[key] = {"key": key, "label": label, "bytes": 0} + category_sizes[key]["bytes"] += item["bytes"] + + categories = [] + for item in category_sizes.values(): + bytes_value = item["bytes"] + categories.append( + { + "key": item["key"], + "label": item["label"], + "bytes": bytes_value, + "formatted": format_bytes(bytes_value), + "percent_of_total": _format_percentage(bytes_value, total_bytes), + } + ) + + categories.sort(key=lambda entry: entry["bytes"], reverse=True) + + other_items = [] + for (bucket, kind), size in other_breakdown.items(): + other_items.append( + { + "bucket": bucket, + "label": bucket, + "kind": kind, + "deletable": kind != "system", + "bytes": size, + "formatted": format_bytes(size), + "percent_of_total": _format_percentage(size, total_bytes), + } + ) + + other_items.sort(key=lambda entry: entry["bytes"], reverse=True) + + return { + "roots": [str(root) for root in unique_roots], + "total_bytes": total_bytes, + "total_formatted": format_bytes(total_bytes), + "categories": categories, + "other_breakdown": other_items, + "scan_errors": error_count, + } + + +async def _get_storage_usage_cached(refresh: bool, max_age_seconds: int) -> dict: + global _storage_usage_cache + global _storage_usage_cache_ts + + now = time.time() + if not refresh and _storage_usage_cache and _storage_usage_cache_ts is not None: + age = now - _storage_usage_cache_ts + if age < max_age_seconds: + return { + **_storage_usage_cache, + "cache": { + "hit": True, + "age_seconds": round(age, 2), + "max_age_seconds": max_age_seconds, + }, + } + + async with _storage_usage_lock: + now = time.time() + if not refresh and _storage_usage_cache and _storage_usage_cache_ts is not None: + age = now - _storage_usage_cache_ts + if age < max_age_seconds: + return { + **_storage_usage_cache, + "cache": { + "hit": True, + "age_seconds": round(age, 2), + "max_age_seconds": max_age_seconds, + }, + } + + snapshot = await asyncio.to_thread(_scan_storage_usage) + _storage_usage_cache = { + **snapshot, + "generated_at": datetime.now().isoformat(), + } + _storage_usage_cache_ts = time.time() + return { + **_storage_usage_cache, + "cache": { + "hit": False, + "age_seconds": 0, + "max_age_seconds": max_age_seconds, + }, + } + + @router.get("/info") async def get_system_info( db: AsyncSession = Depends(get_db), @@ -199,3 +538,14 @@ async def get_system_info( "percent": psutil.cpu_percent(interval=0.1), }, } + + +@router.get("/storage-usage") +async def get_storage_usage( + refresh: bool = False, + max_age_seconds: int = STORAGE_USAGE_CACHE_SECONDS, + _: User | None = RequirePermissionIfAuthEnabled(Permission.SYSTEM_READ), +): + """Get storage usage breakdown for Bambuddy data directories.""" + max_age_seconds = max(0, min(max_age_seconds, 3600)) + return await _get_storage_usage_cached(refresh=refresh, max_age_seconds=max_age_seconds) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 517d2e4a3..27dfcd626 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -3766,6 +3766,14 @@ export const api = { // System Info getSystemInfo: () => request('/system/info'), + getStorageUsage: (options?: { refresh?: boolean }) => { + const params = new URLSearchParams(); + if (options?.refresh) { + params.set('refresh', 'true'); + } + const query = params.toString(); + return request(`/system/storage-usage${query ? `?${query}` : ''}`); + }, // Library (File Manager) getLibraryFolders: () => request('/library/folders'), @@ -4108,6 +4116,39 @@ export interface SystemInfo { }; } +export interface StorageUsageCategory { + key: string; + label: string; + bytes: number; + formatted: string; + percent_of_total: number; +} + +export interface StorageUsageOtherItem { + bucket: string; + label: string; + kind: 'system' | 'data'; + deletable: boolean; + bytes: number; + formatted: string; + percent_of_total: number; +} + +export interface StorageUsageResponse { + roots: string[]; + total_bytes: number; + total_formatted: string; + categories: StorageUsageCategory[]; + other_breakdown: StorageUsageOtherItem[]; + scan_errors: number; + generated_at: string; + cache: { + hit: boolean; + age_seconds: number; + max_age_seconds: number; + }; +} + // Library (File Manager) types export interface LibraryFolderTree { id: number; diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index 807a4cff5..c131fc463 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -1373,6 +1373,8 @@ export default { printsOnly: 'Nur Drucke', totalConsumption: 'Gesamtverbrauch', dataManagement: 'Datenverwaltung', + storageUsage: 'Speichernutzung', + storageUsageDescription: 'Aufschlüsselung der Datennutzung nach Kategorie', clearNotificationLogsDescription: 'Benachrichtigungsprotokolle älter als 30 Tage löschen', resetUiPreferencesDescription: 'Seitenleisten-Reihenfolge, Theme, Ansichtsmodi und Layout-Einstellungen zurücksetzen. Drucker, Archive und Einstellungen werden nicht beeinflusst.', enableHomeAssistant: 'Home Assistant aktivieren', diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 1964383df..03dc59fc6 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -1373,6 +1373,8 @@ export default { printsOnly: 'Prints Only', totalConsumption: 'Total Consumption', dataManagement: 'Data Management', + storageUsage: 'Storage Usage', + storageUsageDescription: 'Breakdown of data usage by category', clearNotificationLogsDescription: 'Delete notification logs older than 30 days', resetUiPreferencesDescription: 'Reset sidebar order, theme, view modes, and layout preferences. Printers, archives, and settings are not affected.', enableHomeAssistant: 'Enable Home Assistant', diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index d45ca703f..45408c00f 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -1369,6 +1369,8 @@ export default { printsOnly: 'Impressions uniquement', totalConsumption: 'Consommation totale', dataManagement: 'Gestion des données', + storageUsage: 'Utilisation du stockage', + storageUsageDescription: 'Répartition de l’utilisation des données par catégorie', clearNotificationLogsDescription: 'Supprimer logs de plus de 30 jours', resetUiPreferencesDescription: 'Réinitialise thèmes et affichage sans toucher aux données.', enableHomeAssistant: 'Activer Home Assistant', diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index 1c308dfb1..e1633c513 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -1289,6 +1289,8 @@ export default { printsOnly: 'Solo stampe', totalConsumption: 'Consumo totale', dataManagement: 'Gestione dati', + storageUsage: 'Memoria utilizzata', + storageUsageDescription: 'Ripartizione della memoria per categoria', clearNotificationLogsDescription: 'Elimina log notifiche più vecchi di 30 giorni', resetUiPreferencesDescription: 'Reimposta ordine barra laterale, tema, modalità vista e preferenze layout. Stampanti, archivi e impostazioni non vengono modificati.', enableHomeAssistant: 'Abilita Home Assistant', diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index fa46bb9a7..0326dfd86 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -1296,6 +1296,8 @@ export default { archiveSettings: 'アーカイブ設定', costTracking: 'コスト追跡', dataManagement: 'データ管理', + storageUsage: 'ストレージ使用量', + storageUsageDescription: 'カテゴリ別のデータ使用量の内訳', enableMqtt: 'MQTTを有効化', useTls: 'TLSを使用', enableMetricsEndpoint: 'メトリクスエンドポイントを有効化', diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index bec8e4c58..5a15d9d57 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -6,7 +6,7 @@ import { api } from '../api/client'; import { useAuth } from '../contexts/AuthContext'; import { formatDateOnly } from '../utils/date'; import { getCurrencySymbol, SUPPORTED_CURRENCIES } from '../utils/currency'; -import type { AppSettings, AppSettingsUpdate, SmartPlug, SmartPlugStatus, NotificationProvider, NotificationTemplate, UpdateStatus, GitHubBackupStatus, CloudAuthStatus, UserCreate, UserUpdate, UserResponse, Group, GroupCreate, GroupUpdate, Permission, PermissionCategory } from '../api/client'; +import type { AppSettings, AppSettingsUpdate, SmartPlug, SmartPlugStatus, NotificationProvider, NotificationTemplate, UpdateStatus, GitHubBackupStatus, CloudAuthStatus, UserCreate, UserUpdate, UserResponse, Group, GroupCreate, GroupUpdate, Permission, PermissionCategory, StorageUsageResponse } from '../api/client'; import { Card, CardContent, CardHeader } from '../components/Card'; import { Button } from '../components/Button'; import { SmartPlugCard } from '../components/SmartPlugCard'; @@ -37,6 +37,38 @@ const validTabs = ['general', 'network', 'plugs', 'notifications', 'filament', ' type TabType = typeof validTabs[number]; type UsersSubTab = 'users' | 'email'; +const STORAGE_CATEGORY_COLORS: Record = { + database: 'bg-blue-600', + library_files: 'bg-green-500', + library_thumbnails: 'bg-teal-500', + library_other: 'bg-emerald-700', + archive_timelapses: 'bg-red-500', + archive_thumbnails: 'bg-amber-500', + archive_files: 'bg-sky-500', + virtual_printer_uploads: 'bg-purple-500', + virtual_printer_upload_cache: 'bg-fuchsia-500', + virtual_printer_certs: 'bg-violet-500', + virtual_printer_other: 'bg-purple-700', + downloads: 'bg-cyan-500', + plate_calibration: 'bg-lime-500', + logs: 'bg-orange-500', + other_data: 'bg-yellow-500', +}; + +const STORAGE_FALLBACK_COLORS = [ + 'bg-blue-500', + 'bg-green-500', + 'bg-yellow-500', + 'bg-red-500', + 'bg-orange-500', + 'bg-teal-500', + 'bg-cyan-500', + 'bg-purple-500', +]; + +const getStorageColor = (key: string, index: number) => + STORAGE_CATEGORY_COLORS[key] || STORAGE_FALLBACK_COLORS[index % STORAGE_FALLBACK_COLORS.length]; + export function SettingsPage() { const queryClient = useQueryClient(); const navigate = useNavigate(); @@ -100,6 +132,7 @@ export function SettingsPage() { const [showChangePasswordModal, setShowChangePasswordModal] = useState(false); const [changePasswordData, setChangePasswordData] = useState({ currentPassword: '', newPassword: '', confirmPassword: '' }); const [changePasswordLoading, setChangePasswordLoading] = useState(false); + const [storageUsageRefreshing, setStorageUsageRefreshing] = useState(false); // User management state const [showCreateUserModal, setShowCreateUserModal] = useState(false); @@ -163,6 +196,33 @@ export function SettingsPage() { queryFn: api.getSettings, }); + const { + data: storageUsage, + isLoading: storageUsageLoading, + isFetching: storageUsageFetching, + } = useQuery({ + queryKey: ['storage-usage'], + queryFn: api.getStorageUsage, + enabled: activeTab === 'general', + staleTime: Infinity, + refetchInterval: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + }); + + const handleStorageUsageRefresh = async () => { + setStorageUsageRefreshing(true); + try { + const data = await api.getStorageUsage({ refresh: true }); + queryClient.setQueryData(['storage-usage'], data); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to refresh storage usage'; + showToast(message, 'error'); + } finally { + setStorageUsageRefreshing(false); + } + }; + const { data: smartPlugs, isLoading: plugsLoading } = useQuery({ queryKey: ['smart-plugs'], queryFn: api.getSmartPlugs, @@ -1875,6 +1935,107 @@ export function SettingsPage() { Reset +
+
+
+

{t('settings.storageUsage', 'Storage Usage')}

+

+ {t('settings.storageUsageDescription', 'Breakdown of data usage by category')} +

+
+ +
+
+ {storageUsageLoading ? ( +
+ + {t('common.loading', 'Loading')} +
+ ) : storageUsage ? ( + <> +
+ {storageUsage.categories + .filter((category) => category.bytes > 0) + .map((category, index) => ( +
+ ))} +
+
+ {storageUsage.categories + .filter((category) => category.bytes > 0) + .map((category, index) => ( +
+ + {category.label} + {category.formatted} + ({category.percent_of_total.toFixed(1)}%) +
+ ))} +
+
+ {t('settings.storageUsageTotal', 'Total')}: {storageUsage.total_formatted} + {storageUsage.scan_errors > 0 && ( + + {t('settings.storageUsageErrors', 'Scan errors')}: {storageUsage.scan_errors} + + )} +
+ {storageUsage.other_breakdown?.length > 0 && ( +
+

+ {t('settings.storageUsageOtherBreakdown', 'Other breakdown')} +

+
+ {storageUsage.other_breakdown.map((item) => ( +
+
+ {item.label} + + {item.kind === 'system' + ? t('settings.storageUsageSystem', 'System') + : t('settings.storageUsageData', 'Data')} + +
+
+ {item.formatted} + ({item.percent_of_total.toFixed(1)}%) +
+
+ ))} +
+
+ )} + + ) : ( +

+ {t('settings.storageUsageUnavailable', 'Storage usage data is unavailable')} +

+ )} +
+

Backup & Restore

From 115f2617cf8e7a4c932ce8099cf30b8c0404f0e6 Mon Sep 17 00:00:00 2001 From: Matteo Parenti Date: Sun, 15 Feb 2026 14:01:54 +0100 Subject: [PATCH 2/3] Fix TS error, add missing translation keys, fix fr locale, cleanup duplicated functions --- backend/app/api/routes/system.py | 20 +++++--------------- frontend/src/i18n/locales/de.ts | 6 ++++++ frontend/src/i18n/locales/en.ts | 6 ++++++ frontend/src/i18n/locales/fr.ts | 8 +++++++- frontend/src/i18n/locales/it.ts | 6 ++++++ frontend/src/i18n/locales/ja.ts | 6 ++++++ frontend/src/pages/SettingsPage.tsx | 2 +- 7 files changed, 37 insertions(+), 17 deletions(-) diff --git a/backend/app/api/routes/system.py b/backend/app/api/routes/system.py index d7a17deca..051e59b37 100644 --- a/backend/app/api/routes/system.py +++ b/backend/app/api/routes/system.py @@ -4,6 +4,7 @@ import asyncio import os import platform import time +from collections.abc import Callable from datetime import datetime from pathlib import Path @@ -116,13 +117,6 @@ def _get_data_dirs() -> list[Path]: ] -def _get_storage_scan_roots() -> list[Path]: - roots = [] - for path in _get_data_dirs(): - roots.append(path) - return roots - - def _is_system_path(path: Path) -> bool: app_dir = _get_app_dir() if not _is_under(path, app_dir): @@ -130,7 +124,7 @@ def _is_system_path(path: Path) -> bool: return all(not _is_under(path, data_dir) for data_dir in _get_data_dirs()) -def _get_storage_rules() -> list[tuple[str, str, callable]]: +def _get_storage_rules() -> list[tuple[str, str, Callable]]: base_dir = settings.base_dir archive_dir = settings.archive_dir library_dir = archive_dir / "library" @@ -213,7 +207,7 @@ def _get_storage_rules() -> list[tuple[str, str, callable]]: ] -def _classify_file(path: Path, rules: list[tuple[str, str, callable]]) -> tuple[str, str]: +def _classify_file(path: Path, rules: list[tuple[str, str, Callable]]) -> tuple[str, str]: for key, label, matcher in rules: try: if matcher(path): @@ -236,11 +230,7 @@ def _get_other_bucket(path: Path, base_dir: Path) -> str: return path.parent.name or path.name parts = relative.parts - if len(parts) > 1: - return parts[0] - if parts: - return parts[0] - return path.name + return parts[0] if parts else path.name def _walk_files(roots: list[Path]) -> list[Path]: @@ -269,7 +259,7 @@ def _scan_storage_usage() -> dict: base_dir = settings.base_dir rules = _get_storage_rules() - roots = _get_storage_scan_roots() + roots = _get_data_dirs() seen_roots = set() unique_roots = [] diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index f89d32ae8..2a4b20a09 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -1379,6 +1379,12 @@ export default { dataManagement: 'Datenverwaltung', storageUsage: 'Speichernutzung', storageUsageDescription: 'Aufschlüsselung der Datennutzung nach Kategorie', + storageUsageTotal: 'Gesamt', + storageUsageErrors: 'Fehler', + storageUsageOtherBreakdown: 'Sonstiges (enthält statische Assets, Skripte und Konfigurationsdateien)', + storageUsageSystem: 'System', + storageUsageData: 'Daten', + storageUsageUnavailable: 'Speichernutzungsinformationen nicht verfügbar', clearNotificationLogsDescription: 'Benachrichtigungsprotokolle älter als 30 Tage löschen', resetUiPreferencesDescription: 'Seitenleisten-Reihenfolge, Theme, Ansichtsmodi und Layout-Einstellungen zurücksetzen. Drucker, Archive und Einstellungen werden nicht beeinflusst.', enableHomeAssistant: 'Home Assistant aktivieren', diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 15692a23c..3b7248da3 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -1379,6 +1379,12 @@ export default { dataManagement: 'Data Management', storageUsage: 'Storage Usage', storageUsageDescription: 'Breakdown of data usage by category', + storageUsageTotal: 'Total', + storageUsageErrors: 'Errors', + storageUsageOtherBreakdown: 'Other (includes static assets, scripts, and configuration files)', + storageUsageSystem: 'System', + storageUsageData: 'Data', + storageUsageUnavailable: 'Storage usage information unavailable', clearNotificationLogsDescription: 'Delete notification logs older than 30 days', resetUiPreferencesDescription: 'Reset sidebar order, theme, view modes, and layout preferences. Printers, archives, and settings are not affected.', enableHomeAssistant: 'Enable Home Assistant', diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index 6cffd85c3..2f1a65ee2 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -1,4 +1,4 @@ -export default { +export default { // Navigation nav: { printers: 'Imprimantes', @@ -1375,6 +1375,12 @@ export default { dataManagement: 'Gestion des données', storageUsage: 'Utilisation du stockage', storageUsageDescription: 'Répartition de l’utilisation des données par catégorie', + storageUsageTotal: 'Total', + storageUsageErrors: 'Erreurs', + storageUsageOtherBreakdown: 'Autre (inclut ressources statiques, scripts et fichiers de configuration)', + storageUsageSystem: 'Système', + storageUsageData: 'Données', + storageUsageUnavailable: 'Informations d\'utilisation du stockage non disponibles', clearNotificationLogsDescription: 'Supprimer logs de plus de 30 jours', resetUiPreferencesDescription: 'Réinitialise thèmes et affichage sans toucher aux données.', enableHomeAssistant: 'Activer Home Assistant', diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index 0a3ca8be8..1c102ba61 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -1285,6 +1285,12 @@ export default { dataManagement: 'Gestione dati', storageUsage: 'Memoria utilizzata', storageUsageDescription: 'Ripartizione della memoria per categoria', + storageUsageTotal: 'Totale', + storageUsageErrors: 'Errori', + storageUsageOtherBreakdown: 'Altro (include risorse statiche, script e file di configurazione)', + storageUsageSystem: 'Sistema', + storageUsageData: 'Dati', + storageUsageUnavailable: 'Informazioni sull\'utilizzo della memoria non disponibili', clearNotificationLogsDescription: 'Elimina log notifiche più vecchi di 30 giorni', resetUiPreferencesDescription: 'Reimposta ordine barra laterale, tema, modalità vista e preferenze layout. Stampanti, archivi e impostazioni non vengono modificati.', enableHomeAssistant: 'Abilita Home Assistant', diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index 9fc8c19db..418ecf88c 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -1303,6 +1303,12 @@ export default { dataManagement: 'データ管理', storageUsage: 'ストレージ使用量', storageUsageDescription: 'カテゴリ別のデータ使用量の内訳', + storageUsageTotal: '合計', + storageUsageErrors: 'エラー', + storageUsageOtherBreakdown: 'その他(静的アセット、スクリプト、設定ファイルを含む)', + storageUsageSystem: 'システム', + storageUsageData: 'データ', + storageUsageUnavailable: 'ストレージ使用量情報は利用できません', enableMqtt: 'MQTTを有効化', useTls: 'TLSを使用', enableMetricsEndpoint: 'メトリクスエンドポイントを有効化', diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 932831d57..b13788bc5 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -202,7 +202,7 @@ export function SettingsPage() { isFetching: storageUsageFetching, } = useQuery({ queryKey: ['storage-usage'], - queryFn: api.getStorageUsage, + queryFn: () => api.getStorageUsage(), enabled: activeTab === 'general', staleTime: Infinity, refetchInterval: false, From 5ad0e59209d1124b8f066f9f95f81249655cda0d Mon Sep 17 00:00:00 2001 From: Matteo Parenti Date: Sun, 15 Feb 2026 14:43:53 +0100 Subject: [PATCH 3/3] Removed BOM, fixed apostrophe --- frontend/src/i18n/locales/fr.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index aecf8fe51..4102de98c 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -1,4 +1,4 @@ -export default { +export default { // Navigation nav: { printers: 'Imprimantes', @@ -1403,7 +1403,7 @@ totalConsumption: 'Consommation totale', dataManagement: 'Gestion des données', storageUsage: 'Utilisation du stockage', - storageUsageDescription: 'Répartition de l’utilisation des données par catégorie', + storageUsageDescription: 'Répartition de l\'utilisation des données par catégorie', storageUsageTotal: 'Total', storageUsageErrors: 'Erreurs', storageUsageOtherBreakdown: 'Autre (inclut ressources statiques, scripts et fichiers de configuration)',