from flask import Flask, jsonify, request, redirect, url_for, render_template, session from flask_cors import CORS import docker import os import json from functools import wraps from werkzeug.security import generate_password_hash, check_password_hash import psutil import logging # Helper: load password hash from auth file or env PROJECT_ROOT = os.path.dirname(__file__) AUTH_FILE = os.path.join(PROJECT_ROOT, 'auth.json') SECRET_FILE = os.path.join(PROJECT_ROOT, '.secret_key') # Setup logging (console + file) — placed after PROJECT_ROOT is known LOGFILE = os.path.join(PROJECT_ROOT, 'webui.log') logger = logging.getLogger('docker-web-ui') if not logger.handlers: logger.setLevel(logging.INFO) fh = logging.FileHandler(LOGFILE) fh.setLevel(logging.INFO) ch = logging.StreamHandler() ch.setLevel(logging.INFO) fmt = logging.Formatter('%(asctime)s %(levelname)s %(message)s') fh.setFormatter(fmt) ch.setFormatter(fmt) logger.addHandler(fh) logger.addHandler(ch) def load_password_hash(): # priority: auth.json file, then env ADMIN_PASSWORD_HASH, then env ADMIN_PASSWORD if os.path.exists(AUTH_FILE): try: with open(AUTH_FILE, 'r') as f: data = json.load(f) return data.get('password_hash') except Exception: return None ph = os.environ.get('ADMIN_PASSWORD_HASH') if ph: return ph pwd = os.environ.get('ADMIN_PASSWORD') if pwd: return generate_password_hash(pwd) return None def get_secret_key(): # persistent secret key for sessions if os.path.exists(SECRET_FILE): with open(SECRET_FILE, 'rb') as f: return f.read() # generate and persist v = os.urandom(24) try: with open(SECRET_FILE, 'wb') as f: f.write(v) except Exception: pass return v app = Flask(__name__, static_folder='static', static_url_path='/') CORS(app) # set secret key app.secret_key = os.environ.get('FLASK_SECRET_KEY') or get_secret_key() # load password hash PASSWORD_HASH = load_password_hash() # NOTE: dynamic logo lookup and external API usage removed. # The frontend will use local static files in `static/game_logos/.png` and # fall back to `/default_game.svg` when a local image is not present. def login_required(f): @wraps(f) def wrapper(*args, **kwargs): if not session.get('logged_in'): return redirect(url_for('login', next=request.path)) return f(*args, **kwargs) return wrapper try: client = docker.from_env() except Exception: client = None @app.route('/') @login_required def index(): return app.send_static_file('index.html') @app.route('/login', methods=['GET', 'POST']) def login(): global PASSWORD_HASH if request.method == 'POST': pwd = request.form.get('password', '') if PASSWORD_HASH and check_password_hash(PASSWORD_HASH, pwd): session['logged_in'] = True nxt = request.args.get('next') or url_for('index') return redirect(nxt) else: return render_template('login.html', error='Mot de passe invalide') return render_template('login.html', error=None) @app.route('/logout') def logout(): session.pop('logged_in', None) return redirect(url_for('login')) @app.route('/api/containers', methods=['GET']) @login_required def list_containers(): if client is None: return jsonify({'error': 'Docker client not available'}), 500 containers = client.containers.list(all=True) res = [] for c in containers: tag = None try: tag = c.image.tags[0] if c.image.tags else None except Exception: tag = None res.append({ 'id': c.id, 'short_id': c.id[:12], 'name': c.name, 'status': c.status, 'image': tag or str(c.image), }) return jsonify(res) @app.route('/api/containers//', methods=['POST']) @login_required def container_action(id_or_name, action): if client is None: return jsonify({'error': 'Docker client not available'}), 500 try: container = client.containers.get(id_or_name) except Exception as e: return jsonify({'error': f'Container not found: {e}'}), 404 try: if action == 'start': container.start() elif action == 'stop': container.stop() elif action == 'restart': container.restart() else: return jsonify({'error': 'Invalid action'}), 400 except Exception as e: return jsonify({'error': str(e)}), 500 return jsonify({'result': 'ok', 'id': container.id, 'status': container.status}) @app.route('/api/containers//logs', methods=['GET']) @login_required def container_logs(id_or_name): if client is None: return jsonify({'error': 'Docker client not available'}), 500 tail = request.args.get('tail') or request.args.get('lines') or '200' try: c = client.containers.get(id_or_name) # get last `tail` lines (docker SDK accepts tail as int) logs = c.logs(tail=int(tail), stdout=True, stderr=True) if isinstance(logs, bytes): logs = logs.decode('utf-8', errors='replace') return jsonify({'logs': logs}) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/containers//exec', methods=['POST']) @login_required def container_exec(id_or_name): if client is None: return jsonify({'error': 'Docker client not available'}), 500 data = request.get_json() or {} cmd = data.get('cmd') or data.get('command') if not cmd: return jsonify({'error': 'No command provided'}), 400 try: c = client.containers.get(id_or_name) # exec_run may return an ExecResult or (exit_code, output) res = c.exec_run(cmd, stdout=True, stderr=True) exit_code = None output = '' try: exit_code = getattr(res, 'exit_code', None) output = getattr(res, 'output', None) except Exception: pass if exit_code is None: # try tuple form try: exit_code, output = res except Exception: exit_code = -1 if isinstance(output, bytes): output = output.decode('utf-8', errors='replace') return jsonify({'exit_code': exit_code, 'output': output}) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/api/stats', methods=['GET']) @login_required def system_stats(): # return CPU%, RAM%, and temperatures (if available) try: cpu = psutil.cpu_percent(interval=0.1) vm = psutil.virtual_memory() mem = vm.percent temps = {} try: t = psutil.sensors_temperatures() for k, v in (t or {}).items(): # take first readable entry if v: temps[k] = [] for si in v: temps[k].append({'label': si.label or k, 'current': getattr(si, 'current', None)}) except Exception: temps = {} return jsonify({'cpu_percent': cpu, 'mem_percent': mem, 'temperatures': temps}) except Exception as e: return jsonify({'error': str(e)}), 500 if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)