diff --git a/app.py b/app.py index e9012ce..85ff6b9 100644 --- a/app.py +++ b/app.py @@ -32,6 +32,41 @@ def save_settings(data): app = Flask(__name__) app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32) + + +def friendly_error(raw, service=""): + """Turn a raw exception string into a short, human-readable cause. + + The service dicts carry str(e) from requests/urllib3, which is useful for + debugging but unreadable on the dashboard. Templates render this instead + and keep the raw string behind a details disclosure. + """ + s = str(raw or "") + low = s.lower() + name = service or "The service" + + if low.strip() == "timeout": + return f"{name} took too long to answer and was dropped at the 5s page deadline." + if "connecttimeout" in low or "timed out" in low or "read timed out" in low: + return f"{name} didn't respond — the connection timed out." + if "connection refused" in low or "newconnectionerror" in low: + return f"{name} refused the connection — check that it's running and the port is right." + if "name or service not known" in low or "nameresolution" in low or "failed to resolve" in low: + return f"{name}'s hostname couldn't be resolved — check the host setting." + if "no route to host" in low or "network is unreachable" in low: + return f"{name} is unreachable from this container — check the network." + if "401" in s or "unauthorized" in low or "403" in s or "forbidden" in low: + return f"{name} rejected the API key." + if "404" in s or "not found" in low: + return f"{name} returned 404 — check the URL or base path." + if any(code in s for code in ("500", "502", "503", "504")): + return f"{name} returned a server error." + if "ssl" in low or "certificate" in low: + return f"{name}'s TLS handshake failed — check the certificate settings." + return f"{name} couldn't be reached." + + +app.jinja_env.filters["friendly_error"] = friendly_error app.config["SESSION_COOKIE_PATH"] = "/" app.config["SESSION_COOKIE_HTTPONLY"] = True app.config["SESSION_COOKIE_SAMESITE"] = "Lax" diff --git a/templates/dashboard.html b/templates/dashboard.html index 46e2979..e52ba04 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -163,11 +163,40 @@ font-style: italic; } .header-right { + display: flex; + align-items: center; + gap: 0.7rem; font-family: var(--brand-font); font-size: 0.85rem; color: var(--muted); text-align: right; } + .header-meta { text-align: right; } + .header-clock { + font-size: 1.1rem; + font-family: var(--brand-font); + color: var(--gold); + letter-spacing: 1px; + } + .header-room { font-size: 0.7rem; color: var(--muted); margin-top: 0.1rem; } + .settings-link { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.35rem; + min-width: 44px; + min-height: 44px; + padding: 0 0.7rem; + background: var(--navy); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--muted); + text-decoration: none; + font-size: 0.78rem; + white-space: nowrap; + transition: color 0.2s, border-color 0.2s; + } + .settings-link:hover { color: var(--gold); border-color: var(--gold); } /* ── Main layout ── */ main { padding: 1.5rem 2rem; max-width: 1280px; margin: 0 auto; } @@ -790,6 +819,34 @@ font-size: 0.85rem; margin-bottom: 1rem; } + .error-detail { margin-top: 0.5rem; } + .error-detail summary { + cursor: pointer; + font-size: 0.75rem; + color: var(--muted); + list-style: none; + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.25rem 0; + } + .error-detail summary::-webkit-details-marker { display: none; } + .error-detail summary::before { content: "▸"; font-size: 0.7rem; } + .error-detail[open] summary::before { content: "▾"; } + .error-detail summary:hover { color: var(--text); } + .error-detail pre { + margin: 0.4rem 0 0; + padding: 0.6rem 0.7rem; + background: rgba(0,0,0,0.25); + border-radius: 6px; + font-size: 0.7rem; + line-height: 1.45; + color: var(--muted); + white-space: pre-wrap; + word-break: break-word; + overflow-x: auto; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + } .rt-error { font-size: 0.8rem; color: #ff8080; padding: 0.4rem 0; } /* ── Footer quote ── */ @@ -820,7 +877,12 @@ } header h1 { font-size: 1.05rem; } .header-sub { display: none; } + /* Title and Settings share the first row; the pill strip wraps below */ + .header-left { order: 1; min-width: 0; flex: 1 1 auto; gap: 0.6rem; } + .header-left h1 { overflow-wrap: anywhere; } + .header-right { order: 2; margin-left: auto; flex: 0 0 auto; } .header-services { + order: 3; width: 100%; overflow-x: auto; flex-wrap: nowrap; @@ -901,12 +963,28 @@ .rt-title { font-size: 0.8rem; } .rt-meta { font-size: 0.68rem; } .svc-pill-stat { display: none; } - .header-right { display: none; } + /* Keep the Settings link reachable on mobile — only the clock/room drop out */ + .header-meta { display: none; } + .settings-label { display: none; } + .settings-link { padding: 0; } .stat .value { font-size: 1.4rem; } .search-bar input { font-size: 16px; } /* prevent iOS zoom */ } +{#- Renders a degraded service as human copy, with the raw exception tucked + behind a disclosure for debugging. -#} +{% macro service_error(name, raw) %} +
{{ raw }}
+