mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 22:07:24 +00:00
feat(phase8): Add MagicMirror smart display platform
- FastAPI backend for MagicMirror² management - Module installation/removal from GitHub - Display brightness and power controls - Web UI with CRT-light theme and shared sidebar Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b849c7f42d
commit
43b56537de
309
packages/secubox-magicmirror/api/main.py
Normal file
309
packages/secubox-magicmirror/api/main.py
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
"""SecuBox MagicMirror API - Smart display platform management."""
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
import subprocess
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
app = FastAPI(title="SecuBox MagicMirror API", version="1.0.0")
|
||||
|
||||
MM_DIR = "/opt/MagicMirror"
|
||||
CONFIG_FILE = f"{MM_DIR}/config/config.js"
|
||||
MODULES_DIR = f"{MM_DIR}/modules"
|
||||
SERVICE_NAME = "magicmirror"
|
||||
|
||||
|
||||
class ModuleConfig(BaseModel):
|
||||
module: str
|
||||
position: str = "top_right"
|
||||
config: dict = {}
|
||||
|
||||
|
||||
class DisplayConfig(BaseModel):
|
||||
port: int = 8080
|
||||
address: str = "0.0.0.0"
|
||||
ipWhitelist: list = []
|
||||
language: str = "en"
|
||||
timeFormat: int = 24
|
||||
units: str = "metric"
|
||||
|
||||
|
||||
def run_cmd(cmd: list, check: bool = True) -> tuple:
|
||||
"""Run command and return (stdout, stderr, returncode)."""
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
return result.stdout, result.stderr, result.returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
return "", "Command timed out", 1
|
||||
except Exception as e:
|
||||
return "", str(e), 1
|
||||
|
||||
|
||||
def is_service_running() -> bool:
|
||||
"""Check if MagicMirror service is running."""
|
||||
_, _, code = run_cmd(["systemctl", "is-active", "--quiet", SERVICE_NAME], check=False)
|
||||
return code == 0
|
||||
|
||||
|
||||
def parse_config_js() -> dict:
|
||||
"""Parse MagicMirror config.js to extract configuration."""
|
||||
if not os.path.exists(CONFIG_FILE):
|
||||
return {}
|
||||
|
||||
try:
|
||||
with open(CONFIG_FILE, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Extract modules array using regex (simplified parsing)
|
||||
modules = []
|
||||
module_pattern = r'\{\s*module:\s*["\']([^"\']+)["\']'
|
||||
for match in re.finditer(module_pattern, content):
|
||||
modules.append(match.group(1))
|
||||
|
||||
# Extract basic settings
|
||||
port_match = re.search(r'port:\s*(\d+)', content)
|
||||
lang_match = re.search(r'language:\s*["\'](\w+)["\']', content)
|
||||
|
||||
return {
|
||||
"modules": modules,
|
||||
"port": int(port_match.group(1)) if port_match else 8080,
|
||||
"language": lang_match.group(1) if lang_match else "en"
|
||||
}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def list_installed_modules() -> list:
|
||||
"""List installed MagicMirror modules."""
|
||||
modules = []
|
||||
default_modules = ["alert", "calendar", "clock", "compliments", "currentweather",
|
||||
"helloworld", "newsfeed", "weatherforecast", "updatenotification"]
|
||||
|
||||
# Add default modules
|
||||
for mod in default_modules:
|
||||
modules.append({"name": mod, "type": "default", "path": f"{MODULES_DIR}/default/{mod}"})
|
||||
|
||||
# Add third-party modules
|
||||
if os.path.exists(MODULES_DIR):
|
||||
for entry in os.listdir(MODULES_DIR):
|
||||
path = os.path.join(MODULES_DIR, entry)
|
||||
if os.path.isdir(path) and entry not in ["default", "node_modules"]:
|
||||
modules.append({"name": entry, "type": "third-party", "path": path})
|
||||
|
||||
return modules
|
||||
|
||||
|
||||
def list_available_modules() -> list:
|
||||
"""Return list of popular MagicMirror modules for installation."""
|
||||
return [
|
||||
{"name": "MMM-GoogleCalendar", "description": "Google Calendar integration"},
|
||||
{"name": "MMM-Spotify", "description": "Spotify now playing widget"},
|
||||
{"name": "MMM-SystemStats", "description": "System CPU/RAM/disk stats"},
|
||||
{"name": "MMM-Network-Signal", "description": "Network signal strength"},
|
||||
{"name": "MMM-Wallpaper", "description": "Dynamic wallpaper backgrounds"},
|
||||
{"name": "MMM-cryptocurrency", "description": "Cryptocurrency prices"},
|
||||
{"name": "MMM-PIR-Sensor", "description": "Motion sensor screen control"},
|
||||
{"name": "MMM-homeassistant-sensors", "description": "Home Assistant integration"},
|
||||
{"name": "MMM-Facial-Recognition", "description": "Face recognition profiles"},
|
||||
{"name": "MMM-voice-assistant", "description": "Voice control integration"},
|
||||
]
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok", "service": "magicmirror"}
|
||||
|
||||
|
||||
@app.get("/status")
|
||||
def get_status():
|
||||
"""Get MagicMirror service status and configuration."""
|
||||
running = is_service_running()
|
||||
installed = os.path.exists(MM_DIR)
|
||||
config = parse_config_js() if installed else {}
|
||||
|
||||
return {
|
||||
"installed": installed,
|
||||
"running": running,
|
||||
"mm_dir": MM_DIR,
|
||||
"config": config,
|
||||
"module_count": len(config.get("modules", []))
|
||||
}
|
||||
|
||||
|
||||
@app.post("/start")
|
||||
def start_service():
|
||||
"""Start MagicMirror service."""
|
||||
stdout, stderr, code = run_cmd(["systemctl", "start", SERVICE_NAME])
|
||||
if code != 0:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to start: {stderr}")
|
||||
return {"status": "started"}
|
||||
|
||||
|
||||
@app.post("/stop")
|
||||
def stop_service():
|
||||
"""Stop MagicMirror service."""
|
||||
stdout, stderr, code = run_cmd(["systemctl", "stop", SERVICE_NAME])
|
||||
if code != 0:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to stop: {stderr}")
|
||||
return {"status": "stopped"}
|
||||
|
||||
|
||||
@app.post("/restart")
|
||||
def restart_service():
|
||||
"""Restart MagicMirror service."""
|
||||
stdout, stderr, code = run_cmd(["systemctl", "restart", SERVICE_NAME])
|
||||
if code != 0:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to restart: {stderr}")
|
||||
return {"status": "restarted"}
|
||||
|
||||
|
||||
@app.get("/modules")
|
||||
def get_modules():
|
||||
"""List all installed modules."""
|
||||
return {"modules": list_installed_modules()}
|
||||
|
||||
|
||||
@app.get("/modules/available")
|
||||
def get_available_modules():
|
||||
"""List popular modules available for installation."""
|
||||
return {"modules": list_available_modules()}
|
||||
|
||||
|
||||
@app.post("/modules/install/{module_name}")
|
||||
def install_module(module_name: str):
|
||||
"""Install a third-party MagicMirror module from GitHub."""
|
||||
if not module_name.startswith("MMM-"):
|
||||
raise HTTPException(status_code=400, detail="Module name must start with MMM-")
|
||||
|
||||
module_path = f"{MODULES_DIR}/{module_name}"
|
||||
if os.path.exists(module_path):
|
||||
raise HTTPException(status_code=409, detail="Module already installed")
|
||||
|
||||
# Clone from GitHub (most MM modules follow this pattern)
|
||||
github_url = f"https://github.com/MichMich/{module_name}.git"
|
||||
stdout, stderr, code = run_cmd(
|
||||
["git", "clone", "--depth", "1", github_url, module_path],
|
||||
check=False
|
||||
)
|
||||
|
||||
if code != 0:
|
||||
# Try alternative org patterns
|
||||
for org in ["MagicMirrorOrg", "bugsounet"]:
|
||||
github_url = f"https://github.com/{org}/{module_name}.git"
|
||||
stdout, stderr, code = run_cmd(
|
||||
["git", "clone", "--depth", "1", github_url, module_path],
|
||||
check=False
|
||||
)
|
||||
if code == 0:
|
||||
break
|
||||
|
||||
if code != 0:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to clone module: {stderr}")
|
||||
|
||||
# Run npm install if package.json exists
|
||||
if os.path.exists(f"{module_path}/package.json"):
|
||||
run_cmd(["npm", "install", "--prefix", module_path], check=False)
|
||||
|
||||
return {"status": "installed", "module": module_name, "path": module_path}
|
||||
|
||||
|
||||
@app.delete("/modules/{module_name}")
|
||||
def uninstall_module(module_name: str):
|
||||
"""Uninstall a third-party module."""
|
||||
module_path = f"{MODULES_DIR}/{module_name}"
|
||||
|
||||
if not os.path.exists(module_path):
|
||||
raise HTTPException(status_code=404, detail="Module not found")
|
||||
|
||||
if module_name in ["default", "node_modules"]:
|
||||
raise HTTPException(status_code=400, detail="Cannot remove system directories")
|
||||
|
||||
import shutil
|
||||
shutil.rmtree(module_path)
|
||||
|
||||
return {"status": "uninstalled", "module": module_name}
|
||||
|
||||
|
||||
@app.get("/config")
|
||||
def get_config():
|
||||
"""Get current MagicMirror configuration."""
|
||||
if not os.path.exists(CONFIG_FILE):
|
||||
raise HTTPException(status_code=404, detail="Config file not found")
|
||||
|
||||
with open(CONFIG_FILE, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
return {"config_file": CONFIG_FILE, "content": content}
|
||||
|
||||
|
||||
@app.get("/logs")
|
||||
def get_logs(lines: int = 50):
|
||||
"""Get recent MagicMirror logs."""
|
||||
stdout, stderr, code = run_cmd(
|
||||
["journalctl", "-u", SERVICE_NAME, "-n", str(lines), "--no-pager"]
|
||||
)
|
||||
return {"logs": stdout.split('\n') if stdout else []}
|
||||
|
||||
|
||||
@app.get("/display")
|
||||
def get_display_info():
|
||||
"""Get display/screen information."""
|
||||
# Check for display server
|
||||
display_info = {
|
||||
"DISPLAY": os.environ.get("DISPLAY", ""),
|
||||
"WAYLAND_DISPLAY": os.environ.get("WAYLAND_DISPLAY", "")
|
||||
}
|
||||
|
||||
# Get screen resolution if xrandr available
|
||||
stdout, _, code = run_cmd(["xrandr", "--current"], check=False)
|
||||
if code == 0:
|
||||
for line in stdout.split('\n'):
|
||||
if '*' in line: # Current resolution marked with *
|
||||
parts = line.split()
|
||||
if parts:
|
||||
display_info["resolution"] = parts[0]
|
||||
break
|
||||
|
||||
return display_info
|
||||
|
||||
|
||||
@app.post("/display/brightness/{level}")
|
||||
def set_brightness(level: int):
|
||||
"""Set display brightness (0-100)."""
|
||||
if not 0 <= level <= 100:
|
||||
raise HTTPException(status_code=400, detail="Brightness must be 0-100")
|
||||
|
||||
# Try xrandr brightness
|
||||
brightness = level / 100.0
|
||||
stdout, stderr, code = run_cmd(
|
||||
["xrandr", "--output", "HDMI-1", "--brightness", str(brightness)],
|
||||
check=False
|
||||
)
|
||||
|
||||
if code != 0:
|
||||
# Try other common output names
|
||||
for output in ["HDMI-0", "DP-1", "eDP-1", "VGA-1"]:
|
||||
_, _, code = run_cmd(
|
||||
["xrandr", "--output", output, "--brightness", str(brightness)],
|
||||
check=False
|
||||
)
|
||||
if code == 0:
|
||||
break
|
||||
|
||||
return {"brightness": level}
|
||||
|
||||
|
||||
@app.post("/display/power/{state}")
|
||||
def set_display_power(state: str):
|
||||
"""Turn display on or off."""
|
||||
if state not in ["on", "off"]:
|
||||
raise HTTPException(status_code=400, detail="State must be 'on' or 'off'")
|
||||
|
||||
if state == "off":
|
||||
run_cmd(["xset", "dpms", "force", "off"], check=False)
|
||||
else:
|
||||
run_cmd(["xset", "dpms", "force", "on"], check=False)
|
||||
run_cmd(["xset", "s", "reset"], check=False)
|
||||
|
||||
return {"display": state}
|
||||
8
packages/secubox-magicmirror/debian/changelog
Normal file
8
packages/secubox-magicmirror/debian/changelog
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
secubox-magicmirror (1.0.0-1) stable; urgency=low
|
||||
|
||||
* Initial release
|
||||
* MagicMirror service management
|
||||
* Module installation and management
|
||||
* Display brightness and power controls
|
||||
|
||||
-- SecuBox Team <team@secubox.local> Sat, 28 Mar 2026 10:00:00 +0000
|
||||
15
packages/secubox-magicmirror/debian/control
Normal file
15
packages/secubox-magicmirror/debian/control
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
Source: secubox-magicmirror
|
||||
Section: web
|
||||
Priority: optional
|
||||
Maintainer: SecuBox Team <team@secubox.local>
|
||||
Build-Depends: debhelper-compat (= 13)
|
||||
Standards-Version: 4.6.0
|
||||
|
||||
Package: secubox-magicmirror
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, python3, python3-fastapi, python3-uvicorn, secubox-core
|
||||
Recommends: nodejs, npm, xserver-xorg, chromium
|
||||
Description: SecuBox MagicMirror management
|
||||
Smart display platform management for SecuBox.
|
||||
Provides API and web UI for MagicMirror² configuration,
|
||||
module management, and display controls.
|
||||
12
packages/secubox-magicmirror/debian/postinst
Executable file
12
packages/secubox-magicmirror/debian/postinst
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
case "$1" in
|
||||
configure)
|
||||
install -d -o secubox -g secubox -m 750 /var/lib/secubox/magicmirror
|
||||
systemctl daemon-reload
|
||||
systemctl enable secubox-magicmirror.service
|
||||
systemctl start secubox-magicmirror.service || true
|
||||
systemctl reload nginx 2>/dev/null || true
|
||||
;;
|
||||
esac
|
||||
#DEBHELPER#
|
||||
16
packages/secubox-magicmirror/debian/rules
Executable file
16
packages/secubox-magicmirror/debian/rules
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/make -f
|
||||
%:
|
||||
dh $@
|
||||
|
||||
override_dh_auto_install:
|
||||
install -d $(CURDIR)/debian/secubox-magicmirror/usr/lib/secubox/magicmirror/api
|
||||
install -m 644 api/main.py $(CURDIR)/debian/secubox-magicmirror/usr/lib/secubox/magicmirror/api/
|
||||
touch $(CURDIR)/debian/secubox-magicmirror/usr/lib/secubox/magicmirror/api/__init__.py
|
||||
install -d $(CURDIR)/debian/secubox-magicmirror/usr/share/secubox/www/magicmirror
|
||||
install -m 644 www/magicmirror/index.html $(CURDIR)/debian/secubox-magicmirror/usr/share/secubox/www/magicmirror/
|
||||
install -d $(CURDIR)/debian/secubox-magicmirror/etc/nginx/secubox.d
|
||||
install -m 644 nginx/magicmirror.conf $(CURDIR)/debian/secubox-magicmirror/etc/nginx/secubox.d/
|
||||
install -d $(CURDIR)/debian/secubox-magicmirror/usr/share/secubox/menu.d
|
||||
install -m 644 menu.d/819-magicmirror.json $(CURDIR)/debian/secubox-magicmirror/usr/share/secubox/menu.d/
|
||||
install -d $(CURDIR)/debian/secubox-magicmirror/usr/lib/systemd/system
|
||||
install -m 644 debian/secubox-magicmirror.service $(CURDIR)/debian/secubox-magicmirror/usr/lib/systemd/system/
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
[Unit]
|
||||
Description=SecuBox MagicMirror API
|
||||
After=network.target secubox-core.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/usr/lib/secubox/magicmirror
|
||||
ExecStart=/usr/bin/uvicorn api.main:app --uds /run/secubox/magicmirror.sock --log-level warning
|
||||
UMask=0117
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
1
packages/secubox-magicmirror/menu.d/819-magicmirror.json
Normal file
1
packages/secubox-magicmirror/menu.d/819-magicmirror.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"id":"magicmirror","name":"MagicMirror","icon":"🪞","path":"/magicmirror/","category":"apps","order":819,"description":"Smart display platform"}
|
||||
2
packages/secubox-magicmirror/nginx/magicmirror.conf
Normal file
2
packages/secubox-magicmirror/nginx/magicmirror.conf
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
location /api/v1/magicmirror/ { proxy_pass http://unix:/run/secubox/magicmirror.sock; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }
|
||||
location /magicmirror/ { alias /usr/share/secubox/www/magicmirror/; try_files $uri $uri/ /magicmirror/index.html; }
|
||||
364
packages/secubox-magicmirror/www/magicmirror/index.html
Normal file
364
packages/secubox-magicmirror/www/magicmirror/index.html
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MagicMirror - SecuBox</title>
|
||||
<link rel="stylesheet" href="/shared/sidebar-light.css">
|
||||
<link rel="stylesheet" href="/shared/crt-light.css">
|
||||
<style>
|
||||
.main-content {
|
||||
margin-left: 200px;
|
||||
padding: 2rem;
|
||||
}
|
||||
.status-card {
|
||||
background: var(--card-bg, #1a1a2e);
|
||||
border: 1px solid var(--border-color, #333);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.status-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid var(--border-color, #333);
|
||||
}
|
||||
.status-row:last-child { border-bottom: none; }
|
||||
.badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
.badge-success { background: #2d5a27; color: #7fff7f; }
|
||||
.badge-danger { background: #5a2727; color: #ff7f7f; }
|
||||
.badge-warning { background: #5a5a27; color: #ffff7f; }
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
.btn-primary { background: var(--accent-color, #4a9eff); color: #000; }
|
||||
.btn-success { background: #27ae60; color: #fff; }
|
||||
.btn-danger { background: #c0392b; color: #fff; }
|
||||
.btn-secondary { background: #555; color: #fff; }
|
||||
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.module-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.module-card {
|
||||
background: var(--card-bg, #1a1a2e);
|
||||
border: 1px solid var(--border-color, #333);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
}
|
||||
.module-card h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: var(--accent-color, #4a9eff);
|
||||
}
|
||||
.module-card .type {
|
||||
font-size: 0.8rem;
|
||||
color: #888;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.section-title {
|
||||
margin: 2rem 0 1rem 0;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--accent-color, #4a9eff);
|
||||
}
|
||||
.controls-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
.slider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
.slider-container input[type="range"] {
|
||||
width: 200px;
|
||||
}
|
||||
.slider-value {
|
||||
min-width: 3rem;
|
||||
text-align: center;
|
||||
}
|
||||
.log-box {
|
||||
background: #0a0a0a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
color: #7fff7f;
|
||||
}
|
||||
.install-form {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.install-form input {
|
||||
flex: 1;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
background: #1a1a2e;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div class="main-content">
|
||||
<h1>🪞 MagicMirror</h1>
|
||||
<p>Smart display platform management</p>
|
||||
|
||||
<div class="status-card">
|
||||
<h3>Service Status</h3>
|
||||
<div class="status-row">
|
||||
<span>MagicMirror Service</span>
|
||||
<span id="service-status" class="badge badge-warning">Checking...</span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span>Installation</span>
|
||||
<span id="install-status" class="badge badge-warning">Checking...</span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span>Active Modules</span>
|
||||
<span id="module-count">-</span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span>Web Interface</span>
|
||||
<span id="web-port">-</span>
|
||||
</div>
|
||||
<div style="margin-top: 1rem;">
|
||||
<button class="btn btn-success" onclick="startService()">Start</button>
|
||||
<button class="btn btn-danger" onclick="stopService()">Stop</button>
|
||||
<button class="btn btn-secondary" onclick="restartService()">Restart</button>
|
||||
<a id="open-mm" href="#" target="_blank" class="btn btn-primary" style="text-decoration: none; display: none;">Open MagicMirror</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="status-card">
|
||||
<h3>Display Controls</h3>
|
||||
<div class="controls-row">
|
||||
<button class="btn btn-success" onclick="displayPower('on')">Display On</button>
|
||||
<button class="btn btn-danger" onclick="displayPower('off')">Display Off</button>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
<label>Brightness:</label>
|
||||
<input type="range" id="brightness" min="0" max="100" value="100" onchange="setBrightness(this.value)">
|
||||
<span class="slider-value" id="brightness-value">100%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">Installed Modules</h2>
|
||||
<div id="modules-grid" class="module-grid">
|
||||
<p>Loading modules...</p>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">Install Module</h2>
|
||||
<div class="install-form">
|
||||
<input type="text" id="module-name" placeholder="MMM-ModuleName (e.g., MMM-SystemStats)">
|
||||
<button class="btn btn-primary" onclick="installModule()">Install</button>
|
||||
</div>
|
||||
|
||||
<h3>Popular Modules</h3>
|
||||
<div id="available-modules" class="module-grid">
|
||||
<p>Loading available modules...</p>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">Logs</h2>
|
||||
<button class="btn btn-secondary" onclick="loadLogs()">Refresh Logs</button>
|
||||
<div id="logs" class="log-box" style="margin-top: 1rem;">
|
||||
Click "Refresh Logs" to load...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/shared/sidebar.js"></script>
|
||||
<script>
|
||||
const API = '/api/v1/magicmirror';
|
||||
|
||||
async function loadStatus() {
|
||||
try {
|
||||
const res = await fetch(`${API}/status`);
|
||||
const data = await res.json();
|
||||
|
||||
document.getElementById('service-status').textContent = data.running ? 'Running' : 'Stopped';
|
||||
document.getElementById('service-status').className = 'badge ' + (data.running ? 'badge-success' : 'badge-danger');
|
||||
|
||||
document.getElementById('install-status').textContent = data.installed ? 'Installed' : 'Not Installed';
|
||||
document.getElementById('install-status').className = 'badge ' + (data.installed ? 'badge-success' : 'badge-danger');
|
||||
|
||||
document.getElementById('module-count').textContent = data.module_count || 0;
|
||||
|
||||
const port = data.config?.port || 8080;
|
||||
document.getElementById('web-port').textContent = `Port ${port}`;
|
||||
|
||||
const openBtn = document.getElementById('open-mm');
|
||||
if (data.running) {
|
||||
openBtn.style.display = 'inline-block';
|
||||
openBtn.href = `http://${window.location.hostname}:${port}`;
|
||||
} else {
|
||||
openBtn.style.display = 'none';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load status:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadModules() {
|
||||
try {
|
||||
const res = await fetch(`${API}/modules`);
|
||||
const data = await res.json();
|
||||
|
||||
const grid = document.getElementById('modules-grid');
|
||||
if (data.modules && data.modules.length > 0) {
|
||||
grid.innerHTML = data.modules.map(m => `
|
||||
<div class="module-card">
|
||||
<h4>${m.name}</h4>
|
||||
<div class="type">${m.type}</div>
|
||||
${m.type === 'third-party' ?
|
||||
`<button class="btn btn-danger" onclick="uninstallModule('${m.name}')">Uninstall</button>` :
|
||||
''}
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
grid.innerHTML = '<p>No modules found</p>';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load modules:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAvailableModules() {
|
||||
try {
|
||||
const res = await fetch(`${API}/modules/available`);
|
||||
const data = await res.json();
|
||||
|
||||
const grid = document.getElementById('available-modules');
|
||||
if (data.modules && data.modules.length > 0) {
|
||||
grid.innerHTML = data.modules.map(m => `
|
||||
<div class="module-card">
|
||||
<h4>${m.name}</h4>
|
||||
<div class="type">${m.description}</div>
|
||||
<button class="btn btn-primary" onclick="installModuleByName('${m.name}')">Install</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load available modules:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function startService() {
|
||||
try {
|
||||
await fetch(`${API}/start`, { method: 'POST' });
|
||||
loadStatus();
|
||||
} catch (e) {
|
||||
alert('Failed to start service: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopService() {
|
||||
try {
|
||||
await fetch(`${API}/stop`, { method: 'POST' });
|
||||
loadStatus();
|
||||
} catch (e) {
|
||||
alert('Failed to stop service: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function restartService() {
|
||||
try {
|
||||
await fetch(`${API}/restart`, { method: 'POST' });
|
||||
loadStatus();
|
||||
} catch (e) {
|
||||
alert('Failed to restart service: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function displayPower(state) {
|
||||
try {
|
||||
await fetch(`${API}/display/power/${state}`, { method: 'POST' });
|
||||
} catch (e) {
|
||||
alert('Failed to control display: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function setBrightness(level) {
|
||||
document.getElementById('brightness-value').textContent = level + '%';
|
||||
try {
|
||||
await fetch(`${API}/display/brightness/${level}`, { method: 'POST' });
|
||||
} catch (e) {
|
||||
console.error('Failed to set brightness:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function installModule() {
|
||||
const name = document.getElementById('module-name').value.trim();
|
||||
if (!name) {
|
||||
alert('Please enter a module name');
|
||||
return;
|
||||
}
|
||||
await installModuleByName(name);
|
||||
}
|
||||
|
||||
async function installModuleByName(name) {
|
||||
try {
|
||||
const res = await fetch(`${API}/modules/install/${name}`, { method: 'POST' });
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
throw new Error(err.detail || 'Install failed');
|
||||
}
|
||||
alert('Module installed successfully! Restart MagicMirror to activate.');
|
||||
loadModules();
|
||||
} catch (e) {
|
||||
alert('Failed to install module: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function uninstallModule(name) {
|
||||
if (!confirm(`Uninstall ${name}?`)) return;
|
||||
try {
|
||||
await fetch(`${API}/modules/${name}`, { method: 'DELETE' });
|
||||
loadModules();
|
||||
} catch (e) {
|
||||
alert('Failed to uninstall module: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
try {
|
||||
const res = await fetch(`${API}/logs?lines=50`);
|
||||
const data = await res.json();
|
||||
document.getElementById('logs').textContent = data.logs?.join('\n') || 'No logs available';
|
||||
} catch (e) {
|
||||
document.getElementById('logs').textContent = 'Failed to load logs';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadStatus();
|
||||
loadModules();
|
||||
loadAvailableModules();
|
||||
});
|
||||
|
||||
// Auto-refresh status every 30s
|
||||
setInterval(loadStatus, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user