From 56d677e9253968a31606753dd4868fda7b85006c Mon Sep 17 00:00:00 2001 From: Dmitrii Iurco Date: Fri, 1 May 2026 11:34:38 -0400 Subject: [PATCH] fix: copy button HTTP fallback, reset-admin-password in Docker, scripts volume - CellNetwork.jsx CopyButton: use execCommand fallback when clipboard API is unavailable (HTTP non-localhost context) - Makefile reset-admin-password: run inside cell-api container via docker exec so bcrypt and all deps are available without host installation - docker-compose.yml: mount ./scripts:/app/scripts:ro in cell-api so the reset script is accessible inside the container - scripts/reset_admin_password.py: auto-detect API module path and data dir so the script works in both host (api/ sibling) and container (/app) layouts Co-Authored-By: Claude Sonnet 4.6 --- Makefile | 2 +- docker-compose.yml | 1 + scripts/reset_admin_password.py | 23 ++++++++++++++++++----- webui/src/pages/CellNetwork.jsx | 12 +++++++++++- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 9c562cb..1aaf4c2 100644 --- a/Makefile +++ b/Makefile @@ -284,7 +284,7 @@ show-admin-password: @sudo python3 scripts/reset_admin_password.py --show reset-admin-password: - @sudo python3 scripts/reset_admin_password.py --generate + @docker exec cell-api python3 /app/scripts/reset_admin_password.py --generate # ── Network / peers ─────────────────────────────────────────────────────────── diff --git a/docker-compose.yml b/docker-compose.yml index 5706bb1..493df73 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -206,6 +206,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock - ./.env:/app/.env.compose - ./docker-compose.yml:/app/docker-compose.yml:ro + - ./scripts:/app/scripts:ro pid: host restart: unless-stopped networks: diff --git a/scripts/reset_admin_password.py b/scripts/reset_admin_password.py index a684d40..1ecca0a 100644 --- a/scripts/reset_admin_password.py +++ b/scripts/reset_admin_password.py @@ -11,11 +11,24 @@ import os import secrets import string -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'api')) +_here = os.path.dirname(os.path.abspath(__file__)) +# Find auth_manager: host layout has api/ sibling, container has it one level up at /app +for _api_path in [os.path.join(_here, '..', 'api'), os.path.join(_here, '..'), '/app']: + if os.path.isfile(os.path.join(_api_path, 'auth_manager.py')): + sys.path.insert(0, os.path.normpath(_api_path)) + break -ROOT = os.path.join(os.path.dirname(__file__), '..') -INIT_PW_FILE = os.path.normpath(os.path.join(ROOT, 'data', 'api', '.admin_initial_password')) -TEST_PW_FILE = os.path.normpath(os.path.join(ROOT, 'data', 'api', '.test_admin_pass')) +ROOT = os.path.normpath(os.path.join(_here, '..')) +# data dir: host uses ROOT/data/api, container mounts data/api at ROOT/data +for _data in [os.path.join(ROOT, 'data', 'api'), os.path.join(ROOT, 'data'), '/app/data']: + if os.path.isdir(_data): + _DATA_DIR = _data + break +else: + _DATA_DIR = os.path.join(ROOT, 'data', 'api') + +INIT_PW_FILE = os.path.join(_DATA_DIR, '.admin_initial_password') +TEST_PW_FILE = os.path.join(_DATA_DIR, '.test_admin_pass') def _generate_password(length: int = 20) -> str: @@ -32,7 +45,7 @@ def _generate_password(length: int = 20) -> str: def _set_password(new_password: str) -> None: from auth_manager import AuthManager - data_dir = os.path.normpath(os.path.join(ROOT, 'data', 'api')) + data_dir = _DATA_DIR os.makedirs(data_dir, exist_ok=True) mgr = AuthManager(data_dir=data_dir, config_dir='/tmp') if mgr.set_password_admin('admin', new_password): diff --git a/webui/src/pages/CellNetwork.jsx b/webui/src/pages/CellNetwork.jsx index ca1b8cc..774eac0 100644 --- a/webui/src/pages/CellNetwork.jsx +++ b/webui/src/pages/CellNetwork.jsx @@ -23,7 +23,17 @@ const SERVICE_DEFS = [ function CopyButton({ text, small }) { const [copied, setCopied] = useState(false); const copy = () => { - navigator.clipboard.writeText(text); + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(text); + } else { + const el = document.createElement('textarea'); + el.value = text; + el.style.cssText = 'position:fixed;opacity:0;top:0;left:0'; + document.body.appendChild(el); + el.select(); + document.execCommand('copy'); + document.body.removeChild(el); + } setCopied(true); setTimeout(() => setCopied(false), 1500); };