feat: pending-restart banner + Apply button for config changes
When ip_range changes, a persistent amber banner appears at the top of
every page showing what changed and a "Apply Now" button. Clicking it
shows a confirmation modal ("containers will restart briefly"), then
calls POST /api/config/apply which runs docker compose up -d from inside
the API container — no manual make start needed.
Backend:
- _set_pending_restart() / _clear_pending_restart() helpers track state
in config_manager so it survives page refresh
- GET /api/config/pending returns { needs_restart, changed_at, changes }
- POST /api/config/apply runs docker compose up -d via the mounted
docker.sock, using the project working_dir label to resolve host paths
- docker-compose.yml mounts docker-compose.yml itself read-only into
the API container so docker compose can read it from inside
Frontend (App.jsx):
- Polls /api/config/pending every 5 s alongside the health check
- PendingRestartBanner component with confirmation modal
- Optimistically clears banner on Apply click; API and containers
restart in the background
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+85
-6
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user