Retour au tableau de bord
{/* ── Tabs ── */}
{[
{ key: 'settings', label: 'Paramètres' },
{ key: 'notifications', label: 'Notifications' },
{ key: 'passkeys', label: 'Passkeys' },
{ key: 'logs', label: 'Connexions' },
{ key: 'database', label: 'Base de données' },
].map(tab => (
setActiveTab(tab.key)}
role="tab"
aria-selected={activeTab === tab.key}
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px whitespace-nowrap ${
activeTab === tab.key
? 'border-indigo-500 text-indigo-400'
: 'border-transparent text-gray-500 hover:text-gray-300'
}`}
>
{tab.label}
))}
{/* ── Tab: Paramètres ── */}
{activeTab === 'settings' && (
Paramètres
Configuration globale de l'application.
{settingsError && (
{settingsError}
)}
{settingsLoading
? Chargement…
: (
)
}
)}
{/* ── Tab: Notifications ── */}
{activeTab === 'notifications' && (
{/* Pushover config */}
Pushover
Recevez une notification push sur vos appareils lors de tout changement d'état d'un conteneur.
{settingsError && (
{settingsError}
)}
{settingsLoading
? Chargement…
: (
{/* Toggle */}
{/* Credentials form */}
{pushoverMsg && (
{pushoverMsg.text}
)}
{pushoverSaving ? : }
Enregistrer
{testingNotif ? : }
Tester
{testResult && (
{testResult.text}
)}
)
}
{/* Container events */}
Historique des événements
{eventsTotal} changement{eventsTotal !== 1 ? 's' : ''} enregistré{eventsTotal !== 1 ? 's' : ''}
loadEvents(eventsPage)}
disabled={eventsLoading}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs bg-gray-800 hover:bg-gray-700 disabled:opacity-50 transition-colors"
>
Actualiser
{eventsError && (
{eventsError}
)}
{eventsLoading && events.length === 0
? Chargement…
: events.length === 0
? (
Aucun événement enregistré.
)
: (
Date / Heure
VPS
Conteneur
Avant
Après
{events.map(ev => (
{new Date(ev.ts).toLocaleString('fr-FR')}
{ev.vps_name}
{ev.container}
))}
)
}
{Math.ceil(eventsTotal / PAGE_SIZE) > 1 && (
loadEvents(eventsPage - 1)}
disabled={eventsPage === 0 || eventsLoading}
className="px-3 py-1.5 rounded-lg text-xs bg-gray-800 hover:bg-gray-700 disabled:opacity-40 transition-colors"
>
← Précédent
Page {eventsPage + 1} / {Math.ceil(eventsTotal / PAGE_SIZE)}
loadEvents(eventsPage + 1)}
disabled={eventsPage >= Math.ceil(eventsTotal / PAGE_SIZE) - 1 || eventsLoading}
className="px-3 py-1.5 rounded-lg text-xs bg-gray-800 hover:bg-gray-700 disabled:opacity-40 transition-colors"
>
Suivant →
)}
)}
{/* ── Tab: Passkeys ── */}
{activeTab === 'passkeys' && (
Passkeys enregistrées
Gérez les passkeys (TouchID / FaceID) de tous les utilisateurs.
Actualiser
{adminPasskeysError && (
{adminPasskeysError}
)}
{adminPasskeysLoading && adminPasskeys.length === 0
? Chargement…
: adminPasskeys.length === 0
? (
Aucune passkey enregistrée.
)
: (
Utilisateur
Appareil
Enregistrée le
{adminPasskeys.map(pk => (
{pk.username}
{pk.name}
{new Date(pk.created_at).toLocaleString('fr-FR')}
handleRevokePasskey(pk.credential_id)}
disabled={revokeLoading === pk.credential_id}
className="px-2 py-1 rounded-md text-xs text-red-400 hover:bg-red-950/50 disabled:opacity-50 transition-colors"
>
{revokeLoading === pk.credential_id ? '…' : 'Révoquer'}
))}
)
}
)}
{/* ── Tab: Connexions ── */}
{activeTab === 'logs' && (
{logsError && (
{logsError}
)}
{logsLoading && logs.length === 0
? Chargement…
: filteredLogs.length === 0
? Aucune entrée.
: (
Date / Heure
Utilisateur
Adresse IP
Résultat
Détail
{filteredLogs.map(log => (
{new Date(log.ts).toLocaleString('fr-FR')}
{log.username}
{log.ip}
{log.success
? (
Succès
) : (
Échec
)
}
{log.reason || '—'}
))}
)
}
{totalPages > 1 && (
loadLogs(logsPage - 1)}
disabled={logsPage === 0 || logsLoading}
className="px-3 py-1.5 rounded-lg text-xs bg-gray-800 hover:bg-gray-700 disabled:opacity-40 transition-colors"
>
← Précédent
Page {logsPage + 1} / {totalPages}
loadLogs(logsPage + 1)}
disabled={logsPage >= totalPages - 1 || logsLoading}
className="px-3 py-1.5 rounded-lg text-xs bg-gray-800 hover:bg-gray-700 disabled:opacity-40 transition-colors"
>
Suivant →
)}
)}
{/* ── Tab: Base de données ── */}
{activeTab === 'database' && (
{/* En-tête */}
Gestion de la base de données
Supprimez les données historiques par table et par période.
Actualiser
{dbInfoError && (
{dbInfoError}
)}
{purgeResult && (
{purgeResult}
)}
{purgeError && (
{purgeError}
)}
{/* Cards par table */}
{[
{ key: 'vps_stats', label: 'Statistiques VPS', icon:
},
{ key: 'login_logs', label: 'Logs de connexion', icon:
},
].map(({ key, label, icon }) => {
const info = dbInfo?.[key]
return (
{icon}
{label}
{info && (
{fmtCount(info.count)} entrée{info.count !== 1 ? 's' : ''}
{info.oldest_ts && ` · du ${fmtTs(info.oldest_ts)} au ${fmtTs(info.newest_ts)}`}
)}
{[
{ period: 'last_24h', label: '24 dernières heures' },
{ period: 'last_7d', label: '7 derniers jours' },
{ period: 'last_30d', label: '30 derniers jours' },
{ period: 'all', label: 'Tout effacer', danger: true },
].map(({ period, label: btnLabel, danger }) => (
requestPurge(key, period)}
disabled={purgeLoading || dbInfoLoading}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs transition-colors disabled:opacity-50 ${
danger
? 'bg-red-950/50 hover:bg-red-900/60 text-red-400 border border-red-800/50'
: 'bg-gray-800 hover:bg-gray-700 text-gray-300'
}`}
>
{btnLabel}
))}
)
})}
{/* Période personnalisée */}
Période personnalisée
Supprimez les données comprises entre deux dates précises.
)}
{/* ── Modale de confirmation de purge ── */}
{confirmState && (
Vous êtes sur le point de supprimer{' '}
{periodLabel[confirmState.period]} {' '}
dans {tableLabel[confirmState.table]} .
Cette action est irréversible.
>
}
confirmLabel={purgeLoading ? 'Suppression…' : 'Supprimer'}
loading={purgeLoading}
onConfirm={confirmPurge}
onCancel={() => setConfirmState(null)}
/>
)}