[2026-06-04] eagle-dashboard: port 80 (pf redirect), memory tracking, fix port 3000→8880 (Virfield conflict)
This commit is contained in:
@@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
**Goal:** Replace `localhost` with a visual control panel that shows health, enables management (start/stop/restart) of all 25+ Eagle LaunchAgent services, and displays network security posture per service.
|
**Goal:** Replace `localhost` with a visual control panel that shows health, enables management (start/stop/restart) of all 25+ Eagle LaunchAgent services, and displays network security posture per service.
|
||||||
|
|
||||||
**Architecture:** FastAPI backend reads a YAML service registry and checks health via HTTP/port/process checks every 30s. HTMX + Tailwind CDN frontend with three tabs: **Services** (health cards + control), **Pages** (local page tester — URL input + iframe), **Files** (embedded FileBrowser). Origin-aware auth middleware: `127.0.0.1` → free access; external via cloudflared `dash.qentra.top` → Bearer token required. LaunchAgent on `:3000`. FileBrowser Docker container on `:8181`.
|
**Architecture:** FastAPI backend reads a YAML service registry and checks health + memory via HTTP/port/process checks every 30s. HTMX + Tailwind CDN frontend with three tabs: **Services** (health cards with memory usage + control), **Pages** (local page tester — URL input + iframe), **Files** (embedded FileBrowser). Origin-aware auth middleware: `127.0.0.1` → free access; external via cloudflared `dash.qentra.top` → Bearer token required.
|
||||||
|
|
||||||
|
Runs internally on `127.0.0.1:8880`. macOS `pf` anchor redirects `localhost:80` → `:8880` so it's accessible at plain `http://localhost` (replacing macOS default httpd). Cloudflared tunnel points directly to `:8880`. FileBrowser Docker container on `:8181`.
|
||||||
|
|
||||||
**Tech Stack:** Python 3.11, FastAPI, uvicorn, HTMX 2, Tailwind CDN, Alpine.js, PyYAML, psutil, httpx, python-dotenv
|
**Tech Stack:** Python 3.11, FastAPI, uvicorn, HTMX 2, Tailwind CDN, Alpine.js, PyYAML, psutil, httpx, python-dotenv
|
||||||
|
|
||||||
@@ -75,10 +77,10 @@ install:
|
|||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
uvicorn main:app --reload --host 127.0.0.1 --port 3000
|
uvicorn main:app --reload --host 127.0.0.1 --port 8880
|
||||||
|
|
||||||
prod:
|
prod:
|
||||||
uvicorn main:app --host 127.0.0.1 --port 3000 --workers 1
|
uvicorn main:app --host 127.0.0.1 --port 8880 --workers 1
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
cp com.eagle.dashboard.plist ~/Library/LaunchAgents/
|
cp com.eagle.dashboard.plist ~/Library/LaunchAgents/
|
||||||
@@ -106,6 +108,95 @@ git init && git add . && git commit -m "feat: project bootstrap"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Task 1b: Port 80 → 8880 via macOS pf Redirect
|
||||||
|
|
||||||
|
**Goal:** Make `http://localhost` (port 80) serve the dashboard without running Python as root. Uses macOS `pf` packet filter to NAT-redirect `127.0.0.1:80` → `127.0.0.1:8880`. Replaces (disables) the macOS built-in httpd.
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `/etc/pf.anchors/eagle-localhost`
|
||||||
|
- Modify: `/etc/pf.conf` (add anchor include)
|
||||||
|
- Create: `/Library/LaunchDaemons/com.eagle.pf-redirect.plist`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Disable macOS default httpd (if running)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo launchctl unload -w /System/Library/LaunchDaemons/org.apache.httpd.plist 2>/dev/null || true
|
||||||
|
sudo apachectl stop 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no process on port 80 afterwards.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write pf anchor rules**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo tee /etc/pf.anchors/eagle-localhost <<'EOF'
|
||||||
|
rdr pass on lo0 inet proto tcp from any to 127.0.0.1 port 80 -> 127.0.0.1 port 8880
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Load the anchor into pf.conf**
|
||||||
|
|
||||||
|
Check if `/etc/pf.conf` already has an anchor block. Add if missing:
|
||||||
|
```bash
|
||||||
|
grep -q 'eagle-localhost' /etc/pf.conf || sudo tee -a /etc/pf.conf <<'EOF'
|
||||||
|
|
||||||
|
# Eagle Dashboard port 80 redirect
|
||||||
|
rdr-anchor "eagle-localhost"
|
||||||
|
load anchor "eagle-localhost" from "/etc/pf.anchors/eagle-localhost"
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable pf and load:
|
||||||
|
```bash
|
||||||
|
sudo pfctl -ef /etc/pf.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Create LaunchDaemon to reload pf at boot**
|
||||||
|
|
||||||
|
`/Library/LaunchDaemons/com.eagle.pf-redirect.plist`:
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key><string>com.eagle.pf-redirect</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/sbin/pfctl</string>
|
||||||
|
<string>-ef</string>
|
||||||
|
<string>/etc/pf.conf</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key><true/>
|
||||||
|
<key>StandardOutPath</key><string>/tmp/eagle-pf.log</string>
|
||||||
|
<key>StandardErrorPath</key><string>/tmp/eagle-pf.log</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo launchctl load /Library/LaunchDaemons/com.eagle.pf-redirect.plist
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Verify**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s http://localhost/api/health # should work once eagle-dash is running
|
||||||
|
sudo pfctl -s nat # should show the rdr rule
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: redirect rule visible in pfctl output.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit plist to repo**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Developer/eagle-dash
|
||||||
|
cp /Library/LaunchDaemons/com.eagle.pf-redirect.plist .
|
||||||
|
git add com.eagle.pf-redirect.plist
|
||||||
|
git commit -m "feat: pf redirect port 80 → 8880 (LaunchDaemon + anchor)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Task 2: Service Registry YAML
|
## Task 2: Service Registry YAML
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
@@ -367,14 +458,14 @@ cd ~/Developer/eagle-dash
|
|||||||
just dev &
|
just dev &
|
||||||
sleep 2
|
sleep 2
|
||||||
# Should work (localhost):
|
# Should work (localhost):
|
||||||
curl -s http://127.0.0.1:3000/api/health
|
curl -s http://127.0.0.1:8880/api/health
|
||||||
|
|
||||||
# Simulate external (should 401):
|
# Simulate external (should 401):
|
||||||
curl -s -H "X-Forwarded-For: 1.2.3.4" http://127.0.0.1:3000/api/health
|
curl -s -H "X-Forwarded-For: 1.2.3.4" http://127.0.0.1:8880/api/health
|
||||||
|
|
||||||
# With token (should work):
|
# With token (should work):
|
||||||
TOKEN=$(grep EAGLE_DASH_TOKEN .env | cut -d= -f2)
|
TOKEN=$(grep EAGLE_DASH_TOKEN .env | cut -d= -f2)
|
||||||
curl -s -H "X-Forwarded-For: 1.2.3.4" -H "Authorization: Bearer $TOKEN" http://127.0.0.1:3000/api/health
|
curl -s -H "X-Forwarded-For: 1.2.3.4" -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8880/api/health
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: first call `{"status":"ok"}`, second `{"error":"unauthorized"}`, third `{"status":"ok"}`.
|
Expected: first call `{"status":"ok"}`, second `{"error":"unauthorized"}`, third `{"status":"ok"}`.
|
||||||
@@ -560,12 +651,20 @@ import httpx
|
|||||||
import psutil
|
import psutil
|
||||||
from registry import Service
|
from registry import Service
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ChildProc:
|
||||||
|
pid: int
|
||||||
|
name: str
|
||||||
|
memory_mb: float
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ServiceStatus:
|
class ServiceStatus:
|
||||||
id: str
|
id: str
|
||||||
healthy: bool
|
healthy: bool
|
||||||
detail: str = ""
|
detail: str = ""
|
||||||
latency_ms: Optional[float] = None
|
latency_ms: Optional[float] = None
|
||||||
|
memory_mb: Optional[float] = None # RSS of main process, MB
|
||||||
|
children: list[ChildProc] = field(default_factory=list)
|
||||||
|
|
||||||
def check_port(port: int, host: str = "127.0.0.1", timeout: float = 1.0) -> bool:
|
def check_port(port: int, host: str = "127.0.0.1", timeout: float = 1.0) -> bool:
|
||||||
try:
|
try:
|
||||||
@@ -574,15 +673,36 @@ def check_port(port: int, host: str = "127.0.0.1", timeout: float = 1.0) -> bool
|
|||||||
except OSError:
|
except OSError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check_process(name: str) -> bool:
|
def _rss_mb(proc: psutil.Process) -> float:
|
||||||
|
try:
|
||||||
|
return proc.memory_info().rss / 1024 / 1024
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def _children_info(proc: psutil.Process) -> list[ChildProc]:
|
||||||
|
result = []
|
||||||
|
try:
|
||||||
|
for child in proc.children(recursive=True):
|
||||||
|
result.append(ChildProc(
|
||||||
|
pid=child.pid,
|
||||||
|
name=child.name(),
|
||||||
|
memory_mb=round(_rss_mb(child), 1),
|
||||||
|
))
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
def check_process(name: str) -> tuple[bool, Optional[float], list[ChildProc]]:
|
||||||
for proc in psutil.process_iter(["name", "cmdline"]):
|
for proc in psutil.process_iter(["name", "cmdline"]):
|
||||||
try:
|
try:
|
||||||
cmdline = " ".join(proc.info["cmdline"] or [])
|
cmdline = " ".join(proc.info["cmdline"] or [])
|
||||||
if name in proc.info["name"] or name in cmdline:
|
if name in proc.info["name"] or name in cmdline:
|
||||||
return True
|
mem = round(_rss_mb(proc), 1)
|
||||||
|
children = _children_info(proc)
|
||||||
|
return True, mem, children
|
||||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||||
pass
|
pass
|
||||||
return False
|
return False, None, []
|
||||||
|
|
||||||
def check_launchctl(label: str) -> bool:
|
def check_launchctl(label: str) -> bool:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
@@ -601,23 +721,45 @@ def check_http(url: str, timeout: float = 3.0) -> tuple[bool, float]:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False, 0.0
|
return False, 0.0
|
||||||
|
|
||||||
|
def _proc_by_label(label: str) -> Optional[psutil.Process]:
|
||||||
|
"""Find process whose cmdline contains the launchd label."""
|
||||||
|
for proc in psutil.process_iter(["cmdline"]):
|
||||||
|
try:
|
||||||
|
cmdline = " ".join(proc.info["cmdline"] or [])
|
||||||
|
if label in cmdline:
|
||||||
|
return proc
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
async def check_service(svc: Service) -> ServiceStatus:
|
async def check_service(svc: Service) -> ServiceStatus:
|
||||||
h = svc.health
|
h = svc.health
|
||||||
if h.type == "http":
|
if h.type == "http":
|
||||||
ok, ms = check_http(h.url)
|
ok, ms = check_http(h.url)
|
||||||
|
# also try to find process by port for memory info
|
||||||
|
proc = _proc_by_label(svc.label) if svc.label else None
|
||||||
|
mem = round(_rss_mb(proc), 1) if proc else None
|
||||||
|
children = _children_info(proc) if proc else []
|
||||||
return ServiceStatus(id=svc.id, healthy=ok, latency_ms=ms,
|
return ServiceStatus(id=svc.id, healthy=ok, latency_ms=ms,
|
||||||
|
memory_mb=mem, children=children,
|
||||||
detail="HTTP ok" if ok else "HTTP failed")
|
detail="HTTP ok" if ok else "HTTP failed")
|
||||||
if h.type == "port":
|
if h.type == "port":
|
||||||
ok = check_port(svc.port)
|
ok = check_port(svc.port)
|
||||||
return ServiceStatus(id=svc.id, healthy=ok,
|
proc = _proc_by_label(svc.label) if svc.label else None
|
||||||
|
mem = round(_rss_mb(proc), 1) if proc else None
|
||||||
|
children = _children_info(proc) if proc else []
|
||||||
|
return ServiceStatus(id=svc.id, healthy=ok, memory_mb=mem, children=children,
|
||||||
detail=f"port {svc.port} {'open' if ok else 'closed'}")
|
detail=f"port {svc.port} {'open' if ok else 'closed'}")
|
||||||
if h.type == "process":
|
if h.type == "process":
|
||||||
ok = check_process(h.name)
|
ok, mem, children = check_process(h.name)
|
||||||
return ServiceStatus(id=svc.id, healthy=ok,
|
return ServiceStatus(id=svc.id, healthy=ok, memory_mb=mem, children=children,
|
||||||
detail=f"process '{h.name}' {'found' if ok else 'not found'}")
|
detail=f"process '{h.name}' {'found' if ok else 'not found'}")
|
||||||
if h.type == "launchctl":
|
if h.type == "launchctl":
|
||||||
ok = check_launchctl(svc.label)
|
ok = check_launchctl(svc.label)
|
||||||
return ServiceStatus(id=svc.id, healthy=ok,
|
proc = _proc_by_label(svc.label) if ok else None
|
||||||
|
mem = round(_rss_mb(proc), 1) if proc else None
|
||||||
|
children = _children_info(proc) if proc else []
|
||||||
|
return ServiceStatus(id=svc.id, healthy=ok, memory_mb=mem, children=children,
|
||||||
detail="loaded" if ok else "not loaded")
|
detail="loaded" if ok else "not loaded")
|
||||||
return ServiceStatus(id=svc.id, healthy=False, detail=f"unknown check type: {h.type}")
|
return ServiceStatus(id=svc.id, healthy=False, detail=f"unknown check type: {h.type}")
|
||||||
```
|
```
|
||||||
@@ -743,6 +885,11 @@ async def list_services():
|
|||||||
"healthy": status.healthy if status else None,
|
"healthy": status.healthy if status else None,
|
||||||
"detail": status.detail if status else "pending",
|
"detail": status.detail if status else "pending",
|
||||||
"latency_ms": status.latency_ms if status else None,
|
"latency_ms": status.latency_ms if status else None,
|
||||||
|
"memory_mb": status.memory_mb if status else None,
|
||||||
|
"children": [
|
||||||
|
{"pid": c.pid, "name": c.name, "memory_mb": c.memory_mb}
|
||||||
|
for c in (status.children if status else [])
|
||||||
|
],
|
||||||
})
|
})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -787,8 +934,8 @@ async def self_health():
|
|||||||
cd ~/Developer/eagle-dash
|
cd ~/Developer/eagle-dash
|
||||||
just dev &
|
just dev &
|
||||||
sleep 2
|
sleep 2
|
||||||
curl -s http://localhost:3000/api/health
|
curl -s http://localhost:8880/api/health
|
||||||
curl -s http://localhost:3000/api/services | python3 -m json.tool | head -40
|
curl -s http://localhost:8880/api/services | python3 -m json.tool | head -40
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: `{"status":"ok","services_registered":14}` and a JSON array of services.
|
Expected: `{"status":"ok","services_registered":14}` and a JSON array of services.
|
||||||
@@ -885,7 +1032,19 @@ The UI uses Tailwind CDN (no build step), HTMX for polling, Alpine.js for intera
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Detail + latency -->
|
<!-- Detail + latency -->
|
||||||
<p class="text-xs text-slate-400 mb-3" x-text="svc.detail + (svc.latency_ms ? ' · ' + Math.round(svc.latency_ms) + 'ms' : '')"></p>
|
<p class="text-xs text-slate-400 mb-1" x-text="svc.detail + (svc.latency_ms ? ' · ' + Math.round(svc.latency_ms) + 'ms' : '')"></p>
|
||||||
|
|
||||||
|
<!-- Memory usage -->
|
||||||
|
<template x-if="svc.memory_mb">
|
||||||
|
<div class="mb-2">
|
||||||
|
<span class="text-xs text-slate-500" x-text="svc.memory_mb + ' MB'"></span>
|
||||||
|
<template x-if="svc.children && svc.children.length">
|
||||||
|
<span class="text-xs text-slate-600 ml-1"
|
||||||
|
x-text="'+ ' + svc.children.length + ' child' + (svc.children.length > 1 ? 'ren' : '') + ' (' + svc.children.reduce((a,c) => a+c.memory_mb, 0).toFixed(1) + ' MB)'">
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Network access badge -->
|
<!-- Network access badge -->
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
@@ -1061,7 +1220,7 @@ loadPage() {
|
|||||||
```bash
|
```bash
|
||||||
cd ~/Developer/eagle-dash
|
cd ~/Developer/eagle-dash
|
||||||
just dev
|
just dev
|
||||||
open http://localhost:3000
|
open http://localhost:8880
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: Dark dashboard with tab bar. Services tab shows cards by category. Pages tab shows URL input with iframe. Files tab shows FileBrowser.
|
Expected: Dark dashboard with tab bar. Services tab shows cards by category. Pages tab shows URL input with iframe. Files tab shows FileBrowser.
|
||||||
@@ -1136,7 +1295,7 @@ And add a "logs" button to each service card (after the restart/stop buttons):
|
|||||||
- [ ] **Step 2: Test log panel**
|
- [ ] **Step 2: Test log panel**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
open http://localhost:3000
|
open http://localhost:8880
|
||||||
```
|
```
|
||||||
|
|
||||||
Click "logs" on the PostgreSQL card. Expected: bottom drawer opens with last 200 lines of the postgres log.
|
Click "logs" on the PostgreSQL card. Expected: bottom drawer opens with last 200 lines of the postgres log.
|
||||||
@@ -1207,7 +1366,7 @@ Expected: `launchctl load` succeeds.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
launchctl list com.eagle.dashboard
|
launchctl list com.eagle.dashboard
|
||||||
curl -s http://localhost:3000/api/health
|
curl -s http://localhost:8880/api/health
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: PID shown in launchctl list; `{"status":"ok"}` from curl.
|
Expected: PID shown in launchctl list; `{"status":"ok"}` from curl.
|
||||||
@@ -1222,7 +1381,7 @@ Add to `services.yaml` under `# ── Infrastructure`:
|
|||||||
port: 3000
|
port: 3000
|
||||||
health:
|
health:
|
||||||
type: http
|
type: http
|
||||||
url: "http://localhost:3000/api/health"
|
url: "http://localhost:8880/api/health"
|
||||||
access: localhost
|
access: localhost
|
||||||
critical: false
|
critical: false
|
||||||
category: infra
|
category: infra
|
||||||
@@ -1233,13 +1392,13 @@ Add to `services.yaml` under `# ── Infrastructure`:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/Developer/eagle-dash
|
cd ~/Developer/eagle-dash
|
||||||
git add . && git commit -m "feat: LaunchAgent deploy — eagle-dashboard is live on port 3000"
|
git add . && git commit -m "feat: LaunchAgent deploy — eagle-dashboard is live on port 8880"
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] **Step 6: Open and verify**
|
- [ ] **Step 6: Open and verify**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
open http://localhost:3000
|
open http://localhost:8880
|
||||||
```
|
```
|
||||||
|
|
||||||
Expected: Dashboard loads, eagle-dashboard itself shows as a green card.
|
Expected: Dashboard loads, eagle-dashboard itself shows as a green card.
|
||||||
@@ -1353,7 +1512,7 @@ git commit -m "feat: FileBrowser Docker container on :8181"
|
|||||||
|
|
||||||
## Task 10: Cloudflared Tunnel for dash.qentra.top
|
## Task 10: Cloudflared Tunnel for dash.qentra.top
|
||||||
|
|
||||||
**Goal:** Expose `dash.qentra.top` via cloudflared → `localhost:3000`. External requests are token-gated by auth middleware (Task 2a).
|
**Goal:** Expose `dash.qentra.top` via cloudflared → `localhost:8880`. External requests are token-gated by auth middleware (Task 2a).
|
||||||
|
|
||||||
- [ ] **Step 1: Add ingress rule to cloudflared config**
|
- [ ] **Step 1: Add ingress rule to cloudflared config**
|
||||||
|
|
||||||
@@ -1362,11 +1521,11 @@ Check existing config:
|
|||||||
cat ~/.cloudflared/config.yml
|
cat ~/.cloudflared/config.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
Add a new ingress entry for the dashboard:
|
Add a new ingress entry for the dashboard (points directly to internal port, bypassing pf):
|
||||||
```yaml
|
```yaml
|
||||||
ingress:
|
ingress:
|
||||||
- hostname: dash.qentra.top
|
- hostname: dash.qentra.top
|
||||||
service: http://localhost:3000
|
service: http://localhost:8880
|
||||||
# ... existing rules ...
|
# ... existing rules ...
|
||||||
- service: http_status:404
|
- service: http_status:404
|
||||||
```
|
```
|
||||||
@@ -1456,3 +1615,7 @@ Implementation: extend `registry.py` with a `bind_check` health type that calls
|
|||||||
- **FileBrowser iframe:** `FB_NOAUTH=true` env var name may differ by image version — check `docker run filebrowser/filebrowser --help` if the login screen still appears. Alternatively set `--username admin --password ""`.
|
- **FileBrowser iframe:** `FB_NOAUTH=true` env var name may differ by image version — check `docker run filebrowser/filebrowser --help` if the login screen still appears. Alternatively set `--username admin --password ""`.
|
||||||
- **Cookie auth and SameSite:** the cookie set by `/login` uses `samesite=strict`, which blocks it on cross-site redirects. Use `samesite=lax` instead if the login flow breaks in some browsers.
|
- **Cookie auth and SameSite:** the cookie set by `/login` uses `samesite=strict`, which blocks it on cross-site redirects. Use `samesite=lax` instead if the login flow breaks in some browsers.
|
||||||
- **Pages tab iframe:** `sandbox="allow-same-origin allow-scripts"` will block pages that set `X-Frame-Options: DENY`. This is expected — most external sites won't load; local services usually will.
|
- **Pages tab iframe:** `sandbox="allow-same-origin allow-scripts"` will block pages that set `X-Frame-Options: DENY`. This is expected — most external sites won't load; local services usually will.
|
||||||
|
- **Memory tracking imports:** `ChildProc` uses `field(default_factory=list)` — add `from dataclasses import dataclass, field` and `from typing import Optional` to `health.py`.
|
||||||
|
- **pf and SIP:** macOS SIP does not block `pfctl` on /etc/pf.conf edits, but some pf operations require rebooting after System Integrity Protection changes. Test with `sudo pfctl -s nat` after loading.
|
||||||
|
- **Port 80 and launchd ordering:** Eagle Dashboard LaunchAgent must start before the pf LaunchDaemon is useful — both happen at login, but pf redirect is independent (kernel-level), so ordering doesn't matter.
|
||||||
|
- **Memory for launchctl services:** `_proc_by_label` searches cmdline for the label string — this works for most launchd jobs whose argv[0] contains the plist label. If a service doesn't include its label in cmdline, memory will show `null`.
|
||||||
|
|||||||
Reference in New Issue
Block a user