From 7d373612a00726ec2ec2deddc78cc8371341fa85 Mon Sep 17 00:00:00 2001 From: Oliver G Date: Mon, 3 Aug 2026 07:11:08 +0200 Subject: [PATCH] feat: fehlende Dienste und die VanityCast App aufnehmen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Abgeglichen gegen die tatsächlich vorhandenen Dienste (Uptime-Kuma-Monitore, Traefik-Hosts auf dem Hostinger VPS, CloudPanel-Sites auf hetzner, NPM-Datenbank auf beiden Proxy-Servern und die Domainliste auf Hostinger). Es fehlten sieben Einträge — vor allem alles, was auf dem Hostinger VPS hinter Traefik läuft. Neu: - VanityCast Landing (vanitycast.vanityontour.de) - Kurzlinks (go.vanityontour.de) - Forgejo (git.giertz.biz) - Postiz (social.vanityontour.de) - Shlink Admin (shlink.vanityontour.de) - Shlink API (go.vanityontour.de/rest/health) - SSL-Laufzeit für vanitycast, go und git.giertz.biz Shlink Admin erwartet bewusst 401: Traefik beantwortet die BasicAuth vor dem Proxy. Das belegt nur, dass Traefik läuft und der Schutz noch greift — eine 200 hiesse, der Schutz ist weg. Dass Shlink selbst antwortet, zeigt der neue Health-Endpunkt. App-Bereich fasst jetzt mehrere Apps: APP_STORE_ID wird zu APP_STORE_IDS, status.json bekommt "apps" als Liste. Das alte Einzelfeld "app" bleibt erhalten, weil index.html nur manuell auf Hostinger aktualisiert wird und eine ältere Seite sonst leer liefe. VanityCast hat noch keine Bewertungen — statt "☆ — (0)", was nach einem Fehler aussieht, steht dort "Noch keine Bewertungen". --- README.md | 33 ++++++++++++++++++++++++++----- public/index.html | 44 ++++++++++++++++++++++++++++++----------- scripts/check_status.py | 42 +++++++++++++++++++++++++++++++-------- 3 files changed, 94 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 2f43568..1fe3775 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,19 @@ Automated status dashboard for all VanityOnTour services, hosted on Hostinger at ## What it monitors -- **Websites**: vanityontour.de, news, wiki, staysense, landing -- **Tools**: N8N, Nginx Proxy Manager, Uptime Kuma, Stats, App Backend, CloudPanel -- **APIs**: RSS News API, StaySense API -- **iOS App**: Vanity Expense Logbook (version, rating, last update) -- **SSL**: Certificate expiry for all main domains +- **Websites**: vanityontour.de, News, Wiki, StaySense, StaySense Landing, + VanityCast Landing, Kurzlinks (`go.`) +- **Tools**: N8N, beide Nginx Proxy Manager, Uptime Kuma, Grafana, App Backend, + CloudPanel, Forgejo, Postiz, Shlink Admin +- **APIs**: RSS News API, StaySense API, Shlink API +- **iOS Apps**: Vanity Expense Logbook und VanityCast (Version, Bewertung, + letztes Update) +- **SSL**: Zertifikatslaufzeit aller Hauptdomains + +`Shlink Admin` ist bewusst ein schwaches Signal: Traefik beantwortet die +BasicAuth **vor** dem Proxy, die erwartete `401` belegt also nur, dass Traefik +läuft und der Schutz noch greift — nicht, dass der Container dahinter lebt. Dass +Shlink selbst antwortet, zeigt `Shlink API` (`go.vanityontour.de/rest/health`). ## How it works @@ -39,6 +47,21 @@ scp scripts/check_status.py hetzner:/opt/check_status.py ssh hetzner '/opt/run_status_check.sh' # einmal testweise ausführen ``` +### Deployment einer Änderung an `public/` + +`index.html`, CSS und Icons werden vom Cron **nicht** mit ausgerollt — der +kopiert nur `status.json`. Der Deploy-Key für Hostinger liegt ausschließlich auf +hetzner (`/root/.ssh/hostinger_ed25519`), der Weg geht deshalb über hetzner: + +```bash +scp public/index.html hetzner:/opt/public/index.html +ssh hetzner 'scp -P 65002 -i /root/.ssh/hostinger_ed25519 /opt/public/index.html \ + u982551092@82.198.228.98:/home/u982551092/domains/status.vanityontour.de/public_html/index.html' +``` + +Vorher prüfen, ob die Live-Datei noch dem Repo-Stand entspricht — sie wird sonst +stillschweigend überschrieben. + ## Dienste hinter dem Management-VPN Seit der VPN-Absicherung antworten die Admin-Oberflächen öffentlich nur noch mit diff --git a/public/index.html b/public/index.html index 95c8d60..53bb894 100644 --- a/public/index.html +++ b/public/index.html @@ -169,6 +169,9 @@ gap: 16px; align-items: flex-start; } + /* Mehrere Apps stehen untereinander in einer Karte — Trennlinie zwischen + ihnen, aber nicht unter der letzten. */ + .app-card + .app-card { border-top: 1px solid var(--border); } .app-icon { width: 72px; height: 72px; @@ -384,14 +387,31 @@ function renderSection(title, services, sslData) { `; } -function renderApp(app) { - if (!app || app.error) { +function renderApps(data) { + // Neuere status.json liefert "apps" als Liste; das alte Einzelfeld "app" + // bleibt als Rückfallebene, falls die Seite mal vor dem Skript aktualisiert wird. + const apps = (data.apps && data.apps.length) ? data.apps : (data.app ? [data.app] : []); + const usable = apps.filter(a => a && !a.error); + + if (!usable.length) { return `
-
iOS App
+
iOS Apps
App Store Daten nicht verfügbar
`; } + + return ` +
+
+ iOS Apps + ${usable.length} im App Store +
+ ${usable.map(appCard).join("")} +
`; +} + +function appCard(app) { const rating = app.rating ? app.rating.toFixed(1) : "—"; const storeUrl = app.store_url || "#"; const iconHtml = app.icon_url @@ -401,12 +421,13 @@ function renderApp(app) { ? new Date(app.last_update).toLocaleDateString("de-DE", { year:"numeric", month:"long", day:"numeric" }) : "—"; + // Eine frisch veröffentlichte App hat noch keine Bewertungen — dann "☆ — (0)" + // zu zeigen sähe nach einem Fehler aus. + const ratingTag = app.rating + ? `${renderStars(app.rating)} ${rating} (${app.rating_count || 0})` + : `Noch keine Bewertungen`; + return ` -
-
- iOS App - App Store -
${iconHtml}
@@ -414,7 +435,7 @@ function renderApp(app) {
${app.seller || ""}
v${app.version || "—"} - ${renderStars(app.rating)} ${rating} (${app.rating_count || 0}) + ${ratingTag} ${app.price || "—"} ${app.category || "—"} iOS ${app.min_ios || "—"}+ @@ -424,8 +445,7 @@ function renderApp(app) { Im App Store ansehen ↗
-
-
`; +
`; } async function loadStatus() { @@ -466,7 +486,7 @@ function render(data) { renderSection("Websites", websites, ssl) + renderSection("Tools & Automation", tools, ssl) + renderSection("APIs", apis, ssl) + - renderApp(data.app) + + renderApps(data) + `
Uptime Kuma diff --git a/scripts/check_status.py b/scripts/check_status.py index be1ada2..137a679 100644 --- a/scripts/check_status.py +++ b/scripts/check_status.py @@ -41,6 +41,8 @@ WEBSITES = [ {"name": "Wiki", "url": "https://wiki.vanityontour.de", "group": "websites", "expect": [200, 301, 302]}, {"name": "StaySense", "url": "https://staysense.vanityontour.de", "group": "websites", "expect": [200, 301, 302]}, {"name": "StaySense Landing", "url": "https://landing.staysense.vanityontour.de", "group": "websites", "expect": [200, 301, 302]}, + {"name": "VanityCast Landing", "url": "https://vanitycast.vanityontour.de", "group": "websites", "expect": [200, 301, 302]}, + {"name": "Kurzlinks", "url": "https://go.vanityontour.de", "group": "websites", "expect": [200, 301, 302]}, {"name": "N8N Automation", "url": "https://n8n.vanityontour.de", "group": "tools", "expect": [200, 301, 302], "check_url": "http://10.10.0.13:5678"}, {"name": "Nginx Proxy Manager", "url": "https://nginx.vanityontour.de", "group": "tools", "expect": [200, 301, 302], "check_url": "http://10.10.0.13:81"}, {"name": "Nginx Proxy Mgr (VoT)", "url": "https://ng.vanityontour.de", "group": "tools", "expect": [200, 301, 302], "check_url": "http://10.10.0.12:81"}, @@ -52,8 +54,18 @@ WEBSITES = [ # Grafana und CloudPanel laufen auf hetzner selbst — localhost statt 10.10.0.14, # damit die Prüfung auch bei liegendem Tunnel noch stimmt. {"name": "CloudPanel", "url": "https://cp.blog.vanityontour.de", "group": "tools", "expect": [200, 301, 302], "check_url": "https://127.0.0.1:8443"}, + # Dienste auf dem Hostinger VPS (Traefik). Forgejo und Postiz haben bewusst + # noch öffentliche Logins und werden deshalb ganz normal von außen geprüft. + {"name": "Forgejo", "url": "https://git.giertz.biz", "group": "tools", "expect": [200, 301, 302]}, + {"name": "Postiz", "url": "https://social.vanityontour.de", "group": "tools", "expect": [200, 301, 302, 307]}, + # Achtung, schwaches Signal: Traefik beantwortet die BasicAuth vor dem Proxy. + # Die 401 belegt also nur, dass Traefik läuft und der Auth-Schutz noch greift — + # nicht, dass der Container dahinter lebt. Dass Shlink selbst antwortet, zeigt + # der Health-Endpunkt weiter unten. Eine 200 hier hiesse: Schutz ist weg. + {"name": "Shlink Admin", "url": "https://shlink.vanityontour.de", "group": "tools", "expect": [401]}, {"name": "RSS News API", "url": "https://news.vanityontour.de/health", "group": "apis", "expect": [200]}, {"name": "StaySense API", "url": "https://staysense.vanityontour.de/api/health", "group": "apis", "expect": [200]}, + {"name": "Shlink API", "url": "https://go.vanityontour.de/rest/health", "group": "apis", "expect": [200]}, ] SSL_DOMAINS = [ @@ -63,10 +75,16 @@ SSL_DOMAINS = [ "n8n.vanityontour.de", "staysense.vanityontour.de", "server.vanityontour.de", + "vanitycast.vanityontour.de", + "go.vanityontour.de", + "git.giertz.biz", ] -APP_STORE_ID = "6742772476" APP_STORE_COUNTRY = "de" +APP_STORE_IDS = [ + "6742772476", # Vanity Expense Logbook + "6781152682", # VanityCast +] def check_http(url: str, expected: list[int]) -> dict: @@ -109,17 +127,18 @@ def check_ssl(domain: str) -> dict: return {"valid": False, "expires_in_days": None, "expires_at": None, "error": str(e)[:60]} -def fetch_app_store() -> dict: - url = f"https://itunes.apple.com/lookup?id={APP_STORE_ID}&country={APP_STORE_COUNTRY}" +def fetch_app_store(app_id: str) -> dict: + url = f"https://itunes.apple.com/lookup?id={app_id}&country={APP_STORE_COUNTRY}" try: with urllib.request.urlopen(url, timeout=10) as resp: data = json.loads(resp.read().decode("utf-8")) if not data.get("results"): - return {"error": "No results"} + return {"id": app_id, "error": "No results"} r = data["results"][0] release_raw = r.get("currentVersionReleaseDate", "") release_fmt = release_raw[:10] if release_raw else None return { + "id": app_id, "name": r.get("trackName"), "version": r.get("version"), "rating": r.get("averageUserRating"), @@ -136,7 +155,7 @@ def fetch_app_store() -> dict: "error": None, } except Exception as e: - return {"error": str(e)[:80]} + return {"id": app_id, "error": str(e)[:80]} def main(): @@ -162,8 +181,12 @@ def main(): print(f" {domain}: {d.get('expires_in_days', '?')} days") print("Fetching App Store data...") - app = fetch_app_store() - print(f" {app.get('name', 'ERROR')} v{app.get('version', '?')} ⭐{app.get('rating', '?')}") + apps = [fetch_app_store(app_id) for app_id in APP_STORE_IDS] + for a in apps: + if a.get("error"): + print(f" {a.get('id')}: FEHLER {a['error']}") + else: + print(f" {a.get('name')} v{a.get('version', '?')} ⭐{a.get('rating') or '—'}") # Overall status downs = [r for r in results if r["status"] == "down"] @@ -180,7 +203,10 @@ def main(): "overall": overall, "services": results, "ssl": ssl_results, - "app": app, + # "app" bleibt als Einzelfeld erhalten, weil index.html nur manuell auf + # Hostinger aktualisiert wird — eine ältere Seite würde sonst leer laufen. + "app": apps[0] if apps else {"error": "keine App konfiguriert"}, + "apps": apps, } with open(OUTPUT_FILE, "w", encoding="utf-8") as f: