Gestion des Thèmes
This commit is contained in:
1 parent
bab7a79b63
commit
be3c93897b
24 files changed
+1041
-141
No files matched your search
+73
-7
@@ -22,11 +22,11 @@ const PENDING_RESTORE = DB_PATH + '.pending-restore';
|
||||
// générées côté dev (api_keys, y compris les clés internes auto-provisionnées
|
||||
// pour l'assistant IA), écrase la configuration des fournisseurs IA propres
|
||||
// à dev (ai_providers/ai_config) par celle de la source restaurée, et
|
||||
// écraserait de la même façon l'étiquette d'environnement de cette instance
|
||||
// (parametres_environnement — Admin > Général, cf. 05/09/26) par celle de la
|
||||
// source restaurée. Ordre important : ai_config référence ai_providers (FK
|
||||
// provider_defaut_id).
|
||||
const ENV_LOCAL_TABLES = ['api_keys', 'ai_providers', 'ai_config', 'parametres_environnement'];
|
||||
// écraserait de la même façon l'étiquette d'environnement (parametres_environnement)
|
||||
// et le thème de couleur (parametres_theme — Admin > Général, cf. 05/09/26)
|
||||
// de cette instance par ceux de la source restaurée. Ordre important :
|
||||
// ai_config référence ai_providers (FK provider_defaut_id).
|
||||
const ENV_LOCAL_TABLES = ['api_keys', 'ai_providers', 'ai_config', 'parametres_environnement', 'parametres_theme'];
|
||||
let pendingRestorePreserve = null;
|
||||
if (fs.existsSync(PENDING_RESTORE) && fs.existsSync(DB_PATH)) {
|
||||
try {
|
||||
@@ -42,7 +42,7 @@ if (fs.existsSync(PENDING_RESTORE) && fs.existsSync(DB_PATH)) {
|
||||
}
|
||||
oldDb.close();
|
||||
} catch (e) {
|
||||
console.error('[restore] Échec de la sauvegarde des tables locales (api_keys/ai_providers/ai_config/parametres_environnement) avant restauration :', e.message);
|
||||
console.error('[restore] Échec de la sauvegarde des tables locales (api_keys/ai_providers/ai_config/parametres_environnement/parametres_theme) avant restauration :', e.message);
|
||||
pendingRestorePreserve = null;
|
||||
}
|
||||
}
|
||||
@@ -1847,6 +1847,72 @@ db.exec(`
|
||||
`);
|
||||
db.exec(`INSERT OR IGNORE INTO parametres_environnement (id) VALUES (1)`);
|
||||
|
||||
// ── Migration : thème de couleur de l'interface (Admin > Général) ───────────
|
||||
// Demande Olivier 05/09/26 : teinte de l'interface (menus, boutons — distincte
|
||||
// du mode clair/sombre existant, cf. ThemeContext.jsx) choisie parmi une
|
||||
// liste de préréglages (frontend/src/styles.css, blocs [data-color-theme]),
|
||||
// appliquée à TOUS les utilisateurs (contrairement au mode clair/sombre, une
|
||||
// préférence par utilisateur). But d'Olivier : garder Production dans le
|
||||
// bleu par défaut mais donner aux autres environnements une couleur
|
||||
// différente, reconnaissable au premier coup d'œil. Table dédiée (même
|
||||
// raisonnement que parametres_environnement ci-dessus) et ajoutée à
|
||||
// ENV_LOCAL_TABLES : ne doit jamais être réécrite par une restauration.
|
||||
//
|
||||
// 05/09/26 (suite, même jour) : les 5 thèmes initiaux (bleu/vert/violet/
|
||||
// orange/bordeaux, couleurs choisies librement) sont remplacés par 10,
|
||||
// alignés sur les 10 palettes de graphiques de Settings > Apparence
|
||||
// (cf. project_chart_colors.md) — chaque thème reprend la TEINTE de la
|
||||
// couleur "Intérêts" de la palette correspondante (même méthode de rotation
|
||||
// que les 5 thèmes initiaux : S/L de chaque jeton CSS inchangés, seule la
|
||||
// teinte tourne). SQLite ne permet pas d'altérer un CHECK constraint : la
|
||||
// table doit être recréée, même stratégie que les migrations perte_definitive/
|
||||
// prolongation sur investissements plus loin dans ce fichier. Valeur existante
|
||||
// migrée : 'violet' reste 'violet' (même nom, teinte recalculée depuis la
|
||||
// palette) ; tout le reste (bleu/vert/orange/bordeaux) retombe sur 'classique'
|
||||
// — décision explicite d'Olivier pour son propre 'bordeaux' actif, généralisée
|
||||
// à toute valeur historique sans successeur direct parmi les 10 palettes.
|
||||
{
|
||||
const NEW_COLOR_THEMES = ['classique', 'indigo_teal', 'nuit', 'nature', 'coucher_de_soleil', 'violet', 'frais', 'contraste', 'pastel', 'moderne'];
|
||||
const schemaTheme = db.prepare(
|
||||
"SELECT sql FROM sqlite_master WHERE type='table' AND name='parametres_theme'"
|
||||
).get()?.sql ?? '';
|
||||
|
||||
if (schemaTheme && !schemaTheme.includes("'classique'")) {
|
||||
const oldValue = db.prepare('SELECT color_theme FROM parametres_theme WHERE id = 1').get()?.color_theme;
|
||||
const migratedValue = NEW_COLOR_THEMES.includes(oldValue) ? oldValue : 'classique';
|
||||
const tempName = '__repair_parametres_theme';
|
||||
|
||||
db.exec('PRAGMA foreign_keys = OFF');
|
||||
db.exec(`DROP TABLE IF EXISTS "${tempName}"`);
|
||||
db.exec(`
|
||||
CREATE TABLE "${tempName}" (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
color_theme TEXT NOT NULL DEFAULT 'classique'
|
||||
CHECK (color_theme IN ('classique', 'indigo_teal', 'nuit', 'nature', 'coucher_de_soleil', 'violet', 'frais', 'contraste', 'pastel', 'moderne')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
db.prepare(`INSERT INTO "${tempName}" (id, color_theme) VALUES (1, ?)`).run(migratedValue);
|
||||
db.exec('DROP TABLE parametres_theme');
|
||||
db.exec('PRAGMA legacy_alter_table = ON');
|
||||
db.exec(`ALTER TABLE "${tempName}" RENAME TO parametres_theme`);
|
||||
db.exec('PRAGMA legacy_alter_table = OFF');
|
||||
db.exec('PRAGMA foreign_keys = ON');
|
||||
|
||||
console.log(`[DB] parametres_theme : migré vers les 10 thèmes basés sur les palettes de graphiques (valeur existante '${oldValue}' → '${migratedValue}')`);
|
||||
} else {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS parametres_theme (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
color_theme TEXT NOT NULL DEFAULT 'classique'
|
||||
CHECK (color_theme IN ('classique', 'indigo_teal', 'nuit', 'nature', 'coucher_de_soleil', 'violet', 'frais', 'contraste', 'pastel', 'moderne')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
db.exec(`INSERT OR IGNORE INTO parametres_theme (id) VALUES (1)`);
|
||||
|
||||
// ── Migration : email_verified sur users ─────────────────────────────────────
|
||||
{
|
||||
const userCols = db.prepare('PRAGMA table_info(users)').all().map(c => c.name);
|
||||
@@ -2881,7 +2947,7 @@ if (pendingRestorePreserve) {
|
||||
}
|
||||
});
|
||||
restoreEnvLocal();
|
||||
console.log('[restore] Tables locales (api_keys/ai_providers/ai_config/parametres_environnement) préservées et réappliquées après restauration.');
|
||||
console.log('[restore] Tables locales (api_keys/ai_providers/ai_config/parametres_environnement/parametres_theme) préservées et réappliquées après restauration.');
|
||||
} catch (e) {
|
||||
console.error('[restore] Échec de la réapplication des tables locales préservées :', e.message);
|
||||
}
|
||||
|
||||
@@ -27,9 +27,11 @@ function ensureRow() {
|
||||
// colonne applique automatiquement son DEFAULT (5 Go / 500 documents,
|
||||
// cf. migration dans db/index.js) quand elle est omise d'un INSERT.
|
||||
}
|
||||
// parametres_environnement : normalement déjà créée avec sa ligne par défaut
|
||||
// par la migration dans db/index.js (INSERT OR IGNORE) — filet de sécurité ici.
|
||||
// parametres_environnement / parametres_theme : normalement déjà créées avec
|
||||
// leur ligne par défaut par la migration dans db/index.js (INSERT OR IGNORE)
|
||||
// — filet de sécurité ici.
|
||||
db.prepare('INSERT OR IGNORE INTO parametres_environnement (id) VALUES (1)').run();
|
||||
db.prepare('INSERT OR IGNORE INTO parametres_theme (id) VALUES (1)').run();
|
||||
}
|
||||
|
||||
// 1 Go = 1024^3 octets (convention déjà utilisée par fmtOctets côté frontend).
|
||||
@@ -40,10 +42,12 @@ router.get('/', (_req, res, next) => {
|
||||
ensureRow();
|
||||
const row = db.prepare('SELECT app_name, app_url, mcp_url, allow_registration, min_password_length, documents_quota_bytes, documents_quota_count FROM smtp_config WHERE id = 1').get();
|
||||
const envRow = db.prepare('SELECT environnement FROM parametres_environnement WHERE id = 1').get();
|
||||
const themeRow = db.prepare('SELECT color_theme FROM parametres_theme WHERE id = 1').get();
|
||||
res.json({
|
||||
appName: row.app_name || 'Crowdlending Tracker',
|
||||
appUrl: row.app_url || '',
|
||||
environnement: envRow?.environnement || 'developpement',
|
||||
colorTheme: themeRow?.color_theme || 'classique',
|
||||
mcpUrl: row.mcp_url || '',
|
||||
allowRegistration: row.allow_registration !== 0,
|
||||
minPasswordLength: row.min_password_length || 8,
|
||||
@@ -57,6 +61,7 @@ const PatchSchema = z.object({
|
||||
appName: z.string().min(1).max(100).optional(),
|
||||
appUrl: z.string().max(500).optional(),
|
||||
environnement: z.enum(['production', 'qualification', 'developpement']).optional(),
|
||||
colorTheme: z.enum(['classique', 'indigo_teal', 'nuit', 'nature', 'coucher_de_soleil', 'violet', 'frais', 'contraste', 'pastel', 'moderne']).optional(),
|
||||
mcpUrl: z.string().max(500).optional(),
|
||||
allowRegistration: z.boolean().optional(),
|
||||
minPasswordLength: z.number().int().min(6).max(64).optional(),
|
||||
@@ -93,6 +98,9 @@ router.patch('/', (req, res, next) => {
|
||||
if (body.environnement !== undefined) {
|
||||
db.prepare('UPDATE parametres_environnement SET environnement = ? WHERE id = 1').run(body.environnement);
|
||||
}
|
||||
if (body.colorTheme !== undefined) {
|
||||
db.prepare('UPDATE parametres_theme SET color_theme = ? WHERE id = 1').run(body.colorTheme);
|
||||
}
|
||||
|
||||
res.json({ ok: true });
|
||||
} catch (e) { next(e); }
|
||||
|
||||
@@ -10,7 +10,18 @@ const ALLOWED_KEYS = new Set([
|
||||
'chart_cashback',
|
||||
'pfo_assujetti',
|
||||
'objectifs_actifs',
|
||||
// Extensible : ajouter ici les futures prefs (theme, font_scale, langue, devise, display_mode…)
|
||||
'color_theme',
|
||||
// Extensible : ajouter ici les futures prefs (font_scale, langue, devise, display_mode…)
|
||||
]);
|
||||
|
||||
// Thème de couleur (Admin > Général, cf. project_theme_interface.md) — même
|
||||
// énumération que la colonne parametres_theme.color_theme côté admin.
|
||||
// Contrairement aux autres clés de ALLOWED_KEYS, la valeur est validée ici
|
||||
// (pas seulement la clé) car une valeur invalide casserait silencieusement
|
||||
// le rendu CSS ([data-color-theme] côté frontend n'a pas de fallback).
|
||||
const COLOR_THEME_VALUES = new Set([
|
||||
'classique', 'indigo_teal', 'nuit', 'nature', 'coucher_de_soleil',
|
||||
'violet', 'frais', 'contraste', 'pastel', 'moderne',
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -51,6 +62,10 @@ router.patch('/', (req, res) => {
|
||||
return res.status(400).json({ error: `Clés inconnues : ${unknown.join(', ')}` });
|
||||
}
|
||||
|
||||
if ('color_theme' in payload && !COLOR_THEME_VALUES.has(payload.color_theme)) {
|
||||
return res.status(400).json({ error: `Valeur invalide pour color_theme : ${payload.color_theme}` });
|
||||
}
|
||||
|
||||
const persist = db.transaction(() => {
|
||||
for (const [key, value] of Object.entries(payload)) {
|
||||
upsert.run(req.user.id, key, String(value));
|
||||
@@ -61,4 +76,21 @@ router.patch('/', (req, res) => {
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/preferences/:key
|
||||
* Supprime la préférence de l'utilisateur connecté pour une clé donnée —
|
||||
* utile pour revenir à un comportement "par défaut" (ex. "Automatique"
|
||||
* pour le thème de couleur, cf. Settings > Apparence) plutôt que de
|
||||
* stocker une valeur sentinelle : une clé absente n'est simplement plus
|
||||
* retournée par GET (le frontend retombe alors sur sa valeur par défaut).
|
||||
*/
|
||||
router.delete('/:key', (req, res) => {
|
||||
const { key } = req.params;
|
||||
if (!ALLOWED_KEYS.has(key)) {
|
||||
return res.status(400).json({ error: `Clé inconnue : ${key}` });
|
||||
}
|
||||
db.prepare('DELETE FROM user_preferences WHERE user_id = ? AND key = ?').run(req.user.id, key);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -96,18 +96,23 @@ app.get('/api/health', (_, res) => res.json({ ok: true, ts: new Date().toISOStri
|
||||
// Informations publiques de la plateforme (utilisées sur la page de login)
|
||||
app.get('/api/app-info', (_, res) => {
|
||||
try {
|
||||
const cfg = getSmtpConfig();
|
||||
const icon = db.prepare(`SELECT filename FROM app_icons WHERE name = 'logo-app' LIMIT 1`).get();
|
||||
const row = db.prepare('SELECT allow_registration, min_password_length, mcp_url FROM smtp_config WHERE id = 1').get();
|
||||
const cfg = getSmtpConfig();
|
||||
const icon = db.prepare(`SELECT filename FROM app_icons WHERE name = 'logo-app' LIMIT 1`).get();
|
||||
const row = db.prepare('SELECT allow_registration, min_password_length, mcp_url FROM smtp_config WHERE id = 1').get();
|
||||
// Thème de couleur (Admin > Général) : appliqué à TOUS les utilisateurs,
|
||||
// y compris sur la page de connexion (avant authentification) — d'où son
|
||||
// exposition ici, comme allowRegistration/mcpUrl déjà publics.
|
||||
const theme = db.prepare('SELECT color_theme FROM parametres_theme WHERE id = 1').get();
|
||||
res.json({
|
||||
appName: cfg.appName || 'Crowdlending Tracker',
|
||||
iconUrl: icon ? `/api/icons-files/${icon.filename}` : null,
|
||||
allowRegistration: row ? row.allow_registration !== 0 : true,
|
||||
minPasswordLength: row ? (row.min_password_length || 8) : 8,
|
||||
mcpUrl: row ? (row.mcp_url || '') : '',
|
||||
colorTheme: theme?.color_theme || 'classique',
|
||||
});
|
||||
} catch {
|
||||
res.json({ appName: 'Crowdlending Tracker', iconUrl: null, allowRegistration: true, minPasswordLength: 8, mcpUrl: '' });
|
||||
res.json({ appName: 'Crowdlending Tracker', iconUrl: null, allowRegistration: true, minPasswordLength: 8, mcpUrl: '', colorTheme: 'classique' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user