494 lines
25 KiB
PHP
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']]);
|