import json import os import re import shutil import subprocess import time from datetime import datetime, timezone import requests from flask import Flask, jsonify, redirect, render_template_string, request app = Flask(__name__) CF_API_TOKEN = os.environ["CF_API_TOKEN"] CF_ZONE_ID = os.environ["CF_ZONE_ID"] SELF_NAME = os.environ.get("SELF_CONTAINER_NAME", "") API_TOKEN = os.environ["PANEL_API_TOKEN"] SERVER_IP = os.environ.get("SERVER_IP", "148.135.181.126") ROUTER_RULE_RE = re.compile(r"^traefik\.http\.routers\.[^.]+\.rule$") def check_auth(): auth = request.headers.get("Authorization", "") return auth == f"Bearer {API_TOKEN}" def extract_hostname(labels): """Scan ALL traefik router rule labels, not just one matching the container's own name — compose-based containers get auto-generated docker names that no longer match the Traefik router name.""" for key, value in labels.items(): if ROUTER_RULE_RE.match(key) and "`" in value: return value.split("`")[1] return None def human_relative(iso_ts): if not iso_ts or iso_ts.startswith("0001-01-01"): return None try: dt = datetime.fromisoformat(iso_ts.replace("Z", "+00:00")) except ValueError: return None secs = int((datetime.now(timezone.utc) - dt).total_seconds()) if secs < 60: return f"{secs} sn önce" mins = secs // 60 if mins < 60: return f"{mins} dk önce" hours = mins // 60 if hours < 24: return f"{hours} sa önce" days = hours // 24 return f"{days} gün önce" def _cpu_times(): with open("/proc/stat") as f: parts = f.readline().split()[1:] nums = [int(x) for x in parts] idle = nums[3] + nums[4] total = sum(nums) return idle, total def server_stats(): stats = {} # CPU: kisa bir ornekleme araligiyla kullanim yuzdesi idle1, total1 = _cpu_times() time.sleep(0.3) idle2, total2 = _cpu_times() d_idle = idle2 - idle1 d_total = total2 - total1 cpu_percent = round((1 - d_idle / d_total) * 100, 1) if d_total > 0 else 0.0 stats["cpu_percent"] = cpu_percent stats["cpu_cores"] = os.cpu_count() with open("/proc/loadavg") as f: stats["load"] = " / ".join(f.read().split()[:3]) meminfo = {} with open("/proc/meminfo") as f: for line in f: key, _, rest = line.partition(":") meminfo[key] = int(rest.strip().split()[0]) # kB mem_total = meminfo.get("MemTotal", 0) mem_available = meminfo.get("MemAvailable", 0) mem_used = mem_total - mem_available stats["mem_used_gb"] = round(mem_used / 1024 / 1024, 1) stats["mem_total_gb"] = round(mem_total / 1024 / 1024, 1) stats["mem_percent"] = round(mem_used / mem_total * 100, 1) if mem_total else 0.0 disk = shutil.disk_usage("/") stats["disk_used_gb"] = round(disk.used / 1024**3, 1) stats["disk_total_gb"] = round(disk.total / 1024**3, 1) stats["disk_percent"] = round(disk.used / disk.total * 100, 1) if disk.total else 0.0 with open("/proc/uptime") as f: up_secs = int(float(f.read().split()[0])) days, rem = divmod(up_secs, 86400) hours, _ = divmod(rem, 3600) stats["uptime"] = f"{days} gün {hours} saat" if days else f"{hours} saat" all_containers = subprocess.run( ["docker", "ps", "-a", "--format", "{{.State}}"], capture_output=True, text=True, ).stdout.split() stats["containers_running"] = sum(1 for s in all_containers if s == "running") stats["containers_total"] = len(all_containers) return stats def inspect_all(cid): return json.loads(subprocess.run( ["docker", "inspect", cid], capture_output=True, text=True, check=True ).stdout)[0] def list_projects(): out = subprocess.run( ["docker", "ps", "-a", "--filter", "label=traefik.enable=true", "--format", "{{.ID}}"], capture_output=True, text=True, check=True, ).stdout.split() projects = [] for cid in out: info = inspect_all(cid) name = info["Name"].lstrip("/") if name == SELF_NAME: continue labels = info["Config"]["Labels"] or {} state = info["State"] status = state["Status"] when = human_relative(state.get("StartedAt") if status == "running" else state.get("FinishedAt")) projects.append({ "name": name, "hostname": extract_hostname(labels), "status": status, "since": when, "image": info["Config"]["Image"], }) projects.sort(key=lambda p: p["name"]) return projects def find_container_hostname(name): result = subprocess.run(["docker", "inspect", name], capture_output=True, text=True) if result.returncode != 0: return None info = json.loads(result.stdout)[0] labels = info["Config"]["Labels"] or {} return extract_hostname(labels) def ensure_dns(hostname): existing = requests.get( f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records", params={"type": "A", "name": hostname}, headers={"Authorization": f"Bearer {CF_API_TOKEN}"}, timeout=10, ).json() if existing.get("result"): return {"created": False} requests.post( f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records", headers={"Authorization": f"Bearer {CF_API_TOKEN}", "Content-Type": "application/json"}, json={"type": "A", "name": hostname, "content": SERVER_IP, "ttl": 1, "proxied": True}, timeout=10, ) return {"created": True} def delete_dns_record(hostname): if not hostname: return r = requests.get( f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records", params={"name": hostname}, headers={"Authorization": f"Bearer {CF_API_TOKEN}"}, timeout=10, ).json() for rec in r.get("result", []): requests.delete( f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records/{rec['id']}", headers={"Authorization": f"Bearer {CF_API_TOKEN}"}, timeout=10, ) TEMPLATE = """ Deploy Panel

Deploy Panel

{{ projects|length }} proje · 20 saniyede bir otomatik yenilenir · şimdi yenile
CPU ({{ s.cpu_cores }} çekirdek)
%{{ s.cpu_percent }}
yük: {{ s.load }}
Bellek
%{{ s.mem_percent }}
{{ s.mem_used_gb }} / {{ s.mem_total_gb }} GB
Disk
%{{ s.disk_percent }}
{{ s.disk_used_gb }} / {{ s.disk_total_gb }} GB
Sunucu
{{ s.uptime }}
çalışma süresi
Container
{{ s.containers_running }} / {{ s.containers_total }}
çalışan / toplam
{% if projects %} {% for p in projects %} {% endfor %}
ProjeAdresDurumİmaj
{{ p.name }} {% if p.hostname %}{{ p.hostname }}{% else %}—{% endif %} {{ p.status }} {% if p.since %}{{ p.since }}{% endif %} {{ p.image }}
Loglar
{% else %}

Henüz deploy edilmiş bir proje yok.

{% endif %} """ LOGS_TEMPLATE = """ {{ name }} - Loglar ← panele dön

{{ name }} — son {{ lines }} satır

{{ logs }}
""" @app.route("/") def index(): return render_template_string(TEMPLATE, projects=list_projects(), s=server_stats()) @app.route("/logs/") def logs_ui(name): result = subprocess.run( ["docker", "logs", "--tail", "150", name], capture_output=True, text=True, ) output = (result.stdout or "") + (result.stderr or "") return render_template_string(LOGS_TEMPLATE, name=name, lines=150, logs=output or "(log yok)") @app.route("/restart/", methods=["POST"]) def restart_ui(name): subprocess.run(["docker", "restart", name]) return redirect("/") @app.route("/delete/", methods=["POST"]) def delete_ui(name): projects = {p["name"]: p for p in list_projects()} target = projects.get(name) if target: subprocess.run(["docker", "rm", "-f", name]) delete_dns_record(target["hostname"]) return redirect("/") @app.route("/api/ensure-dns", methods=["POST"]) def api_ensure_dns(): if not check_auth(): return jsonify({"error": "unauthorized"}), 401 body = request.get_json(force=True) hostname = body.get("hostname") if not hostname: return jsonify({"error": "hostname required"}), 400 result = ensure_dns(hostname) return jsonify(result), 200 @app.route("/api/deploy", methods=["POST"]) def api_deploy(): if not check_auth(): return jsonify({"error": "unauthorized"}), 401 body = request.get_json(force=True) name = body.get("name") hostname = body.get("hostname") port = str(body.get("port", "80")) if not name or not hostname: return jsonify({"error": "name and hostname required"}), 400 current_hostname = find_container_hostname(name) if current_hostname is not None and current_hostname != hostname: return jsonify({ "error": "name_conflict", "message": f"'{name}' is already deployed with a different hostname ({current_hostname}).", }), 409 ensure_dns(hostname) subprocess.run(["docker", "rm", "-f", name]) rule = f"Host(`{hostname}`)" subprocess.run([ "docker", "run", "-d", "--name", name, "--network", "proxy", "--restart", "always", "-l", "traefik.enable=true", "-l", f"traefik.http.routers.{name}.rule={rule}", "-l", f"traefik.http.routers.{name}.entrypoints=websecure", "-l", f"traefik.http.routers.{name}.tls.certresolver=letsencrypt", "-l", f"traefik.http.services.{name}.loadbalancer.server.port={port}", f"{name}:latest", ], check=True) return jsonify({"status": "deployed", "url": f"https://{hostname}"}), 200 @app.route("/api/deploy/", methods=["DELETE"]) def api_delete(name): if not check_auth(): return jsonify({"error": "unauthorized"}), 401 hostname = find_container_hostname(name) subprocess.run(["docker", "rm", "-f", name]) delete_dns_record(hostname) return jsonify({"status": "deleted"}), 200 if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)