diff --git a/README.md b/README.md index a44bd0f..a58691d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,134 @@ -# rpi-tor-relay-panel +Tor Relay Panel for Raspberry Pi +================================ -Run a Tor middle relay on Raspberry Pi with a web dashboard, live traffic charts, reachability checks, and one-command install. \ No newline at end of file +_Run a Tor relay at home with a beautiful, simple web dashboard._ + +Bring real privacy power to the internet—right from your Raspberry Pi. **Tor Relay Panel** makes it easy for anyone to run a **Tor middle relay** with a clean, password-protected dashboard, live charts, and simple controls. No command-line expertise needed. + +Why run a Tor relay? +-------------------- + +* **Strengthen online privacy** – Keep the Tor network fast, healthy, and resilient for everyone who needs it. + +* **Support digital freedom** – Join a global, decentralized infrastructure that protects free expression. + +* **You’re in control** – Set your own bandwidth and monthly data cap to match your connection and plan. + + +Highlights +---------- + +* **Friendly web interface** – Configure everything in minutes. + +* **Middle relay only** – Safe for homes; no exit traffic from your IP. + +* **Live charts & stats** – See traffic trends from the last 48 hours at a glance. + +* **Reachability widget** – Check if your relay is publicly reachable and has the **Running** flag. + +* **Simple controls** – Edit nickname, contact, ORPort, bandwidth, burst, and monthly cap anytime. + +* **Secure session login** – Password-protected dashboard for your local network. + +* **Dark / Light theme** – Switch instantly; your preference is remembered. + +* **First-time setup wizard** – Create your admin account and initial settings in one flow. + +* **Lightweight & open-source** – Built for Raspberry Pi OS (Debian) Lite 64-bit. + + +Who is it for? +-------------- + +* **Home users** who want to contribute to internet privacy. + +* **Makers & Raspberry Pi fans** looking for a meaningful, always-on project. + +* **Community builders** who want a reliable middle relay with simple, visual management. + + +What you need +------------- + +* A **Raspberry Pi** running Raspberry Pi OS (Debian) **Lite 64-bit**. + +* A stable **home internet connection**. + +* Recommended: **IPv4 port forward** (or **IPv6 inbound**) to your Tor **ORPort** so your relay can be publicly reachable. + + +> 🔒 The web dashboard is designed for your **local network**. You don’t need to expose it to the internet. + +Setup in minutes +---------------- + +1. **Get the files**Clone or download this repository to your Raspberry Pi. + +2. **Run the installer**From the project folder, run the install script with sudo (one line). + +3. **Open the dashboard**Visit the Pi’s IP in your browser, follow the **first-time setup**, and you’re done. + + +You’ll then see: + +* **Live status** (Tor version, bootstrap, circuits) + +* **Traffic graphs** (read/written bytes over time) + +* **Reachability check** (consensus, flags, “Tor thinks your address is…”) + +* **Simple edit panel** for bandwidth, burst, and monthly cap + + +Screenshots +----------- + + +* **Dashboard (Dark Mode)**!\[Tor Relay Panel – Dashboard (Dark)\](screenshots/dashboard-dark.jpg) + +* **Dashboard (Light Mode)**!\[Tor Relay Panel – Dashboard (Light)\](screenshots/dashboard-light.jpg) + +* **Login Panel**!\[Tor Relay Panel – Login\](screenshots/login.jpg) + +* **First-Time Setup**!\[Tor Relay Panel – Setup Wizard\](screenshots/setup-wizard.jpg) + + +Frequently asked questions +-------------------------- + +**Do I need to open a port?**Yes. To earn the **Running** flag and carry traffic, your relay must be reachable on its **ORPort** (default **9001/TCP**). The dashboard includes a reachability widget to help verify this. Most homes use a simple router port-forward (or allow inbound on IPv6). + +**Is this an exit relay?**No. This runs as a **non-exit, middle relay**. Your IP is not used to access websites on behalf of others. + +**Will this slow down my internet?**You choose your **bandwidth limit** and **monthly data cap**. Pick values that keep your connection comfortable. + +**Is it safe to run at home?**A middle relay is a popular, community-friendly way to support Tor. Always follow your ISP’s terms and local laws. + +**Do I need to expose the dashboard to the internet?**No. Keep the dashboard **local-only** on your home network. Only the Tor **ORPort** needs to be reachable from the outside. + +**Can I stop or change limits later?**Yes—use the dashboard to pause, adjust bandwidth, burst, or monthly cap any time. + +Tips for the best experience +---------------------------- + +* **Give it time:** after changes, it can take a while before your relay appears with the **Running** flag. + +* **Be consistent:** keeping your Pi on helps the network and improves reliability. + +* **Start modestly:** begin with conservative bandwidth/cap values, then adjust upward as you’re comfortable. + + +Uninstall +--------- + +Prefer to remove everything later? There’s a friendly uninstall script included to clean up the panel and related packages. Run it from the project folder with sudo. + +Join the network +---------------- + +Spin up your Raspberry Pi, run the installer, set your limits, and start helping keep the open internet open. Your small contribution makes a **big** difference. 🧅 + +Keywords (for discoverability) +------------------------------ + +Raspberry Pi Tor relay, Tor middle relay dashboard, easy Tor relay setup, privacy network volunteer, run a Tor node at home, open-source Tor panel, simple Tor web interface, live Tor traffic graphs, secure Tor relay management, Raspberry Pi privacy project, Tor relay Raspberry Pi guide, local web dashboard for Tor, dark mode Tor panel, lightweight Tor admin UI \ No newline at end of file diff --git a/bin/torpanel-collect.py b/bin/torpanel-collect.py new file mode 100644 index 0000000..0e36483 --- /dev/null +++ b/bin/torpanel-collect.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +import json, os, socket, time + +CONTROL_SOCK = "/run/tor/control" +COOKIE_FILE = "/run/tor/control.authcookie" +STATE_FILE = "/var/lib/torpanel/stats.json" +ROLL_MAX = 45000 + +def torctl_send(cmds): + s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + s.settimeout(2.5); s.connect(CONTROL_SOCK) + cookie = open(COOKIE_FILE, "rb").read().hex().upper() + s.sendall(f"AUTHENTICATE {cookie}\r\n".encode()) + if not s.recv(4096).decode(errors="ignore").startswith("250"): + raise RuntimeError("auth failed") + out = {} + for c in cmds: + s.sendall((c+"\r\n").encode()) + buf=b"" + while True: + chunk=s.recv(4096) + if not chunk: break + buf+=chunk + if buf.endswith(b"250 OK\r\n"): break + for line in buf.decode(errors="ignore").strip().splitlines(): + if line.startswith("250-") or line.startswith("250 "): + line=line[4:] + if "=" in line: + k,v=line.split("=",1); out[k.strip()]=v.strip() + s.close(); return out + +def load_state(): + try: return json.load(open(STATE_FILE,"r")) + except: return {"data":[]} + +def save_state(obj): + obj["data"] = obj["data"][-ROLL_MAX:] + tmp = STATE_FILE + ".tmp" + with open(tmp, "w") as f: + json.dump(obj, f, separators=(",", ":")) + f.flush(); os.fsync(f.fileno()) + os.replace(tmp, STATE_FILE) + +def main(): + try: + info=torctl_send(["GETINFO traffic/read","GETINFO traffic/written","GETINFO status/circuit-established","GETINFO version"]) + now=int(time.time()) + state=load_state() + state["data"].append({ + "t":now, + "read":int(info.get("traffic/read","0")), + "written":int(info.get("traffic/written","0")), + "circ":1 if info.get("status/circuit-established","0")=="1" else 0, + "version":info.get("version","unknown") + }) + save_state(state) + except Exception: + pass + +if __name__=="__main__": main() \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..6c46b17 --- /dev/null +++ b/install.sh @@ -0,0 +1,192 @@ +#!/usr/bin/env bash +set -euo pipefail + +C_RESET="\033[0m"; C_DIM="\033[2m"; C_BOLD="\033[1m" +C_RED="\033[31m"; C_GRN="\033[32m"; C_BLU="\033[34m"; C_YEL="\033[33m" +info(){ echo -e "${C_BLU}➜${C_RESET} $*"; } +ok(){ echo -e "${C_GRN}✓${C_RESET} $*"; } +warn(){ echo -e "${C_YEL}!${C_RESET} $*"; } +fail(){ echo -e "${C_RED}✗${C_RESET} $*"; } + +if [[ $EUID -ne 0 ]]; then fail "Run as root (sudo)."; exit 1; fi +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +PANEL_ROOT="/var/www/torpanel" +PANEL_PUBLIC="$PANEL_ROOT/public" +STATE_DIR="/var/lib/torpanel" +LOG_DIR="/var/log/torpanel" +ETC_APP="/etc/torpanel" + +TOR_ETC="/etc/tor" +TOR_TORRC_D="$TOR_ETC/torrc.d" +TOR_PANEL_CONF="$TOR_TORRC_D/99-torpanel.conf" + +NGX_SITE_AVAIL="/etc/nginx/sites-available/torpanel" +NGX_SITE_ENABL="/etc/nginx/sites-enabled/torpanel" +SUDOERS_FILE="/etc/sudoers.d/torpanel" + +COLLECTOR_SRC="$SCRIPT_DIR/bin/torpanel-collect.py" +COLLECTOR_BIN="/usr/local/bin/torpanel-collect.py" +SVC="/etc/systemd/system/torpanel-collector.service" +TIMER="/etc/systemd/system/torpanel-collector.timer" + +export DEBIAN_FRONTEND=noninteractive + +echo -e "${C_BOLD}Installing TorPanel...${C_RESET}" + +info "Updating apt and installing packages" +apt-get update -y >/dev/null +apt-get install -y --no-install-recommends \ + tor tor-geoipdb nginx rsync \ + php-fpm php-cli php-json php-curl php-zip php-common php-opcache \ + python3 >/dev/null +ok "Packages installed" + +PHPV=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;') +PHP_FPM_SVC="php${PHPV}-fpm" +PHP_SOCK="/run/php/php${PHPV}-fpm.sock" +ln -sf "$PHP_SOCK" /run/php/php-fpm.sock || true + +info "Preparing directories" +install -d "$PANEL_PUBLIC" "$STATE_DIR" "$LOG_DIR" "$ETC_APP" +touch "$STATE_DIR/stats.json" +rsync -a --delete "$SCRIPT_DIR/web/" "$PANEL_PUBLIC/" +chown -R www-data:www-data "$PANEL_ROOT" "$STATE_DIR" "$LOG_DIR" +chmod 750 "$PANEL_ROOT" "$STATE_DIR" "$LOG_DIR" +chown root:www-data "$ETC_APP"; chmod 770 "$ETC_APP" +ok "Web files deployed" + +info "Writing Nginx site" +cat > "$NGX_SITE_AVAIL" <<'NGINX' +server { + listen 80 default_server; + server_name _; + + root /var/www/torpanel/public; + index index.php index.html; + + location / { + try_files $uri $uri/ /index.php?$args; + } + + location ~ \.php$ { + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/run/php/php-fpm.sock; + } + + location ~ /\. { + deny all; + } +} +NGINX +rm -f /etc/nginx/sites-enabled/default || true +ln -sf "$NGX_SITE_AVAIL" "$NGX_SITE_ENABL" +ok "Nginx site enabled" + +info "Writing torrc defaults" +install -d "$TOR_TORRC_D" +cat > "$TOR_PANEL_CONF" <<'TORRC' +## --- Managed by TorPanel --- +SocksPort 0 +ORPort 9001 +ExitRelay 0 +ExitPolicy reject *:* +Nickname RaspberryRelay +ContactInfo contact@admin.com +BandwidthRate 5 MB +BandwidthBurst 10 MB +AccountingMax 100 GB +AccountingStart month 1 00:00 +ControlPort 0 +ControlSocket /run/tor/control +CookieAuthentication 1 +CookieAuthFileGroupReadable 1 +# --- End TorPanel block --- +TORRC +ok "torrc written" + +info "Setting permissions for Tor managed config" +chown root:www-data "$TOR_TORRC_D"; chmod 775 "$TOR_TORRC_D" +chown root:www-data "$TOR_PANEL_CONF"; chmod 664 "$TOR_PANEL_CONF" +ok "Permissions applied" + +info "Granting www-data access to Tor cookie" +usermod -aG debian-tor www-data || true +ok "Permissions set" + +info "Allowing www-data to control tor (limited)" +cat > "$SUDOERS_FILE" <<'SUD' +www-data ALL=NOPASSWD:/bin/systemctl reload tor, /bin/systemctl restart tor, /bin/systemctl start tor, /bin/systemctl stop tor +SUD +chmod 440 "$SUDOERS_FILE" +ok "Sudoers entry created" + +info "Installing collector" +install -m 0755 "$COLLECTOR_SRC" "$COLLECTOR_BIN" +chown www-data:www-data "$COLLECTOR_BIN" +cat > "$SVC" <<'UNIT' +[Unit] +Description=TorPanel minute collector +After=tor.service +Wants=tor.service + +[Service] +Type=oneshot +User=www-data +Group=www-data +SupplementaryGroups=debian-tor +ExecStart=/usr/bin/env python3 /usr/local/bin/torpanel-collect.py +NoNewPrivileges=yes +ProtectSystem=strict +ProtectHome=yes +ProtectClock=yes +ProtectHostname=yes +ProtectKernelLogs=yes +ProtectKernelModules=yes +ProtectKernelTunables=yes +PrivateTmp=yes +PrivateDevices=yes +PrivateUsers=yes +LockPersonality=yes +MemoryDenyWriteExecute=yes +RestrictRealtime=yes +RestrictSUIDSGID=yes +UMask=0077 +ReadWriteDirectories=/var/lib/torpanel +ReadOnlyPaths=/run/tor /etc/tor +RestrictAddressFamilies=AF_UNIX +SystemCallFilter=@system-service +CapabilityBoundingSet= + +[Install] +WantedBy=timers.target +UNIT + +cat > "$TIMER" <<'TIMER' +[Unit] +Description=Run TorPanel collector every minute +[Timer] +OnBootSec=30sec +OnUnitActiveSec=60sec +AccuracySec=15sec +Persistent=true +[Install] +WantedBy=timers.target +TIMER +ok "Systemd units installed" + +info "Restarting services" +systemctl daemon-reload +systemctl enable --now tor +systemctl enable "$PHP_FPM_SVC" nginx >/dev/null +systemctl restart "$PHP_FPM_SVC" +systemctl restart nginx +systemctl enable --now torpanel-collector.timer +ok "Services running" + +IP=$(hostname -I 2>/dev/null | awk '{print $1}') +echo +echo -e "${C_BOLD}All set!${C_RESET}" +echo -e " URL: ${C_GRN}http://$IP/${C_RESET}" +echo -e " First run: you'll see a setup page to create the admin user." +echo -e " Tip: forward ${C_BOLD}TCP/9001${C_RESET} to your Pi for a publicly reachable relay." \ No newline at end of file diff --git a/screenshots/dashboard-dark.jpg b/screenshots/dashboard-dark.jpg new file mode 100644 index 0000000..349b421 Binary files /dev/null and b/screenshots/dashboard-dark.jpg differ diff --git a/screenshots/dasrhboard-light.jpg b/screenshots/dasrhboard-light.jpg new file mode 100644 index 0000000..0508cbe Binary files /dev/null and b/screenshots/dasrhboard-light.jpg differ diff --git a/screenshots/login.jpg b/screenshots/login.jpg new file mode 100644 index 0000000..323076d Binary files /dev/null and b/screenshots/login.jpg differ diff --git a/screenshots/setup-wizard.jpg b/screenshots/setup-wizard.jpg new file mode 100644 index 0000000..23f6765 Binary files /dev/null and b/screenshots/setup-wizard.jpg differ diff --git a/uninstall.sh b/uninstall.sh new file mode 100644 index 0000000..8826d87 --- /dev/null +++ b/uninstall.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +set -euo pipefail + +C_RESET="\033[0m"; C_DIM="\033[2m"; C_BOLD="\033[1m" +C_RED="\033[31m"; C_GRN="\033[32m"; C_BLU="\033[34m"; C_YEL="\033[33m" +info(){ echo -e "${C_BLU}➜${C_RESET} $*"; } +ok(){ echo -e "${C_GRN}✓${C_RESET} $*"; } +warn(){ echo -e "${C_YEL}!${C_RESET} $*"; } +fail(){ echo -e "${C_RED}✗${C_RESET} $*"; } +trap 'fail "An unexpected error occurred. Exiting."' ERR + +PANEL_ROOT="/var/www/torpanel" +PANEL_PUBLIC="$PANEL_ROOT/public" +STATE_DIR="/var/lib/torpanel" +LOG_DIR="/var/log/torpanel" +ETC_APP="/etc/torpanel" + +NGX_SITE_AVAIL="/etc/nginx/sites-available/torpanel" +NGX_SITE_ENABL="/etc/nginx/sites-enabled/torpanel" +SUDOERS_FILE="/etc/sudoers.d/torpanel" + +TOR_TORRC_D="/etc/tor/torrc.d" +TOR_PANEL_CONF="$TOR_TORRC_D/99-torpanel.conf" + +COLLECTOR_BIN="/usr/local/bin/torpanel-collect.py" +SVC="/etc/systemd/system/torpanel-collector.service" +TIMER="/etc/systemd/system/torpanel-collector.timer" + +PKGS=("tor" "tor-geoipdb" "nginx" "php-fpm" "php-cli" "php-json" "php-curl" "php-zip" "php-common" "php-opcache") + +YES=0 +PURGE=0 +for arg in "${@:-}"; do + case "$arg" in + -y|--yes) YES=1 ;; + -h|--help) + echo "Usage: sudo bash uninstall.sh [--yes]" + echo " --yes : don't ask for confirmation (defaults to no package purge)" + exit 0 + ;; + *) ;; + esac +done + +if [[ $EUID -ne 0 ]]; then fail "Run as root (sudo)."; exit 1; fi + +echo -e "${C_BOLD}Uninstalling TorPanel...${C_RESET}" + +# Step 1: confirm uninstall +if [[ $YES -ne 1 ]]; then + read -r -p "This will remove TorPanel files/configs. Continue? [y/N] " ans + case "${ans,,}" in y|yes) ;; *) warn "Aborted by user."; exit 0 ;; esac +fi + +# Step 2: ask if we should purge packages (only when not --yes) +if [[ $YES -ne 1 ]]; then + echo + echo -e "${C_DIM}Optional:${C_RESET} You can also ${C_BOLD}purge Tor/Nginx/PHP packages${C_RESET} installed by the panel." + read -r -p "Also purge Tor/Nginx/PHP packages and autoremove? [y/N] " pans + case "${pans,,}" in y|yes) PURGE=1 ;; *) PURGE=0 ;; esac +else + PURGE=0 +fi + +info "Stopping services/timers if present" +systemctl disable --now torpanel-collector.timer 2>/dev/null || true +systemctl disable --now torpanel-collector.service 2>/dev/null || true +ok "Services/timers handled" + +info "Removing systemd units" +rm -f "$SVC" "$TIMER" +systemctl daemon-reload +ok "Systemd units removed" + +info "Removing collector binary" +rm -f "$COLLECTOR_BIN" +ok "Collector removed" + +info "Removing Nginx site" +rm -f "$NGX_SITE_ENABL" "$NGX_SITE_AVAIL" +if nginx -t >/dev/null 2>&1; then + systemctl reload nginx || true +else + warn "Nginx config test failed (maybe not installed); skipping reload" +fi +ok "Nginx site removed" + +info "Removing TorPanel app/config/state" +rm -rf "$PANEL_ROOT" "$STATE_DIR" "$LOG_DIR" "$ETC_APP" +ok "Files removed" + +info "Removing torrc override and reloading Tor" +rm -f "$TOR_PANEL_CONF" +systemctl reload tor 2>/dev/null || true +ok "Tor reloaded" + +info "Removing sudoers entry" +rm -f "$SUDOERS_FILE" +ok "Sudoers cleaned" + +if [[ $PURGE -eq 1 ]]; then + info "Purging Tor/Nginx/PHP packages" + apt-get purge -y "${PKGS[@]}" || true + apt-get autoremove -y || true + ok "Packages purged" +fi + +echo +echo -e "${C_BOLD}All clean!${C_RESET}" +if [[ $PURGE -eq 1 ]]; then + echo -e " TorPanel removed and packages were purged." +else + echo -e " TorPanel files and overrides have been removed (packages remain)." +fi +echo -e " Reinstall anytime with: ${C_GRN}sudo bash install.sh${C_RESET}" \ No newline at end of file diff --git a/web/api/config_get.php b/web/api/config_get.php new file mode 100644 index 0000000..008bbf5 --- /dev/null +++ b/web/api/config_get.php @@ -0,0 +1,22 @@ + $ui['nickname'], + 'ContactInfo' => $ui['contact'], + 'ORPort' => (string)$ui['orport'], + 'BandwidthRate' => rtrim(rtrim(number_format($rate_mb, 2, '.', ''), '0'), '.') . ' MB', + 'BandwidthBurst' => rtrim(rtrim(number_format($burst_mb, 2, '.', ''), '0'), '.') . ' MB', + 'AccountingMax' => $ui['cap_gb'] . ' GB', + 'AccountingStart' => 'month ' . $ui['acc_day'] . ' 00:00', +]; + +echo json_encode(['ok' => true] + $ui + $torrc); \ No newline at end of file diff --git a/web/api/config_set.php b/web/api/config_set.php new file mode 100644 index 0000000..728ccd0 --- /dev/null +++ b/web/api/config_set.php @@ -0,0 +1,12 @@ +false,'error'=>'bad json']); exit; } + +$ok = config_apply($in); +echo json_encode(['ok'=>$ok]); \ No newline at end of file diff --git a/web/api/now.php b/web/api/now.php new file mode 100644 index 0000000..c22790d --- /dev/null +++ b/web/api/now.php @@ -0,0 +1,23 @@ + true, + "read" => (int)($info["traffic/read"] ?? 0), + "written" => (int)($info["traffic/written"] ?? 0), + "circuits" => (($info["status/circuit-established"] ?? "0") === "1"), + "version" => ($info["version"] ?? "unknown"), + "bootstrap" => ($info["status/bootstrap-phase"] ?? "") + ]); +} catch (Throwable $e) { + echo json_encode(["ok"=>false,"error"=>$e->getMessage()]); +} \ No newline at end of file diff --git a/web/api/reach.php b/web/api/reach.php new file mode 100644 index 0000000..15c123c --- /dev/null +++ b/web/api/reach.php @@ -0,0 +1,62 @@ +/dev/null')); + $lan_ip = $lan_ip ? preg_split('/\s+/', $lan_ip)[0] : ''; + + $onionoo = [ + 'found' => false, 'running' => false, 'flags' => [], + 'last_seen' => null, 'nickname' => ($cfg['Nickname'] ?? 'RaspberryRelay'), + 'or_addresses' => [], 'country' => null + ]; + + if ($fingerprint !== '') { + $url = 'https://onionoo.torproject.org/details?lookup=' . urlencode($fingerprint); + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 6, + CURLOPT_USERAGENT => 'TorPanel/1.0 (+relay reachability check)', + ]); + $res = curl_exec($ch); + $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($res !== false && $http === 200) { + $j = json_decode($res, true); + if (!empty($j['relays']) && count($j['relays']) > 0) { + $r = $j['relays'][0]; + $onionoo['found'] = true; + $onionoo['running'] = (bool)($r['running'] ?? false); + $onionoo['flags'] = $r['flags'] ?? []; + $onionoo['last_seen'] = $r['last_seen'] ?? null; + $onionoo['nickname'] = $r['nickname'] ?? $onionoo['nickname']; + $onionoo['or_addresses']= $r['or_addresses'] ?? []; + $onionoo['country'] = $r['country'] ?? null; + } + } + } + + echo json_encode([ + 'ok' => true, + 'fingerprint' => $fingerprint, + 'nickname' => $onionoo['nickname'], + 'orport' => $orport, + 'tor_address' => $tor_addr, + 'lan_ip' => $lan_ip, + 'onionoo' => $onionoo, + ]); +} catch (Throwable $e) { + echo json_encode(['ok'=>false,'error'=>$e->getMessage()]); +} \ No newline at end of file diff --git a/web/api/stats.php b/web/api/stats.php new file mode 100644 index 0000000..bbb3eb4 --- /dev/null +++ b/web/api/stats.php @@ -0,0 +1,7 @@ +[]]); exit; } +$raw = file_get_contents($path); +echo $raw ?: json_encode(["data"=>[]]); diff --git a/web/assets/panel.css b/web/assets/panel.css new file mode 100644 index 0000000..c8bf4f6 --- /dev/null +++ b/web/assets/panel.css @@ -0,0 +1,133 @@ +:root{ + --bg:#0b1220; + --card:#111a2e; + --text:#e7ecf4; + --muted:#c6d3ee; + --brand:#2b7bff; + --border:#223257; + --input:#0c1527; + --grid: rgba(231,236,244,.15); +} + +:root[data-theme="light"]{ + --bg:#f7f9fc; + --card:#ffffff; + --text:#0b1220; + --muted:#56617a; + --brand:#0d6efd; + --border:#dee2e6; + --input:#ffffff; + --grid: rgba(0,0,0,.1); +} + +html,body{ + height:100%; + background:var(--bg); + color:var(--text); +} + +.card{ + background:var(--card); + border:1px solid var(--border); + border-radius:16px; + color:var(--text); +} + +h1,h2,h3,h4,h5,h6 { color:var(--text) !important; } +.h1,.h2,.h3,.h4,.h5,.h6 { color:var(--text) !important; } +.form-label,label{ color:var(--text) !important; } +.small, .text-muted, .form-text{ color:var(--muted) !important; } + +.mono{ + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; + color: var(--text) !important; +} + +.form-control,.form-select{ + background:var(--input); + color:var(--text); + border-color:var(--border); +} +.form-control::placeholder{ color:#9db1d6; opacity:.8; } +:root[data-theme="light"] .form-control::placeholder{ color:#6c757d; } +.form-control:focus,.form-select:focus{ + background:var(--input); + color:var(--text); + border-color:var(--brand); + box-shadow:0 0 0 .2rem color-mix(in oklab, var(--brand) 25%, transparent); +} + +input:-webkit-autofill, +input:-webkit-autofill:hover, +input:-webkit-autofill:focus, +textarea:-webkit-autofill, +select:-webkit-autofill{ + -webkit-box-shadow:0 0 0 30px var(--input) inset !important; + -webkit-text-fill-color:var(--text) !important; + caret-color:var(--text); +} + +.btn-primary{ background:var(--brand); border:0; } +.btn-secondary{ background:#3a4663; border:0; color:#fff; } +:root[data-theme="light"] .btn-secondary{ background:#6c7aa6; } + +.page-wrap{ min-height:100vh; display:flex; align-items:center; justify-content:center; padding:24px; } +.maxw-960{ max-width:960px; } + +.equal-row > [class*="col-"]{ display:flex; } +.equal-row .card{ width:100%; } + +#trafficWrap{ height:260px; } +#trafficChart{ width:100%; height:100%; display:block; } + +.navbar-brand img{ width:20px; height:20px; margin-right:.5rem; vertical-align:-3px; } + +:root[data-theme="dark"] .alert-warning{ + background-color:#3b2b00 !important; + border-color:#8a6d1a !important; + color:var(--text) !important; +} +:root[data-theme="dark"] .alert-warning .fw-semibold, +:root[data-theme="dark"] .alert-warning .mono, +:root[data-theme="dark"] .alert-warning .small, +:root[data-theme="dark"] .alert-warning li, +:root[data-theme="dark"] .alert-warning p{ color:var(--text) !important; } +:root[data-theme="dark"] .alert-warning a{ color:#ffd267 !important; text-decoration:underline; } + +.input-group-text{ + background: var(--input); + color: var(--text); + border-color: var(--border); + padding: .375rem .5rem; +} + +.input-group .form-control{ + background: var(--input); + color: var(--text); + border-color: var(--border); +} +.input-group .form-control:focus{ + background: var(--input); + color: var(--text); + border-color: var(--brand); + box-shadow: 0 0 0 .2rem color-mix(in oklab, var(--brand) 25%, transparent); +} + +.input-group > .form-control, +.input-group > .form-select, +.input-group > .input-group-text{ + border-radius: .375rem; +} + +:root[data-theme="light"] .card .btn-outline-light{ + color: var(--brand) !important; + border-color: var(--brand) !important; +} + +:root[data-theme="light"] .card .btn-outline-light:hover, +:root[data-theme="light"] .card .btn-outline-light:focus{ + background: var(--brand) !important; + color: #fff !important; + border-color: var(--brand) !important; + box-shadow: 0 0 0 .2rem color-mix(in oklab, var(--brand) 25%, transparent); +} \ No newline at end of file diff --git a/web/favicon.svg b/web/favicon.svg new file mode 100644 index 0000000..a90665d --- /dev/null +++ b/web/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/index.php b/web/index.php new file mode 100644 index 0000000..afb3cc2 --- /dev/null +++ b/web/index.php @@ -0,0 +1,447 @@ + + + +
+ +