feat(config): section statistiques — BDD, médias, entités

- GET /api/admin/stats : taille BDD (pg_database_size), nb+poids photos/audio
  (scan filesystem), nb notes/todos/listes (requêtes SQL directes)
- ConfigPage : grille 3 colonnes todos/notes/listes + 2 tuiles médias + ligne BDD

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 16:04:08 +02:00
parent de9a1e3c73
commit 454dbadb2f
3 changed files with 112 additions and 2 deletions
+19
View File
@@ -1,3 +1,22 @@
export interface AppStats {
db_size_bytes: number
media: {
photos: { count: number; size_bytes: number }
audio: { count: number; size_bytes: number }
}
counts: {
notes: number
todos: number
shopping_lists: number
}
}
export async function fetchStats(): Promise<AppStats> {
const res = await fetch('/api/admin/stats')
if (!res.ok) throw new Error('Erreur chargement stats')
return res.json() as Promise<AppStats>
}
export interface BackupFile {
filename: string
size: number
+55 -1
View File
@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useTheme, type ThemeMode } from '../contexts/ThemeContext'
import { fetchBackups, createBackup, restoreBackup, type BackupFile } from '../api/admin'
import { fetchBackups, createBackup, restoreBackup, fetchStats, type BackupFile, type AppStats } from '../api/admin'
const sectionStyle: React.CSSProperties = {
background: 'var(--bg-3)',
@@ -46,12 +46,14 @@ function formatDate(iso: string): string {
export default function ConfigPage() {
const navigate = useNavigate()
const { theme, setTheme, fontScale, setFontScale } = useTheme()
const [stats, setStats] = useState<AppStats | null>(null)
const [backups, setBackups] = useState<BackupFile[]>([])
const [backupLoading, setBackupLoading] = useState(false)
const [restoring, setRestoring] = useState<string | null>(null)
const [backupError, setBackupError] = useState<string | null>(null)
useEffect(() => {
fetchStats().then(setStats).catch(() => null)
fetchBackups().then(setBackups).catch(() => setBackups([]))
}, [])
@@ -132,6 +134,58 @@ export default function ConfigPage() {
</div>
</div>
{/* Statistiques */}
<div style={sectionStyle}>
<div style={labelStyle}>Statistiques</div>
{stats ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{/* Compteurs entités */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 8 }}>
{[
{ icon: 'list-check', label: 'Todos', value: stats.counts.todos },
{ icon: 'note-sticky', label: 'Notes', value: stats.counts.notes },
{ icon: 'cart-shopping', label: 'Listes', value: stats.counts.shopping_lists },
].map(item => (
<div key={item.label} style={{ background: 'var(--bg-4)', borderRadius: 8, padding: '10px 8px', textAlign: 'center' }}>
<i className={`fa-solid fa-${item.icon}`} style={{ color: 'var(--accent)', fontSize: 16, display: 'block', marginBottom: 4 }} />
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 18, fontWeight: 700, color: 'var(--ink-1)' }}>{item.value}</div>
<div style={{ fontFamily: 'var(--font-ui)', fontSize: 10, color: 'var(--ink-4)', textTransform: 'uppercase', letterSpacing: 0.5 }}>{item.label}</div>
</div>
))}
</div>
{/* Médias */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
{[
{ icon: 'image', label: 'Photos', data: stats.media.photos },
{ icon: 'microphone', label: 'Audio', data: stats.media.audio },
].map(item => (
<div key={item.label} style={{ background: 'var(--bg-4)', borderRadius: 8, padding: '8px 10px', display: 'flex', alignItems: 'center', gap: 8 }}>
<i className={`fa-solid fa-${item.icon}`} style={{ color: 'var(--info)', fontSize: 14, flexShrink: 0 }} />
<div>
<div style={{ fontFamily: 'var(--font-ui)', fontSize: 12, color: 'var(--ink-2)' }}>
{item.data.count} {item.label.toLowerCase()}
</div>
<div style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--ink-4)' }}>
{formatSize(item.data.size_bytes)}
</div>
</div>
</div>
))}
</div>
{/* BDD */}
<div style={{ background: 'var(--bg-4)', borderRadius: 8, padding: '8px 10px', display: 'flex', alignItems: 'center', gap: 8 }}>
<i className="fa-solid fa-database" style={{ color: 'var(--ok)', fontSize: 14, flexShrink: 0 }} />
<span style={{ fontFamily: 'var(--font-ui)', fontSize: 12, color: 'var(--ink-2)', flex: 1 }}>Base de données</span>
<span style={{ fontFamily: 'var(--font-mono)', fontSize: 12, color: 'var(--ink-3)' }}>{formatSize(stats.db_size_bytes)}</span>
</div>
</div>
) : (
<div style={{ color: 'var(--ink-4)', fontFamily: 'var(--font-ui)', fontSize: 12 }}>Chargement…</div>
)}
</div>
{/* Sauvegarde & Restauration */}
<div style={sectionStyle}>
<div style={labelStyle}>Base de données</div>