620 lines
34 KiB
HTML
620 lines
34 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="fr">
|
||
<head>
|
||
<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;
|
||
--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="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>
|
||
|
||
<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>
|
||
|
||
<div class="panel">
|
||
<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>
|
||
|
||
<!-- 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>
|
||
|
||
<!-- 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>
|
||
|
||
<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="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>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 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>
|
||
|
||
<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 [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);
|
||
}
|
||
}
|
||
|
||
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 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 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>
|
||
<script src="/api/archi-meta-badge.js" defer></script>
|
||
</body>
|
||
</html>
|