"NaN Glaeser" auf der Karte "Artikel im Blick". Die Karte reichte einen
ARTIKEL an articlePrimary() weiter, das fuer CHARGEN geschrieben ist und
die Menge unter dem Namen `quantity` sucht. Ein Artikel nennt sie
`stock` - also undefined / 190 = NaN. Neu bestandPrimary() und
bestandSecondary() in units.js als die eine Stelle, an der zwischen den
beiden Namen uebersetzt wird; sie delegieren an die vorhandenen
Formatierer, damit es nicht zwei Fassungen gibt. Steckte seit 8959814
drin.
Dazu gibt fmt() fuer nicht-endliche Zahlen jetzt einen Strich aus statt
"NaN". Dem Benutzer "NaN" hinzuschreiben ist nie richtig - der eigentliche
Fehler wird trotzdem an der Quelle behoben, nicht hier.
Die Spalte "Verbrauch" schnitt ab. Zwei beschriftete Knoepfe brauchen
rund 230 px, die Spalte hatte 190, und .datatable ist table-layout:fixed
mit overflow:hidden. Weil die Zelle rechtsbuendig ist, fiel der ANFANG
weg - sichtbar stand dort "rbrauchen". Jetzt 240 px, dazu flex-wrap in
der Zelle, damit die Knoepfe umbrechen statt abzuschneiden, wenn jemand
die Spalte von Hand schmaler zieht.
Die LETZTE Spalte liess sich nicht verbreitern. Ihr Anfasser sitzt exakt
am rechten Rand des Rollbereichs (gemessen: 0 px Luft), und nach rechts
ist dort Schluss - der Zeiger kann nicht weiter, also kam nie ein
positiver Abstand zum Startpunkt zustande. Der Resize rechnet jetzt mit
der BEWEGUNG seit dem letzten Schritt statt mit dem Abstand zum Start,
und in der Randzone waechst die Spalte von selbst weiter, solange die
Taste haelt; ein Taktgeber treibt das an, denn ein stehender Zeiger
meldet keine Bewegung. Die Tabelle rollt dabei mit, damit der Anfasser
unter dem Zeiger bleibt.
"3 Glas" statt "3 Glaeser" in der Chargenliste, waehrend die Uebersicht
"3 Glaeser" schrieb. Die Mehrzahlformen liegen in einer Modulvariablen,
weil auch Nicht-Komponenten sie brauchen - eine Aenderung daran loest
aber kein Neuzeichnen aus. Wer seine Liste zeichnete, bevor
/package-types zurueck war, blieb bei der Einzahl. Ein Zaehler im
Einstellungs-Kontext zieht die Formatierer jetzt nach.
Von Hand geprueft: Karte zeigt "3 Glaeser / 570 g"; beide Knoepfe stehen
vollstaendig in der Spalte; die letzte Spalte waechst beim Halten am
Rand von 240 auf ueber 470 und bleibt beim Loslassen stehen, waehrend
eine mittlere Spalte weiterhin exakt der Ziehstrecke folgt (170 -> 230);
Chargenliste schreibt "3 Glaeser".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Vorrania – Selfhostbare Vorrats- und Gegenstandsverwaltung
Ein selbst gehosteter Dienst, um sowohl deinen Lebensmittelvorrat als auch allgemeine Gegenstände (Haushalt, Elektronik, Kleidung, Verbrauchsmaterial …) zu verwalten: Ein-/Auslagern per Barcode, Chargen mit MHD, Einzelstücke mit eigener UID/QR, Mindestbestände, automatische Einkaufsliste, verschachtelte Lagerorte, Gruppen, Kategorien und Dashboards – mit Web-UI und nativer iOS-App, die beide dieselbe REST-API nutzen.
Was Vorrania verwaltet
Lebensmittel laufen als Chargen mit Mindesthaltbarkeitsdatum. Beim Auslagern wird automatisch die zuerst ablaufende Charge entnommen (FEFO).
Gegenstände kennen drei Arten:
- Menge je Lagerort – reines Zählen (z. B. Unterhosen, Batterien).
- Einzelstücke – jedes Exemplar mit eigener UID/QR, Kaufdatum, Garantie, Bezugsquelle und Beleg (z. B. Powerbank, Kamera).
- Verbrauchsgegenstand – wie ein Lebensmittel als Charge mit Menge und Einheit geführt (z. B. Sonnencreme in ml), nur ohne eindeutigen Code.
Features
- Barcode-Lookup über Open Food Facts und Open Products Facts mit lokalem Fallback; unbekannte Produkte selbst anlegen, Bild wird automatisch geholt.
- Chargen mit MHD & FEFO, Mengen in Packungen oder Teilmengen (Basiseinheit Stück / Gramm / Milliliter plus Packungsgröße/Gebinde).
- Lagerorte beliebig verschachtelt (Keller → Regal 2 → Fach A), mit QR-Etiketten, Inhaltsansicht („was liegt hier?“), Umlagern und Entfernen mit Grund. Chargen und Einzelstücke sind einem Lagerort zugeordnet.
- Einzelstücke verleihen mit Name und Grund als Freitext und optionalem Rückgabedatum – auch direkt nach dem Scannen des QR/NFC-Tags. Der Lagerort bleibt stehen (dorthin gehört das Stück zurück), als verliehen zählt es aber nicht mehr zum Bestand. Zurückgegebene Verleihe bleiben als Verlauf erhalten; überfällige Rückgaben sind in Liste und Dashboard rot.
- Kategorien verschachtelt und pro Typ (Lebensmittel/Gegenstand); für Gegenstände mit eigenen Feldern (z. B. Kapazität, Größe, Farbe).
- Gruppen rechnen Bestände mehrerer Marken/Artikel zusammen (z. B. „5 kg Mehl“) mit eigenem Mindestbestand und Einheit.
- Mindestbestände je Produkt und Gruppe – gesamt und je Lagerort – mit zentraler Übersichtsseite, daraus die automatische Einkaufsliste und Ablaufwarnung für bald ablaufende Chargen.
- Übergreifende Listen für Produkte (getrennt Lebensmittel/Gegenstände), Einzelstücke und Chargen – filter-, sortier- und spaltenkonfigurierbar; bei Chargen lässt sich der Lagerort mehrerer auf einmal setzen.
- Dashboards mit konfigurierbaren Karten und Diagrammen, Verlauf (wer hat wann was ein-/ausgelagert), Stammdaten (Einheiten, Gebinde, Shops) und Import/Export.
- Mehrbenutzer mit Rollen: Admin verwaltet Stammdaten und Benutzer, Nutzer lagert nur ein/aus und sieht Bestände; alle Bewegungen werden protokolliert.
- Single Sign-on (optional) über OpenID Connect, z. B. gegen ein selbst gehostetes Authentik. Der Passwort-Login bleibt daneben bestehen – siehe Single Sign-on.
- Sitzungsdauer einstellbar (Einstellungen → Anmeldung): 7 Tage bis 1 Jahr oder unbegrenzt, für Website und App gemeinsam. Die Frist zählt ab dem letzten Besuch, nicht ab dem Anmelden.
In der iOS-App zusätzlich
- Scannen von Barcode, MHD/Datum per Texterkennung (auch nur Tag+Monat ohne Jahr bei Frischware), Lagerort-QR und Name/Marke per Text-Scan.
- Verleihen direkt nach dem Scannen – QR, eingenähtes NFC-Tag oder angetipptes Tag landen alle im Einzelstück, wo Herausgeben und Zurücknehmen gleich oben stehen.
- Foto direkt beim Anlegen (Kamera/Galerie), Push-Benachrichtigungen bei ablaufenden Produkten, lokale Dashboards und Home-Screen-Schnellaktionen.
Details zur App: ios/README.md.
Architektur
| Teil | Technik |
|---|---|
| Backend | Python · FastAPI · SQLAlchemy |
| Anmeldung | Passwort (JWT) und optional OpenID Connect |
| DB | PostgreSQL (SQLite für Tests/lokal) |
| Web-UI | React · Vite (via nginx ausgeliefert) |
| iOS | Native SwiftUI-App (VisionKit-Scanner) |
| Deploy | Docker Compose · install.sh |
backend/ FastAPI-App + Tests
web/ React/Vite SPA
ios/ Native SwiftUI-App (XcodeGen-Projekt)
deploy/ docker-compose.yml, install.sh, update.sh, .env.example
docs/ Roadmap
Das Schema wird beim Start per idempotenter ALTER TABLE-Migration in
backend/app/main.py nachgezogen (kein Alembic).
Installation (selfhosted, Linux)
Voraussetzung: eine Linux-Maschine (Server, NAS, Raspberry Pi …). Docker wird bei Bedarf automatisch installiert.
git clone https://git.scarriffle.com/Scarriffle/vorrania.git
cd vorrania/deploy
chmod +x install.sh
./install.sh # interaktiv – fragt Port & Admin-Passwort
# oder vollautomatisch mit generiertem Admin-Passwort:
# ./install.sh --yes
Ist das Gitea-Repo privat, fragt
git clonenach Benutzername + Access-Token. Fallsgitfehlt:apt update && apt install -y git.
Nach dem Start:
- Web-UI:
http://<server-ip>:8080(Port konfigurierbar) - Anmeldung mit dem im Installer angezeigten Admin-Benutzer/Passwort.
Die iOS-App wird mit derselben Adresse verbunden (http://<server-ip>:8080).
Single Sign-on (optional)
Neben dem Passwort-Login kann sich Vorrania an einen OpenID-Connect-Dienst hängen (getestet mit Authentik). Ohne Konfiguration ändert sich nichts: Es gibt keinen SSO-Knopf, und der Passwort-Login bleibt der einzige Weg.
1. In Authentik zwei Anwendungen anlegen – Web-UI und App brauchen unterschiedliche Client-Typen, und ein Provider kann nur einer davon sein:
| Für | Client-Typ | Redirect-URI |
|---|---|---|
| Web-UI | Confidential (mit Secret) | https://<deine-adresse>/api/auth/oidc/authentik/callback |
| iOS-App | Public (ohne Secret, PKCE) | vorrania://oauth/callback |
Drei Punkte, an denen es sonst klemmt:
- Das
/apiin der Web-Adresse ist kein Versehen: Es entsteht im mitgelieferten nginx und gehört deshalb in die bei Authentik hinterlegte Adresse. - Signing Key setzen (Provider ▸ Advanced protocol settings). Ohne ihn signiert Authentik mit HS256, und Vorrania prüft gegen den öffentlichen Schlüsselsatz – das Token würde abgelehnt.
- Jede Anwendung hat in Authentik ihren eigenen Issuer (aus dem
Application-Slug). Deshalb bekommt der App-Client eine eigene
Discovery-Adresse:
OIDC_AUTHENTIK_NATIVE_DISCOVERY_URL.
Damit die App ihre Anmeldung im Hintergrund erneuern kann, muss der öffentliche
Client offline_access ausliefern dürfen (in Authentik seit 2024.2 ein eigenes
Scope-Mapping) – sonst kommt kein Refresh-Token zurück.
2. deploy/.env ergänzen (vollständige Vorlage in deploy/.env.example):
PUBLIC_BASE_URL=https://vorrania.example.com
OIDC_PROVIDERS=authentik
OIDC_AUTHENTIK_DISPLAY_NAME=Authentik
OIDC_AUTHENTIK_DISCOVERY_URL=https://authentik.example.com/application/o/vorrania/.well-known/openid-configuration
OIDC_AUTHENTIK_CLIENT_ID=...
OIDC_AUTHENTIK_CLIENT_SECRET=...
OIDC_AUTHENTIK_NATIVE_CLIENT_IDS=vorrania-ios
OIDC_AUTHENTIK_NATIVE_DISCOVERY_URL=https://authentik.example.com/application/o/vorrania-ios/.well-known/openid-configuration
Danach ./update.sh (oder docker compose --env-file .env up -d).
Ob es geklappt hat, verrät curl http://<server>:8080/api/auth/oidc/providers.
3. Bestehende Konten verknüpfen. Beim ersten SSO-Login zu einer noch unbekannten Kennung fragt Vorrania nach, statt still ein zweites Konto anzulegen:
Die Anmeldung über Authentik hat geklappt – zu diesem Zugang gibt es hier aber noch kein Konto. [ Bestehendes Konto verknüpfen ] oder [ Neues Konto anlegen ]
Das Verknüpfen verlangt einmalig Benutzername und Passwort des bestehenden Kontos. Dieser Nachweis ist der Grund, warum der Anmeldeweg selbst nie fragt „welches Konto?" – ohne ihn könnte sich jeder mit einem beliebigen Konto beim Anmeldedienst an ein fremdes Vorrania-Konto hängen. Rolle und Verlauf bleiben dabei erhalten.
Zwei Wege führen daran vorbei, weil sie das Konto schon vorher finden:
- E-Mail-Adresse unter Benutzer eintragen – dieselbe wie in Authentik. Verknüpft wird nur bei bestätigter Adresse.
- Vorab verknüpfen: angemeldet bleiben, unten in der Seitenleiste auf den eigenen Namen → Mein Konto → Verknüpfen. Dort lässt sich eine Verknüpfung auch wieder lösen – außer es wäre die letzte Anmeldemöglichkeit.
Mit OIDC_AUTHENTIK_AUTO_CREATE=false entfällt der Knopf „Neues Konto anlegen";
dann bleibt nur das Verknüpfen mit einem Konto, das der Admin angelegt hat.
Die App zeigt diesen Dialog nicht – dort gilt weiterhin: bekannte Kennung oder
passende Adresse, sonst je nach AUTO_CREATE ein neues Konto oder eine Absage.
Einmal im Browser verknüpft, funktioniert die App danach ohne Weiteres.
Weitere Anbieter sind reine Konfiguration: Kürzel in OIDC_PROVIDERS ergänzen
und einen zweiten Block mit dessen Präfix anlegen (OIDC_<KÜRZEL>_…) – am Code
ändert sich dafür nichts.
Was dabei bewusst so gebaut ist
- Der Anmeldedienst ist nur für die Anmeldung zuständig. Danach stellt Vorrania sein eigenes Token aus, wie beim Passwort-Login – Rollen, API-Tokens und alle Endpunkte bleiben unverändert.
- Rollen kommen nie aus dem Anbieter. Neu angelegte SSO-Konten sind immer Benutzer; Admin vergibt ein Admin von Hand.
- Der Benutzername wird einmal beim Anlegen übernommen und gehört danach Vorrania – ein späterer Namenswechsel beim Anmeldedienst schlägt nicht durch. Ändern lässt er sich jederzeit unter Benutzer: Sitzungen, Verlauf und API-Tokens hängen an der Benutzer-ID, nicht am Namen.
- Automatisch verknüpft wird nur bei bestätigter E-Mail-Adresse. Sonst könnte ein Konto im Anmeldedienst mit frei gewählter Adresse ein fremdes Vorrania-Konto übernehmen. Beim Verknüpfen über Mein Konto entfällt das, weil dort die eigene Sitzung bürgt – eine bereits anderweitig vergebene Kennung wird aber auch dort abgelehnt.
- Die App meldet sich ausschließlich im System-Browser an (AppAuth mit PKCE), nie in einem eingebetteten WebView, und sendet nie ein Client-Secret.
Aktualisieren
cd vorrania/deploy
./update.sh # holt den neuesten Stand aus Git und baut neu
./update.sh --prune # zusätzlich alte Images aufräumen
./update.sh --no-pull # nur neu bauen, ohne git pull
Das Skript ist von überall aufrufbar, nutzt die vorhandene deploy/.env und
prüft am Ende, ob das Backend wieder erreichbar ist.
Verwaltung:
cd deploy
docker compose logs -f # Logs ansehen
docker compose down # stoppen
docker compose up -d # starten
Die Konfiguration liegt in deploy/.env (Passwörter, Port, JWT-Secret). Für den
Produktivbetrieb bitte hinter einen HTTPS-Reverse-Proxy (z. B. Caddy/Traefik) stellen.
Entwicklung (ohne Docker)
Backend:
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# lokale SQLite-DB nutzen:
export DATABASE_URL="sqlite:///./vorrania.db"
uvicorn app.main:app --reload
API-Doku dann unter http://localhost:8000/docs.
Web:
cd web
npm install
npm run dev # http://localhost:5173, proxyt /api → localhost:8000
iOS: siehe ios/README.md (XcodeGen-Projekt, xcodegen generate).
Tests:
cd backend
pip install -r requirements.txt
DATABASE_URL="sqlite://" pytest
Weiteres
Fahrplan und Ideen: docs/ROADMAP.md.