diff --git a/MEMORY.md b/MEMORY.md
index ec81d12..1a3738f 100644
--- a/MEMORY.md
+++ b/MEMORY.md
@@ -1,5 +1,5 @@
# MEMORY.md — Crowdlending Tracker
-*Dernière mise à jour: 2026-05-09 (session 6)*
+*Dernière mise à jour: 2026-06-15 (session 7)*
---
@@ -382,3 +382,46 @@ const isBonus = BONUS_VALUES.includes(form.investissement_id);
7. **Modal + dropdown** : ne jamais utiliser `position: absolute` pour un dropdown dans une modale — utiliser `position: fixed` + `getBoundingClientRect()` (cf. `CategorySelect.jsx`).
8. **Suppression dans modale** : ne pas chaîner `.then(() => closeModal())` sur une fonction qui appelle `confirm()` — inliner la logique avec early return si `!confirm(...)` pour éviter la fermeture sur "Annuler".
9. **Affichage investisseur** : toujours `memberLabel(inv)` — jamais `inv.prenom + ' ' + inv.nom` car `nom` contient déjà le nom complet.
+
+---
+
+## Session 7 — Auth, sécurité, section Admin Général (2026-06-15)
+
+### Système d'audit logs
+- Table `audit_logs` (id, actor_id, target_user_id, action, category, details JSON, ip_address, user_agent, created_at)
+- Util `backend/src/utils/audit.js` : `audit(req, { action, category, actorId, targetUserId, details })` — silencieux, ne throw jamais
+- Route `GET /api/admin/audit-logs` (+ `/categories`) avec filtres page/limit/category/search/dateFrom/dateTo/userId, purge auto 30 jours
+- Instrumenté dans : `auth.js` (register, login_failed, login_success, 2fa), `admin.js` (user_created, status_changed, role_changed, user_deleted), `invitations.js` (invitation_sent, invitation_accepted)
+- Frontend : `AuditLogsSection.jsx` — badges catégories cliquables, recherche 350ms debounce, plage dates, pagination 50/page, `DetailTooltip` JSON au survol
+
+### Bug focus-loss sur InvitationRegister
+- **Cause** : composant `Wrap` défini à l'intérieur du corps de `InvitationRegister()` → new component type à chaque render → React démonte/remonte tous les enfants → perte de focus à chaque frappe
+- **Fix** : déplacer `Wrap` en dehors de la fonction, passer `appInfo` en prop
+- **Règle** : ne jamais définir un composant React à l'intérieur d'un autre composant
+
+### Indicateur de complexité mot de passe (PasswordStrength.jsx)
+- Composant `frontend/src/components/PasswordStrength.jsx`
+- Props : `password: string`, `minLength: number = 8`
+- 5 règles : len (≥ minLength), upper, lower, digit, special
+- 6 niveaux visuels : Très faible → Très fort (couleurs fixes)
+- Règle `len` dynamique via `buildRules(minLength)` appelé dans le composant
+- Déployé sur 5 écrans : Register, ResetPassword, InvitationRegister, MonCompte (SecurityForm), UsersSection (CreateUserModal)
+
+### Section Admin "Général" — paramètres globaux
+- Route backend `GET/PATCH /api/admin/general` → `backend/src/routes/general.js`
+- Colonnes DB ajoutées sur `smtp_config` : `allow_registration INTEGER DEFAULT 1`, `min_password_length INTEGER DEFAULT 8`
+- Route publique `/api/app-info` enrichie : expose `allowRegistration` et `minPasswordLength` (sans auth)
+- `SmtpSection.jsx` : champs appName/appUrl **retirés** (déplacés dans Général)
+- `GeneralSection.jsx` : 3 blocs — Identité (appName, appUrl), Accès (toggle auto-inscription), Sécurité (longueur min MDP)
+- `Admin.jsx` : section "Général" ajoutée en tête du groupe "Administration de la plateforme"
+
+### Contrôle auto-inscription
+- `App.jsx` : route `/register` → `` si `allowRegistration=false`
+- `Login.jsx` : lien "Créer un compte" conditionnel sur `appInfo.allowRegistration !== false`
+- Fetch `/api/app-info` au montage dans App.jsx, état initial `allowRegistration: true` (jamais bloquant par défaut)
+
+### minPasswordLength — propagation
+- Chaque écran password fetch `/api/app-info` au montage et extrait `minPasswordLength`
+- `useState(8)` comme valeur initiale sur tous les écrans → jamais bloquant si l'API est lente
+- Register/ResetPassword/InvitationRegister : via `appInfo.minPasswordLength || 8` (appInfo déjà fetché)
+- MonCompte (SecurityForm) et UsersSection (CreateUserModal) : state local `minPasswordLength` + useEffect fetch dédié
diff --git a/frontend/src/components/EmptyState.jsx b/frontend/src/components/EmptyState.jsx
new file mode 100644
index 0000000..ba355b5
--- /dev/null
+++ b/frontend/src/components/EmptyState.jsx
@@ -0,0 +1,67 @@
+import { useNavigate } from 'react-router-dom';
+
+/**
+ * État vide — affiché quand aucune plateforme n'est configurée.
+ * Props :
+ * icon — emoji ou caractère affiché en grand (défaut : '🏦')
+ * title — titre du message (défaut : 'Aucune plateforme configurée')
+ * message — texte explicatif (défaut : message générique plateformes)
+ * cta — libellé du bouton (défaut : 'Configurer mes plateformes')
+ * to — route cible du bouton (défaut : '/settings?section=plateformes')
+ */
+export default function EmptyState({
+ icon = '🏦',
+ title = 'Aucune plateforme configurée',
+ message = "Vous n'avez pas encore configuré de plateforme de crowdfunding sur votre compte. Associez au moins une plateforme pour commencer à suivre vos investissements.",
+ cta = 'Configurer mes plateformes',
+ to = '/settings?section=plateformes',
+}) {
+ const navigate = useNavigate();
+
+ return (
+
+
{icon}
+
+ {title}
+
+
+ {message}
+
+
+
+ );
+}
diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx
index 2a1bd08..d34b6f0 100644
--- a/frontend/src/pages/Dashboard.jsx
+++ b/frontend/src/pages/Dashboard.jsx
@@ -10,6 +10,7 @@ import InteretsDonutChart from '../components/InteretsDonutChart.jsx';
import { InteretsChartProvider, useInteretsChart } from '../context/InteretsChartContext.jsx';
import TableauInteretsPlateforme from '../components/TableauInteretsPlateforme.jsx';
import DrillCellPanel from '../components/DrillCellPanel.jsx';
+import EmptyState from '../components/EmptyState.jsx';
/* ── Sélecteur d'année — doit être enfant de InteretsChartProvider ── */
function YearSelectorKpi() {
@@ -422,6 +423,7 @@ export default function Dashboard() {
const [loading, setLoading] = useState(false);
const [capitalMensuelData, setCapitalMensuelData] = useState([]);
const [plateformes, setPlateformes] = useState([]);
+ const [platLoaded, setPlatLoaded] = useState(false);
/* ── drillCell : cellule sélectionnée dans le TIP — par défaut mois courant toutes plateformes ── */
const _now = new Date();
@@ -461,7 +463,7 @@ export default function Dashboard() {
useEffect(() => {
api.get('/pfu').then(setPfuRates).catch(() => {});
- api.get('/plateformes').then(setPlateformes).catch(() => {});
+ api.get('/plateformes').then(d => { setPlateformes(d); setPlatLoaded(true); }).catch(() => { setPlatLoaded(true); });
}, []);
useEffect(() => {
@@ -475,6 +477,12 @@ export default function Dashboard() {
const ready = activeView === 'all' ? investisseurs.length > 0 : !!activeId;
if (!ready) return