@yonathan124/zentry 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,908 @@
1
+ "use client";
2
+ import { useState, useEffect, useCallback, useMemo, useRef } from "react";
3
+ import { PHASES, STACKS, GROUPS } from "../config/constants";
4
+
5
+ // ─── Constantes & helpers ──────────────────────────────────────────────────
6
+ const LS = {
7
+ get: (k, d) => { try { const v = localStorage.getItem(k); return v ? JSON.parse(v) : d; } catch { return d; } },
8
+ set: (k, v) => { try { localStorage.setItem(k, JSON.stringify(v)); } catch {} }
9
+ };
10
+ const SK = "vba-v5", SK_H = "vba-hist";
11
+ const SEVS = ["CRITIQUE","HAUTE","MOYENNE","BASSE","INFO"];
12
+ const SEV = {
13
+ CRITIQUE: { bg:"#3B0000", border:"#7F1D1D", text:"#FCA5A5", badge:"#EF4444" },
14
+ HAUTE: { bg:"#2C1500", border:"#7C2D12", text:"#FDBA74", badge:"#F97316" },
15
+ MOYENNE: { bg:"#1C1A00", border:"#713F12", text:"#FDE68A", badge:"#EAB308" },
16
+ BASSE: { bg:"#001F10", border:"#14532D", text:"#86EFAC", badge:"#22C55E" },
17
+ INFO: { bg:"#001525", border:"#1E3A5F", text:"#93C5FD", badge:"#3B82F6" },
18
+ };
19
+ const GRP_ICONS = { discovery:"🗺", security:"🔐", performance:"⚡", quality:"✅", infra:"🏗", ops:"🔄", closure:"🏁" };
20
+
21
+ const PROVIDERS = {
22
+ anthropic: { label:"Claude", icon:"🟤", ph:"sk-ant-…", models:["claude-sonnet-4-20250514","claude-opus-4-20250514","claude-3-5-sonnet-20241022"] },
23
+ openai: { label:"GPT-4o", icon:"🟢", ph:"sk-…", models:["gpt-4o","gpt-4o-mini","o3-mini"] },
24
+ groq: { label:"Groq", icon:"⚡", ph:"gsk_…", models:["llama-3.3-70b-versatile","mixtral-8x7b-32768","llama-3.1-70b-versatile"] },
25
+ gemini: { label:"Gemini", icon:"🔵", ph:"AIza…", models:["gemini-2.0-flash","gemini-1.5-pro","gemini-1.5-flash"] },
26
+ };
27
+
28
+ async function callAI(provider, key, model, systemPrompt, userPrompt) {
29
+ const sys = systemPrompt || "";
30
+ if (provider === "anthropic") {
31
+ const r = await fetch("https://api.anthropic.com/v1/messages", {
32
+ method:"POST", headers:{"Content-Type":"application/json","x-api-key":key,"anthropic-version":"2023-06-01"},
33
+ body: JSON.stringify({ model, max_tokens:3000, system: sys, messages:[{role:"user",content:userPrompt}] })
34
+ });
35
+ const d = await r.json(); return d.content?.[0]?.text || "";
36
+ }
37
+ if (provider === "gemini") {
38
+ const r = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`, {
39
+ method:"POST", headers:{"Content-Type":"application/json"},
40
+ body: JSON.stringify({ systemInstruction:{parts:[{text:sys}]}, contents:[{parts:[{text:userPrompt}]}], generationConfig:{maxOutputTokens:3000} })
41
+ });
42
+ const d = await r.json(); return d.candidates?.[0]?.content?.parts?.[0]?.text || "";
43
+ }
44
+ // OpenAI / Groq (format compatible)
45
+ const endpoints = { openai:"https://api.openai.com/v1/chat/completions", groq:"https://api.groq.com/openai/v1/chat/completions" };
46
+ const r = await fetch(endpoints[provider], {
47
+ method:"POST", headers:{"Content-Type":"application/json","Authorization":`Bearer ${key}`},
48
+ body: JSON.stringify({ model, max_tokens:3000, messages:[{role:"system",content:sys},{role:"user",content:userPrompt}] })
49
+ });
50
+ const d = await r.json(); return d.choices?.[0]?.message?.content || "";
51
+ }
52
+
53
+ const STRICT_SYSTEM = `Tu es un auditeur de sécurité senior expert. Règles ABSOLUES :
54
+ 1. Ne rapporte QUE des vulnérabilités réelles et avérées dans le code fourni.
55
+ 2. Si le code est sécurisé, réponds UNIQUEMENT : [SAFE] - Aucune vulnérabilité détectée.
56
+ 3. Zéro faux positif. Zéro hallucination. Zéro suggestion générique sans preuve dans le code.
57
+ 4. Pour chaque faille : indique la ligne exacte, le risque concret, le correctif précis.
58
+ 5. Format de sortie STRICT : [CRITIQUE/HAUTE/MOYENNE/BASSE] Fichier:ligne — Description — Correctif`;
59
+
60
+ function detectStack(txt) {
61
+ if (!txt) return null;
62
+ if (txt.includes("@supabase") && txt.includes("next")) return "njs-supa";
63
+ if (txt.includes("prisma") && txt.includes("next")) return "njs-prisma";
64
+ if (txt.includes("express")) return "node-express";
65
+ if (txt.includes("django")) return "django";
66
+ if (txt.includes("fastapi")) return "fastapi";
67
+ if (txt.includes("expo") || txt.includes("react-native")) return "react-native";
68
+ if (txt.includes("laravel")) return "laravel";
69
+ if (txt.includes("nuxt")) return "nuxt";
70
+ return null;
71
+ }
72
+
73
+ function buildPrompt(phase, project, fileCode, blocNum, patterns, bizCtx, findings) {
74
+ const sp = STACKS[project.stackProfile] || STACKS["njs-supa"];
75
+ const crossF = (phase.deps||[]).flatMap(dep => (findings[dep]||[]).filter(f=>!f.resolved && (f.severity==="CRITIQUE"||f.severity==="HAUTE")).map(f=>`[${f.severity}] ${f.text}`));
76
+ const customP = patterns.length ? patterns.map(p=>`- [${p.category}] ${p.rule}`).join("\n") : "";
77
+ return (phase.prompt || "")
78
+ .replace(/\{\{PROJECT\}\}/g, project.name || "Projet")
79
+ .replace(/\{\{STACK\}\}/g, sp.label)
80
+ .replace(/\{\{STACK_SEC\}\}/g, (sp.sec||[]).join("\n"))
81
+ .replace(/\{\{BLOC_NUM\}\}/g, blocNum || "")
82
+ .replace(/\{\{FILES\}\}/g, fileCode || "(aucun fichier fourni)")
83
+ .replace(/\{\{COMPLIANCE\}\}/g, (project.compliance||[]).join(", ") || "N/A")
84
+ .replace(/\{\{SENSITIVE_DATA\}\}/g, project.sensitiveData || "N/A")
85
+ .replace(/\{\{USERS\}\}/g, bizCtx?.users || "N/A")
86
+ .replace(/\{\{PEAK_RPS\}\}/g, bizCtx?.peakRps || "N/A")
87
+ .replace(/\{\{SLA\}\}/g, bizCtx?.sla || "N/A")
88
+ .replace(/\{\{GEO\}\}/g, bizCtx?.geo || "N/A")
89
+ .replace(/\{\{CROSS_FINDINGS\}\}/g, crossF.length ? crossF.join("\n") : "Aucun finding critique dans les phases précédentes.")
90
+ .replace(/\{\{CUSTOM_PATTERNS\}\}/g, customP || "Aucune règle personnelle.");
91
+ }
92
+
93
+ async function readLocalFile(dirFiles, path) {
94
+ try {
95
+ if (!dirFiles) return null;
96
+ const file = dirFiles.find(f => f.webkitRelativePath.endsWith(path) || f.webkitRelativePath.includes(path));
97
+ if (file) return await file.text();
98
+ return null;
99
+ } catch { return null; }
100
+ }
101
+
102
+ function Sparkline({ data, color = "#8B5CF6" }) {
103
+ const mx = Math.max(...data, 1);
104
+ const pts = data.map((v, i) => `${i * (48/(data.length-1))},${18 - (v/mx)*16}`).join(" ");
105
+ return (
106
+ <svg width="48" height="20" style={{ flexShrink:0 }}>
107
+ <polyline points={pts} fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
108
+ </svg>
109
+ );
110
+ }
111
+
112
+ function RadialProgress({ pct, size = 48, stroke = 4, color = "#8B5CF6" }) {
113
+ const r = (size - stroke) / 2;
114
+ const circ = 2 * Math.PI * r;
115
+ return (
116
+ <svg width={size} height={size} style={{ transform:"rotate(-90deg)" }}>
117
+ <circle cx={size/2} cy={size/2} r={r} fill="none" stroke="#2D2D3A" strokeWidth={stroke}/>
118
+ <circle cx={size/2} cy={size/2} r={r} fill="none" stroke={color} strokeWidth={stroke}
119
+ strokeDasharray={`${(pct/100)*circ} ${circ}`} strokeLinecap="round"
120
+ style={{ transition:"stroke-dasharray .6s ease" }}/>
121
+ </svg>
122
+ );
123
+ }
124
+
125
+ // ─── CSS global injecté une seule fois ────────────────────────────────────
126
+ const GLOBAL_CSS = `
127
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
128
+ .vba-root * { box-sizing:border-box; font-family:'Inter',system-ui,sans-serif; }
129
+ .vba-root { --bg:#0A0A12; --bg2:#111120; --bg3:#1A1A2E; --card:#16162A;
130
+ --border:#2A2A42; --accent:#8B5CF6; --accent2:#7C3AED; --accentLight:#EDE9FE;
131
+ --text:#F1F0FF; --muted:#6B6B8A; --muted2:#4A4A6A; --green:#22C55E; --red:#EF4444; --yellow:#EAB308; }
132
+ .vba-btn { display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border-radius:12px;
133
+ font-size:13px;font-weight:500;cursor:pointer;border:none;transition:all .2s;outline:none; }
134
+ .vba-btn-primary { background:var(--accent);color:#fff; }
135
+ .vba-btn-primary:hover { background:var(--accent2);transform:translateY(-1px);box-shadow:0 4px 16px #8B5CF640; }
136
+ .vba-btn-ghost { background:transparent;color:var(--muted);border:1px solid var(--border); }
137
+ .vba-btn-ghost:hover { background:var(--bg3);color:var(--text);border-color:var(--accent); }
138
+ .vba-btn-danger { background:#3B0000;color:#FCA5A5;border:1px solid #7F1D1D; }
139
+ .vba-btn-danger:hover { background:#7F1D1D; }
140
+ .vba-input { width:100%;padding:10px 14px;background:var(--bg3);border:1px solid var(--border);
141
+ border-radius:10px;color:var(--text);font-size:13px;outline:none;transition:border .2s; }
142
+ .vba-input:focus { border-color:var(--accent);box-shadow:0 0 0 3px #8B5CF620; }
143
+ .vba-input::placeholder { color:var(--muted2); }
144
+ .vba-card { background:var(--card);border:1px solid var(--border);border-radius:16px;padding:16px; }
145
+ .vba-tab-btn { display:flex;flex-direction:column;align-items:center;gap:3px;padding:8px 12px;
146
+ background:none;border:none;cursor:pointer;color:var(--muted);transition:color .2s;flex:1; }
147
+ .vba-tab-btn.active { color:var(--accent); }
148
+ .vba-tab-btn span { font-size:18px; }
149
+ .vba-tab-btn small { font-size:10px;font-weight:500; }
150
+ .vba-phase-btn { width:100%;padding:10px 12px;background:none;border:none;cursor:pointer;
151
+ display:flex;align-items:center;gap:10px;border-radius:10px;text-align:left;transition:background .15s; }
152
+ .vba-phase-btn:hover { background:var(--bg3); }
153
+ .vba-phase-btn.active { background:#1E1040;border:1px solid #4C1D95; }
154
+ .vba-check-item { display:flex;align-items:flex-start;gap:10px;padding:10px 0;border-top:1px solid var(--border);cursor:pointer; }
155
+ .vba-check-item:first-child { border-top:none; }
156
+ .vba-badge { display:inline-flex;align-items:center;padding:2px 8px;border-radius:99px;font-size:10px;font-weight:700; }
157
+ .vba-anim-in { animation:vba-in .3s ease; }
158
+ @keyframes vba-in { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} }
159
+ .vba-scroll::-webkit-scrollbar { width:4px; }
160
+ .vba-scroll::-webkit-scrollbar-track { background:transparent; }
161
+ .vba-scroll::-webkit-scrollbar-thumb { background:var(--border);border-radius:4px; }
162
+ .vba-pulse { animation:vba-pulse 2s infinite; }
163
+ @keyframes vba-pulse { 0%,100%{opacity:1}50%{opacity:.5} }
164
+ .vba-tag { padding:3px 8px;border-radius:6px;font-size:10px;font-weight:600;letter-spacing:.04em; }
165
+ select.vba-input option { background:#1A1A2E; }
166
+ .vba-matrix-anim { font-family:monospace; color:#22C55E; animation: matrix-fade 1.2s infinite alternate; }
167
+ @keyframes matrix-fade { from {opacity:0.6; text-shadow:0 0 4px #22C55E;} to {opacity:1; text-shadow:0 0 10px #22C55E;} }
168
+ `;
169
+
170
+
171
+ // ─── COMPOSANT PRINCIPAL ──────────────────────────────────────────────────
172
+ export default function Zentry() {
173
+ const sv = LS.get(SK, {});
174
+
175
+ // ── État global ──
176
+ const [tab, setTab] = useState("audit");
177
+ const [project, setProject] = useState(() => sv.project ?? { name:"", stackProfile:"njs-supa", ide:"cursor", compliance:[], sensitiveData:"" });
178
+ const [aiCfg, setAiCfg] = useState(() => sv.aiCfg ?? { provider:"groq", key:"", model:"llama-3.3-70b-versatile", godMode: false });
179
+ const [bizCtx, setBizCtx] = useState(() => sv.bizCtx ?? { users:"", peakRps:"", sla:"", geo:"" });
180
+
181
+ // ── État audit ──
182
+ const initCh = () => { const o={}; PHASES.forEach(p=>{o[p.id]=p.checks.map(()=>false);}); return o; };
183
+ const initArr = () => { const o={}; PHASES.forEach(p=>{o[p.id]=[];}); return o; };
184
+ const initStr = () => { const o={}; PHASES.forEach(p=>{o[p.id]="";}); return o; };
185
+
186
+ const [checks, setChecks] = useState(() => sv.checks ?? initCh());
187
+ const [findings, setFindings] = useState(() => sv.findings ?? initArr());
188
+ const [fileBlks, setFileBlks] = useState(() => sv.fileBlks ?? initStr());
189
+ const [blocNums, setBlocNums] = useState(() => sv.blocNums ?? initStr());
190
+ const [notes, setNotes] = useState(() => sv.notes ?? initStr());
191
+ const [dates, setDates] = useState(() => sv.dates ?? initStr());
192
+ const [verdict, setVerdict] = useState(() => sv.verdict ?? null);
193
+ const [patterns, setPatterns] = useState(() => sv.patterns ?? []);
194
+ const [history, setHistory] = useState(() => LS.get(SK_H, []));
195
+
196
+ // ── UI state ──
197
+ const [activeId, setActiveId] = useState(() => sv.activeId ?? "p0");
198
+ const [dirFiles, setDirFiles] = useState(null);
199
+ const [autoRunning, setAutoRunning] = useState(false);
200
+ const [loadedCode, setLoadedCode] = useState({});
201
+ const [aiLoading, setAiLoading] = useState({});
202
+ const [fixState, setFixState] = useState({});
203
+ const [newF, setNewF] = useState({ sev:"HAUTE", txt:"" });
204
+ const [newPat, setNewPat] = useState({ category:"SÉCURITÉ", rule:"" });
205
+ const [analyzeIn, setAnalyzeIn] = useState("");
206
+ const [sidebarOpen, setSidebarOpen] = useState(true);
207
+ const [scanning, setScanning] = useState(false);
208
+ const [scanFiles, setScanFiles] = useState([]);
209
+ const [scanProgress, setScanProgress] = useState({ done:0, total:0, current:"" });
210
+ const [scanResults, setScanResults] = useState([]);
211
+ const [showKey, setShowKey] = useState(false);
212
+ const [copied, setCopied] = useState(null);
213
+ const [toast, setToast] = useState(null);
214
+
215
+ // ── Persistance ──
216
+ useEffect(() => { LS.set(SK, {project,aiCfg,bizCtx,checks,findings,fileBlks,blocNums,notes,dates,verdict,patterns,activeId}); },
217
+ [project,aiCfg,bizCtx,checks,findings,fileBlks,blocNums,notes,dates,verdict,patterns,activeId]);
218
+
219
+ // ── Toast helper ──
220
+ const showToast = (msg, type="info") => { setToast({msg,type}); setTimeout(()=>setToast(null),3000); };
221
+
222
+ // ── Calculs ──
223
+ const pPct = useCallback((id) => {
224
+ const c = checks[id] || []; const d = c.filter(Boolean).length;
225
+ return { done:d, total:c.length, pct: c.length ? Math.round(d/c.length*100) : 0 };
226
+ }, [checks]);
227
+
228
+ const pSt = useCallback((id) => {
229
+ const {done,total} = pPct(id);
230
+ return done===0?"todo":done===total?"done":"partial";
231
+ }, [pPct]);
232
+
233
+ const totPct = useMemo(() => {
234
+ let d=0,t=0; PHASES.forEach(p=>{const c=checks[p.id]||[];d+=c.filter(Boolean).length;t+=c.length;});
235
+ return {done:d,total:t,pct:t?Math.round(d/t*100):0};
236
+ }, [checks]);
237
+
238
+ const allF = useMemo(() => {
239
+ const a=[]; PHASES.forEach(p=>(findings[p.id]||[]).forEach(f=>a.push({...f,phaseId:p.id,phaseLabel:p.label}))); return a;
240
+ }, [findings]);
241
+
242
+ const sevC = useMemo(() => {
243
+ const c={CRITIQUE:0,HAUTE:0,MOYENNE:0,BASSE:0,INFO:0};
244
+ allF.forEach(f=>{ if(c[f.severity]!==undefined) c[f.severity]++; }); return c;
245
+ }, [allF]);
246
+
247
+ const groupPct = useCallback((gid) => {
248
+ const gp = PHASES.filter(p=>p.group===gid);
249
+ let d=0,t=0; gp.forEach(p=>{const c=checks[p.id]||[];d+=c.filter(Boolean).length;t+=c.length;});
250
+ return t?Math.round(d/t*100):0;
251
+ }, [checks]);
252
+
253
+ const phase = PHASES.find(p=>p.id===activeId) || PHASES[0];
254
+ const phIdx = PHASES.findIndex(p=>p.id===activeId);
255
+ const prevP = phIdx>0?PHASES[phIdx-1]:null;
256
+ const nextP = phIdx<PHASES.length-1?PHASES[phIdx+1]:null;
257
+ const sp = STACKS[project.stackProfile] || STACKS["njs-supa"];
258
+ const prov = PROVIDERS[aiCfg.provider] || PROVIDERS.groq;
259
+ const isConfigured = !!project.name && !!aiCfg.key;
260
+
261
+ // ── Actions ──
262
+ const clip = (text, key) => {
263
+ navigator.clipboard.writeText(text).then(()=>{setCopied(key);setTimeout(()=>setCopied(null),2000);});
264
+ };
265
+
266
+ const toggleCheck = (pid, i) => {
267
+ setChecks(prev=>{const u=[...(prev[pid]||[])];u[i]=!u[i];return{...prev,[pid]:u};});
268
+ if(!dates[pid]) setDates(d=>({...d,[pid]:new Date().toISOString()}));
269
+ };
270
+
271
+ const addFinding = (pid) => {
272
+ if(!newF.txt.trim()) return;
273
+ setFindings(prev=>({...prev,[pid]:[...(prev[pid]||[]),{id:Date.now(),severity:newF.sev,text:newF.txt.trim(),resolved:false}]}));
274
+ setNewF(f=>({...f,txt:""}));
275
+ };
276
+
277
+ const saveSnap = () => {
278
+ const snap = {date:new Date().toISOString(),project:project.name,stack:sp.label,pct:totPct.pct,sev:{...sevC}};
279
+ setHistory(h=>{const u=[...h,snap].slice(-50);LS.set(SK_H,u);return u;});
280
+ showToast("📸 Snapshot sauvegardé !", "success");
281
+ };
282
+
283
+ // ── Auto-Audit ──
284
+ const runAutoAudit = async () => {
285
+ if (!aiCfg.key) { showToast("⚠️ Configure ta clé API dans Paramètres", "error"); setTab("settings"); return; }
286
+ if (!dirFiles && !fileBlks[phase.id]) { showToast("⚠️ Fournis des fichiers ou connecte un dossier", "error"); return; }
287
+ setAutoRunning(true);
288
+ try {
289
+ let code = loadedCode[phase.id] || "";
290
+ if (!code && dirFiles && fileBlks[phase.id]) {
291
+ const paths = fileBlks[phase.id].split("\n").map(p=>p.trim()).filter(Boolean);
292
+ const parts = await Promise.all(paths.map(p => readLocalFile(dirFiles, p)));
293
+ code = paths.map((p,i)=>`// ─── ${p} ───\n${parts[i]||"⚠️ Introuvable"}`).join("\n\n");
294
+ setLoadedCode(prev=>({...prev,[phase.id]:code}));
295
+ }
296
+ const prompt = buildPrompt(phase, project, code, blocNums[phase.id], patterns, bizCtx, findings);
297
+ const raw = await callAI(aiCfg.provider, aiCfg.key, aiCfg.model, STRICT_SYSTEM, prompt);
298
+
299
+ if (raw.trim().startsWith("[SAFE]")) {
300
+ showToast("✅ " + raw.trim(), "success");
301
+ setNotes(n=>({...n,[phase.id]:(n[phase.id]?n[phase.id]+"\n\n":"")+`🛡 Auto-Audit ${new Date().toLocaleTimeString()}:\n${raw.trim()}`}));
302
+ } else {
303
+ const lines = raw.split("\n"); const newFindings=[]; let id=Date.now();
304
+ for (const l of lines) {
305
+ if (l.match(/\[CRITIQUE\]/i)) newFindings.push({id:id++,severity:"CRITIQUE",text:l.replace(/\[CRITIQUE\]\s*/i,"").trim(),resolved:false});
306
+ else if (l.match(/\[HAUTE\]/i)) newFindings.push({id:id++,severity:"HAUTE",text:l.replace(/\[HAUTE\]\s*/i,"").trim(),resolved:false});
307
+ else if (l.match(/\[MOYENNE\]/i)) newFindings.push({id:id++,severity:"MOYENNE",text:l.replace(/\[MOYENNE\]\s*/i,"").trim(),resolved:false});
308
+ else if (l.match(/\[BASSE\]/i)) newFindings.push({id:id++,severity:"BASSE",text:l.replace(/\[BASSE\]\s*/i,"").trim(),resolved:false});
309
+ }
310
+ if (newFindings.length) setFindings(prev=>({...prev,[phase.id]:[...(prev[phase.id]||[]),...newFindings]}));
311
+ setNotes(n=>({...n,[phase.id]:(n[phase.id]?n[phase.id]+"\n\n":"")+`🔍 Auto-Audit ${new Date().toLocaleTimeString()}:\n${raw.trim()}`}));
312
+ showToast(`🔴 ${newFindings.length} finding(s) détecté(s)`, "warning");
313
+ }
314
+ } catch(e) { showToast("❌ Erreur API: " + e.message, "error"); }
315
+ setAutoRunning(false);
316
+ };
317
+
318
+ // ── Scanner automatique ──
319
+ const scanDirectory = async () => {
320
+ if (!dirFiles || dirFiles.length === 0) { showToast("📁 Connecte d'abord un dossier", "error"); return; }
321
+ if (!aiCfg.key) { showToast("⚠️ Configure ta clé API dans Paramètres", "error"); setTab("settings"); return; }
322
+ const IGNORE = /node_modules|\.next|dist|\.git|\.lock$|\.json$|\.png$|\.jpg$|\.svg$|\.css$/;
323
+ const EXTS = /\.(ts|tsx|js|jsx|py|php|rs|go)$/;
324
+
325
+ setScanning(true); setScanResults([]); setScanFiles([]);
326
+ try {
327
+ const files = dirFiles.filter(f => EXTS.test(f.name) && !IGNORE.test(f.webkitRelativePath)).map(f => f.webkitRelativePath);
328
+ setScanFiles(files);
329
+ const BLOCK_SIZE = 4;
330
+ const blocks = [];
331
+ for (let i=0; i<files.length; i+=BLOCK_SIZE) blocks.push(files.slice(i,i+BLOCK_SIZE));
332
+ const results = [];
333
+ for (let bi=0; bi<blocks.length; bi++) {
334
+ const block = blocks[bi];
335
+ setScanProgress({done:bi,total:blocks.length,current:block.join(", ")});
336
+ const parts = await Promise.all(block.map(f => readLocalFile(dirFiles, f)));
337
+ const code = block.map((f,i)=>`// ─── ${f} ───\n${parts[i]||"⚠️ Vide"}`).join("\n\n");
338
+ const prompt = `Audite ces ${block.length} fichier(s) de sécurité.\nProjet: ${project.name||"ORVIA"} | Stack: ${sp.label}\n\n${code}`;
339
+ try {
340
+ const raw = await callAI(aiCfg.provider, aiCfg.key, aiCfg.model, STRICT_SYSTEM, prompt);
341
+ results.push({ block, raw, safe: raw.trim().startsWith("[SAFE]") });
342
+ } catch(e) { results.push({ block, raw: `Erreur: ${e.message}`, safe:false, error:true }); }
343
+ setScanResults([...results]);
344
+ }
345
+ setScanProgress({done:blocks.length,total:blocks.length,current:""});
346
+ showToast(`✅ Scan terminé — ${files.length} fichiers analysés`, "success");
347
+ } catch(e) { showToast("❌ Erreur scan: "+e.message, "error"); }
348
+ setScanning(false);
349
+ };
350
+
351
+ // ── Fix IA & Self-Healing ──
352
+ const applyFix = async (fId, code, filePath) => {
353
+ setFixState(prev=>({...prev,[fId]:{...prev[fId], applying:true}}));
354
+ try {
355
+ const res = await fetch("/api/zentry/fix", {
356
+ method: "POST", headers: {"Content-Type":"application/json"},
357
+ body: JSON.stringify({ filePath: filePath || "apps/web/src/app/page.tsx", content: code })
358
+ });
359
+ const data = await res.json();
360
+ if(data.success) {
361
+ showToast("✅ Code corrigé sur le disque dur !", "success");
362
+ setFindings(prev=>{
363
+ const nf = {...prev};
364
+ Object.keys(nf).forEach(k=>{ nf[k] = nf[k].map(x=>x.id===fId?{...x,resolved:true}:x); });
365
+ return nf;
366
+ });
367
+ } else {
368
+ showToast("⚠️ Mode Cloud / Échec: " + (data.message||data.error), "error");
369
+ }
370
+ } catch(e) { showToast("❌ Erreur d'application", "error"); }
371
+ setFixState(prev=>({...prev,[fId]:{...prev[fId], applying:false}}));
372
+ };
373
+
374
+ const generateFix = async (pid, f) => {
375
+ if(!aiCfg.key) { showToast("Configure ta clé API", "error"); return; }
376
+ setFixState(prev=>({...prev,[f.id]:{loading:true}}));
377
+ try {
378
+ const raw = await callAI(aiCfg.provider, aiCfg.key, aiCfg.model, "",
379
+ `Expert sécurité ${sp.label}. Génère le fichier complet corrigé (JSON sans backticks).\nFinding [${f.severity}]: ${f.text}\nJSON: {"filePath":"apps/web/src/...","code":"// fichier complet corrigé","explanation":"2 phrases","confidence":0.9}`);
380
+ const parsed = JSON.parse(raw.replace(/```json|```/g,"").trim());
381
+ setFixState(prev=>({...prev,[f.id]:{loading:false,...parsed}}));
382
+
383
+ if(aiCfg.godMode && parsed.code && parsed.filePath) {
384
+ await applyFix(f.id, parsed.code, parsed.filePath);
385
+ }
386
+ } catch { setFixState(prev=>({...prev,[f.id]:{loading:false,error:true}})); }
387
+ };
388
+
389
+ const buildReport = () => {
390
+ const lines = [`# RAPPORT D'AUDIT — ${(project.name||"PROJET").toUpperCase()}`,
391
+ `> ${new Date().toLocaleDateString("fr-FR",{day:"2-digit",month:"long",year:"numeric"})} · Zentry Pro v5.0 · ${sp.label}`,
392
+ verdict?`> **Verdict : ${verdict==="pass"?"🟢 PRÊT":verdict==="warn"?"🟡 AVEC RÉSERVES":"🔴 NON PRÊT"}**`:"",
393
+ `> Score global : ${totPct.done}/${totPct.total} (${totPct.pct}%)`, ""];
394
+ PHASES.forEach(p=>{
395
+ const {done,total:t}=pPct(p.id); const st=pSt(p.id);
396
+ lines.push(`## ${st==="done"?"✅":st==="partial"?"🟡":"⬜"} Phase ${p.n} — ${p.label} (${done}/${t})`);
397
+ p.checks.forEach((c,i)=>lines.push(`${checks[p.id][i]?"✅":"⬜"} ${c}`));
398
+ const pf=findings[p.id]||[];
399
+ if(pf.length){lines.push("");pf.forEach(f=>lines.push(`- **[${f.severity}]** ${f.resolved?"~~"+f.text+"~~":f.text}${f.resolved?" ✓":""}`))}
400
+ lines.push("");
401
+ });
402
+ return lines.filter(Boolean).join("\n");
403
+ };
404
+
405
+
406
+ // ─── RENDER ───────────────────────────────────────────────────────────────
407
+ return (
408
+ <div className="vba-root" style={{background:"var(--bg)",minHeight:"100vh",color:"var(--text)",display:"flex",flexDirection:"column",maxWidth:"420px",margin:"0 auto",position:"relative"}}>
409
+ <style>{GLOBAL_CSS}</style>
410
+
411
+ {/* Toast */}
412
+ {toast && (
413
+ <div style={{position:"fixed",top:16,left:"50%",transform:"translateX(-50%)",zIndex:9999,padding:"10px 20px",borderRadius:12,background:toast.type==="success"?"#052e16":toast.type==="error"?"#3B0000":"#1e1b4b",border:`1px solid ${toast.type==="success"?"#22C55E":toast.type==="error"?"#EF4444":"#8B5CF6"}`,color:"#fff",fontSize:13,fontWeight:500,whiteSpace:"nowrap",boxShadow:"0 8px 32px #0008"}}>
414
+ {toast.msg}
415
+ </div>
416
+ )}
417
+
418
+ {/* Input File Invisible (Fallback File System Access API) */}
419
+ <input type="file" id="vba-dir-input" webkitdirectory="" directory="" multiple style={{display:"none"}} onChange={e => {
420
+ const files = Array.from(e.target.files);
421
+ if(files.length){ setDirFiles(files); showToast(`📁 Dossier connecté (${files.length} fichiers)`, "success"); }
422
+ }} />
423
+
424
+ {/* Header */}
425
+ <div style={{padding:"16px 16px 0",flexShrink:0}}>
426
+ <div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:12}}>
427
+ <div>
428
+ <div style={{fontSize:20,fontWeight:800,letterSpacing:"-0.04em"}}>Vibe<span style={{color:"var(--accent)"}}>Audit</span> <span style={{fontSize:11,background:"#1E1040",color:"var(--accent)",padding:"2px 8px",borderRadius:99,verticalAlign:"middle",fontWeight:600}}>Pro v5</span></div>
429
+ <div style={{fontSize:11,color:"var(--muted)",marginTop:2}}>{project.name||"Aucun projet"} · {sp.tag}</div>
430
+ </div>
431
+ <div style={{display:"flex",alignItems:"center",gap:8}}>
432
+ <div style={{position:"relative"}}>
433
+ <RadialProgress pct={totPct.pct} size={44} stroke={4} color={totPct.pct===100?"var(--green)":"var(--accent)"}/>
434
+ <div style={{position:"absolute",inset:0,display:"flex",alignItems:"center",justifyContent:"center",fontSize:10,fontWeight:700,color:totPct.pct===100?"var(--green)":"var(--accent)"}}>{totPct.pct}%</div>
435
+ </div>
436
+ {!isConfigured && <div className="vba-pulse" style={{width:8,height:8,borderRadius:4,background:"var(--yellow)"}}/>}
437
+ </div>
438
+ </div>
439
+
440
+ {/* Status bar */}
441
+ {!isConfigured && (
442
+ <div onClick={()=>setTab("settings")} style={{padding:"10px 14px",background:"#1C1500",border:"1px solid #713F12",borderRadius:12,marginBottom:12,cursor:"pointer",display:"flex",alignItems:"center",gap:8}}>
443
+ <span style={{fontSize:16}}>⚙️</span>
444
+ <div>
445
+ <div style={{fontSize:12,fontWeight:600,color:"#FDE68A"}}>Configuration requise</div>
446
+ <div style={{fontSize:11,color:"#A16207"}}>Appuie ici pour configurer {!project.name?"le projet":"ta clé API"}</div>
447
+ </div>
448
+ <span style={{marginLeft:"auto",color:"#FDE68A",fontSize:16}}>›</span>
449
+ </div>
450
+ )}
451
+
452
+ {/* Tab bar */}
453
+ <div style={{display:"flex",background:"var(--bg3)",borderRadius:14,padding:4,gap:2}}>
454
+ {[{id:"audit",icon:"🛡",label:"Audit"},{id:"scan",icon:"🔍",label:"Scanner"},{id:"report",icon:"📊",label:"Rapport"},{id:"settings",icon:"⚙️",label:"Config"}].map(t=>(
455
+ <button key={t.id} className={`vba-tab-btn${tab===t.id?" active":""}`} onClick={()=>setTab(t.id)}
456
+ style={{borderRadius:10,background:tab===t.id?"var(--card)":"transparent",boxShadow:tab===t.id?"0 2px 8px #0004":"none"}}>
457
+ <span>{t.icon}</span><small>{t.label}</small>
458
+ </button>
459
+ ))}
460
+ </div>
461
+ </div>
462
+
463
+ {/* Content */}
464
+ <div className="vba-scroll" style={{flex:1,overflowY:"auto",padding:16,paddingBottom:24}}>
465
+
466
+ {/* ═══ SETTINGS TAB ══════════════════════════════════════════════ */}
467
+ {tab==="settings" && (
468
+ <div className="vba-anim-in">
469
+ <div style={{fontSize:13,fontWeight:700,color:"var(--muted)",textTransform:"uppercase",letterSpacing:".06em",marginBottom:12}}>Projet</div>
470
+ <div className="vba-card" style={{marginBottom:12}}>
471
+ <label style={{fontSize:11,color:"var(--muted)",display:"block",marginBottom:6}}>NOM DU PROJET</label>
472
+ <input className="vba-input" value={project.name} onChange={e=>setProject(p=>({...p,name:e.target.value}))} placeholder="ex: ORVIA, SIKIA…" style={{marginBottom:10}}/>
473
+ <label style={{fontSize:11,color:"var(--muted)",display:"block",marginBottom:6}}>STACK TECHNIQUE</label>
474
+ <select className="vba-input" value={project.stackProfile} onChange={e=>setProject(p=>({...p,stackProfile:e.target.value}))} style={{marginBottom:10}}>
475
+ {Object.entries(STACKS).map(([k,s])=><option key={k} value={k}>{s.label}</option>)}
476
+ </select>
477
+ <label style={{fontSize:11,color:"var(--muted)",display:"block",marginBottom:6}}>IDE</label>
478
+ <select className="vba-input" value={project.ide} onChange={e=>setProject(p=>({...p,ide:e.target.value}))} style={{marginBottom:10}}>
479
+ {[["cursor","Cursor"],["claude-code","Claude Code"],["windsurf","Windsurf"],["vscode","VS Code"],["generic","Universel"]].map(([k,l])=><option key={k} value={k}>{l}</option>)}
480
+ </select>
481
+ <label style={{fontSize:11,color:"var(--muted)",display:"block",marginBottom:6}}>DONNÉES SENSIBLES</label>
482
+ <input className="vba-input" value={project.sensitiveData} onChange={e=>setProject(p=>({...p,sensitiveData:e.target.value}))} placeholder="audio, PII, paiements…"/>
483
+ </div>
484
+
485
+ <div style={{fontSize:13,fontWeight:700,color:"var(--muted)",textTransform:"uppercase",letterSpacing:".06em",marginBottom:12,marginTop:20}}>Intelligence IA</div>
486
+ <div className="vba-card" style={{marginBottom:12}}>
487
+ <label style={{fontSize:11,color:"var(--muted)",display:"block",marginBottom:8}}>FOURNISSEUR</label>
488
+ <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:8,marginBottom:12}}>
489
+ {Object.entries(PROVIDERS).map(([k,p])=>{
490
+ const sel=aiCfg.provider===k;
491
+ return(
492
+ <button key={k} onClick={()=>setAiCfg(a=>({...a,provider:k,model:p.models[0]}))} style={{padding:"10px 12px",background:sel?"#1E1040":"var(--bg3)",border:`1px solid ${sel?"var(--accent)":"var(--border)"}`,borderRadius:12,cursor:"pointer",display:"flex",alignItems:"center",gap:8,color:"var(--text)"}}>
493
+ <span style={{fontSize:18}}>{p.icon}</span>
494
+ <div style={{textAlign:"left"}}>
495
+ <div style={{fontSize:12,fontWeight:sel?700:400,color:sel?"var(--accent)":"var(--text)"}}>{p.label}</div>
496
+ <div style={{fontSize:9,color:"var(--muted)"}}>Ultra-strict</div>
497
+ </div>
498
+ </button>
499
+ );
500
+ })}
501
+ </div>
502
+ <label style={{fontSize:11,color:"var(--muted)",display:"block",marginBottom:6}}>MODÈLE</label>
503
+ <select className="vba-input" value={aiCfg.model} onChange={e=>setAiCfg(a=>({...a,model:e.target.value}))} style={{marginBottom:10}}>
504
+ {prov.models.map(m=><option key={m} value={m}>{m}</option>)}
505
+ </select>
506
+ <label style={{fontSize:11,color:"var(--muted)",display:"block",marginBottom:6}}>CLÉ API {prov.label.toUpperCase()}</label>
507
+ <div style={{position:"relative"}}>
508
+ <input className="vba-input" type={showKey?"text":"password"} value={aiCfg.key} onChange={e=>setAiCfg(a=>({...a,key:e.target.value}))} placeholder={prov.ph} style={{paddingRight:40}}/>
509
+ <button onClick={()=>setShowKey(v=>!v)} style={{position:"absolute",right:12,top:"50%",transform:"translateY(-50%)",background:"none",border:"none",cursor:"pointer",color:"var(--muted)",fontSize:14}}>
510
+ {showKey?"🙈":"👁"}
511
+ </button>
512
+ </div>
513
+ <div style={{marginTop:8,fontSize:11,color:"var(--muted)",display:"flex",alignItems:"center",gap:4}}>
514
+ 🔒 Stockée uniquement dans ton navigateur. Jamais transmise à nos serveurs.
515
+ </div>
516
+ {aiCfg.key && <div style={{marginTop:8,padding:"6px 10px",background:"#052e16",border:"1px solid #22C55E",borderRadius:8,fontSize:11,color:"#86EFAC"}}>✅ Clé configurée — IA opérationnelle</div>}
517
+
518
+ {/* GAMIFIED GOD MODE TOGGLE */}
519
+ <label style={{fontSize:11,color:"var(--muted)",display:"block",marginBottom:6,marginTop:20}}>🔥 GOD MODE (AUTO-PILOTE & SELF-HEALING)</label>
520
+ <div style={{display:"flex",alignItems:"center",gap:12,padding:"14px",background:aiCfg.godMode?"linear-gradient(45deg, #2a0800, #190033)":"var(--bg3)",border:`1px solid ${aiCfg.godMode?"#EF4444":"var(--border)"}`,borderRadius:12,boxShadow:aiCfg.godMode?"0 0 20px #EF444440":"none",transition:"all 0.3s"}}>
521
+ <div style={{flex:1}}>
522
+ <div style={{fontSize:14,fontWeight:800,color:aiCfg.godMode?"#FCA5A5":"var(--text)",letterSpacing:"0.02em"}}>Remédiation Autonome</div>
523
+ <div style={{fontSize:11,color:aiCfg.godMode?"#FCA5A588":"var(--muted)",marginTop:4,lineHeight:1.4}}>L'outil modifie tes fichiers locaux et corrige les failles tout seul (requiert l'API locale).</div>
524
+ </div>
525
+ <button onClick={()=>setAiCfg(p=>({...p,godMode:!p.godMode}))} style={{padding:"8px 16px",borderRadius:8,background:aiCfg.godMode?"#EF4444":"var(--border)",color:"#fff",border:"none",cursor:"pointer",fontWeight:800,boxShadow:aiCfg.godMode?"0 4px 12px #EF444488":"none",transition:"all 0.2s"}}>{aiCfg.godMode?"ACTIVÉ":"DÉSACTIVÉ"}</button>
526
+ </div>
527
+ </div>
528
+
529
+ <div style={{fontSize:13,fontWeight:700,color:"var(--muted)",textTransform:"uppercase",letterSpacing:".06em",marginBottom:12,marginTop:20}}>Conformité</div>
530
+ <div className="vba-card" style={{marginBottom:12}}>
531
+ <div style={{display:"flex",flexWrap:"wrap",gap:8}}>
532
+ {["RGPD","PCI-DSS","HIPAA","SOC2","ISO27001"].map(c=>{const sel=project.compliance?.includes(c);return(
533
+ <button key={c} onClick={()=>setProject(p=>({...p,compliance:sel?(p.compliance||[]).filter(x=>x!==c):[...(p.compliance||[]),c]}))}
534
+ style={{padding:"6px 12px",borderRadius:99,border:`1px solid ${sel?"var(--accent)":"var(--border)"}`,background:sel?"#1E1040":"transparent",color:sel?"var(--accent)":"var(--muted)",fontSize:12,fontWeight:sel?700:400,cursor:"pointer"}}>
535
+ {c}
536
+ </button>
537
+ );})}
538
+ </div>
539
+ </div>
540
+
541
+ <div style={{fontSize:13,fontWeight:700,color:"var(--muted)",textTransform:"uppercase",letterSpacing:".06em",marginBottom:12,marginTop:20}}>Règles personnelles <span style={{fontSize:10,background:"#1E1040",color:"var(--accent)",padding:"2px 8px",borderRadius:99,fontWeight:600}}>{patterns.length}</span></div>
542
+ <div className="vba-card" style={{marginBottom:12}}>
543
+ <div style={{display:"flex",gap:8,marginBottom:8}}>
544
+ <select className="vba-input" value={newPat.category} onChange={e=>setNewPat(p=>({...p,category:e.target.value}))} style={{width:"auto",flexShrink:0}}>
545
+ {["SÉCURITÉ","PERFORMANCE","ARCHITECTURE","EDGE CASES","QUALITÉ"].map(c=><option key={c}>{c}</option>)}
546
+ </select>
547
+ </div>
548
+ <div style={{display:"flex",gap:8,marginBottom:12}}>
549
+ <input className="vba-input" value={newPat.rule} onChange={e=>setNewPat(p=>({...p,rule:e.target.value}))}
550
+ onKeyDown={e=>{if(e.key==="Enter"&&newPat.rule.trim()){setPatterns(prev=>[...prev,{id:Date.now(),...newPat}]);setNewPat(p=>({...p,rule:""}));}}}
551
+ placeholder="Ex: Chaque appel externe doit avoir un timeout…"/>
552
+ <button className="vba-btn vba-btn-primary" onClick={()=>{if(!newPat.rule.trim())return;setPatterns(prev=>[...prev,{id:Date.now(),...newPat}]);setNewPat(p=>({...p,rule:""}));}} style={{flexShrink:0}}>+</button>
553
+ </div>
554
+ {patterns.map(p=>(
555
+ <div key={p.id} style={{display:"flex",gap:8,padding:"8px 10px",background:"var(--bg3)",borderRadius:10,marginBottom:6,alignItems:"flex-start"}}>
556
+ <span className="vba-badge" style={{background:"#1E1040",color:"var(--accent)",flexShrink:0}}>{p.category}</span>
557
+ <span style={{fontSize:12,flex:1,lineHeight:1.5}}>{p.rule}</span>
558
+ <button onClick={()=>setPatterns(prev=>prev.filter(x=>x.id!==p.id))} style={{background:"none",border:"none",cursor:"pointer",color:"var(--muted)",fontSize:16,padding:0,flexShrink:0}}>×</button>
559
+ </div>
560
+ ))}
561
+ {!patterns.length && <p style={{margin:0,fontSize:12,color:"var(--muted)",fontStyle:"italic"}}>Aucune règle — les règles de stack sont actives par défaut.</p>}
562
+ </div>
563
+
564
+ <div style={{marginTop:20}}>
565
+ <button className="vba-btn vba-btn-danger" onClick={()=>{if(!confirm("Tout réinitialiser ?"))return;setChecks(initCh());setFindings(initArr());setFileBlks(initStr());setBlocNums(initStr());setNotes(initStr());setDates(initStr());setVerdict(null);setActiveId("p0");}}>
566
+ 🗑 Réinitialiser l'audit
567
+ </button>
568
+ </div>
569
+ </div>
570
+ )}
571
+
572
+
573
+ {/* ═══ AUDIT TAB ══════════════════════════════════════════════════ */}
574
+ {tab==="audit" && (
575
+ <div className="vba-anim-in">
576
+ {/* Phase selector */}
577
+ <div style={{marginBottom:12}}>
578
+ {GROUPS.map(g=>{
579
+ const gPhases=PHASES.filter(p=>p.group===g.id);
580
+ const gpct=groupPct(g.id);
581
+ return(
582
+ <div key={g.id} style={{marginBottom:4}}>
583
+ <div style={{display:"flex",alignItems:"center",gap:8,padding:"6px 8px",color:"var(--muted)",fontSize:11,fontWeight:700,textTransform:"uppercase",letterSpacing:".06em"}}>
584
+ <span>{GRP_ICONS[g.id]}</span><span style={{flex:1}}>{g.label}</span>
585
+ <span style={{color:gpct===100?"var(--green)":"var(--muted)"}}>{gpct}%</span>
586
+ </div>
587
+ {gPhases.map(p=>{
588
+ const st=pSt(p.id);
589
+ const fCnt=(findings[p.id]||[]).filter(f=>!f.resolved).length;
590
+ const sel=activeId===p.id;
591
+ return(
592
+ <button key={p.id} className={`vba-phase-btn${sel?" active":""}`} onClick={()=>setActiveId(p.id)}>
593
+ <span style={{fontSize:16,flexShrink:0}}>{p.icon}</span>
594
+ <span style={{fontSize:12,flex:1,color:sel?"var(--accent)":st==="done"?"var(--green)":"var(--text)",fontWeight:sel?600:400,lineHeight:1.3}}>{p.label}</span>
595
+ <div style={{display:"flex",gap:4,alignItems:"center",flexShrink:0}}>
596
+ {fCnt>0&&<span className="vba-badge" style={{background:SEV.CRITIQUE.badge+"22",color:SEV.CRITIQUE.badge}}>{fCnt}</span>}
597
+ <span style={{fontSize:12,color:st==="done"?"var(--green)":st==="partial"?"var(--yellow)":"var(--muted2)"}}>{st==="done"?"✓":st==="partial"?"◐":"○"}</span>
598
+ </div>
599
+ </button>
600
+ );
601
+ })}
602
+ </div>
603
+ );
604
+ })}
605
+ </div>
606
+
607
+ {/* Phase detail */}
608
+ <div className="vba-card" style={{marginBottom:12,borderLeft:`3px solid ${phase.color||"var(--accent)"}`}}>
609
+ <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:12}}>
610
+ <span style={{fontSize:24}}>{phase.icon}</span>
611
+ <div style={{flex:1}}>
612
+ <div style={{fontSize:10,fontWeight:700,color:phase.color||"var(--accent)",textTransform:"uppercase",letterSpacing:".06em"}}>Phase {phase.n} · {phase.tag}</div>
613
+ <div style={{fontSize:15,fontWeight:700}}>{phase.label}</div>
614
+ </div>
615
+ <div style={{textAlign:"right"}}>
616
+ <div style={{fontSize:18,fontWeight:800,color:pPct(phase.id).pct===100?"var(--green)":"var(--accent)"}}>{pPct(phase.id).pct}%</div>
617
+ <div style={{fontSize:10,color:"var(--muted)"}}>{pPct(phase.id).done}/{pPct(phase.id).total}</div>
618
+ </div>
619
+ </div>
620
+ <div style={{height:3,background:"var(--bg3)",borderRadius:2,overflow:"hidden",marginBottom:12}}>
621
+ <div style={{height:"100%",width:`${pPct(phase.id).pct}%`,background:pPct(phase.id).pct===100?"var(--green)":"var(--accent)",borderRadius:2,transition:"width .4s"}}/>
622
+ </div>
623
+ <div style={{fontSize:12,color:"var(--muted)",lineHeight:1.6,marginBottom:4}}><strong style={{color:"var(--text)"}}>Quoi :</strong> {phase.what}</div>
624
+ <div style={{fontSize:12,color:"var(--muted)",lineHeight:1.6}}><strong style={{color:"var(--text)"}}>Comment :</strong> {phase.how}</div>
625
+ </div>
626
+
627
+ {/* Fichiers du bloc */}
628
+ <div className="vba-card" style={{marginBottom:12}}>
629
+ <div style={{fontSize:11,fontWeight:700,color:"var(--muted)",textTransform:"uppercase",letterSpacing:".06em",marginBottom:8}}>📁 Fichiers à auditer</div>
630
+ <div style={{display:"flex",gap:8,marginBottom:8}}>
631
+ <input className="vba-input" value={blocNums[phase.id]||""} onChange={e=>setBlocNums(p=>({...p,[phase.id]:e.target.value}))} placeholder="Bloc n°" style={{width:70,textAlign:"center",fontFamily:"monospace",flexShrink:0}}/>
632
+ <button className={`vba-btn ${dirFiles?"vba-btn-primary":"vba-btn-ghost"}`} style={{fontSize:12,flexShrink:0}}
633
+ onClick={()=>document.getElementById('vba-dir-input').click()}>{dirFiles?"📁 Connecté":"📁 Connecter"}</button>
634
+ </div>
635
+ <textarea className="vba-input" value={fileBlks[phase.id]||""} onChange={e=>setFileBlks(p=>({...p,[phase.id]:e.target.value}))}
636
+ placeholder={"src/app/api/auth/route.ts\nsrc/lib/supabase.ts\nsrc/hooks/useAuth.ts"}
637
+ rows={3} style={{fontFamily:"monospace",fontSize:11,resize:"vertical"}}/>
638
+ </div>
639
+
640
+ {/* Prompt & Auto-Audit */}
641
+ <div className="vba-card" style={{marginBottom:12}}>
642
+ <div style={{display:"flex",gap:8,flexWrap:"wrap",marginBottom:10}}>
643
+ <button className={`vba-btn vba-btn-primary${autoRunning?" vba-pulse":""}`} onClick={runAutoAudit} disabled={autoRunning} style={{flex:1,justifyContent:"center"}}>
644
+ {autoRunning?"⏳ Analyse en cours…":"🚀 Auto-Audit IA"}
645
+ </button>
646
+ <button className="vba-btn vba-btn-ghost" style={{fontSize:12}} onClick={()=>clip(buildPrompt(phase,project,loadedCode[phase.id]||fileBlks[phase.id],blocNums[phase.id],patterns,bizCtx,findings),phase.id)}>
647
+ {copied===phase.id?"✓":"📋"} Copier prompt
648
+ </button>
649
+ </div>
650
+ <div style={{display:"flex",justifyContent:"space-between",fontSize:10,color:"var(--muted)"}}>
651
+ <span>1. Auto-Audit → IA lit tes fichiers directement</span>
652
+ <span>ou</span>
653
+ <span>Copie → colle dans Antigravity</span>
654
+ </div>
655
+ </div>
656
+
657
+ {/* Checklist */}
658
+ <div className="vba-card" style={{marginBottom:12}}>
659
+ <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:12}}>
660
+ <div style={{fontSize:13,fontWeight:600}}>Checklist de validation</div>
661
+ {pPct(phase.id).done<pPct(phase.id).total&&(
662
+ <button className="vba-btn vba-btn-ghost" style={{fontSize:11}} onClick={()=>{setChecks(p=>({...p,[phase.id]:p[phase.id].map(()=>true)}));if(!dates[phase.id])setDates(d=>({...d,[phase.id]:new Date().toISOString()}));}}>Tout ✓</button>
663
+ )}
664
+ </div>
665
+ {phase.checks.map((c,i)=>(
666
+ <div key={i} className="vba-check-item" onClick={()=>toggleCheck(phase.id,i)}>
667
+ <div style={{width:20,height:20,borderRadius:10,border:`2px solid ${checks[phase.id]?.[i]?"var(--green)":"var(--border)"}`,background:checks[phase.id]?.[i]?"var(--green)":"transparent",display:"flex",alignItems:"center",justifyContent:"center",flexShrink:0,marginTop:1,transition:"all .15s"}}>
668
+ {checks[phase.id]?.[i]&&<svg width="9" height="7" viewBox="0 0 9 7" fill="none"><path d="M1 3.5L3.5 6L8 1" stroke="#000" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>}
669
+ </div>
670
+ <span style={{fontSize:12,color:checks[phase.id]?.[i]?"var(--muted)":"var(--text)",textDecoration:checks[phase.id]?.[i]?"line-through":"none",lineHeight:1.5}}>{c}</span>
671
+ </div>
672
+ ))}
673
+ {pPct(phase.id).pct===100&&(
674
+ <div style={{marginTop:12,padding:"8px 12px",background:"#052e16",border:"1px solid var(--green)",borderRadius:10,fontSize:12,color:"#86EFAC",fontWeight:500}}>
675
+ ✅ Phase complète{nextP&&<> · <button onClick={()=>setActiveId(nextP.id)} style={{background:"none",border:"none",cursor:"pointer",color:"#86EFAC",textDecoration:"underline",fontSize:12}}>Phase suivante →</button></>}
676
+ </div>
677
+ )}
678
+ </div>
679
+
680
+ {/* Notes IA */}
681
+ <div className="vba-card" style={{marginBottom:12}}>
682
+ <div style={{fontSize:11,fontWeight:700,color:"var(--muted)",textTransform:"uppercase",letterSpacing:".06em",marginBottom:8}}>📝 Notes & Résultats IA</div>
683
+ <textarea className="vba-input" value={notes[phase.id]||""} onChange={e=>setNotes(n=>({...n,[phase.id]:e.target.value}))}
684
+ placeholder="Les résultats IA apparaîtront ici automatiquement…" rows={5} style={{fontFamily:"monospace",fontSize:11,resize:"vertical"}}/>
685
+ </div>
686
+
687
+ {/* Findings */}
688
+ <div className="vba-card" style={{marginBottom:12}}>
689
+ <div style={{fontSize:13,fontWeight:600,marginBottom:12}}>🔍 Findings <span style={{fontSize:11,color:"var(--muted)"}}>({(findings[phase.id]||[]).length})</span></div>
690
+ <div style={{display:"flex",gap:8,marginBottom:10}}>
691
+ <select className="vba-input" value={newF.sev} onChange={e=>setNewF(f=>({...f,sev:e.target.value}))} style={{width:110,flexShrink:0}}>
692
+ {SEVS.map(s=><option key={s}>{s}</option>)}
693
+ </select>
694
+ <input className="vba-input" value={newF.txt} onChange={e=>setNewF(f=>({...f,txt:e.target.value}))}
695
+ onKeyDown={e=>{if(e.key==="Enter")addFinding(phase.id);}}
696
+ placeholder="Décrire la vulnérabilité…"/>
697
+ <button className="vba-btn vba-btn-primary" onClick={()=>addFinding(phase.id)} style={{flexShrink:0}}>+</button>
698
+ </div>
699
+ {(findings[phase.id]||[]).map(f=>(
700
+ <div key={f.id} style={{padding:"10px 12px",background:SEV[f.severity]?.bg||"var(--bg3)",border:`1px solid ${SEV[f.severity]?.border||"var(--border)"}`,borderRadius:10,marginBottom:8}}>
701
+ <div style={{display:"flex",alignItems:"flex-start",gap:8,marginBottom:6}}>
702
+ <span className="vba-badge" style={{background:SEV[f.severity]?.badge+"33",color:SEV[f.severity]?.badge,flexShrink:0}}>{f.severity}</span>
703
+ <span style={{fontSize:12,color:f.resolved?"var(--muted)":"var(--text)",textDecoration:f.resolved?"line-through":"none",flex:1,lineHeight:1.5}}>{f.text}</span>
704
+ </div>
705
+ <div style={{display:"flex",gap:6,flexWrap:"wrap"}}>
706
+ <button className="vba-btn vba-btn-ghost" style={{fontSize:10,padding:"3px 8px"}} onClick={()=>setFindings(prev=>({...prev,[phase.id]:(prev[phase.id]||[]).map(x=>x.id===f.id?{...x,resolved:!x.resolved}:x)}))}>
707
+ {f.resolved?"↩ Réouvrir":"✓ Résolu"}
708
+ </button>
709
+ {!fixState[f.id]&&<button className="vba-btn vba-btn-ghost" style={{fontSize:10,padding:"3px 8px",color:"var(--accent)",borderColor:"var(--accent)"}} onClick={()=>generateFix(phase.id,f)}>⚡ Fix IA</button>}
710
+ <button className="vba-btn vba-btn-ghost" style={{fontSize:10,padding:"3px 8px",color:"var(--red)"}} onClick={()=>setFindings(prev=>({...prev,[phase.id]:(prev[phase.id]||[]).filter(x=>x.id!==f.id)}))}>🗑</button>
711
+ </div>
712
+ {fixState[f.id]?.loading&&<div style={{marginTop:8,fontSize:11,color:"var(--muted)"}} className="vba-matrix-anim">⏳ {aiCfg.godMode?"Génération & Application du correctif en cours...":"Génération du correctif..."}</div>}
713
+ {fixState[f.id]?.code&&(
714
+ <div style={{marginTop:8,padding:"10px",background:"#0D1F0D",border:"1px solid #22C55E",borderRadius:8}}>
715
+ <div style={{fontSize:11,color:"#86EFAC",marginBottom:4}}>✅ {fixState[f.id].explanation} (confiance: {Math.round((fixState[f.id].confidence||0.9)*100)}%)</div>
716
+ <div style={{fontSize:10,color:"#668",marginBottom:8}}>Fichier ciblé: {fixState[f.id].filePath||"Inconnu"}</div>
717
+ <pre style={{margin:0,fontSize:10,color:"#E2E8F0",fontFamily:"monospace",whiteSpace:"pre-wrap",wordBreak:"break-word",maxHeight:150,overflowY:"auto"}}>{fixState[f.id].code}</pre>
718
+ <div style={{display:"flex",gap:8,marginTop:8}}>
719
+ <button className="vba-btn vba-btn-ghost" style={{fontSize:10}} onClick={()=>clip(fixState[f.id].code,"fix"+f.id)}>📋 Copier</button>
720
+ <button className="vba-btn vba-btn-primary" style={{fontSize:10,background:"var(--green)",color:"#000"}} onClick={()=>applyFix(f.id, fixState[f.id].code, fixState[f.id].filePath)}>
721
+ {fixState[f.id].applying?"Écriture...":"💾 Écrire dans le fichier (Self-Heal)"}
722
+ </button>
723
+ </div>
724
+ </div>
725
+ )}
726
+ {fixState[f.id]?.error&&<div style={{marginTop:8,fontSize:11,color:"var(--red)"}}>❌ Erreur de génération. Réessaie.</div>}
727
+ </div>
728
+ ))}
729
+ {!(findings[phase.id]||[]).length&&<p style={{margin:0,fontSize:12,color:"var(--muted)",fontStyle:"italic",textAlign:"center",padding:"12px 0"}}>Aucun finding — Lance l'Auto-Audit ou ajoute manuellement.</p>}
730
+ </div>
731
+
732
+ {/* Navigation */}
733
+ <div style={{display:"flex",gap:8}}>
734
+ {prevP&&<button className="vba-btn vba-btn-ghost" style={{flex:1,justifyContent:"center"}} onClick={()=>setActiveId(prevP.id)}>← {prevP.label.slice(0,20)}</button>}
735
+ {nextP&&<button className="vba-btn vba-btn-primary" style={{flex:1,justifyContent:"center"}} onClick={()=>setActiveId(nextP.id)}>{nextP.label.slice(0,20)} →</button>}
736
+ </div>
737
+ </div>
738
+ )}
739
+
740
+
741
+ {/* ═══ SCANNER TAB ════════════════════════════════════════════════ */}
742
+ {tab==="scan" && (
743
+ <div className="vba-anim-in">
744
+ <div className="vba-card" style={{marginBottom:12}}>
745
+ <div style={{fontSize:15,fontWeight:700,marginBottom:4}}>🔍 Scanner Automatique</div>
746
+ <div style={{fontSize:12,color:"var(--muted)",lineHeight:1.6,marginBottom:12}}>Connecte ton dossier projet. Le scanner va découvrir <strong style={{color:"var(--text)"}}>tous les fichiers</strong> (.ts, .tsx, .js, .jsx, .py…), les grouper en blocs de 4, et appliquer le prompt sécurité strict sur chacun. Couverture garantie à 100%.</div>
747
+ <div style={{display:"flex",gap:8,marginBottom:12}}>
748
+ <button className={`vba-btn ${dirFiles?"vba-btn-primary":"vba-btn-ghost"}`} style={{flex:1,justifyContent:"center"}}
749
+ onClick={()=>document.getElementById('vba-dir-input').click()}>
750
+ {dirFiles?"📁 Dossier connecté ✓":"📁 Connecter un dossier"}
751
+ </button>
752
+ <button className={`vba-btn ${scanning?"vba-btn-ghost":"vba-btn-primary"}`} onClick={scanDirectory} disabled={scanning} style={{flex:1,justifyContent:"center",overflow:"hidden",position:"relative"}}>
753
+ {scanning?<span className="vba-matrix-anim">▓▒░ ANALYSE EN COURS ░▒▓</span>:"🚀 Lancer le scan"}
754
+ </button>
755
+ </div>
756
+ {scanning && (
757
+ <div style={{background:"#001100",border:"1px solid #004400",borderRadius:8,padding:12,marginTop:12,textAlign:"center"}}>
758
+ <div className="vba-matrix-anim" style={{fontSize:14,fontWeight:700,marginBottom:6}}>{scanProgress.current ? `Décryptage: ${scanProgress.current.split(",").length} fichiers...` : "Initialisation du moteur IA..."}</div>
759
+ <div style={{fontSize:11,color:"#22C55E88"}}>Bloc {scanProgress.done} / {scanProgress.total}</div>
760
+ </div>
761
+ )}
762
+ {!isConfigured&&<div style={{padding:"8px 12px",background:"#1C1500",border:"1px solid #713F12",borderRadius:10,fontSize:12,color:"#FDE68A"}}>⚠️ Configure d'abord ta clé API dans l'onglet <strong>Config</strong></div>}
763
+ </div>
764
+
765
+ {/* Progression */}
766
+ {scanning&&(
767
+ <div className="vba-card" style={{marginBottom:12}}>
768
+ <div style={{display:"flex",justifyContent:"space-between",marginBottom:8}}>
769
+ <span style={{fontSize:12,fontWeight:600}}>Progression</span>
770
+ <span style={{fontSize:12,color:"var(--accent)",fontWeight:700}}>{scanProgress.done}/{scanProgress.total} blocs</span>
771
+ </div>
772
+ <div style={{height:6,background:"var(--bg3)",borderRadius:3,overflow:"hidden",marginBottom:8}}>
773
+ <div style={{height:"100%",width:`${scanProgress.total?Math.round(scanProgress.done/scanProgress.total*100):0}%`,background:"var(--accent)",borderRadius:3,transition:"width .4s"}}/>
774
+ </div>
775
+ {scanProgress.current&&<div style={{fontSize:10,color:"var(--muted)",fontFamily:"monospace",wordBreak:"break-all"}}>→ {scanProgress.current}</div>}
776
+ </div>
777
+ )}
778
+
779
+ {/* Stats fichiers */}
780
+ {scanFiles.length>0&&(
781
+ <div className="vba-card" style={{marginBottom:12}}>
782
+ <div style={{display:"flex",justifyContent:"space-between",marginBottom:8}}>
783
+ <span style={{fontSize:13,fontWeight:600}}>Fichiers découverts</span>
784
+ <span className="vba-badge" style={{background:"#1E1040",color:"var(--accent)"}}>{scanFiles.length}</span>
785
+ </div>
786
+ <div style={{maxHeight:120,overflowY:"auto"}} className="vba-scroll">
787
+ {scanFiles.map((f,i)=>(
788
+ <div key={i} style={{fontSize:10,color:"var(--muted)",fontFamily:"monospace",padding:"2px 0",borderTop:i>0?"1px solid var(--border)":"none"}}>{f}</div>
789
+ ))}
790
+ </div>
791
+ </div>
792
+ )}
793
+
794
+ {/* Résultats scan */}
795
+ {scanResults.length>0&&(
796
+ <div>
797
+ <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:8}}>
798
+ <div style={{fontSize:13,fontWeight:700}}>Résultats</div>
799
+ <div style={{display:"flex",gap:6}}>
800
+ <span className="vba-badge" style={{background:"#052e16",color:"var(--green)"}}>{scanResults.filter(r=>r.safe).length} safe</span>
801
+ <span className="vba-badge" style={{background:"#3B0000",color:"var(--red)"}}>{scanResults.filter(r=>!r.safe&&!r.error).length} failles</span>
802
+ </div>
803
+ </div>
804
+ {scanResults.map((r,i)=>(
805
+ <div key={i} className="vba-card" style={{marginBottom:8,borderLeft:`3px solid ${r.error?"var(--yellow)":r.safe?"var(--green)":"var(--red)"}`}}>
806
+ <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:r.safe?0:8}}>
807
+ <span style={{fontSize:14}}>{r.error?"⚠️":r.safe?"✅":"🔴"}</span>
808
+ <div style={{flex:1}}>
809
+ <div style={{fontSize:11,color:"var(--muted)",fontFamily:"monospace"}}>{r.block.join(", ")}</div>
810
+ </div>
811
+ <button className="vba-btn vba-btn-ghost" style={{fontSize:10,padding:"2px 8px"}} onClick={()=>clip(r.raw,"scan"+i)}>📋</button>
812
+ </div>
813
+ {!r.safe&&<pre style={{margin:0,fontSize:11,color:"var(--text)",whiteSpace:"pre-wrap",wordBreak:"break-word",lineHeight:1.6,fontFamily:"monospace",maxHeight:200,overflowY:"auto"}} className="vba-scroll">{r.raw}</pre>}
814
+ </div>
815
+ ))}
816
+ <button className="vba-btn vba-btn-ghost" style={{width:"100%",justifyContent:"center",marginTop:8}} onClick={()=>{const blob=new Blob([JSON.stringify({date:new Date().toISOString(),project:project.name,files:scanFiles.length,results:scanResults},null,2)],{type:"application/json"});const a=Object.assign(document.createElement("a"),{href:URL.createObjectURL(blob),download:`scan-${project.name||"audit"}-${new Date().toISOString().slice(0,10)}.json`});a.click();}}>
817
+ ⬇ Exporter le rapport JSON
818
+ </button>
819
+ </div>
820
+ )}
821
+ </div>
822
+ )}
823
+
824
+ {/* ═══ REPORT TAB ════════════════════════════════════════════════ */}
825
+ {tab==="report" && (
826
+ <div className="vba-anim-in">
827
+ {/* Score global */}
828
+ <div className="vba-card" style={{marginBottom:12,textAlign:"center",padding:"24px 16px"}}>
829
+ <div style={{display:"flex",justifyContent:"center",marginBottom:12,position:"relative"}}>
830
+ <RadialProgress pct={totPct.pct} size={100} stroke={8} color={totPct.pct===100?"var(--green)":totPct.pct>=70?"var(--accent)":"var(--red)"}/>
831
+ <div style={{position:"absolute",inset:0,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center"}}>
832
+ <div style={{fontSize:24,fontWeight:800,color:totPct.pct===100?"var(--green)":"var(--text)"}}>{totPct.pct}%</div>
833
+ <div style={{fontSize:10,color:"var(--muted)"}}>Complété</div>
834
+ </div>
835
+ </div>
836
+ <div style={{fontSize:15,fontWeight:700,marginBottom:4}}>{project.name||"Projet"} · {sp.label}</div>
837
+ <div style={{fontSize:12,color:"var(--muted)",marginBottom:16}}>{totPct.done}/{totPct.total} vérifications · {allF.length} findings total</div>
838
+ <div style={{display:"flex",gap:8,justifyContent:"center",flexWrap:"wrap",marginBottom:16}}>
839
+ {["pass","warn","blocked"].map(v=>(
840
+ <button key={v} onClick={()=>setVerdict(v)}
841
+ style={{padding:"8px 16px",borderRadius:99,border:`2px solid ${verdict===v?(v==="pass"?"var(--green)":v==="warn"?"var(--yellow)":"var(--red)"):"var(--border)"}`,background:verdict===v?(v==="pass"?"#052e16":v==="warn"?"#1C1500":"#3B0000"):"transparent",color:verdict===v?(v==="pass"?"var(--green)":v==="warn"?"var(--yellow)":"var(--red)"):"var(--muted)",fontSize:12,fontWeight:600,cursor:"pointer"}}>
842
+ {v==="pass"?"🟢 PRÊT":v==="warn"?"🟡 AVEC RÉSERVES":"🔴 NON PRÊT"}
843
+ </button>
844
+ ))}
845
+ </div>
846
+ </div>
847
+
848
+ {/* Sévérités */}
849
+ <div className="vba-card" style={{marginBottom:12}}>
850
+ <div style={{fontSize:13,fontWeight:600,marginBottom:10}}>Résumé des findings</div>
851
+ {SEVS.map(s=>(
852
+ <div key={s} style={{display:"flex",alignItems:"center",gap:10,padding:"6px 0",borderTop:"1px solid var(--border)"}}>
853
+ <span className="vba-badge" style={{background:SEV[s]?.bg,color:SEV[s]?.badge,width:70,justifyContent:"center"}}>{s}</span>
854
+ <div style={{flex:1,height:6,background:"var(--bg3)",borderRadius:3,overflow:"hidden"}}>
855
+ <div style={{height:"100%",width:`${allF.length?Math.round(sevC[s]/allF.length*100):0}%`,background:SEV[s]?.badge,borderRadius:3,transition:"width .4s"}}/>
856
+ </div>
857
+ <span style={{fontSize:13,fontWeight:700,color:SEV[s]?.badge,width:24,textAlign:"right"}}>{sevC[s]}</span>
858
+ </div>
859
+ ))}
860
+ </div>
861
+
862
+ {/* Phases résumé */}
863
+ <div className="vba-card" style={{marginBottom:12}}>
864
+ <div style={{fontSize:13,fontWeight:600,marginBottom:10}}>Avancement par phase</div>
865
+ {PHASES.map(p=>{const {pct}=pPct(p.id); return(
866
+ <div key={p.id} style={{display:"flex",alignItems:"center",gap:8,padding:"5px 0",borderTop:"1px solid var(--border)"}}>
867
+ <span style={{fontSize:12}}>{p.icon}</span>
868
+ <span style={{fontSize:11,flex:1,color:"var(--text)"}}>{p.label}</span>
869
+ <div style={{width:50,height:4,background:"var(--bg3)",borderRadius:2,overflow:"hidden"}}>
870
+ <div style={{height:"100%",width:`${pct}%`,background:pct===100?"var(--green)":"var(--accent)",borderRadius:2}}/>
871
+ </div>
872
+ <span style={{fontSize:11,color:pct===100?"var(--green)":"var(--muted)",width:30,textAlign:"right"}}>{pct}%</span>
873
+ </div>
874
+ );})}
875
+ </div>
876
+
877
+ {/* Actions export */}
878
+ <div className="vba-card" style={{marginBottom:12}}>
879
+ <div style={{fontSize:13,fontWeight:600,marginBottom:10}}>Export & Historique</div>
880
+ <div style={{display:"flex",flexDirection:"column",gap:8}}>
881
+ <button className="vba-btn vba-btn-primary" style={{justifyContent:"center"}} onClick={()=>{const blob=new Blob([buildReport()],{type:"text/markdown"});const a=Object.assign(document.createElement("a"),{href:URL.createObjectURL(blob),download:`rapport-${project.name||"audit"}-${new Date().toISOString().slice(0,10)}.md`});a.click();}}>⬇ Télécharger le rapport Markdown</button>
882
+ <button className="vba-btn vba-btn-ghost" style={{justifyContent:"center"}} onClick={saveSnap}>📸 Sauvegarder un snapshot ({history.length})</button>
883
+ <button className="vba-btn vba-btn-ghost" style={{justifyContent:"center"}} onClick={()=>clip(buildReport(),"report")}>📋 Copier le rapport</button>
884
+ </div>
885
+ </div>
886
+
887
+ {/* Historique */}
888
+ {history.length>0&&(
889
+ <div className="vba-card">
890
+ <div style={{fontSize:13,fontWeight:600,marginBottom:10}}>📈 Historique ({history.length})</div>
891
+ {history.slice(-5).reverse().map((snap,i)=>(
892
+ <div key={i} style={{display:"flex",gap:10,padding:"8px 0",borderTop:"1px solid var(--border)",alignItems:"center"}}>
893
+ <div style={{flex:1}}>
894
+ <div style={{fontSize:12,fontWeight:500}}>{snap.project}</div>
895
+ <div style={{fontSize:10,color:"var(--muted)"}}>{new Date(snap.date).toLocaleDateString("fr-FR")} · {snap.stack}</div>
896
+ </div>
897
+ <div style={{fontSize:18,fontWeight:800,color:snap.pct===100?"var(--green)":snap.pct>=70?"var(--accent)":"var(--red)"}}>{snap.pct}%</div>
898
+ </div>
899
+ ))}
900
+ </div>
901
+ )}
902
+ </div>
903
+ )}
904
+
905
+ </div>
906
+ </div>
907
+ );
908
+ }