Files
html/api/wevia-apple-ingest.php
2026-04-20 03:40:02 +02:00

494 lines
25 KiB
PHP

<?php
// WEVIA APPLE INGEST v3.1 — full iPhone ingestion + AI analysis + task/alert management
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-WEVIA-Token');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS, DELETE');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit;
$action = $_GET['action'] ?? $_POST['action'] ?? 'status';
$DATA_DIR = '/var/www/html/data/wevia-apple';
$UP_DIR = "$DATA_DIR/uploads";
$EVENTS_FILE = "$DATA_DIR/events.jsonl";
$INDEX_FILE = "$DATA_DIR/index.json";
foreach ([$DATA_DIR, $UP_DIR, "$DATA_DIR/photos", "$DATA_DIR/messages", "$DATA_DIR/contacts", "$DATA_DIR/calendar", "$DATA_DIR/notes", "$DATA_DIR/health", "$DATA_DIR/calls", "$DATA_DIR/analysis"] as $d) {
if (!is_dir($d)) @mkdir($d, 0775, true);
}
function load_index() {
global $INDEX_FILE;
if (!file_exists($INDEX_FILE)) {
return [
'total_items' => 0,
'by_type' => ['photo'=>0, 'message'=>0, 'contact'=>0, 'calendar'=>0, 'note'=>0, 'health'=>0, 'call'=>0],
'entities' => ['people'=>[], 'orgs'=>[], 'money'=>[], 'deadlines'=>[], 'locations'=>[], 'emails'=>[], 'phones'=>[], 'urls'=>[], 'apps'=>[], 'oss'=>[]],
'tasks' => [], 'opportunities' => [], 'alerts' => [],
'last_update' => null, 'drill_index' => []
];
}
return json_decode(file_get_contents($INDEX_FILE), true) ?: [];
}
function save_index($idx) {
global $INDEX_FILE;
$idx['last_update'] = date('c');
file_put_contents($INDEX_FILE, json_encode($idx, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
}
function append_event($ev) {
global $EVENTS_FILE;
file_put_contents($EVENTS_FILE, json_encode($ev, JSON_UNESCAPED_UNICODE) . "\n", FILE_APPEND);
}
function extract_entities($text) {
$e = ['people'=>[], 'orgs'=>[], 'money'=>[], 'deadlines'=>[], 'locations'=>[], 'emails'=>[], 'phones'=>[], 'urls'=>[], 'apps'=>[], 'keywords'=>[], 'sentiment'=>'neutral', 'urgency'=>'low', 'oss'=>[]];
preg_match_all('/[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/', $text, $m);
$e['emails'] = array_values(array_unique($m[0]));
preg_match_all('/(?:\+\d{1,3}[\s\-]?)?(?:\(?\d{2,4}\)?[\s\-]?){2,5}\d{2,4}/', $text, $m);
$e['phones'] = array_values(array_unique(array_filter($m[0], function($p) {
$d = preg_replace('/[^\d]/', '', $p);
return strlen($d) >= 8 && strlen($d) <= 15;
})));
preg_match_all('#https?://[^\s<>"\']+#i', $text, $m);
$e['urls'] = array_values(array_unique($m[0]));
preg_match_all('/(?:[\$€£¥]|MAD|DZD|TND|EUR|USD|DH)\s*[\d\s,.]+|\d+(?:[\s,.]\d+)*\s*(?:€|\$|£|MAD|DZD|TND|DH|EUR|USD|k|K|M)\b/u', $text, $m);
$e['money'] = array_values(array_unique(array_map('trim', $m[0])));
preg_match_all('/\b(?:\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}|\d{4}\-\d{2}\-\d{2}|(?:aujourd\'hui|demain|hier|tomorrow|today|yesterday|this week|next week|la semaine prochaine))/iu', $text, $m);
$e['deadlines'] = array_values(array_unique(array_map('trim', $m[0])));
preg_match_all('/\b(?:Dr|Mr|Mrs|Ms|Mme|M\.|Pr|Prof|CEO|CTO|CFO|COO|VP|Dir|Directeur|Director|Manager)\.?\s+([A-ZÀÂÉÈÊÎÔÛÇ][a-zàâéèêîôûç]+(?:\s+[A-ZÀÂÉÈÊÎÔÛÇ][a-zàâéèêîôûç]+)*)/u', $text, $m);
$e['people'] = array_values(array_unique($m[0]));
preg_match_all('/\b[A-Z][a-zA-Z0-9]+(?:\s+[A-Z][a-zA-Z0-9]+)*\s+(?:Inc\.?|LLC|Ltd|Corp\.?|SA|SARL|SAS|GmbH|AG|BV|plc)\b/', $text, $m);
$e['orgs'] = array_values(array_unique($m[0]));
$oss_list = ['langchain','langgraph','crewai','n8n','rasa','ollama','vllm','openrouter','langfuse','dify','flowise','qdrant','chromadb','weaviate','pinecone','milvus','postgres','mongodb','redis','kafka','nginx','kubernetes','docker','grafana','prometheus','stripe','claude','gpt-4','gpt-4o','llama','mistral','gemma','whisper','sap','salesforce','hubspot','notion','obsidian','vercel','cloudflare','github','gitea','supabase','firebase','airtable','zapier','openai','anthropic','gemini','wevads','weval','wevia','ethica','paperclip','arsenal','resend','sendgrid','twilio'];
$tl = ' ' . strtolower($text) . ' ';
foreach ($oss_list as $o) {
if (preg_match('/[^a-z0-9]' . preg_quote($o, '/') . '[^a-z0-9]/i', $tl)) $e['oss'][] = $o;
}
$e['oss'] = array_values(array_unique($e['oss']));
if (preg_match('/\b(urgent|asap|immediatly|critical|critique|deadline|echeance|today|aujourd)/i', $text)) $e['urgency'] = 'high';
elseif (preg_match('/\b(important|priority|priorite|week|semaine|soon|bientot)/i', $text)) $e['urgency'] = 'medium';
$pos_w = preg_match_all('/\b(great|excellent|parfait|super|merci|thanks|good|ok|approved|accepted|yes|oui)\b/i', $text);
$neg_w = preg_match_all('/\b(problem|probleme|error|erreur|refused|rejected|no|non|urgent|complaint|issue|bug|broken)\b/i', $text);
if ($pos_w > $neg_w + 1) $e['sentiment'] = 'positive';
elseif ($neg_w > $pos_w + 1) $e['sentiment'] = 'negative';
$apps = ['whatsapp','telegram','instagram','tiktok','linkedin','twitter','x.com','facebook','messenger','slack','discord','teams','zoom','gmail','outlook','calendly','stripe','paypal','revolut','airbnb','uber'];
foreach ($apps as $a) {
if (stripos($text, $a) !== false) $e['apps'][] = $a;
}
$e['apps'] = array_values(array_unique($e['apps']));
return $e;
}
function generate_recommendations($item) {
$reco = [];
$e = $item['entities'] ?? [];
$type = $item['type'] ?? 'unknown';
$text = $item['text_sample'] ?? '';
if (!empty($e['deadlines'])) {
foreach ($e['deadlines'] as $d) {
$reco[] = ['kind'=>'task_create', 'priority'=>($e['urgency']==='high'?'P0':($e['urgency']==='medium'?'P1':'P2')),
'label'=>"Créer tâche pour échéance: $d",
'action'=>"Ajouter à Calendar/Reminders avec contexte: " . substr($text, 0, 120),
'source'=>$item['id'] ?? null];
}
}
if (!empty($e['money'])) {
$reco[] = ['kind'=>'finance_track','priority'=>'P1',
'label'=>'Montant(s) détecté(s): ' . implode(', ', array_slice($e['money'], 0, 3)),
'action'=>'Vérifier facture/devis et lier CRM WEVAL', 'source'=>$item['id'] ?? null];
}
if (!empty($e['people']) || !empty($e['orgs'])) {
$reco[] = ['kind'=>'crm_enrich','priority'=>'P2',
'label'=>'Contacts détectés: ' . implode(', ', array_slice(array_merge($e['people'], $e['orgs']), 0, 3)),
'action'=>'Ajouter CRM Twenty + enrichir LinkedIn','source'=>$item['id'] ?? null];
}
if (!empty($e['emails']) || !empty($e['phones'])) {
$reco[] = ['kind'=>'contact_capture','priority'=>'P2',
'label'=>count($e['emails']) . ' email(s), ' . count($e['phones']) . ' phone(s)',
'action'=>'Sync iPhone Contacts + CRM','source'=>$item['id'] ?? null];
}
if (!empty($e['oss'])) {
$reco[] = ['kind'=>'tech_research','priority'=>'P3',
'label'=>'Stacks: ' . implode(', ', array_slice($e['oss'], 0, 5)),
'action'=>'OSS Discovery + backlog R&D','source'=>$item['id'] ?? null];
}
if ($e['urgency'] === 'high' && $type !== 'calendar') {
$reco[] = ['kind'=>'urgent_alert','priority'=>'P0',
'label'=>'Item urgent — traitement immédiat',
'action'=>'Telegram @wevia_cyber_bot','source'=>$item['id'] ?? null];
}
if (!empty($e['urls'])) {
foreach (array_slice($e['urls'], 0, 3) as $u) {
if (stripos($u, 'github.com') !== false) {
$reco[] = ['kind'=>'github_track','priority'=>'P3','label'=>"Repo: $u",
'action'=>'OSS Discovery + star + monitor','source'=>$item['id'] ?? null];
} elseif (stripos($u, 'linkedin.com') !== false) {
$reco[] = ['kind'=>'linkedin_track','priority'=>'P2','label'=>"LinkedIn: $u",
'action'=>'Enrichir CRM + monitor posts','source'=>$item['id'] ?? null];
}
}
}
if ($e['sentiment'] === 'negative' && in_array($type, ['message','email','note'])) {
$reco[] = ['kind'=>'needs_reply','priority'=>'P1',
'label'=>'Sentiment négatif — possible plainte/problème',
'action'=>'Draft reply via WEVIA Email','source'=>$item['id'] ?? null];
}
return $reco;
}
function ocr_image($path) {
$ocr = '';
try {
$ch = curl_init('http://127.0.0.1/api/wevia-vision-api.php');
$b64 = base64_encode(file_get_contents($path));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => 1, CURLOPT_POST => 1,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(['image_b64' => $b64, 'prompt' => 'Extract ALL visible text from this image. Be exhaustive. Return ONLY raw text.']),
CURLOPT_TIMEOUT => 45
]);
$resp = curl_exec($ch);
curl_close($ch);
$d = @json_decode($resp, true);
if (isset($d['text']) && strlen($d['text']) > 10) $ocr = $d['text'];
elseif (isset($d['response'])) $ocr = $d['response'];
elseif (isset($d['result'])) $ocr = $d['result'];
} catch (Exception $e) {}
if (!$ocr && shell_exec('which tesseract 2>/dev/null')) {
$tmp = tempnam('/tmp', 'ocr_');
exec("tesseract " . escapeshellarg($path) . " $tmp 2>/dev/null");
if (file_exists("$tmp.txt")) {
$ocr = file_get_contents("$tmp.txt");
@unlink("$tmp.txt");
}
}
return trim($ocr);
}
// FIX v3.1: tasks and alerts can co-exist — task_create P0 goes to BOTH tasks[] AND alerts[]
function apply_reco_to_index(&$idx, $reco, $item_id) {
foreach ($reco as $r) {
$r['id'] = 'reco_' . uniqid('', true);
$r['source_item'] = $item_id;
$r['created_at'] = date('c');
$r['status'] = 'open';
// ALL P0 → alerts
if ($r['priority'] === 'P0') $idx['alerts'][] = $r;
// ALL task_create → tasks (regardless of priority)
if ($r['kind'] === 'task_create') $idx['tasks'][] = $r;
// tech_research → opportunities
if ($r['kind'] === 'tech_research') $idx['opportunities'][] = $r;
}
}
// ===== ACTIONS =====
if ($action === 'status') {
$idx = load_index();
echo json_encode([
'ok' => true, 'v' => 'v3.1-full-ingestion', 'ts' => date('c'),
'total_items' => $idx['total_items'] ?? 0,
'by_type' => $idx['by_type'] ?? [],
'entities_count' => array_map(function($v) { return is_array($v) ? count($v) : 0; }, $idx['entities'] ?? []),
'tasks_pending' => count(array_filter($idx['tasks'] ?? [], function($t) { return ($t['status'] ?? 'open') === 'open'; })),
'tasks_total' => count($idx['tasks'] ?? []),
'opportunities' => count($idx['opportunities'] ?? []),
'alerts_pending' => count(array_filter($idx['alerts'] ?? [], function($a) { return ($a['status'] ?? 'open') === 'open'; })),
'alerts_total' => count($idx['alerts'] ?? []),
'last_update' => $idx['last_update'] ?? null,
'drill_count' => count($idx['drill_index'] ?? [])
], JSON_PRETTY_PRINT);
exit;
}
if ($action === 'ingest_photo') {
if (empty($_FILES['file'])) { echo json_encode(['ok'=>false,'error'=>'no file']); exit; }
$f = $_FILES['file'];
$id = uniqid('photo_', true);
$safe = preg_replace('/[^a-zA-Z0-9._\-]/', '_', $f['name']);
$dest = "$DATA_DIR/photos/$id.$safe";
move_uploaded_file($f['tmp_name'], $dest);
$ocr = ocr_image($dest);
$entities = extract_entities($ocr);
$item = ['id'=>$id, 'type'=>'photo', 'filename'=>$f['name'], 'size'=>filesize($dest), 'path'=>$dest,
'url'=>'/data/wevia-apple/photos/' . basename($dest),
'ocr'=>$ocr, 'ocr_len'=>strlen($ocr), 'text_sample'=>substr($ocr, 0, 500),
'entities'=>$entities, 'ingested_at'=>date('c')];
$item['recommendations'] = generate_recommendations($item);
$idx = load_index();
$idx['total_items']++;
$idx['by_type']['photo'] = ($idx['by_type']['photo'] ?? 0) + 1;
$idx['drill_index'][$id] = $item;
foreach ($entities as $k => $v) {
if (is_array($v)) {
foreach ($v as $val) $idx['entities'][$k][] = ['val'=>$val, 'source'=>$id];
}
}
apply_reco_to_index($idx, $item['recommendations'], $id);
save_index($idx);
append_event(['type'=>'ingest', 'item_id'=>$id, 'ts'=>date('c')]);
echo json_encode(['ok'=>true, 'id'=>$id, 'item'=>$item], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
if ($action === 'ingest_structured') {
$body = json_decode(file_get_contents('php://input'), true) ?: [];
$type = $body['type'] ?? $_POST['type'] ?? 'note';
$items = $body['items'] ?? ($body['item'] ? [$body['item']] : []);
if (empty($items)) { echo json_encode(['ok'=>false,'error'=>'no items']); exit; }
$idx = load_index();
$processed = [];
foreach ($items as $it) {
$id = uniqid($type . '_', true);
if ($type === 'message') $text = trim(($it['from'] ?? '') . " -> " . ($it['to'] ?? '') . "\n" . ($it['body'] ?? ''));
elseif ($type === 'contact') $text = trim(($it['name'] ?? '') . "\n" . ($it['phone'] ?? '') . "\n" . ($it['email'] ?? '') . "\n" . ($it['org'] ?? '') . "\n" . ($it['notes'] ?? ''));
elseif ($type === 'calendar') $text = trim(($it['title'] ?? '') . "\n" . ($it['location'] ?? '') . "\n" . ($it['notes'] ?? '') . "\n" . ($it['start'] ?? '') . ' - ' . ($it['end'] ?? ''));
elseif ($type === 'note') $text = trim(($it['title'] ?? '') . "\n" . ($it['body'] ?? ''));
elseif ($type === 'health') $text = json_encode($it, JSON_UNESCAPED_UNICODE);
elseif ($type === 'call') $text = trim(($it['name'] ?? 'Unknown') . ' - ' . ($it['number'] ?? '') . ' - ' . ($it['duration'] ?? '') . 's - ' . ($it['direction'] ?? ''));
else $text = json_encode($it, JSON_UNESCAPED_UNICODE);
$entities = extract_entities($text);
$item = ['id'=>$id, 'type'=>$type, 'raw'=>$it, 'text_sample'=>substr($text, 0, 500),
'entities'=>$entities, 'ingested_at'=>date('c')];
$item['recommendations'] = generate_recommendations($item);
$idx['total_items']++;
$idx['by_type'][$type] = ($idx['by_type'][$type] ?? 0) + 1;
$idx['drill_index'][$id] = $item;
foreach ($entities as $k => $v) {
if (is_array($v)) foreach ($v as $val) $idx['entities'][$k][] = ['val'=>$val, 'source'=>$id];
}
apply_reco_to_index($idx, $item['recommendations'], $id);
$processed[] = $id;
append_event(['type'=>'ingest', 'item_id'=>$id, 'data_type'=>$type, 'ts'=>date('c')]);
}
save_index($idx);
echo json_encode(['ok'=>true, 'processed'=>count($processed), 'ids'=>$processed]);
exit;
}
if ($action === 'drill') {
$id = $_GET['id'] ?? $_POST['id'] ?? '';
$idx = load_index();
$item = $idx['drill_index'][$id] ?? null;
if (!$item) { echo json_encode(['ok'=>false,'error'=>'not found']); exit; }
// Also return related reco + tasks + alerts for this item
$item['related_tasks'] = array_values(array_filter($idx['tasks'] ?? [], function($t) use ($id) { return ($t['source_item'] ?? '') === $id; }));
$item['related_alerts'] = array_values(array_filter($idx['alerts'] ?? [], function($a) use ($id) { return ($a['source_item'] ?? '') === $id; }));
echo json_encode(['ok'=>true, 'item'=>$item], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
exit;
}
if ($action === 'list') {
$type = $_GET['type'] ?? null;
$limit = min(200, (int)($_GET['limit'] ?? 50));
$idx = load_index();
$items = array_values($idx['drill_index'] ?? []);
if ($type) $items = array_values(array_filter($items, function($i) use ($type) { return ($i['type'] ?? '') === $type; }));
usort($items, function($a, $b) { return strcmp($b['ingested_at'] ?? '', $a['ingested_at'] ?? ''); });
$items = array_slice($items, 0, $limit);
$preview = array_map(function($i) {
return ['id'=>$i['id'], 'type'=>$i['type'], 'ingested_at'=>$i['ingested_at'],
'preview'=>substr($i['text_sample'] ?? '', 0, 180),
'entities_count'=>array_map(function($v) { return is_array($v)?count($v):0; }, $i['entities'] ?? []),
'reco_count'=>count($i['recommendations'] ?? []),
'urgency'=>$i['entities']['urgency'] ?? 'low'];
}, $items);
echo json_encode(['ok'=>true, 'items'=>$preview, 'total'=>$idx['total_items'] ?? 0]);
exit;
}
if ($action === 'recommendations') {
$idx = load_index();
$all = [];
foreach ($idx['drill_index'] ?? [] as $item) {
foreach ($item['recommendations'] ?? [] as $r) {
$all[] = $r + ['item_type'=>$item['type'], 'source_id'=>$item['id']];
}
}
$prio_rank = ['P0'=>0, 'P1'=>1, 'P2'=>2, 'P3'=>3];
usort($all, function($a, $b) use ($prio_rank) { return $prio_rank[$a['priority'] ?? 'P3'] <=> $prio_rank[$b['priority'] ?? 'P3']; });
echo json_encode(['ok'=>true, 'recommendations'=>$all, 'total'=>count($all), 'by_priority'=>array_count_values(array_column($all, 'priority'))]);
exit;
}
if ($action === 'entities') {
$idx = load_index();
$merged = [];
foreach ($idx['entities'] ?? [] as $cat => $list) {
if (!is_array($list)) continue;
$counts = [];
foreach ($list as $e) {
$v = is_array($e) ? ($e['val'] ?? '') : $e;
if ($v) $counts[$v] = ($counts[$v] ?? 0) + 1;
}
arsort($counts);
$merged[$cat] = array_map(function($v, $c) { return ['value'=>$v, 'count'=>$c]; }, array_keys($counts), array_values($counts));
}
echo json_encode(['ok'=>true, 'entities'=>$merged]);
exit;
}
if ($action === 'tasks') {
$idx = load_index();
$filter = $_GET['status'] ?? null;
$tasks = $idx['tasks'] ?? [];
if ($filter) $tasks = array_values(array_filter($tasks, function($t) use ($filter) { return ($t['status'] ?? 'open') === $filter; }));
echo json_encode(['ok'=>true, 'tasks'=>$tasks, 'total'=>count($tasks)]);
exit;
}
if ($action === 'alerts') {
$idx = load_index();
$filter = $_GET['status'] ?? null;
$alerts = $idx['alerts'] ?? [];
if ($filter) $alerts = array_values(array_filter($alerts, function($a) use ($filter) { return ($a['status'] ?? 'open') === $filter; }));
echo json_encode(['ok'=>true, 'alerts'=>$alerts, 'total'=>count($alerts)]);
exit;
}
// NEW v3.1: mark_done / resolve_alert
if ($action === 'mark_done') {
$id = $_GET['id'] ?? $_POST['id'] ?? '';
$idx = load_index();
$found = false;
foreach ($idx['tasks'] ?? [] as &$t) {
if (($t['id'] ?? '') === $id) {
$t['status'] = 'done';
$t['done_at'] = date('c');
$found = true;
break;
}
}
if ($found) { save_index($idx); append_event(['type'=>'task_done', 'task_id'=>$id, 'ts'=>date('c')]); }
echo json_encode(['ok'=>$found, 'id'=>$id]);
exit;
}
if ($action === 'resolve_alert') {
$id = $_GET['id'] ?? $_POST['id'] ?? '';
$idx = load_index();
$found = false;
foreach ($idx['alerts'] ?? [] as &$a) {
if (($a['id'] ?? '') === $id) {
$a['status'] = 'resolved';
$a['resolved_at'] = date('c');
$found = true;
break;
}
}
if ($found) { save_index($idx); append_event(['type'=>'alert_resolved', 'alert_id'=>$id, 'ts'=>date('c')]); }
echo json_encode(['ok'=>$found, 'id'=>$id]);
exit;
}
// NEW v3.1: stats_timeline — daily aggregation for charts
if ($action === 'stats_timeline') {
global $EVENTS_FILE;
if (!file_exists($EVENTS_FILE)) { echo json_encode(['ok'=>true, 'timeline'=>[]]); exit; }
$lines = file($EVENTS_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$byDay = [];
foreach ($lines as $l) {
$ev = @json_decode($l, true);
if (!$ev || empty($ev['ts'])) continue;
$day = substr($ev['ts'], 0, 10);
if (!isset($byDay[$day])) $byDay[$day] = ['date'=>$day, 'ingests'=>0, 'tasks_done'=>0, 'alerts_resolved'=>0];
if (($ev['type'] ?? '') === 'ingest') $byDay[$day]['ingests']++;
elseif (($ev['type'] ?? '') === 'task_done') $byDay[$day]['tasks_done']++;
elseif (($ev['type'] ?? '') === 'alert_resolved') $byDay[$day]['alerts_resolved']++;
}
ksort($byDay);
echo json_encode(['ok'=>true, 'timeline'=>array_values($byDay)]);
exit;
}
// NEW v3.1: delete item (with cascade to tasks/alerts/reco)
if ($action === 'delete_item') {
$id = $_GET['id'] ?? $_POST['id'] ?? '';
$idx = load_index();
if (!isset($idx['drill_index'][$id])) { echo json_encode(['ok'=>false,'error'=>'not found']); exit; }
$removed = $idx['drill_index'][$id];
unset($idx['drill_index'][$id]);
$type = $removed['type'] ?? 'unknown';
if (isset($idx['by_type'][$type]) && $idx['by_type'][$type] > 0) $idx['by_type'][$type]--;
$idx['total_items']--;
// Cascade: remove related tasks, alerts, opportunities
$idx['tasks'] = array_values(array_filter($idx['tasks'] ?? [], function($t) use ($id) { return ($t['source_item'] ?? '') !== $id; }));
$idx['alerts'] = array_values(array_filter($idx['alerts'] ?? [], function($a) use ($id) { return ($a['source_item'] ?? '') !== $id; }));
$idx['opportunities'] = array_values(array_filter($idx['opportunities'] ?? [], function($o) use ($id) { return ($o['source_item'] ?? '') !== $id; }));
// Also remove from entities
foreach ($idx['entities'] ?? [] as $cat => &$list) {
if (is_array($list)) {
$list = array_values(array_filter($list, function($e) use ($id) { return !(is_array($e) && ($e['source'] ?? '') === $id); }));
}
}
// Delete physical file if photo
if ($type === 'photo' && !empty($removed['path']) && file_exists($removed['path'])) @unlink($removed['path']);
save_index($idx);
append_event(['type'=>'delete', 'item_id'=>$id, 'ts'=>date('c')]);
echo json_encode(['ok'=>true, 'deleted'=>$id]);
exit;
}
// NEW v3.1: search across all items
if ($action === 'search') {
$q = trim($_GET['q'] ?? $_POST['q'] ?? '');
if (strlen($q) < 2) { echo json_encode(['ok'=>false, 'error'=>'query too short']); exit; }
$idx = load_index();
$matches = [];
foreach ($idx['drill_index'] ?? [] as $item) {
$hay = strtolower(($item['text_sample'] ?? '') . ' ' . ($item['ocr'] ?? '') . ' ' . json_encode($item['entities'] ?? []));
if (strpos($hay, strtolower($q)) !== false) {
$matches[] = ['id'=>$item['id'], 'type'=>$item['type'], 'ingested_at'=>$item['ingested_at'],
'preview'=>substr($item['text_sample'] ?? '', 0, 200)];
}
}
echo json_encode(['ok'=>true, 'query'=>$q, 'matches'=>$matches, 'total'=>count($matches)]);
exit;
}
if ($action === 'shortcut_manifest') {
echo json_encode([
'ok' => true, 'version' => '3.1',
'endpoint' => 'https://weval-consulting.com/api/wevia-apple-ingest.php',
'downloads' => [
'photos' => 'https://weval-consulting.com/downloads/wevia-shortcut-photos.json',
'messages' => 'https://weval-consulting.com/downloads/wevia-shortcut-messages.json',
'contacts' => 'https://weval-consulting.com/downloads/wevia-shortcut-contacts.json',
'calendar' => 'https://weval-consulting.com/downloads/wevia-shortcut-calendar.json',
'notes' => 'https://weval-consulting.com/downloads/wevia-shortcut-notes.json',
'calls' => 'https://weval-consulting.com/downloads/wevia-shortcut-calls.json',
'health' => 'https://weval-consulting.com/downloads/wevia-shortcut-health.json'
],
'actions' => [
'photos' => ['endpoint_action' => 'ingest_photo', 'method' => 'POST multipart', 'field' => 'file'],
'messages' => ['endpoint_action' => 'ingest_structured', 'body' => ['type'=>'message', 'items'=>[['from'=>'', 'to'=>'', 'body'=>'', 'date'=>'']]]],
'contacts' => ['endpoint_action' => 'ingest_structured', 'body' => ['type'=>'contact', 'items'=>[['name'=>'', 'phone'=>'', 'email'=>'', 'org'=>'', 'notes'=>'']]]],
'calendar' => ['endpoint_action' => 'ingest_structured', 'body' => ['type'=>'calendar', 'items'=>[['title'=>'', 'start'=>'', 'end'=>'', 'location'=>'', 'notes'=>'']]]],
'notes' => ['endpoint_action' => 'ingest_structured', 'body' => ['type'=>'note', 'items'=>[['title'=>'', 'body'=>'', 'folder'=>'']]]],
'calls' => ['endpoint_action' => 'ingest_structured', 'body' => ['type'=>'call', 'items'=>[['name'=>'', 'number'=>'', 'duration'=>0, 'direction'=>'incoming|outgoing|missed', 'date'=>'']]]],
'health' => ['endpoint_action' => 'ingest_structured', 'body' => ['type'=>'health', 'items'=>[['metric'=>'', 'value'=>'', 'unit'=>'', 'date'=>'']]]]
]
], JSON_PRETTY_PRINT);
exit;
}
echo json_encode(['ok'=>false, 'error'=>'unknown action', 'available'=>['status','ingest_photo','ingest_structured','drill','list','recommendations','entities','tasks','alerts','mark_done','resolve_alert','stats_timeline','delete_item','search','shortcut_manifest']]);