mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-29 18:06:21 +00:00
Compare commits
5 Commits
8f46bcb93b
...
877fb9e19a
| Author | SHA1 | Date | |
|---|---|---|---|
| 877fb9e19a | |||
| 5e4c0d2dac | |||
| de15937ccf | |||
| e51a310010 | |||
| 6034dfb0c3 |
|
|
@ -17,6 +17,7 @@ from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
CATALOG_FILE = Path(os.environ.get(
|
CATALOG_FILE = Path(os.environ.get(
|
||||||
"APPSTORE_CATALOG", "/usr/share/secubox/appstore/catalog.json"))
|
"APPSTORE_CATALOG", "/usr/share/secubox/appstore/catalog.json"))
|
||||||
|
|
@ -165,3 +166,73 @@ async def module(name: str):
|
||||||
else:
|
else:
|
||||||
raise HTTPException(status_code=404, detail=f"unknown module {name!r}")
|
raise HTTPException(status_code=404, detail=f"unknown module {name!r}")
|
||||||
return dict(st[name])
|
return dict(st[name])
|
||||||
|
|
||||||
|
|
||||||
|
# ── Lifecycle + config (Phase B/C) ──────────────────────────────────────────
|
||||||
|
# The API runs as the unprivileged `secubox` user; all privileged work goes
|
||||||
|
# through the validated root helper /usr/sbin/secubox-appstorectl via a narrow
|
||||||
|
# sudoers rule (start/stop/restart/enable/disable + write-config only).
|
||||||
|
ACTIONS = {"start", "stop", "restart", "enable", "disable"}
|
||||||
|
APPSTORECTL = "/usr/sbin/secubox-appstorectl"
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigIn(BaseModel):
|
||||||
|
content: str
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve(name: str, st: dict) -> str:
|
||||||
|
if name in st:
|
||||||
|
return name
|
||||||
|
alt = f"secubox-{name}"
|
||||||
|
if alt in st:
|
||||||
|
return alt
|
||||||
|
raise HTTPException(status_code=404, detail=f"unknown module {name!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def _appstorectl(args, input_text=None):
|
||||||
|
try:
|
||||||
|
r = subprocess.run(["sudo", "-n", APPSTORECTL, *args], input=input_text,
|
||||||
|
capture_output=True, text=True, timeout=90)
|
||||||
|
return r.returncode, (r.stdout or "").strip(), (r.stderr or "").strip()
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
return 1, "", str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def _config_path(name: str) -> Path:
|
||||||
|
short = name[len("secubox-"):] if name.startswith("secubox-") else name
|
||||||
|
return Path(f"/etc/secubox/{short}.toml")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/module/{name}/action/{verb}")
|
||||||
|
async def module_action(name: str, verb: str):
|
||||||
|
name = _resolve(name, compute_state())
|
||||||
|
if verb not in ACTIONS:
|
||||||
|
raise HTTPException(status_code=400, detail=f"unknown action {verb!r}")
|
||||||
|
rc, out, err = _appstorectl([verb, name])
|
||||||
|
if rc != 0:
|
||||||
|
raise HTTPException(status_code=500, detail=f"{verb} failed: {err or out}")
|
||||||
|
new = compute_state(force=True).get(name, {})
|
||||||
|
return {"status": "ok", "action": verb, "module": name,
|
||||||
|
"state": new.get("state"), "running": new.get("running"), "message": out}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/module/{name}/config")
|
||||||
|
async def get_config(name: str):
|
||||||
|
name = _resolve(name, compute_state())
|
||||||
|
p = _config_path(name)
|
||||||
|
try:
|
||||||
|
return {"module": name, "path": str(p), "exists": True,
|
||||||
|
"readable": True, "content": p.read_text(encoding="utf-8")}
|
||||||
|
except FileNotFoundError:
|
||||||
|
return {"module": name, "path": str(p), "exists": False, "readable": True, "content": ""}
|
||||||
|
except PermissionError:
|
||||||
|
return {"module": name, "path": str(p), "exists": True, "readable": False, "content": ""}
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/module/{name}/config")
|
||||||
|
async def put_config(name: str, body: ConfigIn):
|
||||||
|
name = _resolve(name, compute_state())
|
||||||
|
rc, out, err = _appstorectl(["write-config", name], input_text=body.content)
|
||||||
|
if rc != 0:
|
||||||
|
raise HTTPException(status_code=400, detail=f"config write failed: {err or out}")
|
||||||
|
return {"status": "ok", "module": name, "message": out}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,33 @@
|
||||||
|
secubox-appstore (0.2.2-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* fix(menu): install the navbar entry to /usr/share/secubox/menu.d (the
|
||||||
|
dir the hub reads) instead of /etc/secubox/menu.d, and place it in the
|
||||||
|
'root' navbar section (order 10) so the App Store is prominent.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Mon, 29 Jun 2026 20:00:00 +0200
|
||||||
|
|
||||||
|
secubox-appstore (0.2.1-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* fix: service needs NoNewPrivileges=no so the API can sudo the root
|
||||||
|
helper (the narrow sudoers rule + helper validation are the boundary);
|
||||||
|
postinst now try-restarts the service on upgrade so new code loads.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Mon, 29 Jun 2026 19:30:00 +0200
|
||||||
|
|
||||||
|
secubox-appstore (0.2.0-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* Phase B/C + navbar integration.
|
||||||
|
- UI: integrated shared sidebar (nav#sidebar + sidebar.js + crt-engine),
|
||||||
|
per-service quick actions (start/stop/restart/enable/disable) and a
|
||||||
|
config editor drawer (view/edit the module TOML, shows dependencies).
|
||||||
|
- api: POST /module/{name}/action/{verb}; GET/PUT /module/{name}/config.
|
||||||
|
- sbin/secubox-appstorectl: validated root helper (secubox-* units +
|
||||||
|
/etc/secubox/<name>.toml only; TOML-validates writes). Narrow sudoers
|
||||||
|
rule lets the unprivileged API invoke it.
|
||||||
|
- postinst rebuilds the hub menu cache so the App Store shows in the navbar.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Mon, 29 Jun 2026 19:00:00 +0200
|
||||||
|
|
||||||
secubox-appstore (0.1.1-1~bookworm1) bookworm; urgency=medium
|
secubox-appstore (0.1.1-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* fix(nginx): serve the catalog API from secubox-routes.d/ (the
|
* fix(nginx): serve the catalog API from secubox-routes.d/ (the
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ Standards-Version: 4.6.2
|
||||||
|
|
||||||
Package: secubox-appstore
|
Package: secubox-appstore
|
||||||
Architecture: all
|
Architecture: all
|
||||||
Depends: ${misc:Depends}, secubox-core (>= 1.0), python3, python3-fastapi | python3-pip, python3-uvicorn | python3-pip
|
Depends: ${misc:Depends}, secubox-core (>= 1.0), python3, python3-fastapi | python3-pip, python3-uvicorn | python3-pip, sudo
|
||||||
Description: SecuBox App Store — module catalog & lifecycle (Phase A)
|
Description: SecuBox App Store — module catalog & lifecycle (Phase A)
|
||||||
Categorized, tiered, searchable catalog of SecuBox modules with live
|
Categorized, tiered, searchable catalog of SecuBox modules with live
|
||||||
install/run state, served as a hub web UI. Phase A is read-only; install,
|
install/run state, served as a hub web UI. Phase A is read-only; install,
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,13 @@ set -e
|
||||||
case "$1" in
|
case "$1" in
|
||||||
configure)
|
configure)
|
||||||
install -d -m 0755 /usr/share/secubox/appstore /var/log/secubox /var/lib/secubox 2>/dev/null || true
|
install -d -m 0755 /usr/share/secubox/appstore /var/log/secubox /var/lib/secubox 2>/dev/null || true
|
||||||
|
# surface the App Store in the hub navbar (menu cache rebuild)
|
||||||
|
rm -f /var/cache/secubox/menu.json 2>/dev/null || true
|
||||||
|
# validate the sudoers drop-in (never leave a broken one)
|
||||||
|
visudo -cf /etc/sudoers.d/secubox-appstore >/dev/null 2>&1 || rm -f /etc/sudoers.d/secubox-appstore
|
||||||
systemctl daemon-reload 2>/dev/null || true
|
systemctl daemon-reload 2>/dev/null || true
|
||||||
systemctl enable --now secubox-appstore.service 2>/dev/null || true
|
systemctl enable --now secubox-appstore.service 2>/dev/null || true
|
||||||
|
systemctl try-restart secubox-appstore.service 2>/dev/null || true
|
||||||
if systemctl is-active --quiet nginx 2>/dev/null; then
|
if systemctl is-active --quiet nginx 2>/dev/null; then
|
||||||
nginx -t >/dev/null 2>&1 && systemctl reload nginx 2>/dev/null || true
|
nginx -t >/dev/null 2>&1 && systemctl reload nginx 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ override_dh_auto_install:
|
||||||
install -m 644 $(CURDIR)/nginx/appstore.conf $(CURDIR)/debian/secubox-appstore/etc/nginx/secubox.d/
|
install -m 644 $(CURDIR)/nginx/appstore.conf $(CURDIR)/debian/secubox-appstore/etc/nginx/secubox.d/
|
||||||
|
|
||||||
# Hub menu entry
|
# Hub menu entry
|
||||||
install -d $(CURDIR)/debian/secubox-appstore/etc/secubox/menu.d
|
install -d $(CURDIR)/debian/secubox-appstore/usr/share/secubox/menu.d
|
||||||
install -m 644 $(CURDIR)/menu.d/580-appstore.json $(CURDIR)/debian/secubox-appstore/etc/secubox/menu.d/
|
install -m 644 $(CURDIR)/menu.d/580-appstore.json $(CURDIR)/debian/secubox-appstore/usr/share/secubox/menu.d/
|
||||||
|
|
||||||
# systemd service
|
# systemd service
|
||||||
install -d $(CURDIR)/debian/secubox-appstore/usr/lib/systemd/system
|
install -d $(CURDIR)/debian/secubox-appstore/usr/lib/systemd/system
|
||||||
|
|
@ -28,6 +28,12 @@ override_dh_auto_install:
|
||||||
install -d $(CURDIR)/debian/secubox-appstore/etc/nginx/secubox-routes.d
|
install -d $(CURDIR)/debian/secubox-appstore/etc/nginx/secubox-routes.d
|
||||||
install -m 644 $(CURDIR)/nginx/appstore-routes.conf $(CURDIR)/debian/secubox-appstore/etc/nginx/secubox-routes.d/
|
install -m 644 $(CURDIR)/nginx/appstore-routes.conf $(CURDIR)/debian/secubox-appstore/etc/nginx/secubox-routes.d/
|
||||||
|
|
||||||
|
# Privileged controller + narrow sudoers rule
|
||||||
|
install -d $(CURDIR)/debian/secubox-appstore/usr/sbin
|
||||||
|
install -m 755 $(CURDIR)/sbin/secubox-appstorectl $(CURDIR)/debian/secubox-appstore/usr/sbin/
|
||||||
|
install -d $(CURDIR)/debian/secubox-appstore/etc/sudoers.d
|
||||||
|
install -m 440 $(CURDIR)/debian/secubox-appstore.sudoers $(CURDIR)/debian/secubox-appstore/etc/sudoers.d/secubox-appstore
|
||||||
|
|
||||||
# Generate the catalog index from every sibling module's debian/secubox.yaml
|
# Generate the catalog index from every sibling module's debian/secubox.yaml
|
||||||
install -d $(CURDIR)/debian/secubox-appstore/usr/share/secubox/appstore
|
install -d $(CURDIR)/debian/secubox-appstore/usr/share/secubox/appstore
|
||||||
python3 $(CURDIR)/scripts/gen-appstore-catalog.py \
|
python3 $(CURDIR)/scripts/gen-appstore-catalog.py \
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ RestartSec=5
|
||||||
StandardOutput=journal
|
StandardOutput=journal
|
||||||
StandardError=journal
|
StandardError=journal
|
||||||
|
|
||||||
NoNewPrivileges=yes
|
# sudo->secubox-appstorectl bridge requires new privileges; the narrow
|
||||||
|
# sudoers rule + helper validation are the security boundary.
|
||||||
|
NoNewPrivileges=no
|
||||||
ProtectHome=yes
|
ProtectHome=yes
|
||||||
PrivateTmp=yes
|
PrivateTmp=yes
|
||||||
ReadWritePaths=/run/secubox /var/log/secubox /var/lib/secubox
|
ReadWritePaths=/run/secubox /var/log/secubox /var/lib/secubox
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# secubox-appstore: allow the unprivileged API user to drive module lifecycle
|
||||||
|
# and config writes ONLY through the validated root helper.
|
||||||
|
secubox ALL=(root) NOPASSWD: /usr/sbin/secubox-appstorectl
|
||||||
|
|
@ -3,8 +3,8 @@
|
||||||
"name": "App Store",
|
"name": "App Store",
|
||||||
"icon": "🛍️",
|
"icon": "🛍️",
|
||||||
"path": "/appstore/",
|
"path": "/appstore/",
|
||||||
"category": "system",
|
"category": "root",
|
||||||
"order": 580,
|
"order": 10,
|
||||||
"description": "Install, enable & configure SecuBox modules",
|
"description": "Install, enable & configure SecuBox modules",
|
||||||
"requires": ["secubox-appstore"]
|
"requires": ["secubox-appstore"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
41
packages/secubox-appstore/sbin/secubox-appstorectl
Executable file
41
packages/secubox-appstore/sbin/secubox-appstorectl
Executable file
|
|
@ -0,0 +1,41 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||||
|
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||||
|
# SecuBox-Deb :: secubox-appstore :: privileged module controller.
|
||||||
|
# Invoked by the unprivileged secubox-appstore API via a narrow sudoers
|
||||||
|
# rule. ONLY operates on secubox-*.service units and /etc/secubox/<name>.toml.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
action="${1:-}"
|
||||||
|
module="${2:-}"
|
||||||
|
|
||||||
|
# Strict allow-list: module name must be secubox-<slug>, nothing else.
|
||||||
|
case "$module" in
|
||||||
|
secubox-[a-z0-9]*) : ;;
|
||||||
|
*) echo "appstorectl: invalid module name" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
unit="${module}.service"
|
||||||
|
short="${module#secubox-}"
|
||||||
|
|
||||||
|
case "$action" in
|
||||||
|
start|stop|restart|enable|disable)
|
||||||
|
exec systemctl "$action" "$unit"
|
||||||
|
;;
|
||||||
|
write-config)
|
||||||
|
dest="/etc/secubox/${short}.toml"
|
||||||
|
tmp="$(mktemp /etc/secubox/.${short}.toml.XXXXXX)"
|
||||||
|
trap 'rm -f "$tmp"' EXIT
|
||||||
|
cat > "$tmp"
|
||||||
|
# Reject anything that is not valid TOML.
|
||||||
|
python3 -c 'import tomllib,sys; tomllib.load(open(sys.argv[1],"rb"))' "$tmp" \
|
||||||
|
|| { echo "appstorectl: invalid TOML, refusing to write" >&2; exit 3; }
|
||||||
|
# Preserve the conventional ownership/perms for SecuBox config.
|
||||||
|
chown root:secubox "$tmp"
|
||||||
|
chmod 0640 "$tmp"
|
||||||
|
mv -f "$tmp" "$dest"
|
||||||
|
trap - EXIT
|
||||||
|
echo "wrote $dest"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "appstorectl: unknown action '$action'" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
|
@ -7,130 +7,174 @@
|
||||||
<link rel="stylesheet" href="/shared/design-tokens.css">
|
<link rel="stylesheet" href="/shared/design-tokens.css">
|
||||||
<link rel="stylesheet" href="/shared/crt-light.css">
|
<link rel="stylesheet" href="/shared/crt-light.css">
|
||||||
<style>
|
<style>
|
||||||
:root{
|
.as-bar{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:14px 0}
|
||||||
--bg:#0a0a0f; --panel:#13131c; --line:#2a2a3a; --gold:#c9a84c;
|
.as-bar input,.as-bar select{background:var(--panel,#13131c);border:1px solid var(--line,#2a2a3a);
|
||||||
--cyan:#00d4ff; --green:#00ff41; --muted:#6b6b7a; --text:#e8e6d9; --cinnabar:#e63946;
|
color:var(--text,#e8e6d9);padding:8px 10px;border-radius:6px;font-family:inherit;font-size:13px}
|
||||||
}
|
.as-bar input{min-width:220px}
|
||||||
*{box-sizing:border-box}
|
.as-stat{color:var(--text-muted,#6b6b7a);font-size:12px;margin-left:auto}
|
||||||
body{margin:0;background:var(--bg);color:var(--text);
|
.as-stat b{color:var(--cyber-cyan,#00d4ff)}
|
||||||
font-family:'JetBrains Mono',ui-monospace,monospace;}
|
.as-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(290px,1fr));gap:14px}
|
||||||
header{padding:18px 24px;border-bottom:1px solid var(--line);
|
.as-card{background:var(--panel,#13131c);border:1px solid var(--line,#2a2a3a);border-radius:10px;
|
||||||
display:flex;align-items:baseline;gap:14px;flex-wrap:wrap;}
|
padding:14px;display:flex;flex-direction:column;gap:8px}
|
||||||
header h1{font-family:'Cinzel',serif;color:var(--gold);margin:0;font-size:22px;letter-spacing:1px;}
|
.as-card:hover{border-color:var(--gold-hermetic,#c9a84c)}
|
||||||
header .sub{color:var(--muted);font-size:13px}
|
.as-top{display:flex;align-items:center;gap:10px}
|
||||||
.bar{display:flex;gap:10px;flex-wrap:wrap;padding:14px 24px;border-bottom:1px solid var(--line);align-items:center}
|
.as-ico{font-size:24px}
|
||||||
.bar input,.bar select{background:var(--panel);border:1px solid var(--line);color:var(--text);
|
.as-name{font-weight:700;font-size:14px;word-break:break-word}
|
||||||
padding:8px 10px;border-radius:6px;font-family:inherit;font-size:13px}
|
.as-desc{color:var(--text-muted,#6b6b7a);font-size:12px;min-height:30px;line-height:1.4}
|
||||||
.bar input{min-width:220px}
|
.as-badges{display:flex;gap:6px;flex-wrap:wrap}
|
||||||
.stat{color:var(--muted);font-size:12px;margin-left:auto}
|
.pill{font-size:10px;padding:2px 8px;border-radius:20px;border:1px solid var(--line,#2a2a3a);text-transform:uppercase;letter-spacing:.4px}
|
||||||
.stat b{color:var(--cyan)}
|
.pill.cat{color:var(--cyber-cyan,#00d4ff)} .pill.tier{color:var(--gold-hermetic,#c9a84c)}
|
||||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px;padding:24px}
|
.pill.s-running{color:var(--matrix-green,#00ff41);border-color:#1d5a2a}
|
||||||
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px;
|
.pill.s-installed{color:var(--cyber-cyan,#00d4ff)}
|
||||||
display:flex;flex-direction:column;gap:8px;transition:border-color .15s}
|
.pill.s-available{color:var(--text-muted,#6b6b7a)}
|
||||||
.card:hover{border-color:var(--gold)}
|
.pill.s-tier-locked{color:var(--cinnabar,#e63946);border-color:#5a1d23}
|
||||||
.card .top{display:flex;align-items:center;gap:10px}
|
.as-actions{display:flex;gap:6px;flex-wrap:wrap;margin-top:auto}
|
||||||
.card .icon{font-size:26px;line-height:1}
|
.as-btn{background:transparent;border:1px solid var(--line,#2a2a3a);color:var(--text,#e8e6d9);
|
||||||
.card .name{font-weight:700;color:var(--text);font-size:14px;word-break:break-word}
|
padding:5px 10px;border-radius:6px;font-family:inherit;font-size:11px;cursor:pointer}
|
||||||
.card .desc{color:var(--muted);font-size:12px;min-height:32px;line-height:1.4}
|
.as-btn:hover{border-color:var(--gold-hermetic,#c9a84c)}
|
||||||
.badges{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
|
.as-btn.danger{color:var(--cinnabar,#e63946)} .as-btn:disabled{opacity:.4;cursor:not-allowed}
|
||||||
.pill{font-size:10px;padding:2px 8px;border-radius:20px;border:1px solid var(--line);text-transform:uppercase;letter-spacing:.5px}
|
.as-over{position:fixed;inset:0;background:rgba(0,0,0,.6);display:none;z-index:1000;align-items:center;justify-content:center}
|
||||||
.pill.cat{color:var(--cyan);border-color:#1d4a55}
|
.as-over.on{display:flex}
|
||||||
.pill.tier{color:var(--gold);border-color:#5a4a1d}
|
.as-modal{background:var(--panel,#13131c);border:1px solid var(--line,#2a2a3a);border-radius:12px;
|
||||||
.pill.state-running{color:var(--green);border-color:#1d5a2a;background:rgba(0,255,65,.08)}
|
width:min(720px,92vw);max-height:88vh;overflow:auto;padding:18px}
|
||||||
.pill.state-installed{color:var(--cyan);border-color:#1d4a55}
|
.as-modal h2{font-family:'Cinzel',serif;color:var(--gold-hermetic,#c9a84c);margin:.2em 0}
|
||||||
.pill.state-available{color:var(--muted)}
|
.as-modal textarea{width:100%;min-height:300px;background:#0a0a0f;color:var(--text,#e8e6d9);
|
||||||
.pill.state-tier-locked{color:var(--cinnabar);border-color:#5a1d23}
|
border:1px solid var(--line,#2a2a3a);border-radius:8px;font-family:'JetBrains Mono',monospace;font-size:12px;padding:10px}
|
||||||
.row{display:flex;justify-content:space-between;align-items:center;margin-top:auto}
|
.as-row{display:flex;gap:8px;justify-content:flex-end;margin-top:10px;align-items:center}
|
||||||
.btn{background:transparent;border:1px solid var(--line);color:var(--muted);
|
.as-msg{font-size:12px;margin-right:auto;color:var(--text-muted,#6b6b7a)}
|
||||||
padding:6px 12px;border-radius:6px;font-family:inherit;font-size:12px;cursor:not-allowed}
|
.as-deps{color:var(--text-muted,#6b6b7a);font-size:12px;margin:6px 0}
|
||||||
.ver{color:var(--muted);font-size:11px}
|
|
||||||
.empty{color:var(--muted);padding:40px;text-align:center;grid-column:1/-1}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="crt-light">
|
||||||
<header>
|
<nav class="sidebar" id="sidebar"></nav>
|
||||||
<h1>⬢ SecuBox App Store</h1>
|
<script src="/shared/sidebar.js"></script>
|
||||||
<span class="sub">module catalog · install / enable / configure — <em>Phase A: read-only</em></span>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="bar">
|
<main class="main">
|
||||||
<input id="q" type="search" placeholder="Search modules…" oninput="render()">
|
<header class="header">
|
||||||
<select id="category" onchange="render()"><option value="">All categories</option></select>
|
<div class="header-title">
|
||||||
<select id="tier" onchange="render()">
|
<h1>🛍️ App Store</h1>
|
||||||
<option value="">All tiers</option><option>lite</option><option>standard</option><option>pro</option><option>all</option>
|
<span class="badge" id="board-badge">tier —</span>
|
||||||
</select>
|
</div>
|
||||||
<select id="state" onchange="render()">
|
</header>
|
||||||
<option value="">All states</option><option value="running">running</option>
|
|
||||||
<option value="installed">installed</option><option value="available">available</option>
|
<div class="as-bar">
|
||||||
<option value="tier-locked">tier-locked</option>
|
<input id="q" type="search" placeholder="Search modules…" oninput="render()">
|
||||||
</select>
|
<select id="category" onchange="render()"><option value="">All categories</option></select>
|
||||||
<span class="stat" id="stat">loading…</span>
|
<select id="tier" onchange="render()"><option value="">All tiers</option>
|
||||||
|
<option>lite</option><option>standard</option><option>pro</option><option>all</option></select>
|
||||||
|
<select id="state" onchange="render()"><option value="">All states</option>
|
||||||
|
<option value="running">running</option><option value="installed">installed</option>
|
||||||
|
<option value="available">available</option><option value="tier-locked">tier-locked</option></select>
|
||||||
|
<span class="as-stat" id="stat">loading…</span>
|
||||||
|
</div>
|
||||||
|
<div class="as-grid" id="grid"></div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Config / detail drawer -->
|
||||||
|
<div class="as-over" id="over">
|
||||||
|
<div class="as-modal">
|
||||||
|
<h2 id="m-title">module</h2>
|
||||||
|
<div class="as-deps" id="m-deps"></div>
|
||||||
|
<div class="as-actions" id="m-actions" style="margin-bottom:10px"></div>
|
||||||
|
<label style="font-size:12px;color:var(--text-muted,#6b6b7a)">Config (<span id="m-path"></span>)</label>
|
||||||
|
<textarea id="m-config" spellcheck="false"></textarea>
|
||||||
|
<div class="as-row">
|
||||||
|
<span class="as-msg" id="m-msg"></span>
|
||||||
|
<button class="as-btn" onclick="closeModal()">Close</button>
|
||||||
|
<button class="as-btn" id="m-save" onclick="saveConfig()">Save config</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid" id="grid"><div class="empty">Loading catalog…</div></div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const API = '/api/v1/appstore';
|
const API='/api/v1/appstore';
|
||||||
const ICONS = {media:'🎬',email:'✉️',ai:'🧠',iot:'🛰️',communication:'💬',publishing:'📰',
|
const ICONS={media:'🎬',email:'✉️',ai:'🧠',iot:'🛰️',communication:'💬',publishing:'📰',
|
||||||
network:'🌐',security:'🛡️',system:'⚙️',vpn:'🔒',dashboard:'📊',monitoring:'📈',misc:'🧩'};
|
network:'🌐',security:'🛡️',system:'⚙️',vpn:'🔒',dashboard:'📊',monitoring:'📈',misc:'🧩'};
|
||||||
let ALL = [];
|
let ALL=[], CUR=null;
|
||||||
|
|
||||||
async function getJSON(p){ try{const r=await fetch(API+p);if(!r.ok)throw 0;return await r.json();}catch(e){return null;} }
|
async function jget(p){try{const r=await fetch(API+p);if(!r.ok)throw 0;return await r.json();}catch(e){return null;}}
|
||||||
|
async function jsend(p,m,b){try{const r=await fetch(API+p,{method:m,headers:{'Content-Type':'application/json'},
|
||||||
|
body:b?JSON.stringify(b):undefined});const j=await r.json().catch(()=>({}));return{ok:r.ok,j};}catch(e){return{ok:false,j:{detail:String(e)}};}}
|
||||||
function esc(s){const d=document.createElement('div');d.textContent=s==null?'':s;return d.innerHTML;}
|
function esc(s){const d=document.createElement('div');d.textContent=s==null?'':s;return d.innerHTML;}
|
||||||
|
|
||||||
async function load(){
|
async function load(){
|
||||||
const cats = await getJSON('/categories');
|
const cats=await jget('/categories');
|
||||||
if(cats && cats.categories){
|
if(cats&&cats.categories){const sel=document.getElementById('category');
|
||||||
const sel=document.getElementById('category');
|
cats.categories.forEach(c=>{const o=document.createElement('option');o.value=c.name;o.textContent=`${c.name} (${c.count})`;sel.appendChild(o);});}
|
||||||
cats.categories.forEach(c=>{const o=document.createElement('option');o.value=c.name;
|
const data=await jget('/catalog');
|
||||||
o.textContent=`${c.name} (${c.count})`;sel.appendChild(o);});
|
ALL=(data&&Array.isArray(data.modules))?data.modules:[];
|
||||||
}
|
document.getElementById('board-badge').textContent='tier '+((cats&&cats.board_tier)||'?');
|
||||||
const data = await getJSON('/catalog');
|
render();
|
||||||
ALL = (data && Array.isArray(data.modules)) ? data.modules : [];
|
|
||||||
render(cats && cats.board_tier);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(boardTier){
|
function actionsFor(m){
|
||||||
|
if(!m.installed) return `<button class="as-btn" disabled title="Install arrives in a later phase">Install</button>`;
|
||||||
|
const b=[];
|
||||||
|
if(m.running){ b.push(`<button class="as-btn" onclick="act('${m.name}','restart')">Restart</button>`);
|
||||||
|
b.push(`<button class="as-btn danger" onclick="act('${m.name}','stop')">Stop</button>`); }
|
||||||
|
else { b.push(`<button class="as-btn" onclick="act('${m.name}','start')">Start</button>`); }
|
||||||
|
b.push(`<button class="as-btn" onclick="act('${m.name}','enable')">Enable</button>`);
|
||||||
|
b.push(`<button class="as-btn danger" onclick="act('${m.name}','disable')">Disable</button>`);
|
||||||
|
b.push(`<button class="as-btn" onclick="openModal('${m.name}')">Config</button>`);
|
||||||
|
return b.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(){
|
||||||
const q=document.getElementById('q').value.toLowerCase();
|
const q=document.getElementById('q').value.toLowerCase();
|
||||||
const cat=document.getElementById('category').value;
|
const cat=document.getElementById('category').value, tier=document.getElementById('tier').value, state=document.getElementById('state').value;
|
||||||
const tier=document.getElementById('tier').value;
|
let items=ALL.filter(m=>{
|
||||||
const state=document.getElementById('state').value;
|
if(cat&&m.category!==cat)return false; if(tier&&m.tier!==tier)return false; if(state&&m.state!==state)return false;
|
||||||
let items = ALL.filter(m=>{
|
if(q&&!(m.name.toLowerCase().includes(q)||(m.description||'').toLowerCase().includes(q)))return false; return true;});
|
||||||
if(cat && m.category!==cat) return false;
|
const running=ALL.filter(m=>m.state==='running').length, installed=ALL.filter(m=>m.installed).length;
|
||||||
if(tier && m.tier!==tier) return false;
|
document.getElementById('stat').innerHTML=`showing <b>${items.length}</b>/${ALL.length} · installed <b>${installed}</b> · running <b>${running}</b>`;
|
||||||
if(state && m.state!==state) return false;
|
|
||||||
if(q && !(m.name.toLowerCase().includes(q) || (m.description||'').toLowerCase().includes(q))) return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
const grid=document.getElementById('grid');
|
const grid=document.getElementById('grid');
|
||||||
const running = ALL.filter(m=>m.state==='running').length;
|
if(!items.length){grid.innerHTML='<div style="color:var(--text-muted,#6b6b7a);padding:30px">No modules match.</div>';return;}
|
||||||
const installed = ALL.filter(m=>m.installed).length;
|
grid.innerHTML=items.map(m=>{
|
||||||
document.getElementById('stat').innerHTML =
|
const ico=ICONS[m.category]||ICONS.misc, label=m.name.replace(/^secubox-/,''), st=m.state||'available';
|
||||||
`showing <b>${items.length}</b> / ${ALL.length} · installed <b>${installed}</b> · running <b>${running}</b>`;
|
return `<div class="as-card">
|
||||||
if(!items.length){ grid.innerHTML='<div class="empty">No modules match.</div>'; return; }
|
<div class="as-top"><span class="as-ico">${ico}</span><span class="as-name">${esc(label)}</span></div>
|
||||||
grid.innerHTML = items.map(m=>{
|
<div class="as-desc">${esc(m.description||'')}</div>
|
||||||
const icon = ICONS[m.category]||ICONS.misc;
|
<div class="as-badges"><span class="pill cat">${esc(m.category)}</span>
|
||||||
const label = m.name.replace(/^secubox-/,'');
|
<span class="pill tier">${esc(m.tier)}</span><span class="pill s-${st}">${st}</span>
|
||||||
const st = m.state||'available';
|
${m.version?`<span class="pill">v${esc(m.version)}</span>`:''}</div>
|
||||||
const btn = st==='running'?'Running' : st==='installed'?'Installed' :
|
<div class="as-actions">${actionsFor(m)}</div>
|
||||||
st==='tier-locked'?'Tier locked' : 'Install';
|
</div>`;}).join('');
|
||||||
return `<div class="card">
|
|
||||||
<div class="top"><span class="icon">${icon}</span><span class="name">${esc(label)}</span></div>
|
|
||||||
<div class="desc">${esc(m.description||'')}</div>
|
|
||||||
<div class="badges">
|
|
||||||
<span class="pill cat">${esc(m.category)}</span>
|
|
||||||
<span class="pill tier">${esc(m.tier)}</span>
|
|
||||||
<span class="pill state-${st}">${st}</span>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<span class="ver">${m.version?('v'+esc(m.version)):''}</span>
|
|
||||||
<button class="btn" title="Lifecycle actions arrive in Phase C" disabled>${btn}</button>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
}).join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', load);
|
async function act(name,verb){
|
||||||
|
const r=await jsend(`/module/${name}/action/${verb}`,'POST');
|
||||||
|
if(!r.ok){alert(`${verb} failed: ${(r.j&&r.j.detail)||'error'}`);return;}
|
||||||
|
const data=await jget('/catalog'); ALL=(data&&Array.isArray(data.modules))?data.modules:ALL; render();
|
||||||
|
if(CUR&&CUR===name) renderModalActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openModal(name){
|
||||||
|
CUR=name; document.getElementById('m-title').textContent=name.replace(/^secubox-/,'');
|
||||||
|
const m=ALL.find(x=>x.name===name)||{};
|
||||||
|
document.getElementById('m-deps').innerHTML='Depends: '+((m.depends&&m.depends.length)?m.depends.map(esc).join(', '):'—');
|
||||||
|
renderModalActions();
|
||||||
|
document.getElementById('m-msg').textContent='loading config…';
|
||||||
|
document.getElementById('m-config').value='';
|
||||||
|
document.getElementById('over').classList.add('on');
|
||||||
|
const cfg=await jget(`/module/${name}/config`);
|
||||||
|
const ta=document.getElementById('m-config'), save=document.getElementById('m-save');
|
||||||
|
document.getElementById('m-path').textContent=(cfg&&cfg.path)||'';
|
||||||
|
if(cfg&&cfg.readable===false){ta.value='';ta.disabled=true;save.disabled=true;document.getElementById('m-msg').textContent='config not readable by appstore';}
|
||||||
|
else if(cfg&&!cfg.exists){ta.value='';ta.disabled=false;save.disabled=false;document.getElementById('m-msg').textContent='no config file yet — saving creates it';}
|
||||||
|
else{ta.value=(cfg&&cfg.content)||'';ta.disabled=false;save.disabled=false;document.getElementById('m-msg').textContent='';}
|
||||||
|
}
|
||||||
|
function renderModalActions(){const m=ALL.find(x=>x.name===CUR)||{};document.getElementById('m-actions').innerHTML=actionsFor(m);}
|
||||||
|
function closeModal(){document.getElementById('over').classList.remove('on');CUR=null;}
|
||||||
|
|
||||||
|
async function saveConfig(){
|
||||||
|
if(!CUR)return; const content=document.getElementById('m-config').value;
|
||||||
|
document.getElementById('m-msg').textContent='saving…';
|
||||||
|
const r=await jsend(`/module/${CUR}/config`,'PUT',{content});
|
||||||
|
document.getElementById('m-msg').textContent=r.ok?'saved ✓ (restart the service to apply)':('save failed: '+((r.j&&r.j.detail)||'error'));
|
||||||
|
}
|
||||||
|
document.getElementById('over').addEventListener('click',e=>{if(e.target.id==='over')closeModal();});
|
||||||
|
document.addEventListener('DOMContentLoaded',load);
|
||||||
</script>
|
</script>
|
||||||
|
<script src="/shared/crt-engine.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,21 @@
|
||||||
|
secubox-portal (2.2.2-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* fix(ui): 'Active bans' now reflects the live /crowdsec/decisions list
|
||||||
|
(includes CAPI community blocklist) instead of the local-only
|
||||||
|
health/summary.active_decisions; hero 'Bans active' kept consistent.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Mon, 29 Jun 2026 21:00:00 +0200
|
||||||
|
|
||||||
|
secubox-portal (2.2.1-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
|
* fix(ui): reskin the portal to the standard SecuBox template — shared
|
||||||
|
sidebar (nav#sidebar + sidebar.js) replacing the bespoke top navbar,
|
||||||
|
design-tokens + crt-light C3BOX palette replacing the P31 green theme,
|
||||||
|
main.main layout + crt-engine. Dashboard content, element IDs and the
|
||||||
|
data-fetch JS are unchanged.
|
||||||
|
|
||||||
|
-- Gerald KERMA <devel@cybermind.fr> Mon, 29 Jun 2026 20:30:00 +0200
|
||||||
|
|
||||||
secubox-portal (2.2.0-1~bookworm1) bookworm; urgency=medium
|
secubox-portal (2.2.0-1~bookworm1) bookworm; urgency=medium
|
||||||
|
|
||||||
* portal: regenerate /portal/index.html as a public-facing operational
|
* portal: regenerate /portal/index.html as a public-facing operational
|
||||||
|
|
|
||||||
|
|
@ -5,74 +5,12 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>SecuBox · Portal</title>
|
<title>SecuBox · Portal</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🛡️</text></svg>">
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🛡️</text></svg>">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="stylesheet" href="/shared/design-tokens.css">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="/shared/crt-light.css">
|
<link rel="stylesheet" href="/shared/crt-light.css">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
|
||||||
--p31-peak: #00dd44;
|
|
||||||
--p31-hot: #00ff55;
|
|
||||||
--p31-mid: #009933;
|
|
||||||
--p31-dim: #006622;
|
|
||||||
--p31-ghost: #003311;
|
|
||||||
--p31-decay: #ffb347;
|
|
||||||
--p31-decay-dim: #cc7722;
|
|
||||||
--tube-light: #e8f5e9;
|
|
||||||
--tube-pale: #c8e6c9;
|
|
||||||
--tube-soft: #a5d6a7;
|
|
||||||
--tube-dark: #1b1b1f;
|
|
||||||
--primary: var(--p31-peak);
|
|
||||||
--cyan: #00d4ff;
|
|
||||||
--red: #ff4466;
|
|
||||||
--yellow: var(--p31-decay);
|
|
||||||
--bloom-text: 0 0 2px var(--p31-peak), 0 0 6px var(--p31-peak), 0 0 14px rgba(51,255,102,0.5);
|
|
||||||
--bloom-amber: 0 0 3px var(--p31-decay), 0 0 10px rgba(255,179,71,0.4);
|
|
||||||
--bloom-red: 0 0 3px var(--red), 0 0 10px rgba(255,68,102,0.4);
|
|
||||||
}
|
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
body {
|
a { color: var(--cyber-cyan, #00d4ff); text-decoration: none; }
|
||||||
font-family: 'Courier Prime', 'Courier New', monospace;
|
a:hover { color: var(--gold-hermetic, #c9a84c); }
|
||||||
background: var(--tube-light);
|
|
||||||
background-image: radial-gradient(ellipse at 50% 40%, rgba(51,255,102,0.025) 0%, transparent 70%);
|
|
||||||
color: var(--tube-dark);
|
|
||||||
min-height: 100vh;
|
|
||||||
padding-bottom: 3rem;
|
|
||||||
}
|
|
||||||
body::before {
|
|
||||||
content: "";
|
|
||||||
position: fixed; inset: 0;
|
|
||||||
background-image:
|
|
||||||
linear-gradient(rgba(0,221,68,0.02) 1px, transparent 1px),
|
|
||||||
linear-gradient(90deg, rgba(0,221,68,0.02) 1px, transparent 1px);
|
|
||||||
background-size: 50px 50px;
|
|
||||||
pointer-events: none; z-index: -1;
|
|
||||||
}
|
|
||||||
a { color: var(--p31-mid); text-decoration: none; }
|
|
||||||
a:hover { color: var(--p31-peak); text-shadow: var(--bloom-text); }
|
|
||||||
|
|
||||||
/* Navbar */
|
|
||||||
.navbar {
|
|
||||||
background: var(--tube-dark);
|
|
||||||
color: var(--p31-peak);
|
|
||||||
padding: 0.75rem 2rem;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
||||||
display: flex; align-items: center; justify-content: space-between;
|
|
||||||
position: sticky; top: 0; z-index: 50;
|
|
||||||
border-bottom: 1px solid var(--p31-mid);
|
|
||||||
}
|
|
||||||
.navbar .brand {
|
|
||||||
font-weight: bold; font-size: 1.1rem; letter-spacing: 1px;
|
|
||||||
text-shadow: var(--bloom-text);
|
|
||||||
}
|
|
||||||
.navbar .brand a { color: var(--p31-peak); }
|
|
||||||
.navbar .nav-links { display: flex; gap: 1.5rem; flex-wrap: wrap; }
|
|
||||||
.navbar .nav-links a {
|
|
||||||
color: var(--p31-mid); font-size: 0.9rem;
|
|
||||||
padding: 0.25rem 0.5rem; border-radius: 3px;
|
|
||||||
transition: all 0.15s;
|
|
||||||
}
|
|
||||||
.navbar .nav-links a:hover { color: var(--p31-peak); background: rgba(0,221,68,0.08); text-shadow: var(--bloom-text); }
|
|
||||||
.navbar .nav-meta { font-size: 0.75rem; color: var(--p31-dim); }
|
|
||||||
|
|
||||||
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||||
|
|
||||||
|
|
@ -80,10 +18,9 @@
|
||||||
.hero {
|
.hero {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
padding: 1.5rem 2rem;
|
padding: 1.5rem 2rem;
|
||||||
background: var(--tube-pale);
|
background: var(--panel, #13131c);
|
||||||
border: 1px solid var(--p31-mid);
|
border: 1px solid var(--line, #2a2a3a);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 0 0 12px rgba(0,221,68,0.15);
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr auto;
|
grid-template-columns: auto 1fr auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -95,16 +32,16 @@
|
||||||
}
|
}
|
||||||
.hero-score .num {
|
.hero-score .num {
|
||||||
font-size: 3.5rem; font-weight: bold; line-height: 1;
|
font-size: 3.5rem; font-weight: bold; line-height: 1;
|
||||||
color: var(--p31-peak); text-shadow: var(--bloom-text);
|
color: var(--matrix-green, #00ff41);
|
||||||
}
|
}
|
||||||
.hero-score .label { font-size: 0.8rem; color: var(--p31-dim); margin-top: 0.25rem; letter-spacing: 1px; }
|
.hero-score .label { font-size: 0.8rem; color: var(--text-muted, #6b6b7a); margin-top: 0.25rem; letter-spacing: 1px; }
|
||||||
.hero-score.warn .num { color: var(--yellow); text-shadow: var(--bloom-amber); }
|
.hero-score.warn .num { color: var(--gold-hermetic, #c9a84c); }
|
||||||
.hero-score.crit .num { color: var(--red); text-shadow: var(--bloom-red); }
|
.hero-score.crit .num { color: var(--cinnabar, #e63946); }
|
||||||
.hero-info { font-size: 0.95rem; line-height: 1.6; }
|
.hero-info { font-size: 0.95rem; line-height: 1.6; }
|
||||||
.hero-info .kv { display: flex; gap: 1.5rem; flex-wrap: wrap; }
|
.hero-info .kv { display: flex; gap: 1.5rem; flex-wrap: wrap; }
|
||||||
.hero-info .k { color: var(--p31-dim); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; }
|
.hero-info .k { color: var(--text-muted, #6b6b7a); font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; }
|
||||||
.hero-info .v { color: var(--tube-dark); font-weight: bold; }
|
.hero-info .v { color: var(--text, #e8e6d9); font-weight: bold; }
|
||||||
.hero-refresh { font-size: 0.75rem; color: var(--p31-dim); text-align: right; }
|
.hero-refresh { font-size: 0.75rem; color: var(--text-muted, #6b6b7a); text-align: right; }
|
||||||
|
|
||||||
/* Section grid */
|
/* Section grid */
|
||||||
.grid {
|
.grid {
|
||||||
|
|
@ -114,101 +51,87 @@
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
.card {
|
.card {
|
||||||
background: var(--tube-light);
|
background: var(--panel, #13131c);
|
||||||
border: 1px solid var(--p31-mid);
|
border: 1px solid var(--line, #2a2a3a);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 0 8px rgba(0,221,68,0.08);
|
|
||||||
}
|
}
|
||||||
.card h3 {
|
.card h3 {
|
||||||
font-size: 0.85rem; text-transform: uppercase; letter-spacing: 2px;
|
font-size: 0.85rem; text-transform: uppercase; letter-spacing: 2px;
|
||||||
color: var(--p31-mid); margin-bottom: 1rem;
|
color: var(--gold-hermetic, #c9a84c); margin-bottom: 1rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
border-bottom: 1px dashed var(--p31-ghost);
|
border-bottom: 1px dashed var(--line, #2a2a3a);
|
||||||
text-shadow: var(--bloom-text);
|
|
||||||
}
|
}
|
||||||
.card .row { display: flex; justify-content: space-between; padding: 0.25rem 0; font-size: 0.9rem; }
|
.card .row { display: flex; justify-content: space-between; padding: 0.25rem 0; font-size: 0.9rem; }
|
||||||
.card .row .k { color: var(--p31-dim); }
|
.card .row .k { color: var(--text-muted, #6b6b7a); }
|
||||||
.card .row .v { color: var(--tube-dark); font-weight: bold; }
|
.card .row .v { color: var(--text, #e8e6d9); font-weight: bold; }
|
||||||
.card .row .v.ok { color: var(--p31-peak); text-shadow: var(--bloom-text); }
|
.card .row .v.ok { color: var(--matrix-green, #00ff41); }
|
||||||
.card .row .v.warn { color: var(--yellow); text-shadow: var(--bloom-amber); }
|
.card .row .v.warn { color: var(--gold-hermetic, #c9a84c); }
|
||||||
.card .row .v.crit { color: var(--red); text-shadow: var(--bloom-red); }
|
.card .row .v.crit { color: var(--cinnabar, #e63946); }
|
||||||
.empty { color: var(--p31-dim); font-style: italic; font-size: 0.85rem; padding: 0.5rem 0; }
|
.empty { color: var(--text-muted, #6b6b7a); font-style: italic; font-size: 0.85rem; padding: 0.5rem 0; }
|
||||||
|
|
||||||
/* Bar */
|
/* Bar */
|
||||||
.bar {
|
.bar {
|
||||||
height: 4px; background: var(--p31-ghost); border-radius: 2px;
|
height: 4px; background: var(--cosmos-black, #0a0a0f); border-radius: 2px;
|
||||||
overflow: hidden; margin: 0.25rem 0;
|
overflow: hidden; margin: 0.25rem 0;
|
||||||
}
|
}
|
||||||
.bar > span { display: block; height: 100%; background: var(--p31-peak); }
|
.bar > span { display: block; height: 100%; background: var(--matrix-green, #00ff41); }
|
||||||
.bar.warn > span { background: var(--yellow); }
|
.bar.warn > span { background: var(--gold-hermetic, #c9a84c); }
|
||||||
.bar.crit > span { background: var(--red); }
|
.bar.crit > span { background: var(--cinnabar, #e63946); }
|
||||||
|
|
||||||
/* Modules LED grid */
|
/* Modules LED grid */
|
||||||
.modules { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.5rem; }
|
.modules { display: grid; grid-template-columns: repeat(5, 1fr); gap: 0.5rem; }
|
||||||
.module {
|
.module {
|
||||||
display: flex; flex-direction: column; align-items: center;
|
display: flex; flex-direction: column; align-items: center;
|
||||||
gap: 0.25rem; padding: 0.5rem; border-radius: 4px;
|
gap: 0.25rem; padding: 0.5rem; border-radius: 4px;
|
||||||
background: var(--tube-pale); border: 1px solid var(--p31-ghost);
|
background: var(--cosmos-black, #0a0a0f); border: 1px solid var(--line, #2a2a3a);
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
}
|
}
|
||||||
.module .led {
|
.module .led {
|
||||||
width: 10px; height: 10px; border-radius: 50%;
|
width: 10px; height: 10px; border-radius: 50%;
|
||||||
background: var(--p31-dim);
|
background: var(--text-muted, #6b6b7a);
|
||||||
}
|
}
|
||||||
.module.ok .led { background: var(--p31-peak); box-shadow: 0 0 8px var(--p31-peak); }
|
.module.ok .led { background: var(--matrix-green, #00ff41); box-shadow: 0 0 6px var(--matrix-green, #00ff41); }
|
||||||
.module.warn .led { background: var(--yellow); box-shadow: 0 0 8px var(--yellow); }
|
.module.warn .led { background: var(--gold-hermetic, #c9a84c); box-shadow: 0 0 6px var(--gold-hermetic, #c9a84c); }
|
||||||
.module.err .led { background: var(--red); box-shadow: 0 0 8px var(--red); }
|
.module.err .led { background: var(--cinnabar, #e63946); box-shadow: 0 0 6px var(--cinnabar, #e63946); }
|
||||||
.module .name { font-size: 0.65rem; text-transform: uppercase; color: var(--p31-dim); }
|
.module .name { font-size: 0.65rem; text-transform: uppercase; color: var(--text-muted, #6b6b7a); }
|
||||||
|
|
||||||
/* Tables — vhosts / ASNs */
|
/* Tables — vhosts / ASNs */
|
||||||
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
||||||
th, td { text-align: left; padding: 0.25rem 0.5rem; }
|
th, td { text-align: left; padding: 0.25rem 0.5rem; }
|
||||||
th { font-size: 0.7rem; color: var(--p31-dim); text-transform: uppercase; letter-spacing: 1px; border-bottom: 1px dashed var(--p31-ghost); }
|
th { font-size: 0.7rem; color: var(--text-muted, #6b6b7a); text-transform: uppercase; letter-spacing: 1px; border-bottom: 1px dashed var(--line, #2a2a3a); }
|
||||||
td.num { text-align: right; color: var(--cyan); font-variant-numeric: tabular-nums; }
|
td.num { text-align: right; color: var(--cyber-cyan, #00d4ff); font-variant-numeric: tabular-nums; }
|
||||||
tr:hover td { background: rgba(0,221,68,0.04); }
|
tr:hover td { background: rgba(201,168,76,0.05); }
|
||||||
|
|
||||||
/* Big number tiles */
|
/* Big number tiles */
|
||||||
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.5rem; }
|
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 0.5rem; }
|
||||||
.tile {
|
.tile {
|
||||||
background: var(--tube-pale); border: 1px solid var(--p31-ghost);
|
background: var(--cosmos-black, #0a0a0f); border: 1px solid var(--line, #2a2a3a);
|
||||||
border-radius: 4px; padding: 0.75rem; text-align: center;
|
border-radius: 4px; padding: 0.75rem; text-align: center;
|
||||||
}
|
}
|
||||||
.tile .v { font-size: 1.75rem; font-weight: bold; color: var(--p31-peak); text-shadow: var(--bloom-text); }
|
.tile .v { font-size: 1.75rem; font-weight: bold; color: var(--matrix-green, #00ff41); }
|
||||||
.tile .v.warn { color: var(--yellow); text-shadow: var(--bloom-amber); }
|
.tile .v.warn { color: var(--gold-hermetic, #c9a84c); }
|
||||||
.tile .v.crit { color: var(--red); text-shadow: var(--bloom-red); }
|
.tile .v.crit { color: var(--cinnabar, #e63946); }
|
||||||
.tile .l { font-size: 0.7rem; text-transform: uppercase; color: var(--p31-dim); letter-spacing: 1px; }
|
.tile .l { font-size: 0.7rem; text-transform: uppercase; color: var(--text-muted, #6b6b7a); letter-spacing: 1px; }
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
margin-top: 3rem; padding-top: 1.5rem;
|
margin-top: 3rem; padding-top: 1.5rem;
|
||||||
border-top: 1px dashed var(--p31-ghost);
|
border-top: 1px dashed var(--line, #2a2a3a);
|
||||||
text-align: center; font-size: 0.8rem; color: var(--p31-dim);
|
text-align: center; font-size: 0.8rem; color: var(--text-muted, #6b6b7a);
|
||||||
}
|
}
|
||||||
.footer a { color: var(--p31-mid); }
|
.footer a { color: var(--cyber-cyan, #00d4ff); }
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.hero { grid-template-columns: 1fr; }
|
.hero { grid-template-columns: 1fr; }
|
||||||
.modules { grid-template-columns: repeat(3, 1fr); }
|
.modules { grid-template-columns: repeat(3, 1fr); }
|
||||||
.navbar { padding: 0.75rem 1rem; flex-direction: column; gap: 0.5rem; }
|
|
||||||
.navbar .nav-links { font-size: 0.8rem; }
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="crt-light">
|
||||||
|
<nav class="sidebar" id="sidebar"></nav>
|
||||||
<nav class="navbar">
|
<script src="/shared/sidebar.js"></script>
|
||||||
<div class="brand"><a href="/portal/">🛡️ SecuBox</a></div>
|
|
||||||
<div class="nav-links">
|
|
||||||
<a href="/portal/">PORTAL</a>
|
|
||||||
<a href="/soc/">SOC</a>
|
|
||||||
<a href="/metablogizer/">METABLOGS</a>
|
|
||||||
<a href="/publish/">PUBLISH</a>
|
|
||||||
<a href="/cookies/">COOKIES</a>
|
|
||||||
<a href="https://github.com/CyberMind-FR/secubox-deb" rel="noopener">REPO</a>
|
|
||||||
</div>
|
|
||||||
<div class="nav-meta" id="nav-hostname">—</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
<!-- Hero -->
|
<!-- Hero -->
|
||||||
|
|
@ -295,7 +218,9 @@
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="/shared/crt-engine.js"></script>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
@ -430,7 +355,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderBans(h, decisions) {
|
function renderBans(h, decisions) {
|
||||||
const ban = h && h.crowdsec && h.crowdsec.active_decisions;
|
// Prefer the live /crowdsec/decisions list (includes CAPI community
|
||||||
|
// blocklist), falling back to health/summary's local-only count.
|
||||||
|
const decList = decisions && Array.isArray(decisions.decisions) ? decisions.decisions
|
||||||
|
: (Array.isArray(decisions) ? decisions : null);
|
||||||
|
const ban = decList ? decList.filter(d => (d.type || 'ban') === 'ban').length
|
||||||
|
: (h && h.crowdsec ? h.crowdsec.active_decisions : null);
|
||||||
const alerts = h && h.crowdsec && h.crowdsec.alerts_today;
|
const alerts = h && h.crowdsec && h.crowdsec.alerts_today;
|
||||||
const wafPct = h && h.waf && h.waf.blocked_pct;
|
const wafPct = h && h.waf && h.waf.blocked_pct;
|
||||||
const wafCls = wafPct == null ? '' : (wafPct >= 25 ? 'crit' : wafPct >= 10 ? 'warn' : 'ok');
|
const wafCls = wafPct == null ? '' : (wafPct >= 25 ? 'crit' : wafPct >= 10 ? 'warn' : 'ok');
|
||||||
|
|
@ -440,6 +370,8 @@
|
||||||
$('t-waf').textContent = wafPct != null ? wafPct + '%' : '—';
|
$('t-waf').textContent = wafPct != null ? wafPct + '%' : '—';
|
||||||
$('t-waf').className = 'v ' + wafCls;
|
$('t-waf').className = 'v ' + wafCls;
|
||||||
$('t-alerts').textContent = alerts != null ? alerts : '—';
|
$('t-alerts').textContent = alerts != null ? alerts : '—';
|
||||||
|
// Keep the hero "Bans active" consistent with the real blocked-IP count.
|
||||||
|
if (ban != null && $('hero-bans')) $('hero-bans').textContent = ban;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderVhosts(d) {
|
function renderVhosts(d) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user