SIMON Master Console Rebuild
True command center for SIMON + WEB360
This rebuild gives you a cleaner production spine: interactive tree, live viewer, guarded editor, dynamic graphics, architecture map, and note storage. It is designed to sit beside your existing
console.php
and become the stronger replacement path.
Dashboard
Tree
Viewer
System Map
File Registry
Notes
Files
15,524
Folders
438
Scanned Size
3.85 GB
PHP Files
996
Editable Text Files
12,771
File viewer
guarded to /htdocs
/si/operations/dash.php
<?php declare(strict_types=1); date_default_timezone_set('America/Chicago'); ob_start(static function(string $html):string{ $html=str_replace('</head>','<link rel="stylesheet" href="portal-v15.css?v=15"><link rel="stylesheet" href="portal-v16.css?v=17"></head>',$html); return str_replace('</body>','<script src="portal-v15.js?v=15" defer></script><script src="portal-v16.js?v=17" defer></script></body>',$html); }); error_reporting(E_ALL);ini_set('display_errors','1');ini_set('log_errors','1'); register_shutdown_function(static function():void{$e=error_get_last();if(!$e||!in_array($e['type'],[E_ERROR,E_PARSE,E_CORE_ERROR,E_COMPILE_ERROR],true))return;http_response_code(500);echo '<!doctype html><meta name="viewport" content="width=device-width"><style>body{margin:0;background:#070a12;color:#eef5ff;font:16px system-ui;padding:32px}.box{max-width:850px;margin:auto;background:#12192a;border:1px solid #ff718b;border-radius:20px;padding:24px}code{color:#ffb8c4;word-break:break-word}</style><div class="box"><h1>SI Dashboard Error</h1><p>The dashboard stopped before rendering. This message replaces the blank white screen.</p><p><b>Error:</b> <code>'.htmlspecialchars((string)$e['message'],ENT_QUOTES,'UTF-8').'</code></p><p><b>File:</b> <code>'.htmlspecialchars(basename((string)$e['file']),ENT_QUOTES,'UTF-8').' line '.(int)$e['line'].'</code></p></div>';}); require_once __DIR__ . '/bootstrap.php'; require_once dirname(__DIR__) . '/_core/memory.php'; require_once dirname(__DIR__) . '/_core/knowledge.php'; if(!function_exists('si_ops_contacts')){function si_ops_contacts():array{return [];}} if(!function_exists('si_ops_contact_index')){function si_ops_contact_index(array $rows):array{return [];}} if(!function_exists('si_ops_tasks_load')){function si_ops_tasks_load():array{return [];}} if(!function_exists('si_ops_tasks_save')){function si_ops_tasks_save(array $rows):bool{return false;}} if(!function_exists('si_memory_contact_count')){function si_memory_contact_count():int{return 0;}} function si_priority_state_file():string{return si_ops_private_root().'/memory/si_priority_state.json';} function si_priority_state_load():array{$file=si_priority_state_file();$rows=is_readable($file)?json_decode((string)file_get_contents($file),true):[];return is_array($rows)?$rows:[];} function si_priority_state_save(array $rows):bool{$file=si_priority_state_file();if(!is_dir(dirname($file)))@mkdir(dirname($file),0700,true);return file_put_contents($file,json_encode($rows,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES),LOCK_EX)!==false;} function si_calendar_file():string{return si_ops_private_root().'/memory/si_calendar_events.json';} function si_calendar_load():array{$file=si_calendar_file();$rows=is_readable($file)?json_decode((string)file_get_contents($file),true):[];return is_array($rows)?array_values(array_filter($rows,'is_array')):[];} function si_calendar_save(array $rows):bool{$file=si_calendar_file();if(!is_dir(dirname($file)))@mkdir(dirname($file),0700,true);return file_put_contents($file,json_encode(array_values($rows),JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES),LOCK_EX)!==false;} function si_portal_settings_file():string{return si_ops_private_root().'/memory/si_portal_settings.json';} function si_portal_settings_defaults():array{return ['greeting'=>'Thank you for calling Gaylord Sinclair and SIMON Enterprise. I am SI, Director of Operations.','voice'=>'Polly.Matthew','business_pronunciation'=>'Gaylord Sinclair and Simon Enterprise','speech_language'=>'en-US','greeting_max_seconds'=>30,'silence_timeout'=>6,'misunderstood_retries'=>2,'transfer_confidence'=>80,'answer_without_keith'=>true,'approved_subjects'=>'Gaylord Sinclair; SIMON Enterprise; general business information; scheduling','escalation_subjects'=>'Legal threats; emergencies; financial commitments; personal matters','after_hours_greeting'=>'You have reached us after hours. SI can take a message or schedule a callback.','holiday_greeting'=>'Our office is observing a holiday. SI can take a message.','ai_failure_message'=>'SI is temporarily unavailable. Please leave a message.','disable_ai'=>false,'recording_default'=>false,'recording_consent'=>true,'dual_channel'=>true,'recording_callback'=>true,'automatic_transcription'=>true,'ai_voicemail_summary'=>true,'voicemail_max_seconds'=>180,'voicemail_silence_seconds'=>7,'recording_retention_days'=>90,'delete_audio_keep_summary'=>false,'urgent_voicemail_alert'=>'immediate','dial_keith_first'=>true,'outgoing_caller_id'=>'','answering_machine_detection'=>true,'leave_voicemail'=>false,'retry_unanswered'=>true,'maximum_retries'=>2,'record_outgoing'=>false,'confirm_international'=>true,'approved_destinations'=>'','alerts_channels'=>'dashboard,sms','alert_recipient_sms'=>'','alert_recipient_email'=>'admin@gaylordsinclair.com','alert_incoming_approved'=>true,'alert_missed_call'=>true,'alert_failed_transfer'=>true,'alert_new_voicemail'=>true,'alert_urgent_caller'=>true,'alert_repeated_caller'=>true,'alert_blocked_attempt'=>true,'alert_webhook_failure'=>true,'alert_phone_offline'=>true,'alert_transcription_failure'=>true,'quiet_hours'=>'22:00-07:00','daily_call_summary'=>true,'call_display_limit'=>'100','auto_refresh'=>true,'timezone'=>'America/Chicago','log_retention_days'=>365,'show_recordings'=>true,'show_errors'=>true,'mask_private_numbers'=>false,'session_timeout_minutes'=>30,'login_throttling'=>true,'two_factor_required'=>false];} function si_portal_settings_load():array{$defaults=si_portal_settings_defaults();$file=si_portal_settings_file();$saved=is_readable($file)?json_decode((string)file_get_contents($file),true):[];return array_replace($defaults,is_array($saved)?array_intersect_key($saved,$defaults):[]);} function si_portal_settings_save(array $settings):bool{$file=si_portal_settings_file();if(!is_dir(dirname($file)))@mkdir(dirname($file),0700,true);return file_put_contents($file,json_encode($settings,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES),LOCK_EX)!==false;} function si_duplicate_submission(string $type,array $payload,int $window=30):bool{$key=hash('sha256',$type.'|'.json_encode($payload));$now=time();$last=(int)($_SESSION['si_submission_guard'][$key]??0);$_SESSION['si_submission_guard']=array_filter((array)($_SESSION['si_submission_guard']??[]),fn($t)=>$now-(int)$t<300);if($last>0&&$now-$last<$window)return true;$_SESSION['si_submission_guard'][$key]=$now;return false;} function si_area_region(string $number):string{$digits=preg_replace('/\D/','',$number)??'';if(strlen($digits)===11&&$digits[0]==='1')$digits=substr($digits,1);$area=substr($digits,0,3);$map=['214'=>'Dallas, TX','469'=>'Dallas–Fort Worth, TX','972'=>'Dallas–Fort Worth, TX','945'=>'Dallas–Fort Worth, TX','817'=>'Fort Worth, TX','682'=>'Fort Worth, TX','903'=>'Northeast Texas','430'=>'Northeast Texas','512'=>'Austin, TX','737'=>'Austin, TX','713'=>'Houston, TX','281'=>'Houston, TX','832'=>'Houston, TX','346'=>'Houston, TX','210'=>'San Antonio, TX','726'=>'San Antonio, TX','501'=>'Central Arkansas','870'=>'Arkansas'];return isset($map[$area])?'Area code '.$area.' · '.$map[$area].' (approximate)':'Area code '.$area.' · region not locally mapped';} session_name('SI_OPS'); if (session_status() !== PHP_SESSION_ACTIVE) session_start(['cookie_httponly'=>true,'cookie_secure'=>true,'cookie_samesite'=>'Strict']); $error=''; $notice=''; if (!isset($_SESSION['csrf'])) $_SESSION['csrf']=bin2hex(random_bytes(32)); if (isset($_POST['access_key'])) { $key=defined('SI_SMS_DASHBOARD_KEY')?(string)SI_SMS_DASHBOARD_KEY:''; $entered=trim((string)$_POST['access_key']); $configuredMatch=$key!=='' && hash_equals($key,$entered); if ($configuredMatch) { session_regenerate_id(true); $_SESSION['si_ops']=true; } else $error='Access key not accepted.'; } if (isset($_POST['logout'])) { $_SESSION=[]; session_destroy(); header('Location: dashboard.php'); exit; } $authorized=($_SESSION['si_ops']??false)===true; $portalSettings=$authorized?si_portal_settings_load():si_portal_settings_defaults(); $callerIds=[];if(defined('TWILIO_PHONE_NUMBER'))$callerIds[]=(string)TWILIO_PHONE_NUMBER;if(defined('SI_OUTBOUND_CALLER_IDS')&&is_array(SI_OUTBOUND_CALLER_IDS))foreach(SI_OUTBOUND_CALLER_IDS as $cid)$callerIds[]=(string)$cid;$callerIds=array_values(array_unique(array_filter($callerIds,fn($n)=>si_ops_e164($n)!==''))); if ($authorized && ($_SERVER['REQUEST_METHOD']??'GET')==='POST') { $isSessionAction=isset($_POST['logout'])||isset($_POST['access_key']); if(!$isSessionAction&&!hash_equals((string)($_SESSION['csrf']??''),(string)($_POST['csrf']??''))){$error='Request expired. Refresh the page and try again.';} elseif(isset($_POST['save_settings'])){$defaults=si_portal_settings_defaults();$input=is_array($_POST['settings']??null)?$_POST['settings']:[];$booleanKeys=['answer_without_keith','disable_ai','recording_default','recording_consent','dual_channel','recording_callback','automatic_transcription','ai_voicemail_summary','delete_audio_keep_summary','dial_keith_first','answering_machine_detection','leave_voicemail','retry_unanswered','record_outgoing','confirm_international','alert_incoming_approved','alert_missed_call','alert_failed_transfer','alert_new_voicemail','alert_urgent_caller','alert_repeated_caller','alert_blocked_attempt','alert_webhook_failure','alert_phone_offline','alert_transcription_failure','daily_call_summary','auto_refresh','show_recordings','show_errors','mask_private_numbers','login_throttling','two_factor_required'];$integerKeys=['greeting_max_seconds','silence_timeout','misunderstood_retries','transfer_confidence','voicemail_max_seconds','voicemail_silence_seconds','recording_retention_days','maximum_retries','log_retention_days','session_timeout_minutes'];$clean=[];foreach($defaults as $key=>$default){$value=$input[$key]??null;if(in_array($key,$booleanKeys,true))$clean[$key]=(string)$value==='1';elseif(in_array($key,$integerKeys,true))$clean[$key]=max(0,min(100000,(int)$value));else{$text=trim((string)$value);$clean[$key]=function_exists('mb_substr')?mb_substr($text,0,5000):substr($text,0,5000);}}if(si_portal_settings_save($clean)){$portalSettings=$clean;$notice='Private portal settings saved.';}else$error='Settings could not be saved.';} elseif(isset($_POST['priority_action'])){$priorityId=preg_replace('/[^a-f0-9]/','',(string)($_POST['priority_id']??''))??'';$action=(string)($_POST['priority_action']??'');$allowed=['acknowledged','in_progress','resolved'];if($priorityId===''||!in_array($action,$allowed,true))$error='Invalid priority transition.';else{$priority=si_priority_state_load();$priority[$priorityId]=['status'=>$action,'updated_at'=>gmdate('c')];si_priority_state_save($priority)?$notice='Priority moved to '.str_replace('_',' ',$action).'.':$error='Priority state could not be saved.';}} elseif(isset($_POST['add_calendar_event'])){$title=trim((string)($_POST['event_title']??''));$startText=trim((string)($_POST['event_start']??''));$type=preg_replace('/[^a-z_]/','',(string)($_POST['event_type']??'event'))?:'event';$contact=si_ops_e164((string)($_POST['event_contact']??''));$notes=trim((string)($_POST['event_notes']??''));try{$local=new DateTimeImmutable($startText,new DateTimeZone('America/Chicago'));}catch(Throwable){$local=false;}if($title===''||!$local)$error='Enter an event title and valid Central date/time.';elseif(si_duplicate_submission('calendar',['title'=>$title,'start'=>$startText,'contact'=>$contact],30))$error='Duplicate calendar event stopped.';else{$calendar=si_calendar_load();$calendar[]=['id'=>bin2hex(random_bytes(8)),'title'=>substr($title,0,160),'start_at'=>$local->setTimezone(new DateTimeZone('UTC'))->format('c'),'timezone'=>'America/Chicago','type'=>$type,'contact_number'=>$contact,'notes'=>substr($notes,0,1000),'status'=>'scheduled','created_at'=>gmdate('c'),'updated_at'=>gmdate('c')];si_calendar_save($calendar)?$notice='Calendar event scheduled.':$error='Calendar event could not be saved.';}} elseif(isset($_POST['add_task'])){$taskText=trim((string)($_POST['task_text']??''));if($taskText==='')$error='Enter a task.';else{$tasksNow=si_ops_tasks_load();$tasksNow[]=['id'=>bin2hex(random_bytes(8)),'type'=>'task','text'=>$taskText,'status'=>'open','created_at'=>gmdate('c')];si_ops_tasks_save($tasksNow)?$notice='Task added.':$error='Task could not be saved.';}} elseif(isset($_POST['resolve_task'])){$taskId=(string)($_POST['resolve_task']??'');$tasksNow=si_ops_tasks_load();foreach($tasksNow as &$task){if((string)($task['id']??'')===$taskId){$task['status']='completed';$task['completed_at']=gmdate('c');}}unset($task);si_ops_tasks_save($tasksNow);$notice='Task completed.';} elseif(isset($_POST['block_number'])||isset($_POST['unblock_number'])){$number=si_ops_e164((string)($_POST['phone']??''));$blocked=si_blocked_numbers();if(isset($_POST['block_number'])&&$number!==''&&!in_array($number,$blocked,true))$blocked[]=$number;if(isset($_POST['unblock_number']))$blocked=array_values(array_filter($blocked,fn($n)=>$n!==$number));si_save_blocked_numbers($blocked);$notice=isset($_POST['block_number'])?'Number blocked.':'Number unblocked.';} elseif(isset($_POST['lookup_number'])){$number=si_ops_e164((string)$_POST['lookup_number']);$lookup=si_lookup_number($number);$lookup['ok']?$notice='Caller ID lookup completed for '.$number.'.':$error=(string)$lookup['error'];} elseif(isset($_POST['ask_si'])){$question=trim((string)($_POST['si_question']??''));if($question==='')$error='Ask SI a question.';else{$reply=si_ask($question);$_SESSION['si_chat'][]=['question'=>$question,'answer'=>$reply['answer'],'time'=>gmdate('c')];$_SESSION['si_chat']=array_slice((array)$_SESSION['si_chat'],-20);$reply['ok']?$notice='SI responded.':$error=$reply['answer'];}} elseif(isset($_POST['clear_voicemail'])||isset($_POST['archive_voicemail'])){$raw=(string)($_POST['clear_voicemail']??$_POST['archive_voicemail']??'');$id=preg_replace('/[^a-f0-9]/','',$raw)??'';$file=si_ops_private_root().'/memory/si_voicemail_state.json';$state=is_readable($file)?json_decode((string)file_get_contents($file),true):[];if(!is_array($state))$state=[];$archive=isset($_POST['archive_voicemail']);$state[$id]=array_merge((array)($state[$id]??[]),$archive?['archived'=>true,'archived_at'=>gmdate('c')]:['cleared'=>true,'cleared_at'=>gmdate('c')]);if(!is_dir(dirname($file)))@mkdir(dirname($file),0700,true);file_put_contents($file,json_encode($state,JSON_PRETTY_PRINT),LOCK_EX);$notice=$archive?'Voicemail archived.':'Voicemail resolved.';} elseif (isset($_POST['send_sms'])) { $to=si_ops_e164((string)($_POST['to']??'')); $body=trim((string)($_POST['message']??'')); if ($to===''||$body==='') $error='Enter a valid number and message.'; elseif(si_duplicate_submission('sms',['to'=>$to,'body'=>$body],30))$error='Duplicate text stopped. The same message to this number was already submitted within 30 seconds.'; elseif($to===si_ops_e164((string)TWILIO_PHONE_NUMBER)){$error='SMS failed: the recipient is the same as the Twilio sending number. Send the test to a different mobile number.';si_core_log('sms','outbound_sms_failed',['from'=>(string)TWILIO_PHONE_NUMBER,'to'=>$to,'body'=>$body,'status'=>'failed','error'=>'From and To are the same Twilio number.']);} else { $smsBody=function_exists('mb_substr')?mb_substr($body,0,1500):substr($body,0,1500); $r=si_ops_twilio('Messages',['To'=>$to,'From'=>(string)TWILIO_PHONE_NUMBER,'Body'=>$smsBody,'StatusCallback'=>si_ops_base_url().'/si/sms/status.php']); $data=(array)($r['data']??[]);$sid=(string)($data['sid']??'');$status=(string)($data['status']??($r['ok']?'submitted':'failed'));if($r['ok']){$_SESSION['si_twilio_auth_mode']=(string)($r['auth']??'Twilio');si_core_log('sms','outbound_sms',['from'=>(string)TWILIO_PHONE_NUMBER,'to'=>$to,'body'=>$smsBody,'status'=>$status,'message_sid'=>$sid,'auth_mode'=>$r['auth']??'']);$notice='Text accepted by Twilio · status '.strtoupper($status).($sid!==''?' · SID '.$sid:'').'.';}else{si_core_log('sms','outbound_sms_failed',['from'=>(string)TWILIO_PHONE_NUMBER,'to'=>$to,'body'=>$smsBody,'status'=>'failed','error'=>(string)$r['error']]);$error='SMS failed: '.$r['error'];} } } elseif (isset($_POST['place_call'])) { $target=si_ops_e164((string)($_POST['call_to']??'')); $operator=defined('SI_OPERATOR_MOBILE_NUMBER')?si_ops_e164((string)SI_OPERATOR_MOBILE_NUMBER):(defined('SI_KEITH_DIRECT_NUMBER')?si_ops_e164((string)SI_KEITH_DIRECT_NUMBER):''); $callerId=si_ops_e164((string)($_POST['caller_id']??($callerIds[0]??'')));if(!in_array($callerId,$callerIds,true))$callerId=''; if ($target===''||$callerId==='') $error='The destination or caller ID is unavailable.'; elseif($operator==='')$error='Set SI_OPERATOR_MOBILE_NUMBER to Keith’s real mobile number. Do not use the public forwarding number or the Twilio number.'; elseif($operator===si_ops_e164((string)TWILIO_PHONE_NUMBER))$error='The operator number cannot be the Twilio number; that would create a call loop.'; elseif(si_duplicate_submission('call',['target'=>$target,'operator'=>$operator,'caller_id'=>$callerId],30))$error='Duplicate call stopped. This call was already submitted within 30 seconds.'; else { $url=si_ops_base_url().'/si/operations/bridge.php?target='.rawurlencode($target).'&caller_id='.rawurlencode($callerId); $r=si_ops_twilio('Calls',['To'=>$operator,'From'=>$callerId,'Url'=>$url,'Method'=>'POST','StatusCallback'=>si_ops_base_url().'/si/phone/status.php','StatusCallbackMethod'=>'POST','StatusCallbackEvent'=>'initiated ringing answered completed']);$data=(array)($r['data']??[]);$sid=(string)($data['sid']??'');$status=(string)($data['status']??($r['ok']?'queued':'failed'));if($r['ok']){$_SESSION['si_twilio_auth_mode']=(string)($r['auth']??'Twilio');si_core_log('phone','outbound_call_requested',['from'=>$callerId,'to'=>$target,'operator'=>$operator,'status'=>$status,'call_sid'=>$sid,'auth_mode'=>$r['auth']??'']);$notice='Call accepted by Twilio · status '.strtoupper($status).($sid!==''?' · SID '.$sid:'').'. Twilio is calling you first.';}else{si_core_log('phone','outbound_call_failed',['from'=>$callerId,'to'=>$target,'operator'=>$operator,'status'=>'failed','error'=>(string)$r['error']]);$error='Call failed: '.$r['error'];} } } elseif (isset($_POST['refresh_twilio'])) { $snapshot=function_exists('si_ops_twilio_snapshot')?si_ops_twilio_snapshot():['calls'=>[],'messages'=>[],'recordings'=>[],'errors'=>['Cloud lookup requires the current operations/bootstrap.php.']]; $_SESSION['si_twilio_snapshot']=$snapshot; $aiTest=si_ask('Reply with exactly: SI ONLINE');$_SESSION['si_ai_verified']=$aiTest['ok'];$_SESSION['si_ai_test_message']=$aiTest['answer']; $snapshot['errors']?$error=implode(' ',array_unique((array)$snapshot['errors'])):$notice='Twilio and SI diagnostics completed.'; } } $cloud=$authorized?(array)($_SESSION['si_twilio_snapshot']??[]):[];$cloudCalls=(array)($cloud['calls']??[]);$cloudMessages=(array)($cloud['messages']??[]);$cloudRecordings=(array)($cloud['recordings']??[]); $calls=$authorized?si_ops_log_rows('si_phone'):[]; $texts=$authorized?si_ops_log_rows('si_sms'):[];function si_nonempty(mixed ...$values):string{foreach($values as $value){if(is_scalar($value)&&trim((string)$value)!=='')return trim((string)$value);}return '';} $callPartyIndex=[];foreach(array_merge($calls,$cloudCalls) as $row){$d=(array)($row['details']??[]);$sid=si_nonempty($row['call_sid']??'', $row['sid']??'', $d['call_sid']??'');if($sid==='')continue;$from=si_nonempty($row['from']??'', $d['from']??'', $d['caller']??'');$to=si_nonempty($row['to']??'', $d['to']??'', $d['destination']??'');$status=si_nonempty($row['status']??'', $d['status']??'', $d['dial_status']??'');$callPartyIndex[$sid]??=['from'=>'','to'=>'','status'=>''];if($from!=='')$callPartyIndex[$sid]['from']=$from;if($to!=='')$callPartyIndex[$sid]['to']=$to;if($status!=='')$callPartyIndex[$sid]['status']=$status;}foreach($calls as &$row){$d=(array)($row['details']??[]);$sid=si_nonempty($row['call_sid']??'', $d['call_sid']??'');if($sid!==''&&isset($callPartyIndex[$sid])){$row['from']=si_nonempty($row['from']??'', $d['from']??'', $d['caller']??'', $callPartyIndex[$sid]['from']);$row['to']=si_nonempty($row['to']??'', $d['to']??'', $d['destination']??'', $callPartyIndex[$sid]['to']);$row['status']=si_nonempty($row['status']??'', $d['status']??'', $d['dial_status']??'', $callPartyIndex[$sid]['status']);}}unset($row); $messagePartyIndex=[];foreach(array_merge($texts,$cloudMessages) as $row){$d=(array)($row['details']??[]);$sid=si_nonempty($row['message_sid']??'', $row['sid']??'', $d['message_sid']??'');if($sid==='')continue;$from=si_nonempty($row['from']??'', $d['from']??'');$to=si_nonempty($row['to']??'', $d['to']??'');$body=si_nonempty($row['body']??'', $d['body']??'', $d['message']??'');$status=si_nonempty($row['status']??'', $d['status']??'', $d['message_status']??'');$messagePartyIndex[$sid]??=['from'=>'','to'=>'','body'=>'','status'=>''];foreach(['from','to','body','status'] as $key)if($$key!=='')$messagePartyIndex[$sid][$key]=$$key;}foreach($texts as &$row){$d=(array)($row['details']??[]);$sid=si_nonempty($row['message_sid']??'', $d['message_sid']??'');if($sid!==''&&isset($messagePartyIndex[$sid])){$row['details']=$d;$row['from']=si_nonempty($row['from']??'', $d['from']??'', $messagePartyIndex[$sid]['from']);$row['to']=si_nonempty($row['to']??'', $d['to']??'', $messagePartyIndex[$sid]['to']);$row['status']=si_nonempty($row['status']??'', $d['status']??'', $messagePartyIndex[$sid]['status']);if(empty($row['details']['body']))$row['details']['body']=$messagePartyIndex[$sid]['body'];}}unset($row); $contacts=$authorized?si_ops_contacts():[];$rawContacts=$contacts;$contactGroups=[];foreach($contacts as $contact){if(!is_array($contact))continue;$number=si_ops_e164((string)($contact['number']??$contact['phone']??''));$name=trim((string)($contact['name']??$contact['first_name']??'Contact'));$key=strtolower(preg_replace('/\s+/',' ',$name)??$name);if($key==='contact'||$key==='')$key=$number;if($key==='')continue;if(!isset($contactGroups[$key])){$contactGroups[$key]=$contact;$contactGroups[$key]['name']=$name;$contactGroups[$key]['numbers']=[];}if($number!=='')$contactGroups[$key]['numbers'][$number]=['number'=>$number,'label'=>(string)($contact['label']??$contact['role']??'Phone')];foreach($contact as $field=>$value)if((!isset($contactGroups[$key][$field])||$contactGroups[$key][$field]==='')&&$value!=='')$contactGroups[$key][$field]=$value;}$contacts=array_values($contactGroups);$contactIndex=si_ops_contact_index($rawContacts);$lookupCache=$authorized?si_lookup_cache():[];$siChat=$authorized?(array)($_SESSION['si_chat']??[]):[];$priorityState=$authorized?si_priority_state_load():[]; $events=[]; foreach($calls as $r){$r['_channel']='call';$events[]=$r;} foreach($texts as $r){$r['_channel']='text';$events[]=$r;} usort($events,function($a,$b){return strcmp((string)($b['time']??''),(string)($a['time']??''));}); $events=array_slice($events,0,100); $voicemails=array_values(array_filter($calls,function($r){$event=(string)($r['event']??'');$d=(array)($r['details']??[]);return $event==='voicemail_saved'&&!empty($d['recording_sid']);}));foreach($cloudRecordings as $recording){$sid=(string)($recording['sid']??'');if($sid==='')continue;$known=false;foreach($voicemails as $existing){if((string)(($existing['details']??[])['recording_sid']??'')===$sid){$known=true;break;}}if($known)continue;$callSid=(string)($recording['call_sid']??'');$parties=(array)($callPartyIndex[$callSid]??[]);$voicemails[]=['time'=>(string)($recording['date_created']??$recording['date_updated']??''),'event'=>'cloud_voicemail_recording','call_sid'=>$callSid,'from'=>(string)($parties['from']??''),'to'=>(string)($parties['to']??''),'details'=>['recording_sid'=>$sid,'recording_url'=>'recording-media.php?sid='.rawurlencode($sid),'download_url'=>'recording-media.php?sid='.rawurlencode($sid).'&download=1','duration_seconds'=>(string)($recording['duration']??''),'status'=>(string)($recording['status']??'completed'),'caller'=>(string)($parties['from']??'')]];} foreach($voicemails as &$vm){$vmd=(array)($vm['details']??[]);$callSid=si_nonempty($vm['call_sid']??'', $vmd['call_sid']??'');$linked=(array)($callPartyIndex[$callSid]??[]);$vmd['caller']=si_nonempty($vmd['caller']??'', $vm['from']??'', $linked['from']??'');if($vmd['caller']===''&&$callSid!=='')$vmd['caller']='Unknown caller · Call '.$callSid;$vm['details']=$vmd;}unset($vm);$vmStateFile=si_ops_private_root().'/memory/si_voicemail_state.json';$vmState=is_readable($vmStateFile)?json_decode((string)file_get_contents($vmStateFile),true):[];if(!is_array($vmState))$vmState=[];$activeVoicemails=[];$archivedVoicemails=[];foreach($voicemails as $vm){$vmd=(array)($vm['details']??[]);$vmid=hash('sha256',(string)($vmd['recording_sid']??$vmd['recording_url']??''));if(($vmState[$vmid]['archived']??false)===true)$archivedVoicemails[]=$vm;else$activeVoicemails[]=$vm;} $missed=array_values(array_filter($calls,function($r){$d=(array)($r['details']??[]);return in_array(strtolower((string)($d['dial_status']??$d['status']??'')),['busy','failed','no-answer','canceled'],true);})); $tasks=$authorized?si_ops_tasks_load():[]; $twilioReady=defined('TWILIO_ACCOUNT_SID')&&defined('TWILIO_PHONE_NUMBER'); $aiReady=defined('OPENAI_API_KEY')&&!str_contains((string)OPENAI_API_KEY,'PASTE_'); $memoryCount=si_memory_contact_count(); function h(string $v):string{return htmlspecialchars($v,ENT_QUOTES,'UTF-8');} function si_lookup_file():string{return si_ops_private_root().'/memory/si_number_lookup.json';}function si_lookup_cache():array{$f=si_lookup_file();$r=is_readable($f)?json_decode((string)file_get_contents($f),true):[];return is_array($r)?$r:[];}function si_lookup_number(string $number):array{if($number==='')return ['ok'=>false,'error'=>'Invalid phone number.'];$sid=defined('TWILIO_ACCOUNT_SID')?(string)TWILIO_ACCOUNT_SID:'';$user=defined('TWILIO_API_KEY_SID')?(string)TWILIO_API_KEY_SID:$sid;$pass=defined('TWILIO_API_KEY_SECRET')?(string)TWILIO_API_KEY_SECRET:(defined('TWILIO_AUTH_TOKEN')?(string)TWILIO_AUTH_TOKEN:'');if($sid===''||$pass===''||!function_exists('curl_init'))return ['ok'=>false,'error'=>'Twilio Lookup credentials are unavailable.'];$url='https://lookups.twilio.com/v2/PhoneNumbers/'.rawurlencode($number).'?Fields=caller_name,line_type_intelligence';$c=curl_init($url);curl_setopt_array($c,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_USERPWD=>$user.':'.$pass,CURLOPT_HTTPAUTH=>CURLAUTH_BASIC,CURLOPT_TIMEOUT=>20]);$body=curl_exec($c);$code=(int)curl_getinfo($c,CURLINFO_RESPONSE_CODE);$curlError=curl_error($c);curl_close($c);$data=is_string($body)?json_decode($body,true):null;if($code<200||$code>=300||!is_array($data))return ['ok'=>false,'error'=>is_array($data)?(string)($data['message']??'Lookup failed.'):($curlError?:'Lookup failed.')];$cache=si_lookup_cache();$cache[preg_replace('/\D/','',$number)??$number]=['time'=>gmdate('c'),'valid'=>$data['valid']??null,'national_format'=>$data['national_format']??'','caller_name'=>$data['caller_name']['caller_name']??$data['caller_name']['name']??'','caller_type'=>$data['caller_name']['caller_type']??'','line_type'=>$data['line_type_intelligence']['type']??'','carrier'=>$data['line_type_intelligence']['carrier_name']??''];$f=si_lookup_file();if(!is_dir(dirname($f)))@mkdir(dirname($f),0700,true);file_put_contents($f,json_encode($cache,JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES),LOCK_EX);return ['ok'=>true,'data'=>$cache[preg_replace('/\D/','',$number)??$number]];} function si_ask(string $question):array{$key=defined('OPENAI_API_KEY')?(string)OPENAI_API_KEY:'';if($key===''||str_contains($key,'PASTE_')||!function_exists('curl_init'))return ['ok'=>false,'answer'=>'SI cannot reach OpenAI because its private API configuration is unavailable.'];$model=defined('OPENAI_MODEL')?(string)OPENAI_MODEL:'gpt-5.6-luna';$knowledge=function_exists('si_knowledge_context')?si_knowledge_context():'';$input="You are SI, Director of Operations for Gaylord Sinclair and SIMON Enterprise. Be concise, honest, privacy-protecting, and distinguish known from unknown. Local company knowledge:\n".$knowledge."\n\nKeith asks: ".$question;$c=curl_init('https://api.openai.com/v1/responses');curl_setopt_array($c,[CURLOPT_POST=>true,CURLOPT_RETURNTRANSFER=>true,CURLOPT_HTTPHEADER=>['Authorization: Bearer '.$key,'Content-Type: application/json'],CURLOPT_POSTFIELDS=>json_encode(['model'=>$model,'input'=>$input,'max_output_tokens'=>800],JSON_UNESCAPED_SLASHES),CURLOPT_TIMEOUT=>45]);$body=curl_exec($c);$code=(int)curl_getinfo($c,CURLINFO_RESPONSE_CODE);$curlError=curl_error($c);curl_close($c);$data=is_string($body)?json_decode($body,true):null;if($code<200||$code>=300||!is_array($data))return ['ok'=>false,'answer'=>is_array($data)?(string)($data['error']['message']??'SI request failed.'):($curlError?:'SI request failed.')];$answer='';foreach((array)($data['output']??[]) as $item)foreach((array)($item['content']??[]) as $content)if(($content['type']??'')==='output_text')$answer.=(string)($content['text']??'');return ['ok'=>$answer!=='','answer'=>$answer!==''?$answer:'SI returned no text.'];} function si_block_file():string{return si_ops_private_root().'/memory/si_sms_blocks.json';}function si_blocked_numbers():array{$f=si_block_file();$r=is_readable($f)?json_decode((string)file_get_contents($f),true):[];return is_array($r)?array_values(array_filter($r,'is_string')):[];}function si_save_blocked_numbers(array $rows):bool{$f=si_block_file();if(!is_dir(dirname($f)))@mkdir(dirname($f),0700,true);return file_put_contents($f,json_encode(array_values(array_unique($rows)),JSON_PRETTY_PRINT),LOCK_EX)!==false;} function si_event_info(array $r):array{$d=(array)($r['details']??[]);$from=si_nonempty($r['from']??'', $d['from']??'', $d['caller']??'', $d['From']??'');$to=si_nonempty($r['to']??'', $d['to']??'', $d['destination']??'', $d['To']??'');$status=si_nonempty($r['status']??'', $r['message_status']??'', $d['status']??'', $d['message_status']??'', $d['dial_status']??'', $r['event']??'event');$parts=[];foreach(['error','transcript','body','message','speech','recording_url'] as $key){$value=si_nonempty($r[$key]??'', $d[$key]??'');if($value!=='')$parts[]=$value;}$sid=si_nonempty($r['call_sid']??'', $r['message_sid']??'', $r['sid']??'', $d['call_sid']??'', $d['message_sid']??'');$data=implode("\n",array_unique($parts));$rawTime=si_nonempty($r['time']??'', $r['date_sent']??'', $r['date_created']??'', $r['date_updated']??'');$stamp=strtotime($rawTime);return ['from'=>$from?:'Unknown','to'=>$to?:'Unknown','status'=>$status,'data'=>$data?:'No additional event data','sid'=>$sid,'raw_time'=>$rawTime,'date'=>$stamp?date('m/d/Y',$stamp):$rawTime,'time'=>$stamp?date('g:i A',$stamp).' Central':''];} function si_contact_for_number(string $number,array $index):array{$digits=preg_replace('/\D/','',$number)??'';if(strlen($digits)===10)$digits='1'.$digits;return is_array($index[$digits]??null)?$index[$digits]:[];} function si_identity(string $number,array $contacts,array $lookups):array{$digits=preg_replace('/\D/','',$number)??'';if(strlen($digits)===10)$digits='1'.$digits;$contact=is_array($contacts[$digits]??null)?$contacts[$digits]:[];$lookup=is_array($lookups[$digits]??null)?$lookups[$digits]:[];$name=(string)($contact['name']??$contact['first_name']??$lookup['caller_name']??'');$address=trim(implode(', ',array_filter([(string)($contact['address']??$contact['street']??''),(string)($contact['city']??''),(string)($contact['state']??''),(string)($contact['zip']??$contact['postal_code']??'')])));return ['known'=>$name!=='','name'=>$name?:'Unknown caller','label'=>(string)($contact['label']??$contact['role']??$lookup['line_type']??''),'carrier'=>(string)($lookup['carrier']??''),'address'=>$address,'locality'=>(string)($lookup['locality']??$lookup['city']??''),'region'=>(string)($lookup['region']??$lookup['state']??''),'source'=>$contact?'Private directory':($lookup?'Twilio Lookup':'Unidentified')];} function si_health_item(string $label,bool $configured,bool $verified,string $detail):array{return ['label'=>$label,'state'=>$verified?'online':($configured?'warning':'offline'),'text'=>$verified?'Operational':($configured?'Configured / unverified':'Not configured'),'detail'=>$detail];} $hasTwilioAuth=(defined('TWILIO_API_KEY_SID')&&defined('TWILIO_API_KEY_SECRET'))||defined('TWILIO_AUTH_TOKEN'); $hasCloudData=count($cloudCalls)+count($cloudMessages)+count($cloudRecordings)>0; $privateRoot=si_ops_private_root();$memoryWritable=is_dir($privateRoot.'/memory')?is_writable($privateRoot.'/memory'):is_writable($privateRoot); $health=[ si_health_item('Twilio API',$twilioReady&&$hasTwilioAuth,$hasCloudData,$hasCloudData?'Cloud records received.':'Press Run All Diagnostics to verify the API.'), si_health_item('Voice webhook',defined('SI_PHONE_WEBHOOK_BASE_URL'),count($calls)>0,count($calls).' local voice events recorded.'), si_health_item('SMS webhook',defined('SI_SMS_WEBHOOK_BASE_URL'),count($texts)>0,count($texts).' local message events recorded.'), si_health_item('Recording pipeline',$twilioReady,count($cloudRecordings)>0,count($cloudRecordings).' cloud recordings loaded.'), si_health_item('OpenAI',$aiReady,($_SESSION['si_ai_verified']??false)===true,$aiReady?(($_SESSION['si_ai_verified']??false)?'SI answered the live API test.':'Key is configured; press Run All Diagnostics to test it.'):'Add OPENAI_API_KEY to private configuration.'), si_health_item('Private storage',true,$memoryWritable,$memoryWritable?'Memory directory is writable.':'Memory directory is not writable.'), si_health_item('PHP runtime',true,version_compare(PHP_VERSION,'8.1.0','>='),'PHP '.PHP_VERSION), si_health_item('cURL',function_exists('curl_init'),function_exists('curl_init'),function_exists('curl_init')?'Available':'PHP cURL extension is missing.'), ]; $healthCounts=['online'=>0,'warning'=>0,'offline'=>0];foreach($health as $check)$healthCounts[$check['state']]++; $openTasks=array_values(array_filter($tasks,fn($t)=>($t['status']??'open')!=='completed')); $threadIndex=[];foreach($events as $event){$d=(array)($event['details']??[]);$party=(string)($event['from']??$d['caller']??$d['from']??$event['to']??$d['to']??'Unknown');$digits=preg_replace('/\D/','',$party)??$party;$threadIndex[$digits]??=['party'=>$party,'events'=>[],'last'=>''];$threadIndex[$digits]['events'][]=$event;$threadIndex[$digits]['last']=(string)($event['time']??$threadIndex[$digits]['last']);}$threads=array_values($threadIndex);usort($threads,fn($a,$b)=>strcmp($b['last'],$a['last'])); $blockedNumbers=si_blocked_numbers();$smsFrom=(string)($_GET['sms_from']??'');$smsTo=(string)($_GET['sms_to']??'');$smsQ=strtolower(trim((string)($_GET['sms_q']??'')));$smsDirection=(string)($_GET['sms_direction']??'');$smsRows=array_values(array_filter($texts,function($row)use($smsFrom,$smsTo,$smsQ,$smsDirection){$i=si_event_info($row);$stamp=strtotime($i['raw_time']);$day=$stamp?date('Y-m-d',$stamp):'';$direction=str_contains(strtolower((string)($row['event']??'')),'out')?'outbound':'inbound';if($smsFrom!==''&&$day<$smsFrom)return false;if($smsTo!==''&&$day>$smsTo)return false;if($smsDirection!==''&&$smsDirection!==$direction)return false;if($smsQ!==''&&!str_contains(strtolower($i['from'].' '.$i['to'].' '.$i['data'].' '.$i['status']),$smsQ))return false;return true;})); $journalMap=[];foreach(array_merge($calls,$texts,$cloudCalls,$cloudMessages) as $row){$i=si_event_info($row);$event=strtolower((string)($row['event']??''));$twilioDirection=strtolower((string)($row['direction']??''));$channel=(str_starts_with($i['sid'],'SM')||isset($row['body'])||str_contains($event,'sms')||str_contains($event,'message'))?'text':'call';$direction=str_contains($twilioDirection,'outbound')||str_contains($event,'outbound')||str_starts_with($event,'out_')?'outbound':'inbound';$party=$direction==='outbound'?$i['to']:$i['from'];$number=si_ops_e164($party);if($number==='')continue;$identity=si_identity($number,$contactIndex,$lookupCache);$entry=['party'=>$number,'name'=>$identity['name'],'label'=>$identity['label'],'channel'=>$channel,'direction'=>$direction,'event'=>(string)($row['event']??($channel==='text'?'text_message':'call_event')),'status'=>$i['status'],'body'=>$i['data']==='No additional event data'?'':$i['data'],'sid'=>$i['sid'],'time'=>$i['raw_time'],'timestamp'=>strtotime($i['raw_time'])?:0];$key=$i['sid']!==''?$channel.'|'.$i['sid']:$channel.'|'.$number.'|'.$entry['timestamp'].'|'.hash('sha256',$entry['body'].$entry['event']);if(!isset($journalMap[$key]))$journalMap[$key]=$entry;else{foreach(['body','status','time','event'] as $field)if($entry[$field]!==''&&($journalMap[$key][$field]??'')==='')$journalMap[$key][$field]=$entry[$field];}}$journalRows=array_values($journalMap);usort($journalRows,fn($a,$b)=>(int)$a['timestamp']<=>(int)$b['timestamp']); ?><!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Gaylord Sinclair × SIMON Operations</title> <meta name="impact-site-verification" value="e8ec6683-663c-421a-a30d-f8c0fe517345"><script async src="https://www.googletagmanager.com/gtag/js?id=G-S6MEHB4BEY"></script><script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}gtag('js',new Date());gtag('config','G-S6MEHB4BEY',{page_title:'SI Spatial Operations'});window.SIMON_CONFIG=Object.freeze({GA4_ID:'G-S6MEHB4BEY',WEBHOOK:'/connlink/test1/ui/webhook_inbox.php',VISIT_STREAM:'visits',PULSE_STREAM:'default'});</script> <script> document.addEventListener('DOMContentLoaded',()=>{ const csrf=<?=json_encode((string)($_SESSION['csrf']??''),JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT)?>;document.querySelectorAll('form[method="post"]').forEach(form=>{if(!form.querySelector('[name="csrf"]')){const input=document.createElement('input');input.type='hidden';input.name='csrf';input.value=csrf;form.prepend(input)}}); const rail=document.querySelector('.rail'); if(rail){ const calls=<?=json_encode(array_slice($cloudCalls,0,20),JSON_UNESCAPED_SLASHES|JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT)?>; const card=document.createElement('div');card.className='card';card.id='twilio-cloud'; const rows=calls.length?calls.map(call=>`<div class="cloud-row"><b>${escapeHtml(call.status||'call')}</b><small>${escapeHtml(call.from||'Unknown')} → ${escapeHtml(call.to||'Unknown')} · ${escapeHtml(call.duration||'0')} sec</small></div>`).join(''):'<p style="color:var(--muted)">Press refresh to load real Twilio calls.</p>'; card.innerHTML=`<h4>Twilio cloud calls</h4><form method="post"><button name="refresh_twilio">↻ Refresh Twilio logs</button></form><div class="cloud-list">${rows}</div><p style="color:var(--muted);font-size:11px">${calls.length} recent cloud calls loaded</p>`; rail.insertBefore(card,rail.children[1]||null); } const saver=document.getElementById('screensaver');let timer;const wake=()=>{saver?.classList.remove('on');saver?.setAttribute('aria-hidden','true');clearTimeout(timer);timer=setTimeout(()=>{saver?.classList.add('on');saver?.setAttribute('aria-hidden','false')},90000)};['pointermove','pointerdown','keydown','touchstart','scroll'].forEach(event=>addEventListener(event,wake,{passive:true}));wake(); const endpoint=window.SIMON_CONFIG.WEBHOOK;const emit=(event,extra={})=>{const body=JSON.stringify({stream:window.SIMON_CONFIG.VISIT_STREAM,event,path:location.pathname,at:new Date().toISOString(),...extra});if(navigator.sendBeacon)navigator.sendBeacon(endpoint,new Blob([body],{type:'application/json'}));else fetch(endpoint,{method:'POST',headers:{'Content-Type':'application/json'},body,keepalive:true}).catch(()=>{})};emit('spatial_operations_view');addEventListener('click',event=>{const target=event.target.closest('button,a');if(target)emit('spatial_operations_action',{label:(target.textContent||target.title||'action').trim().slice(0,80)})},{passive:true}); }); function escapeHtml(value){return String(value).replace(/[&<>"']/g,char=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[char]))} </script> <style> .screensaver{position:fixed;z-index:1000;inset:0;display:none;place-items:center;background:radial-gradient(circle at center,#17245a,#02040b 66%)}.screensaver.on{display:grid}.screen-core{width:min(42vw,420px);aspect-ratio:1;border-radius:50%;display:grid;place-items:center;background:radial-gradient(circle at 35% 28%,#fff 0 2%,#62e6ff 8%,#a77bff 38%,#ff6ac2 59%,#080b25 76%);box-shadow:0 0 90px #62e6ff88,0 0 180px #a77bff66;animation:screenfloat 6s ease-in-out infinite alternate}.screen-core b{font-size:clamp(30px,7vw,78px);color:#070812;letter-spacing:.12em}.screensaver p{position:absolute;bottom:9%;color:#c5cbdf;letter-spacing:.18em}@keyframes screenfloat{to{transform:translate3d(4vw,-3vh,0) rotate(8deg) scale(1.06)}}.cloud-list{max-height:280px;overflow:auto}.cloud-row{padding:9px 0;border-bottom:1px solid var(--line)}.cloud-row small{display:block;color:var(--muted);margin-top:3px}.ops-section{position:relative;z-index:10;width:min(1480px,95vw);margin:16px auto;padding:22px}.health-grid,.thread-grid,.vm-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.health-card{padding:15px;border-radius:17px;background:#ffffff08;border:1px solid var(--line)}.health-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.health-card p{color:var(--muted);font-size:12px;line-height:1.45}.state{display:inline-flex;align-items:center;gap:7px;font-size:11px;text-transform:uppercase;letter-spacing:.08em}.state:before{content:"";width:9px;height:9px;border-radius:50%}.state-online:before{background:#65f6b2;box-shadow:0 0 14px #65f6b2}.state-warning:before{background:#ffc45d;box-shadow:0 0 14px #ffc45d}.state-offline:before{background:#ff718b;box-shadow:0 0 14px #ff718b}.summary-strip{display:flex;gap:9px;flex-wrap:wrap;margin:12px 0}.summary-strip span{padding:7px 11px;border-radius:999px;background:#ffffff09;color:var(--muted)}.thread-grid,.vm-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.thread,.vm-card{padding:15px;border-radius:17px;background:#ffffff07;border:1px solid var(--line)}.thread small,.vm-card small{display:block;color:var(--muted);margin-top:5px}.decision{border-left:3px solid var(--cyan)}.task-row{display:grid;grid-template-columns:1fr auto;gap:9px;align-items:center;padding:10px 0;border-bottom:1px solid var(--line)}.task-row button{width:auto;margin:0}.compact-form{display:grid;grid-template-columns:1fr auto;gap:9px}.compact-form button{width:auto;margin:0}.status-row .dot.online{background:#65f6b2}.status-row .dot.warning{background:#ffc45d;box-shadow:0 0 15px #ffc45d}.status-row .dot.offline{background:#ff718b;box-shadow:0 0 15px #ff718b}@media(max-width:1050px){.health-grid{grid-template-columns:repeat(2,1fr)}.thread-grid,.vm-grid{grid-template-columns:1fr 1fr}}@media(max-width:720px){.ops-section{width:calc(100% - 24px);padding:15px}.health-grid,.thread-grid,.vm-grid{grid-template-columns:1fr}.compact-form{grid-template-columns:1fr}.compact-form button{width:100%}} :root{color-scheme:dark;--bg:#050711;--panel:#101426cc;--line:#ffffff18;--text:#f7f8ff;--muted:#99a2bf;--cyan:#62e6ff;--violet:#a77bff;--pink:#ff6ac2;--ok:#65f6b2;--bad:#ff718b}*{box-sizing:border-box}body{margin:0;min-height:100vh;background:#000;color:var(--text);font:15px Inter,ui-sans-serif,system-ui,-apple-system,sans-serif}.sphere-bg{position:fixed;inset:0;z-index:0;pointer-events:none;opacity:.62}.sphere-bg canvas{display:block;position:fixed;inset:0;pointer-events:none}.sphere-bg #c0{z-index:0}.sphere-bg #c1{z-index:1}.sphere-bg #c2{z-index:2}.sphere-shade{position:fixed;inset:0;z-index:3;pointer-events:none;background:radial-gradient(circle at center,transparent 0,#03050b22 38%,#03050bcc 100%)}.orb{position:fixed;z-index:4;border-radius:50%;filter:blur(1px);opacity:.2;pointer-events:none}.o1{width:260px;height:260px;background:#29d9ff33;right:9%;top:22%;box-shadow:0 0 90px #29d9ff66}.o2{width:190px;height:190px;background:#bd55ff33;left:6%;bottom:8%;box-shadow:0 0 90px #bd55ff55}.shell,.login{z-index:10;position:relative}.shell{width:min(1480px,95vw);margin:28px auto}.top{display:flex;justify-content:space-between;align-items:center;margin-bottom:18px}.brand{display:flex;align-items:center;gap:14px}.mark{width:52px;height:52px;border-radius:18px;background:linear-gradient(135deg,var(--cyan),var(--violet),var(--pink));box-shadow:0 12px 45px #6b7dff66;display:grid;place-items:center;color:#080a13;font-size:22px;font-weight:900}.brand h1{margin:0;font-size:clamp(24px,3vw,40px)}.brand p{margin:3px 0 0;color:var(--muted)}.glass{background:linear-gradient(135deg,#11172ee6,#080c19df);border:1px solid var(--line);box-shadow:0 26px 80px #0009,inset 0 1px #ffffff12;backdrop-filter:blur(24px);border-radius:26px}.layout{display:grid;grid-template-columns:290px minmax(0,1fr) 360px;gap:16px}.side,.rail{padding:18px}.main{padding:20px;min-height:760px}.nav{display:grid;gap:8px}.nav a{padding:13px 14px;border-radius:14px;color:var(--muted);text-decoration:none}.nav a.active,.nav a:hover{color:var(--text);background:#ffffff0c}.status{margin-top:28px}.status-row{display:flex;justify-content:space-between;align-items:center;padding:10px 4px;color:var(--muted)}.dot{width:9px;height:9px;border-radius:50%;display:inline-block;background:var(--ok);box-shadow:0 0 15px var(--ok)}.hero{padding:22px;border-radius:22px;background:linear-gradient(120deg,#182a50cc,#281b4bcc);border:1px solid var(--line);display:flex;justify-content:space-between;align-items:center}.hero h2{font-size:28px;margin:0 0 6px}.hero p{margin:0;color:var(--muted)}.pulse{width:78px;height:78px;border-radius:50%;display:grid;place-items:center;background:#ffffff0b;border:1px solid #ffffff1a;box-shadow:0 0 0 14px #6ee7ff08,0 0 55px #6ee7ff3d;font-size:30px}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:16px 0}.metric{padding:16px}.metric span{color:var(--muted);font-size:12px}.metric strong{display:block;font-size:25px;margin-top:7px}.section-head{display:flex;justify-content:space-between;align-items:center;margin:24px 0 10px}.section-head h3{margin:0}.feed{display:grid;gap:9px}.event{display:grid;grid-template-columns:42px 1fr auto;gap:12px;align-items:center;padding:13px;border-radius:16px;background:#ffffff07;border:1px solid #ffffff0d}.icon{width:40px;height:40px;border-radius:13px;display:grid;place-items:center;background:linear-gradient(135deg,#54ddff22,#b56eff22);color:var(--cyan)}.event b{display:block}.event p{margin:4px 0 0;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:560px}.event time{color:var(--muted);font-size:12px}.tabs{display:flex;gap:7px}.tab{border:1px solid var(--line);border-radius:999px;background:#ffffff08;color:var(--muted);padding:7px 11px}.rail h3{margin:4px 0 14px}.card{padding:16px;margin-bottom:13px;border-radius:20px;background:#ffffff09;border:1px solid #ffffff0d}label{display:block;color:var(--muted);font-size:12px;margin:10px 0 6px}input,textarea,button{width:100%;font:inherit;color:var(--text);background:#070a15dd;border:1px solid #ffffff18;border-radius:13px;padding:11px}textarea{min-height:86px;resize:vertical}button{border:0;background:linear-gradient(120deg,var(--cyan),var(--violet));color:#070911;font-weight:850;cursor:pointer;margin-top:10px;box-shadow:0 10px 28px #6f7cff30}.secondary{background:#ffffff09;color:var(--text);box-shadow:none;border:1px solid var(--line)}.alert{padding:12px 15px;border-radius:15px;margin-bottom:14px}.error{background:#56182b}.notice{background:#124735}.login{max-width:470px;margin:12vh auto;padding:28px}.login h1{margin-top:0}.empty{padding:30px;text-align:center;color:var(--muted)}@media(max-width:1100px){.layout{grid-template-columns:220px 1fr}.rail{grid-column:1/-1;display:grid;grid-template-columns:1fr 1fr;gap:14px}.rail h3{grid-column:1/-1}.metrics{grid-template-columns:repeat(2,1fr)}}@media(max-width:720px){.layout{grid-template-columns:1fr}.side{display:none}.rail{display:block}.metrics{grid-template-columns:1fr 1fr}.event{grid-template-columns:40px 1fr}.event time{display:none}.hero .pulse{display:none}.sphere-bg{opacity:.42}.sphere-shade{background:#02040a80}} @media(max-width:720px){body{font-size:16px;-webkit-text-size-adjust:100%}.shell{width:100%;margin:0;padding:max(16px,env(safe-area-inset-top)) 12px max(20px,env(safe-area-inset-bottom))}.top{align-items:flex-start;gap:12px}.brand{gap:10px}.mark{width:44px;height:44px;border-radius:14px}.brand h1{font-size:23px;line-height:1.08}.brand p{font-size:13px;line-height:1.35}.top form{flex:0 0 auto}.top button{padding:11px 12px}.main,.rail{padding:14px;border-radius:22px}.hero{padding:18px}.hero h2{font-size:24px}.hero p{font-size:15px;line-height:1.45}.metrics{grid-template-columns:1fr 1fr;gap:9px}.metric{padding:14px}.metric strong{font-size:22px}.section-head{align-items:flex-start;gap:10px;flex-direction:column}.tabs{width:100%;overflow-x:auto;padding-bottom:3px}.tab{white-space:nowrap}.event{padding:13px 11px;align-items:start}.event p{white-space:normal;max-width:none;line-height:1.45}.rail{margin-top:0}.card{padding:17px}.card h4{font-size:18px;margin:0 0 7px}input,textarea,button{min-height:48px;font-size:16px;padding:13px}textarea{min-height:110px}.login{margin:7vh 12px;padding:22px}.o1{right:-120px}.o2{left:-100px}} @media(max-width:390px){.metrics{grid-template-columns:1fr}.brand p{max-width:230px}.top .secondary{font-size:13px}.main,.rail{padding:11px}.hero{padding:16px}} .activity-detail{display:grid;grid-template-columns:42px minmax(0,1fr);gap:12px;padding:14px;border-radius:16px;background:#ffffff07;border:1px solid #ffffff0d}.activity-meta{display:flex;gap:8px;flex-wrap:wrap;color:var(--muted);font-size:12px;margin:5px 0}.activity-data{white-space:pre-wrap;line-height:1.45;margin:8px 0}.inline-actions{display:flex;gap:7px;flex-wrap:wrap}.inline-actions form{display:flex;gap:7px;flex:1;min-width:240px}.inline-actions input{min-width:0}.inline-actions button,.mini{width:auto;margin:0;padding:8px 11px}.sms-grid,.address-grid{display:grid;grid-template-columns:330px minmax(0,1fr);gap:14px}.filters{display:grid;grid-template-columns:repeat(5,1fr);gap:8px}.message-card{padding:13px;border-radius:15px;border:1px solid var(--line);background:#ffffff07;margin:9px 0}.message-card.inbound{border-left:4px solid #65f6b2}.message-card.outbound{border-left:4px solid var(--cyan)}.contact-list{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.contact-card{padding:14px;border-radius:16px;background:#ffffff07;border:1px solid var(--line)}.contact-card small{display:block;color:var(--muted);margin:4px 0}.privacy-note{padding:10px 13px;border-radius:13px;background:#16283a;color:#b9dcff;margin-bottom:12px}.select-field{width:100%;font:inherit;color:var(--text);background:#070a15dd;border:1px solid #ffffff18;border-radius:13px;padding:11px}.operations-bar{display:grid;grid-template-columns:auto minmax(220px,1fr) auto;gap:12px;align-items:center;padding:13px 16px;margin-bottom:16px}.operations-bar b{letter-spacing:.08em}.live-clock{font-variant-numeric:tabular-nums;color:#cfd5ff}.global-search{margin:0}.search-empty{display:none;color:var(--muted);padding:14px}.call-table-wrap{overflow:auto}.call-table{width:100%;border-collapse:separate;border-spacing:0 7px}.call-table th{text-align:left;color:var(--muted);font-size:11px;padding:5px 10px;text-transform:uppercase;letter-spacing:.08em}.call-table td{background:#ffffff07;padding:11px 10px;border-block:1px solid var(--line)}.call-table td:first-child{border-left:1px solid var(--line);border-radius:12px 0 0 12px}.call-table td:last-child{border-right:1px solid var(--line);border-radius:0 12px 12px 0}.status-pill{display:inline-block;padding:5px 8px;border-radius:999px;background:#ffffff0d}.call-flow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin:12px 0}.flow-node{padding:12px 14px;border-radius:14px;background:#151b36;border:1px solid #6f63ff66}.flow-arrow{color:var(--violet);font-size:20px}.activity-detail,.thread,.message-card,.contact-card,.health-card,.vm-card,.metric,.card,.call-table tr{transition:transform .18s ease,box-shadow .18s ease,border-color .18s ease,background .18s ease}.activity-detail:hover,.thread:hover,.message-card:hover,.contact-card:hover,.health-card:hover,.vm-card:hover,.metric:hover,.card:hover,.call-table tbody tr:hover{transform:translateY(-2px);border-color:#756cff;background:#29245b66;box-shadow:0 0 0 1px #756cff55,0 14px 36px #34278a55}.hidden-by-search{display:none!important}@media(max-width:950px){.sms-grid,.address-grid{grid-template-columns:1fr}.contact-list{grid-template-columns:1fr 1fr}.filters{grid-template-columns:1fr 1fr}.operations-bar{grid-template-columns:1fr}.live-clock{text-align:left}}@media(max-width:620px){.contact-list,.filters{grid-template-columns:1fr}.inline-actions form{min-width:100%}} </style></head><body><div class="screensaver" id="screensaver" aria-hidden="true"><div class="screen-core"><b>SI</b></div><p>MOVE TO RETURN · SIMON LISTENING</p></div><div class="sphere-bg"><canvas id="c0"></canvas><canvas id="c1"></canvas><canvas id="c2"></canvas><div id="hint" hidden></div></div><div class="sphere-shade"></div><i class="orb o1"></i><i class="orb o2"></i> <?php if(!$authorized):?><main class="login glass spatial-login"><div class="login-emblem"><div class="mark">SI</div></div><div class="login-copy"><h1>Welcome back</h1><p>Sign in to Gaylord Sinclair × SIMON Operations</p></div><?php if($error):?><p class="alert error"><?=h($error)?></p><?php endif?><form method="post"><label for="access_key">Secure access key</label><div class="password-field"><input id="access_key" type="password" name="access_key" autocomplete="current-password" required autofocus><button class="password-toggle" id="passwordToggle" type="button" aria-label="Show access key">◉</button></div><label class="remember-row"><input type="checkbox" name="remember_session" value="1"> Remember this device for this browser session</label><button>Enter operations</button></form><p class="login-security">🔒 Encrypted session · throttled login · private operator access</p></main> <?php else:?><main class="shell"><header class="top"><div class="brand"><div class="mark">SI</div><div><h1>Gaylord Sinclair × SIMON</h1><p>Spatial Operations · Calls, messages, intelligence and system health</p></div></div></header><div class="operations-bar glass"><b>GAYLORD + SIMON LIVE</b><input id="globalSearch" class="global-search" type="search" placeholder="Search calls, texts, numbers, names, status or SID"><div class="top-context"><button class="weather-chip secondary" id="loadWeatherTop" type="button">☁ Weather</button><span id="weatherTopStatus">Location off</span><time id="liveClock" class="live-clock">Loading Central time…</time></div></div><?php if($error):?><div class="alert error"><?=h($error)?></div><?php endif?><?php if($notice):?><div class="alert notice"><?=h($notice)?></div><?php endif?> <div class="layout"><aside class="side glass"><nav class="nav icon-rail"><a class="active" href="#overview" title="Overview"><span class="nav-icon">⌂</span><span class="nav-label">Overview</span></a><a href="#si-console" title="Talk to SI"><span class="nav-icon">●</span><span class="nav-label">Talk to SI</span></a><a href="#diagnostics" title="Diagnostics"><span class="nav-icon">◉</span><span class="nav-label">Diagnostics</span></a><a href="#call-map" title="Calls"><span class="nav-icon">☎</span><span class="nav-label">Calls</span></a><a href="#sms-center" title="Texts"><span class="nav-icon">✦</span><span class="nav-label">Texts</span></a><a href="#voicemail-center" title="Voicemail"><span class="nav-icon">◒</span><span class="nav-label">Voicemail</span></a><a href="#address-book" title="Contacts"><span class="nav-icon">▦</span><span class="nav-label">Contacts</span></a><a href="#task-center" title="Tasks"><span class="nav-icon">✓</span><span class="nav-label">Tasks</span></a></nav><div class="rail-bottom"><a class="rail-action" href="#settings-center" title="Settings"><span class="nav-icon">⚙</span><span class="nav-label">Settings</span></a><form method="post"><button class="rail-action" name="logout" title="Sign out"><span class="nav-icon">↩</span><span class="nav-label">Sign out</span></button></form></div><div class="status compact-status"><h4>System</h4><div class="status-row"><i class="dot <?=($healthCounts['offline']>0?'offline':($healthCounts['warning']>0?'warning':'online'))?>"></i><b><?=h((string)$healthCounts['online'])?>/<?=h((string)count($health))?></b></div></div></aside> <section class="main glass" id="overview"><div class="hero"><div><h2>Director’s Brief</h2><p>SIMON is monitoring calls, messages, voicemail, contacts and follow-ups.</p></div><div class="pulse">◉</div></div><div class="metrics"><div class="metric glass"><span>CALL EVENTS</span><strong><?=count($calls)?></strong></div><div class="metric glass"><span>MISSED CALLS</span><strong><?=count($missed)?></strong></div><div class="metric glass"><span>VOICEMAILS</span><strong><?=count($voicemails)?></strong></div><div class="metric glass"><span>CONTACTS</span><strong><?=count($contacts)?></strong></div></div><div class="section-head"><h3>Detailed Activity Log</h3><div class="tabs"><span class="tab">From / To</span><span class="tab">Date / Time</span><span class="tab">Reply</span></div></div><div class="feed"><?php if(!$events):?><div class="empty">Activity will appear here after the first call or message.</div><?php endif?><?php foreach($events as $r):$i=si_event_info($r);$replyTo=$r['_channel']==='text'&&$i['from']!=='Unknown'?$i['from']:'';?><article class="activity-detail"><div class="icon"><?=$r['_channel']==='call'?'☎':'✦'?></div><div><b><?=h(ucwords(str_replace('_',' ',(string)($r['event']??$r['_channel']))))?></b><div class="activity-meta"><span><b>From:</b> <?=h($i['from'])?></span><span><b>To:</b> <?=h($i['to'])?></span><span><b>Date:</b> <?=h($i['date'])?></span><span><b>Time:</b> <?=h($i['time'])?></span><span><b>Status:</b> <?=h($i['status'])?></span></div><div class="activity-data"><?=h($i['data'])?></div><div class="inline-actions"><?php if($replyTo!==''):?><form method="post"><input type="hidden" name="to" value="<?=h($replyTo)?>"><input name="message" placeholder="Reply to <?=h($replyTo)?>" required><button name="send_sms">Reply</button></form><?php endif?><?php $callNumber=$i['from']!=='Unknown'?$i['from']:$i['to'];if(si_ops_e164($callNumber)!==''):?><form method="post"><input type="hidden" name="call_to" value="<?=h(si_ops_e164($callNumber))?>"><input type="hidden" name="caller_id" value="<?=h((string)($callerIds[0]??''))?>"><button class="secondary mini" name="place_call">Call</button></form><?php endif?></div></div></article><?php endforeach?></div></section> <aside class="rail glass"><h3>Command deck</h3><div class="card decision"><h4>SIMON Decision</h4><p><b>Known:</b> <?=count($events)?> recent communication events.</p><p><b>Needs attention:</b> <?=count($missed)?> missed calls, <?=count($voicemails)?> voicemails, <?=count($openTasks)?> open tasks.</p><p><b>Recommendation:</b> <?=($healthCounts['offline']>0?'Resolve failed system checks first.':($healthCounts['warning']>0?'Run diagnostics, then review voicemail.':'Review the unified conversation queue.'))?></p></div><div class="card"><h4>Spatial dialer</h4><p style="color:var(--muted)">SI calls you first, then connects the destination.</p><form method="post"><label>Number to call</label><input name="call_to" placeholder="+1 555 555 0123" required><label>Caller ID shown</label><select class="select-field" name="caller_id"><?php foreach($callerIds as $cid):?><option value="<?=h($cid)?>"><?=h($cid)?></option><?php endforeach?></select><button name="place_call">Place call</button></form></div><div class="card"><h4>Send message</h4><form method="post"><label>Recipient</label><input name="to" value="<?=h(defined('SI_TRANSFER_NUMBER')?(string)SI_TRANSFER_NUMBER:'')?>" required><label>Message</label><textarea name="message" placeholder="Type a message…" required></textarea><button name="send_sms">Send text</button></form></div></aside></div></main> <section class="ops-section glass" id="diagnostics"><div class="section-head"><div><h3>Central Diagnostics</h3><p style="color:var(--muted)">Green is verified, yellow is configured but unverified, and red needs setup.</p></div><form method="post"><button name="refresh_twilio">Run All Diagnostics</button></form></div><div class="summary-strip"><span><?=$healthCounts['online']?> operational</span><span><?=$healthCounts['warning']?> unverified</span><span><?=$healthCounts['offline']?> failed or missing</span></div><div class="health-grid"><?php foreach($health as $check):?><article class="health-card"><div class="health-head"><b><?=h($check['label'])?></b><span class="state state-<?=h($check['state'])?>"><?=h($check['text'])?></span></div><p><?=h($check['detail'])?></p></article><?php endforeach?></div></section> <section class="ops-section glass" id="status-breakdown"><div class="section-head"><div><h3>Why the System Is Yellow</h3><p style="color:var(--muted)">Yellow means configured but not yet proven by a live test—it does not automatically mean the service is down.</p></div></div><div class="health-grid"><?php foreach($health as $check):if($check['state']!=='warning')continue;?><article class="health-card"><div class="health-head"><b><?=h($check['label'])?></b><span class="state state-warning">Needs verification</span></div><p><?=h($check['detail'])?></p><p><b>Next action:</b> Press Run All Diagnostics. For Twilio Lookup, use Lookup Caller ID on an unknown conversation.</p></article><?php endforeach?></div></section> <section class="ops-section glass" id="si-console"><div class="section-head"><div><h3>Talk to SI</h3><p style="color:var(--muted)">Private operational conversation using your server-side OpenAI connection.</p></div><span class="state state-<?=($aiReady?'warning':'offline')?>"><?=($aiReady?'Configured':'Offline')?></span></div><div class="privacy-note">SI receives your question and local company knowledge. Your full private contact directory is not included.</div><div class="card"><?php if(!$siChat):?><p style="color:var(--muted)">Ask SI about calls, messages, Gaylord Sinclair, SIMON Enterprise or the next operational action.</p><?php endif?><?php foreach($siChat as $turn):?><div class="message-card outbound"><b>You</b><div class="activity-data"><?=h((string)($turn['question']??''))?></div></div><div class="message-card inbound"><b>SI</b><div class="activity-data"><?=h((string)($turn['answer']??''))?></div></div><?php endforeach?><form method="post"><label>Ask SI</label><textarea name="si_question" placeholder="SI, what needs my attention right now?" required></textarea><button name="ask_si">Send to SI</button></form></div></section> <section class="ops-section glass" id="conversations"><div class="section-head"><div><h3>Unified Conversations & Caller ID</h3><p style="color:var(--muted)">Numbers are matched against the private address book first, then optional Twilio Lookup.</p></div><span class="tab"><?=count($threads)?> conversations</span></div><div class="thread-grid"><?php if(!$threads):?><div class="empty">Conversations appear after the first call or text.</div><?php endif?><?php foreach(array_slice($threads,0,50) as $thread):$identity=si_identity((string)$thread['party'],$contactIndex,$lookupCache);?><article class="thread"><b><?=h($identity['name'])?></b><small><?=h((string)$thread['party'])?> · <?=h($identity['source'])?></small><?php if($identity['label']!==''):?><small><?=h($identity['label'])?><?=($identity['carrier']!==''?' · '.h($identity['carrier']):'')?></small><?php endif?><small><?=count($thread['events'])?> events · <?=h($thread['last'])?></small><p><?=h(ucwords(str_replace('_',' ',(string)($thread['events'][0]['event']??'communication'))))?></p><div class="inline-actions"><?php if(!$identity['known']&&si_ops_e164((string)$thread['party'])!==''):?><form method="post"><button class="secondary mini" name="lookup_number" value="<?=h(si_ops_e164((string)$thread['party']))?>">Lookup Caller ID</button></form><?php endif?><form method="post"><input type="hidden" name="call_to" value="<?=h(si_ops_e164((string)$thread['party']))?>"><input type="hidden" name="caller_id" value="<?=h((string)($callerIds[0]??''))?>"><button class="mini" name="place_call">Call</button></form></div></article><?php endforeach?></div></section> <section class="ops-section glass" id="sms-center"><div class="section-head"><div><h3>Connected SMS Center</h3><p style="color:var(--muted)">Uses this Spatial Operations login—no second password.</p></div><span class="tab"><?=count($smsRows)?> messages</span></div><div class="sms-grid"><aside><div class="card"><h4>Send SMS</h4><form method="post"><label>Recipient</label><input name="to" placeholder="+1 214 555 1212" required><label>Message</label><textarea name="message" required></textarea><button name="send_sms">Send</button></form></div><div class="card"><h4>Block number</h4><form method="post"><input name="phone" placeholder="+1 214 555 1212" required><button name="block_number">Block</button></form><?php foreach($blockedNumbers as $number):?><form method="post" class="inline-actions"><input type="hidden" name="phone" value="<?=h($number)?>"><span><?=h($number)?></span><button class="secondary mini" name="unblock_number">Unblock</button></form><?php endforeach?></div></aside><section><form method="get" class="filters"><input type="date" name="sms_from" value="<?=h($smsFrom)?>"><input type="date" name="sms_to" value="<?=h($smsTo)?>"><input name="sms_q" value="<?=h((string)($_GET['sms_q']??''))?>" placeholder="Phone or keyword"><select class="select-field" name="sms_direction"><option value="">All directions</option><option value="inbound" <?=$smsDirection==='inbound'?'selected':''?>>Inbound</option><option value="outbound" <?=$smsDirection==='outbound'?'selected':''?>>Outbound</option></select><button>Search</button></form><?php if(!$smsRows):?><div class="empty">No matching messages.</div><?php endif?><?php foreach(array_slice($smsRows,0,500) as $row):$i=si_event_info($row);$direction=str_contains(strtolower((string)($row['event']??'')),'out')?'outbound':'inbound';$reply=$direction==='inbound'?$i['from']:$i['to'];?><article class="message-card <?=h($direction)?>"><b><?=h(strtoupper($direction))?> · <?=h($i['status'])?></b><div class="activity-meta"><span>From <?=h($i['from'])?></span><span>To <?=h($i['to'])?></span><span><?=h($i['date'])?> <?=h($i['time'])?></span></div><div class="activity-data"><?=h($i['data'])?></div><?php if(si_ops_e164($reply)!==''):?><form method="post" class="inline-actions"><input type="hidden" name="to" value="<?=h(si_ops_e164($reply))?>"><input name="message" placeholder="Reply" required><button name="send_sms">Reply</button></form><?php endif?></article><?php endforeach?></section></div></section> <section class="ops-section glass" id="voicemail-center"><div class="section-head"><div><h3>Voicemail Center</h3><p style="color:var(--muted)">Playback, transcript, download, archive and resolution.</p></div><span class="tab"><?=count($activeVoicemails)?> active · <?=count($archivedVoicemails)?> archived</span></div><div class="vm-grid"><?php if(!$activeVoicemails):?><div class="empty">No active voicemail.</div><?php endif?><?php foreach(array_slice($activeVoicemails,0,12) as $v):$vd=(array)($v['details']??[]);$vmid=hash('sha256',(string)($vd['recording_sid']??$vd['recording_url']??''));?><article class="vm-card"><b><?=h((string)($vd['caller']??$v['from']??'Unknown caller'))?></b><small><?=h((string)($v['time']??''))?> · <?=h((string)($vd['duration_seconds']??'0'))?> sec · SID <?=h((string)($vd['recording_sid']??'Unavailable'))?></small><p><?=h((string)($vd['transcript']??'Transcription pending'))?></p><?php if(!empty($vd['recording_url'])):?><audio controls preload="none" style="width:100%" src="<?=h((string)$vd['recording_url'])?>"></audio><a class="linkbtn" target="_blank" rel="noopener" href="<?=h((string)$vd['recording_url'])?>">Open / Save Recording</a><?php endif?><div class="inline-actions"><form method="post"><button class="secondary mini" name="clear_voicemail" value="<?=h($vmid)?>">Resolve</button><button class="secondary mini" name="archive_voicemail" value="<?=h($vmid)?>">Archive</button></form><?php if(si_ops_e164((string)($vd['caller']??$v['from']??''))!==''):?><form method="post"><input type="hidden" name="call_to" value="<?=h(si_ops_e164((string)($vd['caller']??$v['from']??'')))?>"><input type="hidden" name="caller_id" value="<?=h((string)($callerIds[0]??''))?>"><button class="mini" name="place_call">Call Back</button></form><?php endif?></div></article><?php endforeach?></div><?php if($archivedVoicemails):?><details style="margin-top:16px"><summary>Archived voicemail (<?=count($archivedVoicemails)?>)</summary><div class="vm-grid"><?php foreach($archivedVoicemails as $v):$vd=(array)($v['details']??[]);?><article class="vm-card"><b><?=h((string)($vd['caller']??$v['from']??'Unknown caller'))?></b><small><?=h((string)($v['time']??''))?></small><p><?=h((string)($vd['transcript']??'Transcription pending'))?></p></article><?php endforeach?></div></details><?php endif?></section> <section class="ops-section glass" id="address-book"><div class="section-head"><div><h3>Private Address Book</h3><p style="color:var(--muted)">One card per person or company, with every associated number.</p></div><span class="tab"><?=count($contacts)?> consolidated contacts</span></div><div class="privacy-note">The address book is loaded only from the private server directory. Full contact data is not sent to OpenAI.</div><div class="contact-list"><?php foreach($contacts as $contact):$numbers=array_values((array)($contact['numbers']??[]));if(!$numbers)continue;?><article class="contact-card"><b><?=h((string)($contact['name']??$contact['first_name']??'Contact'))?></b><div class="contact-number-stack"><?php foreach($numbers as $phone):$number=si_ops_e164((string)($phone['number']??''));if($number==='')continue;?><div class="contact-number-row"><div><a class="number-link" href="tel:<?=h($number)?>"><?=h($number)?></a><small><?=h((string)($phone['label']??'Phone'))?></small></div><div class="number-actions"><a href="tel:<?=h($number)?>" title="Call">☎</a><a href="sms:<?=h($number)?>" title="Text">✦</a></div></div><?php endforeach?></div><a class="linkbtn mini" href="#conversations">History</a></article><?php endforeach?></div></section> <section class="ops-section glass" id="task-center"><div class="section-head"><div><h3>Tasks & Callbacks</h3><p style="color:var(--muted)">Keep follow-ups connected to operations.</p></div><span class="tab"><?=count($openTasks)?> open</span></div><form method="post" class="compact-form"><input name="task_text" placeholder="Add a callback, reminder or follow-up" required><button name="add_task">Add Task</button></form><div><?php if(!$openTasks):?><div class="empty">No open tasks.</div><?php endif?><?php foreach($openTasks as $task):?><div class="task-row"><div><b><?=h(ucfirst((string)($task['type']??'task')))?></b><br><span style="color:var(--muted)"><?=h((string)($task['text']??''))?></span></div><?php if(!empty($task['id'])):?><form method="post"><button class="secondary" name="resolve_task" value="<?=h((string)$task['id'])?>">Complete</button></form><?php endif?></div><?php endforeach?></div></section> <section class="ops-section glass settings-center" id="settings-center"><div class="section-head"><div><h3>⚙ SI Operations Settings</h3><p style="color:var(--muted)">Ordinary preferences are saved in private server storage. Credentials never appear here.</p></div><span class="tab">Private configuration</span></div><form method="post"><input type="hidden" name="csrf" value="<?=h((string)($_SESSION['csrf']??''))?>"><?php $settingGroups=['AI Receptionist'=>['greeting','voice','business_pronunciation','speech_language','greeting_max_seconds','silence_timeout','misunderstood_retries','transfer_confidence','answer_without_keith','approved_subjects','escalation_subjects','after_hours_greeting','holiday_greeting','ai_failure_message','disable_ai'],'Recording & Voicemail'=>['recording_default','recording_consent','dual_channel','recording_callback','automatic_transcription','ai_voicemail_summary','voicemail_max_seconds','voicemail_silence_seconds','recording_retention_days','delete_audio_keep_summary','urgent_voicemail_alert'],'Outgoing Calls'=>['dial_keith_first','outgoing_caller_id','answering_machine_detection','leave_voicemail','retry_unanswered','maximum_retries','record_outgoing','confirm_international','approved_destinations'],'Alerts & Notifications'=>['alerts_channels','alert_recipient_sms','alert_recipient_email','alert_incoming_approved','alert_missed_call','alert_failed_transfer','alert_new_voicemail','alert_urgent_caller','alert_repeated_caller','alert_blocked_attempt','alert_webhook_failure','alert_phone_offline','alert_transcription_failure','quiet_hours','daily_call_summary'],'Logs & Security'=>['call_display_limit','auto_refresh','timezone','log_retention_days','show_recordings','show_errors','mask_private_numbers','session_timeout_minutes','login_throttling','two_factor_required']];$longSettings=['greeting','approved_subjects','escalation_subjects','after_hours_greeting','holiday_greeting','ai_failure_message','approved_destinations'];foreach($settingGroups as $group=>$keys):?><details class="settings-group" <?=$group==='AI Receptionist'?'open':''?>><summary><?=h($group)?></summary><div class="settings-grid"><?php foreach($keys as $key):$value=$portalSettings[$key]??'';$label=ucwords(str_replace('_',' ',$key));?><label class="setting-field"><span><?=h($label)?></span><?php if(is_bool($value)):?><input type="hidden" name="settings[<?=h($key)?>]" value="0"><input type="checkbox" name="settings[<?=h($key)?>]" value="1" <?=$value?'checked':''?>><?php elseif(is_int($value)):?><input type="number" name="settings[<?=h($key)?>]" value="<?=h((string)$value)?>"><?php elseif(in_array($key,$longSettings,true)):?><textarea name="settings[<?=h($key)?>]" rows="3"><?=h((string)$value)?></textarea><?php else:?><input name="settings[<?=h($key)?>]" value="<?=h((string)$value)?>"><?php endif?></label><?php endforeach?></div></details><?php endforeach?><button name="save_settings">Save Private Settings</button></form><div class="security-locks"><span>✓ Webhook signatures</span><span>✓ CSRF</span><span>✓ Audit logging</span><span>✓ Credentials masked</span><span>✓ Contacts outside public HTML</span></div></section> <?php endif?><script> (()=>{const csrf=<?=json_encode((string)($_SESSION['csrf']??''),JSON_HEX_TAG|JSON_HEX_AMP)?>;document.querySelectorAll('form[method="post"]').forEach(form=>{if(form.querySelector('[name="csrf"]'))return;const input=document.createElement('input');input.type='hidden';input.name='csrf';input.value=csrf;form.appendChild(input)}); const formatClock=()=>{const text=new Intl.DateTimeFormat('en-US',{timeZone:'America/Chicago',dateStyle:'full',timeStyle:'long'}).format(new Date());document.querySelectorAll('#liveClock,#operationsClock').forEach(el=>el.textContent=text+' · Central Time')};formatClock();setInterval(formatClock,1000); const search=document.getElementById('globalSearch');if(search)search.addEventListener('input',()=>{const q=search.value.trim().toLowerCase();document.querySelectorAll('.activity-detail,.thread,.message-card,.contact-card,.vm-card,.health-card,.call-table tbody tr').forEach(el=>el.classList.toggle('hidden-by-search',q!==''&&!el.innerText.toLowerCase().includes(q)))}); const sms=document.getElementById('sms-center');if(sms){const calls=<?=json_encode(array_slice($calls,0,150),JSON_HEX_TAG|JSON_HEX_AMP|JSON_UNESCAPED_SLASHES)?>;const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));const rows=calls.map(r=>{const d=r.details||{},status=r.status||d.status||d.dial_status||r.event||'event',sid=r.call_sid||d.call_sid||'',from=r.from||d.from||d.caller||'Unknown',to=r.to||d.to||'Unknown',verified=['ringing','in-progress','answered','completed'].includes(String(status).toLowerCase());return `<tr class="searchable"><td>${esc(r.time||'Unknown')}</td><td>${esc(from)}</td><td>${esc(to)}</td><td><span class="status-pill">${esc(String(status).toUpperCase())}</span></td><td>${esc(sid||'Pending')}</td><td>${verified?'Twilio callback received':'Request/log only'}</td></tr>`}).join('');const section=document.createElement('section');section.className='ops-section glass';section.id='call-map';section.innerHTML=`<div class="section-head"><div><h3>Gaylord + SIMON Call Map & Verification</h3><p style="color:var(--muted)">Follow the call through ringing, answer and completion. QUEUED does not prove the phone rang.</p></div><span class="tab">${calls.length} recent events</span></div><div class="call-flow"><span class="flow-node">GAYLORD CONSOLE</span><span class="flow-arrow">→</span><span class="flow-node">SIMON / TWILIO</span><span class="flow-arrow">→</span><span class="flow-node">YOUR PHONE</span><span class="flow-arrow">→</span><span class="flow-node">DESTINATION</span></div><div class="call-table-wrap"><table class="call-table"><thead><tr><th>Recorded time</th><th>From</th><th>To</th><th>Status</th><th>Twilio SID</th><th>Evidence</th></tr></thead><tbody>${rows}</tbody></table></div>`;sms.before(section);const env=document.createElement('section');env.className='ops-section glass';env.id='environment';env.innerHTML=`<div class="section-head"><div><h3>Operations Time, Weather & Route Context</h3><p style="color:var(--muted)">Central time plus optional browser-local weather.</p></div></div><div class="health-grid"><article class="health-card"><b>Central operations time</b><p id="operationsClock"></p></article><article class="health-card"><b>Local weather</b><p id="weatherStatus">Location not requested.</p><button class="secondary mini" id="loadWeather" type="button">Load weather here</button></article><article class="health-card"><b>Call route</b><p>The call map reports Twilio states. Geography is used only when authorized contact or lookup data exists.</p></article></div>`;section.after(env);formatClock();document.getElementById('loadWeather')?.addEventListener('click',()=>{const out=document.getElementById('weatherStatus');out.textContent='Requesting location…';if(!navigator.geolocation){out.textContent='Location is unavailable in this browser.';return}navigator.geolocation.getCurrentPosition(async pos=>{try{const {latitude,longitude}=pos.coords;const url=`https://api.open-meteo.com/v1/forecast?latitude=${encodeURIComponent(latitude)}&longitude=${encodeURIComponent(longitude)}¤t=temperature_2m,apparent_temperature,weather_code,wind_speed_10m&temperature_unit=fahrenheit&wind_speed_unit=mph&timezone=auto`;const data=await fetch(url).then(r=>r.json()),w=data.current||{};out.textContent=`${w.temperature_2m??'?'}°F · feels ${w.apparent_temperature??'?'}°F · wind ${w.wind_speed_10m??'?'} mph · code ${w.weather_code??'?'}`}catch(e){out.textContent='Weather service could not be reached.'}},()=>out.textContent='Location permission was not granted.',{timeout:10000})})} })(); </script><script> window.SI_CALENDAR_EVENTS=<?=json_encode($authorized?si_calendar_load():[],JSON_HEX_TAG|JSON_HEX_AMP|JSON_UNESCAPED_SLASHES)?>;window.SI_CSRF=<?=json_encode((string)($_SESSION['csrf']??''),JSON_HEX_TAG|JSON_HEX_AMP)?>; (()=>{const events=<?=json_encode(array_slice($events,0,100),JSON_HEX_TAG|JSON_HEX_AMP|JSON_UNESCAPED_SLASHES)?>,esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));const important=events.filter(r=>{const d=r.details||{},event=String(r.event||'').toLowerCase(),content=String(d.body||d.message||d.transcript||d.speech||'').trim();return content!==''||event.includes('explicit_transfer')||event.includes('message_received')||event.includes('failed')}).slice(0,12);if(important.length){const box=document.createElement('section');box.id='priority-inbox';box.className='priority-inbox glass';box.innerHTML=`<div class="section-head"><div><h3>⚠ HIGH PRIORITY · Messages & Requests</h3><p>Items containing a message, explicit transfer request, or delivery failure appear first.</p></div><span class="priority-count">${important.length} priority</span></div><div class="priority-grid">${important.map(r=>{const d=r.details||{},content=d.error||d.body||d.message||d.transcript||d.speech||'Action required',from=r.from||d.from||d.caller||'Unknown',to=r.to||d.to||'Unknown';return `<article class="priority-card searchable"><b>${esc(String(r.event||'priority').replaceAll('_',' ').toUpperCase())}</b><small>${esc(from)} → ${esc(to)} · ${esc(r.time||'')}</small><p>${esc(content)}</p></article>`}).join('')}</div>`;const bar=document.querySelector('.operations-bar');bar?.after(box)}const vm=document.getElementById('voicemail-center');if(vm){const form=document.createElement('form');form.method='post';form.className='vm-sync';form.innerHTML=`<input type="hidden" name="csrf" value="<?=h((string)($_SESSION['csrf']??''))?>"><button name="refresh_twilio">↻ Sync Twilio voicemail now</button><small>Loads cloud recordings, caller, duration and protected playback.</small>`;vm.querySelector('.section-head')?.after(form)}})(); </script><style>.priority-inbox{position:relative;z-index:10;width:min(1480px,95vw);margin:0 auto 16px;padding:18px;border:1px solid #ff5f8b88;box-shadow:0 0 50px #ff3f7a22}.priority-inbox .section-head{margin:0 0 12px}.priority-inbox p{margin:5px 0;color:#c7cde4}.priority-count{padding:7px 12px;border-radius:999px;background:#ff4f7e;color:#fff;font-weight:800}.priority-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.priority-card{padding:14px;border-radius:15px;background:linear-gradient(135deg,#35142c,#211d55);border:1px solid #ff6f9e66}.priority-card small{display:block;color:#adb6d4;margin:6px 0}.priority-card:hover{border-color:#8b7cff;box-shadow:0 0 30px #6552ff66;transform:translateY(-2px)}.priority-card[data-priority-status="resolved"]{opacity:.58;border-color:#65f6b255}.priority-card[data-priority-status="acknowledged"]{border-color:#ffc45d}.priority-card[data-priority-status="in_progress"]{border-color:#62e6ff}.priority-actions{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px}.priority-actions form{display:inline}.priority-actions button{width:auto;min-height:36px;margin:0;padding:7px 9px;font-size:12px}.priority-stage{display:inline-block;margin-top:5px;padding:4px 7px;border-radius:999px;background:#ffffff12;color:#dce2ff;font-size:11px;text-transform:uppercase}.vm-sync{display:flex;align-items:center;gap:10px;margin:10px 0 16px}.vm-sync button{width:auto;margin:0}.vm-sync small{color:var(--muted)}@media(max-width:850px){.priority-grid{grid-template-columns:1fr}.vm-sync{align-items:stretch;flex-direction:column}.vm-sync button{width:100%}}</style> <style>.communication-journal{display:grid;grid-template-columns:280px minmax(0,1fr) 250px;min-height:650px;border:1px solid #ffffff16;border-radius:20px;overflow:hidden;background:#070b16aa}.journal-sidebar,.journal-details{background:#0b1020cc;padding:13px}.journal-sidebar{border-right:1px solid #ffffff12}.journal-details{border-left:1px solid #ffffff12}.journal-search{margin:0 0 10px}.journal-parties{display:grid;gap:6px;max-height:570px;overflow:auto}.journal-person{display:grid;grid-template-columns:38px minmax(0,1fr) auto;gap:9px;align-items:center;width:100%;margin:0;padding:10px;text-align:left;background:#ffffff05;color:#fff;box-shadow:none}.journal-person.active{background:linear-gradient(135deg,#265c8b88,#5c3ca188);border:1px solid #6ee7ff88}.journal-avatar{width:38px;height:38px;border-radius:50%;display:grid;place-items:center;background:linear-gradient(135deg,#62e6ff,#a77bff);color:#07101b;font-weight:900}.journal-person small{display:block;color:#9ca8c7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.journal-person em{font-style:normal;background:#7b67ff;padding:3px 7px;border-radius:999px;font-size:11px}.journal-thread{display:grid;grid-template-rows:auto 1fr auto;min-width:0}.journal-thread-head{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid #ffffff12}.journal-timeline{padding:18px;overflow:auto;max-height:505px;display:flex;flex-direction:column;gap:10px}.journal-entry{max-width:78%;padding:10px 12px;border-radius:15px;background:#151c31;border:1px solid #ffffff10}.journal-entry.outbound{align-self:flex-end;background:linear-gradient(135deg,#174c70,#4c398c)}.journal-entry.system{align-self:center;max-width:92%;background:#ffffff08}.journal-entry small{display:block;color:#a9b3d0;margin-top:5px}.journal-composer{display:grid;grid-template-columns:1fr auto;gap:8px;padding:12px;border-top:1px solid #ffffff12}.journal-composer input{margin:0}.journal-composer button{width:auto;margin:0}.journal-details h4{margin:4px 0}.journal-detail-row{padding:9px 0;border-bottom:1px solid #ffffff10;color:#aeb8d5}.journal-detail-row b{display:block;color:#fff}.journal-original{display:none!important}@media(max-width:950px){.communication-journal{grid-template-columns:240px minmax(0,1fr)}.journal-details{display:none}}@media(max-width:720px){.communication-journal{display:block;min-height:0}.journal-sidebar{border:0}.journal-parties{display:flex;overflow:auto;max-height:none}.journal-person{min-width:220px}.journal-thread{min-height:560px}.journal-timeline{max-height:430px}.journal-entry{max-width:88%}}</style> <script>document.addEventListener('DOMContentLoaded',()=>{const rows=<?=json_encode($journalRows,JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT|JSON_UNESCAPED_SLASHES)?>,csrf=<?=json_encode((string)($_SESSION['csrf']??''),JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT)?>,section=document.getElementById('sms-center');if(!section)return;const original=section.querySelector('.sms-grid');original?.classList.add('journal-original');const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));const groups=new Map;rows.forEach(row=>{if(!groups.has(row.party))groups.set(row.party,{party:row.party,name:row.name&&row.name!=='Unknown caller'?row.name:row.party,label:row.label||'',rows:[]});groups.get(row.party).rows.push(row)});const people=[...groups.values()].sort((a,b)=>(b.rows.at(-1)?.timestamp||0)-(a.rows.at(-1)?.timestamp||0));const journal=document.createElement('div');journal.className='communication-journal';journal.innerHTML='<aside class="journal-sidebar"><input class="journal-search" type="search" placeholder="Search conversations"><div class="journal-parties"></div></aside><main class="journal-thread"><header class="journal-thread-head"><div><b>Select a conversation</b><small></small></div></header><div class="journal-timeline"><div class="empty">Messages, calls and voicemail stay together here.</div></div><form method="post" class="journal-composer"><input type="hidden" name="csrf" value="'+esc(csrf)+'"><input type="hidden" name="to"><input name="message" placeholder="Write a message" required><button name="send_sms">Send</button></form></main><aside class="journal-details"><h4>Journal details</h4><div class="journal-detail-content"></div></aside>';section.appendChild(journal);const partyList=journal.querySelector('.journal-parties'),timeline=journal.querySelector('.journal-timeline'),head=journal.querySelector('.journal-thread-head div'),details=journal.querySelector('.journal-detail-content'),to=journal.querySelector('[name="to"]');let current='';const icon=row=>row.channel==='text'?'✦':String(row.event).includes('voicemail')?'◉':'☎';const drawList=(q='')=>{partyList.innerHTML=people.filter(p=>(p.name+' '+p.party+' '+p.label+' '+p.rows.map(r=>r.body).join(' ')).toLowerCase().includes(q)).map(p=>{const last=p.rows.at(-1)||{},initial=(p.name||'?').trim().charAt(0).toUpperCase();return `<button type="button" class="journal-person ${p.party===current?'active':''}" data-party="${esc(p.party)}"><span class="journal-avatar">${esc(initial)}</span><span><b>${esc(p.name)}</b><small>${esc(last.body||last.event||'Communication')}</small></span><em>${p.rows.length}</em></button>`}).join('')||'<div class="empty">No matching journal.</div>';partyList.querySelectorAll('button').forEach(b=>b.onclick=()=>select(b.dataset.party))};const select=party=>{current=party;const p=people.find(x=>x.party===party);if(!p)return;to.value=p.party;head.innerHTML=`<b>${esc(p.name)}</b><small>${esc(p.party)}${p.label?' · '+esc(p.label):''}</small>`;timeline.innerHTML=p.rows.map(row=>{const system=row.channel!=='text'||!row.body,kind=system?'system':row.direction;return `<article class="journal-entry ${kind}"><b>${icon(row)} ${esc(String(row.event).replaceAll('_',' '))}</b>${row.body?`<div>${esc(row.body)}</div>`:''}<small>${esc(row.time||'')} · ${esc(row.status||'unknown')}${row.sid?' · '+esc(row.sid):''}</small></article>`}).join('');timeline.scrollTop=timeline.scrollHeight;details.innerHTML=`<div class="journal-detail-row"><b>${esc(p.name)}</b>${esc(p.party)}</div><div class="journal-detail-row"><b>${p.rows.length}</b>journal entries</div><div class="journal-detail-row"><b>${p.rows.filter(r=>r.channel==='text').length}</b>texts</div><div class="journal-detail-row"><b>${p.rows.filter(r=>r.channel==='call').length}</b>call events</div><div class="journal-detail-row"><a href="tel:${esc(p.party)}">☎ Call</a> · <a href="sms:${esc(p.party)}">✦ Text</a></div>`;drawList(journal.querySelector('.journal-search').value.trim().toLowerCase())};journal.querySelector('.journal-search').oninput=e=>drawList(e.target.value.trim().toLowerCase());drawList();if(people[0])select(people[0].party)});</script> <script>document.addEventListener('DOMContentLoaded',()=>{const states=<?=json_encode($priorityState,JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT)?>,csrf=<?=json_encode((string)($_SESSION['csrf']??''),JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT)?>;const hash=text=>{let h=2166136261;for(let i=0;i<text.length;i++){h^=text.charCodeAt(i);h=Math.imul(h,16777619)}return(h>>>0).toString(16)};document.querySelectorAll('.priority-card').forEach(card=>{const id=hash(card.innerText.trim()),state=states[id]||{status:'new'},status=state.status||'new';card.dataset.priorityStatus=status;const stage=document.createElement('span');stage.className='priority-stage';stage.textContent=status.replace('_',' ')+(state.updated_at?' · '+new Date(state.updated_at).toLocaleString():'');card.appendChild(stage);const actions=document.createElement('div');actions.className='priority-actions';const options=status==='new'?[['acknowledged','Acknowledge'],['in_progress','Start work'],['resolved','Resolve']]:status==='acknowledged'?[['in_progress','Start work'],['resolved','Resolve']]:status==='in_progress'?[['resolved','Resolve']]:[];actions.innerHTML=options.map(([value,label])=>`<form method="post"><input type="hidden" name="csrf" value="${csrf}"><input type="hidden" name="priority_id" value="${id}"><button name="priority_action" value="${value}">${label}</button></form>`).join('');card.appendChild(actions)})});</script><script src="sphere-bg.js" defer></script></body></html>
Save file
Quick jump
open a path
Open