From 6067857dad5abc02e6f0331f0d256d2e7b6ecf7a Mon Sep 17 00:00:00 2001 From: "Gan, Jimmy" Date: Sat, 7 Mar 2026 10:36:37 +0800 Subject: [PATCH 1/2] fix: clarify blocked security events Label unauthorized security events as blocked and highlight likely scanner probes so the log view is easier to interpret during investigations. Co-Authored-By: Claude Opus 4.6 (1M context) --- dashboard/frontend/src/routes/Security.svelte | 44 +++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/dashboard/frontend/src/routes/Security.svelte b/dashboard/frontend/src/routes/Security.svelte index 843611b..29cf99d 100644 --- a/dashboard/frontend/src/routes/Security.svelte +++ b/dashboard/frontend/src/routes/Security.svelte @@ -86,8 +86,41 @@ filter !== "all" || !!ipFilter.trim() || !!startDate || !!endDate ); - const typeLabels = { auth_failure: "Auth", suspicious_request: "Request", error: "Error" }; - const typeColors = { auth_failure: "text-red-400", suspicious_request: "text-amber-400", error: "text-surface-500" }; + function isBlockedScannerEvent(entry) { + if (entry.type !== "unauthorized") return false; + + const message = (entry.message || "").toLowerCase(); + return [ + "/.git/", + "phpinfo", + "debug.log", + "laravel.log", + "stripe", + "swagger.json", + "wp-config", + "credentials", + "secrets", + "config", + "@vite", + "manifest.json", + ].some((needle) => message.includes(needle)); + } + + const logTypeFilters = ["all", "auth_failure", "suspicious_request", "unauthorized"]; + const logTypeButtonLabels = { + all: "All", + auth_failure: "Auth", + suspicious_request: "Requests", + unauthorized: "Blocked", + }; + + const typeLabels = { auth_failure: "Auth", suspicious_request: "Request", unauthorized: "Blocked", error: "Error" }; + const typeColors = { + auth_failure: "text-red-400", + suspicious_request: "text-amber-400", + unauthorized: "text-orange-400", + error: "text-surface-500" + };
@@ -171,9 +204,9 @@
- {#each ["all", "auth_failure", "suspicious_request"] as f} + {#each logTypeFilters as f} {/each}
@@ -236,6 +269,9 @@ {#if e.username} {e.username} {/if} + {#if isBlockedScannerEvent(e)} + scanner + {/if} {e.message} {/each} -- 2.52.0 From 1b9ea140b6aacdf295900017c994583931d3508b Mon Sep 17 00:00:00 2001 From: "Gan, Jimmy" Date: Sat, 7 Mar 2026 10:37:08 +0800 Subject: [PATCH 2/2] fix: raise terminal per-user session cap Allow more concurrent dashboard terminal tabs per user and use the current access token for websocket reconnects so refreshed sessions keep working. Co-Authored-By: Claude Opus 4.6 (1M context) --- dashboard/backend/routers/terminal.py | 47 +++++++- dashboard/frontend/src/routes/Terminal.svelte | 101 ++++++++++++++++-- 2 files changed, 134 insertions(+), 14 deletions(-) diff --git a/dashboard/backend/routers/terminal.py b/dashboard/backend/routers/terminal.py index d575237..180faa9 100644 --- a/dashboard/backend/routers/terminal.py +++ b/dashboard/backend/routers/terminal.py @@ -1,6 +1,7 @@ import asyncio import struct import logging +import shlex from collections import Counter from fastapi import WebSocket, Query import asyncssh @@ -10,19 +11,28 @@ from auth import get_current_user_ws logger = logging.getLogger(__name__) MAX_SESSIONS = 5 -MAX_SESSIONS_PER_USER = 2 +MAX_SESSIONS_PER_USER = 5 _session_lock = asyncio.Lock() _active_sessions: dict[int, str] = {} +PERSISTENCE_STATUS_PREFIX = "__DASHBOARD_PERSISTENCE__:" HOSTS = { "nas": {"host": config.SSH_HOST, "user": config.SSH_USER, "key": config.SSH_KEY_PATH}, "vps": {"host": config.VPS_SSH_HOST, "user": config.VPS_SSH_USER, "key": config.VPS_SSH_KEY_PATH}, "caddy-vps": {"host": config.CADDY_VPS_SSH_HOST, "user": config.CADDY_VPS_SSH_USER, "key": config.VPS_SSH_KEY_PATH}, "mac": {"host": config.MAC_SSH_HOST, "user": config.MAC_SSH_USER, "key": config.MAC_SSH_KEY_PATH}, - "claude-dev": {"host": config.SSH_HOST, "user": config.SSH_USER, "key": config.SSH_KEY_PATH, - "command": "/volume1/@appstore/ContainerManager/usr/bin/docker exec -it claude-dev bash"}, + "claude-dev": { + "host": config.SSH_HOST, + "user": config.SSH_USER, + "key": config.SSH_KEY_PATH, + "persistent_session": { + "container": "claude-dev", + "session_name": "dashboard-terminal", + }, + }, } + # Load known_hosts once at import time — fail closed if missing _known_hosts_available = False try: @@ -33,6 +43,35 @@ except Exception: logger.error("SSH known_hosts file not found at %s — terminal connections will be refused", config.SSH_KNOWN_HOSTS) +def build_host_command(host_config: dict) -> str | None: + persistent = host_config.get("persistent_session") + if not persistent: + return host_config.get("command") + + docker_bin = "/volume1/@appstore/ContainerManager/usr/bin/docker" + container = persistent["container"] + session_name = persistent["session_name"] + quoted_container = shlex.quote(container) + quoted_session = shlex.quote(session_name) + quoted_marker = shlex.quote(PERSISTENCE_STATUS_PREFIX) + attach_cmd = ( + f"{docker_bin} exec -it {quoted_container} " + f"tmux new-session -A -s {quoted_session}" + ) + + script = " ".join([ + "if", + f"{docker_bin} exec {quoted_container} tmux has-session -t {quoted_session} >/dev/null 2>&1;", + "then", + f"printf '%sattached\\n' {quoted_marker};", + "else", + f"printf '%screated\\n' {quoted_marker};", + "fi;", + f"exec {attach_cmd}", + ]) + return f"bash -lc {shlex.quote(script)}" + + async def _reserve_session(session_id: int, username: str) -> tuple[bool, str | None, int | None]: async with _session_lock: if len(_active_sessions) >= MAX_SESSIONS: @@ -98,7 +137,7 @@ async def ws_endpoint(websocket: WebSocket, host: str = Query("nas"), token: str try: process = await conn.create_process( - h.get("command"), term_type="xterm-256color", term_size=(80, 24), + build_host_command(h), term_type="xterm-256color", term_size=(80, 24), encoding=None, ) diff --git a/dashboard/frontend/src/routes/Terminal.svelte b/dashboard/frontend/src/routes/Terminal.svelte index 51c295c..6afd3b7 100644 --- a/dashboard/frontend/src/routes/Terminal.svelte +++ b/dashboard/frontend/src/routes/Terminal.svelte @@ -1,13 +1,17 @@