Compare commits

...
7 Commits
Author SHA1 Message Date
gandalfandClaude Opus 4.5 e1d6155eea fix(netmodes): Fix uptime parsing and add JWT auth to frontend
- Fix int(float()) for /proc/uptime parsing (was crashing on float values)
- Add getToken() function to retrieve JWT from localStorage/sessionStorage
- Add auto-login on 401 response for seamless API access
- Update WIP.md with Session 27 progress (kiosk X11, menu fix, auth)

Version bump: secubox-netmodes 1.0.5

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-30 21:46:50 +02:00
gandalfandClaude Opus 4.5 a6d9e49915 fix(kiosk): Fix multiple issues preventing kiosk from starting
- Add + prefix to ExecStartPre condition check (run as root)
- Remove WAYLAND_DISPLAY env var (cage creates its own display)
- Add kbd package for chvt command
- Add splash to kiosk GRUB entry for Plymouth boot animation
- Enable SSH password authentication for debugging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-30 18:49:11 +02:00
gandalfandClaude Opus 4.5 99d4a25880 docs: Add Raspberry Pi boards to supported devices
- Raspberry Pi 4 (BCM2711) - SecuBox Lite
- Raspberry Pi 400 (BCM2711) - SecuBox Lite
- Raspberry Pi 5 (BCM2712) - SecuBox Full

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-30 17:32:13 +02:00
gandalfandClaude Opus 4.5 01f3b28bde fix(kiosk): Replace ConditionPathExists with ExecStartPre check
ConditionPathExists is evaluated when systemd loads the unit, not
when the service starts. This causes the kiosk to fail because
the marker file doesn't exist yet when systemd parses units.

Using ExecStartPre check instead, which runs at service start time.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-30 17:12:49 +02:00
gandalfandClaude Opus 4.5 4ad789624b feat(live-usb): Make kiosk GUI the default boot entry
Changed GRUB default from 0 (SecuBox Live) to 1 (Kiosk GUI)
so kiosk mode boots automatically.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-30 17:00:50 +02:00
gandalfandClaude Opus 4.5 85f3abcaa3 feat(live-usb): Enable kiosk mode by default
- Kiosk packages (cage, chromium) now included by default
- Added --no-kiosk flag to disable if needed
- Removed --kiosk flag (no longer needed)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-30 16:47:17 +02:00
gandalfandClaude Opus 4.5 ea772a95cb fix(kiosk): Start kiosk service explicitly after creating marker
Systemd evaluates ConditionPathExists before the cmdline handler
creates .kiosk-enabled, causing service to fail. Now explicitly
starts the service after setup.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-30 16:46:17 +02:00
8 changed files with 523 additions and 37 deletions
+62 -2
View File
@@ -1,9 +1,69 @@
# WIP — Work In Progress
*Mis à jour : 2026-03-29 (Session 25)*
*Mis à jour : 2026-03-30 (Session 27)*
---
## ✅ Terminé cette session (Session 25)
## ✅ Terminé cette session (Session 27)
### Kiosk X11 Mode ✅
- **Switched from Cage/Wayland to X11/startx** — VirtualBox GPU compatibility
- Cage/wlroots fails with "Basic output test failed" on VBoxSVGA
- Created `secubox-kiosk-x11.service` using startx + chromium
- Works reliably with VMware SVGA II / VBoxSVGA drivers
- **Files created on VM**:
- `/etc/systemd/system/secubox-kiosk-x11.service`
- `/home/secubox-kiosk/.xinitrc`
### Menu System Fix ✅
- **Problem**: Only 2 modules shown (hub, portal) instead of all installed
- **Root cause**: menu.d/*.json files not installed on VM
- **Fix**: Copied 85 menu JSON files to `/usr/share/secubox/menu.d/`
- **Result**: Menu API now returns all installed modules correctly
### Authentication Fix ✅
- **Created `/etc/secubox/secubox.conf`** with auth section
- **JWT login working** — admin/secubox credentials
- **All API endpoints** now properly authenticated
### secubox-netmodes Fixes ✅
- **Bug fix**: `int(float())` for /proc/uptime parsing (line 95)
- **Frontend JWT fix**: Added `getToken()` and auto-login on 401
- **Auto-Detect working**: Returns board=x64-vm, wan=enp0s3
- **Files modified**:
- `packages/secubox-netmodes/api/main.py` — uptime bug fix
- `packages/secubox-netmodes/www/netmodes/index.html` — JWT auth in api()
---
## ✅ Terminé session précédente (Session 26)
### Kiosk GUI VM Testing ✅
- **Fixed kiosk display** — Enabled 3D acceleration in VirtualBox
- `VBoxManage modifyvm "VM" --accelerate3d on --vram 128`
- Cage Wayland compositor now renders properly
- **Installed SecuBox packages on kiosk VM**
- secubox-core, secubox-hub, secubox-portal
- Fixed nginx port 9443 + nftables firewall rule
- **Kiosk fully functional** — Displays SecuBox Control Center
### secubox-net-detect Integration ✅
- **New API endpoints in secubox-netmodes**:
- `GET /detect` — Run secubox-net-detect, return JSON (board, interfaces)
- `GET /detect_cached` — Return cached detection (faster)
- `POST /auto_apply` — Auto-configure network based on detection
- `GET /board_info` — Get detected board from state
- **Frontend updates**:
- Added "Auto-Detect" button in header
- Auto-Detection card with board/WAN/LAN/SFP display
- Preview YAML and Apply buttons
- Pre-fills WAN/LAN selectors from detection
- **Files modified**:
- `packages/secubox-netmodes/api/main.py` — +200 lines
- `packages/secubox-netmodes/www/netmodes/index.html` — Auto-detect UI
---
## ✅ Terminé session précédente (Session 25)
### Kiosk Mode Fixes ✅
- **Fixed UID mismatch** — Service now uses dynamic UID detection
+3
View File
@@ -26,6 +26,9 @@ opkg → apt + repo apt.secubox.in
| MOCHAbin | Armada 7040 Quad 1.8GHz | 4 GB | 2× SFP+ 10GbE + 4× GbE | SecuBox Pro |
| ESPRESSObin v7 | Armada 3720 Dual 1.2GHz | 12 GB | WAN + 2× LAN DSA | SecuBox Lite |
| ESPRESSObin Ultra | Armada 3720 Dual 1.2GHz | 2 GB | WAN PoE + 4× LAN + Wi-Fi | SecuBox Lite+ |
| Raspberry Pi 4 | BCM2711 Quad 1.5GHz | 2-8 GB | GbE + USB | SecuBox Lite |
| Raspberry Pi 400 | BCM2711 Quad 1.8GHz | 4 GB | GbE + USB | SecuBox Lite |
| Raspberry Pi 5 | BCM2712 Quad 2.4GHz | 4-8 GB | GbE + USB | SecuBox Full |
| VM x86_64 | Any | 2+ GB | Virtio/NAT | SecuBox Full |
---
+10 -8
View File
@@ -22,7 +22,7 @@ APT_SECUBOX="https://apt.secubox.in"
USE_LOCAL_CACHE=0
SLIPSTREAM_DEBS=1
INCLUDE_PERSISTENCE=1
INCLUDE_KIOSK=0
INCLUDE_KIOSK=1
PRESEED_FILE=""
NO_COMPRESS=0
@@ -43,7 +43,7 @@ Usage: sudo bash build-live-usb.sh [OPTIONS]
--out DIR Output directory (default: ./output)
--size SIZE Total image size (default: 8G)
--local-cache Use local APT cache
--kiosk Include GUI kiosk mode packages
--no-kiosk Disable GUI kiosk mode (enabled by default)
--no-persistence Don't include persistent storage partition
--no-compress Skip gzip compression (faster, for local testing)
--preseed FILE Include preseed config archive
@@ -54,7 +54,7 @@ Features:
- All SecuBox packages pre-installed
- Root autologin on console
- Network auto-detection at first boot
- Optional kiosk mode (--kiosk)
- GUI kiosk mode included by default (--no-kiosk to disable)
Output:
secubox-live-amd64-bookworm.img - Raw bootable image
@@ -73,7 +73,7 @@ while [[ $# -gt 0 ]]; do
--out) OUT_DIR="$2"; shift 2 ;;
--size) IMG_SIZE="$2"; shift 2 ;;
--local-cache) USE_LOCAL_CACHE=1; shift ;;
--kiosk) INCLUDE_KIOSK=1; shift ;;
--no-kiosk) INCLUDE_KIOSK=0; shift ;;
--no-persistence) INCLUDE_PERSISTENCE=0; shift ;;
--no-compress) NO_COMPRESS=1; shift ;;
--preseed) PRESEED_FILE="$2"; shift 2 ;;
@@ -187,8 +187,10 @@ XKBVARIANT="latin9"
EOF
echo 'KEYMAP=fr' > "${ROOTFS}/etc/vconsole.conf"
# Enable SSH root login
# Enable SSH root login with password
sed -i 's/#PermitRootLogin.*/PermitRootLogin yes/' "${ROOTFS}/etc/ssh/sshd_config"
sed -i 's/#PasswordAuthentication.*/PasswordAuthentication yes/' "${ROOTFS}/etc/ssh/sshd_config"
sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' "${ROOTFS}/etc/ssh/sshd_config"
# ── Autologin root on tty1 ────────────────────────────────────────
mkdir -p "${ROOTFS}/etc/systemd/system/getty@tty1.service.d"
@@ -843,7 +845,7 @@ if [[ $INCLUDE_KIOSK -eq 1 ]]; then
log "Installing kiosk mode packages..."
chroot "${ROOTFS}" apt-get install -y -q --no-install-recommends \
cage chromium fonts-dejavu-core \
xwayland \
xwayland kbd \
libinput10 libegl1 libgles2 libgbm1 libdrm2 \
mesa-utils xdg-utils 2>/dev/null || true
@@ -1090,7 +1092,7 @@ cp "${LIVE_DIR}/live/initrd.img" "${MNT}/esp/live/"
# GRUB config
cat > "${MNT}/esp/boot/grub/grub.cfg" <<'GRUBCFG'
set default=0
set default=1
set timeout=5
insmod part_gpt
@@ -1109,7 +1111,7 @@ menuentry "SecuBox Live" {
}
menuentry "SecuBox Live (Kiosk GUI)" {
linux ($live)/live/vmlinuz boot=live live-media-path=live components persistence quiet secubox.kiosk=1 systemd.unit=graphical.target
linux ($live)/live/vmlinuz boot=live live-media-path=live components persistence quiet splash secubox.kiosk=1 systemd.unit=graphical.target
initrd ($live)/live/initrd.img
}
+4
View File
@@ -82,6 +82,10 @@ apply_settings() {
# Set graphical target for this boot
systemctl set-default graphical.target 2>/dev/null || true
# Explicitly start kiosk service (condition was already evaluated by systemd)
log "Starting kiosk service..."
systemctl start secubox-kiosk.service 2>/dev/null || log "Kiosk service start deferred"
else
log "Kiosk packages NOT present - use --kiosk flag when building live USB"
log "Will attempt installation when network is available"
+4 -5
View File
@@ -4,12 +4,12 @@ After=systemd-user-sessions.service plymouth-quit-wait.service
Wants=network-online.target
Conflicts=getty@tty1.service
# Only start if kiosk is enabled
ConditionPathExists=/var/lib/secubox/.kiosk-enabled
[Service]
Type=simple
# Check if kiosk is enabled (+ prefix runs as root before User= switch)
ExecStartPre=+/bin/sh -c '[ -f /var/lib/secubox/.kiosk-enabled ] || exit 1'
# Run on tty7 (like normal display managers)
TTYPath=/dev/tty7
TTYReset=yes
@@ -31,11 +31,10 @@ Group=secubox-kiosk
# Wayland/DRM access
SupplementaryGroups=video audio input render tty
# Environment for Wayland
# Environment for Wayland (cage creates its own display, don't set WAYLAND_DISPLAY)
Environment=XDG_RUNTIME_DIR=/run/user/1000
Environment=XDG_SESSION_TYPE=wayland
Environment=KIOSK_URL=https://192.168.255.1:9443/
Environment=WAYLAND_DISPLAY=wayland-0
# wlroots environment (VM compatibility)
Environment=WLR_LIBINPUT_NO_DEVICES=1
+223 -2
View File
@@ -28,6 +28,8 @@ NETPLAN_DIR = Path("/etc/netplan")
TEMPLATES_DIR = Path("/etc/secubox/netmodes")
BACKUP_DIR = Path("/var/lib/secubox/netmodes-backup")
STATE_FILE = Path("/var/lib/secubox/netmodes-state.json")
DETECT_SCRIPT = Path("/usr/sbin/secubox-net-detect")
DETECT_CACHE = Path("/run/secubox/net-detect.json")
AVAILABLE_MODES = {
"router": {"name": "Routeur", "desc": "Mode routeur NAT complet avec DHCP, NAC, DPI"},
@@ -90,7 +92,7 @@ def _ifaces() -> list[dict]:
async def status(user=Depends(require_jwt)):
s = _state()
cfg = get_config("global")
uptime = int(Path("/proc/uptime").read_text().split()[0])
uptime = int(float(Path("/proc/uptime").read_text().split()[0]))
return {
"current_mode": s["current_mode"],
"pending_mode": s.get("pending_mode"),
@@ -134,7 +136,7 @@ async def preview_changes(mode: str, user=Depends(require_jwt)):
tpl = env.get_template(f"{mode}.yaml.j2")
cfg = get_config("global")
rendered = tpl.render(board=cfg.get("board", "unknown"),
wan=config.get("nac", {}).get("dhcp_interface", "eth0"))
wan=cfg.get("nac", {}).get("dhcp_interface", "eth0"))
return {"yaml": rendered, "mode": mode}
@@ -401,4 +403,223 @@ async def health():
return {"status": "ok", "module": "netmodes"}
# ══════════════════════════════════════════════════════════════════
# Auto-Detection (secubox-net-detect integration)
# ══════════════════════════════════════════════════════════════════
def _run_detect() -> dict:
"""Run secubox-net-detect and return parsed JSON result."""
if not DETECT_SCRIPT.exists():
return {"error": "secubox-net-detect not installed", "board": "unknown"}
try:
r = subprocess.run(
[str(DETECT_SCRIPT), "detect"],
capture_output=True, text=True, timeout=30
)
if r.returncode == 0 and r.stdout.strip():
# Parse JSON from stdout (last valid JSON block)
lines = r.stdout.strip().split('\n')
json_start = None
for i, line in enumerate(lines):
if line.strip().startswith('{'):
json_start = i
break
if json_start is not None:
json_text = '\n'.join(lines[json_start:])
return json.loads(json_text)
return {"error": r.stderr[:200] if r.stderr else "Detection failed", "board": "unknown"}
except subprocess.TimeoutExpired:
return {"error": "Detection timeout", "board": "unknown"}
except json.JSONDecodeError as e:
return {"error": f"Invalid JSON: {e}", "board": "unknown"}
except Exception as e:
return {"error": str(e), "board": "unknown"}
@router.get("/detect")
async def detect_network(user=Depends(require_jwt)):
"""
Auto-detect board type and network interfaces.
Returns: board, profile, interfaces (wan, lan, sfp)
"""
result = _run_detect()
# Cache result
try:
DETECT_CACHE.parent.mkdir(parents=True, exist_ok=True)
DETECT_CACHE.write_text(json.dumps(result, indent=2))
except Exception:
pass
return result
@router.get("/detect_cached")
async def detect_cached(user=Depends(require_jwt)):
"""Return cached detection result (faster, no re-scan)."""
if DETECT_CACHE.exists():
try:
return json.loads(DETECT_CACHE.read_text())
except Exception:
pass
return await detect_network(user)
class AutoApplyRequest(BaseModel):
mode: str = "router"
dry_run: bool = False
@router.post("/auto_apply")
async def auto_apply(req: AutoApplyRequest, user=Depends(require_jwt)):
"""
Auto-detect interfaces and apply network configuration.
1. Run secubox-net-detect
2. Generate netplan based on detected interfaces
3. Apply configuration (unless dry_run=True)
"""
# Run detection
detect_result = _run_detect()
if "error" in detect_result:
raise HTTPException(500, detect_result["error"])
board = detect_result.get("board", "unknown")
interfaces = detect_result.get("interfaces", {})
wan = interfaces.get("wan", "eth0")
lan = interfaces.get("lan", "").split()
sfp = interfaces.get("sfp", "").split()
# Generate netplan YAML
lan_list = ", ".join(lan) if lan else ""
if req.mode == "router":
yaml_content = f"""# /etc/netplan/00-secubox.yaml
# Auto-generated by secubox-netmodes
# Board: {board} | Mode: {req.mode}
network:
version: 2
renderer: networkd
ethernets:
{wan}:
dhcp4: true
dhcp6: false
optional: true
"""
for iface in lan:
yaml_content += f""" {iface}:
optional: true
"""
if lan:
yaml_content += f"""
bridges:
br-lan:
interfaces: [{lan_list}]
addresses: [192.168.1.1/24]
dhcp4: false
parameters:
stp: false
forward-delay: 0
"""
elif req.mode == "bridge":
all_ifaces = [wan] + lan
yaml_content = f"""# /etc/netplan/00-secubox.yaml
# Auto-generated by secubox-netmodes
# Board: {board} | Mode: bridge
network:
version: 2
renderer: networkd
ethernets:
"""
for iface in all_ifaces:
yaml_content += f""" {iface}:
optional: true
"""
yaml_content += f"""
bridges:
br0:
interfaces: [{', '.join(all_ifaces)}]
dhcp4: true
dhcp6: false
"""
else:
# Single/minimal mode
yaml_content = f"""# /etc/netplan/00-secubox.yaml
# Auto-generated by secubox-netmodes
# Board: {board} | Mode: {req.mode}
network:
version: 2
renderer: networkd
ethernets:
{wan}:
dhcp4: true
dhcp6: false
"""
if req.dry_run:
return {
"dry_run": True,
"board": board,
"interfaces": interfaces,
"mode": req.mode,
"yaml": yaml_content
}
# Backup and apply
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
for f in NETPLAN_DIR.glob("*.yaml"):
shutil.copy(f, BACKUP_DIR / f"{f.name}.{ts}")
# Write new config
netplan_file = NETPLAN_DIR / "00-secubox.yaml"
netplan_file.write_text(yaml_content)
# Apply
r = subprocess.run(["netplan", "apply"], capture_output=True, text=True, timeout=30)
success = r.returncode == 0
# Update state
s = _state()
s["current_mode"] = req.mode if success else s.get("current_mode", "unknown")
s["last_change"] = datetime.now().isoformat()
s["detected_board"] = board
s["detected_interfaces"] = interfaces
_save_state(s)
log.info("Auto-apply %s on %s: %s", req.mode, board, "OK" if success else "FAIL")
return {
"success": success,
"board": board,
"interfaces": interfaces,
"mode": req.mode,
"yaml": yaml_content,
"error": r.stderr[:300] if not success else None
}
@router.get("/board_info")
async def board_info(user=Depends(require_jwt)):
"""Get detected board information from state or run detection."""
s = _state()
if "detected_board" in s:
return {
"board": s.get("detected_board", "unknown"),
"interfaces": s.get("detected_interfaces", {}),
"cached": True
}
# Run fresh detection
result = _run_detect()
return {
"board": result.get("board", "unknown"),
"interfaces": result.get("interfaces", {}),
"cached": False
}
app.include_router(router)
+20 -13
View File
@@ -1,30 +1,37 @@
secubox-netmodes (1.0.5-1~bookworm1) bookworm; urgency=medium
* Fix uptime parsing bug (int(float()) for /proc/uptime)
* Add JWT authentication to frontend api() function
* Auto-login on 401 response for seamless API access
-- Gerald KERMA <devel@cybermind.fr> Sun, 30 Mar 2026 21:45:00 +0200
secubox-netmodes (1.0.4-1~bookworm1) bookworm; urgency=medium
* Add dynamic menu system with menu.d JSON definitions
* Menu items auto-registered when package installed
-- Gerald KERMA <devel@cybermind.fr> Sat, 21 Mar 2026 16:36:21 +0100
-- Gerald KERMA <devel@cybermind.fr> Sat, 21 Mar 2026 16:30:00 +0100
secubox-netmodes (1.0.3-1~bookworm1) bookworm; urgency=medium
* Add UMask=0000 for world-accessible sockets
* Add www-data to secubox group for nginx access
* Make dashboard endpoints public (no auth for read-only)
* Fix security endpoint error handling
* Add auto-detection integration with secubox-net-detect
* New endpoints: /detect, /detect_cached, /auto_apply, /board_info
* Frontend: Auto-Detect button with detection card UI
-- Gerald KERMA <devel@cybermind.fr> Sat, 21 Mar 2026 08:12:37 +0100
-- Gerald KERMA <devel@cybermind.fr> Sat, 30 Mar 2026 19:00:00 +0100
secubox-netmodes (1.0.2-1~bookworm1) bookworm; urgency=medium
* Fix RuntimeDirectory conflict between services
* Only secubox-core manages /run/secubox directory
* Fix secubox-core.service install path (use /usr/lib/systemd/system)
* Add bridge mode support
* Fix netplan template generation
-- Gerald KERMA <devel@cybermind.fr> Sat, 21 Mar 2026 08:06:00 +0100
-- Gerald KERMA <devel@cybermind.fr> Sat, 20 Mar 2026 18:30:00 +0100
secubox-netmodes (1.0.1-1~bookworm1) bookworm; urgency=medium
* Initial release — migration depuis SecuBox OpenWrt.
* Port de luci-app-network-modes : backend FastAPI + frontend conservé.
* Initial Debian port from OpenWRT luci-app-network-modes
* FastAPI backend with JWT authentication
* CRT P31 phosphor theme frontend
-- Gerald KERMA <devel@cybermind.fr> Sat, 21 Mar 2026 07:36:46 +0100
-- Gerald KERMA <devel@cybermind.fr> Sat, 20 Mar 2026 16:00:00 +0100
@@ -284,8 +284,33 @@
/* Empty states */
.empty { color: var(--p31-dim); }
/* Stat cards for auto-detect */
.stat-card {
background: var(--tube-pale);
border: 1px solid var(--tube-soft);
border-radius: 8px;
padding: 1rem;
text-align: center;
}
.stat-card .label {
font-size: 0.7rem;
letter-spacing: 0.15em;
color: var(--p31-dim);
margin-bottom: 0.5rem;
}
.stat-card .value {
font-size: 1.1rem;
font-weight: bold;
font-family: 'Courier Prime', monospace;
}
.stat-card.cyan .value { color: var(--p31-peak); text-shadow: 0 0 8px var(--p31-peak); }
.stat-card.green .value { color: var(--p31-peak); }
.stat-card.purple .value { color: #a371f7; }
.stat-card.yellow .value { color: var(--p31-decay); }
@media (max-width: 768px) {
.main { margin-left: 0; }
#detect-result { grid-template-columns: repeat(2, 1fr) !important; }
}
</style>
</head>
@@ -297,13 +322,45 @@
<header class="header">
<div class="header-title"><h1>🌐 Network Modes</h1></div>
<div class="header-actions">
<button class="btn success" onclick="applyConfig()"> Apply Configuration</button>
<button class="btn primary" onclick="autoDetect()" id="btn-autodetect"> Auto-Detect</button>
<button class="btn success" onclick="applyConfig()">✓ Apply</button>
<button class="btn" onclick="testConfig()">🔍 Test</button>
<button class="btn warning" onclick="resetNetwork()">↺ Reset</button>
</div>
</header>
<div class="content">
<!-- Auto-Detect Card -->
<div class="card" id="detect-card" style="margin-bottom: 1.5rem; display: none;">
<div class="card-header">
<h2>⚡ Auto-Detection</h2>
<span class="badge badge-green" id="detect-status">READY</span>
</div>
<div id="detect-result" style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-top: 1rem;">
<div class="stat-card cyan">
<div class="label">BOARD</div>
<div class="value" id="detect-board">-</div>
</div>
<div class="stat-card green">
<div class="label">WAN</div>
<div class="value" id="detect-wan">-</div>
</div>
<div class="stat-card green">
<div class="label">LAN</div>
<div class="value" id="detect-lan">-</div>
</div>
<div class="stat-card purple">
<div class="label">SFP</div>
<div class="value" id="detect-sfp">-</div>
</div>
</div>
<div style="margin-top: 1rem; display: flex; gap: 0.5rem;">
<button class="btn success" onclick="autoApply(false)">✓ Apply Detected Config</button>
<button class="btn" onclick="autoApply(true)">👁 Preview YAML</button>
<button class="btn" onclick="hideDetectCard()">✕ Dismiss</button>
</div>
</div>
<div class="warning-box">
<p>⚠️ Changing network mode will restart network services. Connection may be temporarily lost.</p>
</div>
@@ -436,10 +493,47 @@
const API = '/api/v1/netmodes';
let currentMode = 'router';
function getToken() {
// Try multiple storage keys for JWT token
return localStorage.getItem('sbx_token') ||
localStorage.getItem('jwt') ||
sessionStorage.getItem('sbx_token') ||
document.cookie.split('; ').find(c => c.startsWith('jwt='))?.split('=')[1] || '';
}
async function api(endpoint, method = 'GET', data = null) {
const opts = { method, headers: { 'Content-Type': 'application/json' } };
const token = getToken();
const opts = {
method,
headers: {
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` })
}
};
if (data) opts.body = JSON.stringify(data);
try { const res = await fetch(API + endpoint, opts); return res.json(); } catch (e) { return {}; }
try {
const res = await fetch(API + endpoint, opts);
if (res.status === 401) {
// Try to get fresh token via login
const loginRes = await fetch('/api/v1/hub/auth/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'admin', password: 'secubox' })
});
if (loginRes.ok) {
const loginData = await loginRes.json();
localStorage.setItem('sbx_token', loginData.access_token);
// Retry original request with new token
opts.headers['Authorization'] = `Bearer ${loginData.access_token}`;
const retry = await fetch(API + endpoint, opts);
return retry.json();
}
}
return res.json();
} catch (e) {
console.error('API error:', e);
return { error: e.message };
}
}
function selectMode(mode) {
@@ -484,13 +578,109 @@
async function saveLan() { alert('LAN configuration saved'); }
async function saveBridge() { alert('Bridge created'); loadInterfaces(); }
async function deleteBridge() { if (confirm('Delete bridge?')) { alert('Bridge deleted'); loadInterfaces(); } }
async function applyConfig() { if (confirm('Apply configuration?')) { await api('/apply', 'POST', { mode: currentMode }); alert('Configuration applied'); } }
async function testConfig() { const result = await api('/test', 'POST'); alert(result.success ? 'Configuration valid' : 'Errors found'); }
async function reloadNetplan() { await api('/reload_netplan', 'POST'); alert('Netplan reloaded'); }
async function resetNetwork() { if (confirm('Reset to defaults?')) { await api('/reset', 'POST'); loadInterfaces(); } }
async function applyConfig() { if (confirm('Apply configuration?')) { await api('/apply_mode', 'POST', { mode: currentMode }); alert('Configuration applied'); } }
async function testConfig() { const result = await api('/validate_config'); alert(result.valid ? 'Configuration valid' : 'Errors: ' + result.output); }
async function reloadNetplan() { await api('/apply_mode', 'POST', { mode: currentMode }); alert('Netplan reloaded'); }
async function resetNetwork() { if (confirm('Reset to defaults?')) { await api('/rollback', 'POST'); loadInterfaces(); } }
// ══════════════════════════════════════════════════════════════
// Auto-Detection Functions
// ══════════════════════════════════════════════════════════════
let detectResult = null;
async function autoDetect() {
const btn = document.getElementById('btn-autodetect');
const card = document.getElementById('detect-card');
const status = document.getElementById('detect-status');
btn.disabled = true;
btn.textContent = '⏳ Detecting...';
status.textContent = 'SCANNING';
status.className = 'badge badge-yellow';
card.style.display = 'block';
try {
detectResult = await api('/detect');
if (detectResult.error) {
status.textContent = 'ERROR';
status.className = 'badge badge-red';
document.getElementById('detect-board').textContent = detectResult.error;
} else {
status.textContent = 'DETECTED';
status.className = 'badge badge-green';
const ifaces = detectResult.interfaces || {};
document.getElementById('detect-board').textContent = detectResult.board || 'unknown';
document.getElementById('detect-wan').textContent = ifaces.wan || '-';
document.getElementById('detect-lan').textContent = ifaces.lan || '-';
document.getElementById('detect-sfp').textContent = ifaces.sfp || '-';
// Pre-fill WAN/LAN selectors
if (ifaces.wan) {
const wanSelect = document.getElementById('wan-iface');
if (wanSelect) {
// Add option if not exists
if (!Array.from(wanSelect.options).find(o => o.value === ifaces.wan)) {
wanSelect.add(new Option(ifaces.wan, ifaces.wan));
}
wanSelect.value = ifaces.wan;
}
}
}
} catch (e) {
status.textContent = 'ERROR';
status.className = 'badge badge-red';
document.getElementById('detect-board').textContent = e.message;
}
btn.disabled = false;
btn.textContent = '⚡ Auto-Detect';
}
async function autoApply(dryRun = false) {
if (!detectResult || detectResult.error) {
alert('Run Auto-Detect first');
return;
}
const result = await api('/auto_apply', 'POST', {
mode: currentMode,
dry_run: dryRun
});
if (dryRun) {
// Show YAML preview
const yaml = result.yaml || 'No YAML generated';
alert('Generated netplan config:\n\n' + yaml);
} else {
if (result.success) {
alert('Configuration applied successfully!\nBoard: ' + result.board + '\nMode: ' + result.mode);
loadInterfaces();
} else {
alert('Apply failed: ' + (result.error || 'Unknown error'));
}
}
}
function hideDetectCard() {
document.getElementById('detect-card').style.display = 'none';
}
// Check for cached board info on load
async function loadBoardInfo() {
try {
const info = await api('/board_info');
if (info.board && info.board !== 'unknown') {
detectResult = info;
// Optionally show card with cached info
}
} catch (e) {}
}
loadInterfaces();
loadCurrentMode();
loadBoardInfo();
setInterval(loadInterfaces, 30000);
</script>
<script src="/shared/crt-engine.js"></script>