diff --git a/app.py b/app.py index bb99781..fcfdc76 100644 --- a/app.py +++ b/app.py @@ -67,14 +67,12 @@ def _cpu_times(): 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_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: @@ -84,7 +82,7 @@ def server_stats(): with open("/proc/meminfo") as f: for line in f: key, _, rest = line.partition(":") - meminfo[key] = int(rest.strip().split()[0]) # kB + 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 @@ -103,12 +101,12 @@ def server_stats(): hours, _ = divmod(rem, 3600) stats["uptime"] = f"{days} gün {hours} saat" if days else f"{hours} saat" - all_containers = subprocess.run( + 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_containers if s == "running") - stats["containers_total"] = len(all_containers) + stats["containers_running"] = sum(1 for s in all_states if s == "running") + stats["containers_total"] = len(all_states) return stats @@ -119,6 +117,17 @@ def inspect_all(cid): ).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_projects(): out = subprocess.run( ["docker", "ps", "-a", "--filter", "label=traefik.enable=true", "--format", "{{.ID}}"], @@ -146,6 +155,88 @@ def list_projects(): return projects +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 == "" and tag == "": + 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: @@ -190,17 +281,11 @@ def delete_dns_record(hostname): ) -TEMPLATE = """ - - - -Deploy Panel - - - + 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 = """ + +""" + +PROJECTS_TEMPLATE = """ + + + +Deploy Panel + + +

Deploy Panel

+""" + NAV + """
{{ projects|length }} proje · 20 saniyede bir otomatik yenilenir · şimdi yenile
@@ -302,6 +414,141 @@ TEMPLATE = """ """ +CONTAINERS_TEMPLATE = """ + + + +Container'lar - Deploy Panel + + + + + +

Deploy Panel

+""" + NAV + """ +
Sunucudaki TÜM container'lar (sadece Traefik projeleri değil) · {{ containers|length }} adet
+ {% if containers %} + + + {% for c in containers %} + + + + + + + + + {% endfor %} +
İsimAdresPortlarDurumİmaj
{{ c.name }}{% if c.hostname %}{{ c.hostname }}{% else %}—{% endif %}{{ c.ports or '—' }} + {{ c.status }} + {% if c.since %}{{ c.since }}{% endif %} + {{ c.image }} +
+ Loglar + {% if c.status == 'running' %} +
+ +
+
+ +
+ {% else %} +
+ +
+ {% endif %} +
+ +
+
+
+ {% else %} +

Hiç container yok.

+ {% endif %} + + +""" + +SYSTEM_TEMPLATE = """ + + + +Sistem & Disk - Deploy Panel + + + + +

Deploy Panel

+""" + NAV + """ +
Docker'ın diskte kapladığı alan ve imaj/volume yönetimi
+ + + + {% for row in df %} + + + + + + + + {% endfor %} +
TürToplamKullanımdaBoyutGeri kazanılabilir
{{ row.type }}{{ row.total }}{{ row.active }}{{ row.size }}{{ row.reclaimable }}
+ +

İmajlar ({{ images|length }})

+
+ +
+ {% if images %} + + + {% for img in images %} + + + + + + + {% endfor %} +
İmajBoyutOluşturulma
{{ img.label }}{{ img.size }}{{ img.created }} +
+ +
+
+ {% else %} +

Hiç imaj yok.

+ {% endif %} + +

Volume'ler ({{ volumes|length }})

+
+ +
+ {% if volumes %} + + + {% for v in volumes %} + + + + + + {% endfor %} +
İsimDriver
{{ v.name }}{{ v.driver }} +
+ +
+
+ {% else %} +

Hiç volume yok.

+ {% endif %} + + +""" + LOGS_TEMPLATE = """ @@ -327,7 +574,19 @@ LOGS_TEMPLATE = """ @app.route("/") def index(): - return render_template_string(TEMPLATE, projects=list_projects(), s=server_stats()) + return render_template_string(PROJECTS_TEMPLATE, projects=list_projects(), s=server_stats(), page="projects") + + +@app.route("/containers") +def containers_ui(): + return render_template_string(CONTAINERS_TEMPLATE, containers=list_all_containers(), 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("/logs/") @@ -340,20 +599,54 @@ def logs_ui(name): return render_template_string(LOGS_TEMPLATE, name=name, lines=150, logs=output or "(log yok)") +@app.route("/start/", methods=["POST"]) +def start_ui(name): + subprocess.run(["docker", "start", name]) + return redirect(request.referrer or "/") + + +@app.route("/stop/", methods=["POST"]) +def stop_ui(name): + subprocess.run(["docker", "stop", name]) + return redirect(request.referrer or "/") + + @app.route("/restart/", methods=["POST"]) def restart_ui(name): subprocess.run(["docker", "restart", name]) - return redirect("/") + return redirect(request.referrer or "/") @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("/") + hostname = find_container_hostname(name) + subprocess.run(["docker", "rm", "-f", name]) + delete_dns_record(hostname) + return redirect(request.referrer or "/") + + +@app.route("/images/delete/", 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/", 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"])