Öffentliche /help-Seite — Server-rendered FAQ ohne Login

Apple-Reviewer braucht eine publik erreichbare Support-URL. Die
SPA-Hilfeseite (/#hilfe) ist hinter dem Welcome-Overlay für nicht
angemeldete User versteckt. Neue /help-Route rendert serverseitig:

- Holt aktive FAQ-Artikel aus help_articles (über bestehendes
  TTL-Cache _load_active_help_articles).
- Gruppiert nach Kategorie mit deutschen Labels.
- Native HTML5 <details>/<summary> Akkordeon — kein JS nötig.
- Dark Mode via prefers-color-scheme.
- Direkter mailto support@banyaro.app + Verweis auf die volle
  Hilfe nach Login.

Damit haben wir https://banyaro.app/help als Support-URL für App
Store Connect.
This commit is contained in:
rene 2026-05-30 19:34:12 +02:00
parent d23d696745
commit 2d907f6370
6 changed files with 156 additions and 16 deletions

View file

@ -1 +1 @@
1140
1141

View file

@ -1702,6 +1702,146 @@ async def presse():
return FileResponse(f"{STATIC_DIR}/presse.html", headers={"Cache-Control": "max-age=3600"})
@app.get("/help")
async def help_page():
"""Öffentliche, server-gerenderte Hilfe/FAQ-Seite. Kein Login nötig —
Apple-Reviewer und Suchmaschinen kommen hier direkt rein."""
from fastapi.responses import HTMLResponse
import html as _html
from routes.help import _load_active_help_articles
KAT_LABEL = {
"installation": "Installation & PWA",
"erste_schritte": "Erste Schritte",
"standort": "Standort & Wetter",
"account": "Account & Passwort",
"features": "Features erklärt",
"probleme": "Technische Probleme",
}
articles = _load_active_help_articles()
# Gruppieren nach Kategorie, Reihenfolge aus KAT_LABEL
by_kat: dict[str, list] = {}
for a in articles:
by_kat.setdefault(a["kategorie"], []).append(a)
kat_order = [k for k in KAT_LABEL.keys() if k in by_kat] + [
k for k in by_kat.keys() if k not in KAT_LABEL
]
sections_html = ""
for kat in kat_order:
label = KAT_LABEL.get(kat, kat.replace("_", " ").title())
items = "".join(
f'<details><summary>{_html.escape(a["frage"])}</summary>'
f'<div class="answer">{_html.escape(a["antwort"]).replace(chr(10), "<br>")}</div></details>'
for a in by_kat[kat]
)
sections_html += f'<section><h2>{_html.escape(label)}</h2>{items}</section>'
html = f"""<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hilfe & FAQ Ban Yaro</title>
<meta name="description" content="Antworten zu Ban Yaro und Ban Yaro Go: Installation, Standort, Account, Features.">
<link rel="icon" href="/icons/icon-180.png">
<style>
:root {{
--c-bg: #fbfaf6;
--c-text: #1c1917;
--c-text-sec: #57534e;
--c-primary: #C4843A;
--c-border: #e7e5e0;
--c-card: #fff;
}}
@media (prefers-color-scheme: dark) {{
:root {{
--c-bg: #0c0a09; --c-text: #f5f5f4; --c-text-sec: #a8a29e;
--c-border: #292524; --c-card: #1c1917;
}}
}}
* {{ box-sizing: border-box; }}
body {{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--c-bg); color: var(--c-text);
max-width: 760px; margin: 0 auto;
padding: 2rem 1.25rem 4rem; line-height: 1.55;
}}
a {{ color: var(--c-primary); text-decoration: none; }}
a:hover {{ text-decoration: underline; }}
h1 {{ font-size: 1.8rem; margin: 0 0 .25rem; font-weight: 800; }}
.lead {{ color: var(--c-text-sec); margin: 0 0 2rem; }}
section {{ margin-bottom: 2rem; }}
h2 {{ font-size: 1.1rem; margin: 0 0 .75rem;
color: var(--c-text); font-weight: 700;
padding-bottom: .25rem; border-bottom: 1px solid var(--c-border); }}
details {{
background: var(--c-card);
border: 1px solid var(--c-border);
border-radius: 12px;
margin-bottom: .6rem;
padding: 0;
}}
details summary {{
cursor: pointer;
padding: .9rem 1rem;
font-weight: 600;
list-style: none;
position: relative;
padding-right: 2.5rem;
}}
details summary::-webkit-details-marker {{ display: none; }}
details summary::after {{
content: "+";
position: absolute; right: 1rem; top: 50%;
transform: translateY(-50%);
font-weight: 400; color: var(--c-primary);
font-size: 1.4rem; transition: transform .15s;
}}
details[open] summary::after {{ content: ""; }}
.answer {{
padding: 0 1rem 1rem;
color: var(--c-text-sec);
}}
.contact {{
background: var(--c-card);
border: 1px solid var(--c-border);
border-radius: 12px;
padding: 1.25rem;
margin-top: 2.5rem;
}}
.contact h2 {{ border-bottom: none; padding-bottom: 0; margin-bottom: .5rem; }}
.contact p {{ margin: .25rem 0; color: var(--c-text-sec); }}
nav.top {{ margin-bottom: 1.5rem; }}
</style>
</head>
<body>
<nav class="top"><a href="/"> banyaro.app</a></nav>
<h1>Hilfe &amp; FAQ</h1>
<p class="lead">
Antworten zu Ban Yaro und Ban Yaro Go der nativen iOS-App für unterwegs.
</p>
{sections_html}
<div class="contact">
<h2>Direkt-Kontakt</h2>
<p>Wenn du hier nichts findest, schreib uns:</p>
<p><a href="mailto:support@banyaro.app">support@banyaro.app</a></p>
<p style="font-size:.85rem;margin-top:1rem">
Mehr Hilfe gibt es nach dem Anmelden im Suchbereich von
<a href="/#hilfe">banyaro.app/#hilfe</a>.
</p>
</div>
<p style="text-align:center;margin-top:3rem;font-size:.85rem;color:var(--c-text-sec)">
<a href="/impressum">Impressum</a> · <a href="/datenschutz">Datenschutz</a> · <a href="/agb">AGB</a>
</p>
</body>
</html>"""
return HTMLResponse(content=html, headers={"Cache-Control": "max-age=600"})
@app.get("/konto-loeschen")
async def konto_loeschen():
from fastapi.responses import HTMLResponse

View file

@ -86,14 +86,14 @@
<title>Ban Yaro</title>
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
<script src="/js/boot-early.js?v=1140"></script>
<script src="/js/boot-early.js?v=1141"></script>
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
<link rel="stylesheet" href="/css/design-system.css?v=1140">
<link rel="stylesheet" href="/css/layout.css?v=1140">
<link rel="stylesheet" href="/css/components.css?v=1140">
<link rel="stylesheet" href="/css/utilities.css?v=1140">
<link rel="stylesheet" href="/css/lists.css?v=1140">
<link rel="stylesheet" href="/css/design-system.css?v=1141">
<link rel="stylesheet" href="/css/layout.css?v=1141">
<link rel="stylesheet" href="/css/components.css?v=1141">
<link rel="stylesheet" href="/css/utilities.css?v=1141">
<link rel="stylesheet" href="/css/lists.css?v=1141">
</head>
<body>
@ -617,11 +617,11 @@
<div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=1140"></script>
<script src="/js/ui.js?v=1140"></script>
<script src="/js/app.js?v=1140"></script>
<script src="/js/worlds.js?v=1140"></script>
<script src="/js/offline-indicator.js?v=1140"></script>
<script src="/js/api.js?v=1141"></script>
<script src="/js/ui.js?v=1141"></script>
<script src="/js/app.js?v=1141"></script>
<script src="/js/worlds.js?v=1141"></script>
<script src="/js/offline-indicator.js?v=1141"></script>
<!-- Feature-Seiten werden lazy geladen -->
@ -631,7 +631,7 @@
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
<script src="/js/boot.js?v=1140"></script>
<script src="/js/boot.js?v=1141"></script>
</body>

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '1140'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '1141'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
window.APP_VERSION = APP_VERSION;

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<script src="/js/landing-init.js?v=1140"></script>
<script src="/js/landing-init.js?v=1141"></script>
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, ohne App Store.">
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">

View file

@ -4,7 +4,7 @@
============================================================ */
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
const VER = '1140';
const VER = '1141';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten