v3 Control Center — refonte UI desktop-first (sidebar+topbar+dark mode+Vue d'ensemble) + analytics v2 trafic humain (classification UA/IP datacenter-bots-interne, sessions, heartbeat, temps réel, anomalies, inspecteur, config)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8 changed files +1,777 −435
modified
CLAUDE.md
+13 −0
@@ -47,3 +47,16 @@ node server/set-password.js '<nouveau>' # changer le mot de passe | |||||
| 47 | ``` | 47 | ``` | ||
| 48 | 48 | ||||
| 49 | Registre des déploiements : `~/Desktop/cluster-skill/cluster-deployments.json` (laptop, source de vérité). Repo : spbgit `~/srv/git/admin-ka.git`. | 49 | Registre des déploiements : `~/Desktop/cluster-skill/cluster-deployments.json` (laptop, source de vérité). Repo : spbgit `~/srv/git/admin-ka.git`. | ||
| 50 | + | ||||
| 51 | +## v3 (2026-08-26) — Control Center : refonte UI + analytics humains | ||||
| 52 | + | ||||
| 53 | +**UI** : desktop-first — sidebar gauche (≥1020 px) + topbar (titre, incidents, thème, statut) ; bottom nav mobile réduite à 5 (Vue d'ensemble, Claude Code, Écosystème, Visiteurs, Plus). Nouvelle page **Vue d'ensemble** (KPIs humains+éco+Claude, chart 30 j, chips écosystème, incidents, anomalies). **Dark mode** réel (`ka_theme` auto/clair/sombre, variables `--edge/--shadow/--code-bg/...`). Sessions et Prompts en listes denses filtrables (recherche/app/statut ; favoris ★, duplication, compteur d'usage sur les prompts). | ||||
| 54 | + | ||||
| 55 | +**Analytics v2 (`server/analytics.js`) — définitions officielles** : | ||||
| 56 | +- **Visiteur humain** : vid classé `human` = UA navigateur + JS exécuté + réseau non-datacenter/proxy + pas interne + pas de flood. | ||||
| 57 | +- **Visiteur unique** : vid humain distinct dans la fenêtre. **Session** : trou > 30 min = nouvelle session (`sessionMin`). **Page vue** : événement `pv` (navigation réelle, SPA incluse) — un heartbeat n'est JAMAIS une page vue. **En ligne** : dernier événement humain < 120 s (`activeSec`) — expire tout seul. | ||||
| 58 | +- **Classes** : `human / crawler / datacenter / automation / internal` — classées à l'insertion (UA regex + cache `ipinfo` ip-api avec `hosting/proxy/ASN`, reclassement rétroactif quand l'IP se résout, anti-flood `maxIpPerHour`). Les métriques principales n'affichent QUE les humains ; le reste est visible dans la barre de répartition et le toggle « Tout le trafic ». | ||||
| 59 | +- Tables : `events` (brut, rétention 45 j), `ipinfo` (cache IP-intelligence), `daily2` (rollup quotidien par classe, long terme), `meta.migrated_at` (frontière legacy). Les anciennes `hits/daily` sont CONSERVÉES et affichées en pointillé « ancien comptage v1 (non filtré) » — ne jamais comparer directement les deux séries. | ||||
| 60 | +- **Beacon v2** : `public/ka-a.js` (vid/sid localStorage, pv SPA via pushState, heartbeat 25 s seulement onglet visible + interaction < 60 s). Le beacon v1 (ping 60 s) reste supporté : le serveur dé-duplique (même page ~60 s = heartbeat) et sessionne en mémoire. | ||||
| 61 | +- API : `/api/analytics/{summary,series/:site|_all,site/:site,realtime,anomalies,inspect?ip=|vid=,classes,config(GET/PUT)}`. Config runtime dans `config.json → analytics` (fenêtres, IP/UA internes, bots custom, ASNs exclus) — éditable dans Réglages. | ||||
| 62 | +- Tests : `node server/test-analytics.mjs` (28 scénarios : humain/refresh/SPA, Googlebot, uptime bot, IP AWS, curl, interne, dédup v1, invariants uniques≤sessions≤pv, expiration en ligne) — à lancer depuis une COPIE (écrit dans data/). | ||||
modified
public/app.js
+530 −120
@@ -50,96 +50,392 @@ | |||||
| 50 | } | 50 | } | ||
| 51 | function fmtMB(b) { return b ? Math.round(b / 1048576) + ' Mo' : '—'; } | 51 | function fmtMB(b) { return b ? Math.round(b / 1048576) + ' Mo' : '—'; } | ||
| 52 | 52 | ||||
| 53 | − // ---------- navigation ---------- | 53 | + // ---------- navigation (sidebar desktop + bottom nav mobile) ---------- | ||
| 54 | − const views = ['chat', 'sessions', 'prompts', 'eco', 'social', 'visitors', 'settings']; | 54 | + const I = { | ||
| 55 | + home: '<svg viewBox="0 0 24 24"><path d="M4 11l8-7 8 7v9a1.5 1.5 0 01-1.5 1.5H15v-6h-6v6H5.5A1.5 1.5 0 014 20z" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linejoin="round"/></svg>', | ||||
| 56 | + chat: '<svg viewBox="0 0 24 24"><path d="M4 6a3 3 0 013-3h10a3 3 0 013 3v8a3 3 0 01-3 3H9l-4 4v-4a3 3 0 01-1-2.2z" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linejoin="round"/></svg>', | ||||
| 57 | + sessions: '<svg viewBox="0 0 24 24"><path d="M5 5h14M5 12h14M5 19h9" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>', | ||||
| 58 | + prompts: '<svg viewBox="0 0 24 24"><path d="M7 3h10a2 2 0 012 2v16l-7-4-7 4V5a2 2 0 012-2z" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linejoin="round"/></svg>', | ||||
| 59 | + eco: '<svg viewBox="0 0 24 24"><path d="M4 17l4-6 4 3 4-8 4 5" stroke="currentColor" stroke-width="1.9" fill="none" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 21h18" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>', | ||||
| 60 | + visitors: '<svg viewBox="0 0 24 24"><circle cx="9" cy="8" r="3.2" stroke="currentColor" stroke-width="1.8" fill="none"/><path d="M3.5 19a5.5 5.5 0 0111 0" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round"/><path d="M16 5.2a3 3 0 010 5.6M18 19a5.5 5.5 0 00-3-4.9" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round"/></svg>', | ||||
| 61 | + social: '<svg viewBox="0 0 24 24"><path d="M4 5h16v11H4z M8 20h8" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 8l5 2.5-5 2.5z" fill="currentColor"/></svg>', | ||||
| 62 | + settings: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3.2" stroke="currentColor" stroke-width="1.8" fill="none"/><path d="M12 2.8v3M12 18.2v3M2.8 12h3M18.2 12h3M5.5 5.5l2.1 2.1M16.4 16.4l2.1 2.1M18.5 5.5l-2.1 2.1M7.6 16.4l-2.1 2.1" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>', | ||||
| 63 | + more: '<svg viewBox="0 0 24 24"><circle cx="5" cy="12" r="1.8" fill="currentColor"/><circle cx="12" cy="12" r="1.8" fill="currentColor"/><circle cx="19" cy="12" r="1.8" fill="currentColor"/></svg>', | ||||
| 64 | + }; | ||||
| 65 | + const NAV = [ | ||||
| 66 | + { id: 'overview', label: 'Vue d’ensemble', icon: I.home }, | ||||
| 67 | + { id: 'chat', label: 'Claude Code', icon: I.chat }, | ||||
| 68 | + { id: 'sessions', label: 'Sessions', icon: I.sessions }, | ||||
| 69 | + { id: 'prompts', label: 'Prompts', icon: I.prompts }, | ||||
| 70 | + { id: 'eco', label: 'Écosystème', icon: I.eco }, | ||||
| 71 | + { id: 'visitors', label: 'Visiteurs', icon: I.visitors }, | ||||
| 72 | + { id: 'social', label: 'Studio', icon: I.social }, | ||||
| 73 | + { id: 'settings', label: 'Réglages', icon: I.settings }, | ||||
| 74 | + ]; | ||||
| 75 | + const views = NAV.map(n => n.id); | ||||
| 76 | + const TITLES = Object.fromEntries(NAV.map(n => [n.id, n.label])); | ||||
| 77 | + function navBtnHTML(n) { | ||||
| 78 | + return `<button data-view="${n.id}" class="nav-btn">${n.icon}<span>${n.label}</span><i class="badge hidden"></i></button>`; | ||||
| 79 | + } | ||||
| 80 | + $('sideNav').innerHTML = NAV.map(navBtnHTML).join(''); | ||||
| 81 | + const MOBILE_NAV = ['overview', 'chat', 'eco', 'visitors']; | ||||
| 82 | + $('bottomNav').innerHTML = NAV.filter(n => MOBILE_NAV.includes(n.id)).map(navBtnHTML).join('') | ||||
| 83 | + + `<button class="nav-btn" id="navMore">${I.more}<span>Plus</span><i class="badge hidden"></i></button>`; | ||||
| 84 | + $('navMore').onclick = () => { | ||||
| 85 | + openSheet(`<div class="sheet-h"><h3>Navigation</h3></div><div class="more-nav">${ | ||||
| 86 | + NAV.filter(n => !MOBILE_NAV.includes(n.id)).map(n => `<button class="more-btn" data-view="${n.id}">${n.icon}<span>${n.label}</span></button>`).join('') | ||||
| 87 | + }</div>`); | ||||
| 88 | + document.querySelectorAll('.more-btn').forEach(b => { b.onclick = () => { closeSheet(); switchView(b.dataset.view); }; }); | ||||
| 89 | + }; | ||||
| 90 | + | ||||
| 55 | function switchView(name) { | 91 | function switchView(name) { | ||
| 56 | curView = name; | 92 | curView = name; | ||
| 57 | for (const v of views) $('view-' + v).classList.toggle('hidden', v !== name); | 93 | for (const v of views) $('view-' + v).classList.toggle('hidden', v !== name); | ||
| 94 | + const pt = $('pageTitle'); if (pt) pt.textContent = TITLES[name] || name; | ||||
| 58 | document.querySelectorAll('.nav-btn').forEach((b) => { | 95 | document.querySelectorAll('.nav-btn').forEach((b) => { | ||
| 59 | b.classList.toggle('active', b.dataset.view === name); | 96 | b.classList.toggle('active', b.dataset.view === name); | ||
| 60 | − if (b.dataset.view === name) b.querySelector('.badge').classList.add('hidden'); | 97 | + if (b.dataset.view === name) { const bd = b.querySelector('.badge'); if (bd) bd.classList.add('hidden'); } | ||
| 61 | }); | 98 | }); | ||
| 99 | + if (name === 'overview' && window.startOverview) window.startOverview(); | ||||
| 62 | if (name === 'sessions') loadSessions(); | 100 | if (name === 'sessions') loadSessions(); | ||
| 63 | if (name === 'prompts') loadPrompts(); | 101 | if (name === 'prompts') loadPrompts(); | ||
| 64 | if (name === 'eco') { loadEco(); send({ type: 'eco_sub' }); } else { send({ type: 'eco_unsub' }); } | 102 | if (name === 'eco') { loadEco(); send({ type: 'eco_sub' }); } else { send({ type: 'eco_unsub' }); } | ||
| 65 | if (name === 'social' && window.__loadGallery) window.__loadGallery(); | 103 | if (name === 'social' && window.__loadGallery) window.__loadGallery(); | ||
| 66 | if (name === 'visitors' && window.startVisitors) window.startVisitors(); | 104 | if (name === 'visitors' && window.startVisitors) window.startVisitors(); | ||
| 67 | − if (name === 'settings') refreshSettingsView(); | 105 | + if (name === 'settings') { refreshSettingsView(); if (window.loadAnCfg) window.loadAnCfg(); } | ||
| 68 | } | 106 | } | ||
| 69 | − document.querySelectorAll('.nav-btn').forEach((b) => { b.onclick = () => switchView(b.dataset.view); }); | 107 | + document.addEventListener('click', (e) => { | ||
| 108 | + const nb = e.target.closest('.nav-btn[data-view]'); | ||||
| 109 | + if (nb) { switchView(nb.dataset.view); return; } | ||||
| 110 | + const gl = e.target.closest('[data-goto]'); | ||||
| 111 | + if (gl) switchView(gl.dataset.goto); | ||||
| 112 | + }); | ||||
| 70 | 113 | ||||
| 71 | −// ===== VISITORS_TAB ===== | 114 | + // ---------- thème (auto / clair / sombre) ---------- | ||
| 72 | −(function(){ | 115 | + function applyTheme() { | ||
| 116 | + const t = localStorage.getItem('ka_theme') || 'auto'; | ||||
| 117 | + const dark = t === 'dark' || (t === 'auto' && matchMedia('(prefers-color-scheme: dark)').matches); | ||||
| 118 | + document.documentElement.dataset.theme = dark ? 'dark' : 'light'; | ||||
| 119 | + const meta = document.querySelector('meta[name="theme-color"]'); | ||||
| 120 | + if (meta) meta.content = dark ? '#16130e' : '#F2F1EC'; | ||||
| 121 | + document.querySelectorAll('#segTheme button').forEach(b => b.classList.toggle('on', b.dataset.v === t)); | ||||
| 122 | + } | ||||
| 123 | + document.querySelectorAll('#segTheme button').forEach(b => { b.onclick = () => { localStorage.setItem('ka_theme', b.dataset.v); applyTheme(); }; }); | ||||
| 124 | + $('themeBtn').onclick = () => { | ||||
| 125 | + const cur = document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light'; | ||||
| 126 | + localStorage.setItem('ka_theme', cur === 'dark' ? 'light' : 'dark'); | ||||
| 127 | + applyTheme(); | ||||
| 128 | + }; | ||||
| 129 | + matchMedia('(prefers-color-scheme: dark)').addEventListener('change', applyTheme); | ||||
| 130 | + applyTheme(); | ||||
| 131 | + | ||||
| 132 | +// ===== VISITEURS v2 — trafic humain crédible ===== | ||||
| 133 | +(function () { | ||||
| 73 | const $ = (id) => document.getElementById(id); | 134 | const $ = (id) => document.getElementById(id); | ||
| 74 | − let timer=null; | 135 | + let timer = null, mode = 'human', lastSummary = null; | ||
| 75 | − function nf(n){ return (n||0).toLocaleString('fr-CA').replace(/,/g,' '); } | 136 | + const nf = (n) => (n || 0).toLocaleString('fr-CA').replace(/,/g, ' '); | ||
| 76 | − function card(s){ | 137 | + const DEV_LABEL = { m: '📱 mobile', t: '💻 tablette', d: '🖥️ desktop', '?': '—' }; | ||
| 77 | − return '<div class="vis-card" data-site="'+s.site+'" data-label="'+(s.label||s.site)+'">' | 138 | + const CLS_LABEL = { human: 'Humains', crawler: 'Crawlers', datacenter: 'Datacenter', automation: 'Automation', internal: 'Interne' }; | ||
| 78 | − + '<div class="vis-h"><img src="/icons/'+s.site+'"><div class="vis-name">'+(s.label||s.site)+'</div><span class="vis-dot'+(s.online>0?' on':'')+'"></span></div>' | 139 | + const CLS_COLOR = { human: 'var(--good)', crawler: '#6a9fd8', datacenter: 'var(--warn)', automation: '#a08cc9', internal: 'var(--muted)' }; | ||
| 79 | − + '<div class="vis-online"><span class="vis-dot big'+(s.online>0?' on':'')+'"></span><span class="vis-onN">'+nf(s.online)+'</span><span class="vis-onL">en ligne</span></div>' | 140 | + | ||
| 80 | − + '<div class="vis-row">' | 141 | + function delta(cur, prev) { | ||
| 81 | − + '<div class="vis-m"><div class="vm-n">'+nf(s.today)+'</div><div class="vm-l">jour</div></div>' | 142 | + if (!prev) return ''; | ||
| 82 | − + '<div class="vis-m"><div class="vm-n">'+nf(s.d7)+'</div><div class="vm-l">7j</div></div>' | 143 | + const d = Math.round((cur - prev) / prev * 100); | ||
| 83 | − + '<div class="vis-m"><div class="vm-n">'+nf(s.d30)+'</div><div class="vm-l">30j</div></div>' | 144 | + if (!isFinite(d) || Math.abs(d) < 1) return '<span class="kdelta flat">≈ hier</span>'; | ||
| 84 | − + '</div>' | 145 | + return `<span class="kdelta ${d > 0 ? 'up' : 'down'}">${d > 0 ? '+' : ''}${d} % vs hier</span>`; | ||
| 85 | − + '<div class="vis-pv">'+nf(s.pv_today)+' pages vues · voir détail →</div></div>'; | 146 | + } | ||
| 86 | − } | 147 | + function kpi(v, l, extra, accent) { | ||
| 87 | − async function loadVisitors(){ | 148 | + return `<div class="kpi2${accent ? ' accent' : ''}"><div class="kv2">${v}</div><div class="kl2">${l}</div>${extra || ''}</div>`; | ||
| 88 | − try{ | 149 | + } | ||
| 89 | − const d=await (await fetch('/api/analytics/summary')).json(); const g=d.global||{}; | 150 | + function renderGlobal(d) { | ||
| 90 | − if($('visLive')) $('visLive').innerHTML='<span class="vis-dot on"></span> '+nf(g.online)+' en ligne · '+nf(g.today)+' aujourd'hui'; | 151 | + const g = d.global || {}; | ||
| 91 | − if($('visGlobal')) $('visGlobal').innerHTML= | 152 | + $('visLive').innerHTML = '<span class="vis-dot on"></span> ' + nf(g.online) + ' humain' + (g.online > 1 ? 's' : '') + ' en ligne'; | ||
| 92 | − '<div class="vg-item"><div class="vg-n">'+nf(g.online)+'</div><div class="vg-l">en ligne</div></div>' | 153 | + const all = mode === 'all'; | ||
| 93 | − +'<div class="vg-item"><div class="vg-n">'+nf(g.today)+'</div><div class="vg-l">aujourd'hui</div></div>' | 154 | + const autoHits = (g.crawler || 0) + (g.datacenter || 0) + (g.automation || 0) + (g.internal || 0); | ||
| 94 | − +'<div class="vg-item"><div class="vg-n">'+nf(g.d7)+'</div><div class="vg-l">7 jours</div></div>' | 155 | + $('visGlobal').innerHTML = | ||
| 95 | − +'<div class="vg-item"><div class="vg-n">'+nf(g.pv_today)+'</div><div class="vg-l">pages vues</div></div>'; | 156 | + kpi(nf(g.online), 'en ligne (actifs < 2 min)', '', true) | ||
| 96 | − if($('visGrid')){ $('visGrid').innerHTML=(d.sites||[]).map(card).join(''); | 157 | + + kpi(nf(g.today), 'humains aujourd’hui', delta(g.today, g.yesterdaySame)) | ||
| 97 | − $('visGrid').querySelectorAll('.vis-card').forEach(function(c){ c.onclick=function(){ openDetail(c.dataset.site,c.dataset.label); }; }); } | 158 | + + kpi(nf(g.sessions), 'sessions') | ||
| 98 | − }catch(e){} | 159 | + + kpi(nf(g.pv), 'pages vues') | ||
| 160 | + + kpi(nf(g.d7), '7 jours') | ||||
| 161 | + + kpi(nf(g.d30), '30 jours') | ||||
| 162 | + + (all ? kpi(nf(g.total_vids), 'visiteurs (toutes classes)') + kpi(nf(autoHits), 'hits automatisés') : ''); | ||||
| 163 | + // barre de répartition des classes (événements du jour) | ||||
| 164 | + const seg = [['human', g.humanHits], ['crawler', g.crawler], ['datacenter', g.datacenter], ['automation', g.automation], ['internal', g.internal]] | ||||
| 165 | + .map(([k, v]) => [k, v || 0]); | ||||
| 166 | + const tot = seg.reduce((a, [, v]) => a + v, 0) || 1; | ||||
| 167 | + $('visBreak').innerHTML = ` | ||||
| 168 | + <div class="break-bar">${seg.map(([k, v]) => v ? `<i style="width:${(v / tot * 100).toFixed(2)}%;background:${CLS_COLOR[k]}" title="${CLS_LABEL[k]} : ${nf(v)}"></i>` : '').join('')}</div> | ||||
| 169 | + <div class="legend">${seg.map(([k, v]) => `<span><i class="sw" style="background:${CLS_COLOR[k]}"></i>${CLS_LABEL[k]} ${nf(v)}</span>`).join('')} | ||||
| 170 | + <span class="panel-note">événements du jour — les métriques principales n’affichent que les humains</span></div>`; | ||||
| 171 | + } | ||||
| 172 | + | ||||
| 173 | + function siteCard(s) { | ||||
| 174 | + const all = mode === 'all'; | ||||
| 175 | + const autoHits = (s.crawler || 0) + (s.datacenter || 0) + (s.automation || 0) + (s.internal || 0); | ||||
| 176 | + return `<div class="vis-card" data-site="${s.site}" data-label="${s.label || s.site}"> | ||||
| 177 | + <div class="vis-h"><img src="/icons/${s.site}" onerror="this.style.display='none'"><div class="vis-name">${s.label || s.site}</div> | ||||
| 178 | + <span class="vis-on-pill${s.online > 0 ? ' on' : ''}">${nf(s.online)} en ligne</span></div> | ||||
| 179 | + <div class="vis-row"> | ||||
| 180 | + <div class="vis-m"><div class="vm-n">${nf(s.today)}</div><div class="vm-l">humains</div></div> | ||||
| 181 | + <div class="vis-m"><div class="vm-n">${nf(s.sessions)}</div><div class="vm-l">sessions</div></div> | ||||
| 182 | + <div class="vis-m"><div class="vm-n">${nf(s.pv)}</div><div class="vm-l">pages</div></div> | ||||
| 183 | + <div class="vis-m"><div class="vm-n">${nf(s.d7)}</div><div class="vm-l">7 j</div></div> | ||||
| 184 | + </div> | ||||
| 185 | + ${all ? `<div class="vis-pv">🤖 ${nf(autoHits)} hits automatisés (crawler ${nf(s.crawler)} · dc ${nf(s.datacenter)} · autom. ${nf(s.automation)} · interne ${nf(s.internal)})</div>` | ||||
| 186 | + : `<div class="vis-pv">${delta(s.today, s.yesterdaySame) || ' '}<span class="vis-more">détail →</span></div>`} | ||||
| 187 | + </div>`; | ||||
| 188 | + } | ||||
| 189 | + | ||||
| 190 | + async function loadVisitors() { | ||||
| 191 | + try { | ||||
| 192 | + const d = await (await fetch('/api/analytics/summary')).json(); | ||||
| 193 | + lastSummary = d; | ||||
| 194 | + renderGlobal(d); | ||||
| 195 | + $('visGrid').innerHTML = (d.sites || []).map(siteCard).join(''); | ||||
| 196 | + $('visGrid').querySelectorAll('.vis-card').forEach(c => { c.onclick = () => openDetail(c.dataset.site, c.dataset.label); }); | ||||
| 197 | + } catch {} | ||||
| 198 | + loadRealtime(); loadAnoms(); | ||||
| 199 | + } | ||||
| 200 | + | ||||
| 201 | + async function loadRealtime() { | ||||
| 202 | + try { | ||||
| 203 | + const d = await (await fetch('/api/analytics/realtime')).json(); | ||||
| 204 | + $('rtCount').textContent = d.active.length + ' actif' + (d.active.length > 1 ? 's' : '') + ' · fenêtre ' + d.activeSec + ' s'; | ||||
| 205 | + if (!d.active.length) { $('visRealtime').innerHTML = '<div class="soc-sub">Personne en ce moment — le compteur expire tout seul (~2 min).</div>'; return; } | ||||
| 206 | + $('visRealtime').innerHTML = d.active.slice(0, 30).map(a => { | ||||
| 207 | + const dur = Math.max(0, Math.round((Date.now() - (a.since || a.lastTs)) / 60000)); | ||||
| 208 | + return `<div class="rt-row"> | ||||
| 209 | + <img src="/icons/${a.site}" onerror="this.style.display='none'"> | ||||
| 210 | + <span class="rt-site">${a.label}</span> | ||||
| 211 | + <span class="rt-path mono" title="${esc(a.path || '/')}">${esc((a.path || '/').slice(0, 42))}</span> | ||||
| 212 | + <span class="rt-loc">${esc(a.loc)}${a.cc && a.cc !== 'CA' && a.cc !== 'XX' ? ' · ' + a.cc : ''}</span> | ||||
| 213 | + <span class="rt-dev">${DEV_LABEL[a.dev] || ''}</span> | ||||
| 214 | + <span class="rt-dur mono">${dur < 1 ? '<1 min' : dur + ' min'}</span> | ||||
| 215 | + </div>`; | ||||
| 216 | + }).join(''); | ||||
| 217 | + } catch {} | ||||
| 218 | + } | ||||
| 219 | + | ||||
| 220 | + async function loadAnoms() { | ||||
| 221 | + try { | ||||
| 222 | + const d = await (await fetch('/api/analytics/anomalies')).json(); | ||||
| 223 | + const bits = []; | ||||
| 224 | + if (d.datacenter?.surge) bits.push(`<div class="anom warn">▲ Poussée datacenter : ${nf(d.datacenter.lastHour)} hits/h (moy. ${nf(d.datacenter.hourlyAvg)})</div>`); | ||||
| 225 | + for (const s of d.spikes || []) bits.push(`<div class="anom warn">▲ Pic sur <b>${esc(s.site)}</b> : ${nf(s.lastHour)} evt/h (×${s.factor})</div>`); | ||||
| 226 | + for (const ip of (d.topIps || []).slice(0, 5)) bits.push(`<div class="anom"><span class="mono">${esc(ip.ip)}</span> · ${nf(ip.n)} evt/h · ${esc(ip.org || '?')} · classe <b>${esc(ip.cls)}</b> <button class="linklike" data-ip="${esc(ip.ip)}">inspecter</button></div>`); | ||||
| 227 | + $('visAnom').innerHTML = bits.length ? bits.join('') : '<div class="soc-sub">● Rien à signaler — trafic dans les normes.</div>'; | ||||
| 228 | + $('visAnom').querySelectorAll('[data-ip]').forEach(b => { b.onclick = () => inspect(b.dataset.ip); }); | ||||
| 229 | + } catch {} | ||||
| 230 | + } | ||||
| 231 | + | ||||
| 232 | + async function inspect(q) { | ||||
| 233 | + const val = q || prompt('IP ou visitor-id à inspecter :'); | ||||
| 234 | + if (!val) return; | ||||
| 235 | + const isIp = /^[\d.:a-f]+$/i.test(val) && val.includes('.') || val.includes(':'); | ||||
| 236 | + const r = await fetch('/api/analytics/inspect?' + (isIp ? 'ip=' : 'vid=') + encodeURIComponent(val)); | ||||
| 237 | + const d = await r.json(); | ||||
| 238 | + const ipi = d.ipinfo; | ||||
| 239 | + openSheet(`<div class="sheet-h"><h3>🔬 Inspecteur de trafic</h3></div> | ||||
| 240 | + <div class="sheet-sec"> | ||||
| 241 | + <div class="kv"><span>Classe stockée</span><b>${esc(d.storedClass || '—')}</b></div> | ||||
| 242 | + <div class="kv"><span>IP</span><b>${esc(d.ip || '—')}</b></div> | ||||
| 243 | + ${ipi ? `<div class="kv"><span>Réseau</span><b>AS${ipi.asn} · ${esc(ipi.org || ipi.isp || '?')}</b></div> | ||||
| 244 | + <div class="kv"><span>Lieu</span><b>${esc([ipi.city, ipi.region, ipi.country].filter(Boolean).join(', ') || '—')}</b></div> | ||||
| 245 | + <div class="kv"><span>Drapeaux</span><b>${ipi.hosting ? 'hosting ' : ''}${ipi.proxy ? 'proxy ' : ''}${ipi.mobile ? 'mobile ' : ''}${!ipi.hosting && !ipi.proxy ? 'résidentiel' : ''}</b></div>` : '<div class="soc-sub">IP pas encore résolue (file ip-api)</div>'} | ||||
| 246 | + </div> | ||||
| 247 | + <div class="sheet-sec"><h4>Raisons de la classification</h4> | ||||
| 248 | + ${(d.reasons || []).map(x => `<div class="inc-item"><span>·</span><span>${esc(x)}</span></div>`).join('')} | ||||
| 249 | + </div> | ||||
| 250 | + <div class="sheet-sec"><h4>Événements récents (${(d.events || []).length})</h4> | ||||
| 251 | + ${(d.events || []).slice(0, 20).map(e2 => `<div class="inc-item mono" style="font-size:11px"><span>${e2.ev}</span><span>${new Date(e2.ts).toLocaleTimeString('fr-CA')} · ${esc(e2.site)} · ${esc(e2.path)} · ${esc(e2.cls)}</span></div>`).join('') || '<div class="soc-sub">aucun</div>'} | ||||
| 252 | + </div>`); | ||||
| 253 | + } | ||||
| 254 | + $('visDebugBtn').onclick = () => inspect(null); | ||||
| 255 | + | ||||
| 256 | + document.querySelectorAll('#visMode button').forEach(b => { | ||||
| 257 | + b.onclick = () => { | ||||
| 258 | + mode = b.dataset.v; | ||||
| 259 | + document.querySelectorAll('#visMode button').forEach(x => x.classList.toggle('on', x === b)); | ||||
| 260 | + if (lastSummary) { renderGlobal(lastSummary); $('visGrid').innerHTML = (lastSummary.sites || []).map(siteCard).join(''); $('visGrid').querySelectorAll('.vis-card').forEach(c => { c.onclick = () => openDetail(c.dataset.site, c.dataset.label); }); } | ||||
| 261 | + }; | ||||
| 262 | + }); | ||||
| 263 | + | ||||
| 264 | + // graphique série 30 j : humains (trait plein) + héritage v1 (pointillé gris) | ||||
| 265 | + function chart(series, mig) { | ||||
| 266 | + const w = 680, h = 220, pl = 42, pb = 26, pt = 10; | ||||
| 267 | + const n = series.length; if (n < 2) return '<div class="soc-sub">Pas encore assez de données.</div>'; | ||||
| 268 | + const max = Math.max(1, ...series.flatMap(x => [x.humans || 0, x.legacy || 0])); | ||||
| 269 | + const X = i => pl + (w - pl - 10) * (i / (n - 1)); | ||||
| 270 | + const Y = v => h - pb - (h - pb - pt) * (v / max); | ||||
| 271 | + let dH = '', dL = '', pen = false; | ||||
| 272 | + series.forEach((x, i) => { dH += (i ? 'L' : 'M') + X(i).toFixed(1) + ' ' + Y(x.humans || 0).toFixed(1); }); | ||||
| 273 | + series.forEach((x, i) => { if (x.legacy == null) { pen = false; return; } dL += (pen ? 'L' : 'M') + X(i).toFixed(1) + ' ' + Y(x.legacy).toFixed(1); pen = true; }); | ||||
| 274 | + const area = `M${X(0)} ${h - pb} ${dH.replace(/^M/, 'L')} L${X(n - 1)} ${h - pb} Z`; | ||||
| 275 | + let lab = ''; | ||||
| 276 | + for (let i = 0; i < n; i += Math.ceil(n / 6)) lab += `<text x="${X(i)}" y="${h - 8}" font-size="11" fill="var(--muted)" text-anchor="middle" font-family="var(--mono)">${series[i].day.slice(5)}</text>`; | ||||
| 277 | + const grid = [0.5, 1].map(f => `<line x1="${pl}" y1="${Y(max * f)}" x2="${w - 10}" y2="${Y(max * f)}" stroke="var(--grid)"/><text x="${pl - 6}" y="${Y(max * f) + 4}" font-size="10" fill="var(--muted)" text-anchor="end" font-family="var(--mono)">${nf(Math.round(max * f))}</text>`).join(''); | ||||
| 278 | + const migDay = mig ? new Date(mig).toISOString().slice(0, 10) : null; | ||||
| 279 | + const migIdx = migDay ? series.findIndex(x => x.day >= migDay) : -1; | ||||
| 280 | + const migLine = migIdx > 0 ? `<line x1="${X(migIdx)}" y1="${pt}" x2="${X(migIdx)}" y2="${h - pb}" stroke="var(--muted)" stroke-dasharray="3 4"/><text x="${X(migIdx) + 4}" y="${pt + 10}" font-size="9" fill="var(--muted)">migration v2</text>` : ''; | ||||
| 281 | + return `<svg viewBox="0 0 ${w} ${h}" class="vis-chart"> | ||||
| 282 | + ${grid}${migLine} | ||||
| 283 | + <path d="${area}" fill="var(--orange-deep)" opacity="0.12"/> | ||||
| 284 | + <path d="${dL}" fill="none" stroke="var(--muted)" stroke-width="1.6" stroke-dasharray="4 4"/> | ||||
| 285 | + <path d="${dH}" fill="none" stroke="var(--orange-deep)" stroke-width="2.4" stroke-linejoin="round"/> | ||||
| 286 | + ${lab} | ||||
| 287 | + </svg> | ||||
| 288 | + <div class="legend"><span><i class="sw" style="background:var(--orange-deep)"></i>humains (v2)</span><span><i class="sw" style="background:var(--muted)"></i>ancien comptage (non filtré)</span></div>`; | ||||
| 289 | + } | ||||
| 290 | + | ||||
| 291 | + async function openDetail(site, label) { | ||||
| 292 | + openSheet(`<div class="sheet-h"><img class="site-logo big" src="/icons/${site}" onerror="this.style.display='none'"><h3>${esc(label)}</h3></div><div id="vdBody"><div class="soc-sub">Chargement…</div></div>`); | ||||
| 293 | + try { | ||||
| 294 | + const [sm, se, det] = await Promise.all([ | ||||
| 295 | + lastSummary ? Promise.resolve(lastSummary) : (await fetch('/api/analytics/summary')).json(), | ||||
| 296 | + (await fetch('/api/analytics/series/' + site)).json(), | ||||
| 297 | + (await fetch('/api/analytics/site/' + site + '?days=7')).json(), | ||||
| 298 | + ]); | ||||
| 299 | + const s = (sm.sites || []).find(x => x.site === site) || {}; | ||||
| 300 | + const bar = (rows, kL, kV, unit) => { | ||||
| 301 | + const mx = Math.max(1, ...rows.map(r => r[kV])); | ||||
| 302 | + return rows.map(r => `<div class="vd-geo"><span class="vd-geoL" title="${esc(String(r[kL]))}">${esc(String(r[kL]).slice(0, 42))}</span><div class="vd-bar"><div style="width:${Math.round(100 * r[kV] / mx)}%"></div></div><span class="vd-geoN">${nf(r[kV])}${unit || ''}</span></div>`).join('') || '<div class="soc-sub">rien encore</div>'; | ||||
| 303 | + }; | ||||
| 304 | + const devTot = det.devs.reduce((a, x) => a + x.uv, 0) || 1; | ||||
| 305 | + $('vdBody').innerHTML = ` | ||||
| 306 | + <div class="sheet-sec"> | ||||
| 307 | + <div class="vd-kpis"> | ||||
| 308 | + <div class="vd-k"><div class="vd-kn">${nf(s.online)}</div><div class="vd-kl">en ligne</div></div> | ||||
| 309 | + <div class="vd-k"><div class="vd-kn">${nf(s.today)}</div><div class="vd-kl">humains auj.</div></div> | ||||
| 310 | + <div class="vd-k"><div class="vd-kn">${nf(s.sessions)}</div><div class="vd-kl">sessions</div></div> | ||||
| 311 | + <div class="vd-k"><div class="vd-kn">${nf(s.pv)}</div><div class="vd-kl">pages vues</div></div> | ||||
| 312 | + </div> | ||||
| 313 | + <div class="vd-kpis" style="margin-top:8px"> | ||||
| 314 | + <div class="vd-k"><div class="vd-kn">${nf(det.newToday)}</div><div class="vd-kl">nouveaux</div></div> | ||||
| 315 | + <div class="vd-k"><div class="vd-kn">${nf(det.returningToday)}</div><div class="vd-kl">récurrents</div></div> | ||||
| 316 | + <div class="vd-k"><div class="vd-kn">${det.pagesPerSession || 0}</div><div class="vd-kl">pages/session</div></div> | ||||
| 317 | + <div class="vd-k"><div class="vd-kn">${det.avgSessionSec >= 60 ? Math.round(det.avgSessionSec / 60) + ' min' : (det.avgSessionSec || 0) + ' s'}</div><div class="vd-kl">durée moy.</div></div> | ||||
| 318 | + </div> | ||||
| 319 | + </div> | ||||
| 320 | + <div class="sheet-sec"><h4>Visiteurs humains — 30 jours</h4>${chart(se.series || [], se.migratedAt)}</div> | ||||
| 321 | + <div class="sheet-sec"><h4>Pages populaires — 7 j (humains)</h4>${bar(det.pages, 'path', 'pv')}</div> | ||||
| 322 | + <div class="sheet-sec"><h4>Sources / referrers — 7 j</h4>${det.refs.length ? bar(det.refs.map(r => ({ ...r, ref: r.ref.replace(/^https?:\/\//, '') })), 'ref', 'uv') : '<div class="soc-sub">rien encore (referrer fourni par le beacon v2)</div>'}</div> | ||||
| 323 | + <div class="sheet-sec"><h4>Appareils</h4>${bar(det.devs.map(x => ({ l: (DEV_LABEL[x.dev] || 'inconnu (beacon v1)') + ' · ' + Math.round(x.uv / devTot * 100) + ' %', uv: x.uv })), 'l', 'uv')}</div> | ||||
| 324 | + <div class="sheet-sec"><h4>D'où viennent les visiteurs — 7 j</h4> | ||||
| 325 | + ${bar(det.geo.map(x => ({ l: x.city + (x.region ? ', ' + (x.cc === 'CA' ? x.region : x.country) : ''), uv: x.uv })), 'l', 'uv')} | ||||
| 326 | + ${det.countries.length > 1 ? '<h4 style="margin-top:12px">Pays</h4>' + bar(det.countries.map(x => ({ l: x.country, uv: x.uv })), 'l', 'uv') : ''} | ||||
| 327 | + </div> | ||||
| 328 | + <div class="sheet-sec"><h4>Trafic filtré — 7 j</h4> | ||||
| 329 | + ${bar((det.classes || []).filter(c => c.cls !== 'human').map(c => ({ l: (CLS_LABEL[c.cls] || c.cls) + ' (' + nf(c.uv) + ' ids)', n: c.n })), 'l', 'n', ' hits')} | ||||
| 330 | + </div>`; | ||||
| 331 | + } catch (e) { $('vdBody').innerHTML = '<div class="soc-sub">Erreur : ' + esc(e.message) + '</div>'; } | ||||
| 332 | + } | ||||
| 333 | + | ||||
| 334 | + window.startVisitors = function () { | ||||
| 335 | + loadVisitors(); | ||||
| 336 | + clearInterval(timer); | ||||
| 337 | + timer = setInterval(() => { if (curView === 'visitors' && !document.hidden) { loadVisitors(); } }, 12000); | ||||
| 338 | + }; | ||||
| 339 | +})(); | ||||
| 340 | + | ||||
| 341 | +// ===== VUE D'ENSEMBLE ===== | ||||
| 342 | +(function () { | ||||
| 343 | + const $ = (id) => document.getElementById(id); | ||||
| 344 | + let timer = null; | ||||
| 345 | + const nf = (n) => (n || 0).toLocaleString('fr-CA').replace(/,/g, ' '); | ||||
| 346 | + const fmt$$ = (v) => (v || 0).toFixed(2).replace('.', ',') + ' $'; | ||||
| 347 | + function kpi(v, l, extra, cls) { | ||||
| 348 | + return `<div class="kpi2 ${cls || ''}"><div class="kv2">${v}</div><div class="kl2">${l}</div>${extra || ''}</div>`; | ||||
| 349 | + } | ||||
| 350 | + function delta(cur, prev) { | ||||
| 351 | + if (!prev) return ''; | ||||
| 352 | + const d = Math.round((cur - prev) / prev * 100); | ||||
| 353 | + if (!isFinite(d) || Math.abs(d) < 1) return '<span class="kdelta flat">≈ hier</span>'; | ||||
| 354 | + return `<span class="kdelta ${d > 0 ? 'up' : 'down'}">${d > 0 ? '+' : ''}${d} % vs hier</span>`; | ||||
| 355 | + } | ||||
| 356 | + async function load() { | ||||
| 357 | + try { | ||||
| 358 | + const [an, eco, chats, anom, series] = await Promise.all([ | ||||
| 359 | + (await fetch('/api/analytics/summary')).json(), | ||||
| 360 | + (await fetch('/api/eco/summary')).json(), | ||||
| 361 | + (await fetch('/api/chats')).json(), | ||||
| 362 | + (await fetch('/api/analytics/anomalies')).json(), | ||||
| 363 | + (await fetch('/api/analytics/series/_all')).json(), | ||||
| 364 | + ]); | ||||
| 365 | + const g = an.global || {}, eg = eco.global || {}; | ||||
| 366 | + const running = (chats.chats || []).filter(c => c.state !== 'idle'); | ||||
| 367 | + const cost = (chats.chats || []).reduce((a, c) => a + (c.totalCost || 0), 0); | ||||
| 368 | + $('ovStamp').textContent = 'maj ' + new Date().toLocaleTimeString('fr-CA', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); | ||||
| 369 | + const ts = $('topStamp'); if (ts) ts.textContent = 'maj ' + new Date().toLocaleTimeString('fr-CA', { hour: '2-digit', minute: '2-digit' }); | ||||
| 370 | + $('ovKpis').innerHTML = | ||||
| 371 | + kpi(nf(g.online), 'humains en ligne', '', 'accent') | ||||
| 372 | + + kpi(nf(g.today), 'humains aujourd’hui', delta(g.today, g.yesterdaySame)) | ||||
| 373 | + + kpi(nf(g.sessions), 'sessions') | ||||
| 374 | + + kpi(nf(g.pv), 'pages vues') | ||||
| 375 | + + kpi(`${eg.up}<small>/${eg.total}</small>`, 'sites en ligne', '', eg.down ? 'bad' : 'good') | ||||
| 376 | + + kpi(eg.openIncidents || 0, 'incidents ouverts', '', eg.openIncidents ? 'bad' : '') | ||||
| 377 | + + kpi((eg.uptime24 ?? '—') + '<small>%</small>', 'uptime 24 h') | ||||
| 378 | + + kpi(eg.avgLatency + '<small> ms</small>', 'latence moy.') | ||||
| 379 | + + kpi(running.length, 'sessions Claude actives', '', running.length ? 'accent' : '') | ||||
| 380 | + + kpi(fmt$$(cost), 'coût Claude cumulé'); | ||||
| 381 | + // graphique 30 j global | ||||
| 382 | + const s = series.series || []; | ||||
| 383 | + $('ovChart').innerHTML = ovChart(s, series.migratedAt); | ||||
| 384 | + const autoHits = (g.crawler || 0) + (g.datacenter || 0) + (g.automation || 0) + (g.internal || 0); | ||||
| 385 | + $('ovBreak').innerHTML = `<div class="panel-note" style="margin-top:8px">Aujourd’hui : <b>${nf(g.today)}</b> humains · trafic filtré : ${nf(g.crawler)} crawler + ${nf(g.datacenter)} datacenter + ${nf(g.automation)} automation + ${nf(g.internal)} interne (${nf(autoHits)} hits exclus des métriques)</div>`; | ||||
| 386 | + // écosystème compact | ||||
| 387 | + const order = { down: 0, slow: 1, unknown: 2, up: 3 }; | ||||
| 388 | + const stt = (x) => (x.incident || (x.last && !x.last.ok)) ? 'down' : (x.last && x.last.ms > 4000) ? 'slow' : x.last ? 'up' : 'unknown'; | ||||
| 389 | + $('ovEco').innerHTML = '<div class="ov-eco-chips">' + [...eco.sites].sort((a, b) => order[stt(a)] - order[stt(b)]).map(x => { | ||||
| 390 | + const st = stt(x); | ||||
| 391 | + return `<span class="eco-chip ${st}" data-goto="eco" title="${x.label} — ${x.last?.ok ? x.last.ms + ' ms' : 'hors ligne'}">${st === 'up' ? '●' : st === 'slow' ? '▲' : '■'} ${x.label}<b>${x.last?.ok ? x.last.ms + 'ms' : 'OFF'}</b></span>`; | ||||
| 392 | + }).join('') + '</div>'; | ||||
| 393 | + // incidents récents | ||||
| 394 | + const inc = await (await fetch('/api/eco/incidents')).json(); | ||||
| 395 | + $('ovInc').innerHTML = (inc.incidents || []).slice(0, 5).map(i => ` | ||||
| 396 | + <div class="inc-item"><span class="${i.ended ? 'inc-ok' : 'inc-dot'}">${i.ended ? '✓' : '■'}</span> | ||||
| 397 | + <span><b>${esc(i.site)}</b> · ${new Date(i.started).toLocaleString('fr-CA', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })}${i.ended ? ' → ' + Math.round((i.ended - i.started) / 60000) + ' min' : ' — EN COURS'}</span></div>`).join('') | ||||
| 398 | + || '<div class="soc-sub">Aucun incident récent 🎉</div>'; | ||||
| 399 | + // Claude Code | ||||
| 400 | + $('ovClaude').innerHTML = (chats.chats || []).slice(0, 5).map(c => ` | ||||
| 401 | + <div class="ov-sess" data-chat="${c.id}"> | ||||
| 402 | + <span class="sess-badge ${c.state}">${c.state === 'running' ? '●' : c.state === 'waiting_perm' ? '🔐' : '✓'}</span> | ||||
| 403 | + <span class="ov-sess-t">${esc(c.title)}</span> | ||||
| 404 | + <span class="ov-sess-m mono">${esc(c.projectName || '')} · ${fmt$$(c.totalCost)}</span> | ||||
| 405 | + </div>`).join('') || '<div class="soc-sub">Aucune session encore.</div>'; | ||||
| 406 | + $('ovClaude').querySelectorAll('[data-chat]').forEach(x => { x.onclick = () => openChat(x.dataset.chat); }); | ||||
| 407 | + // anomalies | ||||
| 408 | + const bits = []; | ||||
| 409 | + if (anom.datacenter?.surge) bits.push(`<div class="anom warn">▲ Poussée datacenter : ${nf(anom.datacenter.lastHour)} hits/h</div>`); | ||||
| 410 | + for (const sp of anom.spikes || []) bits.push(`<div class="anom warn">▲ Pic sur <b>${esc(sp.site)}</b> : ×${sp.factor}</div>`); | ||||
| 411 | + for (const ip of (anom.topIps || []).slice(0, 3)) bits.push(`<div class="anom"><span class="mono">${esc(ip.ip)}</span> · ${nf(ip.n)} evt/h · ${esc(ip.org || '?')} → <b>${esc(ip.cls)}</b></div>`); | ||||
| 412 | + $('ovAnom').innerHTML = bits.length ? bits.join('') : '<div class="soc-sub">● Rien à signaler.</div>'; | ||||
| 413 | + } catch {} | ||||
| 99 | } | 414 | } | ||
| 100 | − function chart(series){ | 415 | + function ovChart(series, mig) { | ||
| 101 | − var w=680,h=240,pad=30; var vals=series.map(function(x){return x.uniques;}); | 416 | + const w = 760, h = 200, pl = 44, pb = 24, pt = 8; | ||
| 102 | − var max=Math.max(1,Math.max.apply(null,vals)); var n=series.length; | 417 | + const n = series.length; if (n < 2) return '<div class="soc-sub">Les données humaines s’accumulent — revenez dans quelques heures.</div>'; | ||
| 103 | − var X=function(i){return pad+(w-2*pad)*(n<=1?0:i/(n-1));}; | 418 | + const max = Math.max(1, ...series.flatMap(x => [x.humans || 0, x.legacy || 0])); | ||
| 104 | − var Y=function(v){return h-pad-(h-2*pad)*(v/max);}; | 419 | + const X = i => pl + (w - pl - 10) * (i / (n - 1)); | ||
| 105 | − var pts=series.map(function(x,i){return X(i)+','+Y(x.uniques);}).join(' '); | 420 | + const Y = v => h - pb - (h - pb - pt) * (v / max); | ||
| 106 | − var area='M'+X(0)+','+(h-pad)+' L'+pts.replace(/ /g,' L')+' L'+X(n-1)+','+(h-pad)+' Z'; | 421 | + let dH = '', dL = '', pen = false; | ||
| 107 | − var last=series[n-1]||{uniques:0}; | 422 | + series.forEach((x, i) => { dH += (i ? 'L' : 'M') + X(i).toFixed(1) + ' ' + Y(x.humans || 0).toFixed(1); }); | ||
| 108 | − var dots='<circle cx="'+X(n-1)+'" cy="'+Y(last.uniques)+'" r="6" fill="var(--accent,#f97316)"/>'; | 423 | + series.forEach((x, i) => { if (x.legacy == null) { pen = false; return; } dL += (pen ? 'L' : 'M') + X(i).toFixed(1) + ' ' + Y(x.legacy).toFixed(1); pen = true; }); | ||
| 109 | − // libellés d'axe : quelques jours | 424 | + const area = `M${X(0)} ${h - pb} ${dH.replace(/^M/, 'L')} L${X(n - 1)} ${h - pb} Z`; | ||
| 110 | − var lab=''; for(var i=0;i<n;i+=Math.ceil(n/5)){ var dd=series[i].day.slice(5); lab+='<text x="'+X(i)+'" y="'+(h-8)+'" font-size="14" fill="#888" text-anchor="middle">'+dd+'</text>'; } | 425 | + let lab = ''; | ||
| 111 | − return '<svg viewBox="0 0 '+w+' '+h+'" class="vis-chart">' | 426 | + for (let i = 0; i < n; i += Math.ceil(n / 6)) lab += `<text x="${X(i)}" y="${h - 6}" font-size="10" fill="var(--muted)" text-anchor="middle" font-family="var(--mono)">${series[i].day.slice(5)}</text>`; | ||
| 112 | − +'<defs><linearGradient id="ga" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="var(--accent,#f97316)" stop-opacity=".45"/><stop offset="1" stop-color="var(--accent,#f97316)" stop-opacity="0"/></linearGradient></defs>' | 427 | + const grid = [0.5, 1].map(f => `<line x1="${pl}" y1="${Y(max * f)}" x2="${w - 10}" y2="${Y(max * f)}" stroke="var(--grid)"/><text x="${pl - 6}" y="${Y(max * f) + 4}" font-size="10" fill="var(--muted)" text-anchor="end" font-family="var(--mono)">${nf(Math.round(max * f))}</text>`).join(''); | ||
| 113 | − +'<path d="'+area+'" fill="url(#ga)"/>' | 428 | + return `<svg viewBox="0 0 ${w} ${h}" class="vis-chart">${grid} | ||
| 114 | − +'<polyline points="'+pts+'" fill="none" stroke="var(--accent,#f97316)" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/>' | 429 | + <path d="${area}" fill="var(--orange-deep)" opacity="0.10"/> | ||
| 115 | − +dots+lab | 430 | + <path d="${dL}" fill="none" stroke="var(--muted)" stroke-width="1.5" stroke-dasharray="4 4"/> | ||
| 116 | − +'<text x="'+pad+'" y="20" font-size="15" fill="#999">max '+max+'/j</text></svg>'; | 431 | + <path d="${dH}" fill="none" stroke="var(--orange-deep)" stroke-width="2.4" stroke-linejoin="round"/>${lab}</svg> | ||
| 117 | − } | 432 | + <div class="legend"><span><i class="sw" style="background:var(--orange-deep)"></i>humains (v2, filtré)</span><span><i class="sw" style="background:var(--muted)"></i>ancien comptage v1 (non filtré)</span></div>`; | ||
| 118 | − async function openDetail(site,label){ | 433 | + } | ||
| 119 | − var ov=$('visDetail'); | 434 | + window.startOverview = function () { | ||
| 120 | − if(!ov){ ov=document.createElement('div'); ov.id='visDetail'; document.body.appendChild(ov); } | 435 | + load(); | ||
| 121 | − ov.innerHTML='<div class="vd-box"><div class="vd-head"><img src="/icons/'+site+'"><h3>'+label+'</h3><button id="vdClose">✕</button></div><div class="vd-body"><div class="soc-sub">Chargement…</div></div></div>'; | 436 | + clearInterval(timer); | ||
| 122 | − ov.classList.add('on'); $('vdClose').onclick=function(){ ov.classList.remove('on'); }; | 437 | + timer = setInterval(() => { if (curView === 'overview' && !document.hidden) load(); }, 30000); | ||
| 123 | − ov.onclick=function(e){ if(e.target===ov) ov.classList.remove('on'); }; | 438 | + }; | ||
| 124 | − try{ | ||||
| 125 | − var sm=await (await fetch('/api/analytics/summary')).json(); | ||||
| 126 | − var s=(sm.sites||[]).filter(function(x){return x.site===site;})[0]||{}; | ||||
| 127 | − var se=(await (await fetch('/api/analytics/series/'+site)).json()).series||[]; | ||||
| 128 | − var ge=(await (await fetch('/api/analytics/geo/'+site)).json()).geo||[]; | ||||
| 129 | − var maxg=Math.max(1,Math.max.apply(null,ge.map(function(x){return x.c;})||[1])); | ||||
| 130 | − var geoHtml = ge.length ? ge.map(function(x){return '<div class="vd-geo"><span class="vd-geoL">'+x.label+'</span><div class="vd-bar"><div style="width:'+Math.round(100*x.c/maxg)+'%"></div></div><span class="vd-geoN">'+nf(x.c)+'</span></div>';}).join('') : '<div class="soc-sub">Emplacements en cours de géolocalisation…</div>'; | ||||
| 131 | − ov.querySelector('.vd-body').innerHTML= | ||||
| 132 | − '<div class="vd-kpis">' | ||||
| 133 | − +'<div class="vd-k"><div class="vd-kn">'+nf(s.online)+'</div><div class="vd-kl">en ligne</div></div>' | ||||
| 134 | − +'<div class="vd-k"><div class="vd-kn">'+nf(s.today)+'</div><div class="vd-kl">aujourd'hui</div></div>' | ||||
| 135 | − +'<div class="vd-k"><div class="vd-kn">'+nf(s.d7)+'</div><div class="vd-kl">7 jours</div></div>' | ||||
| 136 | − +'<div class="vd-k"><div class="vd-kn">'+nf(s.d30)+'</div><div class="vd-kl">30 jours</div></div>' | ||||
| 137 | − +'</div>' | ||||
| 138 | − +'<div class="vd-title">Visiteurs uniques · 30 jours</div>'+chart(se) | ||||
| 139 | − +'<div class="vd-title">D'où viennent les visiteurs</div>'+geoHtml; | ||||
| 140 | − }catch(e){ ov.querySelector('.vd-body').innerHTML='<div class="soc-sub">Erreur : '+e.message+'</div>'; } | ||||
| 141 | − } | ||||
| 142 | − window.startVisitors=function(){ loadVisitors(); clearInterval(timer); timer=setInterval(function(){ if(curView==='visitors') loadVisitors(); },15000); }; | ||||
| 143 | })(); | 439 | })(); | ||
| 144 | 440 | ||||
| 145 | 441 | ||||
@@ -299,8 +595,7 @@ | |||||
| 299 | bindSocial(); | 595 | bindSocial(); | ||
| 300 | function navBadge(view) { | 596 | function navBadge(view) { | ||
| 301 | if (curView === view) return; | 597 | if (curView === view) return; | ||
| 302 | − const b = document.querySelector(`.nav-btn[data-view="${view}"] .badge`); | 598 | + document.querySelectorAll(`.nav-btn[data-view="${view}"] .badge`).forEach(b => b.classList.remove('hidden')); | ||
| 303 | − if (b) b.classList.remove('hidden'); | ||||
| 304 | } | 599 | } | ||
| 305 | 600 | ||||
| 306 | // ---------- bannières ---------- | 601 | // ---------- bannières ---------- | ||
@@ -660,9 +955,13 @@ | |||||
| 660 | // ---------- WebSocket ---------- | 955 | // ---------- WebSocket ---------- | ||
| 661 | function send(obj) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj)); } | 956 | function send(obj) { if (ws && ws.readyState === 1) ws.send(JSON.stringify(obj)); } | ||
| 662 | function setConn(state) { | 957 | function setConn(state) { | ||
| 663 | − const pill = $('connPill'); | 958 | + const txt = state === 'on' ? 'connecté' : state === 'mid' ? 'reconnexion…' : 'hors ligne'; | ||
| 664 | − pill.className = 'conn-pill ' + state; | 959 | + for (const id of ['connPill', 'connPillSide']) { | ||
| 665 | − $('connText').textContent = state === 'on' ? 'connecté' : state === 'mid' ? 'reconnexion…' : 'hors ligne'; | 960 | + const pill = $(id); | ||
| 961 | + if (!pill) continue; | ||||
| 962 | + pill.className = 'conn-pill ' + state; | ||||
| 963 | + const t = pill.querySelector('.conn-txt'); if (t) t.textContent = txt; | ||||
| 964 | + } | ||||
| 666 | } | 965 | } | ||
| 667 | function connect() { | 966 | function connect() { | ||
| 668 | if (!wsWanted) return; | 967 | if (!wsWanted) return; | ||
@@ -724,7 +1023,7 @@ | |||||
| 724 | return; | 1023 | return; | ||
| 725 | } | 1024 | } | ||
| 726 | if (msg.type === 'eco_alert') { | 1025 | if (msg.type === 'eco_alert') { | ||
| 727 | − if (msg.kind === 'down') banner('bad', `🔴 ${esc(msg.site)} est HORS LIGNE — ${esc(msg.reason || '')}`, () => switchView('eco'), 12000); | 1026 | + if (msg.kind === 'down') { banner('bad', `🔴 ${esc(msg.site)} est HORS LIGNE — ${esc(msg.reason || '')}`, () => switchView('eco'), 12000); const tb = $('topIncBadge'); if (tb) tb.classList.remove('hidden'); } | ||
| 728 | else banner('good', `🟢 ${esc(msg.site)} est de retour en ligne`, () => switchView('eco')); | 1027 | else banner('good', `🟢 ${esc(msg.site)} est de retour en ligne`, () => switchView('eco')); | ||
| 729 | navBadge('eco'); | 1028 | navBadge('eco'); | ||
| 730 | if (curView === 'eco') loadEco(); | 1029 | if (curView === 'eco') loadEco(); | ||
@@ -918,79 +1217,134 @@ | |||||
| 918 | } | 1217 | } | ||
| 919 | 1218 | ||||
| 920 | // ---------- sessions ---------- | 1219 | // ---------- sessions ---------- | ||
| 921 | − async function loadSessions() { | 1220 | + let sessCache = []; | ||
| 922 | − const r = await fetch('/api/chats'); | 1221 | + function sessFilters() { | ||
| 923 | − if (!r.ok) return; | 1222 | + return { q: ($('sessSearch')?.value || '').toLowerCase(), app: $('sessApp')?.value || '', st: $('sessState')?.value || '' }; | ||
| 924 | − const { chats } = await r.json(); | 1223 | + } | ||
| 1224 | + function renderSessions() { | ||||
| 1225 | + const { q, app, st } = sessFilters(); | ||||
| 925 | const list = $('sessList'); | 1226 | const list = $('sessList'); | ||
| 926 | list.innerHTML = ''; | 1227 | list.innerHTML = ''; | ||
| 927 | − if (!chats.length) { list.appendChild(el('div', 'sess-empty', 'Aucune session. Lance une conversation dans l’onglet Chat.')); return; } | 1228 | + const rows = sessCache.filter(c => | ||
| 1229 | + (!q || (c.title + ' ' + (c.projectName || '')).toLowerCase().includes(q)) | ||||
| 1230 | + && (!app || c.projectId === app) | ||||
| 1231 | + && (!st || c.state === st)); | ||||
| 1232 | + if (!rows.length) { list.appendChild(el('div', 'sess-empty', sessCache.length ? 'Aucune session ne correspond aux filtres.' : 'Aucune session. Lance une conversation dans l’onglet Claude Code.')); return; } | ||||
| 928 | const stLabel = { running: '● en cours', waiting_perm: '🔐 permission', idle: '✓ terminée' }; | 1233 | const stLabel = { running: '● en cours', waiting_perm: '🔐 permission', idle: '✓ terminée' }; | ||
| 929 | − for (const c of chats) { | 1234 | + for (const c of rows) { | ||
| 930 | − const card = el('div', 'sess-card'); | 1235 | + const card = el('div', 'sess-card row'); | ||
| 931 | card.innerHTML = ` | 1236 | card.innerHTML = ` | ||
| 932 | − <div class="sess-top"> | 1237 | + <span class="sess-badge ${c.state}" title="${stLabel[c.state] || c.state}">${c.state === 'running' ? '●' : c.state === 'waiting_perm' ? '🔐' : '✓'}</span> | ||
| 933 | − <span class="sess-title">${esc(c.title)}</span> | 1238 | + <div class="sess-main"> | ||
| 934 | − <span class="sess-badge ${c.state}">${stLabel[c.state] || c.state}</span> | 1239 | + <span class="sess-title">${c.batch ? '<span class="batch-tag">🌐</span> ' : ''}${esc(c.title)}</span> | ||
| 1240 | + <span class="sess-info"><span>${esc(c.projectName || '')}</span><span>${MODEL_LABEL[c.model] || c.model}</span><span>${c.contextTokens ? 'ctx ' + fmtTok(c.contextTokens) : ''}</span></span> | ||||
| 935 | </div> | 1241 | </div> | ||
| 936 | − <div class="sess-info"> | 1242 | + <span class="sess-cost mono">${fmt$(c.totalCost)}</span> | ||
| 937 | − ${c.batch ? '<span class="batch-tag">🌐 diffusion</span>' : ''} | 1243 | + <span class="sess-when mono">${timeAgo(c.updated)}</span> | ||
| 938 | − <span>${esc(c.projectName || '')}</span> | 1244 | + <span class="sess-btns"></span>`; | ||
| 939 | − <span>${MODEL_LABEL[c.model] || c.model}</span> | 1245 | + const btns = card.querySelector('.sess-btns'); | ||
| 940 | − <span>${fmt$(c.totalCost)}</span> | ||||
| 941 | − <span>${timeAgo(c.updated)}</span> | ||||
| 942 | − </div>`; | ||||
| 943 | − const actions = el('div', 'sess-actions'); | ||||
| 944 | const open = el('button', '', c.state === 'idle' ? 'Reprendre' : 'Ouvrir'); | 1246 | const open = el('button', '', c.state === 'idle' ? 'Reprendre' : 'Ouvrir'); | ||
| 945 | open.onclick = (e) => { e.stopPropagation(); openChat(c.id); }; | 1247 | open.onclick = (e) => { e.stopPropagation(); openChat(c.id); }; | ||
| 946 | − actions.appendChild(open); | 1248 | + btns.appendChild(open); | ||
| 947 | if (c.state === 'idle') { | 1249 | if (c.state === 'idle') { | ||
| 948 | − const del = el('button', 'del', 'Supprimer'); | 1250 | + const del = el('button', 'del', '✕'); | ||
| 1251 | + del.title = 'Supprimer'; | ||||
| 949 | del.onclick = async (e) => { | 1252 | del.onclick = async (e) => { | ||
| 950 | e.stopPropagation(); | 1253 | e.stopPropagation(); | ||
| 951 | if (!confirm('Supprimer cette session ?')) return; | 1254 | if (!confirm('Supprimer cette session ?')) return; | ||
| 952 | await fetch('/api/chats/' + c.id, { method: 'DELETE' }); | 1255 | await fetch('/api/chats/' + c.id, { method: 'DELETE' }); | ||
| 953 | − card.remove(); | 1256 | + sessCache = sessCache.filter(x => x.id !== c.id); | ||
| 1257 | + renderSessions(); | ||||
| 954 | if (currentChat && currentChat.id === c.id) { currentChat = null; clearConversation(); } | 1258 | if (currentChat && currentChat.id === c.id) { currentChat = null; clearConversation(); } | ||
| 955 | }; | 1259 | }; | ||
| 956 | − actions.appendChild(del); | 1260 | + btns.appendChild(del); | ||
| 957 | } | 1261 | } | ||
| 958 | − card.appendChild(actions); | ||||
| 959 | card.onclick = () => openChat(c.id); | 1262 | card.onclick = () => openChat(c.id); | ||
| 960 | list.appendChild(card); | 1263 | list.appendChild(card); | ||
| 961 | } | 1264 | } | ||
| 962 | } | 1265 | } | ||
| 1266 | + async function loadSessions() { | ||||
| 1267 | + const r = await fetch('/api/chats'); | ||||
| 1268 | + if (!r.ok) return; | ||||
| 1269 | + sessCache = (await r.json()).chats || []; | ||||
| 1270 | + // remplit le filtre app à partir des sessions existantes | ||||
| 1271 | + const sel = $('sessApp'); | ||||
| 1272 | + if (sel && sel.options.length <= 1) { | ||||
| 1273 | + const seen = new Map(); | ||||
| 1274 | + for (const c of sessCache) if (c.projectId && !seen.has(c.projectId)) seen.set(c.projectId, c.projectName || c.projectId); | ||||
| 1275 | + for (const [id2, name] of seen) { const o = document.createElement('option'); o.value = id2; o.textContent = name; sel.appendChild(o); } | ||||
| 1276 | + } | ||||
| 1277 | + renderSessions(); | ||||
| 1278 | + } | ||||
| 963 | $('newSessBtn').onclick = newChat; | 1279 | $('newSessBtn').onclick = newChat; | ||
| 1280 | + for (const id2 of ['sessSearch', 'sessApp', 'sessState']) { const e2 = $(id2); if (e2) e2.oninput = e2.onchange = renderSessions; } | ||||
| 1281 | + | ||||
| 1282 | + // sessions récentes sur l'écran d'accueil du chat | ||||
| 1283 | + async function loadWelcomeRecent() { | ||||
| 1284 | + const box = $('welcomeRecent'); | ||||
| 1285 | + if (!box) return; | ||||
| 1286 | + try { | ||||
| 1287 | + const { chats } = await (await fetch('/api/chats')).json(); | ||||
| 1288 | + if (!chats.length) { box.innerHTML = ''; return; } | ||||
| 1289 | + box.innerHTML = '<div class="wr-title">Reprendre une session</div>' + chats.slice(0, 4).map(c => ` | ||||
| 1290 | + <button class="wr-item" data-chat="${c.id}"> | ||||
| 1291 | + <span class="sess-badge ${c.state}">${c.state === 'running' ? '●' : c.state === 'waiting_perm' ? '🔐' : '✓'}</span> | ||||
| 1292 | + <span class="wr-t">${esc(c.title)}</span> | ||||
| 1293 | + <span class="wr-m mono">${esc(c.projectName || '')}</span> | ||||
| 1294 | + </button>`).join(''); | ||||
| 1295 | + box.querySelectorAll('[data-chat]').forEach(b => { b.onclick = () => openChat(b.dataset.chat); }); | ||||
| 1296 | + } catch {} | ||||
| 1297 | + } | ||||
| 964 | 1298 | ||||
| 965 | // ---------- Prompt Bank ---------- | 1299 | // ---------- Prompt Bank ---------- | ||
| 966 | function projectOptionsHTML(selected) { | 1300 | function projectOptionsHTML(selected) { | ||
| 967 | return [...projectSel.options].map(o => | 1301 | return [...projectSel.options].map(o => | ||
| 968 | `<option value="${esc(o.value)}"${o.value === selected ? ' selected' : ''}>${esc(o.textContent)}</option>`).join(''); | 1302 | `<option value="${esc(o.value)}"${o.value === selected ? ' selected' : ''}>${esc(o.textContent)}</option>`).join(''); | ||
| 969 | } | 1303 | } | ||
| 970 | − async function loadPrompts() { | 1304 | + let promptCache = []; | ||
| 971 | − const r = await fetch('/api/prompts'); | 1305 | + function renderPrompts() { | ||
| 972 | − if (!r.ok) return; | 1306 | + const q = ($('promptSearch')?.value || '').toLowerCase(); | ||
| 973 | − const { prompts } = await r.json(); | 1307 | + const app = $('promptApp')?.value || ''; | ||
| 974 | const list = $('promptList'); | 1308 | const list = $('promptList'); | ||
| 975 | list.innerHTML = ''; | 1309 | list.innerHTML = ''; | ||
| 976 | − if (!prompts.length) { list.appendChild(el('div', 'sess-empty', 'Aucun prompt enregistré. « + Nouveau » pour en créer un — tu pourras l’améliorer et l’envoyer plus tard.')); return; } | 1310 | + const rows = promptCache | ||
| 977 | − for (const pr of prompts) { | 1311 | + .filter(pr => (!q || (pr.title + ' ' + pr.text).toLowerCase().includes(q)) && (!app || pr.project === app)) | ||
| 1312 | + .sort((a, b) => (b.fav ? 1 : 0) - (a.fav ? 1 : 0) || (b.updated || 0) - (a.updated || 0)); | ||||
| 1313 | + if (!rows.length) { list.appendChild(el('div', 'sess-empty', promptCache.length ? 'Aucun prompt ne correspond.' : 'Aucun prompt enregistré. « + Nouveau » pour en créer un — tu pourras l’améliorer et l’envoyer plus tard.')); return; } | ||||
| 1314 | + for (const pr of rows) { | ||||
| 978 | const projName = pr.project ? ([...projectSel.options].find(o => o.value === pr.project)?.textContent || pr.project) : 'app au choix'; | 1315 | const projName = pr.project ? ([...projectSel.options].find(o => o.value === pr.project)?.textContent || pr.project) : 'app au choix'; | ||
| 979 | − const card = el('div', 'sess-card'); | 1316 | + const card = el('div', 'sess-card prow'); | ||
| 980 | card.innerHTML = ` | 1317 | card.innerHTML = ` | ||
| 981 | − <div class="sess-top"><span class="sess-title">${esc(pr.title)}</span></div> | 1318 | + <div class="sess-top"> | ||
| 982 | − <div class="prompt-preview">${esc(pr.text.slice(0, 220))}${pr.text.length > 220 ? '…' : ''}</div> | 1319 | + <button class="fav-btn${pr.fav ? ' on' : ''}" title="Favori">${pr.fav ? '★' : '☆'}</button> | ||
| 983 | − <div class="sess-info"><span>🎯 ${esc(projName)}</span><span>${timeAgo(pr.updated)}</span></div>`; | 1320 | + <span class="sess-title">${esc(pr.title)}</span> | ||
| 1321 | + <span class="sess-info"><span>🎯 ${esc(projName)}</span>${pr.uses ? `<span>${pr.uses}× utilisé</span>` : ''}<span>${timeAgo(pr.updated)}</span></span> | ||||
| 1322 | + </div> | ||||
| 1323 | + <div class="prompt-preview">${esc(pr.text.slice(0, 220))}${pr.text.length > 220 ? '…' : ''}</div>`; | ||||
| 984 | const actions = el('div', 'sess-actions'); | 1324 | const actions = el('div', 'sess-actions'); | ||
| 985 | − const sendB = el('button', 'p-send', 'Envoyer →'); | 1325 | + const sendB = el('button', 'p-send', 'Lancer →'); | ||
| 986 | const editB = el('button', '', '✎ Éditer'); | 1326 | const editB = el('button', '', '✎ Éditer'); | ||
| 987 | const upB = el('button', '', '✨ Améliorer'); | 1327 | const upB = el('button', '', '✨ Améliorer'); | ||
| 1328 | + const dupB = el('button', '', '⧉ Dupliquer'); | ||||
| 988 | const delB = el('button', 'del', 'Suppr.'); | 1329 | const delB = el('button', 'del', 'Suppr.'); | ||
| 989 | − actions.append(sendB, upB, editB, delB); | 1330 | + actions.append(sendB, upB, editB, dupB, delB); | ||
| 990 | card.appendChild(actions); | 1331 | card.appendChild(actions); | ||
| 991 | − sendB.onclick = () => openSendSheet(pr); | 1332 | + card.querySelector('.fav-btn').onclick = async (e) => { | ||
| 1333 | + e.stopPropagation(); | ||||
| 1334 | + pr.fav = !pr.fav; | ||||
| 1335 | + await fetch('/api/prompts/' + pr.id, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fav: pr.fav }) }); | ||||
| 1336 | + renderPrompts(); | ||||
| 1337 | + }; | ||||
| 1338 | + sendB.onclick = () => { | ||||
| 1339 | + fetch('/api/prompts/' + pr.id, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ bumpUse: true }) }).catch(() => {}); | ||||
| 1340 | + openSendSheet(pr); | ||||
| 1341 | + }; | ||||
| 992 | editB.onclick = () => openPromptEditor(pr); | 1342 | editB.onclick = () => openPromptEditor(pr); | ||
| 993 | − delB.onclick = async () => { if (!confirm('Supprimer ce prompt ?')) return; await fetch('/api/prompts/' + pr.id, { method: 'DELETE' }); card.remove(); }; | 1343 | + dupB.onclick = async () => { | ||
| 1344 | + await fetch('/api/prompts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: pr.title + ' (copie)', text: pr.text, project: pr.project }) }); | ||||
| 1345 | + loadPrompts(); | ||||
| 1346 | + }; | ||||
| 1347 | + delB.onclick = async () => { if (!confirm('Supprimer ce prompt ?')) return; await fetch('/api/prompts/' + pr.id, { method: 'DELETE' }); promptCache = promptCache.filter(x => x.id !== pr.id); renderPrompts(); }; | ||||
| 994 | upB.onclick = async () => { | 1348 | upB.onclick = async () => { | ||
| 995 | upB.textContent = '✨ …'; upB.disabled = true; | 1349 | upB.textContent = '✨ …'; upB.disabled = true; | ||
| 996 | try { | 1350 | try { | ||
@@ -1007,7 +1361,23 @@ | |||||
| 1007 | list.appendChild(card); | 1361 | list.appendChild(card); | ||
| 1008 | } | 1362 | } | ||
| 1009 | } | 1363 | } | ||
| 1364 | + async function loadPrompts() { | ||||
| 1365 | + const r = await fetch('/api/prompts'); | ||||
| 1366 | + if (!r.ok) return; | ||||
| 1367 | + promptCache = (await r.json()).prompts || []; | ||||
| 1368 | + const sel = $('promptApp'); | ||||
| 1369 | + if (sel && sel.options.length <= 1) { | ||||
| 1370 | + const seen = new Set(promptCache.map(p2 => p2.project).filter(Boolean)); | ||||
| 1371 | + for (const id2 of seen) { | ||||
| 1372 | + const o = document.createElement('option'); | ||||
| 1373 | + o.value = id2; o.textContent = [...projectSel.options].find(x => x.value === id2)?.textContent || id2; | ||||
| 1374 | + sel.appendChild(o); | ||||
| 1375 | + } | ||||
| 1376 | + } | ||||
| 1377 | + renderPrompts(); | ||||
| 1378 | + } | ||||
| 1010 | $('newPromptBtn').onclick = () => openPromptEditor(null); | 1379 | $('newPromptBtn').onclick = () => openPromptEditor(null); | ||
| 1380 | + for (const id2 of ['promptSearch', 'promptApp']) { const e2 = $(id2); if (e2) e2.oninput = e2.onchange = renderPrompts; } | ||||
| 1011 | 1381 | ||||
| 1012 | function openPromptEditor(pr) { | 1382 | function openPromptEditor(pr) { | ||
| 1013 | const isNew = !pr; | 1383 | const isNew = !pr; | ||
@@ -1079,8 +1449,11 @@ | |||||
| 1079 | banner('info', '🚀 Envoyé — la réponse arrive dans le Chat', null, 3000); | 1449 | banner('info', '🚀 Envoyé — la réponse arrive dans le Chat', null, 3000); | ||
| 1080 | } | 1450 | } | ||
| 1081 | 1451 | ||||
| 1082 | − // incidents globaux (bouton en tête de l'onglet Écosystème) | 1452 | + // incidents globaux (bouton Écosystème + cloche topbar) | ||
| 1083 | − $('incBtn').onclick = async () => { | 1453 | + $('topInc').onclick = () => openIncidents(); | ||
| 1454 | + $('incBtn').onclick = () => openIncidents(); | ||||
| 1455 | + async function openIncidents() { | ||||
| 1456 | + const tb = $('topIncBadge'); if (tb) tb.classList.add('hidden'); | ||||
| 1084 | const r = await fetch('/api/eco/incidents'); | 1457 | const r = await fetch('/api/eco/incidents'); | ||
| 1085 | if (!r.ok) return; | 1458 | if (!r.ok) return; | ||
| 1086 | const { incidents } = await r.json(); | 1459 | const { incidents } = await r.json(); | ||
@@ -1457,6 +1830,40 @@ | |||||
| 1457 | location.reload(); | 1830 | location.reload(); | ||
| 1458 | }; | 1831 | }; | ||
| 1459 | 1832 | ||||
| 1833 | + // ---------- réglages analytics ---------- | ||||
| 1834 | + window.loadAnCfg = async function () { | ||||
| 1835 | + const box = $('anCfg'); | ||||
| 1836 | + if (!box) return; | ||||
| 1837 | + try { | ||||
| 1838 | + const c = await (await fetch('/api/analytics/config')).json(); | ||||
| 1839 | + box.innerHTML = ` | ||||
| 1840 | + <div class="set-row"><span>Fenêtre de session</span><span><input id="acSess" class="ac-num" type="number" min="5" max="240" value="${c.sessionMin}"> min</span></div> | ||||
| 1841 | + <div class="set-row"><span>Fenêtre « en ligne »</span><span><input id="acAct" class="ac-num" type="number" min="30" max="900" value="${c.activeSec}"> s</span></div> | ||||
| 1842 | + <div class="set-row"><span>Rétention des événements</span><span><input id="acRet" class="ac-num" type="number" min="7" max="365" value="${c.retentionDays}"> jours</span></div> | ||||
| 1843 | + <div class="set-row"><span>Seuil anti-flood par IP</span><span><input id="acFlood" class="ac-num" type="number" min="100" max="100000" value="${c.maxIpPerHour}"> evt/h</span></div> | ||||
| 1844 | + <div class="set-label" style="margin-top:10px">IPs internes exclues (préfixes, une par ligne)</div> | ||||
| 1845 | + <textarea id="acIps" class="pe-textarea" style="min-height:56px">${esc((c.internalIps || []).join('\n'))}</textarea> | ||||
| 1846 | + <div class="set-label" style="margin-top:10px">User-Agents internes / bots additionnels (fragments)</div> | ||||
| 1847 | + <textarea id="acUas" class="pe-textarea" style="min-height:56px" placeholder="UA internes…">${esc((c.internalUAs || []).join('\n'))}</textarea> | ||||
| 1848 | + <textarea id="acBots" class="pe-textarea" style="min-height:56px;margin-top:6px" placeholder="UA bots custom…">${esc((c.extraBotUAs || []).join('\n'))}</textarea> | ||||
| 1849 | + <div class="set-label" style="margin-top:10px">ASNs exclus (numéros, un par ligne)</div> | ||||
| 1850 | + <textarea id="acAsn" class="pe-textarea" style="min-height:44px">${esc((c.blockedASNs || []).join('\n'))}</textarea> | ||||
| 1851 | + <div class="set-actions"><button id="acSave" class="btn-cta small">Enregistrer</button><span id="acMsg" class="soc-sub"></span></div>`; | ||||
| 1852 | + $('acSave').onclick = async () => { | ||||
| 1853 | + const lines = (id2) => $(id2).value.split('\n').map(s => s.trim()).filter(Boolean); | ||||
| 1854 | + const body = { | ||||
| 1855 | + sessionMin: +$('acSess').value, activeSec: +$('acAct').value, | ||||
| 1856 | + retentionDays: +$('acRet').value, maxIpPerHour: +$('acFlood').value, | ||||
| 1857 | + internalIps: lines('acIps'), internalUAs: lines('acUas'), | ||||
| 1858 | + extraBotUAs: lines('acBots'), blockedASNs: lines('acAsn').map(Number).filter(Boolean), | ||||
| 1859 | + }; | ||||
| 1860 | + const r2 = await fetch('/api/analytics/config', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); | ||||
| 1861 | + $('acMsg').textContent = r2.ok ? '✓ enregistré et appliqué' : '✗ échec'; | ||||
| 1862 | + setTimeout(() => { $('acMsg').textContent = ''; }, 3000); | ||||
| 1863 | + }; | ||||
| 1864 | + } catch { box.innerHTML = '<div class="soc-sub">Config indisponible.</div>'; } | ||||
| 1865 | + }; | ||||
| 1866 | + | ||||
| 1460 | // ---------- login / init ---------- | 1867 | // ---------- login / init ---------- | ||
| 1461 | async function init() { | 1868 | async function init() { | ||
| 1462 | const me = await fetch('/api/me'); | 1869 | const me = await fetch('/api/me'); | ||
@@ -1489,10 +1896,13 @@ | |||||
| 1489 | }; | 1896 | }; | ||
| 1490 | refreshChips(); | 1897 | refreshChips(); | ||
| 1491 | refreshSettingsView(); | 1898 | refreshSettingsView(); | ||
| 1899 | + loadWelcomeRecent(); | ||||
| 1492 | // reprise automatique : on rouvre la dernière conversation (historique + | 1900 | // reprise automatique : on rouvre la dernière conversation (historique + | ||
| 1493 | // live si une tâche tourne encore) même après fermeture complète de l'app | 1901 | // live si une tâche tourne encore) même après fermeture complète de l'app | ||
| 1494 | if (settings.lastChatId) currentChat = { id: settings.lastChatId }; | 1902 | if (settings.lastChatId) currentChat = { id: settings.lastChatId }; | ||
| 1495 | connect(); | 1903 | connect(); | ||
| 1904 | + // page d'accueil = Vue d'ensemble (le chat garde sa reprise en arrière-plan) | ||||
| 1905 | + switchView('overview'); | ||||
| 1496 | } | 1906 | } | ||
| 1497 | $('loginBtn').onclick = doLogin; | 1907 | $('loginBtn').onclick = doLogin; | ||
| 1498 | $('password').addEventListener('keydown', (e) => { if (e.key === 'Enter') doLogin(); }); | 1908 | $('password').addEventListener('keydown', (e) => { if (e.key === 'Enter') doLogin(); }); | ||
modified
public/index.html
+247 −158
@@ -7,11 +7,15 @@ | |||||
| 7 | <meta name="apple-mobile-web-app-capable" content="yes"> | 7 | <meta name="apple-mobile-web-app-capable" content="yes"> | ||
| 8 | <meta name="apple-mobile-web-app-status-bar-style" content="default"> | 8 | <meta name="apple-mobile-web-app-status-bar-style" content="default"> | ||
| 9 | <meta name="apple-mobile-web-app-title" content="Admin-Ka"> | 9 | <meta name="apple-mobile-web-app-title" content="Admin-Ka"> | ||
| 10 | −<title>Admin-Ka — Console Groupe KA</title> | 10 | +<title>Admin-Ka — Control Center Groupe KA</title> | ||
| 11 | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='9' fill='%23F97316'/%3E%3Ctext x='16' y='22' font-size='14' font-family='-apple-system,sans-serif' fill='%231a1611' text-anchor='middle' font-weight='800'%3EKa%3C/text%3E%3C/svg%3E"> | 11 | <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='9' fill='%23F97316'/%3E%3Ctext x='16' y='22' font-size='14' font-family='-apple-system,sans-serif' fill='%231a1611' text-anchor='middle' font-weight='800'%3EKa%3C/text%3E%3C/svg%3E"> | ||
| 12 | <link rel="apple-touch-icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23F97316'/%3E%3Ctext x='16' y='22' font-size='14' font-family='sans-serif' fill='%231a1611' text-anchor='middle' font-weight='800'%3EKa%3C/text%3E%3C/svg%3E"> | 12 | <link rel="apple-touch-icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23F97316'/%3E%3Ctext x='16' y='22' font-size='14' font-family='sans-serif' fill='%231a1611' text-anchor='middle' font-weight='800'%3EKa%3C/text%3E%3C/svg%3E"> | ||
| 13 | <link rel="stylesheet" href="/vendor/hljs-github.min.css"> | 13 | <link rel="stylesheet" href="/vendor/hljs-github.min.css"> | ||
| 14 | <link rel="stylesheet" href="/style.css"> | 14 | <link rel="stylesheet" href="/style.css"> | ||
| 15 | +<script>/* thème avant peinture (anti-flash) */ | ||||
| 16 | +(function(){try{var t=localStorage.getItem('ka_theme')||'auto'; | ||||
| 17 | +var dark=t==='dark'||(t==='auto'&&matchMedia('(prefers-color-scheme: dark)').matches); | ||||
| 18 | +document.documentElement.dataset.theme=dark?'dark':'light';}catch(e){}})();</script> | ||||
| 15 | </head> | 19 | </head> | ||
| 16 | <body> | 20 | <body> | ||
| 17 | 21 | ||||
@@ -20,7 +24,7 @@ | |||||
| 20 | <div class="login-box"> | 24 | <div class="login-box"> | ||
| 21 | <div class="ka-mark big">Ka</div> | 25 | <div class="ka-mark big">Ka</div> | ||
| 22 | <h1 class="login-title">Admin<span class="tiret">-</span>Ka</h1> | 26 | <h1 class="login-title">Admin<span class="tiret">-</span>Ka</h1> | ||
| 23 | − <p class="login-sub">Console Claude Code · Groupe KA</p> | 27 | + <p class="login-sub">Control Center · Groupe KA</p> | ||
| 24 | <input id="password" type="password" placeholder="Mot de passe" autocomplete="current-password"> | 28 | <input id="password" type="password" placeholder="Mot de passe" autocomplete="current-password"> | ||
| 25 | <button id="loginBtn" class="btn-cta">Se connecter</button> | 29 | <button id="loginBtn" class="btn-cta">Se connecter</button> | ||
| 26 | <div id="loginError" class="login-error"></div> | 30 | <div id="loginError" class="login-error"></div> | ||
@@ -31,183 +35,268 @@ | |||||
| 31 | <!-- ============ App ============ --> | 35 | <!-- ============ App ============ --> | ||
| 32 | <div id="app" class="app hidden"> | 36 | <div id="app" class="app hidden"> | ||
| 33 | 37 | ||||
| 34 | − <header class="header"> | 38 | + <!-- Sidebar (desktop) --> | ||
| 35 | − <div class="brand"> | 39 | + <aside class="sidebar"> | ||
| 40 | + <div class="side-brand"> | ||||
| 36 | <span class="ka-mark">Ka</span> | 41 | <span class="ka-mark">Ka</span> | ||
| 37 | <span class="brand-name">Admin<span class="tiret">-</span>Ka</span> | 42 | <span class="brand-name">Admin<span class="tiret">-</span>Ka</span> | ||
| 38 | </div> | 43 | </div> | ||
| 39 | − <div id="connPill" class="conn-pill off"><span class="conn-dot"></span><span id="connText">hors ligne</span></div> | 44 | + <nav class="side-nav" id="sideNav"></nav> | ||
| 40 | − </header> | 45 | + <div class="side-foot"> | ||
| 41 | − | 46 | + <div id="connPillSide" class="conn-pill off"><span class="conn-dot"></span><span class="conn-txt">hors ligne</span></div> | ||
| 42 | − <!-- Bannière notifications / alertes --> | 47 | + <div class="side-node mono">M3U96a · v3</div> | ||
| 43 | − <div id="banners" class="banners"></div> | ||||
| 44 | − | ||||
| 45 | − <!-- ======== Vue CHAT ======== --> | ||||
| 46 | − <section id="view-chat" class="view"> | ||||
| 47 | − <div class="chatbar"> | ||||
| 48 | − <select id="projectSel" class="pill-select" title="Projet (repo + nœud)"></select> | ||||
| 49 | − <button id="chipModel" class="chip" title="Modèle">opus</button> | ||||
| 50 | − <button id="chipPerm" class="chip" title="Permissions">normal</button> | ||||
| 51 | − <button id="chipMore" class="chip ghost" title="Options de session">⋯</button> | ||||
| 52 | − </div> | ||||
| 53 | − <div id="sessMeta" class="sess-meta hidden"> | ||||
| 54 | − <span id="metaModel"></span><span id="metaSid"></span><span id="metaCost"></span><span id="metaCtx"></span> | ||||
| 55 | </div> | 48 | </div> | ||
| 49 | + </aside> | ||||
| 50 | + | ||||
| 51 | + <div class="main-col"> | ||||
| 52 | + <!-- Topbar --> | ||||
| 53 | + <header class="header"> | ||||
| 54 | + <div class="brand mobile-only"> | ||||
| 55 | + <span class="ka-mark">Ka</span> | ||||
| 56 | + <span class="brand-name">Admin<span class="tiret">-</span>Ka</span> | ||||
| 57 | + </div> | ||||
| 58 | + <h1 id="pageTitle" class="page-title desktop-only">Vue d’ensemble</h1> | ||||
| 59 | + <div class="head-right"> | ||||
| 60 | + <span id="topStamp" class="top-stamp desktop-only"></span> | ||||
| 61 | + <button id="topInc" class="icon-btn" title="Incidents"><svg viewBox="0 0 24 24"><path d="M12 3l9 16H3z" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linejoin="round"/><path d="M12 10v4M12 16.6v.5" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg><i id="topIncBadge" class="badge hidden"></i></button> | ||||
| 62 | + <button id="themeBtn" class="icon-btn" title="Thème clair/sombre"><svg viewBox="0 0 24 24"><path d="M12 4.5a7.5 7.5 0 100 15 7.5 7.5 0 000-15zM12 4.5v15" stroke="currentColor" stroke-width="1.8" fill="none"/><path d="M12 4.5a7.5 7.5 0 010 15z" fill="currentColor"/></svg></button> | ||||
| 63 | + <div id="connPill" class="conn-pill off"><span class="conn-dot"></span><span id="connText" class="conn-txt">hors ligne</span></div> | ||||
| 64 | + </div> | ||||
| 65 | + </header> | ||||
| 56 | 66 | ||||
| 57 | − <main id="messages" class="messages"></main> | 67 | + <!-- Bannière notifications / alertes --> | ||
| 68 | + <div id="banners" class="banners"></div> | ||||
| 58 | 69 | ||||
| 59 | − <div id="welcome" class="welcome"> | 70 | + <!-- ======== Vue VUE D'ENSEMBLE ======== --> | ||
| 60 | − <div class="ka-mark big">Ka</div> | 71 | + <section id="view-overview" class="view scroll hidden"> | ||
| 61 | − <h2>Console Claude Code</h2> | 72 | + <div class="view-head"> | ||
| 62 | − <p>Le vrai Claude Code, lancé directement sur le nœud de chaque app du Groupe KA.</p> | 73 | + <h2>Vue d’ensemble</h2> | ||
| 63 | − <div class="hints"> | 74 | + <span id="ovStamp" class="eco-stamp"></span> | ||
| 64 | − <button class="hint">Quel modèle es-tu et dans quel répertoire travailles-tu ?</button> | ||||
| 65 | − <button class="hint">Montre les 5 derniers commits</button> | ||||
| 66 | − <button class="hint">État PM2 de l'app</button> | ||||
| 67 | </div> | 75 | </div> | ||
| 68 | − </div> | 76 | + <div id="ovKpis" class="ov-kpis"><div class="eco-loading">Chargement…</div></div> | ||
| 77 | + <div class="ov-cols"> | ||||
| 78 | + <div class="panel"> | ||||
| 79 | + <div class="panel-h"><h3>Visiteurs humains — 30 jours</h3><span class="panel-note">tous les sites KA</span></div> | ||||
| 80 | + <div id="ovChart"></div> | ||||
| 81 | + <div id="ovBreak"></div> | ||||
| 82 | + </div> | ||||
| 83 | + <div class="panel"> | ||||
| 84 | + <div class="panel-h"><h3>État de l’écosystème</h3><button class="linklike" data-goto="eco">tout voir →</button></div> | ||||
| 85 | + <div id="ovEco"></div> | ||||
| 86 | + <div class="panel-h" style="margin-top:14px"><h3>Incidents récents</h3></div> | ||||
| 87 | + <div id="ovInc"></div> | ||||
| 88 | + </div> | ||||
| 89 | + </div> | ||||
| 90 | + <div class="ov-cols"> | ||||
| 91 | + <div class="panel"> | ||||
| 92 | + <div class="panel-h"><h3>Claude Code</h3><button class="linklike" data-goto="sessions">sessions →</button></div> | ||||
| 93 | + <div id="ovClaude"></div> | ||||
| 94 | + </div> | ||||
| 95 | + <div class="panel"> | ||||
| 96 | + <div class="panel-h"><h3>Anomalies de trafic</h3><span class="panel-note">dernière heure</span></div> | ||||
| 97 | + <div id="ovAnom"></div> | ||||
| 98 | + </div> | ||||
| 99 | + </div> | ||||
| 100 | + </section> | ||||
| 69 | 101 | ||||
| 70 | − <footer class="composer"> | 102 | + <!-- ======== Vue CHAT (Claude Code) ======== --> | ||
| 71 | − <div class="composer-inner"> | 103 | + <section id="view-chat" class="view"> | ||
| 72 | − <button id="upgradeBtn" class="wand-btn" title="Améliorer le prompt (Opus 4.8 + savoir Groupe KA)"><svg viewBox="0 0 24 24"><path d="M5 3v4M3 5h4M6 17v4M4 19h4M13 3l2.5 6.5L22 12l-6.5 2.5L13 21l-2.5-6.5L4 12l6.5-2.5z" stroke="currentColor" stroke-width="1.7" fill="none" stroke-linejoin="round"/></svg></button> | 104 | + <div class="chatbar"> | ||
| 73 | − <textarea id="input" rows="1" placeholder="Demander à Claude Code…" enterkeyhint="send"></textarea> | 105 | + <select id="projectSel" class="pill-select" title="Projet (repo + nœud)"></select> | ||
| 74 | − <button id="stopBtn" class="send-btn stop hidden" title="Arrêter"><svg viewBox="0 0 24 24"><rect x="7" y="7" width="10" height="10" rx="2" fill="currentColor"/></svg></button> | 106 | + <button id="chipModel" class="chip" title="Modèle">opus</button> | ||
| 75 | − <button id="sendBtn" class="send-btn" title="Envoyer"><svg viewBox="0 0 24 24"><path d="M12 19V6M6 11l6-6 6 6" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg></button> | 107 | + <button id="chipPerm" class="chip" title="Permissions">normal</button> | ||
| 76 | − </div> | 108 | + <button id="chipMore" class="chip ghost" title="Options de session">⋯</button> | ||
| 77 | − <div id="slashHint" class="slash-hint hidden"></div> | 109 | + </div> | ||
| 78 | − </footer> | 110 | + <div id="sessMeta" class="sess-meta hidden"> | ||
| 79 | − </section> | 111 | + <span id="metaModel"></span><span id="metaSid"></span><span id="metaCost"></span><span id="metaCtx"></span> | ||
| 80 | − | 112 | + </div> | ||
| 81 | − <!-- ======== Vue SESSIONS ======== --> | ||||
| 82 | − <section id="view-sessions" class="view hidden"> | ||||
| 83 | − <div class="view-head"> | ||||
| 84 | − <h2>Sessions</h2> | ||||
| 85 | − <button id="newSessBtn" class="btn-cta small">+ Nouvelle</button> | ||||
| 86 | − </div> | ||||
| 87 | − <div id="sessList" class="sess-list"></div> | ||||
| 88 | − </section> | ||||
| 89 | − | ||||
| 90 | − <!-- ======== Vue PROMPTS (bank) ======== --> | ||||
| 91 | − <section id="view-prompts" class="view hidden"> | ||||
| 92 | − <div class="view-head"> | ||||
| 93 | − <h2>Prompts</h2> | ||||
| 94 | − <button id="newPromptBtn" class="btn-cta small">+ Nouveau</button> | ||||
| 95 | − </div> | ||||
| 96 | − <div id="promptList" class="sess-list"></div> | ||||
| 97 | − </section> | ||||
| 98 | 113 | ||||
| 99 | − <!-- ======== Vue ÉCOSYSTÈME ======== --> | 114 | + <main id="messages" class="messages"></main> | ||
| 100 | − <section id="view-eco" class="view hidden"> | 115 | + | ||
| 101 | − <div class="view-head"> | 116 | + <div id="welcome" class="welcome"> | ||
| 102 | − <h2>Écosystème</h2> | 117 | + <div class="welcome-inner"> | ||
| 103 | − <div class="head-actions"> | 118 | + <div class="ka-mark big">Ka</div> | ||
| 104 | − <button id="incBtn" class="chip">Incidents</button> | 119 | + <h2>Claude Code</h2> | ||
| 105 | − <span id="ecoStamp" class="eco-stamp"></span> | 120 | + <p>Le vrai Claude Code, lancé sur le nœud de chaque app du Groupe KA.</p> | ||
| 121 | + <div class="hints"> | ||||
| 122 | + <button class="hint">Quel modèle es-tu et dans quel répertoire travailles-tu ?</button> | ||||
| 123 | + <button class="hint">Montre les 5 derniers commits</button> | ||||
| 124 | + <button class="hint">État PM2 de l'app</button> | ||||
| 125 | + </div> | ||||
| 126 | + <div id="welcomeRecent" class="welcome-recent"></div> | ||||
| 127 | + </div> | ||||
| 106 | </div> | 128 | </div> | ||
| 107 | − </div> | 129 | + | ||
| 108 | − <div id="ecoOverview" class="eco-overview"></div> | 130 | + <footer class="composer"> | ||
| 109 | − <div id="ecoGrid" class="eco-grid"><div class="eco-loading">Chargement…</div></div> | 131 | + <div class="composer-inner"> | ||
| 110 | − <h3 class="eco-subhead">Nœuds du cluster</h3> | 132 | + <button id="upgradeBtn" class="wand-btn" title="Améliorer le prompt (Opus 4.8 + savoir Groupe KA)"><svg viewBox="0 0 24 24"><path d="M5 3v4M3 5h4M6 17v4M4 19h4M13 3l2.5 6.5L22 12l-6.5 2.5L13 21l-2.5-6.5L4 12l6.5-2.5z" stroke="currentColor" stroke-width="1.7" fill="none" stroke-linejoin="round"/></svg></button> | ||
| 111 | − <div id="ecoNodes" class="node-grid"></div> | 133 | + <textarea id="input" rows="1" placeholder="Demander à Claude Code…" enterkeyhint="send"></textarea> | ||
| 112 | − </section> | 134 | + <button id="stopBtn" class="send-btn stop hidden" title="Arrêter"><svg viewBox="0 0 24 24"><rect x="7" y="7" width="10" height="10" rx="2" fill="currentColor"/></svg></button> | ||
| 113 | − | 135 | + <button id="sendBtn" class="send-btn" title="Envoyer"><svg viewBox="0 0 24 24"><path d="M12 19V6M6 11l6-6 6 6" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg></button> | ||
| 114 | − <!-- ======== Vue RÉGLAGES ======== --> | 136 | + </div> | ||
| 115 | − <section id="view-settings" class="view hidden"> | 137 | + <div id="slashHint" class="slash-hint hidden"></div> | ||
| 116 | − <div class="view-head"><h2>Réglages</h2></div> | 138 | + </footer> | ||
| 117 | − <div class="set-card"> | 139 | + </section> | ||
| 118 | − <div class="set-label">Modèle par défaut</div> | 140 | + | ||
| 119 | − <div class="seg" id="segModel"> | 141 | + <!-- ======== Vue SESSIONS ======== --> | ||
| 120 | − <button data-v="fable">Fable 5</button><button data-v="opus">Opus 5</button><button data-v="sonnet">Sonnet 5</button><button data-v="haiku">Haiku</button> | 142 | + <section id="view-sessions" class="view scroll hidden"> | ||
| 143 | + <div class="view-head"> | ||||
| 144 | + <h2>Sessions</h2> | ||||
| 145 | + <button id="newSessBtn" class="btn-cta small">+ Nouvelle</button> | ||||
| 121 | </div> | 146 | </div> | ||
| 122 | − </div> | 147 | + <div class="filterbar"> | ||
| 123 | − <div class="set-card"> | 148 | + <input id="sessSearch" class="f-input" type="search" placeholder="Rechercher…"> | ||
| 124 | − <div class="set-label">Permissions par défaut</div> | 149 | + <select id="sessApp" class="f-select"><option value="">Toutes les apps</option></select> | ||
| 125 | − <div class="seg vert" id="segPerm"> | 150 | + <select id="sessState" class="f-select"> | ||
| 126 | − <button data-v="default">Normal — demande dans le chat</button> | 151 | + <option value="">Tous les statuts</option> | ||
| 127 | − <button data-v="acceptEdits">acceptEdits — éditions auto</button> | 152 | + <option value="running">En cours</option> | ||
| 128 | − <button data-v="plan">Plan — proposer avant d'agir</button> | 153 | + <option value="waiting_perm">Permission</option> | ||
| 129 | − <button data-v="bypass">⚠ Tout autoriser (bypass)</button> | 154 | + <option value="idle">Terminées</option> | ||
| 155 | + </select> | ||||
| 130 | </div> | 156 | </div> | ||
| 131 | − </div> | 157 | + <div id="sessList" class="sess-list"></div> | ||
| 132 | − <div class="set-card"> | 158 | + </section> | ||
| 133 | − <div class="set-label">Conversation active</div> | 159 | + | ||
| 134 | − <div class="set-row"><span>Session Claude Code</span><span id="setSid" class="mono">—</span></div> | 160 | + <!-- ======== Vue PROMPTS ======== --> | ||
| 135 | − <div class="set-row"><span>Coût cumulé</span><span id="setCost" class="mono">—</span></div> | 161 | + <section id="view-prompts" class="view scroll hidden"> | ||
| 136 | − <div class="set-row"><span>Contexte utilisé</span><span id="setCtx" class="mono">—</span></div> | 162 | + <div class="view-head"> | ||
| 137 | − <div class="set-actions"> | 163 | + <h2>Prompts</h2> | ||
| 138 | − <button id="btnClear" class="btn-ghost">/clear — effacer le contexte</button> | 164 | + <button id="newPromptBtn" class="btn-cta small">+ Nouveau</button> | ||
| 139 | − <button id="btnCompact" class="btn-ghost">/compact — compacter</button> | ||||
| 140 | </div> | 165 | </div> | ||
| 141 | − </div> | 166 | + <div class="filterbar"> | ||
| 142 | − <div class="set-card"> | 167 | + <input id="promptSearch" class="f-input" type="search" placeholder="Rechercher un prompt…"> | ||
| 143 | − <div class="set-row"><span>Backend</span><span id="setHost" class="mono">M3U96a:3300</span></div> | 168 | + <select id="promptApp" class="f-select"><option value="">Toutes les apps</option></select> | ||
| 144 | − <div class="set-row"><span>Version</span><span class="mono">2.0.0</span></div> | 169 | + </div> | ||
| 145 | − </div> | 170 | + <div id="promptList" class="sess-list"></div> | ||
| 146 | − <button id="logoutBtn" class="btn-danger">Se déconnecter</button> | 171 | + </section> | ||
| 147 | − </section> | ||||
| 148 | 172 | ||||
| 149 | − <!-- ======== Vue SOCIAL ======== --> | 173 | + <!-- ======== Vue ÉCOSYSTÈME ======== --> | ||
| 150 | − <section id="view-social" class="view hidden"> | 174 | + <section id="view-eco" class="view scroll hidden"> | ||
| 151 | − <div class="view-head"><h2>🎨 Studio</h2><button id="stRefresh" class="btn-ghost small">↻ Galerie</button></div> | 175 | + <div class="view-head"> | ||
| 152 | − <p class="soc-sub" style="margin:-4px 0 14px">Génère des visuels prêts à publier — télécharge-les et copie le texte. Rien n'est publié automatiquement.</p> | 176 | + <h2>Écosystème</h2> | ||
| 177 | + <div class="head-actions"> | ||||
| 178 | + <button id="incBtn" class="chip">Incidents</button> | ||||
| 179 | + <span id="ecoStamp" class="eco-stamp"></span> | ||||
| 180 | + </div> | ||||
| 181 | + </div> | ||||
| 182 | + <div id="ecoOverview" class="eco-overview"></div> | ||||
| 183 | + <div id="ecoGrid" class="eco-grid"><div class="eco-loading">Chargement…</div></div> | ||||
| 184 | + <h3 class="eco-subhead">Nœuds du cluster</h3> | ||||
| 185 | + <div id="ecoNodes" class="node-grid"></div> | ||||
| 186 | + </section> | ||||
| 153 | 187 | ||||
| 154 | − <div class="st-grid"> | 188 | + <!-- ======== Vue VISITEURS (analytics humains) ======== --> | ||
| 155 | − <div class="soc-card st-gen"> | 189 | + <section id="view-visitors" class="view scroll hidden"> | ||
| 156 | − <h3>🖼️ Publication</h3> | 190 | + <div class="view-head"> | ||
| 157 | − <textarea id="stPostPrompt" rows="2" placeholder="Sujet (optionnel). Ex : dernière propriété à vendre sur Immo-Ka."></textarea> | 191 | + <h2>Visiteurs</h2> | ||
| 158 | − <div class="soc-actions"> | 192 | + <div class="head-actions"> | ||
| 159 | − <select id="stPostSite" class="soc-select"><option value="">Choix auto</option><option value="lou-ka">Lou·Ka</option><option value="immo-ka">Immo·Ka</option><option value="vrai-prix">Vrai-Prix</option><option value="auto-ka">Auto·Ka</option><option value="food-ka">Food·Ka</option><option value="fabri-ka">Fabri·Ka</option><option value="resto-ka">Resto·Ka</option><option value="sorti-ka">Sorti·Ka</option><option value="crea-ka">Créa·Ka</option><option value="job-ka">Job·Ka</option><option value="trouve-ka">Trouve·Ka</option></select> | 193 | + <div class="seg mini" id="visMode"> | ||
| 160 | − <button id="stPostGen" class="btn-cta small">✨ Générer une image</button> | 194 | + <button data-v="human" class="on">Humains</button> | ||
| 195 | + <button data-v="all">Tout le trafic</button> | ||||
| 196 | + </div> | ||||
| 197 | + <span id="visLive" class="eco-stamp"></span> | ||||
| 161 | </div> | 198 | </div> | ||
| 162 | </div> | 199 | </div> | ||
| 163 | − <div class="soc-card st-gen"> | 200 | + | ||
| 164 | − <h3>🎬 Reel vidéo</h3> | 201 | + <div id="visGlobal" class="ov-kpis compact"></div> | ||
| 165 | − <textarea id="stReelPrompt" rows="2" placeholder="Sujet (optionnel). Ex : le dernier logement en annonce sur Lou-Ka."></textarea> | 202 | + <div id="visBreak" class="panel slim"></div> | ||
| 166 | − <div class="soc-actions"> | 203 | + | ||
| 167 | − <select id="stReelSite" class="soc-select"><option value="">Choix auto</option><option value="lou-ka">Lou·Ka</option><option value="immo-ka">Immo·Ka</option><option value="vrai-prix">Vrai-Prix</option><option value="auto-ka">Auto·Ka</option><option value="food-ka">Food·Ka</option><option value="fabri-ka">Fabri·Ka</option><option value="resto-ka">Resto·Ka</option><option value="sorti-ka">Sorti·Ka</option><option value="crea-ka">Créa·Ka</option><option value="job-ka">Job·Ka</option><option value="trouve-ka">Trouve·Ka</option></select> | 204 | + <div class="ov-cols"> | ||
| 168 | − <button id="stReelGen" class="btn-cta small">🎬 Générer un reel</button> | 205 | + <div class="panel"> | ||
| 206 | + <div class="panel-h"><h3>Temps réel — humains actifs</h3><span id="rtCount" class="panel-note"></span></div> | ||||
| 207 | + <div id="visRealtime" class="rt-list"><div class="soc-sub">Chargement…</div></div> | ||||
| 169 | </div> | 208 | </div> | ||
| 170 | − <div class="soc-actions" style="margin-top:8px"> | 209 | + <div class="panel"> | ||
| 171 | − <button id="stReel10" class="btn-ghost small">🔥 Générer 10 reels</button> | 210 | + <div class="panel-h"><h3>Anomalies</h3><button class="linklike" id="visDebugBtn">🔬 inspecter une IP / un visiteur</button></div> | ||
| 172 | − <span id="stGenStatus" class="soc-sub"></span> | 211 | + <div id="visAnom"></div> | ||
| 173 | </div> | 212 | </div> | ||
| 174 | </div> | 213 | </div> | ||
| 175 | − </div> | ||||
| 176 | 214 | ||||
| 177 | − <div class="view-head" style="margin-top:18px"><h2 style="font-size:17px">Galerie</h2><span id="stCount" class="soc-sub"></span></div> | 215 | + <div class="view-head" style="margin-top:16px"><h3 class="sub-h">Par application</h3></div> | ||
| 178 | − <div id="stGallery" class="st-gallery"><div class="soc-sub">Aucun visuel encore — génère ton premier post ou reel ci-dessus.</div></div> | 216 | + <div id="visGrid" class="vis-grid"><div class="soc-sub">Chargement…</div></div> | ||
| 179 | − </section> | 217 | + <p class="defs-note">Définitions — <b>humain</b> : navigateur réel, réseau non-datacenter, JS exécuté · <b>en ligne</b> : activité < 2 min (expire seul) · <b>page vue</b> : navigation réelle (jamais un heartbeat) · <b>session</b> : inactivité > 30 min = nouvelle session. Datacenters, crawlers, monitoring et trafic interne sont exclus des métriques principales.</p> | ||
| 180 | − | 218 | + </section> | ||
| 181 | −<section id="view-visitors" class="view hidden"> | 219 | + | ||
| 182 | − <div class="view-head"><h2>👥 Visiteurs</h2><span id="visLive" class="soc-sub"></span></div> | 220 | + <!-- ======== Vue RÉGLAGES ======== --> | ||
| 183 | − <div id="visGlobal" class="vis-global"></div> | 221 | + <section id="view-settings" class="view scroll hidden"> | ||
| 184 | − <div id="visGrid" class="vis-grid"><div class="soc-sub">Chargement…</div></div> | 222 | + <div class="view-head"><h2>Réglages</h2></div> | ||
| 185 | − </section> | 223 | + <div class="set-card"> | ||
| 186 | − | 224 | + <div class="set-label">Apparence</div> | ||
| 187 | − <!-- ======== Bottom nav ======== --> | 225 | + <div class="seg" id="segTheme"> | ||
| 188 | − <nav class="bottomnav"> | 226 | + <button data-v="auto">Auto</button><button data-v="light">Clair</button><button data-v="dark">Sombre</button> | ||
| 189 | − <button data-view="chat" class="nav-btn active"> | 227 | + </div> | ||
| 190 | − <svg viewBox="0 0 24 24"><path d="M4 6a3 3 0 013-3h10a3 3 0 013 3v8a3 3 0 01-3 3H9l-4 4v-4a3 3 0 01-1-2.2z" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linejoin="round"/></svg> | 228 | + </div> | ||
| 191 | − <span>Chat</span><i class="badge hidden"></i> | 229 | + <div class="set-card"> | ||
| 192 | − </button> | 230 | + <div class="set-label">Modèle par défaut</div> | ||
| 193 | − <button data-view="sessions" class="nav-btn"> | 231 | + <div class="seg" id="segModel"> | ||
| 194 | − <svg viewBox="0 0 24 24"><path d="M5 5h14M5 12h14M5 19h9" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg> | 232 | + <button data-v="fable">Fable 5</button><button data-v="opus">Opus 5</button><button data-v="sonnet">Sonnet 5</button><button data-v="haiku">Haiku</button> | ||
| 195 | − <span>Sessions</span><i class="badge hidden"></i> | 233 | + </div> | ||
| 196 | − </button> | 234 | + </div> | ||
| 197 | − <button data-view="prompts" class="nav-btn"> | 235 | + <div class="set-card"> | ||
| 198 | − <svg viewBox="0 0 24 24"><path d="M7 3h10a2 2 0 012 2v16l-7-4-7 4V5a2 2 0 012-2z" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linejoin="round"/></svg> | 236 | + <div class="set-label">Permissions par défaut</div> | ||
| 199 | − <span>Prompts</span><i class="badge hidden"></i> | 237 | + <div class="seg vert" id="segPerm"> | ||
| 200 | − </button> | 238 | + <button data-v="default">Normal — demande dans le chat</button> | ||
| 201 | − <button data-view="eco" class="nav-btn"> | 239 | + <button data-v="acceptEdits">acceptEdits — éditions auto</button> | ||
| 202 | − <svg viewBox="0 0 24 24"><path d="M4 17l4-6 4 3 4-8 4 5" stroke="currentColor" stroke-width="1.9" fill="none" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 21h18" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg> | 240 | + <button data-v="plan">Plan — proposer avant d'agir</button> | ||
| 203 | − <span>Écosystème</span><i class="badge hidden"></i> | 241 | + <button data-v="bypass">⚠ Tout autoriser (bypass)</button> | ||
| 204 | − </button> | 242 | + </div> | ||
| 205 | − <button data-view="social" class="nav-btn"><svg viewBox="0 0 24 24"><path d="M4 5h16v11H4z M8 20h8" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 8l5 2.5-5 2.5z" fill="currentColor"/></svg><span>Studio</span><i class="badge hidden"></i></button> | 243 | + </div> | ||
| 206 | − <button data-view="visitors" class="nav-btn"><svg viewBox="0 0 24 24"><circle cx="9" cy="8" r="3.2" stroke="currentColor" stroke-width="1.8" fill="none"/><path d="M3.5 19a5.5 5.5 0 0111 0" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round"/><path d="M16 5.2a3 3 0 010 5.6M18 19a5.5 5.5 0 00-3-4.9" stroke="currentColor" stroke-width="1.8" fill="none" stroke-linecap="round"/></svg><span>Visiteurs</span><i class="badge hidden"></i></button><button data-view="settings" class="nav-btn"> | 244 | + <div class="set-card"> | ||
| 207 | − <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3.2" stroke="currentColor" stroke-width="1.8" fill="none"/><path d="M12 2.8v3M12 18.2v3M2.8 12h3M18.2 12h3M5.5 5.5l2.1 2.1M16.4 16.4l2.1 2.1M18.5 5.5l-2.1 2.1M7.6 16.4l-2.1 2.1" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg> | 245 | + <div class="set-label">Analytics — définitions & filtres</div> | ||
| 208 | − <span>Réglages</span><i class="badge hidden"></i> | 246 | + <div id="anCfg" class="an-cfg"><div class="soc-sub">Chargement…</div></div> | ||
| 209 | − </button> | 247 | + </div> | ||
| 210 | − </nav> | 248 | + <div class="set-card"> | ||
| 249 | + <div class="set-label">Conversation active</div> | ||||
| 250 | + <div class="set-row"><span>Session Claude Code</span><span id="setSid" class="mono">—</span></div> | ||||
| 251 | + <div class="set-row"><span>Coût cumulé</span><span id="setCost" class="mono">—</span></div> | ||||
| 252 | + <div class="set-row"><span>Contexte utilisé</span><span id="setCtx" class="mono">—</span></div> | ||||
| 253 | + <div class="set-actions"> | ||||
| 254 | + <button id="btnClear" class="btn-ghost">/clear — effacer le contexte</button> | ||||
| 255 | + <button id="btnCompact" class="btn-ghost">/compact — compacter</button> | ||||
| 256 | + </div> | ||||
| 257 | + </div> | ||||
| 258 | + <div class="set-card"> | ||||
| 259 | + <div class="set-row"><span>Backend</span><span id="setHost" class="mono">M3U96a:3300</span></div> | ||||
| 260 | + <div class="set-row"><span>Version</span><span class="mono">3.0.0</span></div> | ||||
| 261 | + </div> | ||||
| 262 | + <button id="logoutBtn" class="btn-danger">Se déconnecter</button> | ||||
| 263 | + </section> | ||||
| 264 | + | ||||
| 265 | + <!-- ======== Vue STUDIO ======== --> | ||||
| 266 | + <section id="view-social" class="view scroll hidden"> | ||||
| 267 | + <div class="view-head"><h2>🎨 Studio</h2><button id="stRefresh" class="btn-ghost small">↻ Galerie</button></div> | ||||
| 268 | + <p class="soc-sub" style="margin:-4px 0 14px">Génère des visuels prêts à publier — télécharge-les et copie le texte. Rien n'est publié automatiquement.</p> | ||||
| 269 | + | ||||
| 270 | + <div class="st-grid"> | ||||
| 271 | + <div class="soc-card st-gen"> | ||||
| 272 | + <h3>🖼️ Publication</h3> | ||||
| 273 | + <textarea id="stPostPrompt" rows="2" placeholder="Sujet (optionnel). Ex : dernière propriété à vendre sur Immo-Ka."></textarea> | ||||
| 274 | + <div class="soc-actions"> | ||||
| 275 | + <select id="stPostSite" class="soc-select"><option value="">Choix auto</option><option value="lou-ka">Lou·Ka</option><option value="immo-ka">Immo·Ka</option><option value="vrai-prix">Vrai-Prix</option><option value="auto-ka">Auto·Ka</option><option value="food-ka">Food·Ka</option><option value="fabri-ka">Fabri·Ka</option><option value="resto-ka">Resto·Ka</option><option value="sorti-ka">Sorti·Ka</option><option value="crea-ka">Créa·Ka</option><option value="job-ka">Job·Ka</option><option value="trouve-ka">Trouve·Ka</option></select> | ||||
| 276 | + <button id="stPostGen" class="btn-cta small">✨ Générer une image</button> | ||||
| 277 | + </div> | ||||
| 278 | + </div> | ||||
| 279 | + <div class="soc-card st-gen"> | ||||
| 280 | + <h3>🎬 Reel vidéo</h3> | ||||
| 281 | + <textarea id="stReelPrompt" rows="2" placeholder="Sujet (optionnel). Ex : le dernier logement en annonce sur Lou-Ka."></textarea> | ||||
| 282 | + <div class="soc-actions"> | ||||
| 283 | + <select id="stReelSite" class="soc-select"><option value="">Choix auto</option><option value="lou-ka">Lou·Ka</option><option value="immo-ka">Immo·Ka</option><option value="vrai-prix">Vrai-Prix</option><option value="auto-ka">Auto·Ka</option><option value="food-ka">Food·Ka</option><option value="fabri-ka">Fabri·Ka</option><option value="resto-ka">Resto·Ka</option><option value="sorti-ka">Sorti·Ka</option><option value="crea-ka">Créa·Ka</option><option value="job-ka">Job·Ka</option><option value="trouve-ka">Trouve·Ka</option></select> | ||||
| 284 | + <button id="stReelGen" class="btn-cta small">🎬 Générer un reel</button> | ||||
| 285 | + </div> | ||||
| 286 | + <div class="soc-actions" style="margin-top:8px"> | ||||
| 287 | + <button id="stReel10" class="btn-ghost small">🔥 Générer 10 reels</button> | ||||
| 288 | + <span id="stGenStatus" class="soc-sub"></span> | ||||
| 289 | + </div> | ||||
| 290 | + </div> | ||||
| 291 | + </div> | ||||
| 292 | + | ||||
| 293 | + <div class="view-head" style="margin-top:18px"><h2 style="font-size:17px">Galerie</h2><span id="stCount" class="soc-sub"></span></div> | ||||
| 294 | + <div id="stGallery" class="st-gallery"><div class="soc-sub">Aucun visuel encore — génère ton premier post ou reel ci-dessus.</div></div> | ||||
| 295 | + </section> | ||||
| 296 | + | ||||
| 297 | + <!-- ======== Bottom nav (mobile) ======== --> | ||||
| 298 | + <nav class="bottomnav" id="bottomNav"></nav> | ||||
| 299 | + </div> | ||||
| 211 | 300 | ||||
| 212 | <!-- ======== Feuille détail (éco / options) ======== --> | 301 | <!-- ======== Feuille détail (éco / options) ======== --> | ||
| 213 | <div id="sheet" class="sheet hidden"> | 302 | <div id="sheet" class="sheet hidden"> | ||
added
public/ka-a.js
+71 −0
@@ -0,0 +1,71 @@ | |||||
| 1 | +/* ka-a.js v2 — analytics first-party Groupe KA (administration-ka.com) | ||||
| 2 | + * Léger, asynchrone, sans cookie tiers, sans fingerprinting, sans contenu saisi. | ||||
| 3 | + * - visitor_id (KA_VID, localStorage) : anonyme, persistant, dédup refresh | ||||
| 4 | + * - session_id (KA_SID) : nouvelle session après 30 min d'inactivité | ||||
| 5 | + * - page view : chargement + navigations SPA (pushState/replaceState/popstate) | ||||
| 6 | + * - heartbeat 25 s : SEULEMENT si l'onglet est visible ET activité < 60 s | ||||
| 7 | + * (scroll/pointeur/clavier/touch — on note QU'une interaction a eu lieu, jamais son contenu) | ||||
| 8 | + * → un onglet fermé ou inactif disparaît du compteur « en ligne » en ~2 min. */ | ||||
| 9 | +(function () { | ||||
| 10 | + try { | ||||
| 11 | + if (window.__kaAn2) return; window.__kaAn2 = 1; | ||||
| 12 | + var HOST = 'https://www.administration-ka.com'; | ||||
| 13 | + var SITE = window.__kaSite || (document.currentScript && document.currentScript.dataset.site) || ''; | ||||
| 14 | + if (!SITE) return; | ||||
| 15 | + var SESS_MS = 30 * 60 * 1000, HB_MS = 25 * 1000, INT_MS = 60 * 1000; | ||||
| 16 | + | ||||
| 17 | + function rid() { return Date.now().toString(36) + Math.random().toString(36).slice(2, 8); } | ||||
| 18 | + function lsGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } } | ||||
| 19 | + function lsSet(k, v) { try { localStorage.setItem(k, v); } catch (e) {} } | ||||
| 20 | + | ||||
| 21 | + var vid = lsGet('KA_VID'); if (!vid) { vid = rid(); lsSet('KA_VID', vid); } | ||||
| 22 | + function sid() { | ||||
| 23 | + var now = Date.now(), s = lsGet('KA_SID'), last = +(lsGet('KA_SLAST') || 0); | ||||
| 24 | + if (!s || now - last > SESS_MS) { s = rid(); lsSet('KA_SID', s); } | ||||
| 25 | + lsSet('KA_SLAST', String(now)); | ||||
| 26 | + return s; | ||||
| 27 | + } | ||||
| 28 | + var dev = (function () { var w = Math.min(screen.width, screen.height) || innerWidth; return w <= 500 ? 'm' : w <= 900 ? 't' : 'd'; })(); | ||||
| 29 | + | ||||
| 30 | + function send(ev, withRef) { | ||||
| 31 | + var u = HOST + '/collect?site=' + encodeURIComponent(SITE) | ||||
| 32 | + + '&vid=' + vid + '&sid=' + sid() + '&ev=' + ev | ||||
| 33 | + + '&p=' + encodeURIComponent(location.pathname) | ||||
| 34 | + + '&d=' + dev | ||||
| 35 | + + (withRef && document.referrer && document.referrer.indexOf(location.host) === -1 | ||||
| 36 | + ? '&r=' + encodeURIComponent(document.referrer.slice(0, 180)) : '') | ||||
| 37 | + + '&t=' + Date.now(); | ||||
| 38 | + if (navigator.sendBeacon) { try { navigator.sendBeacon(u); return; } catch (e) {} } | ||||
| 39 | + var i = new Image(); i.src = u; | ||||
| 40 | + } | ||||
| 41 | + | ||||
| 42 | + var lastPath = location.pathname; | ||||
| 43 | + function pv() { lastPath = location.pathname; send('pv', true); } | ||||
| 44 | + | ||||
| 45 | + // navigation SPA = nouvelle page vue (jamais un heartbeat) | ||||
| 46 | + function hookHistory(fn) { | ||||
| 47 | + var orig = history[fn]; | ||||
| 48 | + history[fn] = function () { var r = orig.apply(this, arguments); onNav(); return r; }; | ||||
| 49 | + } | ||||
| 50 | + function onNav() { if (location.pathname !== lastPath) setTimeout(pv, 0); } | ||||
| 51 | + hookHistory('pushState'); hookHistory('replaceState'); | ||||
| 52 | + addEventListener('popstate', onNav); | ||||
| 53 | + | ||||
| 54 | + // présence réelle : interaction récente + onglet visible | ||||
| 55 | + var lastInt = Date.now(); | ||||
| 56 | + function bump() { lastInt = Date.now(); } | ||||
| 57 | + ['pointerdown', 'keydown', 'scroll', 'touchstart', 'mousemove'].forEach(function (e) { | ||||
| 58 | + addEventListener(e, bump, { passive: true, capture: true }); | ||||
| 59 | + }); | ||||
| 60 | + setInterval(function () { | ||||
| 61 | + if (document.visibilityState !== 'visible') return; | ||||
| 62 | + if (Date.now() - lastInt > INT_MS) return; | ||||
| 63 | + send('hb'); | ||||
| 64 | + }, HB_MS); | ||||
| 65 | + document.addEventListener('visibilitychange', function () { | ||||
| 66 | + if (document.visibilityState === 'visible') { bump(); send('hb'); } | ||||
| 67 | + }); | ||||
| 68 | + | ||||
| 69 | + pv(); | ||||
| 70 | + } catch (e) {} | ||||
| 71 | +})(); | ||||
modified
public/style.css
+303 −61
@@ -20,8 +20,8 @@ | |||||
| 20 | --r-lg: 20px; | 20 | --r-lg: 20px; | ||
| 21 | --r-md: 14px; | 21 | --r-md: 14px; | ||
| 22 | --r-sm: 10px; | 22 | --r-sm: 10px; | ||
| 23 | − --bshadow: 3px 3px 0 var(--ink); | 23 | + --bshadow: 3px 3px 0 var(--shadow); | ||
| 24 | − --bshadow-sm: 2px 2px 0 var(--ink); | 24 | + --bshadow-sm: 2px 2px 0 var(--shadow); | ||
| 25 | --z-banner: 140; | 25 | --z-banner: 140; | ||
| 26 | --z-header: 100; | 26 | --z-header: 100; | ||
| 27 | --z-nav: 110; | 27 | --z-nav: 110; | ||
@@ -47,7 +47,7 @@ button { font-family: inherit; color: inherit; } | |||||
| 47 | display: inline-flex; align-items: center; justify-content: center; | 47 | display: inline-flex; align-items: center; justify-content: center; | ||
| 48 | width: 34px; height: 34px; border-radius: 11px; | 48 | width: 34px; height: 34px; border-radius: 11px; | ||
| 49 | background: var(--orange); color: var(--ink); | 49 | background: var(--orange); color: var(--ink); | ||
| 50 | − border: 2px solid var(--ink); box-shadow: var(--bshadow-sm); | 50 | + border: 2px solid var(--edge); box-shadow: var(--bshadow-sm); | ||
| 51 | font-weight: 800; font-size: 15px; letter-spacing: -0.2px; | 51 | font-weight: 800; font-size: 15px; letter-spacing: -0.2px; | ||
| 52 | flex-shrink: 0; | 52 | flex-shrink: 0; | ||
| 53 | } | 53 | } | ||
@@ -56,15 +56,15 @@ button { font-family: inherit; color: inherit; } | |||||
| 56 | 56 | ||||
| 57 | .btn-cta { | 57 | .btn-cta { | ||
| 58 | background: var(--orange); color: var(--ink); | 58 | background: var(--orange); color: var(--ink); | ||
| 59 | − border: 2px solid var(--ink); border-radius: 999px; | 59 | + border: 2px solid var(--edge); border-radius: 999px; | ||
| 60 | box-shadow: var(--bshadow); | 60 | box-shadow: var(--bshadow); | ||
| 61 | font-weight: 800; font-size: 16px; padding: 13px 22px; cursor: pointer; | 61 | font-weight: 800; font-size: 16px; padding: 13px 22px; cursor: pointer; | ||
| 62 | transition: transform 0.08s, box-shadow 0.08s; | 62 | transition: transform 0.08s, box-shadow 0.08s; | ||
| 63 | } | 63 | } | ||
| 64 | −.btn-cta:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--ink); } | 64 | +.btn-cta:active { transform: translate(2px, 2px); box-shadow: 1px 1px 0 var(--shadow); } | ||
| 65 | .btn-cta.small { font-size: 13.5px; padding: 8px 16px; box-shadow: var(--bshadow-sm); } | 65 | .btn-cta.small { font-size: 13.5px; padding: 8px 16px; box-shadow: var(--bshadow-sm); } | ||
| 66 | .btn-ghost { | 66 | .btn-ghost { | ||
| 67 | − background: var(--card); border: 1.5px solid var(--ink); border-radius: 999px; | 67 | + background: var(--card); border: 1.5px solid var(--edge); border-radius: 999px; | ||
| 68 | font-weight: 700; font-size: 13.5px; padding: 10px 16px; cursor: pointer; | 68 | font-weight: 700; font-size: 13.5px; padding: 10px 16px; cursor: pointer; | ||
| 69 | } | 69 | } | ||
| 70 | .btn-danger { | 70 | .btn-danger { | ||
@@ -82,15 +82,15 @@ button { font-family: inherit; color: inherit; } | |||||
| 82 | } | 82 | } | ||
| 83 | .login-box { | 83 | .login-box { | ||
| 84 | width: 100%; max-width: 360px; | 84 | width: 100%; max-width: 360px; | ||
| 85 | − background: var(--card); border: 2px solid var(--ink); border-radius: var(--r-xl); | 85 | + background: var(--card); border: 2px solid var(--edge); border-radius: var(--r-xl); | ||
| 86 | − box-shadow: 6px 6px 0 var(--ink); | 86 | + box-shadow: 6px 6px 0 var(--shadow); | ||
| 87 | padding: 34px 26px 26px; | 87 | padding: 34px 26px 26px; | ||
| 88 | display: flex; flex-direction: column; gap: 13px; text-align: center; | 88 | display: flex; flex-direction: column; gap: 13px; text-align: center; | ||
| 89 | } | 89 | } | ||
| 90 | .login-title { margin: 6px 0 0; font-size: 27px; font-weight: 800; letter-spacing: -0.5px; } | 90 | .login-title { margin: 6px 0 0; font-size: 27px; font-weight: 800; letter-spacing: -0.5px; } | ||
| 91 | .login-sub { margin: 0 0 8px; color: var(--muted); font-size: 13.5px; font-weight: 600; } | 91 | .login-sub { margin: 0 0 8px; color: var(--muted); font-size: 13.5px; font-weight: 600; } | ||
| 92 | .login input { | 92 | .login input { | ||
| 93 | − background: var(--bg); border: 2px solid var(--ink); color: var(--ink); | 93 | + background: var(--bg); border: 2px solid var(--edge); color: var(--ink); | ||
| 94 | border-radius: var(--r-md); padding: 14px 16px; font-size: 16px; outline: none; | 94 | border-radius: var(--r-md); padding: 14px 16px; font-size: 16px; outline: none; | ||
| 95 | font-family: inherit; | 95 | font-family: inherit; | ||
| 96 | } | 96 | } | ||
@@ -105,14 +105,14 @@ button { font-family: inherit; color: inherit; } | |||||
| 105 | position: sticky; top: 0; z-index: var(--z-header); | 105 | position: sticky; top: 0; z-index: var(--z-header); | ||
| 106 | display: flex; align-items: center; justify-content: space-between; gap: 10px; | 106 | display: flex; align-items: center; justify-content: space-between; gap: 10px; | ||
| 107 | background: var(--bg); | 107 | background: var(--bg); | ||
| 108 | − border-bottom: 2px solid var(--ink); | 108 | + border-bottom: 2px solid var(--edge); | ||
| 109 | padding: 8px 14px; | 109 | padding: 8px 14px; | ||
| 110 | padding-top: calc(8px + env(safe-area-inset-top)); | 110 | padding-top: calc(8px + env(safe-area-inset-top)); | ||
| 111 | } | 111 | } | ||
| 112 | .brand { display: flex; align-items: center; gap: 10px; font-weight: 800; font-size: 19px; letter-spacing: -0.4px; } | 112 | .brand { display: flex; align-items: center; gap: 10px; font-weight: 800; font-size: 19px; letter-spacing: -0.4px; } | ||
| 113 | .conn-pill { | 113 | .conn-pill { | ||
| 114 | display: flex; align-items: center; gap: 6px; | 114 | display: flex; align-items: center; gap: 6px; | ||
| 115 | − border: 1.5px solid var(--ink); border-radius: 999px; | 115 | + border: 1.5px solid var(--edge); border-radius: 999px; | ||
| 116 | background: var(--card); font-size: 11.5px; font-weight: 700; | 116 | background: var(--card); font-size: 11.5px; font-weight: 700; | ||
| 117 | padding: 4px 10px; | 117 | padding: 4px 10px; | ||
| 118 | } | 118 | } | ||
@@ -125,7 +125,7 @@ button { font-family: inherit; color: inherit; } | |||||
| 125 | .banners { position: fixed; top: calc(52px + env(safe-area-inset-top)); left: 0; right: 0; z-index: var(--z-banner); display: flex; flex-direction: column; gap: 8px; padding: 8px 14px; pointer-events: none; } | 125 | .banners { position: fixed; top: calc(52px + env(safe-area-inset-top)); left: 0; right: 0; z-index: var(--z-banner); display: flex; flex-direction: column; gap: 8px; padding: 8px 14px; pointer-events: none; } | ||
| 126 | .banner { | 126 | .banner { | ||
| 127 | pointer-events: auto; cursor: pointer; | 127 | pointer-events: auto; cursor: pointer; | ||
| 128 | − border: 2px solid var(--ink); border-radius: var(--r-md); | 128 | + border: 2px solid var(--edge); border-radius: var(--r-md); | ||
| 129 | box-shadow: var(--bshadow); padding: 11px 14px; | 129 | box-shadow: var(--bshadow); padding: 11px 14px; | ||
| 130 | font-weight: 700; font-size: 13.5px; | 130 | font-weight: 700; font-size: 13.5px; | ||
| 131 | display: flex; gap: 10px; align-items: center; | 131 | display: flex; gap: 10px; align-items: center; | ||
@@ -148,7 +148,7 @@ button { font-family: inherit; color: inherit; } | |||||
| 148 | position: fixed; bottom: 0; left: 0; right: 0; z-index: var(--z-nav); | 148 | position: fixed; bottom: 0; left: 0; right: 0; z-index: var(--z-nav); | ||
| 149 | display: flex; | 149 | display: flex; | ||
| 150 | background: var(--card); | 150 | background: var(--card); | ||
| 151 | − border-top: 2px solid var(--ink); | 151 | + border-top: 2px solid var(--edge); | ||
| 152 | padding: 6px 8px calc(6px + env(safe-area-inset-bottom)); | 152 | padding: 6px 8px calc(6px + env(safe-area-inset-bottom)); | ||
| 153 | } | 153 | } | ||
| 154 | .nav-btn { | 154 | .nav-btn { | ||
@@ -164,7 +164,7 @@ button { font-family: inherit; color: inherit; } | |||||
| 164 | .nav-btn .badge { | 164 | .nav-btn .badge { | ||
| 165 | position: absolute; top: 3px; right: calc(50% - 17px); | 165 | position: absolute; top: 3px; right: calc(50% - 17px); | ||
| 166 | width: 9px; height: 9px; border-radius: 50%; | 166 | width: 9px; height: 9px; border-radius: 50%; | ||
| 167 | − background: var(--orange); border: 1.5px solid var(--ink); | 167 | + background: var(--orange); border: 1.5px solid var(--edge); | ||
| 168 | } | 168 | } | ||
| 169 | 169 | ||||
| 170 | /* ================= Vue chat ================= */ | 170 | /* ================= Vue chat ================= */ | ||
@@ -178,7 +178,7 @@ button { font-family: inherit; color: inherit; } | |||||
| 178 | .pill-select { | 178 | .pill-select { | ||
| 179 | appearance: none; -webkit-appearance: none; | 179 | appearance: none; -webkit-appearance: none; | ||
| 180 | background: var(--card); color: var(--ink); | 180 | background: var(--card); color: var(--ink); | ||
| 181 | − border: 2px solid var(--ink); border-radius: 999px; | 181 | + border: 2px solid var(--edge); border-radius: 999px; | ||
| 182 | font-weight: 800; font-size: 13.5px; font-family: inherit; | 182 | font-weight: 800; font-size: 13.5px; font-family: inherit; | ||
| 183 | padding: 7px 26px 7px 13px; | 183 | padding: 7px 26px 7px 13px; | ||
| 184 | box-shadow: var(--bshadow-sm); | 184 | box-shadow: var(--bshadow-sm); | ||
@@ -187,7 +187,7 @@ button { font-family: inherit; color: inherit; } | |||||
| 187 | background-repeat: no-repeat; background-position: right 9px center; background-size: 12px; | 187 | background-repeat: no-repeat; background-position: right 9px center; background-size: 12px; | ||
| 188 | } | 188 | } | ||
| 189 | .chip { | 189 | .chip { | ||
| 190 | − border: 1.5px solid var(--ink); border-radius: 999px; | 190 | + border: 1.5px solid var(--edge); border-radius: 999px; | ||
| 191 | background: var(--card2); font-weight: 700; font-size: 12.5px; | 191 | background: var(--card2); font-weight: 700; font-size: 12.5px; | ||
| 192 | padding: 7px 12px; cursor: pointer; white-space: nowrap; flex-shrink: 0; | 192 | padding: 7px 12px; cursor: pointer; white-space: nowrap; flex-shrink: 0; | ||
| 193 | } | 193 | } | ||
@@ -212,14 +212,14 @@ button { font-family: inherit; color: inherit; } | |||||
| 212 | .msg-user { | 212 | .msg-user { | ||
| 213 | align-self: flex-end; max-width: 86%; | 213 | align-self: flex-end; max-width: 86%; | ||
| 214 | background: var(--orange); color: var(--ink); | 214 | background: var(--orange); color: var(--ink); | ||
| 215 | − border: 2px solid var(--ink); border-radius: var(--r-lg) var(--r-lg) 6px var(--r-lg); | 215 | + border: 2px solid var(--edge); border-radius: var(--r-lg) var(--r-lg) 6px var(--r-lg); | ||
| 216 | box-shadow: var(--bshadow-sm); | 216 | box-shadow: var(--bshadow-sm); | ||
| 217 | padding: 10px 15px; font-weight: 600; | 217 | padding: 10px 15px; font-weight: 600; | ||
| 218 | white-space: pre-wrap; word-break: break-word; | 218 | white-space: pre-wrap; word-break: break-word; | ||
| 219 | } | 219 | } | ||
| 220 | .msg-claude { | 220 | .msg-claude { | ||
| 221 | align-self: stretch; | 221 | align-self: stretch; | ||
| 222 | − background: var(--card); border: 2px solid var(--ink); border-radius: var(--r-lg); | 222 | + background: var(--card); border: 2px solid var(--edge); border-radius: var(--r-lg); | ||
| 223 | box-shadow: var(--bshadow-sm); | 223 | box-shadow: var(--bshadow-sm); | ||
| 224 | padding: 13px 16px; word-break: break-word; | 224 | padding: 13px 16px; word-break: break-word; | ||
| 225 | } | 225 | } | ||
@@ -227,7 +227,7 @@ button { font-family: inherit; color: inherit; } | |||||
| 227 | .msg-claude .md > :last-child { margin-bottom: 0; } | 227 | .msg-claude .md > :last-child { margin-bottom: 0; } | ||
| 228 | .md p { margin: 0.55em 0; } | 228 | .md p { margin: 0.55em 0; } | ||
| 229 | .md pre { | 229 | .md pre { | ||
| 230 | − background: #f2efe7; border: 1.5px solid var(--line); border-radius: var(--r-sm); | 230 | + background: var(--code-bg); border: 1.5px solid var(--line); border-radius: var(--r-sm); | ||
| 231 | padding: 11px; overflow-x: auto; font-size: 12.5px; line-height: 1.5; | 231 | padding: 11px; overflow-x: auto; font-size: 12.5px; line-height: 1.5; | ||
| 232 | } | 232 | } | ||
| 233 | .md code { font-family: var(--mono); font-size: 0.88em; } | 233 | .md code { font-family: var(--mono); font-size: 0.88em; } | ||
@@ -261,7 +261,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 261 | 261 | ||||
| 262 | /* --- cartes outils --- */ | 262 | /* --- cartes outils --- */ | ||
| 263 | .tool { | 263 | .tool { | ||
| 264 | − background: var(--card); border: 2px solid var(--ink); border-radius: var(--r-md); | 264 | + background: var(--card); border: 2px solid var(--edge); border-radius: var(--r-md); | ||
| 265 | overflow: hidden; font-size: 13px; box-shadow: var(--bshadow-sm); | 265 | overflow: hidden; font-size: 13px; box-shadow: var(--bshadow-sm); | ||
| 266 | } | 266 | } | ||
| 267 | .tool summary { | 267 | .tool summary { | ||
@@ -271,7 +271,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 271 | .tool summary::-webkit-details-marker { display: none; } | 271 | .tool summary::-webkit-details-marker { display: none; } | ||
| 272 | .tool .t-icon { | 272 | .tool .t-icon { | ||
| 273 | flex-shrink: 0; width: 28px; height: 28px; border-radius: 9px; | 273 | flex-shrink: 0; width: 28px; height: 28px; border-radius: 9px; | ||
| 274 | − border: 1.5px solid var(--ink); | 274 | + border: 1.5px solid var(--edge); | ||
| 275 | display: flex; align-items: center; justify-content: center; | 275 | display: flex; align-items: center; justify-content: center; | ||
| 276 | font-size: 13px; background: var(--card2); | 276 | font-size: 13px; background: var(--card2); | ||
| 277 | } | 277 | } | ||
@@ -288,7 +288,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 288 | .tool .t-state.pending { color: var(--warn); background: var(--warn-soft); } | 288 | .tool .t-state.pending { color: var(--warn); background: var(--warn-soft); } | ||
| 289 | .tool .t-state.ok { color: var(--good); background: var(--good-soft); } | 289 | .tool .t-state.ok { color: var(--good); background: var(--good-soft); } | ||
| 290 | .tool .t-state.err { color: var(--bad); background: var(--bad-soft); } | 290 | .tool .t-state.err { color: var(--bad); background: var(--bad-soft); } | ||
| 291 | −.tool-body { border-top: 2px solid var(--ink); padding: 9px 13px 11px; background: var(--card2); } | 291 | +.tool-body { border-top: 2px solid var(--edge); padding: 9px 13px 11px; background: var(--card2); } | ||
| 292 | .tool-body pre { | 292 | .tool-body pre { | ||
| 293 | margin: 6px 0; background: var(--card); border: 1.5px solid var(--line); | 293 | margin: 6px 0; background: var(--card); border: 1.5px solid var(--line); | ||
| 294 | border-radius: var(--r-sm); padding: 9px; | 294 | border-radius: var(--r-sm); padding: 9px; | ||
@@ -300,8 +300,8 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 300 | 300 | ||||
| 301 | .diff { font-family: var(--mono); font-size: 12px; margin: 6px 0; border: 1.5px solid var(--line); border-radius: var(--r-sm); overflow: hidden; background: var(--card); } | 301 | .diff { font-family: var(--mono); font-size: 12px; margin: 6px 0; border: 1.5px solid var(--line); border-radius: var(--r-sm); overflow: hidden; background: var(--card); } | ||
| 302 | .diff .dl { padding: 2px 10px; white-space: pre-wrap; word-break: break-word; } | 302 | .diff .dl { padding: 2px 10px; white-space: pre-wrap; word-break: break-word; } | ||
| 303 | −.diff .del { background: #ffe4e1; color: #8c1622; } | 303 | +.diff .del { background: var(--del-bg); color: var(--del-ink); } | ||
| 304 | −.diff .add { background: #ddf3e4; color: #0c5326; } | 304 | +.diff .add { background: var(--add-bg); color: var(--add-ink); } | ||
| 305 | 305 | ||||
| 306 | .todos { margin: 4px 0; padding: 0; list-style: none; font-size: 13.5px; } | 306 | .todos { margin: 4px 0; padding: 0; list-style: none; font-size: 13.5px; } | ||
| 307 | .todos li { padding: 3px 0; } | 307 | .todos li { padding: 3px 0; } | ||
@@ -310,9 +310,9 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 310 | 310 | ||||
| 311 | /* --- permission --- */ | 311 | /* --- permission --- */ | ||
| 312 | .perm { | 312 | .perm { | ||
| 313 | − background: var(--warn-soft); border: 2px solid var(--ink); | 313 | + background: var(--warn-soft); border: 2px solid var(--edge); | ||
| 314 | border-radius: var(--r-lg); padding: 14px; font-size: 13.5px; | 314 | border-radius: var(--r-lg); padding: 14px; font-size: 13.5px; | ||
| 315 | − box-shadow: 4px 4px 0 var(--ink); | 315 | + box-shadow: 4px 4px 0 var(--shadow); | ||
| 316 | } | 316 | } | ||
| 317 | .perm-title { font-weight: 800; margin-bottom: 8px; font-size: 14.5px; } | 317 | .perm-title { font-weight: 800; margin-bottom: 8px; font-size: 14.5px; } | ||
| 318 | .perm pre { | 318 | .perm pre { | ||
@@ -325,7 +325,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 325 | .perm-actions button { | 325 | .perm-actions button { | ||
| 326 | flex: 1; padding: 11px 6px; border-radius: 999px; | 326 | flex: 1; padding: 11px 6px; border-radius: 999px; | ||
| 327 | font-size: 13.5px; font-weight: 800; cursor: pointer; min-height: 46px; | 327 | font-size: 13.5px; font-weight: 800; cursor: pointer; min-height: 46px; | ||
| 328 | − border: 2px solid var(--ink); | 328 | + border: 2px solid var(--edge); | ||
| 329 | } | 329 | } | ||
| 330 | .perm-allow { background: var(--good); color: #fff; box-shadow: var(--bshadow-sm); } | 330 | .perm-allow { background: var(--good); color: #fff; box-shadow: var(--bshadow-sm); } | ||
| 331 | .perm-always { background: var(--good-soft); color: var(--good); line-height: 1.05; } | 331 | .perm-always { background: var(--good-soft); color: var(--good); line-height: 1.05; } | ||
@@ -333,7 +333,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 333 | .perm-deny { background: var(--card); color: var(--bad); } | 333 | .perm-deny { background: var(--card); color: var(--bad); } | ||
| 334 | .perm-allall { | 334 | .perm-allall { | ||
| 335 | width: 100%; margin-top: 8px; padding: 12px; border-radius: 999px; | 335 | width: 100%; margin-top: 8px; padding: 12px; border-radius: 999px; | ||
| 336 | − border: 2px solid var(--ink); background: var(--ink); color: var(--orange); | 336 | + border: 2px solid var(--edge); background: var(--ink); color: var(--orange); | ||
| 337 | font-weight: 800; font-size: 13.5px; cursor: pointer; box-shadow: var(--bshadow-sm); | 337 | font-weight: 800; font-size: 13.5px; cursor: pointer; box-shadow: var(--bshadow-sm); | ||
| 338 | } | 338 | } | ||
| 339 | .perm-allall:active { transform: translate(1px,1px); box-shadow: none; } | 339 | .perm-allall:active { transform: translate(1px,1px); box-shadow: none; } | ||
@@ -357,11 +357,11 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 357 | align-self: flex-start; | 357 | align-self: flex-start; | ||
| 358 | display: flex; align-items: center; gap: 10px; | 358 | display: flex; align-items: center; gap: 10px; | ||
| 359 | font-size: 13.5px; font-weight: 700; | 359 | font-size: 13.5px; font-weight: 700; | ||
| 360 | − background: var(--card); border: 2px solid var(--ink); | 360 | + background: var(--card); border: 2px solid var(--edge); | ||
| 361 | border-radius: 999px; padding: 8px 16px; box-shadow: var(--bshadow-sm); | 361 | border-radius: 999px; padding: 8px 16px; box-shadow: var(--bshadow-sm); | ||
| 362 | } | 362 | } | ||
| 363 | .dots { display: flex; gap: 4px; } | 363 | .dots { display: flex; gap: 4px; } | ||
| 364 | −.dots i { width: 7px; height: 7px; border-radius: 50%; background: var(--orange); border: 1px solid var(--ink); animation: bounce 1.1s ease-in-out infinite; } | 364 | +.dots i { width: 7px; height: 7px; border-radius: 50%; background: var(--orange); border: 1px solid var(--edge); animation: bounce 1.1s ease-in-out infinite; } | ||
| 365 | .dots i:nth-child(2) { animation-delay: 0.14s; } | 365 | .dots i:nth-child(2) { animation-delay: 0.14s; } | ||
| 366 | .dots i:nth-child(3) { animation-delay: 0.28s; } | 366 | .dots i:nth-child(3) { animation-delay: 0.28s; } | ||
| 367 | @keyframes bounce { 0%, 100% { transform: translateY(0); opacity: 0.5; } 40% { transform: translateY(-4px); opacity: 1; } } | 367 | @keyframes bounce { 0%, 100% { transform: translateY(0); opacity: 0.5; } 40% { transform: translateY(-4px); opacity: 1; } } | ||
@@ -377,7 +377,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 377 | .welcome p { margin: 0; color: var(--muted); font-size: 14px; max-width: 300px; font-weight: 600; } | 377 | .welcome p { margin: 0; color: var(--muted); font-size: 14px; max-width: 300px; font-weight: 600; } | ||
| 378 | .hints { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 14px; } | 378 | .hints { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-top: 14px; } | ||
| 379 | .hint { | 379 | .hint { | ||
| 380 | − background: var(--card); border: 1.5px solid var(--ink); | 380 | + background: var(--card); border: 1.5px solid var(--edge); | ||
| 381 | font-size: 13px; font-weight: 700; | 381 | font-size: 13px; font-weight: 700; | ||
| 382 | border-radius: 999px; padding: 9px 14px; cursor: pointer; | 382 | border-radius: 999px; padding: 9px 14px; cursor: pointer; | ||
| 383 | box-shadow: var(--bshadow-sm); | 383 | box-shadow: var(--bshadow-sm); | ||
@@ -393,7 +393,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 393 | display: flex; align-items: flex-end; gap: 8px; | 393 | display: flex; align-items: flex-end; gap: 8px; | ||
| 394 | max-width: 780px; margin: 0 auto; | 394 | max-width: 780px; margin: 0 auto; | ||
| 395 | background: var(--card); | 395 | background: var(--card); | ||
| 396 | − border: 2px solid var(--ink); border-radius: var(--r-xl); | 396 | + border: 2px solid var(--edge); border-radius: var(--r-xl); | ||
| 397 | box-shadow: var(--bshadow); | 397 | box-shadow: var(--bshadow); | ||
| 398 | padding: 6px 6px 6px 6px; gap: 4px; | 398 | padding: 6px 6px 6px 6px; gap: 4px; | ||
| 399 | } | 399 | } | ||
@@ -406,7 +406,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 406 | #input::placeholder { color: var(--muted); font-weight: 600; } | 406 | #input::placeholder { color: var(--muted); font-weight: 600; } | ||
| 407 | .send-btn { | 407 | .send-btn { | ||
| 408 | width: 42px; height: 42px; border-radius: 50%; | 408 | width: 42px; height: 42px; border-radius: 50%; | ||
| 409 | − border: 2px solid var(--ink); | 409 | + border: 2px solid var(--edge); | ||
| 410 | background: var(--orange); color: var(--ink); | 410 | background: var(--orange); color: var(--ink); | ||
| 411 | cursor: pointer; flex-shrink: 0; | 411 | cursor: pointer; flex-shrink: 0; | ||
| 412 | display: flex; align-items: center; justify-content: center; | 412 | display: flex; align-items: center; justify-content: center; | ||
@@ -419,7 +419,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 419 | .send-btn.stop { background: var(--bad); color: #fff; } | 419 | .send-btn.stop { background: var(--bad); color: #fff; } | ||
| 420 | .wand-btn { | 420 | .wand-btn { | ||
| 421 | width: 38px; height: 38px; border-radius: 50%; flex-shrink: 0; | 421 | width: 38px; height: 38px; border-radius: 50%; flex-shrink: 0; | ||
| 422 | − border: 2px solid var(--ink); background: var(--card2); color: var(--orange-deep); | 422 | + border: 2px solid var(--edge); background: var(--card2); color: var(--orange-deep); | ||
| 423 | cursor: pointer; display: flex; align-items: center; justify-content: center; | 423 | cursor: pointer; display: flex; align-items: center; justify-content: center; | ||
| 424 | align-self: flex-end; margin-bottom: 2px; | 424 | align-self: flex-end; margin-bottom: 2px; | ||
| 425 | } | 425 | } | ||
@@ -431,29 +431,29 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 431 | max-width: 780px; margin: 6px auto 0; display: flex; gap: 6px; flex-wrap: wrap; | 431 | max-width: 780px; margin: 6px auto 0; display: flex; gap: 6px; flex-wrap: wrap; | ||
| 432 | } | 432 | } | ||
| 433 | .slash-hint button { | 433 | .slash-hint button { | ||
| 434 | − border: 1.5px solid var(--ink); background: var(--card2); border-radius: 999px; | 434 | + border: 1.5px solid var(--edge); background: var(--card2); border-radius: 999px; | ||
| 435 | font-size: 12px; font-weight: 700; padding: 5px 11px; cursor: pointer; | 435 | font-size: 12px; font-weight: 700; padding: 5px 11px; cursor: pointer; | ||
| 436 | } | 436 | } | ||
| 437 | 437 | ||||
| 438 | /* ================= Sessions ================= */ | 438 | /* ================= Sessions ================= */ | ||
| 439 | .sess-list { display: flex; flex-direction: column; gap: 12px; max-width: 780px; margin: 0 auto; width: 100%; } | 439 | .sess-list { display: flex; flex-direction: column; gap: 12px; max-width: 780px; margin: 0 auto; width: 100%; } | ||
| 440 | .sess-card { | 440 | .sess-card { | ||
| 441 | − background: var(--card); border: 2px solid var(--ink); border-radius: var(--r-lg); | 441 | + background: var(--card); border: 2px solid var(--edge); border-radius: var(--r-lg); | ||
| 442 | box-shadow: var(--bshadow-sm); padding: 13px 15px; cursor: pointer; | 442 | box-shadow: var(--bshadow-sm); padding: 13px 15px; cursor: pointer; | ||
| 443 | display: flex; flex-direction: column; gap: 6px; | 443 | display: flex; flex-direction: column; gap: 6px; | ||
| 444 | } | 444 | } | ||
| 445 | .sess-top { display: flex; align-items: center; gap: 8px; } | 445 | .sess-top { display: flex; align-items: center; gap: 8px; } | ||
| 446 | .sess-title { flex: 1; min-width: 0; font-weight: 800; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | 446 | .sess-title { flex: 1; min-width: 0; font-weight: 800; font-size: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | ||
| 447 | −.sess-badge { font-size: 11px; font-weight: 800; border-radius: 999px; padding: 3px 10px; border: 1.5px solid var(--ink); flex-shrink: 0; } | 447 | +.sess-badge { font-size: 11px; font-weight: 800; border-radius: 999px; padding: 3px 10px; border: 1.5px solid var(--edge); flex-shrink: 0; } | ||
| 448 | .sess-badge.running { background: var(--orange); animation: pulse 1.4s infinite; } | 448 | .sess-badge.running { background: var(--orange); animation: pulse 1.4s infinite; } | ||
| 449 | .sess-badge.waiting_perm { background: var(--warn-soft); color: var(--warn); } | 449 | .sess-badge.waiting_perm { background: var(--warn-soft); color: var(--warn); } | ||
| 450 | .sess-badge.idle { background: var(--card2); color: var(--muted); } | 450 | .sess-badge.idle { background: var(--card2); color: var(--muted); } | ||
| 451 | .batch-tag { background: var(--orange-soft); color: var(--orange-deep); border-radius: 999px; padding: 1px 8px; font-weight: 800; } | 451 | .batch-tag { background: var(--orange-soft); color: var(--orange-deep); border-radius: 999px; padding: 1px 8px; font-weight: 800; } | ||
| 452 | −.pill-select.all-mode { background: var(--orange); box-shadow: 3px 3px 0 var(--ink); } | 452 | +.pill-select.all-mode { background: var(--orange); box-shadow: 3px 3px 0 var(--shadow); } | ||
| 453 | .sess-info { display: flex; gap: 10px; flex-wrap: wrap; font-size: 11.5px; color: var(--muted); font-family: var(--mono); } | 453 | .sess-info { display: flex; gap: 10px; flex-wrap: wrap; font-size: 11.5px; color: var(--muted); font-family: var(--mono); } | ||
| 454 | .sess-actions { display: flex; gap: 8px; margin-top: 4px; } | 454 | .sess-actions { display: flex; gap: 8px; margin-top: 4px; } | ||
| 455 | .sess-actions button { | 455 | .sess-actions button { | ||
| 456 | − border: 1.5px solid var(--ink); border-radius: 999px; background: var(--card2); | 456 | + border: 1.5px solid var(--edge); border-radius: 999px; background: var(--card2); | ||
| 457 | font-size: 12px; font-weight: 800; padding: 6px 13px; cursor: pointer; | 457 | font-size: 12px; font-weight: 800; padding: 6px 13px; cursor: pointer; | ||
| 458 | } | 458 | } | ||
| 459 | .sess-actions .del { color: var(--bad); } | 459 | .sess-actions .del { color: var(--bad); } | ||
@@ -467,13 +467,13 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 467 | } | 467 | } | ||
| 468 | .sess-actions .p-send { background: var(--orange); color: var(--ink); border-color: var(--ink); box-shadow: var(--bshadow-sm); } | 468 | .sess-actions .p-send { background: var(--orange); color: var(--ink); border-color: var(--ink); box-shadow: var(--bshadow-sm); } | ||
| 469 | .pe-input { | 469 | .pe-input { | ||
| 470 | − width: 100%; background: var(--bg); border: 2px solid var(--ink); color: var(--ink); | 470 | + width: 100%; background: var(--bg); border: 2px solid var(--edge); color: var(--ink); | ||
| 471 | border-radius: var(--r-md); padding: 11px 13px; font-size: 15px; font-family: inherit; font-weight: 600; outline: none; | 471 | border-radius: var(--r-md); padding: 11px 13px; font-size: 15px; font-family: inherit; font-weight: 600; outline: none; | ||
| 472 | } | 472 | } | ||
| 473 | .pe-input:focus { border-color: var(--orange); } | 473 | .pe-input:focus { border-color: var(--orange); } | ||
| 474 | .pe-textarea { | 474 | .pe-textarea { | ||
| 475 | width: 100%; margin-top: 10px; min-height: 180px; resize: vertical; | 475 | width: 100%; margin-top: 10px; min-height: 180px; resize: vertical; | ||
| 476 | − background: var(--bg); border: 2px solid var(--ink); color: var(--ink); | 476 | + background: var(--bg); border: 2px solid var(--edge); color: var(--ink); | ||
| 477 | border-radius: var(--r-md); padding: 12px 13px; font-size: 14px; line-height: 1.5; | 477 | border-radius: var(--r-md); padding: 12px 13px; font-size: 14px; line-height: 1.5; | ||
| 478 | font-family: var(--mono); outline: none; | 478 | font-family: var(--mono); outline: none; | ||
| 479 | } | 479 | } | ||
@@ -490,7 +490,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 490 | max-width: 780px; margin-left: auto; margin-right: auto; | 490 | max-width: 780px; margin-left: auto; margin-right: auto; | ||
| 491 | } | 491 | } | ||
| 492 | .eco-stat { | 492 | .eco-stat { | ||
| 493 | − flex: 1; background: var(--card); border: 2px solid var(--ink); border-radius: var(--r-lg); | 493 | + flex: 1; background: var(--card); border: 2px solid var(--edge); border-radius: var(--r-lg); | ||
| 494 | box-shadow: var(--bshadow-sm); padding: 10px 12px; text-align: center; | 494 | box-shadow: var(--bshadow-sm); padding: 10px 12px; text-align: center; | ||
| 495 | } | 495 | } | ||
| 496 | .eco-stat .v { font-size: 22px; font-weight: 800; letter-spacing: -0.5px; } | 496 | .eco-stat .v { font-size: 22px; font-weight: 800; letter-spacing: -0.5px; } | ||
@@ -504,7 +504,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 504 | @media (min-width: 560px) { .eco-grid { grid-template-columns: 1fr 1fr; } } | 504 | @media (min-width: 560px) { .eco-grid { grid-template-columns: 1fr 1fr; } } | ||
| 505 | .eco-loading { color: var(--muted); text-align: center; padding: 40px; font-weight: 700; } | 505 | .eco-loading { color: var(--muted); text-align: center; padding: 40px; font-weight: 700; } | ||
| 506 | .site-card { | 506 | .site-card { | ||
| 507 | − background: var(--card); border: 2px solid var(--ink); border-radius: var(--r-lg); | 507 | + background: var(--card); border: 2px solid var(--edge); border-radius: var(--r-lg); | ||
| 508 | box-shadow: var(--bshadow-sm); padding: 12px 14px; cursor: pointer; | 508 | box-shadow: var(--bshadow-sm); padding: 12px 14px; cursor: pointer; | ||
| 509 | display: flex; flex-direction: column; gap: 8px; | 509 | display: flex; flex-direction: column; gap: 8px; | ||
| 510 | transition: transform 0.08s; | 510 | transition: transform 0.08s; | ||
@@ -513,7 +513,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 513 | .site-card.down { border-color: var(--bad); box-shadow: 3px 3px 0 var(--bad); } | 513 | .site-card.down { border-color: var(--bad); box-shadow: 3px 3px 0 var(--bad); } | ||
| 514 | .site-top { display: flex; align-items: center; gap: 8px; } | 514 | .site-top { display: flex; align-items: center; gap: 8px; } | ||
| 515 | .site-name { font-weight: 800; font-size: 16px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | 515 | .site-name { font-weight: 800; font-size: 16px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | ||
| 516 | −.st-pill { font-size: 10.5px; font-weight: 800; border-radius: 999px; padding: 3px 10px; border: 1.5px solid var(--ink); flex-shrink: 0; display: flex; gap: 4px; align-items: center; } | 516 | +.st-pill { font-size: 10.5px; font-weight: 800; border-radius: 999px; padding: 3px 10px; border: 1.5px solid var(--edge); flex-shrink: 0; display: flex; gap: 4px; align-items: center; } | ||
| 517 | .st-pill.up { background: var(--good-soft); color: var(--good); } | 517 | .st-pill.up { background: var(--good-soft); color: var(--good); } | ||
| 518 | .st-pill.slow { background: var(--warn-soft); color: var(--warn); } | 518 | .st-pill.slow { background: var(--warn-soft); color: var(--warn); } | ||
| 519 | .st-pill.down { background: var(--bad); color: #fff; } | 519 | .st-pill.down { background: var(--bad); color: #fff; } | ||
@@ -530,11 +530,11 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 530 | .eco-subhead { max-width: 780px; margin: 20px auto 10px; width: 100%; font-size: 15px; font-weight: 800; letter-spacing: -0.3px; } | 530 | .eco-subhead { max-width: 780px; margin: 20px auto 10px; width: 100%; font-size: 15px; font-weight: 800; letter-spacing: -0.3px; } | ||
| 531 | .node-grid { display: grid; grid-template-columns: 1fr; gap: 10px; max-width: 780px; margin: 0 auto; width: 100%; } | 531 | .node-grid { display: grid; grid-template-columns: 1fr; gap: 10px; max-width: 780px; margin: 0 auto; width: 100%; } | ||
| 532 | @media (min-width: 560px) { .node-grid { grid-template-columns: 1fr 1fr; } } | 532 | @media (min-width: 560px) { .node-grid { grid-template-columns: 1fr 1fr; } } | ||
| 533 | −.node-card { background: var(--card); border: 2px solid var(--ink); border-radius: var(--r-lg); box-shadow: var(--bshadow-sm); padding: 12px 14px; } | 533 | +.node-card { background: var(--card); border: 2px solid var(--edge); border-radius: var(--r-lg); box-shadow: var(--bshadow-sm); padding: 12px 14px; } | ||
| 534 | .node-card.warn { border-color: var(--warn); } | 534 | .node-card.warn { border-color: var(--warn); } | ||
| 535 | .node-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 9px; } | 535 | .node-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 9px; } | ||
| 536 | .node-name { font-weight: 800; font-size: 15px; font-family: var(--mono); } | 536 | .node-name { font-weight: 800; font-size: 15px; font-family: var(--mono); } | ||
| 537 | −.node-apps { font-size: 11px; font-weight: 800; border: 1.5px solid var(--ink); border-radius: 999px; padding: 2px 9px; background: var(--good-soft); color: var(--good); } | 537 | +.node-apps { font-size: 11px; font-weight: 800; border: 1.5px solid var(--edge); border-radius: 999px; padding: 2px 9px; background: var(--good-soft); color: var(--good); } | ||
| 538 | .node-apps.bad { background: var(--warn-soft); color: var(--warn); } | 538 | .node-apps.bad { background: var(--warn-soft); color: var(--warn); } | ||
| 539 | .nbar { margin: 5px 0; } | 539 | .nbar { margin: 5px 0; } | ||
| 540 | .nbar-l { display: flex; justify-content: space-between; font-size: 11px; font-weight: 700; color: var(--muted); margin-bottom: 2px; } | 540 | .nbar-l { display: flex; justify-content: space-between; font-size: 11px; font-weight: 700; color: var(--muted); margin-bottom: 2px; } | ||
@@ -546,7 +546,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 546 | .admin-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } | 546 | .admin-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } | ||
| 547 | .admin-actions .btn-ghost { text-align: center; text-decoration: none; } | 547 | .admin-actions .btn-ghost { text-align: center; text-decoration: none; } | ||
| 548 | .admin-out { margin-top: 10px; } | 548 | .admin-out { margin-top: 10px; } | ||
| 549 | −.admin-out pre { background: var(--ink); color: #e8e6df; border-radius: var(--r-sm); padding: 10px; font-size: 11px; font-family: var(--mono); overflow-x: auto; max-height: 320px; overflow-y: auto; white-space: pre-wrap; word-break: break-word; margin: 0; } | 549 | +.admin-out pre { background: var(--term-bg); color: var(--term-ink); border-radius: var(--r-sm); padding: 10px; font-size: 11px; font-family: var(--mono); overflow-x: auto; max-height: 320px; overflow-y: auto; white-space: pre-wrap; word-break: break-word; margin: 0; } | ||
| 550 | .admin-load { color: var(--muted); font-weight: 700; font-size: 13px; } | 550 | .admin-load { color: var(--muted); font-weight: 700; font-size: 13px; } | ||
| 551 | .admin-ok { color: var(--good); font-weight: 800; } | 551 | .admin-ok { color: var(--good); font-weight: 800; } | ||
| 552 | .admin-err { color: var(--bad); font-weight: 800; } | 552 | .admin-err { color: var(--bad); font-weight: 800; } | ||
@@ -558,7 +558,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 558 | } | 558 | } | ||
| 559 | @media (min-width: 560px) { .eco-overview { grid-template-columns: repeat(6, 1fr); } } | 559 | @media (min-width: 560px) { .eco-overview { grid-template-columns: repeat(6, 1fr); } } | ||
| 560 | .kpi { | 560 | .kpi { | ||
| 561 | − background: var(--card); border: 2px solid var(--ink); border-radius: var(--r-md); | 561 | + background: var(--card); border: 2px solid var(--edge); border-radius: var(--r-md); | ||
| 562 | box-shadow: var(--bshadow-sm); padding: 9px 8px; text-align: center; | 562 | box-shadow: var(--bshadow-sm); padding: 9px 8px; text-align: center; | ||
| 563 | } | 563 | } | ||
| 564 | .kpi .v { font-size: 21px; font-weight: 800; letter-spacing: -0.5px; line-height: 1; } | 564 | .kpi .v { font-size: 21px; font-weight: 800; letter-spacing: -0.5px; line-height: 1; } | ||
@@ -599,7 +599,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 599 | .sheet-panel { | 599 | .sheet-panel { | ||
| 600 | position: absolute; left: 0; right: 0; bottom: 0; | 600 | position: absolute; left: 0; right: 0; bottom: 0; | ||
| 601 | max-height: 88dvh; overflow-y: auto; -webkit-overflow-scrolling: touch; | 601 | max-height: 88dvh; overflow-y: auto; -webkit-overflow-scrolling: touch; | ||
| 602 | − background: var(--bg); border: 2px solid var(--ink); border-bottom: none; | 602 | + background: var(--bg); border: 2px solid var(--edge); border-bottom: none; | ||
| 603 | border-radius: var(--r-xl) var(--r-xl) 0 0; | 603 | border-radius: var(--r-xl) var(--r-xl) 0 0; | ||
| 604 | padding: 8px 16px calc(20px + env(safe-area-inset-bottom)); | 604 | padding: 8px 16px calc(20px + env(safe-area-inset-bottom)); | ||
| 605 | animation: sheet-up 0.22s ease-out; | 605 | animation: sheet-up 0.22s ease-out; | ||
@@ -607,7 +607,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 607 | .sheet-grip { width: 42px; height: 5px; border-radius: 3px; background: var(--line); margin: 6px auto 12px; } | 607 | .sheet-grip { width: 42px; height: 5px; border-radius: 3px; background: var(--line); margin: 6px auto 12px; } | ||
| 608 | .sheet-h { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; } | 608 | .sheet-h { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; } | ||
| 609 | .sheet-h h3 { margin: 0; font-size: 21px; font-weight: 800; flex: 1; } | 609 | .sheet-h h3 { margin: 0; font-size: 21px; font-weight: 800; flex: 1; } | ||
| 610 | −.sheet-sec { background: var(--card); border: 2px solid var(--ink); border-radius: var(--r-lg); box-shadow: var(--bshadow-sm); padding: 13px 15px; margin-bottom: 12px; } | 610 | +.sheet-sec { background: var(--card); border: 2px solid var(--edge); border-radius: var(--r-lg); box-shadow: var(--bshadow-sm); padding: 13px 15px; margin-bottom: 12px; } | ||
| 611 | .sheet-sec h4 { margin: 0 0 9px; font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.7px; color: var(--muted); } | 611 | .sheet-sec h4 { margin: 0 0 9px; font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.7px; color: var(--muted); } | ||
| 612 | .kv { display: flex; justify-content: space-between; gap: 10px; padding: 4px 0; font-size: 13.5px; } | 612 | .kv { display: flex; justify-content: space-between; gap: 10px; padding: 4px 0; font-size: 13.5px; } | ||
| 613 | .kv b { font-family: var(--mono); font-weight: 700; } | 613 | .kv b { font-family: var(--mono); font-weight: 700; } | ||
@@ -629,14 +629,14 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 629 | /* ================= Réglages ================= */ | 629 | /* ================= Réglages ================= */ | ||
| 630 | .set-card { | 630 | .set-card { | ||
| 631 | max-width: 780px; margin: 0 auto 12px; width: 100%; | 631 | max-width: 780px; margin: 0 auto 12px; width: 100%; | ||
| 632 | − background: var(--card); border: 2px solid var(--ink); border-radius: var(--r-lg); | 632 | + background: var(--card); border: 2px solid var(--edge); border-radius: var(--r-lg); | ||
| 633 | box-shadow: var(--bshadow-sm); padding: 14px 16px; | 633 | box-shadow: var(--bshadow-sm); padding: 14px 16px; | ||
| 634 | } | 634 | } | ||
| 635 | .set-label { font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.7px; color: var(--muted); margin-bottom: 10px; } | 635 | .set-label { font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.7px; color: var(--muted); margin-bottom: 10px; } | ||
| 636 | .seg { display: flex; gap: 7px; } | 636 | .seg { display: flex; gap: 7px; } | ||
| 637 | .seg.vert { flex-direction: column; } | 637 | .seg.vert { flex-direction: column; } | ||
| 638 | .seg button { | 638 | .seg button { | ||
| 639 | − flex: 1; border: 1.5px solid var(--ink); border-radius: 999px; | 639 | + flex: 1; border: 1.5px solid var(--edge); border-radius: 999px; | ||
| 640 | background: var(--card2); font-weight: 700; font-size: 13.5px; | 640 | background: var(--card2); font-weight: 700; font-size: 13.5px; | ||
| 641 | padding: 10px 12px; cursor: pointer; text-align: center; | 641 | padding: 10px 12px; cursor: pointer; text-align: center; | ||
| 642 | } | 642 | } | ||
@@ -647,16 +647,16 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 647 | 647 | ||||
| 648 | 648 | ||||
| 649 | /* ---- Social ---- */ | 649 | /* ---- Social ---- */ | ||
| 650 | −.soc-card{background:var(--card,#fff);border:2px solid var(--ink,#1a1a1a);border-radius:16px;padding:14px 16px;margin:0 0 14px;box-shadow:3px 3px 0 var(--ink,#1a1a1a);} | 650 | +.soc-card{background:var(--card,#fff);border:2px solid var(--edge);border-radius:16px;padding:14px 16px;margin:0 0 14px;box-shadow:3px 3px 0 var(--shadow);} | ||
| 651 | .soc-card h3{margin:0 0 10px;font-size:15px;} | 651 | .soc-card h3{margin:0 0 10px;font-size:15px;} | ||
| 652 | .soc-switch{display:flex;align-items:center;gap:10px;font-weight:700;cursor:pointer;} | 652 | .soc-switch{display:flex;align-items:center;gap:10px;font-weight:700;cursor:pointer;} | ||
| 653 | .soc-switch input{width:20px;height:20px;accent-color:var(--accent,#f97316);} | 653 | .soc-switch input{width:20px;height:20px;accent-color:var(--accent,#f97316);} | ||
| 654 | .soc-sub{color:var(--muted,#6b7280);font-size:13px;margin-top:8px;} | 654 | .soc-sub{color:var(--muted,#6b7280);font-size:13px;margin-top:8px;} | ||
| 655 | .soc-actions{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-top:12px;} | 655 | .soc-actions{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-top:12px;} | ||
| 656 | −.soc-select{padding:9px 12px;border:2px solid var(--ink,#1a1a1a);border-radius:10px;font:inherit;background:#fff;} | 656 | +.soc-select{padding:9px 12px;border:2px solid var(--edge);border-radius:10px;font:inherit;background:#fff;} | ||
| 657 | .soc-draft{margin-top:14px;display:flex;flex-direction:column;gap:10px;} | 657 | .soc-draft{margin-top:14px;display:flex;flex-direction:column;gap:10px;} | ||
| 658 | −.soc-draft img{width:100%;max-width:340px;border-radius:12px;border:2px solid var(--ink,#1a1a1a);align-self:center;} | 658 | +.soc-draft img{width:100%;max-width:340px;border-radius:12px;border:2px solid var(--edge);align-self:center;} | ||
| 659 | −#view-social textarea{width:100%;padding:10px 12px;border:2px solid var(--ink,#1a1a1a);border-radius:10px;font:inherit;resize:vertical;} | 659 | +#view-social textarea{width:100%;padding:10px 12px;border:2px solid var(--edge);border-radius:10px;font:inherit;resize:vertical;} | ||
| 660 | .soc-log{display:flex;flex-direction:column;gap:6px;max-height:340px;overflow-y:auto;} | 660 | .soc-log{display:flex;flex-direction:column;gap:6px;max-height:340px;overflow-y:auto;} | ||
| 661 | .soc-logrow{font-size:13px;padding:6px 8px;border-radius:8px;background:rgba(0,0,0,.04);} | 661 | .soc-logrow{font-size:13px;padding:6px 8px;border-radius:8px;background:rgba(0,0,0,.04);} | ||
| 662 | .soc-logt{color:var(--muted,#6b7280);font-variant-numeric:tabular-nums;margin-right:6px;} | 662 | .soc-logt{color:var(--muted,#6b7280);font-variant-numeric:tabular-nums;margin-right:6px;} | ||
@@ -669,7 +669,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 669 | @media(max-width:640px){.st-grid{grid-template-columns:1fr}} | 669 | @media(max-width:640px){.st-grid{grid-template-columns:1fr}} | ||
| 670 | .st-gen h3{margin:0 0 8px} | 670 | .st-gen h3{margin:0 0 8px} | ||
| 671 | .st-gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px;margin-top:8px} | 671 | .st-gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:14px;margin-top:8px} | ||
| 672 | −.st-item{background:var(--card,#fff);border:2px solid var(--ink,#1a1a1a);border-radius:14px;overflow:hidden;box-shadow:3px 3px 0 var(--ink,#1a1a1a);display:flex;flex-direction:column} | 672 | +.st-item{background:var(--card,#fff);border:2px solid var(--edge);border-radius:14px;overflow:hidden;box-shadow:3px 3px 0 var(--shadow);display:flex;flex-direction:column} | ||
| 673 | .st-media{position:relative;aspect-ratio:4/5;background:#0d0d0d;overflow:hidden} | 673 | .st-media{position:relative;aspect-ratio:4/5;background:#0d0d0d;overflow:hidden} | ||
| 674 | .st-media img,.st-media video{width:100%;height:100%;object-fit:cover;display:block} | 674 | .st-media img,.st-media video{width:100%;height:100%;object-fit:cover;display:block} | ||
| 675 | .st-badge{position:absolute;left:8px;top:8px;background:rgba(13,13,13,.82);color:#fff;font-size:11px;font-weight:800;padding:4px 9px;border-radius:999px;letter-spacing:.02em} | 675 | .st-badge{position:absolute;left:8px;top:8px;background:rgba(13,13,13,.82);color:#fff;font-size:11px;font-weight:800;padding:4px 9px;border-radius:999px;letter-spacing:.02em} | ||
@@ -681,7 +681,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 681 | /* ---- Studio loading ---- */ | 681 | /* ---- Studio loading ---- */ | ||
| 682 | #stLoad{position:fixed;inset:0;z-index:9999;display:none;align-items:center;justify-content:center;background:rgba(8,8,10,.72);backdrop-filter:blur(4px)} | 682 | #stLoad{position:fixed;inset:0;z-index:9999;display:none;align-items:center;justify-content:center;background:rgba(8,8,10,.72);backdrop-filter:blur(4px)} | ||
| 683 | #stLoad.on{display:flex} | 683 | #stLoad.on{display:flex} | ||
| 684 | −.stload-box{background:var(--card,#141414);border:2px solid var(--ink,#1a1a1a);border-radius:18px;padding:26px 30px;text-align:center;box-shadow:4px 4px 0 var(--ink,#1a1a1a);max-width:80vw} | 684 | +.stload-box{background:var(--card,#141414);border:2px solid var(--edge);border-radius:18px;padding:26px 30px;text-align:center;box-shadow:4px 4px 0 var(--shadow);max-width:80vw} | ||
| 685 | .stload-spin{width:44px;height:44px;margin:0 auto 14px;border:5px solid rgba(127,127,127,.25);border-top-color:var(--accent,#f97316);border-radius:50%;animation:stspin .8s linear infinite} | 685 | .stload-spin{width:44px;height:44px;margin:0 auto 14px;border:5px solid rgba(127,127,127,.25);border-top-color:var(--accent,#f97316);border-radius:50%;animation:stspin .8s linear infinite} | ||
| 686 | @keyframes stspin{to{transform:rotate(360deg)}} | 686 | @keyframes stspin{to{transform:rotate(360deg)}} | ||
| 687 | .stload-msg{font-weight:800;font-size:15px} | 687 | .stload-msg{font-weight:800;font-size:15px} | ||
@@ -693,11 +693,11 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 693 | /* ---- Visiteurs ---- */ | 693 | /* ---- Visiteurs ---- */ | ||
| 694 | #view-visitors{overflow-y:auto;-webkit-overflow-scrolling:touch;padding:14px 14px calc(86px + env(safe-area-inset-bottom))} | 694 | #view-visitors{overflow-y:auto;-webkit-overflow-scrolling:touch;padding:14px 14px calc(86px + env(safe-area-inset-bottom))} | ||
| 695 | .vis-global{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:14px} | 695 | .vis-global{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:14px} | ||
| 696 | −.vg-item{background:var(--card,#fff);border:2px solid var(--ink,#1a1a1a);border-radius:14px;padding:14px;text-align:center;box-shadow:3px 3px 0 var(--ink,#1a1a1a)} | 696 | +.vg-item{background:var(--card,#fff);border:2px solid var(--edge);border-radius:14px;padding:14px;text-align:center;box-shadow:3px 3px 0 var(--shadow)} | ||
| 697 | .vg-n{font-size:26px;font-weight:900;color:var(--accent,#f97316)} | 697 | .vg-n{font-size:26px;font-weight:900;color:var(--accent,#f97316)} | ||
| 698 | .vg-l{font-size:11px;font-weight:700;color:var(--muted,#777);text-transform:uppercase;letter-spacing:.05em;margin-top:2px} | 698 | .vg-l{font-size:11px;font-weight:700;color:var(--muted,#777);text-transform:uppercase;letter-spacing:.05em;margin-top:2px} | ||
| 699 | .vis-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:14px} | 699 | .vis-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:14px} | ||
| 700 | −.vis-card{background:var(--card,#fff);border:2px solid var(--ink,#1a1a1a);border-radius:16px;padding:16px;box-shadow:3px 3px 0 var(--ink,#1a1a1a)} | 700 | +.vis-card{background:var(--card,#fff);border:2px solid var(--edge);border-radius:16px;padding:16px;box-shadow:3px 3px 0 var(--shadow)} | ||
| 701 | .vis-h{display:flex;align-items:center;gap:10px} | 701 | .vis-h{display:flex;align-items:center;gap:10px} | ||
| 702 | .vis-h img{width:26px;height:26px;border-radius:6px} | 702 | .vis-h img{width:26px;height:26px;border-radius:6px} | ||
| 703 | .vis-name{font-weight:800;font-size:15px;flex:1} | 703 | .vis-name{font-weight:800;font-size:15px;flex:1} | ||
@@ -718,7 +718,7 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 718 | .vis-card:active{transform:scale(.98)} | 718 | .vis-card:active{transform:scale(.98)} | ||
| 719 | #visDetail{position:fixed;inset:0;z-index:9998;display:none;align-items:flex-start;justify-content:center;background:rgba(8,8,10,.72);backdrop-filter:blur(4px);overflow-y:auto;padding:40px 14px} | 719 | #visDetail{position:fixed;inset:0;z-index:9998;display:none;align-items:flex-start;justify-content:center;background:rgba(8,8,10,.72);backdrop-filter:blur(4px);overflow-y:auto;padding:40px 14px} | ||
| 720 | #visDetail.on{display:flex} | 720 | #visDetail.on{display:flex} | ||
| 721 | −.vd-box{background:var(--card,#141414);border:2px solid var(--ink,#1a1a1a);border-radius:18px;width:100%;max-width:720px;box-shadow:5px 5px 0 var(--ink,#1a1a1a)} | 721 | +.vd-box{background:var(--card,#141414);border:2px solid var(--edge);border-radius:18px;width:100%;max-width:720px;box-shadow:5px 5px 0 var(--shadow)} | ||
| 722 | .vd-head{display:flex;align-items:center;gap:12px;padding:16px 18px;border-bottom:1px solid rgba(127,127,127,.2)} | 722 | .vd-head{display:flex;align-items:center;gap:12px;padding:16px 18px;border-bottom:1px solid rgba(127,127,127,.2)} | ||
| 723 | .vd-head img{width:30px;height:30px;border-radius:8px} | 723 | .vd-head img{width:30px;height:30px;border-radius:8px} | ||
| 724 | .vd-head h3{flex:1;font-size:18px} | 724 | .vd-head h3{flex:1;font-size:18px} | ||
@@ -735,3 +735,245 @@ details.thinking-box summary { color: var(--muted); font-size: 12.5px; cursor: p | |||||
| 735 | .vd-bar{flex:1;height:14px;background:rgba(127,127,127,.15);border-radius:7px;overflow:hidden} | 735 | .vd-bar{flex:1;height:14px;background:rgba(127,127,127,.15);border-radius:7px;overflow:hidden} | ||
| 736 | .vd-bar>div{height:100%;background:var(--accent,#f97316);border-radius:7px} | 736 | .vd-bar>div{height:100%;background:var(--accent,#f97316);border-radius:7px} | ||
| 737 | .vd-geoN{font-size:13px;font-weight:800;width:44px;text-align:right} | 737 | .vd-geoN{font-size:13px;font-weight:800;width:44px;text-align:right} | ||
| 738 | + | ||||
| 739 | +/* ==================================================================== | ||||
| 740 | + Admin-Ka v3 — Control Center : layout desktop, dark mode, densité | ||||
| 741 | + ==================================================================== */ | ||||
| 742 | +:root { | ||||
| 743 | + --shadow: #1a1611; | ||||
| 744 | + --edge: #1a1611; | ||||
| 745 | + --on-accent: #1a1611; | ||||
| 746 | + --code-bg: #f2efe7; | ||||
| 747 | + --term-bg: #1a1611; | ||||
| 748 | + --term-ink: #e8e6df; | ||||
| 749 | + --del-bg: #ffe4e1; --del-ink: #8c1622; | ||||
| 750 | + --add-bg: #ddf3e4; --add-ink: #0c5326; | ||||
| 751 | + --panel-gap: 14px; | ||||
| 752 | +} | ||||
| 753 | +:root[data-theme="dark"] { | ||||
| 754 | + --bg: #16130e; | ||||
| 755 | + --card: #201c15; | ||||
| 756 | + --card2: #2a251c; | ||||
| 757 | + --ink: #ede6d8; | ||||
| 758 | + --muted: #a39a89; | ||||
| 759 | + --line: #3a342a; | ||||
| 760 | + --grid: #322d24; | ||||
| 761 | + --edge: #4a4335; | ||||
| 762 | + --shadow: #0a0806; | ||||
| 763 | + --orange-soft: #3d2510; | ||||
| 764 | + --good: #3fbf78; --good-soft: #14301f; | ||||
| 765 | + --warn: #e0a93e; --warn-soft: #362a10; | ||||
| 766 | + --bad: #f0616d; --bad-soft: #3a1518; | ||||
| 767 | + --code-bg: #17140f; | ||||
| 768 | + --del-bg: #3d1a1d; --del-ink: #f4a9ad; | ||||
| 769 | + --add-bg: #12301c; --add-ink: #9fdcb2; | ||||
| 770 | +} | ||||
| 771 | +:root[data-theme="dark"] .hljs { background: var(--code-bg); color: #d8d2c4; } | ||||
| 772 | +:root[data-theme="dark"] .site-logo, :root[data-theme="dark"] .vis-h img, :root[data-theme="dark"] .rt-row img { background: #f4f1ea; } | ||||
| 773 | + | ||||
| 774 | +/* texte sombre garanti sur les fonds orange (les deux thèmes) */ | ||||
| 775 | +.btn-cta, .ka-mark, .send-btn:not(.stop), .msg-user, .seg button.on, | ||||
| 776 | +.pill-select.all-mode, .sess-badge.running, .perm-allall { color: var(--on-accent); } | ||||
| 777 | +:root[data-theme="dark"] .perm-allall { background: var(--orange); color: var(--on-accent); } | ||||
| 778 | + | ||||
| 779 | +/* ---------- coquille : sidebar + colonne principale ---------- */ | ||||
| 780 | +.app { display: flex; flex-direction: row; height: 100dvh; } | ||||
| 781 | +.main-col { flex: 1; min-width: 0; display: flex; flex-direction: column; height: 100dvh; } | ||||
| 782 | +.sidebar { display: none; } | ||||
| 783 | +.desktop-only { display: none !important; } | ||||
| 784 | +.page-title { margin: 0; font-size: 17px; font-weight: 800; letter-spacing: -0.3px; } | ||||
| 785 | +.head-right { display: flex; align-items: center; gap: 8px; } | ||||
| 786 | +.icon-btn { | ||||
| 787 | + position: relative; width: 34px; height: 34px; border-radius: 10px; | ||||
| 788 | + border: 1.5px solid var(--edge); background: var(--card); cursor: pointer; | ||||
| 789 | + display: flex; align-items: center; justify-content: center; color: var(--ink); | ||||
| 790 | +} | ||||
| 791 | +.icon-btn svg { width: 17px; height: 17px; } | ||||
| 792 | +.icon-btn .badge { position: absolute; top: -4px; right: -4px; width: 10px; height: 10px; border-radius: 50%; background: var(--bad); border: 1.5px solid var(--edge); } | ||||
| 793 | +.top-stamp { font-size: 11px; color: var(--muted); font-family: var(--mono); } | ||||
| 794 | +.view.scroll { overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 14px 14px calc(86px + env(safe-area-inset-bottom)); } | ||||
| 795 | + | ||||
| 796 | +@media (min-width: 1020px) { | ||||
| 797 | + .desktop-only { display: block !important; } | ||||
| 798 | + .mobile-only { display: none !important; } | ||||
| 799 | + .bottomnav { display: none; } | ||||
| 800 | + .sidebar { | ||||
| 801 | + display: flex; flex-direction: column; width: 224px; flex-shrink: 0; | ||||
| 802 | + background: var(--card); border-right: 2px solid var(--edge); | ||||
| 803 | + padding: 14px 10px 12px; | ||||
| 804 | + } | ||||
| 805 | + .side-brand { display: flex; align-items: center; gap: 10px; font-weight: 800; font-size: 18px; letter-spacing: -0.4px; padding: 2px 8px 14px; } | ||||
| 806 | + .side-nav { display: flex; flex-direction: column; gap: 2px; flex: 1; } | ||||
| 807 | + .side-nav .nav-btn { | ||||
| 808 | + flex-direction: row; justify-content: flex-start; gap: 10px; | ||||
| 809 | + font-size: 13.5px; font-weight: 700; color: var(--muted); | ||||
| 810 | + padding: 9px 12px; border-radius: 10px; text-align: left; | ||||
| 811 | + } | ||||
| 812 | + .side-nav .nav-btn svg { width: 18px; height: 18px; flex-shrink: 0; } | ||||
| 813 | + .side-nav .nav-btn.active { background: var(--orange-soft); color: var(--ink); border: 1.5px solid var(--edge); padding: 7.5px 10.5px; } | ||||
| 814 | + .side-nav .nav-btn.active svg { color: var(--orange-deep); } | ||||
| 815 | + .side-nav .nav-btn .badge { position: absolute; top: 8px; right: 10px; left: auto; } | ||||
| 816 | + .side-foot { display: flex; flex-direction: column; gap: 8px; padding: 10px 8px 0; border-top: 1.5px solid var(--line); } | ||||
| 817 | + .side-node { font-size: 11px; color: var(--muted); } | ||||
| 818 | + .header { padding: 10px 20px; } | ||||
| 819 | + .head-right #connPill { display: none; } | ||||
| 820 | + .view.scroll { padding: 18px 24px 40px; } | ||||
| 821 | + .messages, .composer-inner, .slash-hint { max-width: 920px; } | ||||
| 822 | + .eco-grid, .eco-overview, .node-grid, .eco-subhead, .sess-list, .set-card { max-width: 1180px; } | ||||
| 823 | + .eco-grid { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); } | ||||
| 824 | + .node-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); } | ||||
| 825 | + .eco-overview { grid-template-columns: repeat(6, 1fr); } | ||||
| 826 | + html, body { font-size: 14.5px; } | ||||
| 827 | +} | ||||
| 828 | + | ||||
| 829 | +/* ---------- KPI v3 ---------- */ | ||||
| 830 | +.ov-kpis { | ||||
| 831 | + display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); | ||||
| 832 | + gap: 10px; margin-bottom: var(--panel-gap); max-width: 1180px; | ||||
| 833 | +} | ||||
| 834 | +.ov-kpis.compact { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); } | ||||
| 835 | +.kpi2 { | ||||
| 836 | + background: var(--card); border: 1.5px solid var(--edge); border-radius: var(--r-md); | ||||
| 837 | + padding: 10px 12px; box-shadow: var(--bshadow-sm); | ||||
| 838 | +} | ||||
| 839 | +.kpi2 .kv2 { font-size: 24px; font-weight: 800; letter-spacing: -0.6px; line-height: 1.1; font-variant-numeric: tabular-nums; } | ||||
| 840 | +.kpi2 .kv2 small { font-size: 12px; color: var(--muted); font-weight: 700; } | ||||
| 841 | +.kpi2 .kl2 { font-size: 10px; color: var(--muted); font-weight: 800; text-transform: uppercase; letter-spacing: 0.5px; margin-top: 2px; } | ||||
| 842 | +.kpi2.accent .kv2 { color: var(--orange-deep); } | ||||
| 843 | +.kpi2.good .kv2 { color: var(--good); } | ||||
| 844 | +.kpi2.bad .kv2 { color: var(--bad); } | ||||
| 845 | +.kdelta { display: inline-block; font-size: 11px; font-weight: 800; margin-top: 3px; border-radius: 999px; padding: 1px 8px; } | ||||
| 846 | +.kdelta.up { background: var(--good-soft); color: var(--good); } | ||||
| 847 | +.kdelta.down { background: var(--bad-soft); color: var(--bad); } | ||||
| 848 | +.kdelta.flat { background: var(--card2); color: var(--muted); } | ||||
| 849 | + | ||||
| 850 | +/* ---------- panneaux ---------- */ | ||||
| 851 | +.ov-cols { display: grid; grid-template-columns: 1fr; gap: var(--panel-gap); margin-bottom: var(--panel-gap); max-width: 1180px; } | ||||
| 852 | +@media (min-width: 900px) { .ov-cols { grid-template-columns: 3fr 2fr; } .ov-cols + .ov-cols { grid-template-columns: 1fr 1fr; } } | ||||
| 853 | +.panel { | ||||
| 854 | + background: var(--card); border: 2px solid var(--edge); border-radius: var(--r-lg); | ||||
| 855 | + box-shadow: var(--bshadow-sm); padding: 14px 16px; min-width: 0; | ||||
| 856 | +} | ||||
| 857 | +.panel.slim { padding: 10px 14px; margin-bottom: var(--panel-gap); max-width: 1180px; } | ||||
| 858 | +.panel-h { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 10px; } | ||||
| 859 | +.panel-h h3 { margin: 0; font-size: 13px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.6px; color: var(--muted); } | ||||
| 860 | +.panel-note { font-size: 11px; color: var(--muted); font-weight: 600; } | ||||
| 861 | +.linklike { background: none; border: none; color: var(--orange-deep); font-weight: 800; font-size: 12px; cursor: pointer; padding: 0; } | ||||
| 862 | +.sub-h { margin: 0; font-size: 16px; font-weight: 800; letter-spacing: -0.3px; } | ||||
| 863 | +.defs-note { max-width: 1180px; color: var(--muted); font-size: 11.5px; line-height: 1.6; margin: 14px 2px 0; } | ||||
| 864 | + | ||||
| 865 | +/* écosystème compact (overview) */ | ||||
| 866 | +.ov-eco-chips { display: flex; flex-wrap: wrap; gap: 6px; } | ||||
| 867 | +.eco-chip { | ||||
| 868 | + display: inline-flex; align-items: center; gap: 6px; cursor: pointer; | ||||
| 869 | + border: 1.5px solid var(--edge); border-radius: 999px; padding: 4px 10px; | ||||
| 870 | + font-size: 11.5px; font-weight: 700; background: var(--card2); | ||||
| 871 | +} | ||||
| 872 | +.eco-chip b { font-family: var(--mono); font-weight: 700; color: var(--muted); } | ||||
| 873 | +.eco-chip.up { color: var(--good); } | ||||
| 874 | +.eco-chip.slow { color: var(--warn); background: var(--warn-soft); } | ||||
| 875 | +.eco-chip.down { color: #fff; background: var(--bad); border-color: var(--bad); } | ||||
| 876 | +.eco-chip.down b { color: #fff; } | ||||
| 877 | +.ov-sess { display: flex; align-items: center; gap: 9px; padding: 7px 0; border-bottom: 1.5px solid var(--line); cursor: pointer; } | ||||
| 878 | +.ov-sess:last-child { border-bottom: none; } | ||||
| 879 | +.ov-sess-t { flex: 1; min-width: 0; font-weight: 700; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | ||||
| 880 | +.ov-sess-m { font-size: 11px; color: var(--muted); flex-shrink: 0; } | ||||
| 881 | +.anom { font-size: 12.5px; padding: 7px 0; border-bottom: 1.5px solid var(--line); font-weight: 600; } | ||||
| 882 | +.anom:last-child { border-bottom: none; } | ||||
| 883 | +.anom.warn { color: var(--warn); font-weight: 800; } | ||||
| 884 | + | ||||
| 885 | +/* répartition humains / bots / datacenter */ | ||||
| 886 | +.break-bar { display: flex; height: 12px; border-radius: 999px; overflow: hidden; background: var(--card2); border: 1.5px solid var(--line); } | ||||
| 887 | +.break-bar i { display: block; height: 100%; } | ||||
| 888 | + | ||||
| 889 | +/* temps réel */ | ||||
| 890 | +.rt-list { display: flex; flex-direction: column; max-height: 420px; overflow-y: auto; } | ||||
| 891 | +.rt-row { | ||||
| 892 | + display: flex; align-items: center; gap: 8px; | ||||
| 893 | + padding: 6px 0; border-bottom: 1.5px solid var(--line); font-size: 12.5px; | ||||
| 894 | +} | ||||
| 895 | +.rt-row:last-child { border-bottom: none; } | ||||
| 896 | +.rt-row img { width: 18px; height: 18px; border-radius: 5px; flex-shrink: 0; } | ||||
| 897 | +.rt-site { font-weight: 800; flex-shrink: 0; } | ||||
| 898 | +.rt-path { color: var(--muted); font-size: 11px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | ||||
| 899 | +.rt-loc { color: var(--ink); font-weight: 600; font-size: 11.5px; flex-shrink: 0; } | ||||
| 900 | +.rt-dev { font-size: 10.5px; flex-shrink: 0; } | ||||
| 901 | +.rt-dur { color: var(--muted); font-size: 10.5px; flex-shrink: 0; } | ||||
| 902 | + | ||||
| 903 | +/* visiteurs v2 */ | ||||
| 904 | +.vis-grid { max-width: 1180px; } | ||||
| 905 | +.vis-card { padding: 12px 14px; box-shadow: var(--bshadow-sm); border-width: 2px; } | ||||
| 906 | +.vis-on-pill { | ||||
| 907 | + font-size: 11px; font-weight: 800; border-radius: 999px; padding: 3px 10px; | ||||
| 908 | + border: 1.5px solid var(--line); color: var(--muted); background: var(--card2); flex-shrink: 0; | ||||
| 909 | +} | ||||
| 910 | +.vis-on-pill.on { background: var(--good-soft); color: var(--good); border-color: var(--good); } | ||||
| 911 | +.vis-row { margin-top: 10px; } | ||||
| 912 | +.vis-m { background: var(--card2); border: 1.5px solid var(--line); } | ||||
| 913 | +.vm-n { font-size: 17px; font-variant-numeric: tabular-nums; } | ||||
| 914 | +.vis-pv { display: flex; justify-content: space-between; align-items: center; margin-top: 8px; font-size: 11.5px; } | ||||
| 915 | +.vis-more { color: var(--orange-deep); font-weight: 800; } | ||||
| 916 | +.seg.mini button { padding: 6px 12px; font-size: 12px; } | ||||
| 917 | +.seg.mini { gap: 5px; } | ||||
| 918 | + | ||||
| 919 | +/* filtres listes */ | ||||
| 920 | +.filterbar { display: flex; gap: 8px; flex-wrap: wrap; margin: 0 0 12px; max-width: 1180px; } | ||||
| 921 | +.f-input, .f-select { | ||||
| 922 | + background: var(--card); border: 1.5px solid var(--edge); color: var(--ink); | ||||
| 923 | + border-radius: 999px; padding: 8px 14px; font-size: 13px; font-weight: 600; font-family: inherit; outline: none; | ||||
| 924 | +} | ||||
| 925 | +.f-input { flex: 1; min-width: 140px; } | ||||
| 926 | +.f-input:focus { border-color: var(--orange); } | ||||
| 927 | + | ||||
| 928 | +/* sessions denses */ | ||||
| 929 | +.sess-card.row { | ||||
| 930 | + flex-direction: row; align-items: center; gap: 10px; padding: 10px 14px; | ||||
| 931 | + border-width: 1.5px; box-shadow: none; | ||||
| 932 | +} | ||||
| 933 | +.sess-card.row:hover { border-color: var(--orange-deep); } | ||||
| 934 | +.sess-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; } | ||||
| 935 | +.sess-cost, .sess-when { flex-shrink: 0; font-size: 11.5px; color: var(--muted); } | ||||
| 936 | +.sess-btns { display: flex; gap: 6px; flex-shrink: 0; } | ||||
| 937 | +.sess-btns button { border: 1.5px solid var(--edge); border-radius: 999px; background: var(--card2); font-size: 11.5px; font-weight: 800; padding: 5px 12px; cursor: pointer; } | ||||
| 938 | +.sess-btns .del { color: var(--bad); padding: 5px 9px; } | ||||
| 939 | +@media (max-width: 640px) { .sess-when { display: none; } } | ||||
| 940 | + | ||||
| 941 | +/* prompts */ | ||||
| 942 | +.sess-card.prow { box-shadow: none; border-width: 1.5px; } | ||||
| 943 | +.sess-card.prow:hover { border-color: var(--orange-deep); } | ||||
| 944 | +.fav-btn { background: none; border: none; font-size: 17px; color: var(--muted); cursor: pointer; padding: 0 2px; flex-shrink: 0; } | ||||
| 945 | +.fav-btn.on { color: var(--orange-deep); } | ||||
| 946 | +.sess-top .sess-info { flex-shrink: 0; } | ||||
| 947 | + | ||||
| 948 | +/* nav mobile « Plus » */ | ||||
| 949 | +.more-nav { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding-bottom: 8px; } | ||||
| 950 | +.more-btn { | ||||
| 951 | + display: flex; align-items: center; gap: 10px; | ||||
| 952 | + background: var(--card); border: 2px solid var(--edge); border-radius: var(--r-md); | ||||
| 953 | + box-shadow: var(--bshadow-sm); padding: 13px 14px; font-weight: 800; font-size: 14px; cursor: pointer; | ||||
| 954 | +} | ||||
| 955 | +.more-btn svg { width: 20px; height: 20px; color: var(--orange-deep); } | ||||
| 956 | + | ||||
| 957 | +/* accueil chat */ | ||||
| 958 | +.welcome { position: absolute; inset: 110px 0 150px; overflow-y: auto; } | ||||
| 959 | +.welcome-inner { display: flex; flex-direction: column; align-items: center; gap: 9px; width: 100%; max-width: 560px; margin: auto; } | ||||
| 960 | +.welcome-recent { width: 100%; margin-top: 18px; display: flex; flex-direction: column; gap: 6px; } | ||||
| 961 | +.wr-title { font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.6px; color: var(--muted); text-align: left; } | ||||
| 962 | +.wr-item { | ||||
| 963 | + display: flex; align-items: center; gap: 9px; text-align: left; | ||||
| 964 | + background: var(--card); border: 1.5px solid var(--edge); border-radius: var(--r-md); | ||||
| 965 | + padding: 9px 12px; cursor: pointer; font-family: inherit; | ||||
| 966 | +} | ||||
| 967 | +.wr-t { flex: 1; min-width: 0; font-weight: 700; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); } | ||||
| 968 | +.wr-m { font-size: 11px; color: var(--muted); flex-shrink: 0; } | ||||
| 969 | + | ||||
| 970 | +/* réglages analytics */ | ||||
| 971 | +.an-cfg .ac-num { width: 74px; background: var(--bg); border: 1.5px solid var(--edge); color: var(--ink); border-radius: 8px; padding: 5px 8px; font-family: var(--mono); font-size: 13px; } | ||||
| 972 | +.an-cfg .pe-textarea { font-size: 12px; } | ||||
| 973 | + | ||||
| 974 | +/* studio / vieux composants : bascule douce vers les variables */ | ||||
| 975 | +.soc-select, #view-social textarea { background: var(--card); color: var(--ink); } | ||||
| 976 | +.vd-box, .stload-box { background: var(--card); } | ||||
| 977 | +.vd-head h3 { color: var(--ink); } | ||||
| 978 | +.st-cap { color: var(--muted); } | ||||
| 979 | +.vis-chart { background: var(--card2); border-radius: 12px; } | ||||
modified
server/analytics.js
+466 −90
Large diff (629 lines) is not loaded by default to keep the page fast.Load diff
modified
server/server.js
+19 −6
Large diff (62 lines) is not loaded by default to keep the page fast.Load diff
added
server/test-analytics.mjs
+128 −0
Large diff (129 lines) is not loaded by default to keep the page fast.Load diff