Files
deploy-panel/app.py
T
2026-09-23 16:49:59 +03:00

860 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
TRAEFIK_API_URL = os.environ.get("TRAEFIK_API_URL", "http://traefik:8080")
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 = {}
idle1, total1 = _cpu_times()
time.sleep(0.3)
idle2, total2 = _cpu_times()
d_idle = idle2 - idle1
d_total = total2 - total1
stats["cpu_percent"] = round((1 - d_idle / d_total) * 100, 1) if d_total > 0 else 0.0
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])
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_states = subprocess.run(
["docker", "ps", "-a", "--format", "{{.State}}"],
capture_output=True, text=True,
).stdout.split()
stats["containers_running"] = sum(1 for s in all_states if s == "running")
stats["containers_total"] = len(all_states)
return stats
def inspect_all(cid):
return json.loads(subprocess.run(
["docker", "inspect", cid], capture_output=True, text=True, check=True
).stdout)[0]
def port_summary(info):
ports = info.get("NetworkSettings", {}).get("Ports") or {}
out = []
for container_port, bindings in ports.items():
if not bindings:
continue
for b in bindings:
out.append(f"{b.get('HostPort')}{container_port}")
return ", ".join(out) if out else None
def list_all_containers():
out = subprocess.run(
["docker", "ps", "-a", "--format", "{{.ID}}"],
capture_output=True, text=True, check=True,
).stdout.split()
containers = []
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"))
containers.append({
"name": name,
"hostname": extract_hostname(labels),
"status": status,
"since": when,
"image": info["Config"]["Image"],
"ports": port_summary(info),
})
containers.sort(key=lambda c: c["name"])
return containers
def list_images():
out = subprocess.run(
["docker", "images", "--format", "{{json .}}"],
capture_output=True, text=True, check=True,
).stdout.strip().splitlines()
images = []
for line in out:
if not line:
continue
d = json.loads(line)
repo, tag = d.get("Repository", ""), d.get("Tag", "")
if repo == "<none>" and tag == "<none>":
label = d.get("ID", "")[:19]
else:
label = f"{repo}:{tag}"
images.append({
"id": d.get("ID"),
"label": label,
"size": d.get("Size"),
"created": d.get("CreatedSince"),
})
images.sort(key=lambda i: i["label"])
return images
def list_volumes():
out = subprocess.run(
["docker", "volume", "ls", "--format", "{{json .}}"],
capture_output=True, text=True, check=True,
).stdout.strip().splitlines()
volumes = []
for line in out:
if not line:
continue
d = json.loads(line)
volumes.append({"name": d.get("Name"), "driver": d.get("Driver")})
volumes.sort(key=lambda v: v["name"])
return volumes
def docker_system_df():
out = subprocess.run(["docker", "system", "df"], capture_output=True, text=True).stdout
lines = out.strip().splitlines()
rows = []
for line in lines[1:]:
parts = re.split(r"\s{2,}", line.strip())
if len(parts) >= 4:
rows.append({
"type": parts[0], "total": parts[1], "active": parts[2],
"size": parts[3], "reclaimable": parts[4] if len(parts) > 4 else "",
})
return rows
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,
)
def list_dns_records():
records = []
page = 1
while True:
r = requests.get(
f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records",
params={"page": page, "per_page": 100},
headers={"Authorization": f"Bearer {CF_API_TOKEN}"},
timeout=10,
).json()
records.extend(r.get("result", []))
info = r.get("result_info", {})
if page >= info.get("total_pages", 1):
break
page += 1
records.sort(key=lambda x: (x.get("type", ""), x.get("name", "")))
return records
def create_dns_record(rtype, name, content, proxied, ttl=1):
return 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": rtype, "name": name, "content": content, "ttl": int(ttl), "proxied": proxied},
timeout=10,
).json()
def update_dns_record(record_id, content, proxied):
return requests.patch(
f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records/{record_id}",
headers={"Authorization": f"Bearer {CF_API_TOKEN}", "Content-Type": "application/json"},
json={"content": content, "proxied": proxied},
timeout=10,
).json()
def delete_dns_record_by_id(record_id):
requests.delete(
f"https://api.cloudflare.com/client/v4/zones/{CF_ZONE_ID}/dns_records/{record_id}",
headers={"Authorization": f"Bearer {CF_API_TOKEN}"},
timeout=10,
)
def list_traefik_routers():
try:
routers = requests.get(f"{TRAEFIK_API_URL}/api/http/routers", timeout=3).json()
services = requests.get(f"{TRAEFIK_API_URL}/api/http/services", timeout=3).json()
except requests.RequestException:
return None
services_by_name = {s["name"].split("@")[0]: s for s in services}
result = []
for r in routers:
if r.get("provider") != "docker":
continue
rule = r.get("rule", "")
hostname = rule.split("`")[1] if "`" in rule else rule
service = services_by_name.get(r.get("service"), {})
servers = service.get("loadBalancer", {}).get("servers", [])
backend = servers[0].get("url") if servers else None
result.append({
"name": r.get("name", "").split("@")[0],
"hostname": hostname,
"entrypoints": ", ".join(r.get("entryPoints", [])),
"tls": bool(r.get("tls")),
"status": r.get("status"),
"backend": backend,
})
result.sort(key=lambda x: x["name"])
return result
BASE_STYLE = """
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2.5rem; }
h1 { color: #38bdf8; margin-bottom: 0.2rem; }
h2 { color: #38bdf8; font-size: 1.1rem; margin: 2rem 0 0.8rem; }
.sub { opacity: 0.55; font-size: 0.85rem; margin-bottom: 1rem; }
table { width: 100%; border-collapse: collapse; }
td, th { padding: 0.7rem; border-bottom: 1px solid #334155; text-align: left; vertical-align: middle; }
th { opacity: 0.7; font-weight: 600; font-size: 0.85rem; text-transform: uppercase; letter-spacing: .03em; }
a { color: #38bdf8; text-decoration: none; }
a:hover { text-decoration: underline; }
.badge { padding: 0.25rem 0.65rem; border-radius: 999px; font-size: 0.78rem; font-weight: 600; }
.running { background: #065f46; color: #d1fae5; }
.exited { background: #7f1d1d; color: #fecaca; }
.since { opacity: 0.55; font-size: 0.8rem; display: block; }
.actions { display: flex; gap: 0.4rem; flex-wrap: wrap; }
button, .btn { border: none; padding: 0.4rem 0.8rem; border-radius: 6px; cursor: pointer;
font-size: 0.85rem; color: white; }
.btn-restart, .btn-start { background: #1e3a8a; }
.btn-restart:hover, .btn-start:hover { background: #1e40af; }
.btn-stop { background: #78350f; }
.btn-stop:hover { background: #92400e; }
.btn-logs { background: #334155; }
.btn-logs:hover { background: #475569; }
.btn-delete { background: #7f1d1d; }
.btn-delete:hover { background: #991b1b; }
.btn-prune { background: #4c1d95; }
.btn-prune:hover { background: #5b21b6; }
.empty { opacity: 0.6; margin-top: 1.5rem; }
form { display: inline; }
code { background: #1e293b; padding: 0.1rem 0.4rem; border-radius: 4px; font-size: 0.85rem; }
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 1rem; margin-bottom: 2rem; }
.stat-card { background: #1e293b; border-radius: 10px; padding: 1rem 1.2rem; }
.stat-label { font-size: 0.75rem; opacity: 0.6; text-transform: uppercase; letter-spacing: .03em; }
.stat-value { font-size: 1.5rem; font-weight: 700; margin: 0.15rem 0 0.5rem; }
.stat-sub { font-size: 0.78rem; opacity: 0.55; }
.bar { height: 6px; border-radius: 999px; background: #334155; overflow: hidden; margin-top: 0.5rem; }
.bar-fill { height: 100%; background: #38bdf8; }
.bar-fill.warn { background: #f59e0b; }
.bar-fill.crit { background: #ef4444; }
nav { margin-bottom: 1.5rem; display: flex; gap: 1.2rem; border-bottom: 1px solid #334155;
padding-bottom: 0.8rem; }
nav a { font-size: 0.9rem; opacity: 0.65; }
nav a.active { opacity: 1; color: #38bdf8; font-weight: 600; }
"""
NAV = """
<nav>
<a href="/" class="{{ 'active' if page == 'containers' else '' }}">Container'lar</a>
<a href="/traefik" class="{{ 'active' if page == 'traefik' else '' }}">Traefik</a>
<a href="/dns" class="{{ 'active' if page == 'dns' else '' }}">DNS</a>
<a href="/system" class="{{ 'active' if page == 'system' else '' }}">Sistem &amp; Disk</a>
</nav>
"""
CONTAINERS_TEMPLATE = """
<!doctype html>
<html>
<head>
<title>Deploy Panel</title>
<meta charset="utf-8">
<meta http-equiv="refresh" content="20">
<style>""" + BASE_STYLE + """</style>
</head>
<body>
<h1>Deploy Panel</h1>
""" + NAV + """
<div class="sub">{{ containers|length }} container · 20 saniyede bir otomatik yenilenir · <a href="/">şimdi yenile</a></div>
<div class="stats">
<div class="stat-card">
<div class="stat-label">CPU ({{ s.cpu_cores }} çekirdek)</div>
<div class="stat-value">%{{ s.cpu_percent }}</div>
<div class="bar"><div class="bar-fill {{ 'crit' if s.cpu_percent > 85 else ('warn' if s.cpu_percent > 60 else '') }}" style="width: {{ s.cpu_percent }}%"></div></div>
<div class="stat-sub">yük: {{ s.load }}</div>
</div>
<div class="stat-card">
<div class="stat-label">Bellek</div>
<div class="stat-value">%{{ s.mem_percent }}</div>
<div class="bar"><div class="bar-fill {{ 'crit' if s.mem_percent > 85 else ('warn' if s.mem_percent > 60 else '') }}" style="width: {{ s.mem_percent }}%"></div></div>
<div class="stat-sub">{{ s.mem_used_gb }} / {{ s.mem_total_gb }} GB</div>
</div>
<div class="stat-card">
<div class="stat-label">Disk</div>
<div class="stat-value">%{{ s.disk_percent }}</div>
<div class="bar"><div class="bar-fill {{ 'crit' if s.disk_percent > 85 else ('warn' if s.disk_percent > 60 else '') }}" style="width: {{ s.disk_percent }}%"></div></div>
<div class="stat-sub">{{ s.disk_used_gb }} / {{ s.disk_total_gb }} GB</div>
</div>
<div class="stat-card">
<div class="stat-label">Sunucu</div>
<div class="stat-value" style="font-size:1.1rem;">{{ s.uptime }}</div>
<div class="stat-sub">çalışma süresi</div>
</div>
<div class="stat-card">
<div class="stat-label">Container</div>
<div class="stat-value" style="font-size:1.1rem;">{{ s.containers_running }} / {{ s.containers_total }}</div>
<div class="stat-sub">çalışan / toplam</div>
</div>
</div>
{% if containers %}
<table>
<tr><th>İsim</th><th>Adres</th><th>Portlar</th><th>Durum</th><th>İmaj</th><th></th></tr>
{% for c in containers %}
<tr>
<td>{{ c.name }}</td>
<td>{% if c.hostname %}<a href="https://{{ c.hostname }}" target="_blank">{{ c.hostname }}</a>{% else %}—{% endif %}</td>
<td>{{ c.ports or '—' }}</td>
<td>
<span class="badge {{ c.status }}">{{ c.status }}</span>
{% if c.since %}<span class="since">{{ c.since }}</span>{% endif %}
</td>
<td>{{ c.image }}</td>
<td>
<div class="actions">
<a class="btn btn-logs" href="/logs/{{ c.name }}">Loglar</a>
{% if c.status == 'running' %}
<form method="post" action="/stop/{{ c.name }}">
<button class="btn-stop" type="submit">Durdur</button>
</form>
<form method="post" action="/restart/{{ c.name }}">
<button class="btn-restart" type="submit">Yeniden Başlat</button>
</form>
{% else %}
<form method="post" action="/start/{{ c.name }}">
<button class="btn-start" type="submit">Başlat</button>
</form>
{% endif %}
<form method="post" action="/delete/{{ c.name }}"
onsubmit="return confirm('{{ c.name }} silinsin mi?{{ ' DNS kaydı da kaldırılacak.' if c.hostname else '' }}');">
<button class="btn-delete" type="submit">Sil</button>
</form>
</div>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="empty">Hiç container yok.</p>
{% endif %}
</body>
</html>
"""
SYSTEM_TEMPLATE = """
<!doctype html>
<html>
<head>
<title>Sistem &amp; Disk - Deploy Panel</title>
<meta charset="utf-8">
<style>""" + BASE_STYLE + """</style>
</head>
<body>
<h1>Deploy Panel</h1>
""" + NAV + """
<div class="sub">Docker'ın diskte kapladığı alan ve imaj/volume yönetimi</div>
<table>
<tr><th>Tür</th><th>Toplam</th><th>Kullanımda</th><th>Boyut</th><th>Geri kazanılabilir</th></tr>
{% for row in df %}
<tr>
<td>{{ row.type }}</td>
<td>{{ row.total }}</td>
<td>{{ row.active }}</td>
<td>{{ row.size }}</td>
<td>{{ row.reclaimable }}</td>
</tr>
{% endfor %}
</table>
<h2>İmajlar ({{ images|length }})</h2>
<form method="post" action="/images/prune" onsubmit="return confirm('Hiçbir container tarafından kullanılmayan TÜM imajlar silinecek. Emin misin?');">
<button class="btn-prune" type="submit">Kullanılmayan imajları temizle</button>
</form>
{% if images %}
<table>
<tr><th>İmaj</th><th>Boyut</th><th>Oluşturulma</th><th></th></tr>
{% for img in images %}
<tr>
<td><code>{{ img.label }}</code></td>
<td>{{ img.size }}</td>
<td>{{ img.created }}</td>
<td>
<form method="post" action="/images/delete/{{ img.id }}"
onsubmit="return confirm('{{ img.label }} silinsin mi? Kullanan bir container varsa silme başarısız olur.');">
<button class="btn-delete" type="submit">Sil</button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="empty">Hiç imaj yok.</p>
{% endif %}
<h2>Volume'ler ({{ volumes|length }})</h2>
<form method="post" action="/volumes/prune" onsubmit="return confirm('Hiçbir container tarafından kullanılmayan TÜM volume\\'ler silinecek. Emin misin?');">
<button class="btn-prune" type="submit">Kullanılmayan volume'leri temizle</button>
</form>
{% if volumes %}
<table>
<tr><th>İsim</th><th>Driver</th><th></th></tr>
{% for v in volumes %}
<tr>
<td><code>{{ v.name }}</code></td>
<td>{{ v.driver }}</td>
<td>
<form method="post" action="/volumes/delete/{{ v.name }}"
onsubmit="return confirm('{{ v.name }} silinsin mi? Kullanan bir container varsa silme başarısız olur.');">
<button class="btn-delete" type="submit">Sil</button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="empty">Hiç volume yok.</p>
{% endif %}
</body>
</html>
"""
TRAEFIK_TEMPLATE = """
<!doctype html>
<html>
<head>
<title>Traefik - Deploy Panel</title>
<meta charset="utf-8">
<style>""" + BASE_STYLE + """</style>
</head>
<body>
<h1>Deploy Panel</h1>
""" + NAV + """
<div class="sub">Traefik'in kendi API'sinden canlı okunan router/servis listesi</div>
{% if routers is none %}
<p class="empty">Traefik API'sine ulaşılamadı ({{ api_url }}).</p>
{% elif routers %}
<table>
<tr><th>Router</th><th>Adres</th><th>Entrypoint</th><th>TLS</th><th>Durum</th><th>Backend</th></tr>
{% for r in routers %}
<tr>
<td>{{ r.name }}</td>
<td>{% if r.hostname %}<a href="https://{{ r.hostname }}" target="_blank">{{ r.hostname }}</a>{% else %}{{ r.hostname }}{% endif %}</td>
<td>{{ r.entrypoints }}</td>
<td>{{ '✅' if r.tls else '—' }}</td>
<td><span class="badge {{ 'running' if r.status == 'enabled' else 'exited' }}">{{ r.status }}</span></td>
<td><code>{{ r.backend or '—' }}</code></td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="empty">Aktif router yok.</p>
{% endif %}
</body>
</html>
"""
DNS_TEMPLATE = """
<!doctype html>
<html>
<head>
<title>DNS - Deploy Panel</title>
<meta charset="utf-8">
<style>""" + BASE_STYLE + """</style>
<style>
.dns-form { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; margin-bottom: 1.5rem;
background: #1e293b; padding: 1rem; border-radius: 10px; }
.dns-form input, .dns-form select {
background: #0f172a; border: 1px solid #334155; color: #e2e8f0; padding: 0.45rem 0.6rem;
border-radius: 6px; font-size: 0.85rem;
}
.dns-form label { font-size: 0.8rem; opacity: 0.7; display: flex; align-items: center; gap: 0.3rem; }
.btn-add { background: #065f46; }
.btn-add:hover { background: #047857; }
.edit-row input[type=text] { width: 100%; }
</style>
</head>
<body>
<h1>Deploy Panel</h1>
""" + NAV + """
<div class="sub">akalan.dev · Cloudflare DNS kayıtları · {{ records|length }} kayıt</div>
<form class="dns-form" method="post" action="/dns/add">
<select name="type">
<option value="A">A</option>
<option value="CNAME">CNAME</option>
<option value="TXT">TXT</option>
<option value="MX">MX</option>
</select>
<input type="text" name="name" placeholder="ad (örn. blog veya blog.akalan.dev)" required>
<input type="text" name="content" placeholder="değer (örn. 148.135.181.126)" required>
<label><input type="checkbox" name="proxied" checked> turuncu bulut</label>
<button class="btn-add" type="submit">Ekle</button>
</form>
{% if records %}
<table>
<tr><th>Tip</th><th>Ad</th><th>Değer</th><th>Turuncu Bulut</th><th></th></tr>
{% for r in records %}
<tr class="edit-row">
<td>{{ r.type }}</td>
<td>{{ r.name }}</td>
<td>
<form method="post" action="/dns/update/{{ r.id }}" style="display:flex; gap:0.4rem; align-items:center;">
<input type="text" name="content" value="{{ r.content }}">
<label><input type="checkbox" name="proxied" {{ 'checked' if r.proxied else '' }}> proxy</label>
<button class="btn-restart" type="submit">Güncelle</button>
</form>
</td>
<td>{{ '🟠 açık' if r.proxied else '⚪ kapalı' }}</td>
<td>
<form method="post" action="/dns/delete/{{ r.id }}"
onsubmit="return confirm('{{ r.name }} ({{ r.type }}) kaydı silinsin mi?');">
<button class="btn-delete" type="submit">Sil</button>
</form>
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p class="empty">Hiç DNS kaydı yok.</p>
{% endif %}
</body>
</html>
"""
LOGS_TEMPLATE = """
<!doctype html>
<html>
<head>
<title>{{ name }} - Loglar</title>
<meta charset="utf-8">
<style>
body { font-family: ui-monospace, monospace; background: #0f172a; color: #e2e8f0; padding: 2rem; }
a { color: #38bdf8; }
h1 { font-family: system-ui, sans-serif; color: #38bdf8; font-size: 1.2rem; }
pre { background: #1e293b; padding: 1rem; border-radius: 8px; overflow-x: auto;
white-space: pre-wrap; word-break: break-word; font-size: 0.85rem; line-height: 1.4; }
</style>
</head>
<body>
<a href="/">&larr; panele dön</a>
<h1>{{ name }} — son {{ lines }} satır</h1>
<pre>{{ logs }}</pre>
</body>
</html>
"""
@app.route("/")
def index():
return render_template_string(
CONTAINERS_TEMPLATE, containers=list_all_containers(), s=server_stats(), page="containers"
)
@app.route("/system")
def system_ui():
return render_template_string(
SYSTEM_TEMPLATE, df=docker_system_df(), images=list_images(), volumes=list_volumes(), page="system"
)
@app.route("/traefik")
def traefik_ui():
return render_template_string(
TRAEFIK_TEMPLATE, routers=list_traefik_routers(), api_url=TRAEFIK_API_URL, page="traefik"
)
@app.route("/dns")
def dns_ui():
return render_template_string(DNS_TEMPLATE, records=list_dns_records(), page="dns")
@app.route("/dns/add", methods=["POST"])
def dns_add():
rtype = request.form.get("type", "A")
name = request.form.get("name", "").strip()
content = request.form.get("content", "").strip()
proxied = request.form.get("proxied") == "on"
if name and content:
if name == "@":
name = "akalan.dev"
elif not name.endswith("akalan.dev"):
name = f"{name}.akalan.dev"
create_dns_record(rtype, name, content, proxied)
return redirect("/dns")
@app.route("/dns/update/<record_id>", methods=["POST"])
def dns_update(record_id):
content = request.form.get("content", "").strip()
proxied = request.form.get("proxied") == "on"
if content:
update_dns_record(record_id, content, proxied)
return redirect("/dns")
@app.route("/dns/delete/<record_id>", methods=["POST"])
def dns_delete(record_id):
delete_dns_record_by_id(record_id)
return redirect("/dns")
@app.route("/logs/<name>")
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("/start/<name>", methods=["POST"])
def start_ui(name):
subprocess.run(["docker", "start", name])
return redirect(request.referrer or "/")
@app.route("/stop/<name>", methods=["POST"])
def stop_ui(name):
subprocess.run(["docker", "stop", name])
return redirect(request.referrer or "/")
@app.route("/restart/<name>", methods=["POST"])
def restart_ui(name):
subprocess.run(["docker", "restart", name])
return redirect(request.referrer or "/")
@app.route("/delete/<name>", methods=["POST"])
def delete_ui(name):
hostname = find_container_hostname(name)
subprocess.run(["docker", "rm", "-f", name])
delete_dns_record(hostname)
return redirect(request.referrer or "/")
@app.route("/images/delete/<image_id>", methods=["POST"])
def delete_image(image_id):
subprocess.run(["docker", "rmi", image_id])
return redirect("/system")
@app.route("/images/prune", methods=["POST"])
def prune_images():
subprocess.run(["docker", "image", "prune", "-af"])
return redirect("/system")
@app.route("/volumes/delete/<name>", methods=["POST"])
def delete_volume(name):
subprocess.run(["docker", "volume", "rm", name])
return redirect("/system")
@app.route("/volumes/prune", methods=["POST"])
def prune_volumes():
subprocess.run(["docker", "volume", "prune", "-f"])
return redirect("/system")
@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/<name>", 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)