hostname bug fix + restart/loglar/uptime/otomatik yenileme ekle
Deploy / deploy (push) Successful in 19s
Deploy / deploy (push) Successful in 19s
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from flask import Flask, jsonify, redirect, render_template_string, request
|
from flask import Flask, jsonify, redirect, render_template_string, request
|
||||||
@@ -13,20 +15,50 @@ SELF_NAME = os.environ.get("SELF_CONTAINER_NAME", "")
|
|||||||
API_TOKEN = os.environ["PANEL_API_TOKEN"]
|
API_TOKEN = os.environ["PANEL_API_TOKEN"]
|
||||||
SERVER_IP = os.environ.get("SERVER_IP", "148.135.181.126")
|
SERVER_IP = os.environ.get("SERVER_IP", "148.135.181.126")
|
||||||
|
|
||||||
|
ROUTER_RULE_RE = re.compile(r"^traefik\.http\.routers\.[^.]+\.rule$")
|
||||||
|
|
||||||
|
|
||||||
def check_auth():
|
def check_auth():
|
||||||
auth = request.headers.get("Authorization", "")
|
auth = request.headers.get("Authorization", "")
|
||||||
return auth == f"Bearer {API_TOKEN}"
|
return auth == f"Bearer {API_TOKEN}"
|
||||||
|
|
||||||
|
|
||||||
def extract_hostname(labels, name):
|
def extract_hostname(labels):
|
||||||
key = f"traefik.http.routers.{name}.rule"
|
"""Scan ALL traefik router rule labels, not just one matching the
|
||||||
rule = labels.get(key, "")
|
container's own name — compose-based containers get auto-generated
|
||||||
if "`" in rule:
|
docker names that no longer match the Traefik router name."""
|
||||||
return rule.split("`")[1]
|
for key, value in labels.items():
|
||||||
|
if ROUTER_RULE_RE.match(key) and "`" in value:
|
||||||
|
return value.split("`")[1]
|
||||||
return None
|
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 inspect_all(cid):
|
||||||
|
return json.loads(subprocess.run(
|
||||||
|
["docker", "inspect", cid], capture_output=True, text=True, check=True
|
||||||
|
).stdout)[0]
|
||||||
|
|
||||||
|
|
||||||
def list_projects():
|
def list_projects():
|
||||||
out = subprocess.run(
|
out = subprocess.run(
|
||||||
["docker", "ps", "-a", "--filter", "label=traefik.enable=true", "--format", "{{.ID}}"],
|
["docker", "ps", "-a", "--filter", "label=traefik.enable=true", "--format", "{{.ID}}"],
|
||||||
@@ -35,18 +67,19 @@ def list_projects():
|
|||||||
|
|
||||||
projects = []
|
projects = []
|
||||||
for cid in out:
|
for cid in out:
|
||||||
info = json.loads(subprocess.run(
|
info = inspect_all(cid)
|
||||||
["docker", "inspect", cid], capture_output=True, text=True, check=True
|
|
||||||
).stdout)[0]
|
|
||||||
name = info["Name"].lstrip("/")
|
name = info["Name"].lstrip("/")
|
||||||
if name == SELF_NAME:
|
if name == SELF_NAME:
|
||||||
continue
|
continue
|
||||||
labels = info["Config"]["Labels"] or {}
|
labels = info["Config"]["Labels"] or {}
|
||||||
hostname = extract_hostname(labels, name)
|
state = info["State"]
|
||||||
|
status = state["Status"]
|
||||||
|
when = human_relative(state.get("StartedAt") if status == "running" else state.get("FinishedAt"))
|
||||||
projects.append({
|
projects.append({
|
||||||
"name": name,
|
"name": name,
|
||||||
"hostname": hostname,
|
"hostname": extract_hostname(labels),
|
||||||
"status": info["State"]["Status"],
|
"status": status,
|
||||||
|
"since": when,
|
||||||
"image": info["Config"]["Image"],
|
"image": info["Config"]["Image"],
|
||||||
})
|
})
|
||||||
projects.sort(key=lambda p: p["name"])
|
projects.sort(key=lambda p: p["name"])
|
||||||
@@ -59,7 +92,7 @@ def find_container_hostname(name):
|
|||||||
return None
|
return None
|
||||||
info = json.loads(result.stdout)[0]
|
info = json.loads(result.stdout)[0]
|
||||||
labels = info["Config"]["Labels"] or {}
|
labels = info["Config"]["Labels"] or {}
|
||||||
return extract_hostname(labels, name)
|
return extract_hostname(labels)
|
||||||
|
|
||||||
|
|
||||||
def ensure_dns(hostname):
|
def ensure_dns(hostname):
|
||||||
@@ -103,23 +136,36 @@ TEMPLATE = """
|
|||||||
<head>
|
<head>
|
||||||
<title>Deploy Panel</title>
|
<title>Deploy Panel</title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="refresh" content="20">
|
||||||
<style>
|
<style>
|
||||||
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2.5rem; }
|
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2.5rem; }
|
||||||
h1 { color: #38bdf8; }
|
h1 { color: #38bdf8; margin-bottom: 0.2rem; }
|
||||||
table { width: 100%; border-collapse: collapse; margin-top: 1.5rem; }
|
.sub { opacity: 0.55; font-size: 0.85rem; margin-bottom: 1.5rem; }
|
||||||
td, th { padding: 0.7rem; border-bottom: 1px solid #334155; text-align: left; }
|
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 { color: #38bdf8; text-decoration: none; }
|
||||||
.badge { padding: 0.2rem 0.6rem; border-radius: 4px; font-size: 0.8rem; }
|
a:hover { text-decoration: underline; }
|
||||||
.running { background: #065f46; }
|
.badge { padding: 0.25rem 0.65rem; border-radius: 999px; font-size: 0.78rem; font-weight: 600; }
|
||||||
.exited { background: #7f1d1d; }
|
.running { background: #065f46; color: #d1fae5; }
|
||||||
button { background: #7f1d1d; color: white; border: none; padding: 0.4rem 0.9rem;
|
.exited { background: #7f1d1d; color: #fecaca; }
|
||||||
border-radius: 4px; cursor: pointer; }
|
.since { opacity: 0.55; font-size: 0.8rem; display: block; }
|
||||||
button:hover { background: #991b1b; }
|
.actions { display: flex; gap: 0.4rem; }
|
||||||
|
button, .btn { border: none; padding: 0.4rem 0.8rem; border-radius: 6px; cursor: pointer;
|
||||||
|
font-size: 0.85rem; color: white; }
|
||||||
|
.btn-restart { background: #1e3a8a; }
|
||||||
|
.btn-restart:hover { background: #1e40af; }
|
||||||
|
.btn-logs { background: #334155; }
|
||||||
|
.btn-logs:hover { background: #475569; }
|
||||||
|
.btn-delete { background: #7f1d1d; }
|
||||||
|
.btn-delete:hover { background: #991b1b; }
|
||||||
.empty { opacity: 0.6; margin-top: 1.5rem; }
|
.empty { opacity: 0.6; margin-top: 1.5rem; }
|
||||||
|
form { display: inline; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Deploy Panel</h1>
|
<h1>Deploy Panel</h1>
|
||||||
|
<div class="sub">{{ projects|length }} proje · 20 saniyede bir otomatik yenilenir · <a href="/">şimdi yenile</a></div>
|
||||||
{% if projects %}
|
{% if projects %}
|
||||||
<table>
|
<table>
|
||||||
<tr><th>Proje</th><th>Adres</th><th>Durum</th><th>İmaj</th><th></th></tr>
|
<tr><th>Proje</th><th>Adres</th><th>Durum</th><th>İmaj</th><th></th></tr>
|
||||||
@@ -127,13 +173,22 @@ TEMPLATE = """
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{ p.name }}</td>
|
<td>{{ p.name }}</td>
|
||||||
<td>{% if p.hostname %}<a href="https://{{ p.hostname }}" target="_blank">{{ p.hostname }}</a>{% else %}—{% endif %}</td>
|
<td>{% if p.hostname %}<a href="https://{{ p.hostname }}" target="_blank">{{ p.hostname }}</a>{% else %}—{% endif %}</td>
|
||||||
<td><span class="badge {{ p.status }}">{{ p.status }}</span></td>
|
<td>
|
||||||
|
<span class="badge {{ p.status }}">{{ p.status }}</span>
|
||||||
|
{% if p.since %}<span class="since">{{ p.since }}</span>{% endif %}
|
||||||
|
</td>
|
||||||
<td>{{ p.image }}</td>
|
<td>{{ p.image }}</td>
|
||||||
<td>
|
<td>
|
||||||
<form method="post" action="/delete/{{ p.name }}"
|
<div class="actions">
|
||||||
onsubmit="return confirm('{{ p.name }} silinsin mi? Container ve DNS kaydı kalıcı olarak kaldırılacak.');">
|
<a class="btn btn-logs" href="/logs/{{ p.name }}">Loglar</a>
|
||||||
<button type="submit">Sil</button>
|
<form method="post" action="/restart/{{ p.name }}">
|
||||||
</form>
|
<button class="btn-restart" type="submit">Yeniden Başlat</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/delete/{{ p.name }}"
|
||||||
|
onsubmit="return confirm('{{ p.name }} silinsin mi? Container ve DNS kaydı kalıcı olarak kaldırılacak.');">
|
||||||
|
<button class="btn-delete" type="submit">Sil</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -145,12 +200,50 @@ TEMPLATE = """
|
|||||||
</html>
|
</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="/">← panele dön</a>
|
||||||
|
<h1>{{ name }} — son {{ lines }} satır</h1>
|
||||||
|
<pre>{{ logs }}</pre>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
return render_template_string(TEMPLATE, projects=list_projects())
|
return render_template_string(TEMPLATE, projects=list_projects())
|
||||||
|
|
||||||
|
|
||||||
|
@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("/restart/<name>", methods=["POST"])
|
||||||
|
def restart_ui(name):
|
||||||
|
subprocess.run(["docker", "restart", name])
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/delete/<name>", methods=["POST"])
|
@app.route("/delete/<name>", methods=["POST"])
|
||||||
def delete_ui(name):
|
def delete_ui(name):
|
||||||
projects = {p["name"]: p for p in list_projects()}
|
projects = {p["name"]: p for p in list_projects()}
|
||||||
|
|||||||
Reference in New Issue
Block a user