diff options
| author | yyamashita <yyamashita@hetzner.yyamashita.com> | 2026-10-31 13:20:27 +0900 |
|---|---|---|
| committer | yyamashita <yyamashita@hetzner.yyamashita.com> | 2026-10-31 13:20:27 +0900 |
| commit | b4ecef96fae14a92d12279927986e69c2e223180 (patch) | |
| tree | 822b448e7544aa197a9db6efc43f2ddb875eae55 /hetzner-admin/app.py | |
| parent | 5a55c7893615c5a64856ecca681325a609d1fac0 (diff) | |
Add status pages, ntfy alerts, and license-slot-checker repo
- status/: lightweight status checker (status.yyamashita.com, /api/status)
with ntfy DOWN/recovered alerts after consecutive failures
- uptime-kuma/: Uptime Kuma at uptime.yyamashita.com
- hetzner-admin: authenticated /status page (status-app results + docker ps -a)
- ntfy/: self-hosted ntfy at ntfy.yyamashita.com (deny-all auth, install.sh
creates admin user and status-app token)
- Caddyfile, DNS, post-receive hook, tests/check.sh, docs updated
- license-slot-checker: add to repos.txt, hook, and Claude sessions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'hetzner-admin/app.py')
| -rw-r--r-- | hetzner-admin/app.py | 66 |
1 files changed, 66 insertions, 0 deletions
diff --git a/hetzner-admin/app.py b/hetzner-admin/app.py index c63da80..274364e 100644 --- a/hetzner-admin/app.py +++ b/hetzner-admin/app.py @@ -1,5 +1,7 @@ import functools +import json import os +import urllib.request import paramiko from flask import Flask, Response, render_template_string, request @@ -12,6 +14,10 @@ SSH_HOST = os.environ.get("SSH_HOST", "host.docker.internal") SSH_PORT = int(os.environ.get("SSH_PORT", "22")) SSH_USER = os.environ.get("SSH_USER", "root") SSH_KEY_PATH = os.environ.get("SSH_KEY_PATH", "/app/id_rsa") +STATUS_API = os.environ.get("STATUS_API", "http://status-app:8080/api/status") + +# /status ページでコンテナ一覧を取るコマンド(run-command.sh のホワイトリストに要登録) +STATUS_COMMAND = "docker ps -a --format '{{.Names}}|{{.Status}}'" COMMANDS = [ ("Docker Status", "docker ps"), @@ -30,6 +36,7 @@ HTML = """<!DOCTYPE html> <head><title>Server Admin</title></head> <body> <h3>Server Admin</h3> +<p><a href="/status">Status</a></p> {% for label, _ in commands %} <form method="post" action="/run" style="display:block;margin-bottom:8px"> <input type="hidden" name="cmd" value="{{ label }}"> @@ -47,6 +54,40 @@ HTML = """<!DOCTYPE html> </body> </html>""" +STATUS_HTML = """<!DOCTYPE html> +<html> +<head><title>Server Status</title><meta http-equiv="refresh" content="60"></head> +<body> +<h3>Server Status</h3> +<p><a href="/">← Admin</a></p> +<h4>Services</h4> +{% if services is none %} +<pre>{{ services_error }}</pre> +{% else %} +<table border="1" cellpadding="4" style="border-collapse:collapse"> +<tr><th>Service</th><th>State</th><th>Detail</th><th>Latency</th><th>Uptime</th></tr> +{% for s in services %} +<tr><td>{{ s.name }}</td> +<td style="color:{{ 'green' if s.up else 'red' }}">{{ 'UP' if s.up else 'DOWN' }}</td> +<td>{{ s.detail }}</td><td>{{ s.ms }} ms</td><td>{{ s.uptime }}%</td></tr> +{% endfor %} +</table> +{% endif %} +<h4>Containers</h4> +{% if containers is none %} +<pre>{{ containers_error }}</pre> +{% else %} +<table border="1" cellpadding="4" style="border-collapse:collapse"> +<tr><th>Container</th><th>Status</th></tr> +{% for name, st in containers %} +<tr><td>{{ name }}</td> +<td style="color:{{ 'green' if st.startswith('Up') and '(unhealthy)' not in st else 'red' }}">{{ st }}</td></tr> +{% endfor %} +</table> +{% endif %} +</body> +</html>""" + def requires_auth(f): @functools.wraps(f) @@ -94,3 +135,28 @@ def run(): except Exception as e: output = f"Error: {e}" return render_template_string(HTML, commands=COMMANDS, output=output) + + +@app.route("/status") +@requires_auth +def status(): + services = services_error = containers = containers_error = None + try: + with urllib.request.urlopen(STATUS_API, timeout=5) as r: + services = json.load(r)["services"] + except Exception as e: + services_error = f"status-app に接続できない: {e}" + try: + out = run_ssh(STATUS_COMMAND) + containers = sorted( + tuple(line.split("|", 1)) for line in out.splitlines() if "|" in line + ) + except Exception as e: + containers_error = f"Error: {e}" + return render_template_string( + STATUS_HTML, + services=services, + services_error=services_error, + containers=containers, + containers_error=containers_error, + ) |
