This commit is contained in:
@@ -16,6 +16,7 @@ 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$")
|
||||
|
||||
@@ -254,6 +255,81 @@ def delete_dns_record(hostname):
|
||||
)
|
||||
|
||||
|
||||
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"]: 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; }
|
||||
@@ -303,6 +379,8 @@ BASE_STYLE = """
|
||||
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 & Disk</a>
|
||||
</nav>
|
||||
"""
|
||||
@@ -474,6 +552,110 @@ SYSTEM_TEMPLATE = """
|
||||
</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>
|
||||
@@ -511,6 +693,48 @@ def system_ui():
|
||||
)
|
||||
|
||||
|
||||
@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(
|
||||
|
||||
Reference in New Issue
Block a user