mirror of
https://github.com/myronblair/jarvis
synced 2026-07-29 01:03:14 -05:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 10350cbcd8 | |||
| 24bc876c1d | |||
| cfb5b2a3f9 | |||
| 0b1a19d9de |
@@ -1,5 +1,5 @@
|
||||
# INFRASTRUCTURE REFERENCE — COMPLETE SYSTEM MAP
|
||||
**Last Updated:** 2026-07-02
|
||||
**Last Updated:** 2026-07-06
|
||||
**Owner:** Myron Blair — myronblair@outlook.com
|
||||
|
||||
---
|
||||
@@ -17,6 +17,7 @@
|
||||
10. [Backup Systems](#10-backup-systems)
|
||||
11. [SSH Quick Reference](#11-ssh-quick-reference)
|
||||
12. [Critical Credentials Master List](#12-critical-credentials-master-list)
|
||||
13. [Git & Repository Management](#13-git--repository-management)
|
||||
|
||||
---
|
||||
|
||||
@@ -29,7 +30,7 @@ INTERNET
|
||||
[Cloudflare CDN] ──────────────────────────────────────────────────────────────
|
||||
│ (proxied DNS for public sites)
|
||||
│
|
||||
├─► [DigitalOcean 165.22.1.228] — CyberPanel/OLS — All websites (6 sites)
|
||||
├─► [DigitalOcean 165.22.1.228] — CyberPanel/OLS — All websites (7 sites)
|
||||
│
|
||||
└─► [FusionPBX 134.209.72.226] — FreeSWITCH PBX (SSH via DO relay)
|
||||
|
||||
@@ -73,7 +74,7 @@ FortiGate Port Forwards:
|
||||
| **OS** | Ubuntu 22.04 LTS |
|
||||
| **Panel** | CyberPanel (OpenLiteSpeed) |
|
||||
| **SSH** | `ssh root@165.22.1.228` — password: `Gonewalk1974!@#` |
|
||||
| **Purpose** | All public websites (6 sites) — webhook deploy for websites |
|
||||
| **Purpose** | All public websites (7 sites) — webhook deploy for websites |
|
||||
|
||||
**Key Paths:**
|
||||
- All sites: `/home/<domain>/public_html/`
|
||||
@@ -371,14 +372,14 @@ Vision is enabled via: `/etc/systemd/system/jarvis-arc.service.d/vision.conf`
|
||||
|
||||
All sites are at `/home/<domain>/public_html/` on DO (165.22.1.228).
|
||||
**Auto-deploy:** Push to `main` on GitHub → webhook → server pulls in ~1 min.
|
||||
**GitHub PAT:** `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` (expires ~2026-08-20)
|
||||
**GitHub PAT:** `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` (rotated 2026-07-05 — old PAT `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` was found exposed in `.git/config` on all 6 original sites and must be treated as compromised/revoked)
|
||||
|
||||
---
|
||||
|
||||
### jarvis.orbishosting.com — JARVIS AI Dashboard (MOVED TO PVE1 VM 211)
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **URL** | http://jarvis.orbishosting.com:1972 |
|
||||
| **URL** | http://jarvis.orbishosting.com (port 80 — old `:1972` reference was wrong, corrected 2026-07-04) |
|
||||
| **Path** | `/var/www/jarvis/ (on JARVIS VM 10.48.200.211)` |
|
||||
| **GitHub** | `myronblair/jarvis` |
|
||||
| **Login** | `myron / Joker1974!!!` |
|
||||
@@ -388,6 +389,24 @@ See Section 7 for full JARVIS details.
|
||||
|
||||
---
|
||||
|
||||
### worktracking.orbishosting.com — ChuckCo Time Keeper
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **URL** | https://worktracking.orbishosting.com |
|
||||
| **Path** | `/home/worktracking.orbishosting.com/public_html/` |
|
||||
| **GitHub** | `myronblair/chucko` (private) |
|
||||
| **Gitea** | `myron/chucko` (pull-mirror of GitHub) |
|
||||
| **Local clone** | `C:\Users\myron\repos\chucko` on admin Windows machine |
|
||||
| **Purpose** | Work-tracking app for a flat-rate 5-day (Fri–Thu) work week — self-reported hours via personal secret-URL tokens (no login), single shared admin password, phone-friendly screenshot pages for texting workers/payer |
|
||||
| **Admin URL** | `https://worktracking.orbishosting.com/admin/login.php` — password `Joker1974!!!` |
|
||||
| **DB** | `workt_track_db` / `workt_track_user` / `ZWCNMRP2N5NVPsghmve5aRS9` |
|
||||
| **Linked from Blair HQ** | `web.orbishosting.com` dashboard's "Websites" card has direct links to the admin login and the all-workers overview page (site-wide token `972f82cbf7832fdb2cffcdcc84129a4af69e30bd`) |
|
||||
| **Note** | Built 2026-07-05. `includes/config.php` (DB creds, admin password hash, site token) lives outside `public_html`/webroot and is intentionally NOT in the git repo. |
|
||||
| **Admin login rate limiting** | Added 2026-07-06 — `login_attempts` table (`ip_address`, `attempts`, `last_attempt`) in `workt_track_db`; 5 failed attempts locks that IP out for 15 minutes. |
|
||||
| **Code review (2026-07-06)** | 5 findings fixed and deployed: `w.php` mark_paid now rejects any `week_start` that isn't the true current week (was trusting client input); `admin/worker.php` no longer double-HTML-escapes the page title; `all.php` now shows a Paid/Unpaid badge per worker for the displayed week; `s.php`/`p.php`/`all.php` validate the `week` param before building dates (malformed input used to throw an uncaught DateTime exception); admin login rate-limited (see above). |
|
||||
|
||||
---
|
||||
|
||||
### tomsjavajive.com — Tom's Java Jive
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
@@ -471,11 +490,24 @@ See Section 7 for full JARVIS details.
|
||||
|
||||
## 7. JARVIS AI SYSTEM
|
||||
|
||||
**URL:** http://jarvis.orbishosting.com:1972
|
||||
**URL:** http://jarvis.orbishosting.com
|
||||
**Files:** `/var/www/jarvis/` on JARVIS VM (PVE1 VM 211 — 10.48.200.211, 8 cores, 16GB RAM)
|
||||
**DB:** `jarvis_db` — `jarvis_user / J4rv1s_Pr0t0c0l_2026!`
|
||||
**Login:** `myron / Joker1974!!!`
|
||||
**Admin portal:** http://jarvis.orbishosting.com:1972/admin
|
||||
**Admin portal:** http://jarvis.orbishosting.com/admin
|
||||
|
||||
### Security hardening (2026-07-06 code review)
|
||||
Full front-end + admin panel review found and fixed 8 issues, 2 of them live critical exposures:
|
||||
- **INFRASTRUCTURE-REFERENCE.md was publicly downloadable with zero auth** (static nginx path bypassed the admin session check entirely). Fixed: file moved to `/var/www/jarvis-private/INFRASTRUCTURE-REFERENCE.md` (owned `www-data:www-data`, mode 640, NOT under `public_html` so nginx never serves it directly), and the DOCS tab now downloads it via a new authenticated `docs_download` action in `admin/index.php` that gates on the existing `loggedIn()` check and streams the file with `readfile()`.
|
||||
- **Two full backup copies of the 5000-line admin panel (`index.php.bak2`, `index.php.bak.<date>`) were sitting in `public_html/admin/` and downloadable with no auth**, exposing the entire admin source/attack surface. Relocated to `/root/jarvis-old-backups/` (no secrets were found in them, so no credential rotation was needed).
|
||||
- `esc()` (the admin panel's JS HTML-escaper) doesn't escape `'`, so it doesn't protect values embedded inside a single-quoted JS string within an `onclick` attribute — HTML-decoding happens before the JS parser sees it. Added a proper `escJs()` helper (backslash + quote + newline escaping, then HTML-escape) and applied it to the Network/Alerts/Intents/Custom-News/Calendar-Feeds edit-modal `onclick` handlers, which were reachable by e.g. any device on the LAN setting a malicious DHCP/mDNS hostname.
|
||||
- Same class of bug on the front-end dashboard (`assets/js/jarvis-app.js`, `assets/js/panels/jarvis-agents.js`): device names and news article titles/sources were inserted into `innerHTML` completely unescaped — a rogue LAN device or a malicious/compromised news feed could inject script that runs with the logged-in session. Added `escHtml()`/`escJs()` helpers directly in `jarvis-app.js` and applied them to device names, VM names, agent hostnames, and news content.
|
||||
- `session.cookie_httponly` was **Off** server-wide (PHP default), meaning the actual session cookie — not just the app's own bearer token — was readable via `document.cookie` from any of the above XSS bugs. Fixed at the PHP-FPM level (`/etc/php/8.3/fpm/php.ini`): `session.cookie_httponly = 1`, `session.cookie_samesite = Lax`, `php8.3-fpm` restarted. Verified live: `Set-Cookie` now includes `HttpOnly; SameSite=Lax`.
|
||||
- `api.php` had `Access-Control-Allow-Origin: *` — tightened to an explicit allow-list of the real JARVIS origin only, with `Access-Control-Allow-Credentials: true` only sent when the origin matches.
|
||||
- Two admin actions (Arc Reactor restart/setup) called an undefined function `k()` instead of the real JSON responder `j()`, causing a PHP fatal error even though the underlying `systemctl` command still fired. Fixed (verified via direct API test — clean `{"ok":true,...}` response now).
|
||||
- Calendar feed passwords were stored and returned in plaintext via `cal_feeds_list`'s `SELECT *`. Changed to return a `has_password` boolean instead of the raw password (the edit UI never actually displayed the password back anyway — write-only field, "leave blank to keep").
|
||||
|
||||
All fixes verified via direct API testing (SSH + curl through the login/action flow) since this doesn't have a staging environment. Pushed to `myronblair/jarvis` master, commit `24bc876`.
|
||||
|
||||
### Architecture (end-to-end)
|
||||
|
||||
@@ -611,6 +643,7 @@ fs_cli -x "reloadacl" # reload ACL (safe)
|
||||
- WAN IP: 97.154.109.245 (dynamic)
|
||||
- DDNS: `orbisne.fortiddns.com` (FortiGate auto-updates on IP change)
|
||||
- Blocks: outbound port 53 (DNS) — MediaStack uses PVE1 dnsmasq (10.48.200.90) as resolver → 100.100.100.100
|
||||
- **Upstream DNS (changed 2026-07-05):** Network → DNS set to "Specify" mode — Primary `1.1.1.1` (Cloudflare), Secondary `8.8.4.4` (Google). Previously defaulted to the router itself (`10.48.200.1`)/ISP-provided servers. Admin: `https://10.48.200.1:9443` — `admin / Joker1974!!!`. Note: `8.8.8.8` specifically showed as "Unreachable" during setup (transient — ISP's own DNS servers were also showing high latency at that moment); `1.1.1.1`/`8.8.4.4` tested healthy and are what's live now.
|
||||
|
||||
**Port Forwards:**
|
||||
| External Port | Internal Destination | Purpose |
|
||||
@@ -661,8 +694,9 @@ fs_cli -x "reloadacl" # reload ACL (safe)
|
||||
- **Repo:** `myronblair/fusionpbx-config`
|
||||
- **Schedule:** Weekly, Sunday 5am
|
||||
- **Launcher:** `/usr/local/bin/fusionpbx-backup`
|
||||
- **Covers:** PostgreSQL dump (gzip, ~29MB) + FreeSWITCH configs
|
||||
- **Covers:** PostgreSQL dump (gzip, ~29-60MB) + FreeSWITCH configs
|
||||
- **Restore:** 10-phase wizard in `restore.sh`
|
||||
- **Known issue (found 2026-07-05):** this repo has grown to ~166MB on GitHub / ~196MB on Gitea because the DB dump gets committed directly into git history on every backup run (3 copies in history as of this writing, each ~60MB) rather than being excluded/rotated. Options not yet decided: gitignore the dump going forward, or purge it from history with `git filter-repo` + force-push (destructive, needs explicit sign-off).
|
||||
|
||||
### MSP360 Backup Status (Dashboard Integration)
|
||||
- **Client software:** MSP360 (CloudBerry) Backup CLI installed on all 6 hosts — PVE1, JARVIS (211), NovaCPX (110), Jellyfin (33), MediaStack (35), Homebridge (18)
|
||||
@@ -741,8 +775,9 @@ sshpass -p 'Joker1974!!!' ssh root@10.48.200.90 \
|
||||
| phpMyAdmin (DO) | https://165.22.1.228/phpmyadmin | myron | `Joker1974!!!` |
|
||||
| Proxmox PVE1 | https://orbisne.fortiddns.com:8006 | root | `Joker1974!!!` |
|
||||
| Proxmox PVE2 | https://10.48.200.91:8006 | root | `Joker1974!!!` |
|
||||
| JARVIS | http://jarvis.orbishosting.com:1972 | myron | `Joker1974!!!` |
|
||||
| JARVIS Admin | http://jarvis.orbishosting.com:1972/admin | myron | `Joker1974!!!` |
|
||||
| JARVIS | http://jarvis.orbishosting.com | myron | `Joker1974!!!` |
|
||||
| JARVIS Admin | http://jarvis.orbishosting.com/admin | myron | `Joker1974!!!` |
|
||||
| ChuckCo Time Keeper Admin | https://worktracking.orbishosting.com/admin/login.php | — | `Joker1974!!!` |
|
||||
| FusionPBX | https://fusion.orbishosting.com | admin | `fY7XP5swgtpbzrYLhkeVYkA4744` |
|
||||
| Home Assistant | http://orbisne.fortiddns.com:8123 | myron | (HA password) |
|
||||
| NovaCPX Admin | https://10.48.200.110:8882 | admin | `Admin2026!` |
|
||||
@@ -764,13 +799,14 @@ sshpass -p 'Joker1974!!!' ssh root@10.48.200.90 \
|
||||
| Epic Travel | `epic_travel_db` | (see config.php) | (see config.php) |
|
||||
| Epic/Parker Slingshot | `epic_parkersling` | `epic_parkersling` | `Joker1974!!!` |
|
||||
| NovaCPX | SQLite: `/var/lib/novacpx/panel.db` | — | — |
|
||||
| ChuckCo Time Keeper | `workt_track_db` | `workt_track_user` | `ZWCNMRP2N5NVPsghmve5aRS9` |
|
||||
| FusionPBX | PostgreSQL | `fusionpbx` | `pSJaF9mUJqPr4Sj5mwJyRqvCCpc` |
|
||||
| MySQL root (DO) | — | root | `b71e5c1a8c7457541b9c1db822de37adfa271926a38b6c20` |
|
||||
|
||||
### API Keys
|
||||
| Service | Key |
|
||||
|---------|-----|
|
||||
| GitHub PAT | `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` (exp ~2026-08-20) |
|
||||
| GitHub PAT | `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` (rotated 2026-07-05, scopes `repo`+`workflow`) |
|
||||
| JARVIS Agent Registration | `f846a9aaf7ce9a61742c63c87c4186052a71d2a580c65518` |
|
||||
| Proxmox API Token | `root@pam!jarvis=c45b5feb-f9a9-445d-a626-14fbb959f78b` |
|
||||
| HA Long-lived Token | `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzNmI0N2I1Njk5ZGQ0MTQ2ODMwZWFmYjZiYTQ1MjJkMSIsImlhdCI6MTc4MDIwMzU5NCwiZXhwIjoyMDk1NTYzNTk0fQ.sYRok-jRDlA4lFgWxLQELcEjkJNGQdprk6ZziLwLtXE` |
|
||||
@@ -796,4 +832,16 @@ sshpass -p 'Joker1974!!!' ssh root@10.48.200.90 \
|
||||
|
||||
---
|
||||
|
||||
## 13. GIT & REPOSITORY MANAGEMENT
|
||||
|
||||
**GitHub** (`myronblair`, 26 private repos as of 2026-07-05) is the permanent/source-of-truth storage. **Gitea** (`gitea.orbishosting.com`, hosted on the Synology NAS, login `myron / Joker1974!!!`) mirrors it one-way — GitHub → Gitea only, via pull-mirrors triggered by `POST /api/v1/repos/<owner>/<repo>/mirror-sync`. Never push directly to a Gitea remote.
|
||||
|
||||
**5 repos are Gitea-only by design, never pushed to GitHub** (deliberately kept off a third-party cloud service since they hold real credentials): `fortigate-config`, `infra-private`, `jarvis-secrets`, `msp360-config`, `proxmox-secrets`.
|
||||
|
||||
**Local clones** live on the admin Windows machine at `C:\Users\myron\repos\` (currently `chucko`, `web-dashboard` — more cloned there as needed). Periodic maintenance: `git gc --aggressive --prune=now` to keep loose objects packed.
|
||||
|
||||
**Per-site deploy pattern (the 7 DO-hosted sites + jarvis + web-dashboard):** `.git` metadata is relocated outside the public webroot (e.g. `/home/<site>/git-data` with a `gitdir:` pointer file left in `public_html/.git`) so `.git` itself is never web-accessible, while `git` commands run normally from inside `public_html`. Most sites deploy via a GitHub webhook → queue file → cron puller (~1 min); `chucko` (ChuckCo Time Keeper) currently has no auto-deploy hook — pushes are manual (SSH in, `git pull`/`push` directly).
|
||||
|
||||
**Known repo-hygiene issue:** `fusionpbx-config` bloated to ~166-196MB from repeatedly committing a large DB dump straight into history — see Section 10.
|
||||
|
||||
*This document contains sensitive credentials. Store securely and do not share.*
|
||||
|
||||
@@ -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)}
|
||||
<div class="card" style="padding:24px;margin:20px 0">
|
||||
<div style="font-size:0.7rem;letter-spacing:2px;color:var(--cyan);margin-bottom:8px">INFRASTRUCTURE REFERENCE</div>
|
||||
<div style="color:var(--text-dim);font-size:0.75rem;margin-bottom:16px">Complete server map, credentials, deployment workflow, service configs, and phone system reference.</div>
|
||||
<a href="downloads/INFRASTRUCTURE-REFERENCE.md" download="INFRASTRUCTURE-REFERENCE.md"
|
||||
<a href="?action=docs_download" download="INFRASTRUCTURE-REFERENCE.md"
|
||||
style="display:inline-block;padding:8px 20px;background:rgba(0,212,255,0.1);border:1px solid var(--cyan);color:var(--cyan);font-size:0.7rem;letter-spacing:2px;text-decoration:none">
|
||||
↓ DOWNLOAD INFRASTRUCTURE-REFERENCE.MD
|
||||
</a>
|
||||
@@ -2171,6 +2182,12 @@ let _alertFilter = 'active';
|
||||
let _modalCb = null;
|
||||
|
||||
function esc(s){ return String(s||'').replace(/&/g,'&').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() {
|
||||
<td class="ts">${ago(d.last_seen)}</td>
|
||||
<td><div class="actions-col">
|
||||
<button class="btn btn-xs" onclick="pingDev('${esc(d.ip)}',this)">PING</button>
|
||||
<button class="btn btn-xs btn-yellow" onclick="netModal(${d.id},'${esc(d.ip)}','${esc(d.alias||'')}','${esc(d.device_type||'')}')">NAME</button>
|
||||
<button class="btn btn-xs btn-yellow" onclick="netModal(${d.id},'${escJs(d.ip)}','${escJs(d.alias||'')}','${escJs(d.device_type||'')}')">NAME</button>
|
||||
<button class="btn btn-xs btn-red" onclick="delNet(${d.id},'${esc(name)}')">DEL</button>
|
||||
</div></td>`;
|
||||
}, null, null);
|
||||
@@ -3157,7 +3174,7 @@ async function loadAlerts() {
|
||||
<td class="ts">${ts(a.created_at)}</td>
|
||||
<td><div class="actions-col">
|
||||
${!a.resolved?`<button class="btn btn-xs btn-green" onclick="apiPost('alerts_resolve',{id:${a.id}},()=>{toast('Resolved','ok');loadAlerts()})">RESOLVE</button>`:''}
|
||||
<button class="btn btn-xs btn-yellow" onclick="alertModal(${a.id},'${esc(a.alert_type)}','${esc(a.title)}','${esc(a.message||'')}','${esc(a.severity)}')">EDIT</button>
|
||||
<button class="btn btn-xs btn-yellow" onclick="alertModal(${a.id},'${escJs(a.alert_type)}','${escJs(a.title)}','${escJs(a.message||'')}','${escJs(a.severity)}')">EDIT</button>
|
||||
<button class="btn btn-xs btn-red" onclick="apiPost('alerts_delete',{id:${a.id}},()=>{toast('Deleted','ok');loadAlerts()})">DEL</button>
|
||||
</div></td>`, null, null);
|
||||
}
|
||||
@@ -3267,7 +3284,7 @@ function renderIntents(intents) {
|
||||
<td>${i.active?'<span class="badge badge-green">ON</span>':'<span class="badge badge-dim">OFF</span>'}</td>
|
||||
<td><div class="actions-col">
|
||||
<button class="btn btn-xs" onclick="apiPost('intents_toggle',{id:${i.id}},()=>{toast('Toggled','ok');loadIntents()})">${i.active?'DISABLE':'ENABLE'}</button>
|
||||
<button class="btn btn-xs btn-yellow" onclick='intentModal(${i.id},"${esc(i.intent_name)}","${esc(i.pattern)}",${JSON.stringify(i.response_template||"")},"${esc(i.action_type)}",${i.priority},${i.active})'>EDIT</button>
|
||||
<button class="btn btn-xs btn-yellow" onclick="intentModal(${i.id},'${escJs(i.intent_name)}','${escJs(i.pattern)}','${escJs(i.response_template||'')}','${escJs(i.action_type)}',${i.priority},${i.active})">EDIT</button>
|
||||
<button class="btn btn-xs btn-red" onclick="apiPost('intents_delete',{id:${i.id}},()=>{toast('Deleted','ok');loadIntents()})">DEL</button>
|
||||
</div></td>`, null, null);
|
||||
}
|
||||
@@ -3499,7 +3516,7 @@ async function loadNews() {
|
||||
<div style="font-size:0.75rem">${esc(c.title)}</div>
|
||||
${c.url?`<div style="font-size:0.6rem;color:var(--dim)">${esc(c.url)}</div>`:''}
|
||||
</div>
|
||||
<button class="btn btn-xs btn-yellow" onclick='newsCustomModal(${c.id},"${esc(c.title)}","${esc(c.url||"")}")'>EDIT</button>
|
||||
<button class="btn btn-xs btn-yellow" onclick="newsCustomModal(${c.id},'${escJs(c.title)}','${escJs(c.url||'')}')">EDIT</button>
|
||||
<button class="btn btn-xs btn-red" onclick="apiPost('news_custom_delete',{id:${c.id}},()=>{toast('Deleted','ok');loadNews()})">DEL</button>
|
||||
</div>`).join('');
|
||||
}
|
||||
@@ -4884,7 +4901,7 @@ async function loadCalFeeds() {
|
||||
<td>${ts(f.last_sync)}</td>
|
||||
<td>${f.last_count||0}</td>
|
||||
<td>${f.active?'<span class="badge badge-green">ACTIVE</span>':'<span class="badge badge-red">PAUSED</span>'}</td>
|
||||
<td><button class="btn btn-xs" onclick='calFeedModal(${JSON.stringify(f)})'>EDIT</button>
|
||||
<td><button class="btn btn-xs" onclick="calFeedModal(${esc(JSON.stringify(f))})">EDIT</button>
|
||||
<button class="btn btn-xs btn-red" onclick="calFeedDel(${f.id})">DEL</button></td>
|
||||
</tr>`).join('')}</tbody></table>`;
|
||||
}
|
||||
|
||||
+6
-1
@@ -27,7 +27,12 @@ if (!$_skipSession) {
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
$_allowedOrigins = ['https://jarvis.orbishosting.com', 'http://jarvis.orbishosting.com'];
|
||||
$_origin = $_SERVER['HTTP_ORIGIN'] ?? '';
|
||||
if (in_array($_origin, $_allowedOrigins, true)) {
|
||||
header('Access-Control-Allow-Origin: ' . $_origin);
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
}
|
||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type, X-Session-Token');
|
||||
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
// ── GLOBALS ──────────────────────────────────────────────────────────
|
||||
function escHtml(s) {
|
||||
return String(s == null ? '' : s).replace(/&/g,'&').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)}')"). escHtml() alone isn't enough there: the browser
|
||||
// HTML-decodes the attribute before parsing it as JS, so an encoded quote would just
|
||||
// turn back into a literal ' before the JS parser ever sees it.
|
||||
function escJs(s) {
|
||||
return escHtml(String(s == null ? '' : s).replace(/\\/g,'\\\\').replace(/'/g,"\\'").replace(/\n/g,'\\n').replace(/\r/g,'\\r'));
|
||||
}
|
||||
let sessionToken = '';
|
||||
let sessionUser = '';
|
||||
let sessionId = 'session_' + Date.now();
|
||||
@@ -596,15 +606,15 @@ function renderNetworkStatus(n) {
|
||||
agent_id: d.agent_id, hostname: d.name};
|
||||
const lat = d.latency_ms ? ' · ' + d.latency_ms + 'ms' : '';
|
||||
const badge = d.source === 'agent'
|
||||
? `<span style="font-size:0.53rem;color:var(--cyan);letter-spacing:1px;margin-left:4px">${(d.agent_type||'AGENT').toUpperCase()}</span>` : '';
|
||||
? `<span style="font-size:0.53rem;color:var(--cyan);letter-spacing:1px;margin-left:4px">${escHtml((d.agent_type||'AGENT').toUpperCase())}</span>` : '';
|
||||
const del = d.deletable
|
||||
? `<button onclick="deleteNetworkDevice('${d.ip}',event)" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:0.9rem;padding:0 2px;opacity:0.5;flex-shrink:0" title="Remove">×</button>` : '';
|
||||
? `<button onclick="deleteNetworkDevice('${escJs(d.ip)}',event)" style="background:none;border:none;color:var(--red);cursor:pointer;font-size:0.9rem;padding:0 2px;opacity:0.5;flex-shrink:0" title="Remove">×</button>` : '';
|
||||
const bl = d.source === 'agent' ? 'border-left:2px solid ' + (alive ? 'var(--green)' : 'var(--red)') + ';' : '';
|
||||
return `<div class="device-item" data-ctx-key="${ctxKey}" onclick="selectContext('${ctxKey}')" style="${bl}display:flex;align-items:center">
|
||||
return `<div class="device-item" data-ctx-key="${ctxKey}" onclick="selectContext('${escJs(ctxKey)}')" style="${bl}display:flex;align-items:center">
|
||||
<div class="device-status ${alive?'on':'off'}" style="flex-shrink:0"></div>
|
||||
<div class="device-info" style="flex:1;min-width:0">
|
||||
<div class="device-name" style="display:flex;align-items:center">${d.name||d.ip}${badge}</div>
|
||||
<div class="device-ip">${d.ip||''}${lat}</div>
|
||||
<div class="device-name" style="display:flex;align-items:center">${escHtml(d.name||d.ip)}${badge}</div>
|
||||
<div class="device-ip">${escHtml(d.ip||'')}${lat}</div>
|
||||
</div>${del}
|
||||
</div>`;
|
||||
}
|
||||
@@ -684,7 +694,7 @@ async function loadProxmox() {
|
||||
type_label:vm.type||'qemu', uptime:vm.uptime||0};
|
||||
return `<div class="vm-card" data-ctx-key="${ctxKey}" onclick="selectContext('${ctxKey}')" title="Click to ask Jarvis about this VM">
|
||||
<div class="vm-header">
|
||||
<span class="vm-name">${vm.name}</span>
|
||||
<span class="vm-name">${escHtml(vm.name)}</span>
|
||||
<span style="color:${statusColor};font-size:0.65rem">● ${(vm.status||'').toUpperCase()}</span>
|
||||
</div>
|
||||
<div class="vm-metrics">
|
||||
@@ -1031,10 +1041,11 @@ async function loadNews() {
|
||||
const ctxKey = 'news_' + (cat + '_' + a.title).replace(/[^a-z0-9]/gi,'').slice(0,30);
|
||||
_panelCtx[ctxKey] = {type:'news', label:a.title,
|
||||
title:a.title, source:a.source, pub:a.pub||'', category:cat};
|
||||
const titleDisplay = a.title.length > 90 ? a.title.slice(0,87)+'…' : a.title;
|
||||
html += `<div class="news-item" data-ctx-key="${ctxKey}" onclick="selectContext('${ctxKey}')" title="Click to ask Jarvis about this story">
|
||||
<div class="news-source">${a.source}</div>
|
||||
<div class="news-title">${a.title.length > 90 ? a.title.slice(0,87)+'…' : a.title}</div>
|
||||
${a.pub ? '<div class="news-time">' + a.pub + '</div>' : ''}
|
||||
<div class="news-source">${escHtml(a.source)}</div>
|
||||
<div class="news-title">${escHtml(titleDisplay)}</div>
|
||||
${a.pub ? '<div class="news-time">' + escHtml(a.pub) + '</div>' : ''}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,8 +401,8 @@ function renderAgentsTab(agents, metrics) {
|
||||
style="flex-direction:column;align-items:stretch;border-left:3px solid ${alive ? 'var(--green)' : 'var(--red)'}">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
||||
<div style="width:8px;height:8px;border-radius:50%;background:${alive ? 'var(--green)' : 'var(--red)'};box-shadow:${alive ? '0 0 6px var(--green)' : 'none'};flex-shrink:0"></div>
|
||||
<span style="font-family:var(--font-mono);font-size:0.72rem;color:var(--text);flex:1">${ag.hostname}</span>
|
||||
<span style="font-size:0.58rem;color:var(--text-dim)">${ag.agent_type.toUpperCase()} · ${ag.ip_address}</span>
|
||||
<span style="font-family:var(--font-mono);font-size:0.72rem;color:var(--text);flex:1">${escHtml(ag.hostname)}</span>
|
||||
<span style="font-size:0.58rem;color:var(--text-dim)">${escHtml(ag.agent_type.toUpperCase())} · ${escHtml(ag.ip_address)}</span>
|
||||
<span style="font-size:0.58rem;color:${alive ? 'var(--green)' : 'var(--red)'};">${alive ? 'ONLINE' : 'OFFLINE'}</span>
|
||||
</div>
|
||||
${alive ? `<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:4px">
|
||||
@@ -425,8 +425,8 @@ function renderAgentsTab(agents, metrics) {
|
||||
${svcs ? `<div style="font-size:0.58rem">${svcs}</div>` : ''}
|
||||
</div>
|
||||
${alive ? `<div style="display:flex;gap:5px;margin-top:6px">
|
||||
<button onclick="event.stopPropagation();agentScreenshot('${ag.hostname}')" style="flex:1;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--cyan);font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer">◈ SCREENSHOT</button>
|
||||
<button onclick="event.stopPropagation();agentSysinfo('${ag.hostname}')" style="flex:1;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer">⚡ SYSINFO</button>
|
||||
<button onclick="event.stopPropagation();agentScreenshot('${escJs(ag.hostname)}')" style="flex:1;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--cyan);font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer">◈ SCREENSHOT</button>
|
||||
<button onclick="event.stopPropagation();agentSysinfo('${escJs(ag.hostname)}')" style="flex:1;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer">⚡ SYSINFO</button>
|
||||
</div>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
Reference in New Issue
Block a user