WEVIA Apple v3: full iPhone ingestion + rich drill-down + MCPs + 7 intents
This commit is contained in:
105
api/wevia-apple-intents.php
Normal file
105
api/wevia-apple-intents.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
// WEVIA APPLE v3 INTENTS — Opus 20avr
|
||||
// Sourced by WEVIA Master dispatcher via wevia-tool-registry.json
|
||||
|
||||
function wevia_apple_v3_intents($prompt) {
|
||||
// apple status
|
||||
if (preg_match('/\b(apple|iphone|wevia\s*apple)\s*(status|stat|etat|count)\b|combien.*photos?.*apple/iu', $prompt)) {
|
||||
$s = @json_decode(@file_get_contents("http://127.0.0.1/api/wevia-apple-ingest.php?action=status"), true);
|
||||
if ($s) {
|
||||
$lines = ["🍎 WEVIA Apple v3 — État actuel"];
|
||||
$lines[] = "Items ingérés: " . ($s["total_items"] ?? 0);
|
||||
foreach ($s["by_type"] ?? [] as $t => $n) if ($n) $lines[] = " · " . $t . ": " . $n;
|
||||
$lines[] = "Tasks pending: " . ($s["tasks_pending"] ?? 0);
|
||||
$lines[] = "Alertes P0: " . ($s["alerts"] ?? 0);
|
||||
$lines[] = "Opportunities: " . ($s["opportunities"] ?? 0);
|
||||
$lines[] = "\nUI: https://weval-consulting.com/wevia-apple.html";
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
}
|
||||
// apple recommendations
|
||||
if (preg_match('/\b(apple|iphone)\s*(reco|recommand)/iu', $prompt) || preg_match('/recommand.*(apple|iphone)/iu', $prompt)) {
|
||||
$r = @json_decode(@file_get_contents("http://127.0.0.1/api/wevia-apple-ingest.php?action=recommendations"), true);
|
||||
if ($r) {
|
||||
$lines = ["💡 Top recommandations WEVIA Apple (" . ($r["total"] ?? 0) . " total)"];
|
||||
foreach (array_slice($r["recommendations"] ?? [], 0, 10) as $i => $reco) {
|
||||
$lines[] = ($i+1) . ". [" . $reco["priority"] . "] " . $reco["label"];
|
||||
$lines[] = " → " . $reco["action"];
|
||||
}
|
||||
if (empty($r["recommendations"])) $lines[] = "(Aucune reco — ingère du contenu d'abord)";
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
}
|
||||
// apple alerts
|
||||
if (preg_match('/\b(apple|iphone)\s*(alert|urgent|urgence)/iu', $prompt)) {
|
||||
$a = @json_decode(@file_get_contents("http://127.0.0.1/api/wevia-apple-ingest.php?action=alerts"), true);
|
||||
if ($a) {
|
||||
$lines = ["🚨 Alertes WEVIA Apple (" . count($a["alerts"] ?? []) . ")"];
|
||||
foreach ($a["alerts"] ?? [] as $al) {
|
||||
$lines[] = "- " . ($al["label"] ?? "") . " → " . ($al["action"] ?? "");
|
||||
}
|
||||
if (empty($a["alerts"])) $lines[] = "Aucune alerte urgente 👍";
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
}
|
||||
// apple ingest note
|
||||
if (preg_match('/^(apple\s*note|ingest\s*note|note\s*apple)\s*:?\s*(.+)/iu', $prompt, $m)) {
|
||||
$body = trim($m[2]);
|
||||
if ($body) {
|
||||
$ch = curl_init("http://127.0.0.1/api/wevia-apple-ingest.php?action=ingest_structured");
|
||||
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>1,CURLOPT_POST=>1,
|
||||
CURLOPT_HTTPHEADER=>["Content-Type: application/json"],
|
||||
CURLOPT_POSTFIELDS=>json_encode(["type"=>"note","items"=>[["title"=>"WEVIA Master ingest","body"=>$body]]])]);
|
||||
$rr = curl_exec($ch); curl_close($ch);
|
||||
$d = @json_decode($rr, true);
|
||||
return "📝 Note ingérée → ID: " . ($d["ids"][0] ?? "?") . " · Analyse IA en cours (entités + reco)";
|
||||
}
|
||||
}
|
||||
// apple entities
|
||||
if (preg_match('/\b(apple|iphone)\s*(entit|entities)/iu', $prompt)) {
|
||||
$e = @json_decode(@file_get_contents("http://127.0.0.1/api/wevia-apple-ingest.php?action=entities"), true);
|
||||
if ($e) {
|
||||
$lines = ["🔎 Entités extraites WEVIA Apple"];
|
||||
foreach ($e["entities"] ?? [] as $cat => $list) {
|
||||
if (!empty($list)) {
|
||||
$top = array_slice($list, 0, 5);
|
||||
$vals = array_map(function($x) { return $x["value"] . " (×" . $x["count"] . ")"; }, $top);
|
||||
$lines[] = $cat . " [" . count($list) . "]: " . implode(", ", $vals);
|
||||
}
|
||||
}
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
}
|
||||
// apple tasks
|
||||
if (preg_match('/\b(apple|iphone)\s*(task|tache)/iu', $prompt)) {
|
||||
$t = @json_decode(@file_get_contents("http://127.0.0.1/api/wevia-apple-ingest.php?action=tasks"), true);
|
||||
if ($t) {
|
||||
$lines = ["📋 Tasks WEVIA Apple (" . count($t["tasks"] ?? []) . ")"];
|
||||
foreach (array_slice($t["tasks"] ?? [], 0, 10) as $task) {
|
||||
$lines[] = "- [" . ($task["priority"] ?? "") . "] " . ($task["label"] ?? "") . " (" . ($task["status"] ?? "open") . ")";
|
||||
}
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
}
|
||||
// apple setup / iphone setup
|
||||
if (preg_match('/\b(apple|iphone)\s*(setup|install|config|shortcut)/iu', $prompt)) {
|
||||
return "📲 WEVIA Apple v3 Setup\n\n"
|
||||
. "1. Ouvre https://weval-consulting.com/wevia-apple.html sur ton iPhone\n"
|
||||
. "2. Clique '📲 iPhone Setup' en haut à droite\n"
|
||||
. "3. Suis les 3 solutions (iPhone Shortcuts / Automation iCloud / Blade MCP)\n\n"
|
||||
. "Endpoint: https://weval-consulting.com/api/wevia-apple-ingest.php\n"
|
||||
. "7 types supportés: photos, messages, contacts, calendar, notes, calls, health";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Auto-register with the orchestrator if available
|
||||
$__wa_prompt = $_POST['prompt'] ?? $_POST['message'] ?? $_GET['q'] ?? null;
|
||||
if ($__wa_prompt && (defined('WEVIA_APPLE_STANDALONE') || strpos($_SERVER['SCRIPT_NAME'] ?? '', 'wevia-apple-intents') !== false)) {
|
||||
$r = wevia_apple_v3_intents($__wa_prompt);
|
||||
if ($r !== null) {
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(["ok"=>true, "response"=>$r, "provider"=>"wevia-apple-v3"]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -3480,6 +3480,69 @@
|
||||
"id": "smart_tips_v84",
|
||||
"kw": "tips.*renouve|tips.*creation.*compte|tips.*cyber|tips.*selenium|tips.*office.*recovery|tips.*opus|tips.*disponibles|quels tips|liste tips",
|
||||
"cmd": "curl -sk --max-time 10 'http://127.0.0.1/api/wevia-dynamic-resolver.php?q=v82_tips_summary' -H 'Host: weval-consulting.com' 2>&1 | head -c 2500"
|
||||
},
|
||||
{
|
||||
"id": "apple_status",
|
||||
"name": "WEVIA Apple v3 status",
|
||||
"kw_regex": "\b(apple|iphone)\\s*(status|stat|etat|count)\b|combien.*photos?.*apple",
|
||||
"file": "/api/wevia-apple-intents.php",
|
||||
"action": "POST_prompt",
|
||||
"priority": 90,
|
||||
"desc": "État WEVIA Apple ingestion"
|
||||
},
|
||||
{
|
||||
"id": "apple_reco",
|
||||
"name": "WEVIA Apple recommendations",
|
||||
"kw_regex": "\b(apple|iphone)\\s*(reco|recommand)|recommand.*(apple|iphone)",
|
||||
"file": "/api/wevia-apple-intents.php",
|
||||
"action": "POST_prompt",
|
||||
"priority": 91,
|
||||
"desc": "Recommandations IA"
|
||||
},
|
||||
{
|
||||
"id": "apple_alerts",
|
||||
"name": "WEVIA Apple alerts",
|
||||
"kw_regex": "\b(apple|iphone)\\s*(alert|urgent|urgence)",
|
||||
"file": "/api/wevia-apple-intents.php",
|
||||
"action": "POST_prompt",
|
||||
"priority": 92,
|
||||
"desc": "Alertes P0"
|
||||
},
|
||||
{
|
||||
"id": "apple_ingest_note",
|
||||
"name": "WEVIA Apple ingest note",
|
||||
"kw_regex": "^(apple\\s*note|ingest\\s*note|note\\s*apple)\\s*:",
|
||||
"file": "/api/wevia-apple-intents.php",
|
||||
"action": "POST_prompt",
|
||||
"priority": 93,
|
||||
"desc": "Ingest note to Apple v3"
|
||||
},
|
||||
{
|
||||
"id": "apple_entities",
|
||||
"name": "WEVIA Apple entities",
|
||||
"kw_regex": "\b(apple|iphone)\\s*(entit|entities)",
|
||||
"file": "/api/wevia-apple-intents.php",
|
||||
"action": "POST_prompt",
|
||||
"priority": 94,
|
||||
"desc": "Entités extraites"
|
||||
},
|
||||
{
|
||||
"id": "apple_tasks",
|
||||
"name": "WEVIA Apple tasks",
|
||||
"kw_regex": "\b(apple|iphone)\\s*(task|tache)",
|
||||
"file": "/api/wevia-apple-intents.php",
|
||||
"action": "POST_prompt",
|
||||
"priority": 95,
|
||||
"desc": "Tasks pending"
|
||||
},
|
||||
{
|
||||
"id": "apple_setup",
|
||||
"name": "WEVIA Apple setup",
|
||||
"kw_regex": "\b(apple|iphone)\\s*(setup|install|config|shortcut)",
|
||||
"file": "/api/wevia-apple-intents.php",
|
||||
"action": "POST_prompt",
|
||||
"priority": 96,
|
||||
"desc": "Setup iPhone Shortcuts"
|
||||
}
|
||||
]
|
||||
}
|
||||
937
wevia-apple.html
937
wevia-apple.html
@@ -1,399 +1,618 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>WEVIA Apple — Photos Scanner iPhone + iCloud</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-title" content="WEVIA Scan">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>🍎 WEVIA Apple v3 — iPhone Intelligence Hub</title>
|
||||
<style>
|
||||
:root{--bg:#0a0e1a;--panel:#111827;--panel2:#1f2937;--br:#1f2937;--fg:#e5e7eb;--mute:#94a3b8;--accent:#60a5fa;--gold:#fbbf24;--ok:#22c55e;--warn:#f59e0b;--err:#ef4444}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;background:radial-gradient(ellipse at top,#0f172a 0%,var(--bg) 60%);color:var(--fg);font-family:-apple-system,BlinkMacSystemFont,"SF Pro Display","Segoe UI",system-ui,sans-serif;min-height:100vh}
|
||||
.wrap{max-width:1400px;margin:0 auto;padding:24px}
|
||||
.hdr{display:flex;align-items:center;gap:16px;margin-bottom:24px;padding-bottom:20px;border-bottom:1px solid var(--br);flex-wrap:wrap}
|
||||
.hdr .logo{width:48px;height:48px;background:linear-gradient(135deg,#000,#1d1d1f);border-radius:12px;display:grid;place-items:center;font-size:28px}
|
||||
.hdr h1{margin:0;font-size:1.6rem;font-weight:700;letter-spacing:-0.3px}
|
||||
.hdr .sub{color:var(--mute);font-size:.9rem;margin-top:2px}
|
||||
.hdr .spacer{flex:1}
|
||||
.hdr a.btn{background:rgba(96,165,250,.1);border:1px solid rgba(96,165,250,.3);color:var(--accent);padding:8px 14px;border-radius:8px;text-decoration:none;font-size:.85rem;font-weight:600;margin-left:8px}
|
||||
.hdr a.btn:hover{background:rgba(96,165,250,.2)}
|
||||
.hdr a.btn.shortcut{background:rgba(34,197,94,.1);border-color:rgba(34,197,94,.3);color:var(--ok)}
|
||||
.kpi-strip{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:24px}
|
||||
.kpi{background:linear-gradient(180deg,var(--panel),var(--panel2));border:1px solid var(--br);border-radius:12px;padding:16px;position:relative;overflow:hidden}
|
||||
.kpi::before{content:"";position:absolute;top:0;left:0;width:3px;height:100%}
|
||||
.kpi.k1::before{background:var(--accent)}.kpi.k2::before{background:var(--gold)}.kpi.k3::before{background:var(--ok)}.kpi.k4::before{background:#c084fc}
|
||||
.kpi .lbl{color:var(--mute);font-size:.72rem;text-transform:uppercase;letter-spacing:.8px;font-weight:600}
|
||||
.kpi .val{font-size:2rem;font-weight:700;margin-top:6px;letter-spacing:-.5px}
|
||||
.kpi .hint{color:var(--mute);font-size:.75rem;margin-top:4px}
|
||||
.grid{display:grid;grid-template-columns:2fr 1fr;gap:24px}
|
||||
.panel{background:var(--panel);border:1px solid var(--br);border-radius:12px;padding:20px}
|
||||
.panel h3{margin:0 0 16px;font-size:1.05rem;font-weight:600}
|
||||
.uploader{border:2px dashed rgba(96,165,250,.4);border-radius:12px;padding:32px;text-align:center;background:rgba(96,165,250,.05);transition:all .2s}
|
||||
.uploader.drag{background:rgba(96,165,250,.15);border-color:var(--accent)}
|
||||
.uploader .ico{font-size:48px;margin-bottom:12px}
|
||||
.uploader p{margin:8px 0;color:var(--mute)}
|
||||
.uploader .big{color:var(--fg);font-size:1.05rem;font-weight:600}
|
||||
.upload-btns{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-top:14px}
|
||||
.btn-primary{background:linear-gradient(135deg,#3b82f6,#1d4ed8);border:none;color:white;padding:11px 18px;border-radius:8px;font-weight:600;cursor:pointer;font-size:.9rem;transition:transform .15s}
|
||||
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 4px 12px rgba(59,130,246,.4)}
|
||||
.btn-secondary{background:rgba(148,163,184,.1);border:1px solid var(--br);color:var(--fg);padding:11px 18px;border-radius:8px;font-weight:600;cursor:pointer;font-size:.9rem}
|
||||
.btn-secondary:hover{background:rgba(148,163,184,.2)}
|
||||
.hidden-input{display:none}
|
||||
.queue{margin-top:20px;display:none}
|
||||
.queue.active{display:block}
|
||||
.queue-item{display:flex;align-items:center;gap:10px;padding:10px 14px;background:rgba(0,0,0,.25);border-radius:8px;margin-bottom:6px;font-size:.85rem}
|
||||
.queue-item.done{border-left:3px solid var(--ok)}
|
||||
.queue-item.err{border-left:3px solid var(--err)}
|
||||
.queue-item.pending{border-left:3px solid var(--mute)}
|
||||
.queue-item.scanning{border-left:3px solid var(--accent)}
|
||||
.queue-item .n{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:400px}
|
||||
.queue-item .s{color:var(--mute);font-size:.72rem;margin-left:8px}
|
||||
.queue-item .badge-q{padding:2px 8px;border-radius:10px;font-size:.7rem;font-weight:600}
|
||||
.queue-item.done .badge-q{background:rgba(34,197,94,.2);color:var(--ok)}
|
||||
.queue-item.err .badge-q{background:rgba(239,68,68,.2);color:var(--err)}
|
||||
.queue-item.scanning .badge-q{background:rgba(96,165,250,.2);color:var(--accent)}
|
||||
.queue-item.pending .badge-q{background:rgba(148,163,184,.2);color:var(--mute)}
|
||||
.progress-global{margin-top:12px;padding:10px 14px;background:rgba(96,165,250,.08);border:1px solid rgba(96,165,250,.2);border-radius:8px;font-size:.85rem;display:none}
|
||||
.progress-global.active{display:block}
|
||||
.progress-bar{height:6px;background:rgba(255,255,255,.1);border-radius:3px;margin-top:6px;overflow:hidden}
|
||||
.progress-bar-fill{height:100%;background:linear-gradient(90deg,var(--accent),var(--ok));transition:width .3s;width:0%}
|
||||
.shortcut-hint{margin-top:18px;padding:14px;background:rgba(34,197,94,.05);border:1px solid rgba(34,197,94,.2);border-radius:8px;font-size:.82rem;color:var(--mute);text-align:left}
|
||||
.shortcut-hint h4{margin:0 0 8px;color:var(--ok);font-size:.92rem}
|
||||
.shortcut-hint ol{margin:4px 0 0;padding-left:20px}
|
||||
.shortcut-hint ol li{margin-bottom:4px;line-height:1.4}
|
||||
.shortcut-hint code{background:rgba(0,0,0,.4);padding:2px 6px;border-radius:4px;color:var(--gold);font-family:"SF Mono",monospace;font-size:.8em;word-break:break-all}
|
||||
.copy-btn{background:rgba(251,191,36,.1);border:1px solid rgba(251,191,36,.3);color:var(--gold);padding:6px 10px;border-radius:6px;cursor:pointer;font-size:.75rem;margin-left:8px}
|
||||
.copy-btn:hover{background:rgba(251,191,36,.2)}
|
||||
.scan-list{max-height:680px;overflow-y:auto}
|
||||
.scan-item{padding:12px;border-radius:8px;margin-bottom:10px;background:rgba(0,0,0,.2);border:1px solid var(--br);cursor:pointer;transition:all .15s}
|
||||
.scan-item:hover{background:rgba(96,165,250,.08);border-color:var(--accent);transform:translateX(2px)}
|
||||
.scan-item .top{display:flex;justify-content:space-between;align-items:center;gap:8px}
|
||||
.scan-item .name{font-weight:600;font-size:.9rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:70%}
|
||||
.scan-item .time{color:var(--mute);font-size:.72rem}
|
||||
.scan-item .badges{display:flex;gap:6px;margin-top:8px;flex-wrap:wrap}
|
||||
.badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:.7rem;font-weight:600}
|
||||
.b-gh{background:rgba(34,197,94,.15);color:var(--ok);border:1px solid rgba(34,197,94,.3)}
|
||||
.b-pj{background:rgba(251,191,36,.15);color:var(--gold);border:1px solid rgba(251,191,36,.3)}
|
||||
.b-dk{background:rgba(96,165,250,.15);color:var(--accent);border:1px solid rgba(96,165,250,.3)}
|
||||
.empty{text-align:center;padding:40px 20px;color:var(--mute);font-size:.9rem}
|
||||
.detail{display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.85);backdrop-filter:blur(8px);padding:20px;overflow-y:auto}
|
||||
.detail.open{display:block}
|
||||
.detail .inner{max-width:1100px;margin:20px auto;background:var(--panel);border:1px solid var(--br);border-radius:16px;padding:24px;position:relative}
|
||||
.detail .close{position:absolute;top:16px;right:16px;background:none;border:1px solid var(--br);color:var(--fg);width:32px;height:32px;border-radius:8px;cursor:pointer;font-size:18px}
|
||||
.detail .close:hover{background:rgba(239,68,68,.2);border-color:var(--err)}
|
||||
.detail img{max-width:100%;max-height:400px;border-radius:8px;display:block;margin-bottom:16px}
|
||||
.detail .sec{background:rgba(0,0,0,.25);border-radius:8px;padding:14px;margin-top:14px}
|
||||
.detail .sec h4{margin:0 0 8px;font-size:.85rem;color:var(--mute);text-transform:uppercase;letter-spacing:.5px}
|
||||
.detail .sec .txt{font-family:"SF Mono",monospace;font-size:.82rem;color:#cbd5e1;white-space:pre-wrap;line-height:1.5;max-height:260px;overflow-y:auto}
|
||||
.oss-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:8px;margin-top:10px}
|
||||
.oss-card{background:rgba(34,197,94,.08);border:1px solid rgba(34,197,94,.25);padding:10px;border-radius:8px;text-align:center;font-size:.82rem;text-decoration:none;color:var(--ok);transition:all .15s}
|
||||
.oss-card:hover{background:rgba(34,197,94,.15);transform:translateY(-2px)}
|
||||
.oss-card b{display:block;font-weight:700;margin-bottom:2px}
|
||||
.oss-card .sub{font-size:.68rem;color:var(--mute);word-break:break-all}
|
||||
.modal-shortcut{display:none;position:fixed;inset:0;z-index:9999;background:rgba(0,0,0,.85);backdrop-filter:blur(8px);padding:20px;overflow-y:auto}
|
||||
.modal-shortcut.open{display:block}
|
||||
.modal-shortcut .inner{max-width:800px;margin:20px auto;background:var(--panel);border:1px solid var(--br);border-radius:16px;padding:28px;position:relative}
|
||||
.modal-shortcut .inner h2{margin:0 0 6px}
|
||||
.modal-shortcut .sub{color:var(--mute);margin-bottom:20px;font-size:.9rem}
|
||||
.modal-shortcut ol li{margin-bottom:10px;line-height:1.5}
|
||||
.toast{position:fixed;bottom:20px;right:20px;background:var(--panel);border:1px solid var(--br);border-left:3px solid var(--ok);padding:14px 18px;border-radius:8px;box-shadow:0 10px 40px rgba(0,0,0,.5);z-index:10000;max-width:360px;font-size:.88rem}
|
||||
.toast.err{border-left-color:var(--err)}
|
||||
@media(max-width:900px){.grid{grid-template-columns:1fr}.kpi-strip{grid-template-columns:repeat(2,1fr)}}
|
||||
:root {
|
||||
--bg: #0a0e1a;
|
||||
--surface: #131827;
|
||||
--surface-alt: #1a2136;
|
||||
--border: #252d45;
|
||||
--text: #e8ecf5;
|
||||
--mute: #7a869e;
|
||||
--accent: #5b9eff;
|
||||
--success: #4ade80;
|
||||
--warning: #fbbf24;
|
||||
--danger: #f87171;
|
||||
--urgent: #ef4444;
|
||||
--p0: #ef4444;
|
||||
--p1: #f59e0b;
|
||||
--p2: #3b82f6;
|
||||
--p3: #64748b;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; line-height: 1.5; min-height: 100vh; }
|
||||
.container { max-width: 1600px; margin: 0 auto; padding: 20px; }
|
||||
|
||||
/* Header */
|
||||
.hero { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; padding: 16px 20px; background: linear-gradient(135deg, #1a2136 0%, #131827 100%); border-radius: 12px; border: 1px solid var(--border); }
|
||||
.hero-ico { font-size: 48px; }
|
||||
.hero h1 { font-size: 22px; font-weight: 700; margin-bottom: 4px; }
|
||||
.hero .sub { color: var(--mute); font-size: 13px; }
|
||||
.hero-actions { margin-left: auto; display: flex; gap: 8px; }
|
||||
.btn { padding: 8px 14px; border-radius: 8px; border: 1px solid var(--border); background: var(--surface-alt); color: var(--text); cursor: pointer; font-size: 13px; font-weight: 500; transition: all 0.15s; text-decoration: none; display: inline-flex; align-items: center; gap: 6px; }
|
||||
.btn:hover { background: #242b42; border-color: var(--accent); }
|
||||
.btn-primary { background: var(--accent); border-color: var(--accent); color: white; }
|
||||
.btn-primary:hover { background: #4a8de8; }
|
||||
|
||||
/* KPIs */
|
||||
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-bottom: 20px; }
|
||||
.kpi { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; border-left: 3px solid var(--accent); }
|
||||
.kpi-lbl { color: var(--mute); font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
|
||||
.kpi-val { font-size: 26px; font-weight: 700; margin-top: 4px; }
|
||||
.kpi-hint { color: var(--mute); font-size: 11px; margin-top: 2px; }
|
||||
.kpi.urgent { border-left-color: var(--urgent); }
|
||||
.kpi.success { border-left-color: var(--success); }
|
||||
.kpi.warning { border-left-color: var(--warning); }
|
||||
|
||||
/* Tabs */
|
||||
.tabs { display: flex; gap: 2px; background: var(--surface); padding: 4px; border-radius: 10px; margin-bottom: 16px; border: 1px solid var(--border); overflow-x: auto; }
|
||||
.tab { padding: 10px 16px; border-radius: 7px; cursor: pointer; font-size: 13px; font-weight: 500; white-space: nowrap; transition: all 0.15s; color: var(--mute); }
|
||||
.tab:hover { background: var(--surface-alt); color: var(--text); }
|
||||
.tab.active { background: var(--accent); color: white; }
|
||||
.tab-badge { display: inline-block; background: rgba(255,255,255,0.2); color: inherit; padding: 1px 8px; border-radius: 10px; font-size: 11px; margin-left: 6px; font-weight: 600; }
|
||||
|
||||
/* Main grid */
|
||||
.main-grid { display: grid; grid-template-columns: 1fr 380px; gap: 16px; }
|
||||
@media (max-width: 1100px) { .main-grid { grid-template-columns: 1fr; } }
|
||||
|
||||
/* Panel */
|
||||
.panel { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
|
||||
.panel-header { padding: 14px 16px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; }
|
||||
.panel-header h3 { font-size: 15px; font-weight: 600; }
|
||||
.panel-header .count { color: var(--mute); font-size: 12px; margin-left: auto; }
|
||||
.panel-body { padding: 12px; max-height: 600px; overflow-y: auto; }
|
||||
|
||||
/* Upload dropzone */
|
||||
.drop { border: 2px dashed var(--border); border-radius: 10px; padding: 30px 20px; text-align: center; cursor: pointer; transition: all 0.15s; background: var(--surface-alt); }
|
||||
.drop:hover, .drop.dragover { border-color: var(--accent); background: rgba(91, 158, 255, 0.05); }
|
||||
.drop-ico { font-size: 40px; margin-bottom: 8px; }
|
||||
.drop-title { font-weight: 600; margin-bottom: 4px; }
|
||||
.drop-sub { color: var(--mute); font-size: 12px; }
|
||||
.drop input[type=file] { display: none; }
|
||||
|
||||
/* Item row */
|
||||
.item-row { padding: 10px 12px; border-radius: 8px; background: var(--surface-alt); margin-bottom: 6px; cursor: pointer; border-left: 3px solid var(--border); transition: all 0.1s; }
|
||||
.item-row:hover { background: #242b42; border-left-color: var(--accent); }
|
||||
.item-row.urgent { border-left-color: var(--urgent); }
|
||||
.item-row.medium { border-left-color: var(--warning); }
|
||||
.item-head { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
|
||||
.item-type { font-size: 11px; text-transform: uppercase; font-weight: 600; color: var(--mute); padding: 1px 6px; border-radius: 4px; background: rgba(91,158,255,0.1); }
|
||||
.item-type.photo { color: #a78bfa; background: rgba(167,139,250,0.1); }
|
||||
.item-type.message { color: #4ade80; background: rgba(74,222,128,0.1); }
|
||||
.item-type.contact { color: #5b9eff; background: rgba(91,158,255,0.1); }
|
||||
.item-type.calendar { color: #f59e0b; background: rgba(245,158,11,0.1); }
|
||||
.item-type.note { color: #ec4899; background: rgba(236,72,153,0.1); }
|
||||
.item-type.call { color: #06b6d4; background: rgba(6,182,212,0.1); }
|
||||
.item-type.health { color: #10b981; background: rgba(16,185,129,0.1); }
|
||||
.item-ts { color: var(--mute); font-size: 11px; margin-left: auto; }
|
||||
.item-preview { color: var(--text); font-size: 12px; opacity: 0.85; word-break: break-word; }
|
||||
.item-badges { display: flex; gap: 4px; margin-top: 6px; flex-wrap: wrap; }
|
||||
.badge { padding: 1px 6px; border-radius: 4px; font-size: 10px; font-weight: 600; background: rgba(255,255,255,0.05); color: var(--mute); }
|
||||
.badge.reco { background: rgba(91,158,255,0.15); color: var(--accent); }
|
||||
.badge.urgent { background: rgba(239,68,68,0.15); color: var(--urgent); }
|
||||
|
||||
/* Reco cards */
|
||||
.reco-card { padding: 10px 12px; background: var(--surface-alt); border-radius: 8px; margin-bottom: 6px; border-left: 3px solid var(--p3); cursor: pointer; }
|
||||
.reco-card.P0 { border-left-color: var(--p0); }
|
||||
.reco-card.P1 { border-left-color: var(--p1); }
|
||||
.reco-card.P2 { border-left-color: var(--p2); }
|
||||
.reco-head { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
|
||||
.reco-prio { padding: 1px 6px; border-radius: 4px; font-size: 10px; font-weight: 700; color: white; }
|
||||
.reco-prio.P0 { background: var(--p0); }
|
||||
.reco-prio.P1 { background: var(--p1); }
|
||||
.reco-prio.P2 { background: var(--p2); }
|
||||
.reco-prio.P3 { background: var(--p3); }
|
||||
.reco-kind { font-size: 11px; color: var(--mute); text-transform: uppercase; font-weight: 600; }
|
||||
.reco-label { font-size: 12px; font-weight: 500; margin-bottom: 2px; }
|
||||
.reco-action { color: var(--mute); font-size: 11px; }
|
||||
|
||||
/* Entities chips */
|
||||
.chips { display: flex; flex-wrap: wrap; gap: 4px; padding: 8px; }
|
||||
.chip { padding: 3px 8px; border-radius: 12px; font-size: 11px; background: var(--surface-alt); border: 1px solid var(--border); cursor: pointer; transition: all 0.1s; }
|
||||
.chip:hover { border-color: var(--accent); background: rgba(91,158,255,0.1); }
|
||||
.chip-count { color: var(--mute); font-weight: 600; margin-left: 3px; }
|
||||
|
||||
/* Drill-down modal */
|
||||
.drill { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.75); z-index: 1000; backdrop-filter: blur(4px); padding: 20px; overflow-y: auto; }
|
||||
.drill.open { display: flex; justify-content: center; align-items: flex-start; }
|
||||
.drill-content { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; max-width: 900px; width: 100%; max-height: 90vh; overflow-y: auto; position: relative; }
|
||||
.drill-header { padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; position: sticky; top: 0; background: var(--surface); z-index: 5; }
|
||||
.drill-title { font-size: 16px; font-weight: 700; }
|
||||
.drill-close { margin-left: auto; padding: 4px 10px; background: var(--surface-alt); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; color: var(--text); font-size: 13px; }
|
||||
.drill-body { padding: 20px; }
|
||||
.drill-section { margin-bottom: 20px; }
|
||||
.drill-section h4 { font-size: 13px; text-transform: uppercase; color: var(--mute); margin-bottom: 8px; letter-spacing: 0.5px; }
|
||||
.drill-text { background: var(--surface-alt); padding: 12px; border-radius: 8px; white-space: pre-wrap; font-family: 'SF Mono', Consolas, monospace; font-size: 12px; color: #cbd5e1; max-height: 300px; overflow-y: auto; word-break: break-word; }
|
||||
.drill-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 8px; }
|
||||
.drill-kv { background: var(--surface-alt); padding: 8px 10px; border-radius: 6px; }
|
||||
.drill-kv-k { font-size: 10px; text-transform: uppercase; color: var(--mute); font-weight: 600; letter-spacing: 0.3px; }
|
||||
.drill-kv-v { font-size: 13px; font-weight: 500; margin-top: 2px; }
|
||||
|
||||
/* Empty */
|
||||
.empty { color: var(--mute); font-size: 13px; text-align: center; padding: 40px 20px; }
|
||||
|
||||
/* Toast */
|
||||
.toast { position: fixed; bottom: 20px; right: 20px; padding: 12px 18px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; z-index: 9999; display: none; }
|
||||
.toast.show { display: block; animation: slideIn 0.2s; }
|
||||
@keyframes slideIn { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
||||
|
||||
/* Progress */
|
||||
.progress { padding: 12px; background: var(--surface-alt); border-radius: 8px; margin-top: 12px; display: none; }
|
||||
.progress.show { display: block; }
|
||||
.progress-bar { height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; margin-top: 8px; }
|
||||
.progress-fill { height: 100%; background: var(--accent); transition: width 0.3s; }
|
||||
|
||||
/* MCP section */
|
||||
.mcp-card { padding: 10px 12px; background: var(--surface-alt); border-radius: 8px; margin-bottom: 6px; border-left: 3px solid var(--success); }
|
||||
.mcp-card.missing { border-left-color: var(--warning); }
|
||||
.mcp-card.planned { border-left-color: var(--p3); }
|
||||
.mcp-name { font-weight: 600; font-size: 13px; margin-bottom: 2px; }
|
||||
.mcp-desc { font-size: 11px; color: var(--mute); }
|
||||
.mcp-status { float: right; font-size: 10px; padding: 2px 6px; border-radius: 4px; font-weight: 600; }
|
||||
.mcp-status.ok { background: rgba(74,222,128,0.15); color: var(--success); }
|
||||
.mcp-status.missing { background: rgba(251,191,36,0.15); color: var(--warning); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="hdr">
|
||||
<div class="logo">🍎</div>
|
||||
<div>
|
||||
<h1>WEVIA Apple — Photos Scanner</h1>
|
||||
<div class="sub">OCR + Gemini 2.5 Flash Vision · HEIC natif · iPhone Shortcuts compatible · Multi-upload + dossiers</div>
|
||||
<div class="container">
|
||||
|
||||
<div class="hero">
|
||||
<div class="hero-ico">🍎</div>
|
||||
<div>
|
||||
<h1>WEVIA Apple <span style="color:var(--mute);font-weight:400">v3</span> — iPhone Intelligence Hub</h1>
|
||||
<div class="sub">Full ingestion: Photos · Messages · Contacts · Calendar · Notes · Calls · Health → Entity extraction + AI recommendations</div>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<a class="btn" href="wevia-apple.html" title="Legacy v2">v2</a>
|
||||
<button class="btn" onclick="showMCPs()">🔌 MCPs</button>
|
||||
<button class="btn" onclick="showShortcuts()">📲 iPhone Setup</button>
|
||||
<button class="btn btn-primary" onclick="loadAll()">⟳ Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kpis" id="kpis"></div>
|
||||
|
||||
<div class="tabs" id="tabs">
|
||||
<div class="tab active" data-tab="all">📊 Overview</div>
|
||||
<div class="tab" data-tab="photo">📸 Photos <span class="tab-badge" id="c-photo">0</span></div>
|
||||
<div class="tab" data-tab="message">💬 Messages <span class="tab-badge" id="c-message">0</span></div>
|
||||
<div class="tab" data-tab="contact">👤 Contacts <span class="tab-badge" id="c-contact">0</span></div>
|
||||
<div class="tab" data-tab="calendar">📅 Calendar <span class="tab-badge" id="c-calendar">0</span></div>
|
||||
<div class="tab" data-tab="note">📝 Notes <span class="tab-badge" id="c-note">0</span></div>
|
||||
<div class="tab" data-tab="call">📞 Calls <span class="tab-badge" id="c-call">0</span></div>
|
||||
<div class="tab" data-tab="health">❤️ Health <span class="tab-badge" id="c-health">0</span></div>
|
||||
<div class="tab" data-tab="reco">💡 Recommandations <span class="tab-badge" id="c-reco">0</span></div>
|
||||
<div class="tab" data-tab="entities">🔎 Entités</div>
|
||||
<div class="tab" data-tab="alerts">🚨 Alertes <span class="tab-badge" id="c-alerts">0</span></div>
|
||||
</div>
|
||||
|
||||
<div class="main-grid">
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<h3 id="list-title">📥 Flux d'ingestion</h3>
|
||||
<span class="count" id="list-count">0 items</span>
|
||||
</div>
|
||||
<div class="panel-body" id="list-body">
|
||||
<div class="empty">Charge le contenu de ton iPhone avec le Shortcut ou uploade des photos ci-contre →</div>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<a class="btn" href="/weval-technology-platform.html">← WTP</a>
|
||||
<a class="btn" href="/wevia-master.html">WEVIA Master</a>
|
||||
<a class="btn shortcut" href="javascript:openShortcut()">📲 iPhone Setup</a>
|
||||
</div>
|
||||
|
||||
<div class="kpi-strip">
|
||||
<div class="kpi k1"><div class="lbl">Photos scannées</div><div class="val" id="k-total">0</div><div class="hint" id="k-last">—</div></div>
|
||||
<div class="kpi k2"><div class="lbl">Projets OSS identifiés</div><div class="val" id="k-oss">0</div><div class="hint">Word-boundary strict</div></div>
|
||||
<div class="kpi k3"><div class="lbl">GitHub URLs</div><div class="val" id="k-gh">0</div><div class="hint">Repos mentionnés</div></div>
|
||||
<div class="kpi k4"><div class="lbl">Top OSS</div><div class="val" id="k-top" style="font-size:1rem;line-height:1.3">—</div><div class="hint">Projet le plus cité</div></div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="panel">
|
||||
<h3>📱 Upload — photos, dossiers, batch</h3>
|
||||
<div class="uploader" id="drop">
|
||||
<div class="ico">🍎</div>
|
||||
<p class="big">Glisse des photos/dossiers OU clique pour choisir</p>
|
||||
<p>JPG · PNG · <b style="color:var(--ok)">HEIC natif iPhone</b> · WEBP · GIF · Multi-sélection OK</p>
|
||||
<input type="file" class="hidden-input" id="file-multi" accept="image/*,.heic,.heif" multiple>
|
||||
<input type="file" class="hidden-input" id="file-dir" webkitdirectory directory multiple>
|
||||
<div class="upload-btns">
|
||||
<button class="btn-primary" onclick="document.getElementById('file-multi').click()">📷 Photos (multi)</button>
|
||||
<button class="btn-secondary" onclick="document.getElementById('file-dir').click()">📁 Dossier entier</button>
|
||||
<button class="btn-secondary" onclick="openShortcut()">📲 iPhone Shortcut</button>
|
||||
<div>
|
||||
<div class="panel" style="margin-bottom:12px">
|
||||
<div class="panel-header"><h3>📱 Upload rapide</h3></div>
|
||||
<div class="panel-body">
|
||||
<div class="drop" id="drop" onclick="document.getElementById('f').click()">
|
||||
<div class="drop-ico">🍎</div>
|
||||
<div class="drop-title">Glisse photos / HEIC / captures</div>
|
||||
<div class="drop-sub">JPG · PNG · HEIC · WEBP · PDF · multi</div>
|
||||
<input type="file" id="f" accept="image/*,.heic,.heif,.pdf" multiple>
|
||||
</div>
|
||||
<div class="progress" id="prog">
|
||||
<div id="prog-txt">0 / 0</div>
|
||||
<div class="progress-bar"><div class="progress-fill" id="prog-fill" style="width:0%"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-global" id="prog">
|
||||
<div><b id="prog-txt">Queue vide</b> · <span id="prog-rate" style="color:var(--mute)">—</span></div>
|
||||
<div class="progress-bar"><div class="progress-bar-fill" id="prog-fill"></div></div>
|
||||
</div>
|
||||
<div class="queue" id="queue"></div>
|
||||
|
||||
<div class="shortcut-hint">
|
||||
<h4>🍎 Connecter ton iPhone (doctrine honnête)</h4>
|
||||
Apple bloque l'accès direct à iCloud Photos depuis le web. <b>3 solutions qui marchent</b> :
|
||||
<ol>
|
||||
<li><b>iPhone Shortcut</b> (recommandé, 2 min setup): clique <b>📲 iPhone Setup</b> ci-dessus pour les étapes détaillées. Une fois installé, sélectionne N photos dans l'app Photos → Partager → <code>Scan WEVIA</code> → tout est scanné automatiquement.</li>
|
||||
<li><b>Dossier iCloud Drive</b>: dépose tes photos dans un dossier iCloud Drive partagé avec ton Mac. Sur Mac, glisse le dossier ici (bouton 📁 Dossier entier). Le navigateur upload chaque image séquentiellement.</li>
|
||||
<li><b>Automation iCloud</b>: crée un album iCloud "WEVIA Scan" → Automation iPhone "Quand photo ajoutée à album 'WEVIA Scan' → exécuter raccourci Scan WEVIA". Toutes les nouvelles photos sont scannées automatiquement.</li>
|
||||
</ol>
|
||||
Endpoint: <code>POST /api/wevia-apple-scan.php?action=upload</code> <button class="copy-btn" onclick="copyEndpoint()">📋 Copier</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>📚 Historique scans <span style="font-weight:400;color:var(--mute);font-size:.85rem">· clique pour drill-down</span></h3>
|
||||
<div class="scan-list" id="list"><div class="empty">Aucun scan pour le moment.</div></div>
|
||||
<div class="panel-header"><h3>🎯 Top Recommandations</h3></div>
|
||||
<div class="panel-body" id="top-reco">
|
||||
<div class="empty">Pas encore de recommandations.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail" id="detail"><div class="inner"><button class="close" onclick="closeDetail()">×</button><div id="detail-content"></div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="modal-shortcut" id="modal-shortcut"><div class="inner">
|
||||
<button class="close" onclick="closeShortcut()">×</button>
|
||||
<h2>📲 Setup iPhone Shortcut — Scan WEVIA</h2>
|
||||
<p class="sub">Connecte ton iPhone à WEVIA en 2 minutes. Une fois installé, toute photo iPhone peut être scannée en 1 tap.</p>
|
||||
<ol id="shortcut-steps"><li>Chargement…</li></ol>
|
||||
<div style="margin-top:16px;padding:14px;background:rgba(251,191,36,.08);border:1px solid rgba(251,191,36,.25);border-radius:8px;font-size:.85rem">
|
||||
<b style="color:var(--gold)">💡 Multi-photos batch</b> : sélectionne plusieurs photos dans Photos iPhone, puis Partager → <code>Scan WEVIA</code>. Le raccourci les envoie toutes automatiquement.
|
||||
<!-- Drill-down modal -->
|
||||
<div class="drill" id="drill">
|
||||
<div class="drill-content">
|
||||
<div class="drill-header">
|
||||
<div class="drill-title" id="drill-title">—</div>
|
||||
<button class="drill-close" onclick="closeDrill()">✕ Fermer</button>
|
||||
</div>
|
||||
<div class="drill-body" id="drill-body"></div>
|
||||
</div>
|
||||
<div style="margin-top:12px;padding:14px;background:rgba(96,165,250,.08);border:1px solid rgba(96,165,250,.2);border-radius:8px;font-size:.85rem">
|
||||
<b style="color:var(--accent)">🤖 Auto-scan album iCloud</b>: app Raccourcis iPhone → onglet Automation → Nouvelle → "Quand photo ajoutée à l'album 'WEVIA'" → choisir <code>Scan WEVIA</code>. Tout ajout = scan auto.
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API = '/api/wevia-apple-scan.php';
|
||||
let SCANS = []; let QUEUE = []; let PROCESSING = false;
|
||||
<!-- Shortcuts modal -->
|
||||
<div class="drill" id="shortcuts">
|
||||
<div class="drill-content">
|
||||
<div class="drill-header">
|
||||
<div class="drill-title">📲 iPhone Setup — Doctrine honnête</div>
|
||||
<button class="drill-close" onclick="closeSC()">✕ Fermer</button>
|
||||
</div>
|
||||
<div class="drill-body">
|
||||
<p style="margin-bottom:16px;color:var(--mute)">Apple bloque l'accès direct à iCloud depuis le web. Voici les 3 méthodes qui marchent :</p>
|
||||
|
||||
async function loadAll(){
|
||||
try {
|
||||
const [stats, list] = await Promise.all([
|
||||
fetch(API+'?action=stats&_='+Date.now()).then(r=>r.json()),
|
||||
fetch(API+'?action=list&_='+Date.now()).then(r=>r.json())
|
||||
]);
|
||||
document.getElementById('k-total').textContent = stats.scans_total || 0;
|
||||
document.getElementById('k-oss').textContent = stats.oss_total || 0;
|
||||
document.getElementById('k-gh').textContent = stats.github_urls_total || 0;
|
||||
const top = stats.top_projects ? Object.entries(stats.top_projects)[0] : null;
|
||||
document.getElementById('k-top').textContent = top ? (top[0]+' ('+top[1]+')') : '—';
|
||||
SCANS = list.scans || [];
|
||||
renderList();
|
||||
document.getElementById('k-last').textContent = SCANS[0] ? ('Dernier : '+new Date(SCANS[0].scanned_at).toLocaleString('fr')) : 'Aucun scan';
|
||||
} catch(e){ console.error(e); }
|
||||
}
|
||||
|
||||
function renderList(){
|
||||
const el = document.getElementById('list');
|
||||
if (!SCANS.length){ el.innerHTML = '<div class="empty">Aucun scan pour le moment.</div>'; return; }
|
||||
el.innerHTML = SCANS.slice(0,50).map(s => `
|
||||
<div class="scan-item" onclick="openDetail('${s.id}')">
|
||||
<div class="top">
|
||||
<div class="name">📸 ${escape(s.filename)}</div>
|
||||
<div class="time">${new Date(s.scanned_at).toLocaleString('fr')}</div>
|
||||
<div class="drill-section">
|
||||
<h4>1. iPhone Shortcuts (recommandé - 5 min setup)</h4>
|
||||
<p style="font-size:12px;margin-bottom:8px">Crée 1 shortcut par type de données dans l'app <b>Raccourcis</b> iPhone :</p>
|
||||
<div style="display:grid;gap:6px">
|
||||
<div class="drill-kv"><div class="drill-kv-k">Photos → OCR + entités</div><div class="drill-kv-v">POST multipart <code style="font-size:11px;background:rgba(255,255,255,0.05);padding:2px 4px;border-radius:3px">/api/wevia-apple-ingest.php?action=ingest_photo</code></div></div>
|
||||
<div class="drill-kv"><div class="drill-kv-k">Messages → SMS/iMessage</div><div class="drill-kv-v">POST json <code style="font-size:11px;background:rgba(255,255,255,0.05);padding:2px 4px;border-radius:3px">action=ingest_structured, type=message</code></div></div>
|
||||
<div class="drill-kv"><div class="drill-kv-k">Contacts → CRM enrich</div><div class="drill-kv-v">POST json <code style="font-size:11px;background:rgba(255,255,255,0.05);padding:2px 4px;border-radius:3px">type=contact</code></div></div>
|
||||
<div class="drill-kv"><div class="drill-kv-k">Calendar → tasks auto</div><div class="drill-kv-v">POST json <code style="font-size:11px;background:rgba(255,255,255,0.05);padding:2px 4px;border-radius:3px">type=calendar</code></div></div>
|
||||
<div class="drill-kv"><div class="drill-kv-k">Notes</div><div class="drill-kv-v">POST json <code style="font-size:11px;background:rgba(255,255,255,0.05);padding:2px 4px;border-radius:3px">type=note</code></div></div>
|
||||
<div class="drill-kv"><div class="drill-kv-k">Call log</div><div class="drill-kv-v">POST json <code style="font-size:11px;background:rgba(255,255,255,0.05);padding:2px 4px;border-radius:3px">type=call</code></div></div>
|
||||
<div class="drill-kv"><div class="drill-kv-k">Health (HealthKit)</div><div class="drill-kv-v">POST json <code style="font-size:11px;background:rgba(255,255,255,0.05);padding:2px 4px;border-radius:3px">type=health</code></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="badges">
|
||||
${s.counts.project_names ? `<span class="badge b-pj">📦 ${s.counts.project_names} OSS</span>`:''}
|
||||
${s.counts.github_urls ? `<span class="badge b-gh">🐙 ${s.counts.github_urls} GitHub</span>`:''}
|
||||
${s.counts.docker_images ? `<span class="badge b-dk">🐳 ${s.counts.docker_images} Docker</span>`:''}
|
||||
<span class="badge" style="background:rgba(148,163,184,.15);color:var(--mute)">${s.scan_ms}ms</span>
|
||||
|
||||
<div class="drill-section">
|
||||
<h4>2. Automation iCloud (zéro action manuelle)</h4>
|
||||
<p style="font-size:12px">Crée album iCloud "WEVIA Scan" + Automation iPhone "Quand photo ajoutée à album → exécuter Shortcut Scan WEVIA". Toutes les nouvelles photos scannées automatiquement.</p>
|
||||
</div>
|
||||
|
||||
<div class="drill-section">
|
||||
<h4>3. Blade MCP (full access via Razer)</h4>
|
||||
<p style="font-size:12px">Installe l'agent Blade sur Razer → connecte iPhone en USB → MCP <code>apple_scrape_*</code> lit chat.db, Photos.app, Contacts.app via AppleScript/Finder sans sync iCloud requise.</p>
|
||||
</div>
|
||||
|
||||
<div class="drill-section">
|
||||
<h4>Endpoint</h4>
|
||||
<div class="drill-text">https://weval-consulting.com/api/wevia-apple-ingest.php</div>
|
||||
<button class="btn" onclick="navigator.clipboard.writeText('https://weval-consulting.com/api/wevia-apple-ingest.php');showToast('Endpoint copié ✓')" style="margin-top:8px">📋 Copier endpoint</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
async function openDetail(id){
|
||||
const scan = SCANS.find(s => s.id === id); if (!scan) return;
|
||||
const oss = scan.oss_extracted || {};
|
||||
document.getElementById('detail-content').innerHTML = `
|
||||
<h2 style="margin:0 0 12px">📸 ${escape(scan.filename)}</h2>
|
||||
<div style="color:var(--mute);font-size:.85rem;margin-bottom:14px">${new Date(scan.scanned_at).toLocaleString('fr')} · ${scan.scan_ms}ms · ${Math.round(scan.size_bytes/1024)}KB</div>
|
||||
<img src="${scan.image_url}" alt="scan">
|
||||
${scan.caption ? `<div class="sec"><h4>Caption</h4><div class="txt">${escape(scan.caption)}</div></div>`:''}
|
||||
${oss.project_names && oss.project_names.length ? `<div class="sec"><h4>📦 Projets OSS identifiés (${oss.project_names.length})</h4><div class="oss-grid">${oss.project_names.map(p => `<a class="oss-card" href="https://github.com/search?q=${encodeURIComponent(p)}" target="_blank"><b>${escape(p)}</b><span class="sub">github search</span></a>`).join('')}</div></div>`:''}
|
||||
${oss.github_urls && oss.github_urls.length ? `<div class="sec"><h4>🐙 GitHub URLs (${oss.github_urls.length})</h4><div class="oss-grid">${oss.github_urls.map(u => `<a class="oss-card" href="${u.url}" target="_blank"><b>${escape(u.repo)}</b><span class="sub">${escape(u.owner)}</span></a>`).join('')}</div></div>`:''}
|
||||
${oss.docker_images && oss.docker_images.length ? `<div class="sec"><h4>🐳 Docker images (${oss.docker_images.length})</h4><div class="txt">${oss.docker_images.map(escape).join('\n')}</div></div>`:''}
|
||||
<div class="sec"><h4>🔍 Vision LLM (Gemini 2.5 Flash)</h4><div class="txt">${escape(scan.vision_text || '(pas de réponse vision)')}</div></div>
|
||||
<div class="sec"><h4>📝 OCR brut (tesseract)</h4><div class="txt">${escape(scan.ocr_text || '(OCR vide)')}</div></div>
|
||||
`;
|
||||
document.getElementById('detail').classList.add('open');
|
||||
}
|
||||
function closeDetail(){ document.getElementById('detail').classList.remove('open'); }
|
||||
<!-- MCPs modal -->
|
||||
<div class="drill" id="mcps">
|
||||
<div class="drill-content">
|
||||
<div class="drill-header">
|
||||
<div class="drill-title">🔌 MCPs — actifs & manquants</div>
|
||||
<button class="drill-close" onclick="closeMCP()">✕ Fermer</button>
|
||||
</div>
|
||||
<div class="drill-body" id="mcps-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
async function openShortcut(){
|
||||
document.getElementById('modal-shortcut').classList.add('open');
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
const API = '/api/wevia-apple-ingest.php';
|
||||
let STATE = { currentTab: 'all', items: [], reco: [], entities: {}, tasks: [], alerts: [], status: {} };
|
||||
|
||||
async function loadAll() {
|
||||
try {
|
||||
const d = await fetch(API+'?action=shortcut').then(r=>r.json());
|
||||
const ol = document.getElementById('shortcut-steps');
|
||||
ol.innerHTML = d.setup_steps.map(s => `<li>${escape(s).replace(/https:\/\/[^\s]+/g, m => `<code>${m}</code>`).replace(/'([^']+)'/g, (_,x)=>`<code>${x}</code>`)}</li>`).join('');
|
||||
} catch(e){}
|
||||
}
|
||||
function closeShortcut(){ document.getElementById('modal-shortcut').classList.remove('open'); }
|
||||
function copyEndpoint(){ navigator.clipboard.writeText('https://weval-consulting.com/api/wevia-apple-scan.php?action=upload'); showToast('Endpoint copié ✓'); }
|
||||
function escape(s){ return String(s||'').replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
||||
document.addEventListener('keydown', e => { if (e.key === 'Escape'){ closeDetail(); closeShortcut(); } });
|
||||
|
||||
const drop = document.getElementById('drop');
|
||||
drop.addEventListener('dragover', e => { e.preventDefault(); drop.classList.add('drag'); });
|
||||
drop.addEventListener('dragleave', () => drop.classList.remove('drag'));
|
||||
drop.addEventListener('drop', e => {
|
||||
e.preventDefault(); drop.classList.remove('drag');
|
||||
const files = Array.from(e.dataTransfer.files || []).filter(f => f.type.startsWith('image/') || /\.(heic|heif)$/i.test(f.name));
|
||||
if (files.length) enqueue(files);
|
||||
});
|
||||
document.getElementById('file-multi').addEventListener('change', e => { if (e.target.files.length) enqueue(Array.from(e.target.files)); e.target.value=''; });
|
||||
document.getElementById('file-dir').addEventListener('change', e => {
|
||||
const files = Array.from(e.target.files).filter(f => f.type.startsWith('image/') || /\.(heic|heif|jpg|jpeg|png|webp|gif)$/i.test(f.name));
|
||||
if (files.length) enqueue(files);
|
||||
e.target.value='';
|
||||
});
|
||||
|
||||
function enqueue(files){
|
||||
files.forEach((f,i) => QUEUE.push({id:'q'+Date.now()+i, file:f, status:'pending'}));
|
||||
renderQueue();
|
||||
if (!PROCESSING) processQueue();
|
||||
}
|
||||
|
||||
function renderQueue(){
|
||||
const el = document.getElementById('queue');
|
||||
const pending = QUEUE.filter(q=>q.status==='pending').length;
|
||||
const scanning = QUEUE.filter(q=>q.status==='scanning').length;
|
||||
const done = QUEUE.filter(q=>q.status==='done').length;
|
||||
const err = QUEUE.filter(q=>q.status==='err').length;
|
||||
const total = QUEUE.length;
|
||||
if (total === 0){ el.classList.remove('active'); document.getElementById('prog').classList.remove('active'); return; }
|
||||
el.classList.add('active');
|
||||
document.getElementById('prog').classList.add('active');
|
||||
document.getElementById('prog-txt').textContent = `${done+err}/${total} traités · ${pending} en attente · ${scanning} en cours`;
|
||||
const rate = done>0 ? `≈${Math.round((done/(total))*100)}% done, ${err} err` : '—';
|
||||
document.getElementById('prog-rate').textContent = rate;
|
||||
document.getElementById('prog-fill').style.width = `${((done+err)/total)*100}%`;
|
||||
el.innerHTML = QUEUE.slice(-12).reverse().map(q => {
|
||||
const badge = {'pending':'⏳ Attente','scanning':'🔄 Scan…','done':'✅ OK','err':'❌'}[q.status];
|
||||
const hint = q.status==='done' ? `${q.result?.counts?.project_names||0} OSS · ${q.result?.scan_ms||0}ms` : (q.status==='err' ? q.err : '');
|
||||
return `<div class="queue-item ${q.status}"><div class="n">${escape(q.file.name)} <span class="s">${Math.round(q.file.size/1024)}KB</span></div><span class="badge-q">${badge}</span><span class="s">${escape(hint)}</span></div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function processQueue(){
|
||||
PROCESSING = true;
|
||||
const CONCURRENT = 2;
|
||||
while (true){
|
||||
const next = QUEUE.filter(q => q.status==='pending');
|
||||
if (!next.length) break;
|
||||
const batch = next.slice(0, CONCURRENT);
|
||||
batch.forEach(q => q.status='scanning');
|
||||
renderQueue();
|
||||
await Promise.all(batch.map(q => uploadOne(q)));
|
||||
renderQueue();
|
||||
const [status, list, reco, ents, alerts] = await Promise.all([
|
||||
fetch(API + '?action=status&_=' + Date.now()).then(r => r.json()),
|
||||
fetch(API + '?action=list&limit=200&_=' + Date.now()).then(r => r.json()),
|
||||
fetch(API + '?action=recommendations&_=' + Date.now()).then(r => r.json()),
|
||||
fetch(API + '?action=entities&_=' + Date.now()).then(r => r.json()),
|
||||
fetch(API + '?action=alerts&_=' + Date.now()).then(r => r.json())
|
||||
]);
|
||||
STATE.status = status;
|
||||
STATE.items = list.items || [];
|
||||
STATE.reco = reco.recommendations || [];
|
||||
STATE.entities = ents.entities || {};
|
||||
STATE.alerts = alerts.alerts || [];
|
||||
renderKPIs();
|
||||
renderTabs();
|
||||
renderContent();
|
||||
renderTopReco();
|
||||
} catch (e) {
|
||||
showToast('Erreur chargement: ' + e.message, true);
|
||||
}
|
||||
PROCESSING = false;
|
||||
await loadAll();
|
||||
const done = QUEUE.filter(q=>q.status==='done').length;
|
||||
const err = QUEUE.filter(q=>q.status==='err').length;
|
||||
showToast(`✅ Batch fini · ${done} scannées / ${err} erreurs`);
|
||||
}
|
||||
|
||||
async function uploadOne(q){
|
||||
const fd = new FormData();
|
||||
fd.append('image', q.file);
|
||||
fd.append('caption', 'batch upload web '+new Date().toISOString());
|
||||
function renderKPIs() {
|
||||
const s = STATE.status;
|
||||
const byType = s.by_type || {};
|
||||
const entsCount = s.entities_count || {};
|
||||
const kpis = [
|
||||
{ lbl: 'Items ingérés', val: s.total_items || 0, hint: 'Photos + messages + contacts + …', cls: '' },
|
||||
{ lbl: 'Recommandations', val: STATE.reco.length, hint: 'Actions proposées IA', cls: 'success' },
|
||||
{ lbl: 'Alertes P0', val: (STATE.alerts || []).length, hint: 'Urgences détectées', cls: 'urgent' },
|
||||
{ lbl: 'Tasks pending', val: s.tasks_pending || 0, hint: 'Échéances à traiter', cls: 'warning' },
|
||||
{ lbl: 'Contacts', val: (entsCount.people || 0) + (entsCount.orgs || 0), hint: entsCount.emails + ' emails · ' + entsCount.phones + ' phones', cls: '' },
|
||||
{ lbl: 'OSS mentionnés', val: entsCount.oss || 0, hint: 'Tech stacks détectés', cls: '' }
|
||||
];
|
||||
document.getElementById('kpis').innerHTML = kpis.map(k => `
|
||||
<div class="kpi ${k.cls}">
|
||||
<div class="kpi-lbl">${k.lbl}</div>
|
||||
<div class="kpi-val">${k.val}</div>
|
||||
<div class="kpi-hint">${k.hint}</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
function renderTabs() {
|
||||
const bt = STATE.status.by_type || {};
|
||||
['photo','message','contact','calendar','note','call','health'].forEach(t => {
|
||||
const el = document.getElementById('c-' + t);
|
||||
if (el) el.textContent = bt[t] || 0;
|
||||
});
|
||||
document.getElementById('c-reco').textContent = STATE.reco.length;
|
||||
document.getElementById('c-alerts').textContent = (STATE.alerts || []).length;
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
const tab = STATE.currentTab;
|
||||
const body = document.getElementById('list-body');
|
||||
const title = document.getElementById('list-title');
|
||||
const countEl = document.getElementById('list-count');
|
||||
|
||||
if (tab === 'reco') {
|
||||
title.textContent = '💡 Recommandations (triées P0 → P3)';
|
||||
countEl.textContent = STATE.reco.length + ' reco';
|
||||
if (!STATE.reco.length) { body.innerHTML = '<div class="empty">Pas de recommandations — ingère du contenu.</div>'; return; }
|
||||
body.innerHTML = STATE.reco.map(r => renderReco(r)).join('');
|
||||
return;
|
||||
}
|
||||
if (tab === 'entities') {
|
||||
title.textContent = '🔎 Entités extraites';
|
||||
countEl.textContent = Object.values(STATE.entities).reduce((n, list) => n + (list?.length || 0), 0) + ' total';
|
||||
body.innerHTML = Object.entries(STATE.entities).filter(([k, v]) => v && v.length).map(([cat, list]) => `
|
||||
<div class="drill-section">
|
||||
<h4>${catIcon(cat)} ${cat} <span style="color:var(--mute);font-size:11px">(${list.length})</span></h4>
|
||||
<div class="chips">${list.slice(0, 50).map(e => `<div class="chip" onclick="filterByEntity('${cat}','${escapeAttr(e.value)}')">${escape(e.value)}<span class="chip-count">×${e.count}</span></div>`).join('')}</div>
|
||||
</div>`).join('') || '<div class="empty">Aucune entité détectée.</div>';
|
||||
return;
|
||||
}
|
||||
if (tab === 'alerts') {
|
||||
title.textContent = '🚨 Alertes P0 — traitement immédiat';
|
||||
countEl.textContent = (STATE.alerts || []).length + ' alertes';
|
||||
if (!STATE.alerts.length) { body.innerHTML = '<div class="empty">Aucune alerte urgente 👍</div>'; return; }
|
||||
body.innerHTML = STATE.alerts.map(r => renderReco(r)).join('');
|
||||
return;
|
||||
}
|
||||
|
||||
const items = tab === 'all' ? STATE.items : STATE.items.filter(i => i.type === tab);
|
||||
title.textContent = tab === 'all' ? '📥 Flux d\'ingestion' : tabLabel(tab);
|
||||
countEl.textContent = items.length + ' items';
|
||||
if (!items.length) { body.innerHTML = '<div class="empty">Aucun item de ce type.</div>'; return; }
|
||||
body.innerHTML = items.map(i => renderItemRow(i)).join('');
|
||||
}
|
||||
|
||||
function renderItemRow(i) {
|
||||
const u = i.urgency || 'low';
|
||||
const cls = u === 'high' ? 'urgent' : (u === 'medium' ? 'medium' : '');
|
||||
const ec = i.entities_count || {};
|
||||
const badges = [];
|
||||
if (ec.people) badges.push(`<span class="badge">👤 ${ec.people}</span>`);
|
||||
if (ec.emails) badges.push(`<span class="badge">✉️ ${ec.emails}</span>`);
|
||||
if (ec.phones) badges.push(`<span class="badge">📱 ${ec.phones}</span>`);
|
||||
if (ec.money) badges.push(`<span class="badge">💰 ${ec.money}</span>`);
|
||||
if (ec.deadlines) badges.push(`<span class="badge urgent">⏰ ${ec.deadlines}</span>`);
|
||||
if (ec.oss) badges.push(`<span class="badge">🔧 ${ec.oss}</span>`);
|
||||
if (i.reco_count) badges.push(`<span class="badge reco">💡 ${i.reco_count} reco</span>`);
|
||||
return `
|
||||
<div class="item-row ${cls}" onclick="openDrill('${i.id}')">
|
||||
<div class="item-head">
|
||||
<span class="item-type ${i.type}">${i.type}</span>
|
||||
<span class="item-ts">${new Date(i.ingested_at).toLocaleString('fr-FR')}</span>
|
||||
</div>
|
||||
<div class="item-preview">${escape((i.preview || '').substring(0, 180)) || '<em style="color:var(--mute)">(pas d\'aperçu)</em>'}</div>
|
||||
${badges.length ? `<div class="item-badges">${badges.join('')}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderReco(r) {
|
||||
return `
|
||||
<div class="reco-card ${r.priority}" onclick="${r.source_id ? `openDrill('${r.source_id}')` : ''}">
|
||||
<div class="reco-head">
|
||||
<span class="reco-prio ${r.priority}">${r.priority}</span>
|
||||
<span class="reco-kind">${r.kind}</span>
|
||||
</div>
|
||||
<div class="reco-label">${escape(r.label)}</div>
|
||||
<div class="reco-action">→ ${escape(r.action)}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderTopReco() {
|
||||
const top = STATE.reco.slice(0, 5);
|
||||
const el = document.getElementById('top-reco');
|
||||
if (!top.length) { el.innerHTML = '<div class="empty">Pas encore.</div>'; return; }
|
||||
el.innerHTML = top.map(r => renderReco(r)).join('');
|
||||
}
|
||||
|
||||
async function openDrill(id) {
|
||||
try {
|
||||
const r = await fetch(API+'?action=upload', { method:'POST', body:fd });
|
||||
const d = await r.json();
|
||||
if (d.ok){ q.status='done'; q.result=d.scan; }
|
||||
else { q.status='err'; q.err=d.error||'fail'; }
|
||||
} catch(e){ q.status='err'; q.err=e.message; }
|
||||
renderQueue();
|
||||
const d = await fetch(API + '?action=drill&id=' + encodeURIComponent(id)).then(r => r.json());
|
||||
if (!d.ok) { showToast('Item introuvable', true); return; }
|
||||
const it = d.item;
|
||||
document.getElementById('drill-title').innerHTML = `<span class="item-type ${it.type}">${it.type}</span> ${escape(it.filename || it.id)}`;
|
||||
const body = document.getElementById('drill-body');
|
||||
|
||||
let html = '';
|
||||
|
||||
// Photo preview
|
||||
if (it.type === 'photo' && it.url) {
|
||||
html += `<div class="drill-section"><h4>📸 Image originale</h4><img src="${it.url}" style="max-width:100%;border-radius:8px;border:1px solid var(--border)"></div>`;
|
||||
}
|
||||
|
||||
// Raw content
|
||||
if (it.ocr) {
|
||||
html += `<div class="drill-section"><h4>📄 OCR complet (${it.ocr.length} caractères)</h4><div class="drill-text">${escape(it.ocr)}</div></div>`;
|
||||
} else if (it.raw) {
|
||||
html += `<div class="drill-section"><h4>📋 Raw data</h4><div class="drill-text">${escape(JSON.stringify(it.raw, null, 2))}</div></div>`;
|
||||
} else if (it.text_sample) {
|
||||
html += `<div class="drill-section"><h4>📄 Contenu</h4><div class="drill-text">${escape(it.text_sample)}</div></div>`;
|
||||
}
|
||||
|
||||
// Entities
|
||||
if (it.entities) {
|
||||
const e = it.entities;
|
||||
const parts = [];
|
||||
['people','orgs','emails','phones','urls','money','deadlines','oss','apps'].forEach(k => {
|
||||
if (e[k] && e[k].length) {
|
||||
parts.push(`<div class="drill-kv"><div class="drill-kv-k">${catIcon(k)} ${k} (${e[k].length})</div><div class="drill-kv-v">${e[k].slice(0, 10).map(v => escape(v)).join(', ')}${e[k].length > 10 ? '…' : ''}</div></div>`);
|
||||
}
|
||||
});
|
||||
if (parts.length) html += `<div class="drill-section"><h4>🔎 Entités extraites</h4><div class="drill-grid">${parts.join('')}</div></div>`;
|
||||
|
||||
// Sentiment + urgency
|
||||
html += `<div class="drill-section"><h4>📊 Analyse</h4><div class="drill-grid">
|
||||
<div class="drill-kv"><div class="drill-kv-k">Urgence</div><div class="drill-kv-v" style="color:${e.urgency==='high'?'var(--urgent)':(e.urgency==='medium'?'var(--warning)':'var(--mute)')}">${e.urgency}</div></div>
|
||||
<div class="drill-kv"><div class="drill-kv-k">Sentiment</div><div class="drill-kv-v" style="color:${e.sentiment==='negative'?'var(--danger)':(e.sentiment==='positive'?'var(--success)':'var(--mute)')}">${e.sentiment}</div></div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
// Recommandations
|
||||
if (it.recommendations && it.recommendations.length) {
|
||||
html += `<div class="drill-section"><h4>💡 Recommandations IA (${it.recommendations.length})</h4>${it.recommendations.map(r => renderReco(r)).join('')}</div>`;
|
||||
}
|
||||
|
||||
// Metadata
|
||||
html += `<div class="drill-section"><h4>ℹ️ Metadata</h4><div class="drill-grid">
|
||||
<div class="drill-kv"><div class="drill-kv-k">ID</div><div class="drill-kv-v" style="font-family:monospace;font-size:11px">${it.id}</div></div>
|
||||
<div class="drill-kv"><div class="drill-kv-k">Ingéré</div><div class="drill-kv-v">${new Date(it.ingested_at).toLocaleString('fr-FR')}</div></div>
|
||||
${it.size ? `<div class="drill-kv"><div class="drill-kv-k">Taille</div><div class="drill-kv-v">${(it.size/1024).toFixed(1)} KB</div></div>` : ''}
|
||||
${it.ocr_len ? `<div class="drill-kv"><div class="drill-kv-k">OCR length</div><div class="drill-kv-v">${it.ocr_len} chars</div></div>` : ''}
|
||||
</div></div>`;
|
||||
|
||||
body.innerHTML = html;
|
||||
document.getElementById('drill').classList.add('open');
|
||||
} catch (e) {
|
||||
showToast('Erreur drill: ' + e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function showToast(msg, isErr){
|
||||
const t = document.createElement('div');
|
||||
t.className = 'toast'+(isErr?' err':'');
|
||||
t.textContent = msg;
|
||||
document.body.appendChild(t);
|
||||
setTimeout(() => t.remove(), 4500);
|
||||
function closeDrill() { document.getElementById('drill').classList.remove('open'); }
|
||||
function closeSC() { document.getElementById('shortcuts').classList.remove('open'); }
|
||||
function closeMCP() { document.getElementById('mcps').classList.remove('open'); }
|
||||
function showShortcuts() { document.getElementById('shortcuts').classList.add('open'); }
|
||||
|
||||
function showMCPs() {
|
||||
const mcps = [
|
||||
{ name: 'blade_exec', desc: 'Execute PowerShell on Razer', status: 'ok' },
|
||||
{ name: 'blade_chrome_cdp', desc: 'Chrome DevTools Protocol via Razer', status: 'ok' },
|
||||
{ name: 'blade_screenshot', desc: 'Capture Razer desktop', status: 'ok' },
|
||||
{ name: 'blade_file_read/write', desc: 'Razer filesystem access', status: 'ok' },
|
||||
{ name: 'wevia_apple_ingest (native)', desc: 'Photo/structured ingestion + AI', status: 'ok' },
|
||||
{ name: '🔴 apple_photos_scrape', desc: 'Read iCloud Photos via AppleScript (Razer Mac or Blade)', status: 'missing' },
|
||||
{ name: '🔴 apple_messages_scrape', desc: 'Read ~/Library/Messages/chat.db (macOS required)', status: 'missing' },
|
||||
{ name: '🔴 apple_contacts_scrape', desc: 'Read AddressBook via AppleScript', status: 'missing' },
|
||||
{ name: '🔴 apple_calendar_scrape', desc: 'Read Calendar.app via EventKit', status: 'missing' },
|
||||
{ name: '🔴 apple_health_export', desc: 'Parse HealthKit XML export', status: 'missing' },
|
||||
{ name: '🔴 apple_notes_scrape', desc: 'Read Notes.app SQLite DB', status: 'missing' },
|
||||
{ name: '🔴 apple_reminders_scrape', desc: 'Read Reminders.app', status: 'missing' },
|
||||
{ name: '🔴 apple_safari_history', desc: 'Read Safari History.db', status: 'missing' }
|
||||
];
|
||||
const ok = mcps.filter(m => m.status === 'ok').length;
|
||||
const miss = mcps.filter(m => m.status === 'missing').length;
|
||||
document.getElementById('mcps-body').innerHTML = `
|
||||
<p style="margin-bottom:16px;color:var(--mute)">MCP (Model Context Protocol) tools exposés via Blade-MCP server (port 8765, HTTPS via /mcp/blade)</p>
|
||||
<div class="drill-grid" style="margin-bottom:16px">
|
||||
<div class="drill-kv"><div class="drill-kv-k">Actifs</div><div class="drill-kv-v" style="color:var(--success);font-size:20px;font-weight:700">${ok}</div></div>
|
||||
<div class="drill-kv"><div class="drill-kv-k">Manquants</div><div class="drill-kv-v" style="color:var(--warning);font-size:20px;font-weight:700">${miss}</div></div>
|
||||
</div>
|
||||
<div class="drill-section">
|
||||
<h4>✅ Actifs</h4>
|
||||
${mcps.filter(m => m.status==='ok').map(m => `
|
||||
<div class="mcp-card">
|
||||
<span class="mcp-status ok">ACTIVE</span>
|
||||
<div class="mcp-name">${m.name}</div>
|
||||
<div class="mcp-desc">${m.desc}</div>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
<div class="drill-section">
|
||||
<h4>⚠️ À implémenter (nécessitent Mac pour macOS AppleScript, pas juste Razer Windows)</h4>
|
||||
${mcps.filter(m => m.status==='missing').map(m => `
|
||||
<div class="mcp-card missing">
|
||||
<span class="mcp-status missing">MISSING</span>
|
||||
<div class="mcp-name">${m.name}</div>
|
||||
<div class="mcp-desc">${m.desc}</div>
|
||||
</div>`).join('')}
|
||||
</div>
|
||||
<div class="drill-section">
|
||||
<h4>🔑 Endpoint MCP</h4>
|
||||
<div class="drill-text">https://weval-consulting.com/mcp/blade
|
||||
Authorization: Bearer wevia_blade_mcp_20avr_k9f3m2x8n5q7p1
|
||||
Content-Type: application/json
|
||||
Body: {"jsonrpc":"2.0","id":1,"method":"tools/list"}</div>
|
||||
</div>`;
|
||||
document.getElementById('mcps').classList.add('open');
|
||||
}
|
||||
|
||||
function filterByEntity(cat, val) {
|
||||
showToast(`Filtre: ${cat}="${val}" (à implémenter)`, false);
|
||||
}
|
||||
|
||||
function catIcon(cat) {
|
||||
return { people:'👤', orgs:'🏢', emails:'✉️', phones:'📱', urls:'🔗', money:'💰', deadlines:'⏰', oss:'🔧', apps:'📲', locations:'📍', keywords:'🔑' }[cat] || '•';
|
||||
}
|
||||
function tabLabel(t) { return { photo:'📸 Photos', message:'💬 Messages', contact:'👤 Contacts', calendar:'📅 Calendar', note:'📝 Notes', call:'📞 Calls', health:'❤️ Health' }[t] || t; }
|
||||
function escape(s) { return String(s||'').replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
||||
function escapeAttr(s) { return String(s||'').replace(/'/g, "\\'").replace(/"/g, '\\"'); }
|
||||
function showToast(msg, err) {
|
||||
const t = document.getElementById('toast');
|
||||
t.textContent = msg; t.style.borderColor = err ? 'var(--danger)' : 'var(--success)';
|
||||
t.classList.add('show');
|
||||
setTimeout(() => t.classList.remove('show'), 2500);
|
||||
}
|
||||
|
||||
// Tab switching
|
||||
document.querySelectorAll('.tab').forEach(t => {
|
||||
t.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab').forEach(x => x.classList.remove('active'));
|
||||
t.classList.add('active');
|
||||
STATE.currentTab = t.dataset.tab;
|
||||
renderContent();
|
||||
});
|
||||
});
|
||||
|
||||
// Upload handling
|
||||
const drop = document.getElementById('drop');
|
||||
const fInput = document.getElementById('f');
|
||||
['dragover','dragenter'].forEach(ev => drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.add('dragover'); }));
|
||||
['dragleave','drop'].forEach(ev => drop.addEventListener(ev, e => { e.preventDefault(); drop.classList.remove('dragover'); }));
|
||||
drop.addEventListener('drop', e => { handleFiles(e.dataTransfer.files); });
|
||||
fInput.addEventListener('change', e => handleFiles(e.target.files));
|
||||
|
||||
async function handleFiles(files) {
|
||||
if (!files || !files.length) return;
|
||||
const prog = document.getElementById('prog');
|
||||
prog.classList.add('show');
|
||||
let done = 0;
|
||||
for (const f of files) {
|
||||
document.getElementById('prog-txt').textContent = `${done+1} / ${files.length} — ${f.name}`;
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('file', f);
|
||||
await fetch(API + '?action=ingest_photo', { method: 'POST', body: fd });
|
||||
} catch (e) { console.error(e); }
|
||||
done++;
|
||||
document.getElementById('prog-fill').style.width = (done/files.length*100) + '%';
|
||||
}
|
||||
document.getElementById('prog-txt').textContent = `✓ ${done} photos ingérées`;
|
||||
setTimeout(() => prog.classList.remove('show'), 2000);
|
||||
loadAll();
|
||||
}
|
||||
|
||||
// Escape key to close modals
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape') { closeDrill(); closeSC(); closeMCP(); }
|
||||
});
|
||||
|
||||
loadAll();
|
||||
setInterval(loadAll, 30000);
|
||||
</script>
|
||||
|
||||
<!-- === OPUS UNIVERSAL DRILL-DOWN v1 19avr — append-only, doctrine #14 === -->
|
||||
<script>
|
||||
(function(){
|
||||
if (window.__opusUniversalDrill) return; window.__opusUniversalDrill = true;
|
||||
var d = document;
|
||||
var m = d.createElement('div');
|
||||
m.id = 'opus-udrill';
|
||||
m.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.82);backdrop-filter:blur(6px);display:none;align-items:center;justify-content:center;z-index:99995;padding:20px;cursor:pointer';
|
||||
var inner = d.createElement('div');
|
||||
inner.id = 'opus-udrill-in';
|
||||
inner.style.cssText = 'max-width:900px;width:100%;max-height:90vh;overflow:auto;background:#0b0d15;border:1px solid rgba(99,102,241,0.35);border-radius:14px;padding:28px;cursor:default;box-shadow:0 20px 60px rgba(0,0,0,0.6);color:#e2e8f0;font:14px/1.55 Inter,system-ui,sans-serif';
|
||||
inner.addEventListener('click', function(e){ e.stopPropagation(); });
|
||||
m.appendChild(inner);
|
||||
m.addEventListener('click', function(){ m.style.display='none'; });
|
||||
d.addEventListener('keydown', function(e){ if(e.key==='Escape') m.style.display='none'; });
|
||||
(d.body || d.documentElement).appendChild(m);
|
||||
function openCard(card) {
|
||||
var html = '<div style="display:flex;justify-content:flex-end;margin-bottom:14px"><button id="opus-udrill-close" style="padding:6px 14px;background:#171b2a;border:1px solid rgba(99,102,241,0.25);color:#e2e8f0;border-radius:8px;cursor:pointer;font-size:12px">✕ Fermer (Esc)</button></div>';
|
||||
html += '<div style="transform-origin:top left;font-size:1.05em">' + card.outerHTML + '</div>';
|
||||
inner.innerHTML = html;
|
||||
d.getElementById('opus-udrill-close').onclick = function(){ m.style.display='none'; };
|
||||
m.style.display = 'flex';
|
||||
}
|
||||
function wire(root) {
|
||||
var sels = '.card,[class*="card"],.kpi,[class*="kpi"],.stat,[class*="stat"],.tile,[class*="tile"],.metric,[class*="metric"],.widget,[class*="widget"]';
|
||||
var cards = root.querySelectorAll(sels);
|
||||
for (var i = 0; i < cards.length; i++) {
|
||||
var c = cards[i];
|
||||
if (c.__opusWired) continue;
|
||||
if (c.closest('button, a, input, select, textarea, #opus-udrill')) continue;
|
||||
var r = c.getBoundingClientRect();
|
||||
if (r.width < 60 || r.height < 40) continue;
|
||||
c.__opusWired = true;
|
||||
c.style.cursor = 'pointer';
|
||||
c.setAttribute('role','button');
|
||||
c.setAttribute('tabindex','0');
|
||||
c.addEventListener('click', function(ev){
|
||||
if (ev.target.closest('[data-pp-id]') && window.__opusDrillInit) return;
|
||||
if (ev.target.closest('a,button,input,select')) return;
|
||||
ev.preventDefault(); ev.stopPropagation();
|
||||
openCard(this);
|
||||
});
|
||||
c.addEventListener('keydown', function(ev){ if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();openCard(this);} });
|
||||
}
|
||||
}
|
||||
var initRun = function(){ wire(d.body || d.documentElement); };
|
||||
if (d.readyState === 'loading') d.addEventListener('DOMContentLoaded', initRun);
|
||||
else initRun();
|
||||
var mo = new MutationObserver(function(muts){
|
||||
var newCard = false;
|
||||
for (var i=0;i<muts.length;i++) if (muts[i].addedNodes.length) { newCard = true; break; }
|
||||
if (newCard) initRun();
|
||||
});
|
||||
mo.observe(d.body || d.documentElement, {childList:true, subtree:true});
|
||||
})();
|
||||
</script>
|
||||
<!-- === OPUS UNIVERSAL DRILL-DOWN END === -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user