<?php
require_once('config.php');
require_once('include/service/contacts_match.php');
if(!isset($_SESSION['user'])||!has_permission('faxdashboard')){header("Location:login.php");exit;}
function faxViewToken(string $path):string{
$data=realpath($path);
$ts=time();
$payload=$ts.'|'.$data;
$sig=hash_hmac('sha256',$payload,FAX_VIEW_SECRET);
return rtrim(strtr(base64_encode($sig.'|'.$payload),'+/','-_'),'=');
}
function extractFaxNumber(string $filename):string{
$base=pathinfo($filename,PATHINFO_FILENAME);
if(preg_match('/^\d{8}_\d{4}_([a-zA-Z0-9]+)$/',$base,$m)){return$m[1];}return'Unknown';
}
function extractFaxDateKey(string $filename):int{if(preg_match('/(\d{8})_(\d{4})_/',$filename,$m)){return(int)($m[1].$m[2]);}return 0;}
$contactMap=getContactMap();
function scanFaxFiles(string $dir,bool $useMeta=false):array{
global $scan_limit,$recent_limit,$subfolder_level,$applicationArchivePath;
$limit=$scan_limit??30;
$displayCount=$recent_limit??3;
$subfolderlevel=$subfolder_level??1;
$result=[];
if(!is_dir($dir))return $result;
$isInboxDir=(realpath($dir)===realpath($applicationArchivePath));
$directory=new RecursiveDirectoryIterator($dir,FilesystemIterator::SKIP_DOTS);
$items=new RecursiveIteratorIterator($directory);
$items->setMaxDepth($subfolderlevel);
$pdfFiles=[];
foreach($items as $item){if($item->isFile()&&strtolower($item->getExtension())==='pdf'){$pdfFiles[]=$item;}}
usort($pdfFiles,fn($a,$b)=>$b->getMTime()<=>$a->getMTime());
$checkCount=0;
foreach($pdfFiles as $item){
if(++$checkCount>$limit)break;
$realPath=realpath($item->getPathname());
$realBase=realpath($dir);
if(!$realPath||strpos($realPath,$realBase)!==0)continue;
$fname=$item->getFilename();
$mTime=$item->getMTime();
$number='Unknown';
$dateStr=date('Y/m/d H:i',$mTime);
$sortKey=$mTime;
$isRead=false;
$hasMemo=false;
$memo='';
$isScheduled=false;
$metaStatus='';
if($useMeta){
$metaFile=dirname($realPath).DIRECTORY_SEPARATOR.pathinfo($realPath,PATHINFO_FILENAME).'.meta';
if(is_file($metaFile)){
$meta=loadMeta($metaFile);
if($meta){
$number=$meta['destination']??$meta['number']??'Unknown';
$timeField=$isInboxDir?'received_time':'created_time';
$rawTime=$meta[$timeField]??$meta['created_time']??$meta['received_time']??'';
if(!empty($rawTime)){
$dt=DateTime::createFromFormat('Y-m-d H:i:s',$rawTime)
?:DateTime::createFromFormat('Y-m-d H:i',$rawTime);
if($dt){
$dateStr=$dt->format('Y/m/d H:i');
$sortKey=$dt->getTimestamp();
}}
$isRead=($meta['read']??false)===true;
$memo=trim($meta['memo']??'');
$hasMemo=($memo!=='');
$isScheduled=!empty($meta['schedule']);
$metaStatus=$meta['status']??'';
}}}
if($number==='Unknown'){
$base=pathinfo($fname,PATHINFO_FILENAME);
$parts=explode('_',$base);
if(count($parts)>=3){$number=$parts[2];
}elseif(preg_match('/^\d{8}_\d{4}_([a-zA-Z0-9]+)/',$base,$m)){$number=$m[1];}
}
$result[]=[
'path'=>$item->getPathname(),
'number'=>$number,
'date'=>$dateStr,
'key'=>$sortKey,
'isRead'=>$isRead,
'hasMemo'=>$hasMemo,
'memo'=>$memo,
'isScheduled'=>$isScheduled,
'metaStatus'=>$metaStatus,
'mtime'=>$mTime
];
}
usort($result,fn($a,$b)=>$b['key']<=>$a['key']);
return array_slice($result,0,min($limit,$displayCount));
}
$recentInbox=scanFaxFiles($applicationArchivePath,true);
$recentOutbox=scanFaxFiles($applicationOutboxPath,true);
$spoolDir=rtrim($applicationSpoolDir,'/').'/';
$incomingFaxes=glob($spoolDir.'*.tif')?:[];
function safe_exec(string $cmd,array $args=[]):string{
$allowed_cmds=['faxstat','pgrep'];
if(!in_array($cmd,$allowed_cmds,true))return'';
$allowed_targets=['faxq','hfaxd','-s'];
$validatedArgs=[];
foreach($args as $arg){if(in_array($arg,$allowed_targets,true)||preg_match('/^-[sadv]$/',$arg)){$validatedArgs[]=escapeshellarg($arg);}}
if(empty($validatedArgs)&&!empty($args))return'Invalid arguments';
$fullCommand=$cmd.' '.implode(' ',$validatedArgs).' 2>&1';
return trim(shell_exec($fullCommand)??'');
}
$modemStatusTable=[];
$faxstat=safe_exec('faxstat',['-s']);
$lines=explode("\n",$faxstat);
$inQueue=false;
foreach($lines as $line){
$line=trim($line);
if(!$line)continue;
if(str_starts_with($line,'JID')){$inQueue=true;continue;}
if($inQueue){$parts=preg_split('/\s+/',$line,8);
if(count($parts)>=7)$modemStatusTable[]=$parts;}
}
$schedulerRunning=safe_exec('pgrep',['faxq'])!=='';
$hfaxdRunning=safe_exec('pgrep',['hfaxd'])!=='';
$usbModemStatus=[];
$modems=glob('/dev/ttyACM*')?:[];
if($modems){foreach($modems as $m){$usbModemStatus[basename($m)]=(is_readable($m)&&is_writable($m));}
}else{$usbModemStatus['No Modem']=false;}
$overallStatus=$schedulerRunning&&$hfaxdRunning&&!in_array(false,$usbModemStatus,true);
function getLastFaxTime(string $dir):string{
if(!is_dir($dir))return'-';
$it=new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir,FilesystemIterator::SKIP_DOTS));
$last=0;
foreach($it as $f){if($f->isFile()&&strtolower($f->getExtension())==='pdf'){$last=max($last,$f->getMTime());}}
return$last?date('Y/m/d H:i',$last):'-';
}
$lastIncomingTime=getLastFaxTime($applicationArchivePath);
$lastOutgoingTime=getLastFaxTime($applicationOutboxPath);
$diskTotal=disk_total_space($applicationArchivePath);
$diskFree=disk_free_space($applicationArchivePath);
$diskUsedPct=$diskTotal>0?round(($diskTotal-$diskFree)/$diskTotal*100):0;
$diskClass=$diskUsedPct>=85?'fail':($diskUsedPct>=70?'warn':'ok');
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title><?=ns(APP_NAME)?> - <?php _nsl('Fax Dashboard');?></title>
<link rel="stylesheet" href="include/css/style.css">
<?php include('include/web/header.php');?>
<script>
window.FAX_LANG={
ok:"<?=ns(__('Ok'))?>",
cancel:"<?=ns(__('Cancel'))?>",
faxMemo:"<?=ns(__('Fax Memo'))?>",
alert:"<?=ns(__('Alert'))?>",
pushTitle:"<?=ns(__('Push Notification'))?>",
pushDesc:"<?=ns(__('You can register or test push notifications'))?>",
pushRegister:"<?=ns(__('Subscribe'))?>",
pushTest:"<?=ns(__('Test'))?>",
pushNotSupported:"<?=ns(__('Push is not supported in this browser'))?>",
pushNeedPermission:"<?=ns(__('Notification permission is required'))?>",
pushAlready:"<?=ns(__('Already subscribed'))?>",
pushSuccess:"<?=ns(__('Push subscription completed'))?>",
pushTestSuccess:"<?=ns(__('Test notification sent'))?>",
pushTestFail:"<?=ns(__('Test failed'))?>",
help:"<?=ns(__('Help'))?>",
help1:"<?=ns(__('Shows the overall health and operational status of the fax system'))?>",
help2:"<?=ns(__('Manages the fax job queue and schedules sending tasks'))?>",
help3:"<?=ns(__('Core service that handles fax transmission and reception'))?>",
help4:"<?=ns(__('Indicates the connection status of the modem device'))?>"
};
window.PUSH_CONFIG={publicKey:"<?=PUSH_PUBLIC_KEY?>"};
</script>
<script src="include/js/modal.js" defer></script>
<script src="include/js/base.js" defer></script>
</head>
<body>
<div id="menuToggle">≡ <?php _nsl('Menu');?></div>
<div id="container">
<?php include("menu.php");?>
<div id="main">
<h2><?php _nsl('Fax Dashboard');?></h2>
<div class="dashboard-grid">
<?php
function recentTable($title,$data,$link,$isInbox){
global $statusMap,$contactMap;
echo"<div class='dashboard-col'><div class='table-wrapper'><table>";
echo"<thead><tr><th class='faxlist-top' colspan='2'><a class='menulink' href='".ns($link)."'>".ns($title)."</a></th></tr></thead><tbody>";
if(!$data){
echo"<tr class='index-msg'><td colspan='2'>".ns(__('No faxes'))."</td></tr>";
}else{
$i=0;
foreach($data as $f){
$row=($i++%2)?'row-classic':'row-alternate';
$real=$f['path'];
$url='include/service/fax_view.php?mode=viewer&id='.faxViewToken($real);
$safeFileName=ns(basename($real));
$baseName=pathinfo($safeFileName,PATHINFO_FILENAME);
$displayName=resolveContactName($f['number'],$contactMap);
if($isInbox){
$newBadge=!$f['isRead']?"<span class='fax-new'>N</span>":"";
$iconHtml="<div class='icon-wrapper'><img src='img/box_file.png' class='fax-icon'>{$newBadge}</div>";
}else{
$metaStatus=$f['metaStatus']??'';
$badge=match($metaStatus){
'scheduled'=>'🕒',
'sent'=>'🟡',
'success'=>'🟢',
'failed'=>'🔴',
'scheduled_cancel'=>'🚫',
default=>'⚫️',
};
$statusLabel=match($metaStatus){
'scheduled'=>'Scheduled',
'sent'=>'Sending',
'success'=>'Success',
'failed'=>'Failed',
'scheduled_cancel'=>'Cancelled',
default=>'Unknown',
};
$iconHtml="<div class='icon-wrapper'><img src='img/box_file.png'><span class='status-badge' title='".ns($statusLabel)."'>".ns($badge)."</span></div>";
}
$cls=($isInbox&&!$f['isRead'])?'fax-unread':'fax-read';
$memoIcon='';
if(!empty($f['hasMemo'])){
$memoIcon="<span class='memo-icon' data-memo='".ns($f['memo'])."' data-date='".ns($f['date'])."' onclick='openMemoAlert(this)'><span class='fax-memo'>M</span></span>";
}
echo"<tr class='".ns($row)."'>";
?>
<td class="command_button"><a href="javascript:void(0);" onclick="openFaxViewer('<?=ns($url)?>')" class="menulink fax-link <?=ns($cls)?>">
<?=$iconHtml?></a>
</td>
<td><a href="javascript:void(0);" onclick="openFaxViewer('<?=ns($url)?>')" class="menulink fax-link <?=ns($cls)?>">
<strong><?=$displayName?></strong></a> <?=$memoIcon?><br>
<span class="fax-date"><?=ns($f['date'])?></span></td>
<?php
echo"</tr>";
}}
echo"</tbody></table></div></div>";
}
if(has_permission('inboxmenu'))recentTable(ns(__('Recent Incoming Faxes')),$recentInbox,'fax_inbox.php',true);
if(has_permission('outboxmenu'))recentTable(ns(__('Recent Sent Faxes')),$recentOutbox,'fax_outbox.php',false);
?>
</div>
<div class="dashboard-bottom-grid">
<?php if(has_permission('inboxmenu')):?>
<div class="dashboard-bottom-col">
<div class="table-wrapper">
<table>
<thead><tr><th class="faxlist-top"><a class='menulink' href='fax_queue_in.php'><?php _nsl('Incoming Queue');?></a></th></tr></thead>
<tbody>
<?php $incomingCount=count($incomingFaxes);?>
<tr class="index-msg">
<td>
<a class='menulink' href='fax_queue_in.php'>
<?php
if($incomingCount==0){_nsl('No faxes in the incoming queue');}else{echo ns($incomingCount).' ';_nsl('incoming faxes pending');}
?>
</a></td></tr></tbody>
</table>
</div></div>
<?php endif;?>
<?php if(has_permission('outboxmenu')):?>
<div class="dashboard-bottom-col">
<div class="table-wrapper">
<table>
<thead><tr><th class="faxlist-top"><a class='menulink' href='fax_queue_out.php'><?php _nsl('Outgoing Queue');?></a></th></tr></thead>
<tbody>
<?php $outgoingCount=count($modemStatusTable);?>
<tr class="index-msg">
<td>
<a class='menulink' href='fax_queue_out.php'>
<?php
if($outgoingCount==0){_nsl('No faxes in the outgoing queue');}else{echo ns($outgoingCount).' ';_nsl('outgoing faxes pending');}
?>
</a></td></tr></tbody>
</table>
</div></div>
<?php endif;?>
</div>
<div class="dashboard-bottom-grid">
<div class="dashboard-bottom-col">
<div class="table-wrapper">
<table>
<thead><tr><th class="faxlist-top" colspan="2"><?php _nsl('Last Fax Activity');?></th></tr></thead>
<tbody>
<tr class="fax_status-trclass">
<td><strong><?php _nsl('Incoming');?></strong><br><?=ns($lastIncomingTime)?></td>
<td><strong><?php _nsl('Outgoing');?></strong><br><?=ns($lastOutgoingTime)?></td>
</tr>
</tbody>
</table>
</div></div>
<div class="dashboard-bottom-col">
<div class="table-wrapper">
<table>
<thead><tr><th class="faxlist-top"><?php _nsl('Disk Usage');?></th></tr></thead>
<tbody>
<tr class="fax_status-trclass">
<td>
<div class="disk-percent <?=ns($diskClass)?>"><?=ns($diskUsedPct)?>%</div>
<div class="disk-bar"><div class="disk-bar-fill <?=ns($diskClass)?>" style="width:<?=ns($diskUsedPct)?>%"></div></div>
<div class="disk-free"><?=ns(round($diskFree/1024/1024/1024,1))?><?php _nsl('GB free');?></div>
</td></tr>
</tbody>
</table>
</div></div></div>
<div class="dashboard-bottom-grid">
<div class="dashboard-bottom-col">
<div class="table-wrapper">
<table>
<thead><tr><th class="faxlist-top"><?php _nsl('Fax Status');?></th></tr></thead>
<tbody>
<tr class="index-msg"><td>
<div class="fax-status-bar">
<div class="fax-status-item">
<div class="fax-status-icon <?=$overallStatus?'ok':'fail'?>"><span class="help-text" data-help="help1"><?=$overallStatus?'✔':'⚠'?></span></div>
<div class="fax-status-label"><span class="help-text" data-help="help1">FAX<br><?php _nsl('Overall Status');?></span></div>
</div>
<div class="fax-status-item">
<div class="fax-status-icon <?=$schedulerRunning?'ok':'fail'?>"><span class="help-text" data-help="help2"><?=$schedulerRunning?'✔':'⚠'?></span></div>
<div class="fax-status-label"><span class="help-text" data-help="help2">HylaFAX<br><?php _nsl('Scheduler');?></span></div>
</div>
<div class="fax-status-item">
<div class="fax-status-icon <?=$hfaxdRunning?'ok':'fail'?>"><span class="help-text" data-help="help3"><?=$hfaxdRunning?'✔':'⚠'?></span></div>
<div class="fax-status-label"><span class="help-text" data-help="help3">HylaFAX<br><?php _nsl('Server Daemon');?></span></div>
</div>
<?php foreach($usbModemStatus as $m=>$ok):?>
<div class="fax-status-item">
<div class="fax-status-icon <?=$ok?'ok':'fail'?>"><span class="help-text" data-help="help4"><?=$ok?'✔':'⚠'?></span></div>
<div class="fax-status-label"><span class="help-text" data-help="help4"><?=ns($m)?><br><?php _nsl('Connect');?></span></div>
</div>
<?php endforeach;?>
</div>
</td></tr>
</tbody>
</table>
</div></div></div></div></div>
<?php include('include/web/modal.php');?>
<?php include('include/web/footer.php');?>
</body>
</html>
