feat: Phase 0 – Gerüst (FastAPI+SQLite, Multi-User-Login, React-App-Shell, Deploy)

Grundgerüst für Certain Finance:
- Backend: FastAPI + SQLAlchemy + SQLite, Alembic-Migrationen (Batch-Mode),
  argon2-Passwort-Hashing, server-seitige Sitzungen im Cookie, Erst-Admin,
  Benutzerverwaltung mit Temporär-Passwort, Einstellungen je Benutzer.
- Ein Prozess, ein Port: uvicorn liefert API (/api) und gebautes SPA aus,
  inkl. SPA-Deeplink-Fallback.
- Frontend: React 18 + Vite + JSX, Design-Tokens (Light/Dark), i18n DE/EN,
  Inline-SVG-Icons (keine Emojis), Login, Passwort-ändern, Dashboard-Platzhalter,
  Benutzer- und Einstellungsseiten.
- Deploy: nativer LXC-Installer + Update-Skript (systemd), env-Vorlage, Doku.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guido
2026-08-19 18:27:42 +02:00
co-authored by Claude Opus 4.8
commit 2c795a57c8
59 changed files with 6419 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
import { createContext, useCallback, useContext, useEffect, useState } from "react";
import i18n from "./i18n";
import { api } from "./api";
import { useAuth } from "./auth";
import { applyTheme } from "./theme";
import { formatMoney as fmtMoney, formatDate as fmtDate } from "./format";
const DEFAULTS = {
base_currency: "EUR",
locale: "de",
theme: "system",
date_format: "de",
budget_start_day: "1",
};
const SettingsContext = createContext({
...DEFAULTS,
formatMoney: (cents, currency) => fmtMoney(cents, currency),
formatDate: (iso) => fmtDate(iso, "de"),
update: async () => {},
reload: async () => {},
});
export function SettingsProvider({ children }) {
const { user } = useAuth();
const [values, setValues] = useState(DEFAULTS);
// Sprache und Thema anwenden, sobald sich die Werte ändern.
useEffect(() => {
if (i18n.language !== values.locale) i18n.changeLanguage(values.locale);
}, [values.locale]);
useEffect(() => {
applyTheme(values.theme);
}, [values.theme]);
const reload = useCallback(async () => {
try {
const rows = await api.listSettings();
const map = { ...DEFAULTS };
for (const r of rows) {
if (r.value != null) map[r.key] = r.value;
}
setValues(map);
} catch {
/* Einstellungen sind optional – Standardwerte bleiben bestehen. */
}
}, []);
useEffect(() => {
if (user) reload();
else setValues(DEFAULTS); // nach Logout auf Standard zurück
}, [user, reload]);
// Eine Einstellung setzen: erst lokal (sofort sichtbar), dann speichern.
const update = useCallback(async (key, value) => {
setValues((prev) => ({ ...prev, [key]: value }));
await api.setSetting(key, value);
}, []);
const value = {
...values,
baseCurrency: values.base_currency,
formatMoney: (cents, currency) => fmtMoney(cents, currency ?? values.base_currency),
formatDate: (iso) => fmtDate(iso, values.locale),
update,
reload,
};
return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>;
}
export function useSettings() {
return useContext(SettingsContext);
}