from pathlib import Path import json root = Path('/mnt/data/work_routeros3/frontend/src') (root / 'app/features/dashboard/dashboard-page.component.html').write_text('''
{{ 'dashboard.latestSnapshot' | translate }} {{ latestBackupLabel }} {{ latestBackupHint }}
{{ 'dashboard.coverageLabel' | translate }} {{ coveragePercent }}% {{ 'dashboard.coverageHint' | translate }}
{{ 'dashboard.weeklyActivityLabel' | translate }} {{ backupsLast7Days }} {{ 'dashboard.weeklyActivityHint' | translate }}
{{ 'dashboard.busiestRouterLabel' | translate }} {{ busiestRouterLabel }} {{ busiestRouterHint }}
{{ storageViewDescriptionKey | translate }}
{{ formatPercent(usedPercent) }} {{ 'dashboard.diskUsage' | translate }}
{{ item.label | translate }} {{ item.value }}
{{ 'dashboard.diskUsed' | translate }} {{ formatBytes(storageUsedBytes) }} {{ 'dashboard.storageInsightUsage' | translate }}
{{ 'dashboard.folderUsage' | translate }} {{ formatPercent(repositorySharePercent) }}
{{ 'dashboard.freeSpace' | translate }} {{ formatPercent(freePercent) }}
{{ 'dashboard.exportShareLabel' | translate }} {{ exportsSharePercent }}%
{{ 'dashboard.activityTodayLabel' | translate }} {{ activityToday }}
{{ row.label | translate }} {{ row.value }}
{{ formatPercent(row.percent) }}
{{ 'dashboard.exportShareLabel' | translate }} {{ exportsSharePercent }}%
{{ 'dashboard.binaryCard' | translate }} {{ binarySharePercent }}%
{{ 'dashboard.avgBackupsPerRouter' | translate }} {{ averageBackupsPerRouter }}
{{ 'dashboard.storageViewActivity' | translate }} {{ 'dashboard.storageActivityHint' | translate }}
{{ backupsLast7Days }}
{{ bar.value }} {{ bar.label }}
{{ 'dashboard.latestSnapshot' | translate }} {{ latestBackupLabel }}
{{ 'dashboard.busiestRouterLabel' | translate }} {{ busiestRouterLabel }}
{{ 'dashboard.activityTodayLabel' | translate }} {{ activityToday }}
{{ 'dashboard.totalDisk' | translate }} {{ formatBytes(data.storage.total) }}
{{ 'dashboard.diskUsed' | translate }} {{ formatBytes(storageUsedBytes) }}
{{ 'dashboard.freeSpace' | translate }} {{ formatBytes(data.storage.free) }}
{{ 'dashboard.folderUsage' | translate }} {{ formatBytes(data.storage.folder_used) }}
{{ 'dashboard.avgBackupsPerRouter' | translate }} {{ averageBackupsPerRouter }}
{{ 'dashboard.activityTodayLabel' | translate }} {{ activityToday }}
{{ activityLabel(log.message) }} {{ log.timestamp | date: 'dd.MM.yyyy HH:mm:ss' }}
{{ log.message }}

{{ 'dashboard.noActivity' | translate }}

''') (root / 'app/features/dashboard/dashboard-page.component.ts').write_text('''import { CommonModule } from '@angular/common'; import { Component, OnInit, inject } from '@angular/core'; import { RouterLink } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { ButtonModule } from 'primeng/button'; import { forkJoin } from 'rxjs'; import { ApiService } from '../../core/services/api.service'; import { LanguageService } from '../../core/services/language.service'; import { UiService } from '../../core/services/ui.service'; import { PageHeaderComponent } from '../../shared/ui/page-header.component'; import { SectionCardComponent } from '../../shared/ui/section-card.component'; import { StatCardComponent } from '../../shared/ui/stat-card.component'; interface DashboardData { routers_count: number; export_count: number; binary_count: number; total_backups: number; recent_logs: { timestamp: string; message: string }[]; storage: { total: number; used: number; free: number; folder_used: number; usage_percent: number }; } interface BackupInventoryItem { id: number; router_id: number; router_name?: string; backup_type: 'export' | 'binary'; created_at: string; file_size?: number | null; } interface RouterInventoryItem { id: number; name: string; } type StorageView = 'overview' | 'composition' | 'activity'; @Component({ standalone: true, imports: [CommonModule, RouterLink, TranslateModule, ButtonModule, PageHeaderComponent, SectionCardComponent, StatCardComponent], templateUrl: './dashboard-page.component.html' }) export class DashboardPageComponent implements OnInit { private readonly api = inject(ApiService); private readonly ui = inject(UiService); private readonly language = inject(LanguageService); data?: DashboardData; backups: BackupInventoryItem[] = []; routers: RouterInventoryItem[] = []; exporting = false; runningBinary = false; readonly activityPageSize = 6; activityPage = 0; storageView: StorageView = 'overview'; readonly storageViewOptions: { value: StorageView; label: string; icon: string }[] = [ { value: 'overview', label: 'dashboard.storageViewOverview', icon: 'pi pi-chart-pie' }, { value: 'composition', label: 'dashboard.storageViewComposition', icon: 'pi pi-sliders-h' }, { value: 'activity', label: 'dashboard.storageViewActivity', icon: 'pi pi-chart-bar' } ]; ngOnInit() { this.load(); } load() { forkJoin({ dashboard: this.api.http.get(`${this.api.baseUrl}/dashboard`), backups: this.api.http.get(`${this.api.baseUrl}/backups`), routers: this.api.http.get(`${this.api.baseUrl}/routers`) }).subscribe(({ dashboard, backups, routers }) => { this.data = dashboard; this.backups = backups; this.routers = routers; this.activityPage = 0; }); } exportAll() { if (this.exporting) { return; } this.exporting = true; this.api.http.post(`${this.api.baseUrl}/backups/routers/export-all`, {}).subscribe({ next: (result) => { this.ui.success('toast.exportedRouters', { count: result.filter((item) => item.status === 'ok').length }); this.load(); }, complete: () => { this.exporting = false; } }); } binaryAll() { if (this.runningBinary) { return; } this.runningBinary = true; this.api.http.post(`${this.api.baseUrl}/backups/routers/binary-all`, {}).subscribe({ next: (result) => { this.ui.success('toast.binaryCompletedRouters', { count: result.filter((item) => item.status === 'ok').length }); this.load(); }, complete: () => { this.runningBinary = false; } }); } setStorageView(view: StorageView) { this.storageView = view; } get storageViewDescriptionKey(): string { switch (this.storageView) { case 'composition': return 'dashboard.storageViewCompositionHint'; case 'activity': return 'dashboard.storageViewActivityHint'; default: return 'dashboard.storageViewOverviewHint'; } } get storageUsedBytes(): number { const storage = this.data?.storage; if (!storage) { return 0; } if (storage.total > 0 && storage.free >= 0 && storage.free <= storage.total) { return Math.max(0, storage.total - storage.free); } return Math.max(0, storage.used || 0); } get usedPercent(): number { const storage = this.data?.storage; if (!storage?.total) { return Number(storage?.usage_percent || 0); } return Number(((this.storageUsedBytes / storage.total) * 100).toFixed(1)); } get freePercent(): number { const storage = this.data?.storage; if (!storage?.total) { return Math.max(0, 100 - this.usedPercent); } return Number(((storage.free / storage.total) * 100).toFixed(1)); } get repositorySharePercent(): number { const storage = this.data?.storage; if (!storage?.total) { return 0; } return Number(Math.min(100, (storage.folder_used / storage.total) * 100).toFixed(1)); } get repositoryVsUsedPercent(): number { if (!this.storageUsedBytes) { return 0; } return Number(Math.min(100, ((this.data?.storage.folder_used || 0) / this.storageUsedBytes) * 100).toFixed(1)); } get binarySharePercent(): number { return Math.max(0, 100 - this.exportsSharePercent); } get averageBackupsPerRouter(): string { if (!this.data?.routers_count) { return '0'; } return (this.data.total_backups / this.data.routers_count).toFixed(1); } get coveragePercent(): number { if (!this.routers.length) { return 0; } const routersWithBackups = new Set(this.backups.map((item) => item.router_id)).size; return Math.round((routersWithBackups / this.routers.length) * 100); } get exportsSharePercent(): number { if (!this.backups.length) { return 0; } return Math.round((this.backups.filter((item) => item.backup_type === 'export').length / this.backups.length) * 100); } get backupsLast7Days(): number { const threshold = Date.now() - 7 * 24 * 60 * 60 * 1000; return this.backups.filter((item) => new Date(item.created_at).getTime() >= threshold).length; } get latestBackupLabel(): string { const latest = this.latestBackup; if (!latest) { return this.ui.instant('dashboard.noneLabel'); } return latest.router_name || `#${latest.router_id}`; } get latestBackupHint(): string { const latest = this.latestBackup; if (!latest) { return this.ui.instant('dashboard.noActivity'); } return `${latest.backup_type === 'export' ? this.ui.instant('files.exportType') : this.ui.instant('files.binaryType')} · ${this.relativeAge(latest.created_at)}`; } get busiestRouterLabel(): string { const busiest = this.busiestRouter; if (!busiest) { return this.ui.instant('dashboard.noneLabel'); } return busiest.name; } get busiestRouterHint(): string { const busiest = this.busiestRouter; if (!busiest) { return this.ui.instant('dashboard.noActivity'); } return this.ui.instant('dashboard.routerSnapshotsHint', { count: busiest.count }); } get activityToday(): number { if (!this.data?.recent_logs?.length) { return 0; } const today = new Date(); return this.data.recent_logs.filter((log) => { const value = new Date(log.timestamp); return value.getFullYear() === today.getFullYear() && value.getMonth() === today.getMonth() && value.getDate() === today.getDate(); }).length; } get activityPageCount(): number { const total = this.data?.recent_logs?.length || 0; return Math.max(1, Math.ceil(total / this.activityPageSize)); } get pagedRecentLogs() { if (!this.data?.recent_logs?.length) { return []; } const start = this.activityPage * this.activityPageSize; return this.data.recent_logs.slice(start, start + this.activityPageSize); } get activityRangeLabel(): string { const total = this.data?.recent_logs?.length || 0; if (!total) { return '0 / 0'; } const start = this.activityPage * this.activityPageSize + 1; const end = Math.min(total, start + this.activityPageSize - 1); return `${start}-${end} / ${total}`; } get storageRingBackground(): string { const safePercent = Math.min(100, Math.max(0, this.usedPercent)); return `conic-gradient(var(--accent) 0deg ${safePercent * 3.6}deg, rgba(129, 149, 167, 0.18) ${safePercent * 3.6}deg 360deg)`; } get storageOverviewLegend(): { label: string; value: string; tone: 'accent' | 'success' | 'neutral' }[] { return [ { label: 'dashboard.totalDisk', value: this.formatBytes(this.data?.storage.total || 0), tone: 'accent' }, { label: 'dashboard.freeSpace', value: this.formatBytes(this.data?.storage.free || 0), tone: 'success' }, { label: 'dashboard.folderUsage', value: this.formatBytes(this.data?.storage.folder_used || 0), tone: 'neutral' } ]; } get storageCompositionRows(): { label: string; value: string; percent: number; tone: 'accent' | 'success' | 'neutral' }[] { return [ { label: 'dashboard.diskUsed', value: this.formatBytes(this.storageUsedBytes), percent: this.usedPercent, tone: 'accent' }, { label: 'dashboard.freeSpace', value: this.formatBytes(this.data?.storage.free || 0), percent: this.freePercent, tone: 'success' }, { label: 'dashboard.folderUsage', value: this.formatBytes(this.data?.storage.folder_used || 0), percent: this.repositoryVsUsedPercent, tone: 'neutral' } ]; } get recentBackupBars(): { label: string; value: number; percent: number; tooltip: string }[] { const locale = this.currentLocale(); const formatter = new Intl.DateTimeFormat(locale, { weekday: 'short' }); const today = new Date(); const buckets: { date: Date; label: string; value: number }[] = []; for (let offset = 6; offset >= 0; offset -= 1) { const date = new Date(today); date.setHours(0, 0, 0, 0); date.setDate(today.getDate() - offset); buckets.push({ date, label: formatter.format(date), value: 0 }); } for (const backup of this.backups) { const created = new Date(backup.created_at); created.setHours(0, 0, 0, 0); const bucket = buckets.find((item) => item.date.getTime() === created.getTime()); if (bucket) { bucket.value += 1; } } const maxValue = Math.max(...buckets.map((item) => item.value), 0); return buckets.map((item) => ({ label: item.label.replace('.', ''), value: item.value, percent: maxValue ? Math.max(14, (item.value / maxValue) * 100) : 14, tooltip: `${item.label}: ${item.value}` })); } previousActivityPage() { this.activityPage = Math.max(0, this.activityPage - 1); } nextActivityPage() { this.activityPage = Math.min(this.activityPageCount - 1, this.activityPage + 1); } formatBytes(value: number): string { if (!value) return '0 B'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; let size = value; let unit = 0; while (size >= 1024 && unit < units.length - 1) { size /= 1024; unit += 1; } return `${size.toFixed(size >= 10 || unit === 0 ? 0 : 1)} ${units[unit]}`; } formatPercent(value: number): string { return `${Number(value || 0).toFixed(1)}%`; } relativeAge(value: string): string { const diff = Date.now() - new Date(value).getTime(); const hours = Math.floor(diff / 3_600_000); if (hours < 1) { const minutes = Math.max(1, Math.floor(diff / 60_000)); return this.ui.instant('files.minutesAgo', { value: minutes }); } if (hours < 24) { return this.ui.instant('files.hoursAgo', { value: hours }); } const days = Math.floor(hours / 24); return this.ui.instant('files.daysAgo', { value: days }); } activityTone(message: string): 'success' | 'danger' | 'warning' | 'info' { const normalized = message.toLowerCase(); if (normalized.includes('fail') || normalized.includes('error')) return 'danger'; if (normalized.includes('cleanup') || normalized.includes('retention')) return 'warning'; if (normalized.includes('upload') || normalized.includes('email')) return 'info'; return 'success'; } activityIcon(message: string): string { const tone = this.activityTone(message); if (tone === 'danger') return 'pi pi-exclamation-triangle'; if (tone === 'warning') return 'pi pi-broom'; if (tone === 'info') return 'pi pi-send'; return 'pi pi-check'; } activityLabel(message: string): string { const tone = this.activityTone(message); if (tone === 'danger') return this.ui.instant('dashboard.activityFailure'); if (tone === 'warning') return this.ui.instant('dashboard.activityMaintenance'); if (tone === 'info') return this.ui.instant('dashboard.activityDelivery'); return this.ui.instant('dashboard.activitySuccess'); } private currentLocale(): string { switch (this.language.current()) { case 'no': return 'nb-NO'; case 'es': return 'es-ES'; case 'en': return 'en-GB'; default: return 'pl-PL'; } } private get latestBackup(): BackupInventoryItem | undefined { return this.backups.slice().sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0]; } private get busiestRouter(): { name: string; count: number } | null { if (!this.backups.length) { return null; } const counters = new Map(); for (const backup of this.backups) { const entry = counters.get(backup.router_id) || { name: backup.router_name || `#${backup.router_id}`, count: 0 }; entry.count += 1; counters.set(backup.router_id, entry); } return Array.from(counters.values()).sort((a, b) => b.count - a.count)[0] || null; } } ''') settings_html = root / 'app/features/settings/settings-page.component.html' text = settings_html.read_text() text = text.replace('
\n ', '
\n ', 1) text = text.replace('', '') text = text.replace('', '') settings_html.write_text(text) app_html = root / 'app/app.component.html' app_html.write_text('''
{{ 'footer.apiOfflineTitle' | translate }} {{ 'footer.apiOfflineMessage' | translate }}
''') app_ts = root / 'app/app.component.ts' text = app_ts.read_text() text = text.replace(" readonly author = 'MateuszG';\n", " readonly authorName = 'Mateusz Gruszczyński';\n readonly authorHandle = '@linuxiarz.pl';\n") app_ts.write_text(text) styles_path = root / 'styles.css' styles = styles_path.read_text() styles += ''' /* --- 2026 patch: dashboard storage switcher, settings dropdown, footer author --- */ .storage-panel--enhanced { grid-template-columns: minmax(0, 1.32fr) minmax(280px, 0.9fr); align-items: stretch; } .storage-panel__toolbar { display: grid; gap: 0.9rem; margin-bottom: 1rem; } .storage-panel__eyebrow { font-size: 0.72rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-soft); } .storage-view-switch { display: inline-flex; flex-wrap: wrap; gap: 0.45rem; padding: 0.35rem; border-radius: 999px; border: 1px solid var(--border-color); background: color-mix(in srgb, var(--surface-1) 92%, transparent); } .storage-view-switch__btn { border: 0; background: transparent; color: var(--text-soft); min-height: 2.4rem; padding: 0.55rem 0.9rem; border-radius: 999px; display: inline-flex; align-items: center; gap: 0.5rem; font: inherit; font-size: 0.76rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; cursor: pointer; transition: background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease; } .storage-view-switch__btn:hover { background: color-mix(in srgb, var(--surface-2) 88%, transparent); color: var(--text-main); } .storage-view-switch__btn.is-active { background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 88%, #fff 0%), color-mix(in srgb, var(--blue) 84%, #fff 0%)); color: #eef6ff; box-shadow: 0 12px 24px color-mix(in srgb, var(--accent) 24%, transparent); } .storage-stage { min-width: 0; } .storage-stage--overview, .storage-stage--composition, .storage-stage--activity { display: grid; gap: 1rem; } .storage-stage--overview { grid-template-columns: minmax(220px, 280px) minmax(0, 1fr); align-items: stretch; } .storage-ring-panel, .storage-overview-side, .storage-composition-card, .storage-trend-card { min-width: 0; } .storage-ring-panel { padding: 1rem; border-radius: 22px; border: 1px solid var(--border-color); background: color-mix(in srgb, var(--surface-1) 92%, transparent); } .storage-ring { margin-inline: auto; } .storage-legend { display: grid; gap: 0.65rem; } .storage-legend__item { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.75rem 0.85rem; border-radius: 16px; border: 1px solid var(--border-color); background: color-mix(in srgb, var(--surface-0) 96%, transparent); } .storage-legend__item strong { font-family: var(--font-title); font-size: 0.88rem; } .storage-legend__item[data-tone='accent'] { border-color: color-mix(in srgb, var(--accent) 26%, var(--border-color)); } .storage-legend__item[data-tone='success'] { border-color: color-mix(in srgb, var(--success) 28%, var(--border-color)); } .storage-overview-side { display: grid; gap: 0.95rem; } .storage-callout { display: grid; gap: 0.32rem; padding: 1rem 1.05rem; border-radius: 20px; border: 1px solid var(--border-color); background: linear-gradient(180deg, color-mix(in srgb, var(--surface-1) 96%, transparent), color-mix(in srgb, var(--surface-0) 95%, transparent)); } .storage-callout--accent { border-color: color-mix(in srgb, var(--accent) 26%, var(--border-color)); box-shadow: 0 16px 28px color-mix(in srgb, var(--accent) 12%, transparent); } .storage-callout strong, .storage-mini-card strong, .storage-trend-card__header span { font-family: var(--font-title); font-size: 1.08rem; letter-spacing: 0.05em; color: var(--text-main); } .storage-callout small, .storage-trend-card__header small { color: var(--text-soft); line-height: 1.55; } .storage-bars--stacked { gap: 0.85rem; } .storage-mini-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.85rem; } .storage-mini-grid--triple { grid-template-columns: repeat(3, minmax(0, 1fr)); } .storage-mini-card { display: grid; gap: 0.35rem; padding: 0.9rem 1rem; border-radius: 18px; border: 1px solid var(--border-color); background: color-mix(in srgb, var(--surface-1) 94%, transparent); } .storage-composition-card { padding: 1rem; border-radius: 22px; border: 1px solid var(--border-color); background: color-mix(in srgb, var(--surface-1) 92%, transparent); } .storage-composition-list { display: grid; gap: 0.85rem; } .storage-composition-row { display: grid; gap: 0.45rem; } .storage-composition-row__meta { display: flex; justify-content: space-between; gap: 1rem; align-items: center; } .storage-composition-row__meta strong { font-family: var(--font-title); font-size: 0.9rem; } .storage-composition-row__track { height: 12px; border-radius: 999px; background: rgba(128, 145, 164, 0.14); overflow: hidden; } .storage-composition-row__track span { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--accent), var(--blue)); } .storage-composition-row[data-tone='success'] .storage-composition-row__track span, .storage-bars__track--success span { background: linear-gradient(90deg, var(--success), #2d8d74); } .storage-composition-row[data-tone='neutral'] .storage-composition-row__track span { background: linear-gradient(90deg, #8e9cab, #b0bcc8); } .storage-composition-row small { color: var(--text-faint); } .storage-trend-card { padding: 1rem; border-radius: 22px; border: 1px solid var(--border-color); background: color-mix(in srgb, var(--surface-1) 92%, transparent); } .storage-trend-card__header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; } .storage-trend-card__header strong { display: block; margin-bottom: 0.2rem; } .storage-trend-bars { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 0.7rem; align-items: end; min-height: 220px; } .storage-trend-bars__item { display: grid; gap: 0.45rem; justify-items: center; } .storage-trend-bars__column { width: 100%; min-height: 150px; border-radius: 18px; padding: 0.45rem; display: flex; align-items: flex-end; background: linear-gradient(180deg, color-mix(in srgb, var(--surface-0) 98%, transparent), color-mix(in srgb, var(--surface-2) 96%, transparent)); border: 1px solid var(--border-color); } .storage-trend-bars__column span { display: block; width: 100%; min-height: 14%; border-radius: 14px; background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 88%, #fff 0%), color-mix(in srgb, var(--blue) 86%, #fff 0%)); box-shadow: 0 14px 22px color-mix(in srgb, var(--accent) 18%, transparent); } .storage-trend-bars__item strong { font-family: var(--font-title); font-size: 0.84rem; } .storage-trend-bars__item small { text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-soft); } .storage-snapshot-grid { gap: 0.75rem; align-content: start; } .storage-snapshot-card { min-height: 88px; padding: 0.9rem; } .settings-interface-grid { align-items: start; row-gap: 1.1rem; } .settings-interface-grid .p-dropdown { width: 100%; } .settings-floating-dropdown { z-index: 1300 !important; } .layout-footer__author { display: inline-flex; align-items: center; gap: 0.6rem; padding: 0.5rem 0.75rem; border-radius: 999px; border: 1px solid var(--border-color); background: color-mix(in srgb, var(--surface-1) 94%, transparent); text-transform: none; letter-spacing: normal; color: var(--text-main); } .layout-footer__label { color: var(--text-faint); } .layout-footer__author strong { font-family: var(--font-title); font-size: 0.78rem; letter-spacing: 0.04em; } .layout-footer__author a { font-size: 0.78rem; letter-spacing: 0.04em; text-transform: none; color: var(--accent); } @media (max-width: 1180px) { .storage-panel--enhanced, .storage-stage--overview { grid-template-columns: 1fr; } } @media (max-width: 780px) { .storage-mini-grid, .storage-mini-grid--triple, .storage-trend-bars, .storage-snapshot-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .storage-trend-bars { min-height: unset; } .storage-trend-bars__column { min-height: 120px; } } @media (max-width: 560px) { .storage-view-switch { display: grid; grid-template-columns: 1fr; } .storage-mini-grid, .storage-mini-grid--triple, .storage-snapshot-grid, .storage-trend-bars { grid-template-columns: 1fr; } .storage-trend-card__header, .storage-composition-row__meta, .layout-footer__author { align-items: flex-start; flex-direction: column; } } ''' styles_path.write_text(styles) for name, lang_data in { 'pl.json': { ('dashboard', 'diskUsed'): 'Zajętość dysku', ('dashboard', 'storageViewOverview'): 'Podgląd', ('dashboard', 'storageViewComposition'): 'Proporcje', ('dashboard', 'storageViewActivity'): 'Aktywność 7 dni', ('dashboard', 'storageViewOverviewHint'): 'Pierścień użycia i szybkie wskaźniki miejsca.', ('dashboard', 'storageViewCompositionHint'): 'Udziały zajętości, wolnego miejsca i repozytorium.', ('dashboard', 'storageViewActivityHint'): 'Nowe backupy z ostatnich 7 dni i tempo zmian.', ('dashboard', 'storageActivityHint'): 'Liczba nowych backupów w ostatnim tygodniu.', ('dashboard', 'storageInsightUsage'): 'Rzeczywiście zajęta przestrzeń na dysku według danych systemowych.' }, 'en.json': { ('dashboard', 'diskUsed'): 'Disk used', ('dashboard', 'storageViewOverview'): 'Overview', ('dashboard', 'storageViewComposition'): 'Breakdown', ('dashboard', 'storageViewActivity'): '7-day activity', ('dashboard', 'storageViewOverviewHint'): 'Usage ring and quick space indicators.', ('dashboard', 'storageViewCompositionHint'): 'Relative split of used, free and repository space.', ('dashboard', 'storageViewActivityHint'): 'New backups over the last 7 days and their pace.', ('dashboard', 'storageActivityHint'): 'Number of new backups created during the last week.', ('dashboard', 'storageInsightUsage'): 'Actual disk occupancy calculated from current system values.' }, 'es.json': { ('dashboard', 'diskUsed'): 'Disco usado', ('dashboard', 'storageViewOverview'): 'Vista general', ('dashboard', 'storageViewComposition'): 'Proporciones', ('dashboard', 'storageViewActivity'): 'Actividad 7 días', ('dashboard', 'storageViewOverviewHint'): 'Anillo de uso e indicadores rápidos de espacio.', ('dashboard', 'storageViewCompositionHint'): 'Reparto del espacio usado, libre y del repositorio.', ('dashboard', 'storageViewActivityHint'): 'Backups nuevos de los últimos 7 días y su ritmo.', ('dashboard', 'storageActivityHint'): 'Número de backups nuevos creados durante la última semana.', ('dashboard', 'storageInsightUsage'): 'Ocupación real del disco calculada con los valores actuales del sistema.' }, 'no.json': { ('dashboard', 'diskUsed'): 'Brukt disk', ('dashboard', 'storageViewOverview'): 'Oversikt', ('dashboard', 'storageViewComposition'): 'Fordeling', ('dashboard', 'storageViewActivity'): '7 dagers aktivitet', ('dashboard', 'storageViewOverviewHint'): 'Bruksring og raske plassindikatorer.', ('dashboard', 'storageViewCompositionHint'): 'Fordeling av brukt, ledig og lagringsplass for repoet.', ('dashboard', 'storageViewActivityHint'): 'Nye sikkerhetskopier de siste 7 dagene og tempoet.', ('dashboard', 'storageActivityHint'): 'Antall nye sikkerhetskopier opprettet den siste uken.', ('dashboard', 'storageInsightUsage'): 'Faktisk diskbruk beregnet fra systemets nåværende verdier.' } }.items(): path = root / 'assets/i18n' / name data = json.loads(path.read_text()) for (section, key), value in lang_data.items(): data.setdefault(section, {})[key] = value path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + '\n') print('[OK] patched')