Files
html/wevia-apple.html

694 lines
40 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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.1</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>
<input type="search" id="search" placeholder="🔎 Chercher..." onkeydown="if(event.key==='Enter')doSearch()" style="padding:8px 12px;border-radius:8px;border:1px solid var(--border);background:var(--surface-alt);color:var(--text);font-size:13px;width:200px">
<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>📥 Télécharge les 7 guides de configuration iOS Shortcut</h4>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:6px;margin-bottom:16px">
<a class="btn" href="/downloads/wevia-shortcut-photos.json" download>📸 Photos</a>
<a class="btn" href="/downloads/wevia-shortcut-messages.json" download>💬 Messages</a>
<a class="btn" href="/downloads/wevia-shortcut-contacts.json" download>👤 Contacts</a>
<a class="btn" href="/downloads/wevia-shortcut-calendar.json" download>📅 Calendar</a>
<a class="btn" href="/downloads/wevia-shortcut-notes.json" download>📝 Notes</a>
<a class="btn" href="/downloads/wevia-shortcut-calls.json" download>📞 Calls</a>
<a class="btn" href="/downloads/wevia-shortcut-health.json" download>❤️ Health</a>
</div>
</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="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) {
const hasAction = r.id && (r.kind === 'task_create' || r.kind === 'urgent_alert');
const isTask = r.kind === 'task_create';
return `
<div class="reco-card ${r.priority}">
<div class="reco-head">
<span class="reco-prio ${r.priority}">${r.priority}</span>
<span class="reco-kind">${r.kind}</span>
${r.status === 'done' || r.status === 'resolved' ? '<span class="badge" style="margin-left:auto;background:rgba(74,222,128,0.15);color:var(--success)">✓ ' + r.status + '</span>' : ''}
</div>
<div class="reco-label" onclick="${r.source_id || r.source_item ? `openDrill('${r.source_item || r.source_id}')` : ''}" style="cursor:${r.source_item || r.source_id ? 'pointer' : 'default'}">${escape(r.label)}</div>
<div class="reco-action">→ ${escape(r.action)}</div>
${hasAction && r.status !== 'done' && r.status !== 'resolved' ? `
<div style="margin-top:6px;display:flex;gap:4px">
${isTask ? `<button class="btn" style="padding:3px 8px;font-size:11px" onclick="event.stopPropagation();markDone('${r.id}')">✓ Mark done</button>` : ''}
${r.kind === 'urgent_alert' ? `<button class="btn" style="padding:3px 8px;font-size:11px" onclick="event.stopPropagation();resolveAlert('${r.id}')">🛑 Resolve</button>` : ''}
</div>` : ''}
</div>`;
}
async function markDone(id) {
const r = await fetch(API + '?action=mark_done&id=' + encodeURIComponent(id)).then(r => r.json());
if (r.ok) { showToast('✓ Tâche marquée done'); loadAll(); }
else showToast('Erreur: ' + (r.error || 'unknown'), true);
}
async function resolveAlert(id) {
const r = await fetch(API + '?action=resolve_alert&id=' + encodeURIComponent(id)).then(r => r.json());
if (r.ok) { showToast('✓ Alerte résolue'); loadAll(); }
else showToast('Erreur: ' + (r.error || 'unknown'), true);
}
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_status', desc: 'Razer CPU/RAM/disk/uptime', status: 'ok' },
{ name: 'blade_open_url / send_keys / file_read / file_write', desc: 'Razer interaction primitives', status: 'ok' },
{ name: 'apple_ingest_note', desc: 'Ingest text via MCP (auto entity extract + reco)', status: 'ok' },
{ name: 'apple_ingest_message', desc: 'Ingest SMS/iMessage via MCP', status: 'ok' },
{ name: 'apple_status', desc: 'WEVIA Apple state (items, tasks, alerts)', status: 'ok' },
{ name: 'apple_search', desc: 'Full-text search in ingested Apple data', status: 'ok' },
{ name: 'apple_recommendations', desc: 'Top IA reco P0-P3 via MCP', status: 'ok' },
{ name: 'apple_tasks_pending / apple_mark_task_done', desc: 'Task management via MCP', status: 'ok' },
{ name: '⚠️ apple_mac_scrape_photos', desc: 'Scrape iCloud Photos via AppleScript (needs Mac agent)', status: 'mac_required' },
{ name: '⚠️ apple_mac_scrape_messages', desc: 'Read chat.db on Mac (needs Full Disk Access)', status: 'mac_required' },
{ name: '🔴 apple_contacts_scrape', desc: 'AddressBook via AppleScript on Mac', status: 'missing' },
{ name: '🔴 apple_calendar_scrape', desc: 'Calendar.app via EventKit on Mac', status: 'missing' },
{ name: '🔴 apple_health_export', desc: 'Parse HealthKit XML export', status: 'missing' },
{ name: '🔴 apple_notes_scrape', desc: 'Notes.app SQLite DB (Mac)', status: 'missing' },
{ name: '🔴 apple_reminders_scrape', desc: 'Reminders.app (Mac)', status: 'missing' },
{ name: '🔴 apple_safari_history', desc: 'Safari History.db (Mac)', 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">Nécessite Mac</div><div class="drill-kv-v" style="color:#06b6d4;font-size:20px;font-weight:700">${mac_req}</div></div>
<div class="drill-kv"><div class="drill-kv-k">À implémenter</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>🔵 Prêts si Blade agent installé sur Mac (USB iPhone → scrape)</h4>
${mcps.filter(m => m.status==='mac_required').map(m => `
<div class="mcp-card" style="border-left-color:#06b6d4">
<span class="mcp-status" style="background:rgba(6,182,212,0.15);color:#06b6d4">MAC AGENT</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 (roadmap)</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"}
Version: v1.1.0 · 17 tools exposed (8 blade + 9 apple)</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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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);
}
async function doSearch() {
const q = document.getElementById('search').value.trim();
if (q.length < 2) { showToast('Query trop courte', true); return; }
try {
const r = await fetch(API + '?action=search&q=' + encodeURIComponent(q)).then(r => r.json());
if (!r.ok) { showToast('Erreur search', true); return; }
const body = document.getElementById('list-body');
document.getElementById('list-title').textContent = '🔎 Recherche: "' + q + '"';
document.getElementById('list-count').textContent = r.total + ' match(es)';
if (!r.matches.length) { body.innerHTML = '<div class="empty">Aucun résultat pour "' + q + '"</div>'; return; }
body.innerHTML = r.matches.map(m => `
<div class="item-row" onclick="openDrill('${m.id}')">
<div class="item-head"><span class="item-type ${m.type}">${m.type}</span>
<span class="item-ts">${new Date(m.ingested_at).toLocaleString('fr-FR')}</span></div>
<div class="item-preview">${escape(m.preview)}</div>
</div>`).join('');
} catch (e) { showToast('Erreur: ' + e.message, true); }
}
// 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>(function(){var p=window.location.pathname;var pub=["/","/index.html","/wevia.html","/wevia-widget.html","/enterprise-model.html","/wevia","/login","/register.html","/agents-archi.html","/wevia-meeting-rooms.html","/director-center.html","/director-chat.html","/l99-brain.html","/agents-fleet.html","/value-streaming.html","/architecture.html","/openclaw.html","/l99-saas.html","/admin-saas.html","/agents-goodjob.html","/ai-benchmark.html","/oss-discovery.html","/paperclip.html","/agents-3d.html","/agents-alive.html","/agents-enterprise.html","/agents-hd.html","/agents-iso3d.html","/agents-sim.html","/agents-valuechain.html","/avatar-picker.html","/login.html","/register","/weval-login.html","/weval-login","/arsenal-login.html","/arsenal-login","/ethica-login.html","/ethica-login","/office-login.html","/office-login","/wtp-login.html","/wtp-login","/azure-reregister.html","/azure-reregister","/authentik-callback","/auth-callback","/reset-password.html","/reset-password","/signup.html","/signup"];var isPub=pub.indexOf(p)>=0||p.indexOf("/products/")===0||p.indexOf("/solutions/")===0||p.indexOf("/blog/")===0||p.indexOf("/service/")===0||p.indexOf("/marketplace")===0||p.indexOf("/contact")===0||p.indexOf("/tarifs")===0||p.indexOf("/news")===0;if(isPub||document.getElementById("weval-gl"))return;var a=document.createElement("a");a.id="weval-gl";a.href="/logout";a.textContent="Logout";a.style.cssText="position:fixed;top:10px;right:12px;z-index:99990;padding:5px 10px;background:rgba(30,30,50,0.7);color:rgba(200,210,230,0.8);border:1px solid rgba(100,100,140,0.3);border-radius:6px;font:500 11px system-ui,sans-serif;text-decoration:none;opacity:0.6;cursor:pointer;backdrop-filter:blur(6px);transition:all .15s";a.onmouseover=function(){this.style.opacity="1";this.style.background="rgba(239,68,68,0.85)";this.style.color="white"};a.onmouseout=function(){this.style.opacity="0.6";this.style.background="rgba(30,30,50,0.7)";this.style.color="rgba(200,210,230,0.8)"};document.body.appendChild(a)})()</script><script src="/api/archi-meta-badge.js" defer></script>
<script src="/api/a11y-auto-enhancer.js" defer></script>
<!-- WTP_UDOCK_V1 (Opus 21-avr t32b4) --><script src="/wtp-unified-dock.js" defer></script>
</body>
</html>