Fix unreachable mobile Settings link and raw error tracebacks
All checks were successful
Deploy / deploy (push) Successful in 2m6s
All checks were successful
Deploy / deploy (push) Successful in 2m6s
Settings lived inside .header-right, which is hidden below 600px, leaving no way to reach it on mobile. It's now a standalone 44x44px button next to the title. Dashboard error cards showed raw Python/urllib3 exception strings. Added a friendly_error() filter that classifies the failure (timeout, connection refused, DNS, auth, 4xx/5xx, TLS) into plain-English copy, with the raw string still available behind a details disclosure. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
9804283396
commit
2e1f8efa33
2 changed files with 133 additions and 16 deletions
35
app.py
35
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"
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
{#- Renders a degraded service as human copy, with the raw exception tucked
|
||||
behind a disclosure for debugging. -#}
|
||||
{% macro service_error(name, raw) %}
|
||||
<div class="error" role="alert">
|
||||
<div>{{ raw | friendly_error(name) }}</div>
|
||||
{% if raw %}
|
||||
<details class="error-detail">
|
||||
<summary>Technical details</summary>
|
||||
<pre>{{ raw }}</pre>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
<body>
|
||||
|
||||
<header>
|
||||
|
|
@ -1064,15 +1142,19 @@
|
|||
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div id="header-clock" style="font-size:1.1rem; font-family:var(--brand-font); color:var(--gold); letter-spacing:1px;">—</div>
|
||||
<div style="font-size:0.7rem; color:var(--muted); margin-top:0.1rem;">
|
||||
{% if theme == 'office' %}Dunder Mifflin HQ
|
||||
{% elif theme == 'parks' %}Pawnee City Hall
|
||||
{% elif theme == 'potter' %}Hogwarts Great Hall
|
||||
{% elif theme == 'starwars' %}Death Star Control Room
|
||||
{% else %}Study Room F{% endif %}
|
||||
· <a href="/settings" style="color:var(--muted); text-decoration:none;">⚙ Settings</a>
|
||||
<div class="header-meta">
|
||||
<div id="header-clock" class="header-clock">—</div>
|
||||
<div class="header-room">
|
||||
{% if theme == 'office' %}Dunder Mifflin HQ
|
||||
{% elif theme == 'parks' %}Pawnee City Hall
|
||||
{% elif theme == 'potter' %}Hogwarts Great Hall
|
||||
{% elif theme == 'starwars' %}Death Star Control Room
|
||||
{% else %}Study Room F{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ url_for('settings_page') }}" class="settings-link" aria-label="Settings">
|
||||
<span aria-hidden="true">⚙</span><span class="settings-label">Settings</span>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
@ -1138,7 +1220,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="error">Could not load system stats: {{ unraid.error }}</div>
|
||||
{{ service_error("Unraid", unraid.error) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1174,7 +1256,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="error">Could not reach Sonarr: {{ sonarr.error }}</div>
|
||||
{{ service_error("Sonarr", sonarr.error) }}
|
||||
{% endif %}
|
||||
|
||||
<div class="section-title">🍅 Popular on RT</div>
|
||||
|
|
@ -1242,7 +1324,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="error">Could not reach Radarr: {{ radarr.error }}</div>
|
||||
{{ service_error("Radarr", radarr.error) }}
|
||||
{% endif %}
|
||||
|
||||
<div class="section-title">🍅 Top Box Office on RT</div>
|
||||
|
|
@ -1301,7 +1383,7 @@
|
|||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="error">Could not reach Prowlarr: {{ prowlarr.error }}</div>
|
||||
{{ service_error("Prowlarr", prowlarr.error) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1351,7 +1433,7 @@
|
|||
⏱ ETA: <span id="sab-timeleft"></span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="error">Could not reach SABnzbd: {{ sabnzbd.error }}</div>
|
||||
{{ service_error("SABnzbd", sabnzbd.error) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1387,7 +1469,7 @@
|
|||
<div style="color:var(--muted); font-size:0.85rem;">No recent requests.</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="error">Could not reach Ombi: {{ ombi.error }}</div>
|
||||
{{ service_error("Ombi", ombi.error) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1509,7 +1591,7 @@
|
|||
</div>
|
||||
|
||||
{% else %}
|
||||
<div class="error">Could not reach Tautulli: {{ tautulli.error }}</div>
|
||||
{{ service_error("Tautulli", tautulli.error) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue