feat: fully editable Settings page with service configs, backup/restore, export/import

- Rewrote Settings.jsx: Cell Identity editor, per-service config sections
  (network, wireguard, email, calendar, files, routing, vault) with
  collapsible cards, appropriate input types, and per-section Save buttons
- Added Backup & Restore panel with create/restore/delete actions
- Added Export (download JSON) and Import (upload JSON) panel
- Added PUT /api/config identity field persistence (_identity key in cell_config.json)
  so cell_name/domain/ip_range/wireguard_port survive restarts
- GET /api/config now returns service_configs separately and prefers stored identity
- Added DELETE /api/config/backups/<id> endpoint
- Extended cellAPI in api.js with createBackup, listBackups, restoreBackup,
  deleteBackup, exportConfig, importConfig

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 03:19:46 -04:00
parent 8e1814c7d2
commit c778ee8eb8
3 changed files with 626 additions and 107 deletions
+31 -9
View File
@@ -373,13 +373,14 @@ def get_config():
"""Get cell configuration."""
try:
service_configs = config_manager.get_all_configs()
identity = service_configs.pop('_identity', {})
config = {
'cell_name': os.environ.get('CELL_NAME', 'mycell'),
'domain': os.environ.get('CELL_DOMAIN', 'cell'),
'ip_range': os.environ.get('CELL_IP_RANGE', '172.20.0.0/16'),
'wireguard_port': int(os.environ.get('WG_PORT', '51820')),
'cell_name': identity.get('cell_name', os.environ.get('CELL_NAME', 'mycell')),
'domain': identity.get('domain', os.environ.get('CELL_DOMAIN', 'cell')),
'ip_range': identity.get('ip_range', os.environ.get('CELL_IP_RANGE', '172.20.0.0/16')),
'wireguard_port': identity.get('wireguard_port', int(os.environ.get('WG_PORT', '51820'))),
}
config.update(service_configs)
config['service_configs'] = service_configs
return jsonify(config)
except Exception as e:
logger.error(f"Error getting config: {e}")
@@ -392,18 +393,26 @@ def update_config():
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "No data provided"}), 400
# Update configuration using config manager
# Handle identity fields (cell_name, domain, ip_range, wireguard_port)
identity_keys = {'cell_name', 'domain', 'ip_range', 'wireguard_port'}
identity_updates = {k: v for k, v in data.items() if k in identity_keys}
if identity_updates:
stored = config_manager.configs.get('_identity', {})
stored.update(identity_updates)
config_manager.configs['_identity'] = stored
config_manager._save_all_configs()
# Update service configurations
for service, config in data.items():
if service in config_manager.service_schemas:
success = config_manager.update_service_config(service, config)
if success:
# Publish config change event
service_bus.publish_event(EventType.CONFIG_CHANGED, service, {
'service': service,
'config': config
})
logger.info(f"Updated config: {data}")
return jsonify({"message": "Configuration updated successfully"})
except Exception as e:
@@ -483,6 +492,19 @@ def import_config():
logger.error(f"Error importing config: {e}")
return jsonify({"error": str(e)}), 500
@app.route('/api/config/backups/<backup_id>', methods=['DELETE'])
def delete_config_backup(backup_id):
"""Delete a configuration backup."""
try:
success = config_manager.delete_backup(backup_id)
if success:
return jsonify({"message": f"Backup {backup_id} deleted"})
else:
return jsonify({"error": f"Failed to delete backup {backup_id}"}), 500
except Exception as e:
logger.error(f"Error deleting backup: {e}")
return jsonify({"error": str(e)}), 500
# Service bus endpoints
@app.route('/api/services/bus/status', methods=['GET'])
def get_service_bus_status():