Commit Graph
303 Commits
Author SHA1 Message Date
ScarriffleandClaude Opus 5 a3fd9819c4 Rueckgabe sichtbar machen, Zubehoerteile am Einzelstueck
RUECKGABE
Der Knopf war da, nur nicht zu finden: ein 16-px-Symbol in einer Spalte ohne
Ueberschrift, 52 px breit, ganz rechts in einer horizontal scrollenden Tabelle.
Beschriftet war er allein ueber title= - ein Tooltip, den auf einem Touchgeraet
nie jemand sieht.

Jetzt traegt er Text ("Zuruecknehmen"), die Spalte heisst "Verleih" und ist
breit genug, und beide Knoepfe haben ein aria-label. Dazu eine neue Seite
"Verliehen" in der Seitenleiste: alles Draussene an einem Ort, Rueckgabe direkt
aus der Liste. Bisher gab es diese Uebersicht nur als zwei Dashboard-Karten, und
die stehen nicht im Standard-Layout - wer sie nicht selbst hinzufuegte, hatte
gar keine.

In der App fuehrte der einzige Weg ueber das Bearbeiten-Formular des Stuecks;
jetzt geht es per Wischgeste und Kontextmenue direkt aus der Liste. Der
Verleih-Filter dort sass hinter einem nackten Symbol und ist beschriftet.

Zurueckgenommen wird nur noch nach Rueckfrage, in Web und App.

ZUBEHOER
Ein Netzteil gehoert zu einem Laptop: items.parent_item_id, genau ein Geraet je
Teil, beliebig viele Teile je Geraet. Eine Spalte genuegt dafuer, es braucht
keine Verknuepfungstabelle. ON DELETE SET NULL, damit das Netzteil den Laptop
ueberlebt statt mitgeloescht zu werden - und weil SQLite Fremdschluessel nicht
erzwingt, loest der Router die Zuordnung beim Loeschen zusaetzlich selbst.

Ringe (A traegt B, B traegt A) weist die Zuordnung mit 409 ab; die Elternkette
wird dafuer hochgelaufen, wie beim Kategoriebaum.

Die Beziehung laeuft ueber eigene Endpunkte und bewusst NICHT ueber ItemUpdate:
dessen Schema sendet immer alle Felder mit, auch leere, damit sich Felder
loeschen lassen. Genau so gingen schon einmal Kaufpreis und Waehrung verloren,
als ein Aufrufer ein Feld vergass.

Beim Verleihen fragt die Oberflaeche, welche Teile mitgehen (vorausgewaehlt:
alles, was im Haus ist), beim Zuruecknehmen kommen sie zusammen zurueck. Ohne
das Spiegelbild kaeme das Geraet zurueck und sein Netzteil bliebe ewig
"verliehen", weil niemand mehr danach sucht. Ungefragt nimmt der Server nie
etwas mit.

In der App sind zubehoer und gehoert_zu optional dekodiert: Die App spricht mit
mehreren Serverprofilen, und ein aelterer Server schickt die Felder nicht - ein
Vorgabewert reicht dafuer nicht, Swift nimmt ihn beim Dekodieren nicht her.

12 neue Tests (320 gesamt). Der Umlauf ist ausserdem gegen ein lokales Backend
im Browser durchgespielt: verleihen mit Zubehoer, Rueckfrage, beide zusammen
zurueck.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 14:08:11 +02:00
ScarriffleandClaude Opus 5 ca4e9a2264 Einzelstueck anlegen: im Verlauf sichtbar und vom Artikel aus erreichbar
Zwei Luecken, die zusammen den Eindruck machten, ein angelegtes Einzelstueck
sei verschwunden.

Der Verlauf kannte nur das Verschwinden. remove_item schreibt seit jeher eine
Bewegung, create_items nicht - Abgaenge von Einzelstuecken tauchten also auf,
Zugaenge nie, im Verlauf wie im Dashboard. Jetzt schreibt das Anlegen je Stueck
eine in-Bewegung mit UID (oder der eigenen Notiz, falls angegeben), Lagerort und
Benutzer, spiegelbildlich zum Entfernen. Der Bestand bleibt unberuehrt: er
zaehlt bei Einzelstuecken die Zeilen in items und rechnet nicht aus Bewegungen.

Und der Weg dorthin fehlte. ProductFormView liefert den angelegten Artikel, aber
zwei der drei Aufrufer warfen ihn weg ("{ _ in showNew = false }") - nur der
Einlagern-Weg nutzte ihn. "Einzelstuecke anlegen" steckt ausschliesslich in der
Artikelansicht, die man vom Scan-Tab und aus den Listen nach dem Anlegen gar
nicht erreicht. Man musste den frischen Artikel von Hand wiederfinden.

Bei Gegenstaenden mit eigener UID je Stueck oeffnet sich jetzt direkt das
Anlege-Formular. Gesetzt wird das im onDismiss des Artikelformulars: beides im
selben Zug zu setzen verschluckt SwiftUI, weil das zweite Sheet erscheinen soll,
waehrend das erste noch verschwindet.

Der Parameter von create_items heisst dadurch user statt _; die drei Testaufrufe
sind mitgezogen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 12:12:42 +02:00
ScarriffleandClaude Opus 5 c924dffad6 Einzelstuecke: Zeile zeigt nur noch, was man im Alltag braucht
Die Tabelle trug neun Spalten, davon sechs als dauernd offene
Eingabefelder - bei einem einzigen Stueck schon eine Wand aus
Bedienelementen. Jetzt stehen in der Zeile QR, UID, Lagerort, "Verliehen
an" und die Aktionen; Kaufdatum, Garantie, Shop, Notiz, Kaufpreis, Belege
und der Verleih-Verlauf klappen je Stueck auf. Aenderbar bleibt alles,
nur nicht alles gleichzeitig sichtbar.

Dazu zwei Fehler von mir aus dem Verleih-Commit:

- Die drei Aktionssymbole standen uebereinander statt nebeneinander. Die
  Aktionszelle benutzte .field-inline, und das bricht in Tabellen bewusst
  um (styles.css) - mit dem vierten Knopf reichte der Platz nicht mehr.
  Jetzt .cell-row, das umbricht nicht.
- "ueberfaellig (vor 1 Woche)" las sich als Zeitpunkt, gemeint ist eine
  Dauer. Jetzt "7 Tage ueberfaellig".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 10:13:27 +02:00
ScarriffleandClaude Opus 5 b2dda78a7b Rueckgabe-Erinnerung: ein Meldetag ist eine Meldung
Erinnert an faellige Rueckgaben verliehener Einzelstuecke, auf dem Weg der
Ablauf-Meldungen: lokale Benachrichtigung, geplant aus GET /loans?due_in=.
Je Server einstellbar - "X vorher", am Rueckgabetag, und taeglich weiter,
solange etwas ueberfaellig ist. Ein Verleih ohne Rueckgabedatum ist nie
faellig und kommt nicht vor.

Zwei Aenderungen am bestehenden Plan waren dafuer noetig, und beide haben
denselben Grund: Der Plan muss allein stehen. Der BGAppRefreshTask ist
keine Garantie - iOS fuehrt ihn nur aus, wenn es will -, also darf die
Erinnerung nicht davon abhaengen, dass die App geoeffnet wird.

1. Alles, was an einem Tag auf einem Server anliegt, wird EINE Meldung.
   Vorher waren es zwei Arten (bald ablaufend, abgelaufen) mit je eigener
   Kennung; mit dem Verleih waeren es vier geworden. iOS erlaubt 64
   geplante Mitteilungen je App - ein Platz je Tag statt je Art ist die
   Voraussetzung dafuer, dass der Horizont ueberhaupt hineinpasst. Ein
   vergessener Verleih hat sonst mit taeglicher Meldung den ganzen Monat
   belegt und alle MHD-Meldungen verdraengt, weil die weiter in der
   Zukunft liegen und bei der Kappung zuerst wegfallen.

2. Horizont 30 statt 60 Tage. Er ist damit die Zusage, wie lange die App
   ungeoeffnet bleiben darf, ohne dass Erinnerungen ausfallen. Ein
   laengerer Horizont haette nur spaetere Tage eingeplant und dafuer die
   naechsten verdraengt. Preis: Eine MHD-Regel mit mehr als 30 Tagen
   Vorlauf greift erst, wenn das Datum in dieses Fenster rueckt.

Der LoanPlanner ist wie der ExpiryPlanner netzfrei und ohne iOS-Typen
gebaut, damit sich die Entscheidungen ohne Geraet nachrechnen lassen.
Gespeicherte Konfigurationen werden mit decodeIfPresent gelesen - eine
bestehende Einstellung bleibt nach dem Update gueltig.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 09:47:26 +02:00
ScarriffleandClaude Opus 5 aa72d8342a App: Einzelstuecke verleihen - alle Scan-Wege enden schon dort
Verleih-Abschnitt im Einzelstueck: an wen, seit wann, bis wann, dazu
Herausgeben bzw. Zuruecknehmen und der Verleih-Verlauf. Pflicht ist nur der
Name; Grund und Rueckgabedatum sind abschaltbar, weil "kurz mal
mitgenommen" nicht an einem Datum scheitern soll.

Der Abschnitt sitzt bewusst oben in ItemEditView, und zwar nicht nur der
Wichtigkeit wegen: QR ueber Nachschlagen, eingenaehtes NFC-Tag und ein
angetipptes Tag (vorrania://i/<UID>) landen alle in genau dieser Ansicht.
Damit ist "scannen und verleihen" ueberall moeglich, ohne einen eigenen
Weg dafuer zu bauen.

In der Einzelstueck-Liste steht der Verleih unter dem Stueck - ueberfaellig
in Rot - und das Filter-Menue kann auf Verliehen, Ueberfaellig oder Im Haus
eingrenzen. Wo etwas ist, war bisher immer der Lagerort; ist es verliehen,
ist der Entleiher die Antwort, und der Lagerort steht daneben.

days_overdue rechnet der Server (GET /loans, ItemOut.loan), damit App und
Web nicht getrennt rechnen und nicht auseinanderlaufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 09:36:05 +02:00
ScarriffleandClaude Opus 5 1332dd86fa Web: Einzelstuecke verleihen, auch direkt nach dem Scannen
Verleihen und Zuruecknehmen in der Einzelstueck-Tabelle eines Artikels und
in der Liste aller Einzelstuecke, dort als Spalten "Verliehen an" und
"Zurueck bis" (filter- und sortierbar). Ueberfaellig ist rot, demnaechst
faellig gelb - dieselben Zustandsfarben wie beim MHD.

Die Scan-Zielseite /i/<uid> leitete vorher nur zum Artikel weiter. Fuer
"scannen und verleihen" braucht die Aktion aber genau dort ihren Platz:
Die Seite zeigt jetzt, was man in der Hand haelt - Stueck, Artikel,
Lagerort, Verleihzustand - und bietet Verleihen bzw. Zuruecknehmen an.
Gegenstueck zur Lagerort-Seite /l/<code>, die es fuers Einlagern genauso
macht.

Anzeige und Aktionen liegen einmal in components/Verleih.jsx, weil alle
drei Stellen dasselbe brauchen. Der Knopf haengt nicht an isAdmin:
Verleihen ist eine Buchung wie Ein- und Auslagern.

Dazu zwei Dashboard-Karten: alles Verliehene und, getrennt, nur das
Ueberfaellige. Stuecke ohne Rueckgabedatum sind nie faellig und tauchen in
der zweiten Karte nicht auf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 09:30:14 +02:00
ScarriffleandClaude Opus 5 467ef3c804 Einzelstuecke verleihen: eigener Vorgang statt Lagerort-Wert
Der Lagerort bleibt beim Verleihen stehen - er sagt weiter, wohin das
Stueck nach der Rueckgabe zurueckgehoert. Ein Lagerort-Wert "verliehen"
haette diese Information vernichtet. Stattdessen eine eigene Tabelle
item_loans mit Entleiher und Grund als Freitext, Verleihdatum und einem
optionalen Rueckgabedatum: "kurz mal mitgenommen" soll nicht an einem
Datumszwang scheitern.

Zurueckgegebene Vorgaenge bleiben stehen, damit die Tabelle gleichzeitig
die Verleih-Historie je Stueck ist. Ein Teilindex auf die offenen
Vorgaenge haelt fest, dass ein Stueck nicht zweimal gleichzeitig
verliehen sein kann - wie bei den "Ueberall"-Mindestbestaenden, weil
Postgres NULLs in UNIQUE als verschieden zaehlt.

Verliehen heisst nicht im Bestand: Ein Stueck, das nicht im Haus ist,
zaehlt nirgends mehr mit - weder am Lagerort noch im Gesamtbestand.
Entschieden wird das an einer Stelle (services/loans.py::nicht_verliehen),
die alle fuenf Zaehlungen benutzen. Sonst behauptete "Was liegt hier?"
weiterhin, die Kamera liege im Regal. Folge: Faellt ein verliehenes
Stueck unter den Mindestbestand, erscheint der Artikel auf der
Einkaufsliste.

Verleihen und Zuruecknehmen darf jeder angemeldete Benutzer - es ist eine
Buchung wie Ein- und Auslagern, keine Stammdatenaenderung - und wird mit
dem Konto protokolliert. Nur das Loeschen eines Vorgangs bleibt Admins
vorbehalten, weil es Historie vernichtet.

Bewusst keine Movement-Bewegung je Verleih: Der Verlauf rechnet den
Bestand rueckwaerts aus vorzeichenbehafteten Bewegungen, ein Abgang ohne
Gegenbuchung wuerde diese Kurve verfaelschen.

GET /loans?due_in= liefert die faelligen Verleihe und ist damit schon die
Datenquelle fuer die spaetere Rueckgabe-Erinnerung der App. Oberflaeche in
Web und App fehlt noch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 09:16:47 +02:00
ScarriffleandClaude Opus 5 ba84e4ea01 Ort zuordnen: Karte zeigt, was man da in der Hand haelt
Bisher stand dort nur "Einzelstück R9D55SDC". Die UID sagt aber nichts darueber,
welches Stueck das ist - gerade bei Kleidung, wo das NFC-Tag eingenaeht und
unsichtbar ist. Unter dem Sucher lag dafuer die halbe Seite ungenutzt.

Die Karte zeigt jetzt Vorschaubild, Artikelname, Marke und die Notiz, darunter
als beschriftete Zeilen die Einzelstuecknummer und den aktuellen Lagerort. Der
Ort ist beim Zuordnen die nuetzlichste Angabe ueberhaupt: man sieht sofort, ob
das Stueck schon irgendwo liegt.

Der Block war mit 50 Zeilen zu gross fuer den ohnehin langen body und steht
jetzt als EinzelstueckKarte fuer sich.

ProductThumb bekommt dafuer einen Groessenparameter (Vorgabe 34 wie bisher, also
keine Aenderung an den Listen) und bei grossen Bildern einen passenderen
Eckenradius und ein groesseres Ersatzsymbol.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 14:05:47 +02:00
ScarriffleandClaude Opus 5 a220823882 Versionsnummern kommen aus project.yml statt dreifach auseinanderzulaufen
Die Info.plist hatte CFBundleShortVersionString und CFBundleVersion fest auf
"1.0" und "1" stehen. Damit blieb jede Aenderung an MARKETING_VERSION wirkungslos:
Xcode baute weiter 1.0, und da 1.0 bereits veroeffentlicht ist, lehnte App Store
Connect den Upload ab ("Invalid Pre-Release Train").

Beide Werte ziehen jetzt ueber $(MARKETING_VERSION) und
$(CURRENT_PROJECT_VERSION) aus den Build-Einstellungen - so, wie es die Datei
fuer EXECUTABLE_NAME, PRODUCT_NAME und den Bundle-Identifier schon tat. Gepflegt
werden sie in project.yml, die ohnehin die Quelle des Projekts ist; ein in Xcode
angeklicktes 1.1 hatte der naechste "xcodegen generate" sonst wieder verworfen.

Auf 1.1 (2) gesetzt und gegengeprueft: das gebaute Bundle meldet 1.1 / 2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 13:55:39 +02:00
ScarriffleandClaude Opus 5 140b81798c NFC: Entitlement TAG statt NDEF, Tag-Session statt NDEF-Session
Der Upload nach App Store Connect scheiterte an der Entitlement-Pruefung:
"NDEF is disallowed" und "TAG is missing in the entitlement". Apple hat den Wert
NDEF zugunsten von TAG abgeloest; mit dem SDK von iOS 26/27 wird er abgelehnt.

TAG ist der Wert fuer NFCTagReaderSession, deshalb laeuft der Lese- und
Schreibweg jetzt darueber statt ueber NFCNDEFReaderSession. Die NDEF-Befehle
bleiben dabei vollstaendig erhalten: alle vier Tag-Protokolle (MiFare, ISO7816,
ISO15693, FeliCa) erben laut SDK-Header von NFCNDEFTag, also gibt es weiterhin
queryNDEFStatus, readNDEF, writeNDEF und writeLock. Gepollt wird iso14443 (die
ueblichen NTAG-Aufkleber) und iso15693 (Waeschetags mancher Hersteller).

Die eigene Klasse hiess NFCTag und kollidierte damit mit dem gleichnamigen
CoreNFC-Protokoll, das der neue Weg braucht - sie heisst jetzt NFCDienst.

Info.plist enthaelt ausserdem die Neuformatierung durch Xcode (Schluessel
sortiert, Kommentare entfernt). Inhaltlich unveraendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 13:48:40 +02:00
ScarriffleandClaude Opus 5 09a763a823 NFC-Tags fuer Einzelstuecke: beschreiben und lesen
Ein aufgeklebtes QR-Etikett uebersteht keine Waesche. Kleidungsstuecke bekommen
deshalb eingenaehte NFC-Tags. Auf das Tag kommt genau das, was auch im QR steckt
(vorrania://i/<UID>) - damit bleibt das Einlesen inhaltlich unveraendert, nur die
Erfassungsart ist neu. Backend und Web brauchten nichts: die UID vergibt der
Server, und GET /items/by-uid/<UID> gibt es laengst.

Gelesen wird nur bei geoeffneter App. iOS startet Apps beim Antippen im
Hintergrund ausschliesslich ueber https-Universal-Links auf einer beanspruchten
Domain und ignoriert eigene Schemes dabei - eine feste Domain ist fuer einen
selbstgehosteten Dienst aber gerade der falsche Weg (siehe f981f41).

Die /i/-Zerlegung lag dreifach kopiert in CheckInView, LookupScanView und
AssignScanView und steckt jetzt in einzelstueckCode(aus:). Die nackte UID wird
nur fuer NFC akzeptiert: eine achtstellige EAN-8 kann zufaellig genau wie eine
UID aussehen, und die Kamera sieht fremde Barcodes - ein Tag haben wir selbst
beschrieben.

Zwei Altlasten in denselben Dateien mit erledigt:
- project.yml deklarierte die applinks-Domain weiter, obwohl f981f41 sie
  entfernt hat. XcodeGen erzeugt die Entitlements aus diesem Block und schrieb
  sie bei jedem "generate" zurueck; die Domain existiert laengst nicht mehr.
- AssignScanView sendete ItemUpdateRequest ohne priceCents/currency. Da encode
  immer alle Felder schreibt (auch null), hat ein Zuordnen per Scan Kaufpreis
  und Waehrung stillschweigend geloescht.

Ebenfalls raus: QRImage und qrLink(forUid:) in ItemViews - ungenutzt, und
qrLink baute noch das alte https-Format.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 13:37:24 +02:00
ScarriffleandClaude Opus 5 9be772557e Sitzung haengt an der Benutzer-ID, Umbenennen wird moeglich
Das Sitzungs-Token benannte den Benutzer ueber seinen Namen (sub = username).
Damit war der Name die Kennung der Sitzung - ein Umbenennen haette jede laufende
Anmeldung dieses Benutzers ungueltig gemacht, und deshalb war es gar nicht erst
vorgesehen. Beim Anmeldedienst geaenderte Namen liessen sich in Vorrania
folglich nirgends nachziehen.

Neue Token tragen die ID (sub = "<id>", zusaetzlich uid), und deps.py loest die
Sitzung darueber auf. Alles andere haengt ohnehin schon an der ID: Bewegungen,
Dashboards, API-Tokens. Der Name ist damit reine Anzeige.

Benutzer lassen sich jetzt unter "Benutzer" umbenennen - auch sich selbst, ohne
aus der Sitzung zu fliegen. Der Anmeldeweg selbst sucht weiterhin ueber den
Namen; das ist das Feld im Formular und soll so bleiben.

Bestandsschutz: Token ohne uid werden weiter ueber den Namen aufgeloest, damit
beim Update niemand abgemeldet wird. Der Zweig darf nach Ablauf der
Token-Lebensdauer weg. Das zusaetzliche uid unterscheidet alt von neu eindeutig -
ein Benutzer koennte schlicht "42" heissen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 22:34:22 +02:00
ScarriffleandClaude Opus 5 eb29143f97 Erstanmeldung fragt nach, statt still ein zweites Konto anzulegen
Wer sich zum ersten Mal ueber den Anmeldedienst anmeldet und hier noch kein
Konto hat, bekam bisher wortlos ein neues - mit Rolle "Benutzer" und ohne
Verlauf. Wer schon ein Konto hatte, wollte aber fast immer genau das verknuepfen
und stand dann mit zwei Konten da.

Jetzt zeigt der Rueckweg eine Entscheidungsseite: bestehendes Konto verknuepfen
oder ein neues anlegen. Das Verknuepfen verlangt einmalig Benutzername und
Passwort des bestehenden Kontos. Genau dieser Nachweis ist der Grund, warum der
Anmeldeweg selbst nie fragen darf, welches Konto gemeint ist - ohne ihn koennte
sich jeder mit einem beliebigen Konto beim Anmeldedienst an ein fremdes
Vorrania-Konto haengen.

Die geprueften Angaben liegen dafuer kurzlebig und einmalig in
oidc_pending_links; vom Code steht dort nur der SHA-256-Hash, wie bei den
API-Tokens. Ein falsch getipptes Passwort verbraucht den Vorgang nicht, und
unbekannter Benutzername und falsches Passwort antworten gleich - sonst liessen
sich vorhandene Konten abzaehlen.

AUTO_CREATE=false blendet den Knopf zum Anlegen aus und laesst sich auch ueber
den Dialog nicht umgehen. Der App-Weg bleibt unveraendert, weil er keinen
Zwischenschritt anzeigen kann.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 22:28:38 +02:00
ScarriffleandClaude Opus 5 a134b2aad5 update.sh: Netzprobleme nicht mehr als lokale Aenderungen ausgeben
Bisher hiess JEDER fehlgeschlagene git pull "lokale Änderungen?" - samt dem Rat,
"git checkout -- ." auszufuehren. Bei einem DNS- oder Netzausfall behebt das
nichts und wirft im schlimmsten Fall Aenderungen weg, die voellig in Ordnung
waren.

Die Ausgabe von git wird jetzt ausgewertet und die Ursache benannt: nicht
aufloesbarer Name, nicht erreichbarer Server oder tatsaechlich abweichender
lokaler Stand. Nur im letzten Fall erscheint der checkout-Hinweis. Dazu der
betroffene Rechnername und die passenden Pruefbefehle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 22:11:26 +02:00
ScarriffleandClaude Opus 5 31b3624cc7 App-Client darf eine eigene Anwendung beim Anmeldedienst sein
In Authentik hat jede Anwendung standardmaessig ihren eigenen Issuer, abgeleitet
aus dem Application-Slug. Web- und App-Client brauchen dort aber zwei getrennte
Anwendungen, weil ein Provider entweder vertraulich ODER oeffentlich ist.

Bisher wurde das App-Token gegen den Issuer und den Schluesselsatz der
Web-Anwendung geprueft - die App-Anmeldung waere damit an der iss-Pruefung
gescheitert. Discovery und JWKS werden jetzt je Adresse gepuffert statt je
Anbieter, und OIDC_<KUERZEL>_NATIVE_DISCOVERY_URL benennt die Anwendung des
App-Clients. Ohne die Variable bleibt es bei einer gemeinsamen Adresse.

Ausserdem im README: der Signing Key muss gesetzt sein, sonst signiert Authentik
mit HS256 und das Token wird abgelehnt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 22:00:37 +02:00
ScarriffleandClaude Opus 5 3d0ff54bf5 Anmeldung wahlweise ueber OpenID Connect (Authentik)
Neben dem Passwort-Login gibt es jetzt einen zweiten, gleichberechtigten Weg:
OpenID Connect gegen einen selbst gehosteten Anmeldedienst. Ohne Konfiguration
aendert sich nichts - keine SSO-Knoepfe, Passwort-Login wie bisher.

Der Anmeldedienst ist NUR fuer die Anmeldung zustaendig. Danach stellt Vorrania
sein eigenes Token aus wie beim Passwort-Login, deshalb bleiben deps.py, die
Rollenpruefung und die langlebigen API-Tokens unveraendert.

Backend
- Anbieter kommen aus der Umgebung (OIDC_PROVIDERS + OIDC_<KUERZEL>_*). Ein
  zweiter Anbieter ist damit reine Konfiguration, ohne Codeaenderung.
- Browser-Weg (vertraulicher Client) und App-Weg (oeffentlicher Client, PKCE)
  enden beide im selben Token-Schema.
- state/nonce/PKCE-Verifier liegen in einer kurzlebigen Tabelle statt in einem
  Cookie: Starlettes CORS-Schicht spiegelt bei allow_origins=["*"] zusammen mit
  allow_credentials die Herkunft zurueck, sobald ein Request ein Cookie traegt.
  Ohne Cookie entsteht diese Wechselwirkung erst gar nicht - und der Ablauf
  bleibt mit der db-Fixture pruefbar.
- Zuordnung ueber die sub des Anbieters; die Erstverknuepfung laeuft ueber die
  BESTAETIGTE E-Mail-Adresse. Rollen kommen nie aus dem Anbieter, und ein
  gleichlautender preferred_username uebernimmt nie ein bestehendes Konto.
- users.password_hash darf jetzt leer sein (reine SSO-Konten). verify_password
  faengt das ab, sonst antwortete der Login mit 500 statt 401.

Verknuepfen bestehender Konten
- Neue Seite "Mein Konto" (fuer alle Benutzer, nicht nur Admins): verknuepfen
  und wieder loesen. Die Berechtigung stammt aus der laufenden Sitzung, es
  braucht also weder eine hinterlegte Adresse noch Admin-Rechte.
- Der Start laeuft ueber POST, weil eine Seitennavigation das Bearer-Token
  nicht mitschicken kann.
- Die letzte Anmeldemoeglichkeit eines Kontos laesst sich nicht loesen.

iOS
- AppAuth (erste Paketabhaengigkeit) mit PKCE im System-Browser, nie in einem
  eingebetteten WebView, und ohne Client-Secret.
- offline_access nur im App-Weg: der Browser-Weg braucht kein Refresh-Token und
  bekaeme sonst einen ueberfluessigen Zustimmungsdialog.
- Anmeldezustand je Serverprofil im Keychain; abgelaufene Token erneuert die App
  still im Hintergrund. Gleichzeitige 401er teilen sich eine Erneuerung.

Deployment
- docker-compose reicht die ganze .env durch. Einzeln aufzuzaehlen ginge nur mit
  fest verdrahteten Kuerzeln - genau das soll ein weiterer Anbieter nicht
  erfordern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 21:55:38 +02:00
ScarriffleandClaude Opus 5 8d4954610b Bilder blockieren nicht mehr die halbe Seite
Warum eine angeklickte Artikelseite nur zur Haelfte lud: Die Bild-Route wird je
Tabellenzeile einmal aufgerufen, und ein Browser haelt je Host nur rund sechs
Verbindungen offen. Was diese Route aufhaelt, legt die ganze Oberflaeche lahm.
Sie hielt gleich dreifach auf:

1. Fehlte die lokale Kopie, holte ``images.ensure`` das Bild MITTEN in der
   Anfrage von der fremden Adresse - bis zu 15 Sekunden, je Zeile, und bei
   jedem Aufruf erneut, weil ein Fehlschlag nirgends vermerkt wurde. Artikel
   aus einem Backup-Import haben genau diese Ausgangslage. Gemessen: 2,01 s
   je Aufruf, dreimal hintereinander. Jetzt: 0,01 s, das Nachholen laeuft nach
   der Antwort im Hintergrund und ein toter Link wird eine Stunde gesperrt.

2. Ein Vorschaubild ist 34x34 Pixel gross - ausgeliefert wurde das Original.
   Gemessen: 2530 KB je Briefmarke, bei 40 Zeilen 99 MB fuer eine Liste. Neu
   erzeugt die Bild-Route (Pillow) eine Vorschau und legt sie daneben ab;
   ``?thumb=1`` liefert sie aus. Gemessen: 0,6 KB.

3. If-None-Match wurde ignoriert. Nach den 5 Minuten Cache-Frist lud der
   Browser jedes Bild komplett neu - das erklaert, warum es "alle 20 Minuten"
   wieder losging. Jetzt 304 ohne Daten.

Dazu im Web: laufende Bildabrufe werden beim Seitenwechsel abgebrochen (sonst
steht die neue Seite hinter Bildern Schlange, die niemand mehr sieht), und wo
``image_version`` bekannt ist, entfaellt die Anfrage fuer bildlose Artikel ganz.

Und der Verbindungsvorrat der Datenbank: 5 (+10) gegen 40 Arbeits-Threads von
FastAPI. Ab der 16. gleichzeitigen Anfrage wartete eine Route stillschweigend
30 Sekunden - von aussen ein haengender Server. Jetzt 10 (+20) mit 10 Sekunden
Frist: lieber ein Fehler als eine halbe Minute Stille.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:14:43 +02:00
ScarriffleandClaude Opus 5 4e2dd3e3eb DataTable: neue Spalten ruecken in die gespeicherte Reihenfolge nach
Die Gruppen-Spalte fehlte im Spalten-Menue - aus zwei Gruenden, und der zweite
ist der ernstere.

1) order wurde EINMAL beim Mounten berechnet: gespeicherte Reihenfolge plus
alles, was zu dem Zeitpunkt in columns stand. Aendert eine Seite ihre Spalten
danach - die Artikelliste zeigt "Verwaltung" nur bei Gegenstaenden -, fehlt die
neue Spalte in order und faellt damit aus allOrdered heraus. Sie ist dann nicht
bloss aus dem Menue verschwunden, sondern aus der TABELLE. Ein useEffect zieht
fehlende Schluessel jetzt nach; angehaengt, nicht einsortiert, damit eine selbst
gewaehlte Reihenfolge nicht umspringt.

2) Die Liste im Menue war fest auf 240px begrenzt. Bei zehn Spalten stand der
letzte Eintrag ausserhalb, und weil macOS die Bildlaufleiste ausblendet, sah es
aus, als gaebe es ihn nicht. Die Hoehe richtet sich jetzt nach dem freien Platz
bis zum unteren Bildrand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 22:31:40 +02:00
ScarriffleandClaude Opus 5 68feb43b3e Icons: Auslagern-Pfeil zeigt jetzt WEG vom Lager
Ein- und Auslagern sind ein Paar und duerfen sich nur in EINEM Merkmal
unterscheiden - der Pfeilrichtung. Tatsaechlich sprang auch die Linie: beim
Einlagern lag sie unten und der Pfeil zeigte auf sie zu (richtig), beim
Auslagern lag sie oben und der Pfeil zeigte ebenfalls auf sie zu. Das las sich
wie "hinein, nur nach oben" statt wie "heraus".

Jetzt liegt die Linie bei beiden unten - sie ist das Lager - und nur der Pfeil
dreht sich: hinein beim Einlagern, weg beim Auslagern. Das ist auch die
Konvention der Vorlage (Feather download/upload), an der sich die uebrigen
Icons orientieren.

Wirkt ueberall, wo das Icon steht: Navigation, Ein-/Auslagern-Knoepfe,
Schnellbuchung, Umlagern, Export und die Verlaufsansicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 12:52:02 +02:00
ScarriffleandClaude Opus 5 ad3100462b Verlauf: klarere Bewegungs-Icons, Spalte nach vorn
Die beiden Kisten unterschieden sich nur in einem kleinen Pfeil. Bei 18 px war
das nicht zu trennen - erkannt hat man sie allein an der Farbe, und die war
gerade wegen der Wertung (gruen rein, orange raus) rausgeflogen. Ohne Farbe
blieb nichts uebrig, woran man sie auseinanderhaelt.

Jetzt die Strich-Icons checkin/checkout/edit: grosser Pfeil auf eine Linie zu
bzw. von ihr weg, Stift fuer die Korrektur. Dieselben Zeichen wie auf den
Ein-/Auslagern-Knoepfen, also schon vertraut, und auch klein eindeutig - die
Richtung liegt im Zeichen statt in der Farbe.

Auf der Verlaufsseite steht die Spalte jetzt an erster Stelle; sie sagt in
einem Blick worum es geht und braucht als Zeichen kaum Breite.

Der gefuellte Icon-Zweig in Icon.jsx wird damit nicht mehr gebraucht und ist
raus. Die SVG-Quellen bleiben unter brand/icons/ liegen - das README sagt jetzt
ausdruecklich, dass sie ungenutzt sind, warum, und wie man sie zurueckholt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 12:48:25 +02:00
ScarriffleandClaude Opus 5 e9738e28e7 Verlauf: keine Wertung mehr in der Farbe der Bewegungsart
Gruen fuer Einlagern und Orange fuer Auslagern las sich wie gut und schlecht -
dabei ist Auslagern schlicht der Normalfall, kein Missstand. Die Farbe kam noch
aus der Badge-Zeit, wo sie neben dem Wort stand; als alleiniges Signal wertet
sie.

Alle drei Arten tragen jetzt dieselbe neutrale Kachel. Unterschieden wird ueber
die Pfeilrichtung im Icon, und die Bedeutung steht weiterhin in title und
aria-label.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 12:42:44 +02:00
ScarriffleandClaude Opus 5 e33c01ec37 Icon-Quellen nach brand/icons/ verschieben und benennen
Die drei SVGs lagen in der Repo-Wurzel und hiessen noch wie beim Download
("open-box (1).svg" – inklusive Leerzeichen und Klammer). Durch mein git add -A
im letzten Commit sind sie dort auch gelandet.

Jetzt liegen sie in brand/icons/ und heissen wie die Icons im Code:
einlagern.svg, auslagern.svg, korrektur.svg. Ein README daneben haelt fest,
wofuer jede da ist - die beiden Kisten unterscheiden sich nur in der
Pfeilrichtung, wer sie verwechselt dreht Ein- und Auslagern um, ohne dass etwas
kaputtgeht - und wie man die Pfaddaten nach einer Aenderung neu uebernimmt.

Zur Laufzeit laedt niemand diese Dateien: die Pfade stehen inline in Icon.jsx,
damit die Icons ueber currentColor die Textfarbe annehmen. Die Originale tragen
festes Schwarz.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 12:41:21 +02:00
ScarriffleandClaude Opus 5 9a2dc3a482 Verlauf: Bewegungsart als Sinnbild statt als Wort
"Eingelagert"/"Ausgelagert"/"Korrektur" standen als Textbadge in einer eigenen
Spalte - in der schmalen Ansicht als dritte beschriftete Zeile je Eintrag. Jetzt
eine getoente Kachel mit Icon: Kiste mit Pfeil hinein, Kiste mit Pfeil heraus,
Stapel mit Stift (aus open-box (1).svg, open-box.svg, edit-server.svg). Die
Farbe traegt dieselbe Bedeutung wie vorher das Badge - gruen rein, orange raus,
neutral Korrektur; Titel und aria-label behalten das Wort.

Die Dashboard-Karte bekommt dabei das Layout aus der Skizze: Kachel links,
Zeitpunkt ueber Produkt, ohne Tabelle. Die Verlaufs-SEITE bleibt eine Tabelle -
dort stehen noch Menge und Benutzer -, dort weicht nur das Wort der Kachel.

Die Icon-Komponente kann jetzt auch GEFUELLTE Grafiken mit eigenem viewBox; die
vorhandenen sind Strichzeichnungen (fill:none, stroke:currentColor) und
vertragen sich damit nicht. Die Pfaddaten sind per Skript aus den SVGs
uebernommen, nicht abgetippt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 12:38:05 +02:00
ScarriffleandClaude Opus 5 e9a2b70728 Web: Fehlergrenzen - weisse Seiten zeigen jetzt, was schiefging
Im ganzen Frontend gab es keine einzige Error Boundary. In React 18 haengt eine
Ausnahme beim Rendern damit den KOMPLETTEN Baum aus: die Seite wird schlagartig
weiss und nur ein Neuladen hilft. Genau das gemeldete Bild - "keine Karten",
"gar nichts", "muss F5 druecken". Schlimmer noch: man sah nicht, WAS passiert
ist, und konnte es folglich auch nicht melden.

Zwei Ebenen: eine aeussere in main.jsx als letzte Instanz (faengt auch Fehler in
den Providern) und eine je Seite in Protected, damit bei einem Absturz
wenigstens die Navigation bedienbar bleibt. Die innere haengt am Pfad, sonst
haenge man nach einem Fehler dauerhaft in der Fehleranzeige fest.

Angezeigt wird die Meldung selbst, nicht nur ein "Ups", plus die technischen
Angaben zum Aufklappen. Damit ist der naechste Vorfall in einer Minute
eingeordnet statt in einer Stunde.

Das erklaert die Haeufigkeit, die meine vorherige Erklaerung (abgelaufene
Anmeldung) nicht hergab: ein Token gilt sieben Tage, nicht zwanzig Minuten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 11:12:15 +02:00
ScarriffleandClaude Opus 5 eddbb3710e Web: abgelaufene Anmeldung erkennen statt leere Seiten zeigen
Zwei Defekte, die zusammen das gemeldete Muster ergeben.

1) Es gab keine zentrale Behandlung von HTTP 401. AuthProvider prueft die
Anmeldung EINMAL beim Start; laeuft das Token danach ab, bleibt der Zustand
"angemeldet" stehen, waehrend jede Abfrage scheitert. Seiten mit
.catch(() => {}) (18 Stellen) zeigen dann schlicht nichts, andere eine
englische Servermeldung - genau das "mal gar nichts, mal die Haelfte".
Jetzt raeumt der API-Client bei 401 die Sitzung auf und der Login sagt, dass
sie abgelaufen ist. Beim Anmelden selbst greift das nicht, dort heisst 401
"Passwort falsch".

2) Der Start hatte ein blankes `catch`, das das Token bei JEDEM Fehler loeschte -
auch bei 502 oder weggebrochenem Netz. Wer waehrend eines Server-Neustarts F5
drueckte, war deshalb abgemeldet, obwohl die Sitzung voellig in Ordnung war.
Das duerfte der haeufigere Ausloeser gewesen sein, weil in letzter Zeit oft neu
deployt wurde. Jetzt wird nur bei 401/403 abgemeldet; bei allem anderen bleibt
die Anmeldung liegen und es erscheint "Server nicht erreichbar" mit einem
Knopf zum erneuten Versuchen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 11:07:55 +02:00
ScarriffleandClaude Opus 5 01c93d8b2c Web: Mindestbestaende in festen Spalten, Detail-Spalten verschiebbar
Die Eingabefelder auf der Mindestbestaende-Seite standen je Zeile woanders. Die
Zeile war ein .field-inline, und dort waechst sowohl der Name als auch das
Eingabefeld (.field-inline > input { flex: 1 1 auto }) - beide teilten sich den
Restplatz, also verschob ein laengerer Name das Feld nach rechts und machte es
schmaler. Jetzt ist die Zeile ein Raster mit festen Spalten (Name, Menge,
Einheit, Bestand, Ort, Loeschen); ohne Adminrechte bleiben die letzten beiden
Zellen leer, damit die Zeilen trotzdem fluchten. Schmale Fenster stellen den
Namen ueber die volle Breite.

Die "Details"-Spalte liess sich nicht verschieben: sie war fixed und hatte
zusaetzlich einen leeren Kopf - es gab also nichts zu greifen. Sie ist jetzt
frei und traegt einen sichtbaren Titel. Dasselbe fuer die gleichartigen
Link-Spalten in der Einzelstueck- und der Chargenliste ("Artikel",
"Aufteilen"). Sortieren bleibt dort aus, weil toggleSort ohne sortValue
ohnehin aussteigt. Bild- und Auswahlspalten bleiben fest - die haben keinen
sinnvollen Titel und gehoeren an den Anfang.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 10:36:48 +02:00
ScarriffleandClaude Opus 5 e00eef275c Gruppen hierarchisch: in allen Auswahlfeldern und in der Artikelliste
Die Gruppen-Dropdowns waren flache, alphabetische Listen - man sah nicht, dass
"Grillwurst" unter "Wurst" haengt. Ein gemeinsamer Helfer (gruppenOptionen)
rollt den Graphen zum Baum aus und rueckt ein; eine Gruppe mit mehreren
Obergruppen erscheint unter jeder, denn genau das ist ihr Sinn, und ausgewaehlt
wird ohnehin dieselbe ID.

Eingerueckt wird mit GESCHUETZTEN Leerzeichen - normale fasst der Browser in
einem <option> zusammen, die Einrueckung waere sonst wirkungslos.

Betroffen: Gruppen-Auswahl im Artikelformular (beide Zweige), beim Einlagern,
die Ziel-Auswahl der Mindestbestaende und die Obergruppen-Mehrfachauswahl (die
beim Suchen bewusst abflacht, weil eine Einrueckung ohne die Eltern in die Irre
fuehrt). In der App dasselbe in beiden Gruppen-Pickern.

Die Gruppen-Spalte der Artikelliste zeigt jetzt den Weg von oben statt eines
Etiketts - "Wurst → Grillwurst", mit gedaempften Vorfahren wie bei der
Kategorie. Gefiltert wird ueber ALLE Wege: wer auf "Wurst" filtert, sieht auch
die Artikel aus den Untergruppen. Dafuer gibt es gruppenInfoMap als Gegenstueck
zu categoryInfoMap, das mit mehreren Wegen je Gruppe umgehen kann.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 09:59:32 +02:00
ScarriffleandClaude Opus 5 302e7625e5 Web: Gruppentabelle auf volle Breite, Karten darunter
Die Seite hatte die Tabelle links und eine feste rechte Spalte (24% der
Breite). Seit die Tabelle Obergruppen, Bestand mit Einheit und die
Gebinde-Erlaeuterung traegt, war die rechte Spalte teurer als sie half - und sie
stand auch dann da, wenn gar keine Gruppe ausgewaehlt war.

Jetzt bekommt die Tabelle die volle Breite. "Neue Gruppe" steht darunter, die
Karten zur ausgewaehlten Gruppe (EAN-Codes, Mindestbestand) erscheinen beim
"verwalten" ebenfalls darunter, nebeneinander. Kein Springen des Layouts beim
Auswaehlen, weil die Tabelle ihre Breite nie aendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 09:44:29 +02:00
ScarriffleandClaude Opus 5 3de168b598 Gruppen: Bestand in der richtigen Einheit, Baumansicht, Gruppen-Spalte
Anzeigefehler: die Gruppenliste klebte immer min_stock_unit_name hinter den
Bestand - auch wenn die Gruppe laengst im Gruppen-Gebinde zaehlt. Aus 790 g bei
einem 190-g-Glas wurde so "4,16 Gramm" statt "4,16 Glaeser". Gerechnet war
richtig, beschriftet falsch.

Ursache war doppelte Logik: MinStock.jsx hatte grpPkgMode/grpFactor/grpUnit
lokal, Groups.jsx gar nicht. Die drei stehen jetzt in units.js und werden von
beiden benutzt. Die Einheiten-Spalte sagt ausserdem, wenn eine Gruppe im
Gebinde zaehlt ("zaehlt in Glaeser a 190 g") und bietet den Weg zurueck an.

Gruppenliste als Baum: eine Gruppe mit zwei Obergruppen erscheint unter beiden -
das ist die ehrliche Darstellung eines Graphen. Jede Zeile traegt ihren PFAD als
Schluessel, damit die vorhandene Baumlogik der DataTable unveraendert zurecht
kommt; die kennt nur einen Elternteil je Zeile, und ein Pfad hat genau einen.
Beim Sortieren/Filtern flacht sie wie gewohnt ab.

"1 Untergruppen" heisst jetzt "1 Untergruppe" - an allen vier Stellen ueber
einen gemeinsamen Helfer (anzahlWort), Web und App.

Lebensmittelliste bekommt eine Spalte "Gruppe" mit Filter. Dafuer liefert
ProductOut jetzt group_name, symmetrisch zu category_name, mit joinedload gegen
N+1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 09:40:09 +02:00
ScarriffleandClaude Opus 5 158175f693 App: Einheiten-Felder als zusammenhaengender Block
Dieselbe Umstellung wie im Web. Der Abschnitt "Artikel" traegt nur noch Name
und Marke; alles zur Einheit steht unter "Einheit und Menge" und liest sich als
Satz ("1 Packung sind 3 Stueck"). Das Gebinde heisst jetzt auch so statt
"Bezeichnung" und steht vor der Packungsgroesse, weil die Zeile darunter es
beim Namen nennt.

Die Umrechnung heisst nach dem, was sie liefert (Gewicht / Volumen /
Stueckzahl), beginnt mit einer einzigen Auswahl und zeigt den Satz erst danach -
mit passendem Verb und Ein-/Mehrzahl. Die linke Zahl wird beim Waehlen der
Zielart einmalig mit der Packungsgroesse vorbelegt.

Das Anlege-Formular kannte die Umrechnung bisher gar nicht; wer in der App
einen Artikel anlegte, musste ihn erst speichern und wieder oeffnen. Es hat
jetzt denselben Abschnitt und uebernimmt den Vorschlag aus dem Barcode-Lookup
("3 x 80 g").

Reine Oberflaeche - 226 Backend-Tests unveraendert gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 09:21:42 +02:00
ScarriffleandClaude Opus 5 37bc7663af Web: Einheiten-Felder als zusammenhaengender Block
Im Grillwurst-Fall stand dieselbe Zahl zweimal im Formular, mit zwei
Bedeutungen und in identischer Aufmachung: "Packungsgroesse 3" (1 Packung = 3
Stueck) und "Zweiteinheit 3 Stk ≙ 250 g" (3 Stueck wiegen 250 g). Dazu sass der
Zweiteinheit-Block zwischen Gebinde/MHD und Mindestbestand/Gruppe, hiess nach
einem Fachbegriff und benutzte ≙ statt eines Satzes.

Jetzt steht alles zur Einheit unter einer Ueberschrift und liest sich als Satz:
"Gezaehlt wird in Stueck", "Gebinde Packung", "1 Packung sind 3 Stueck". Das
Gebinde steht dafuer vor der Packungsgroesse - die Zeile darunter nennt es beim
Namen.

Die Umrechnung heisst nach dem, was sie liefert (Gewicht / Volumen / Stueckzahl)
und beginnt mit einer einzigen Auswahl; erst danach erscheint der Satz "3 Stk
wiegen 250 g" mit passendem Verb und Ein-/Mehrzahl. Die linke Zahl wird beim
Waehlen der Zielart EINMALIG mit der Packungsgroesse vorbelegt - zu tippen
bleibt nur die 250. Bewusst keine dauerhafte Kopplung: wer die Packung spaeter
auf 4 aendert, hat dieselbe Wurst, und ein mitgezogener Wert wuerde
stillschweigend behaupten, sie sei leichter geworden.

MHD-Angabe wandert aus dem Einheiten-Block zur Gruppen-Zeile; sie stand nur
zufaellig neben dem Gebinde.

Reine Oberflaeche - Datenmodell, API und Backend-Tests unveraendert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 09:19:08 +02:00
ScarriffleandClaude Opus 5 71d52917e4 App: Zweiteinheit erfassen und anzeigen
Eigener Abschnitt im Artikel mit "Menge [Stk] entspricht [Gramm] das sind
[250]" und der ausgerechneten Kontrolle in der Fusszeile. Nur Charge-Artikel,
und nur die anderen Arten stehen zur Wahl - eine Bruecke auf die eigene Art
haette nichts umzurechnen.

ProductUpdateRequest bekommt sendSecondary nach dem Vorbild von sendMinStock:
ein PATCH aus einer anderen Maske soll die Bruecke nicht unbeabsichtigt
loeschen. Die Einheitenauswahl beim Ein- und Auslagern nimmt bei hinterlegter
Bruecke die Gegenart mit auf, der Bestand steht in beiden Lesarten
("6 Stueck · 500 g").

Alle neuen Product-Felder sind Optionals, damit die App auch gegen einen
aelteren Server laeuft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 00:42:52 +02:00
ScarriffleandClaude Opus 5 788ef2caf2 Web: Zweiteinheit erfassen und anzeigen
Im Artikel-Formular eine Zeile "3 [Stk] ≙ 250 [Gramm]" mit ausgerechneter
Kontrolle darunter ("1 Stk ≈ 83,33 g"). Links steht die Basiseinheit des
Artikels fest, rechts stehen nur die anderen Arten zur Wahl - eine Bruecke auf
die eigene Art haette nichts umzurechnen. Nur vollstaendig ausgefuellt wird sie
gespeichert; Gegenstaende ohne Charge tragen keine.

buildUnitOptions laesst bei hinterlegter Bruecke auch die Einheiten der Gegenart
zu - damit erben Ein- und Auslagern die Faehigkeit ohne eigene Logik. Beim
Auslagern steht der Bestand jetzt in beiden Lesarten ("6 Stueck · 500 g").

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 00:40:33 +02:00
ScarriffleandClaude Opus 5 9f3225113e Zweiteinheit am Artikel: Bruecke zwischen Stueck, Gramm und Milliliter
Die drei Einheiten-Arten waren bisher strikt getrennt: BASE_OF_KIND bildet
count/weight/volume 1:1 auf Stueck/Gramm/Milliliter ab, ohne jeden Faktor
dazwischen. Zwei Stellen setzten das durch - to_base lehnte artfremde Einheiten
beim Ein-/Auslagern ab, und group_min_context filterte stueckweise gefuehrte
Artikel aus einer Kilogramm-Gruppe stillschweigend heraus. Letzteres war der
Anlass: eine Gruppe "Wurst" in kg sah Bratwuerste in Stueck gar nicht.

Ein Artikel darf jetzt eine Zweiteinheit tragen: "3 Stueck ≙ 250 g". Gespeichert
wird das eingegebene PAAR, nicht der Faktor - wer 3 und 250 eintippt, sieht beim
naechsten Oeffnen genau das wieder. Das hat auch einen rechnerischen Grund:
250 * 3 / 250 ist exakt 3, der Umweg ueber 250/3 ergibt 3,0000000000000004 und
liefe damit gegen die Bestandspruefung beim Auslagern.

Der Artikel bleibt in seiner Basiseinheit gefuehrt; die Bruecke ist reine
Rechnung. Gruppen zaehlen artfremde Artikel jetzt mit ihrem Faktor mit
(GroupMinContext.faktoren), Bestandssummen laufen dafuer je Artikel gewichtet -
weiterhin zwei Abfragen, nur mit GROUP BY. Ein-/Auslagern in der Fremdeinheit
geht, krumme Mengen werden bewusst gebucht statt gerundet: 100 g sind 1,2 Stueck,
und Runden wuerde stumm etwas anderes buchen als angegeben.

WICHTIGE KORREKTUR am urspruenglichen Plan: die Teilmengen-Bedingung in
_gruppen_bedarfe konnte NICHT bleiben. Sie war bisher zugleich ein
Einheiten-Schutz, weil Artikel verschiedener Arten zwangslaeufig disjunkt waren.
Mit der Bruecke gilt sie ploetzlich auch zwischen einer Stueck- und einer
Gramm-Gruppe - und _netted_topups haette einen Bedarf in Stueck von einem in
Gramm abgezogen. Jetzt wird nur noch zwischen Gruppen derselben Basiseinheit
verrechnet.

Open Food Facts: "3 x 80 g" verlor bisher den Multiplikator, weil der Regex den
ersten Zahl-Einheit-Treffer nahm. parse_gebinde liefert jetzt Gesamtmenge UND
Stueckzahl und belegt die Zweiteinheit vor; parse_quantity behaelt seinen
schmalen Vertrag.

18 neue Tests. Dass test_wrong_kind_rejected und
test_einheitenfilter_gilt_auch_fuer_untergruppen unveraendert gruen bleiben, ist
selbst der Beleg: ohne Bruecke aendert sich nichts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 00:38:13 +02:00
ScarriffleandClaude Opus 5 f33054c534 App: Obergruppen zuordnen, Mindestbestaende je Ort bearbeiten
Die App konnte bei Gruppen bisher nur umbenennen und Mindestbestaende lesen.
Jetzt gibt es einen vollen Gruppen-Editor (Sources/GroupViews.swift): Name,
Obergruppen als Mehrfachauswahl, Einheit und Mindestbestaende je Ort. Bewusst
eine flache Liste mit "unter: ..." statt TreeMasterView - das Protokoll TreeItem
kennt genau einen Elternteil und wuerde eine Gruppe mit zwei Obergruppen doppelt
zeigen. Die eigene Gruppe und ihre Untergruppen sind in der Auswahl
ausgeschlossen, damit kein Ring entsteht.

Mindestbestaende-Uebersicht ist nach Lagerort gegliedert ("Ueberall" zuerst) und
nicht mehr nur lesend - Gruppenzeilen oeffnen den neuen Editor. Am Artikel
entfaellt das separate Feld "Gesamt": der Bedarf haengt immer an einem Ort, und
"Ueberall" ist einer davon. Das Stammdaten-Speichern schickt min_stock nicht
mehr mit, sonst wuerde es die dort gesetzte Ueberall-Zeile ueberschreiben.

APIClient kann jetzt updateGroup und setGroupLocationMinStock. GroupUpdateRequest
bekommt bewusst KEINEN eigenen Encoder: der synthetisierte laesst nil-Felder weg,
ein handgeschriebener wuerde "name": null senden und den Namen loeschen.

Neue Datei -> xcodegen generate; Projektdatei ist mit committet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 00:02:43 +02:00
ScarriffleandClaude Opus 5 0537a2b53f Web: Obergruppen zuordnen, Mindestbestaende nach Lagerort
Mindestbestaende-Seite ist jetzt nach Lagerort gegliedert statt nach Artikel -
"Ueberall (egal wo)" zuerst, danach die Lagerorte. Es gibt keine automatische
Leerzeile je Artikel mehr; angelegt wird ueber "+ hinzufuegen" im jeweiligen
Abschnitt, dann steht der Ort schon fest und es fehlen nur Ziel und Menge.
Das Zeilenmodell faellt damit von vier Faellen auf zwei, und weil der Server
einheitlich Basiseinheiten liefert, bleibt genau eine Umrechnung uebrig statt
der drei gegenlaeufigen von vorher.

Gruppen: neue Spalte "Obergruppen" mit Mehrfachauswahl (GroupParentSelect).
Bewusst kein Baum - CategorySelect und die tree-Prop der DataTable koennen nur
einen Elternteil und wuerden eine Gruppe mit zwei Obergruppen doppelt anzeigen.
Die eigene Gruppe und ihre Untergruppen sind in der Auswahl gesperrt, damit die
Ringregel sichtbar wird, bevor der Server sie ablehnt. Produktzahl zeigt
transitiv und direkt ("12 (3 direkt)"), dazu Badges fuer Untergruppen und eine
Warnung, wenn eine Gruppe mit Untergruppen keine Einheit hat - sonst summiert
sie Gramm und Stueck zu einer sinnlosen Zahl.

Einkaufsliste: "Gesamt" heisst "Ueberall", Gruppen zeigen ihre mitgezaehlten
Untergruppen, und eine Fussnote benennt die eine Stelle, die sich nicht
verrechnen laesst (zwei Obergruppen mit gemeinsamer Untergruppe).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 23:56:18 +02:00
ScarriffleandClaude Opus 5 54261b98c0 Sicherung: Obergruppen und Mindestbestaende je Ort mitsichern
Das JSON-Backup schrieb bei Gruppen nur Name, Mindestbestand und Einheit -
Gebinde und Ort-Bedarfe fehlten schon vorher, ein Restore verlor sie also
stillschweigend. Mit den Obergruppen waere die komplette Hierarchie dazu
gekommen.

Format v3: Gruppen bringen jetzt parents (als NAMEN, nicht IDs - die sind
zwischen zwei Instanzen nicht gleich), Gebinde-Felder und ihre Mindestbestaende
je Ort mit; Artikel ebenso. Ort null = "Ueberall". Der Import laeuft dafuer
zweiphasig, weil Kanten erst gesetzt werden koennen, wenn alle Gruppen und
Lagerorte existieren, und weist Ringe ab - eine beschaedigte Datei darf keinen
einschleusen, der danach jede Auswertung im Kreis laufen liesse.

v2-Sicherungen bleiben lesbar; ihnen fehlen die neuen Listen einfach. Die
CSV-Spalte "mindestbestand" meint weiterhin den Bedarf ohne Ortsangabe und
landet in der Ueberall-Zeile.

maintenance.py raeumt die n:m-Zeilen jetzt ausdruecklich ab: das Core-DELETE
nimmt sie nicht mit, und SQLite erzwingt keine Fremdschluessel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 23:48:47 +02:00
ScarriffleandClaude Opus 5 eaacfd03e5 Gruppen: Obergruppen + Mindestbestaende nur noch je Lagerort (Backend)
Zwei zusammenhaengende Umbauten, weil sie dieselben Stellen betreffen.

Obergruppen: Gruppen bilden jetzt einen gerichteten azyklischen Graphen statt
einer flachen Liste. Eine Gruppe darf unter MEHREREN Obergruppen haengen -
"Grillwurst" unter "Wurst" UND unter "Grillgut"; mit einem einzelnen parent_id
waere genau das nicht abbildbar. Bestand und Mindestbestand einer Gruppe zaehlen
den gesamten Untergraphen, wobei eine ueber zwei Wege erreichbare Untergruppe
nur einmal zaehlt (services/gruppen.py arbeitet durchgaengig mit Mengen).
Product.group_id bleibt unveraendert - ein Artikel haengt weiter an genau einer
Gruppe.

Mindestbestaende: der separate Gesamt-Mindestbestand entfaellt. Er wird zur
Zeile mit location_id NULL ("Ueberall") und ist damit die Wurzel ueber allen
Lagerorten - dieselbe Verrechnung wie bei verschachtelten Orten greift jetzt
auch zwischen Ueberall und Kueche, wodurch derselbe Artikel nicht mehr doppelt
in der Einkaufsliste steht. Alle Werte liegen einheitlich in Basiseinheiten
statt in drei verschiedenen Einheiten nebeneinander; das Umrechnen beim
Umschalten der Erfassungseinheit entfaellt dadurch ersatzlos.

_netted_topups nimmt die Hierarchie jetzt als Parameter und faltet damit
Lagerort-Baum und Gruppen-Graph. Verrechnet wird zwischen zwei Gruppen nur,
wenn die zaehlenden Artikel der Untergruppe eine Teilmenge der Obergruppe sind -
zaehlt die Obergruppe in Kilogramm und die Untergruppe in Stueck, kommt ein Kauf
dort oben nicht an.

Die vierfach kopierte Bestandssumme wandert in Sammelabfragen
(summe_bestand_base), sonst vervielfacht der transitive Teilgraph die Abfragen.

Einmalige Datenwanderung beim Start (Merker in den Einstellungen), 18 neue
Tests - darunter Doppelzaehlung ueber zwei Wege, Ringschutz und die bewusst
offene Grenze bei zwei Obergruppen mit gemeinsamer Untergruppe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 21:48:14 +02:00
ScarriffleandClaude Opus 5 df65d9583c App: Lagerort bei Chargen anzeigen und aendern
In der Chargenliste eines Artikels standen nur MHD und Menge. Zwei Chargen
mit gleichem MHD, die sich nur im Lagerort unterscheiden, sahen dadurch
identisch aus. Die Zeile nennt jetzt zusaetzlich den Lagerort-Pfad (bzw.
"ohne Ort").

Im Sheet "Charge aendern" kommt ein Lagerort-Picker samt QR-Scan dazu -
wie in der Weboberflaeche. Das Backend konnte das ueber PATCH /lots/{id}
schon; LotUpdateRequest sendet location_id jetzt immer mit (auch null),
damit sich ein gesetzter Ort wieder entfernen laesst.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 14:54:44 +02:00
ScarriffleandClaude Opus 4.8 86e0edace8 Mindestbestände: Gruppen in Packungen erfassen (Gruppen-Gebinde)
Gruppen mit verwalteter Einheit lassen sich in der Spalte 'Einheit' auf ein
Gruppen-Gebinde umschalten. Über 'Gebinde festlegen…' definiert man einmalig
'1 Packung ≈ X g/ml' (Richtwert, da Produkte unterschiedlich große Packungen
haben können). Danach werden Mindestbestand UND Bestand der Gruppe in
Packungen angezeigt/erfasst; das Backend rechnet vorhandene Werte beim
Umschalten um.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 18:42:15 +02:00
ScarriffleandClaude Opus 4.8 ecc1570100 Gruppen-Gebinde: Mindestbestand/Bestand in Packungen (Backend)
Gruppen bekommen ein eigenes Richt-Gebinde (package_size/label) plus
min_stock_in_packages. Weil die Produkte einer Gruppe unterschiedlich
große Packungen haben können (Pesto 99 g vs. 160 g), legt die Gruppe einen
gemeinsamen Richtwert fest (1 Glas ≈ X g).

- Neuer Helfer group_min_context() zentralisiert, in welcher Einheit der
  Gruppen-Mindestbestand zaehlt (Gebinde ODER verwaltete Einheit).
- Einkaufsliste (gesamt + je Ort), Dashboard-Bedarf und GroupOut nutzen ihn;
  need.text kommt so als 'x Glaeser (y g)'.
- update_group rechnet bestehende Werte beim Umschalten der Einheit um, damit
  der physische Bedarf gleich bleibt.
- Migration: groups.package_size/package_label/min_stock_in_packages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 18:38:12 +02:00
ScarriffleandClaude Opus 4.8 28785b0a10 Mindestbestände: Einheit je Produkt umschaltbar (g/ml <-> Gebinde)
Produkte mit Packungsgröße lassen sich auf der Mindestbestandsseite nun
wahlweise in ihrer Gebinde-Einheit (Glas/Dose) statt nur in g/ml erfassen
- Auswahl in der Spalte 'Einheit'. Mindestbestand UND Bestand werden dann
in Packungen angezeigt; der Hinzufügen-Dialog hat den Umschalter ebenfalls.
Gespeicherte Werte bleiben physisch gleich (nur Anzeige/Erfassung wechselt),
das Backend-Flag min_stock_in_packages steuert die Umrechnung.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 18:31:30 +02:00
ScarriffleandClaude Opus 4.8 f2c657ff31 Einkaufsliste: Fehlmenge als Gebinde (Glaeser/Dosen), Basiseinheit klein
Statt 'fehlt 500 Gramm' jetzt die Produktgroesse als Leitangabe:
'fehlt 3 Glaeser (500 g)'. Auf ganze Gebinde aufgerundet, weil man nur
ganze Glaeser/Dosen kauft; Basiseinheit als kleiner Hinweis.

Backend: neues ShoppingNeed-Objekt an jeder Einkaufslisten-Zeile (Produkte
+ Gruppen, gesamt + je Ort) - damit die Angabe auch direkt ueber die API
kommt ('x Glaeser (y g)'), inkl. Pluralisierung aus der Gebinde-Tabelle.
Gruppen leiten das Gebinde ab, wenn alle passenden Produkte dasselbe haben,
sonst bleibt es bei der Basiseinheit. Bestehende Felder unveraendert.

Frontend: gemeinsame Komponente ShoppingNeedText fuer Karte und volle Seite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 18:03:37 +02:00
ScarriffleandClaude Opus 4.8 e214bb76a9 Web: Skeleton-Loading (schimmernde Ladeplatzhalter)
Statt nacktem 'Lädt…'/'Wird geladen…' jetzt geformte, schimmernde
Platzhalter an den zentralen Stellen:
- Neuer Baustein components/Skeleton.jsx (Skeleton/SkeletonLines/CardSkeleton),
  Sweep-Animation, Farben aus CSS-Tokens (Dark-Mode automatisch), respektiert
  prefers-reduced-motion.
- DataTable: bei Erstladung Platzhalterzeilen in Spaltenform (deckt alle
  8 Tabellenseiten). Nachladen mit vorhandenen Zeilen laesst Daten stehen.
- Dashboard-Karten: Zustand-Wrapper + Inline-Diagramm-Lader zeigen
  KPI-/Listen-/Diagramm-Skelette passend zur Kartenform.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-04 00:30:25 +02:00
ScarriffleandClaude Opus 4.8 da5934948e iOS: Code-Review-Korrekturen an Icon/Info.plist/Projekt
- hexagon.svg (neues App-Icon) mit einchecken; alte Icon-SVGs entfernt
- LSApplicationCategoryType ins Info.plist: bei GENERATE_INFOPLIST_FILE=NO
  wird die INFOPLIST_KEY_*-Build-Einstellung NICHT ins gebaute Plist
  uebernommen -> Kategorie 'utilities' landete bisher nirgends
- Info.plist wieder handgepflegt (deutsche Kommentare zurueck), von Xcode
  weggeschriebene Doku wiederhergestellt
- iPhone bewusst nur Hochkant; iPad-Unterstuetzung wiederhergestellt
  (TARGETED_DEVICE_FAMILY zurueck auf 1,2), iPad behaelt alle Ausrichtungen
- ItemListView.swift im Projekt (von Xcode ergaenzt)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 18:19:53 +02:00
ScarriffleandClaude Opus 4.8 63ee9608af iOS: Am Ablauftag 'heute' statt '0 Tage'
Die Uebersichtskarte 'Bald ablaufend' zeigte am Ablauftag '0 Tage'.
Jetzt gemeinsame Formulierung auf ExpiringItem.restlaufzeitText:
abgelaufen / heute / morgen / X Tage - Karte und volle Liste identisch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 16:40:29 +02:00
ScarriffleandClaude Opus 4.8 6ddf6024f7 iOS: 'Bearbeiten'-Leiste sichtbar machen (nicht mehr in versteckter .bottomBar)
Der Start-Tab liegt in einer TabView; deren Tab-Balken verdeckte die
.bottomBar-Werkzeugleiste, deshalb war 'Bearbeiten' nicht auffindbar.
Jetzt eine eigene Leiste per safeAreaInset direkt ueber dem Tab-Balken.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 22:53:42 +02:00
ScarriffleandClaude Opus 4.8 80aa857bb0 iOS: Diagramm-Einstellungen im lokalen App-Editor
Der lokale Uebersicht-Editor kann jetzt pro Diagramm-Karte einstellen,
wie im Web-Editor:
- Ablauf-Ring: 'Ohne MHD einbeziehen'
- Kategorien-Ring: 'Detailansicht' (alle einzeln) + 'Kategorie-Tiefe'

AppCard speichert die Optionen (optional -> alte Uebersichten dekodieren
weiter) und reicht sie an DashboardCardView durch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 22:45:09 +02:00
ScarriffleandClaude Opus 4.8 d680614526 iOS: Diagramm-Einstellungen der Web-Karten beim Spiegeln uebernehmen
Die gespiegelte Web-Uebersicht ignorierte bisher die Karten-Props der
Diagramme. Jetzt werden uebernommen:
- Ablauf-Ring: 'Ohne MHD einbeziehen' (mit_ohne_mhd)
- Kategorien-Ring: 'Detailansicht' (alle einzeln statt 'Uebrige') (detail)
- Kategorien-Ring: 'Kategorie-Tiefe' (tiefe -> /by-category?depth=N)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 22:41:36 +02:00
ScarriffleandClaude Opus 4.8 ade3c86747 Web: nur Tabelleninhalte markierbar, Titel/UI nicht (kein Text-Caret mehr)
cursor:default nimmt nur den Maus-I-Beam weg; bei markierbarem Text poppt beim
Klick auf einen Titel trotzdem ein blinkendes Caret auf. Jetzt: user-select:none
auf body, wieder markierbar nur .table td, Eingabefelder, textarea, code/pre und
.selectable. So bleiben Artikel-/Markennamen aus Tabellen kopierbar, Titel/Labels
zeigen kein Caret/I-Beam mehr.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 11:10:56 +02:00