add feed caching, developer settings section, fix deploy toolchain

- cache feed items in localStorage, merge fresh with cached, auto-refresh hourly
- add refresh bar with timestamp, spinning icon during background refresh
- add developer section: backend health check, test push, mass-follow buttons
- add POST /push/test endpoint for test notifications
- activate mise in deploy.sh so bun is always on $PATH

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 08:17:51 +01:00
parent 4e3aa682ac
commit 6c58b59031
4 changed files with 295 additions and 38 deletions
@@ -2,9 +2,11 @@ import { Button } from "@/shared/components/ui/button"
import { Card, CardContent } from "@/shared/components/ui/card"
import { Switch } from "@/shared/components/ui/switch"
import { useDeviceId } from "@/shared/hooks/use-device-id"
import { useFollows } from "@/shared/hooks/use-follows"
import { usePush } from "@/shared/hooks/use-push"
import { usePwaUpdate } from "@/shared/hooks/use-pwa-update"
import { VAPID_PUBLIC_KEY } from "@/shared/lib/constants"
import { fetchTopics } from "@/shared/lib/aw-api"
import { BACKEND_URL, VAPID_PUBLIC_KEY } from "@/shared/lib/constants"
import { useCallback, useEffect, useState } from "react"
import { type GeoResult, clearGeoCache, detectFromCoords, loadCachedResult } from "../../location/lib/geo"
import { NotificationGuide } from "./notification-guide"
@@ -31,11 +33,16 @@ export function SettingsPage() {
const deviceId = useDeviceId()
const { needRefresh, checkForUpdate, applyUpdate } = usePwaUpdate()
const push = usePush()
const { follow } = useFollows()
const [checking, setChecking] = useState(false)
const [loading, setLoading] = useState(false)
const [result, setResult] = useState<GeoResult | null>(null)
const [errorMsg, setErrorMsg] = useState<string | null>(null)
const [showGuide, setShowGuide] = useState(false)
const [devHealth, setDevHealth] = useState<string | null>(null)
const [devPush, setDevPush] = useState<string | null>(null)
const [devTopics, setDevTopics] = useState<string | null>(null)
const [devPoliticians, setDevPoliticians] = useState<string | null>(null)
useEffect(() => {
const cached = loadCachedResult()
@@ -286,6 +293,128 @@ export function SettingsPage() {
</CardContent>
</Card>
</section>
{/* --- Developer --- */}
<section>
<h2 className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-2 px-1">Entwickler</h2>
<Card>
<CardContent className="p-0">
{/* Backend Health */}
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm">Backend Health</span>
<div className="flex items-center gap-2">
{devHealth && (
<span className={`text-xs ${devHealth === "ok" ? "text-green-600" : "text-destructive"}`}>
{devHealth}
</span>
)}
<Button
variant="outline"
size="xs"
onClick={async () => {
setDevHealth(null)
try {
const res = await fetch(`${BACKEND_URL}/health`)
setDevHealth(res.ok ? "ok" : `${res.status}`)
} catch (e) {
setDevHealth(String(e))
}
}}
>
Prüfen
</Button>
</div>
</div>
<div className="border-t border-border mx-4" />
{/* Test Push */}
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm">Test-Benachrichtigung</span>
<div className="flex items-center gap-2">
{devPush && (
<span className={`text-xs ${devPush === "ok" ? "text-green-600" : "text-destructive"}`}>
{devPush}
</span>
)}
<Button
variant="outline"
size="xs"
onClick={async () => {
setDevPush(null)
try {
const res = await fetch(`${BACKEND_URL}/push/test`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ device_id: deviceId }),
})
setDevPush(res.ok ? "ok" : `${res.status}`)
} catch (e) {
setDevPush(String(e))
}
}}
>
Senden
</Button>
</div>
</div>
<div className="border-t border-border mx-4" />
{/* Follow all topics */}
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm">Alle Themen folgen</span>
<div className="flex items-center gap-2">
{devTopics && <span className="text-xs text-green-600">{devTopics}</span>}
<Button
variant="outline"
size="xs"
onClick={async () => {
setDevTopics(null)
try {
const topics = await fetchTopics()
for (const t of topics) follow("topic", t.id, t.label)
setDevTopics(`${topics.length}`)
} catch (e) {
setDevTopics(String(e))
}
}}
>
Folgen
</Button>
</div>
</div>
<div className="border-t border-border mx-4" />
{/* Follow all politicians */}
<div className="flex items-center justify-between px-4 py-3">
<span className="text-sm">Alle Abgeordnete folgen</span>
<div className="flex items-center gap-2">
{devPoliticians && <span className="text-xs text-green-600">{devPoliticians}</span>}
<Button
variant="outline"
size="xs"
onClick={() => {
setDevPoliticians(null)
const cached = loadCachedResult()
if (!cached || cached.mandates.length === 0) {
setDevPoliticians("Kein Standort-Cache")
return
}
for (const m of cached.mandates) {
follow("politician", m.politician.id, m.politician.label)
}
setDevPoliticians(`${cached.mandates.length}`)
}}
>
Folgen
</Button>
</div>
</div>
</CardContent>
</Card>
</section>
</main>
)
}