diff --git a/public_html/admin/index.php b/public_html/admin/index.php
index c858e63..da67a7c 100644
--- a/public_html/admin/index.php
+++ b/public_html/admin/index.php
@@ -430,7 +430,7 @@ if ($action) {
case 'cal_feeds_list':
- j(JarvisDB::query("SELECT * FROM calendar_feeds ORDER BY source,name") ?? []);
+ j(JarvisDB::query("SELECT id,name,source,ics_url,username,(password != '' AND password IS NOT NULL) AS has_password,active,created_at FROM calendar_feeds ORDER BY source,name") ?? []);
case 'cal_feed_save':
$id = (int)($_POST['id'] ?? 0);
@@ -552,7 +552,7 @@ if ($action) {
} else { bad('Unknown cron worker'); }
} elseif ($wType === 'daemon' && $wId === 'arc_reactor' && $wAction === 'restart') {
shell_exec('systemctl restart jarvis-arc 2>&1');
- k(['ok'=>true,'msg'=>'Arc Reactor restarting via systemd']);
+ j(['ok'=>true,'msg'=>'Arc Reactor restarting via systemd']);
} elseif ($wType === 'daemon' && $wId === 'arc_reactor' && $wAction === 'setup') {
$log = '/var/log/jarvis/arc-setup.log';
$cmd = implode(' && ', [
@@ -567,7 +567,7 @@ if ($action) {
'systemctl restart jarvis-arc',
]);
shell_exec("($cmd) >> " . escapeshellarg($log) . " 2>&1 &");
- k(['ok'=>true,'msg'=>'Arc Reactor setup started — check ' . $log]);
+ j(['ok'=>true,'msg'=>'Arc Reactor setup started — check ' . $log]);
} elseif ($wType === 'agent' && $wAction === 'update_status') {
$ag = JarvisDB::single('SELECT version, status FROM registered_agents WHERE agent_id=?', [$wId]);
j(['ok'=>true,'version'=>$ag['version']??null,'status'=>$ag['status']??'unknown']);
@@ -1255,6 +1255,17 @@ if ($action) {
readfile($path);
exit;
+ case 'docs_download':
+ $path = '/var/www/jarvis-private/INFRASTRUCTURE-REFERENCE.md';
+ if (!file_exists($path)) bad('File not found', 404);
+ header('Content-Type: text/markdown');
+ header('Content-Disposition: attachment; filename="INFRASTRUCTURE-REFERENCE.md"');
+ header('Content-Length: ' . filesize($path));
+ header('X-Accel-Buffering: no');
+ ob_end_clean();
+ readfile($path);
+ exit;
+
default: bad('Unknown action');
}
}
@@ -1644,7 +1655,7 @@ select.filter-sel:focus{border-color:var(--cyan)}
INFRASTRUCTURE REFERENCE
Complete server map, credentials, deployment workflow, service configs, and phone system reference.
-
↓ DOWNLOAD INFRASTRUCTURE-REFERENCE.MD
@@ -2171,6 +2182,12 @@ let _alertFilter = 'active';
let _modalCb = null;
function esc(s){ return String(s||'').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); }
+// For values embedded inside a single-quoted JS string literal within an HTML attribute
+// (e.g. onclick="fn('${escJs(x)}')"). Escaping the quote via esc() alone is NOT enough:
+// the browser HTML-decodes the attribute before parsing it as JS, so '/" would
+// just turn back into a literal ' before the JS parser ever sees it. Backslash-escaping
+// survives that decode step, so JS-escape first, then HTML-escape on top.
+function escJs(s){ return esc(String(s||'').replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/\n/g,'\\n').replace(/\r/g,'\\r')); }
function ts(s){ if(!s) return '—'; const d=new Date(s); return d.toLocaleString('en-US',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}); }
function ago(s){ if(!s) return '—'; const sec=Math.floor((Date.now()-new Date(s))/1000); if(sec<60) return sec+'s ago'; if(sec<3600) return Math.floor(sec/60)+'m ago'; return Math.floor(sec/3600)+'h ago'; }
function fmtUp(s){ const d=Math.floor(s/86400),h=Math.floor((s%86400)/3600),m=Math.floor((s%3600)/60); return (d>0?d+'d ':'')+h+'h '+m+'m'; }
@@ -3078,7 +3095,7 @@ function renderNetwork() {