replace dev section with dev mode toggle, seed/delete mock data on toggle, bump to 2026.03.10.1
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "abgeordnetenwatch-pwa",
|
||||
"version": "2026.03.10",
|
||||
"version": "2026.03.10.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -10,6 +10,7 @@ import { fetchTopics } from "@/shared/lib/aw-api"
|
||||
import {
|
||||
APP_VERSION,
|
||||
BACKEND_URL,
|
||||
STORAGE_KEYS,
|
||||
VAPID_PUBLIC_KEY,
|
||||
} from "@/shared/lib/constants"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
@@ -52,7 +53,9 @@ export function SettingsPage() {
|
||||
string | null
|
||||
>(null)
|
||||
const [devReload, setDevReload] = useState<string | null>(null)
|
||||
const [devSeedMock, setDevSeedMock] = useState<string | null>(null)
|
||||
const [devMode, setDevMode] = useState(
|
||||
() => localStorage.getItem(STORAGE_KEYS.devMode) === "true",
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
loadCachedResult(db).then((cached) => {
|
||||
@@ -247,227 +250,218 @@ export function SettingsPage() {
|
||||
{deviceId}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<span className="text-sm" id="dev-mode-label">
|
||||
Entwicklermodus
|
||||
</span>
|
||||
<Switch
|
||||
checked={devMode}
|
||||
aria-labelledby="dev-mode-label"
|
||||
onCheckedChange={async (checked) => {
|
||||
setDevMode(checked)
|
||||
localStorage.setItem(STORAGE_KEYS.devMode, String(checked))
|
||||
try {
|
||||
await fetch(`${BACKEND_URL}/legislation/seed-mock`, {
|
||||
method: checked ? "POST" : "DELETE",
|
||||
})
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
{/* --- Developer --- */}
|
||||
<section>
|
||||
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||
Entwickler
|
||||
</h2>
|
||||
<Card className="py-0 gap-0">
|
||||
<CardContent className="p-0 divide-y divide-border">
|
||||
<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"}`}
|
||||
{/* --- Developer (visible only in dev mode) --- */}
|
||||
{devMode && (
|
||||
<section>
|
||||
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||
Entwickler
|
||||
</h2>
|
||||
<Card className="py-0 gap-0">
|
||||
<CardContent className="p-0 divide-y divide-border">
|
||||
<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
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
setDevHealth(null)
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_URL}/health`)
|
||||
setDevHealth(res.ok ? "ok" : `${res.status}`)
|
||||
} catch (e) {
|
||||
setDevHealth(String(e))
|
||||
}
|
||||
}}
|
||||
>
|
||||
{devHealth}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
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>
|
||||
Prüfen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<span className="text-sm">Test-Push</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{devPush && (
|
||||
<span
|
||||
className={`text-xs ${devPush === "ok" ? "text-green-600" : "text-destructive"}`}
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<span className="text-sm">Test-Push</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{devPush && (
|
||||
<span
|
||||
className={`text-xs ${devPush === "ok" ? "text-green-600" : "text-destructive"}`}
|
||||
>
|
||||
{devPush}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
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))
|
||||
}
|
||||
}}
|
||||
>
|
||||
{devPush}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
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>
|
||||
Senden
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
size="sm"
|
||||
variant="outline"
|
||||
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="flex items-center justify-between px-4 py-3">
|
||||
<span className="text-sm">Allen Themen entfolgen</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{devUnfollowTopics && (
|
||||
<span className="text-xs text-green-600">
|
||||
{devUnfollowTopics}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
setDevUnfollowTopics(null)
|
||||
await unfollowAllTopics()
|
||||
setDevUnfollowTopics("ok")
|
||||
}}
|
||||
>
|
||||
Entfolgen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
setDevPoliticians(null)
|
||||
const cached = await loadCachedResult(db)
|
||||
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>
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<span className="text-sm">Allen Abgeordneten entfolgen</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{devUnfollowPoliticians && (
|
||||
<span className="text-xs text-green-600">
|
||||
{devUnfollowPoliticians}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
setDevUnfollowPoliticians(null)
|
||||
await unfollowAllPoliticians()
|
||||
setDevUnfollowPoliticians("ok")
|
||||
}}
|
||||
>
|
||||
Entfolgen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<span className="text-sm">Testdaten laden</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{devSeedMock && (
|
||||
<span
|
||||
className={`text-xs ${devSeedMock.startsWith("ok") ? "text-green-600" : "text-destructive"}`}
|
||||
<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
|
||||
size="sm"
|
||||
variant="outline"
|
||||
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))
|
||||
}
|
||||
}}
|
||||
>
|
||||
{devSeedMock}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
setDevSeedMock(null)
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${BACKEND_URL}/legislation/seed-mock`,
|
||||
{ method: "POST" },
|
||||
)
|
||||
if (!res.ok) {
|
||||
setDevSeedMock(`${res.status}`)
|
||||
Folgen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<span className="text-sm">Allen Themen entfolgen</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{devUnfollowTopics && (
|
||||
<span className="text-xs text-green-600">
|
||||
{devUnfollowTopics}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
setDevUnfollowTopics(null)
|
||||
await unfollowAllTopics()
|
||||
setDevUnfollowTopics("ok")
|
||||
}}
|
||||
>
|
||||
Entfolgen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
setDevPoliticians(null)
|
||||
const cached = await loadCachedResult(db)
|
||||
if (!cached || cached.mandates.length === 0) {
|
||||
setDevPoliticians("Kein Standort-Cache")
|
||||
return
|
||||
}
|
||||
const data = await res.json()
|
||||
setDevSeedMock(`ok (${data.count})`)
|
||||
} catch (e) {
|
||||
setDevSeedMock(String(e))
|
||||
}
|
||||
}}
|
||||
>
|
||||
Laden
|
||||
</Button>
|
||||
for (const m of cached.mandates) {
|
||||
follow(
|
||||
"politician",
|
||||
m.politician.id,
|
||||
m.politician.label,
|
||||
)
|
||||
}
|
||||
setDevPoliticians(`${cached.mandates.length}`)
|
||||
}}
|
||||
>
|
||||
Folgen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<span className="text-sm">Abgeordnete neu laden</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{devReload && (
|
||||
<span className="text-xs text-green-600">{devReload}</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={loading || !hasLocation}
|
||||
onClick={() => {
|
||||
setDevReload(null)
|
||||
detect(true)
|
||||
setDevReload("gestartet")
|
||||
}}
|
||||
>
|
||||
Neu laden
|
||||
</Button>
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<span className="text-sm">Allen Abgeordneten entfolgen</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{devUnfollowPoliticians && (
|
||||
<span className="text-xs text-green-600">
|
||||
{devUnfollowPoliticians}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
setDevUnfollowPoliticians(null)
|
||||
await unfollowAllPoliticians()
|
||||
setDevUnfollowPoliticians("ok")
|
||||
}}
|
||||
>
|
||||
Entfolgen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
<div className="flex items-center justify-between px-4 py-3">
|
||||
<span className="text-sm">Abgeordnete neu laden</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{devReload && (
|
||||
<span className="text-xs text-green-600">{devReload}</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={loading || !hasLocation}
|
||||
onClick={() => {
|
||||
setDevReload(null)
|
||||
detect(true)
|
||||
setDevReload("gestartet")
|
||||
}}
|
||||
>
|
||||
Neu laden
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const APP_VERSION = "2026.03.10"
|
||||
export const APP_VERSION = "2026.03.10.1"
|
||||
|
||||
export const AW_API_BASE = "https://www.abgeordnetenwatch.de/api/v2"
|
||||
export const AW_API_TIMEOUT_MS = 20_000
|
||||
@@ -20,4 +20,5 @@ export const STORAGE_KEYS = {
|
||||
follows: "agw_follows",
|
||||
geoCache: "agw_geo_cache",
|
||||
pushEnabled: "agw_push_enabled",
|
||||
devMode: "agw_dev_mode",
|
||||
} as const
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Hono } from "hono"
|
||||
import { castVoteSchema } from "./schema"
|
||||
import {
|
||||
castVote,
|
||||
deleteMockLegislation,
|
||||
getLegislation,
|
||||
getLegislationResults,
|
||||
getLegislationText,
|
||||
@@ -18,6 +19,11 @@ legislationRouter.post("/seed-mock", async (c) => {
|
||||
return c.json({ ok: true, count: ids.length, ids }, 201)
|
||||
})
|
||||
|
||||
legislationRouter.delete("/seed-mock", async (c) => {
|
||||
const count = await deleteMockLegislation()
|
||||
return c.json({ ok: true, deleted: count })
|
||||
})
|
||||
|
||||
legislationRouter.get("/upcoming", async (c) => {
|
||||
const items = await getUpcomingLegislation()
|
||||
return c.json(items)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { and, desc, eq } from "drizzle-orm"
|
||||
import { and, desc, eq, inArray } from "drizzle-orm"
|
||||
import { db } from "../../shared/db/client"
|
||||
import {
|
||||
legislationSummaries,
|
||||
@@ -254,3 +254,19 @@ export async function seedMockLegislation() {
|
||||
|
||||
return inserted
|
||||
}
|
||||
|
||||
const MOCK_IDS = MOCK_LEGISLATION.map((m) => m.dipVorgangsId)
|
||||
|
||||
export async function deleteMockLegislation() {
|
||||
const rows = await db
|
||||
.select({ id: legislationTexts.id })
|
||||
.from(legislationTexts)
|
||||
.where(inArray(legislationTexts.dipVorgangsId, MOCK_IDS))
|
||||
|
||||
if (rows.length === 0) return 0
|
||||
|
||||
const ids = rows.map((r) => r.id)
|
||||
await db.delete(legislationTexts).where(inArray(legislationTexts.id, ids))
|
||||
|
||||
return ids.length
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user