Planificateur « Suggérer » : 4 stratégies (smart / meilleurs d'abord / équilibré / juste ce qu'il faut), compétences+niveaux par tech (édition inline), niveau requis par compétence + par job, carte des tournées (1 couleur/tech, domicile→arrêts, sélecteur de jour), fenêtre de dispatch auj.+demain (dates sélectionnées), règle week-end + placeholder « en attente du quart », clustering + lasso + filtre-date sur la carte, accès rapide « À assigner » (badge). Boîte : liste /conversations allégée (45 Mo → ~1 Mo, 17×) + messages chargés à l'ouverture. Rapports : cache SWR sur revenue-explorer (22×). Session : keep-alive + timeout fetch global + authFetch durci → fin des rechargements manuels. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
51 lines
2.9 KiB
JavaScript
51 lines
2.9 KiB
JavaScript
'use strict'
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// Préférences d'AFFICHAGE par utilisateur (filtres, tri, densité, vues…), par
|
|
// ESPACE DE NOMS (namespace : 'inbox', 'planif', 'tickets', …). Stockage serveur
|
|
// keyé par courriel Authentik → suit l'usager entre appareils ET lisible par un
|
|
// admin (impersonation LECTURE SEULE, phase 3). Réutilise le patron notif-prefs.
|
|
//
|
|
// GET /prefs → { prefs: { <ns>: {…}, … }, user, impersonating }
|
|
// GET /prefs?as=<email> → (ADMIN seulement) les prefs d'un AUTRE usager (lecture seule)
|
|
// PUT /prefs { namespace, value } → remplace ce namespace par `value` (état complet)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
const { json, parseBody, readJsonFile, writeJsonFile } = require('./helpers')
|
|
|
|
const FILE = '/app/data/user_prefs.json'
|
|
const meOf = (req) => String(req.headers['x-authentik-email'] || '').toLowerCase()
|
|
|
|
// Garde admin pour l'impersonation (phase 3). x-authentik-groups est transmis par le proxy si configuré ; sinon → jamais admin
|
|
// (repli SÛR : pas de groupe = pas d'impersonation, on ne voit que ses propres prefs).
|
|
function isAdmin (req) {
|
|
const g = String(req.headers['x-authentik-groups'] || req.headers['x-authentik-group'] || '')
|
|
return /(^|[,|;])\s*(ops-?admin|admin(istrateurs?)?|superviseurs?)\s*([,|;]|$)/i.test(g)
|
|
}
|
|
|
|
async function handle (req, res, method, path) {
|
|
if (path !== '/prefs') return json(res, 404, { error: 'not found' })
|
|
const me = meOf(req)
|
|
const all = readJsonFile(FILE, {})
|
|
|
|
if (method === 'GET') {
|
|
const as = String(new URL(req.url, 'http://localhost').searchParams.get('as') || '').toLowerCase()
|
|
const target = (as && as !== me && isAdmin(req)) ? as : me // impersonation lecture seule réservée admin
|
|
return json(res, 200, { prefs: all[target] || {}, user: target, impersonating: !!(as && target === as && as !== me) })
|
|
}
|
|
|
|
if (method === 'PUT') {
|
|
if (!me) return json(res, 400, { error: 'utilisateur inconnu (x-authentik-email absent)' })
|
|
const b = await parseBody(req)
|
|
const ns = String((b && b.namespace) || '').trim()
|
|
if (!ns) return json(res, 400, { error: 'namespace requis' })
|
|
const value = (b && b.value && typeof b.value === 'object' && !Array.isArray(b.value)) ? b.value : {}
|
|
all[me] = all[me] || {}
|
|
all[me][ns] = value // le client envoie l'ÉTAT COMPLET du namespace → remplacement (pas de fusion partielle)
|
|
writeJsonFile(FILE, all)
|
|
return json(res, 200, { ok: true, namespace: ns, value })
|
|
}
|
|
|
|
return json(res, 405, { error: 'method not allowed' })
|
|
}
|
|
|
|
module.exports = { handle }
|