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
+67
View File
@@ -0,0 +1,67 @@
import { useTranslation } from "react-i18next";
import { useSettings } from "../settings";
import { useToast } from "../toast";
export default function Settings() {
const { t } = useTranslation();
const s = useSettings();
const toast = useToast();
async function set(key, value) {
try {
await s.update(key, value);
toast(t("settings.saved"));
} catch (err) {
toast(err.message, "error");
}
}
return (
<div>
<h1>{t("settings.title")}</h1>
<div className="card" style={{ maxWidth: 520 }}>
<label>
<span>{t("settings.baseCurrency")}</span>
<select value={s.base_currency} onChange={(e) => set("base_currency", e.target.value)}>
<option value="EUR">Euro (EUR)</option>
<option value="CHF">Schweizer Franken (CHF)</option>
</select>
</label>
<p className="muted small" style={{ marginTop: -8 }}>{t("settings.baseCurrencyHint")}</p>
<label>
<span>{t("settings.locale")}</span>
<select value={s.locale} onChange={(e) => set("locale", e.target.value)}>
<option value="de">{t("settings.languageDe")}</option>
<option value="en">{t("settings.languageEn")}</option>
</select>
</label>
<label>
<span>{t("settings.theme")}</span>
<select value={s.theme} onChange={(e) => set("theme", e.target.value)}>
<option value="system">{t("settings.themeSystem")}</option>
<option value="light">{t("settings.themeLight")}</option>
<option value="dark">{t("settings.themeDark")}</option>
</select>
</label>
<label>
<span>{t("settings.budgetStartDay")}</span>
<input
type="number"
min="1"
max="31"
value={s.budget_start_day}
onChange={(e) => {
const day = Math.max(1, Math.min(31, Number(e.target.value) || 1));
set("budget_start_day", String(day));
}}
/>
</label>
<p className="muted small" style={{ marginTop: -8 }}>{t("settings.budgetStartDayHint")}</p>
</div>
</div>
);
}