- Unable to connect to the Personal Internet Cell backend. + Unable to connect to the Personal Internet Cell backend. Please ensure the API server is running on port 3000.
diff --git a/api/app.py b/api/app.py index b87527f..25a0a11 100644 --- a/api/app.py +++ b/api/app.py @@ -493,12 +493,9 @@ def update_config(): firewall_manager.ensure_caddy_virtual_ips() # Write new .env so docker-compose picks up new container IPs on next start env_file = os.environ.get('COMPOSE_ENV_FILE', '/app/.env.compose') - if ip_utils.write_env_file(new_range, env_file): - all_warnings.append( - 'Container IPs updated — run `make start` to apply to running containers') - else: - all_warnings.append( - 'Could not write .env — run `make setup && make start` to apply container IP changes') + ip_utils.write_env_file(new_range, env_file) + # Mark containers as needing restart + _set_pending_restart([f'ip_range changed to {new_range} — container IPs updated']) logger.info(f"Updated config, restarted: {all_restarted}") return jsonify({ @@ -510,6 +507,88 @@ def update_config(): logger.error(f"Error updating config: {e}") return jsonify({"error": str(e)}), 500 + +# --------------------------------------------------------------------------- +# Pending-restart helpers +# --------------------------------------------------------------------------- + +def _set_pending_restart(changes: list): + """Record that containers need to be restarted to apply configuration.""" + from datetime import datetime as _dt + config_manager.configs['_pending_restart'] = { + 'needs_restart': True, + 'changed_at': _dt.utcnow().isoformat(), + 'changes': changes, + } + config_manager._save_all_configs() + + +def _clear_pending_restart(): + config_manager.configs['_pending_restart'] = {'needs_restart': False, 'changes': []} + config_manager._save_all_configs() + + +@app.route('/api/config/pending', methods=['GET']) +def get_pending_config(): + """Return whether there are unapplied configuration changes that require a restart.""" + pending = config_manager.configs.get('_pending_restart', {}) + return jsonify({ + 'needs_restart': pending.get('needs_restart', False), + 'changed_at': pending.get('changed_at'), + 'changes': pending.get('changes', []), + }) + + +@app.route('/api/config/apply', methods=['POST']) +def apply_pending_config(): + """Apply pending configuration by restarting containers via docker compose up -d.""" + try: + pending = config_manager.configs.get('_pending_restart', {}) + if not pending.get('needs_restart'): + return jsonify({'message': 'No pending changes to apply'}) + + # Get project working dir from our own container labels (set by docker-compose) + project_dir = '/home/roof/pic' + try: + import docker as _docker_sdk + _client = _docker_sdk.from_env() + _self = _client.containers.get('cell-api') + project_dir = _self.labels.get('com.docker.compose.project.working_dir', project_dir) + except Exception: + pass + + # Clear pending flag before we restart so it shows cleared after the new container starts + _clear_pending_restart() + + # Run docker compose up -d in a background thread; the 0.3s delay lets Flask + # finish sending this response before cell-api itself gets recreated. + def _do_apply(): + import time as _time + _time.sleep(0.3) + result = subprocess.run( + ['docker', 'compose', + '--project-directory', project_dir, + '-f', '/app/docker-compose.yml', + '--env-file', '/app/.env.compose', + 'up', '-d'], + capture_output=True, text=True, timeout=120 + ) + if result.returncode != 0: + logger.error(f"docker compose up failed: {result.stderr.strip()}") + else: + logger.info('docker compose up -d completed successfully') + + threading.Thread(target=_do_apply, daemon=False).start() + + return jsonify({ + 'message': 'Applying configuration — containers are restarting', + 'restart_in_progress': True, + }) + except Exception as e: + logger.error(f"Error applying config: {e}") + return jsonify({'error': str(e)}), 500 + + # Configuration management endpoints @app.route('/api/config/backup', methods=['POST']) def create_config_backup(): diff --git a/docker-compose.yml b/docker-compose.yml index 72c4c8a..162a778 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -202,6 +202,7 @@ services: - ./data/logs:/app/api/data/logs - /var/run/docker.sock:/var/run/docker.sock - ./.env:/app/.env.compose + - ./docker-compose.yml:/app/docker-compose.yml:ro pid: host restart: unless-stopped networks: diff --git a/webui/src/App.jsx b/webui/src/App.jsx index 5d34688..b57c40d 100644 --- a/webui/src/App.jsx +++ b/webui/src/App.jsx @@ -1,22 +1,24 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; -import { useState, useEffect } from 'react'; -import { - Home, - Users, - Network, - Shield, - Mail, - Calendar as CalendarIcon, - FolderOpen, +import { useState, useEffect, useCallback } from 'react'; +import { + Home, + Users, + Network, + Shield, + Mail, + Calendar as CalendarIcon, + FolderOpen, Activity, Wifi, Server, Key, Package2, Settings as SettingsIcon, - Link2 + Link2, + RefreshCw, + AlertTriangle, } from 'lucide-react'; -import { healthAPI } from './services/api'; +import { healthAPI, cellAPI } from './services/api'; import { ConfigProvider } from './contexts/ConfigContext'; import Sidebar from './components/Sidebar'; import Dashboard from './pages/Dashboard'; @@ -33,27 +35,120 @@ import Vault from './pages/Vault'; import ContainerDashboard from './components/ContainerDashboard'; import CellNetwork from './pages/CellNetwork'; +function PendingRestartBanner({ pending, onApply }) { + const [confirming, setConfirming] = useState(false); + const [applying, setApplying] = useState(false); + + const handleApply = async () => { + setApplying(true); + setConfirming(false); + try { + await onApply(); + } finally { + setApplying(false); + } + }; + + return ( + <> +
+ Configuration changes pending — containers need restart +
+ {pending.changes?.length > 0 && ( ++ All containers will be restarted to apply the new configuration. + The UI will be briefly unavailable during the restart. +
+- Unable to connect to the Personal Internet Cell backend. + Unable to connect to the Personal Internet Cell backend. Please ensure the API server is running on port 3000.