feat: add Admin Panel with JWT auth, DB settings, and integration management
Complete admin backend with login, where all integrations (weather, news, Home Assistant, Vikunja, Unraid, MQTT) can be configured via web UI instead of ENV variables. Two-layer config: ENV seeds DB on first start, then DB is source of truth. Auto-migration system on startup. Backend: db.py shared pool, auth.py JWT, settings_service CRUD, seed_service, admin router (protected), test_connections per integration, config.py rewrite. Frontend: react-router v6, login page, admin layout with sidebar, 8 settings pages (General, Weather, News, HA, Vikunja, Unraid, MQTT, ChangePassword), shared IntegrationForm + TestButton components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
89ed0c6d0a
commit
f6a42c2dd2
40 changed files with 3487 additions and 311 deletions
80
web/src/admin/pages/WeatherSettings.tsx
Normal file
80
web/src/admin/pages/WeatherSettings.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Cloud, Loader2 } from "lucide-react";
|
||||
import PageHeader from "../components/PageHeader";
|
||||
import FormField, { TextInput } from "../components/FormField";
|
||||
import IntegrationForm from "../components/IntegrationForm";
|
||||
import { getIntegration, type Integration } from "../api";
|
||||
|
||||
export default function WeatherSettings() {
|
||||
const [integration, setIntegration] = useState<Integration | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
loadIntegration();
|
||||
}, []);
|
||||
|
||||
const loadIntegration = async () => {
|
||||
try {
|
||||
const data = await getIntegration("weather");
|
||||
setIntegration(data);
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="w-5 h-5 text-slate-400 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !integration) {
|
||||
return (
|
||||
<div>
|
||||
<PageHeader icon={Cloud} title="Wetter" description="Wetteranbieter konfigurieren" />
|
||||
<div className="glass-card p-6">
|
||||
<p className="text-sm text-red-400">{error || "Integration nicht gefunden"}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
icon={Cloud}
|
||||
title="Wetter"
|
||||
description="Standorte für die Wettervorhersage konfigurieren (wttr.in)"
|
||||
/>
|
||||
|
||||
<div className="glass-card p-6">
|
||||
<IntegrationForm integration={integration} onSaved={setIntegration}>
|
||||
{(config, setConfig) => (
|
||||
<>
|
||||
<FormField label="Primärer Standort" description="Stadt oder Koordinaten für die Hauptanzeige">
|
||||
<TextInput
|
||||
value={(config.primary_location as string) || ""}
|
||||
onChange={(v) => setConfig("primary_location", v)}
|
||||
placeholder="z.B. Berlin oder 52.52,13.405"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Sekundärer Standort" description="Zweiter Standort für den Vergleich">
|
||||
<TextInput
|
||||
value={(config.secondary_location as string) || ""}
|
||||
onChange={(v) => setConfig("secondary_location", v)}
|
||||
placeholder="z.B. München oder 48.137,11.576"
|
||||
/>
|
||||
</FormField>
|
||||
</>
|
||||
)}
|
||||
</IntegrationForm>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue