Die Anmeldung galt fest 7 Tage - und die Frist lief ab dem *Anmelden*, nicht
ab der letzten Nutzung. Wer die App taeglich benutzte, flog trotzdem
punktgenau nach einer Woche hinaus.
Beides ist jetzt geregelt:
- Einstellungen -> Anmeldung: 7 Tage bis 1 Jahr oder unbegrenzt. Gilt fuer
Website und App gemeinsam, weil beide dasselbe Token bekommen - auch nach
Single Sign-on.
- POST /auth/refresh schiebt die Frist beim Start nach vorn. Damit zaehlt die
eingestellte Dauer ab dem letzten Besuch, und eine geaenderte Einstellung
greift ohne Neuanmeldung.
"Unbegrenzt" heisst ein Token ganz ohne exp. Das ist bewusst folgenreich: Es
gibt keine Sperrliste, eine erteilte Anmeldung laesst sich nicht einzeln
zurueckziehen. Die Oberflaeche sagt das dazu und nennt den einzigen Ausweg
(JWT_SECRET aendern).
/auth/refresh nimmt bewusst nur JWTs an, keine langlebigen API-Token: aus
einem jederzeit widerrufbaren Zugang (Home Assistant) waere sonst ein
unwiderruflicher geworden.
Der Rueckfallwert aus der Umgebung steigt von 7 auf 30 Tage - er gilt nur noch,
solange nichts eingestellt ist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
- Kassenzettel-Kachel vom Listen- in den Scannen-Tab verschoben (passt thematisch).
- App-Akzent auf Markenblau (#3F51B5/#7C8AE8, wie Web/App-Icon) via .tint; alle
Color.accentColor -> Color.marke. System-Blau verschwindet.
- Kachel-Buttons/Links auf .buttonStyle(.plain): weisse Schrift/Symbole statt
getoentem Blau.
- Kassenzettel-Pruefen-Liste: je Artikel eine eigene Section (insetGrouped) mit
Artikel/Menge/Lagerort in eigenen Zeilen - klar gruppiert, nicht mehr gequetscht.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neuer Flow (Kachel im Listen-Tab): Foto aufnehmen/waehlen, Artikelbereich per
Rechteck markieren, on-device OCR (Vision, deutsch; Bild bleibt auf dem Geraet),
Zeilen an /products/match schicken. Pruefen-Liste je Zeile: Artikel-Picker
(Auto-Treffer ueber Schwellwert + eigene Lebensmittel-Suche), Menge (aus Zeile
geparst, korrigierbar), Lagerort-Dropdown + QR-Scan. Einlagern per checkInBatch,
unbekannte Zeilen werden uebersprungen; keine Neuanlage.
Backend: MatchCandidate traegt package_size/base_unit mit, damit die App die
Einheit ohne Extra-Abruf kennt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Universal Links (Associated Domains) sind an eine feste Domain gebunden - fuer
einen selbstgehosteten Dienst untauglich (jede Instanz hat eine andere URL) und
die Domain stand im offenen Quellcode (Leak). Jetzt tragen Einzelstueck-QRs das
Custom-Scheme vorrania://i/<UID>: Die normale Kamera-App oeffnet die App weiterhin,
aber ohne Domain-Bindung und ohne Server im Code. applinks-Eintrag entfernt.
Web: itemDeepLink()-Helfer, genutzt in Einzelstuecke-Liste/-Etiketten und im
P-touch-Export. Lagerort-QRs bleiben unveraendert (https).
iOS: Router erkennt vorrania://i/<UID> und oeffnet das Einzelstueck am aktiven Server.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bisher nur globale Produkt-Mindestbestaende - wer (wie im Screenshot) nur Gruppen-
oder Lagerort-Bedarfe hatte, sah "keine gesetzt". Jetzt Produkte UND Gruppen,
jeweils Gesamt und je Lagerort, mit Soll (Anzeige-/Gruppeneinheit) und "niedrig"-
Hinweis. Produktzeilen oeffnen den Artikel zum Bearbeiten. GroupItem traegt dafuer
die Mindestbestand-Felder.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1) In den Benachrichtigungs-Regeln liess sich keine Kategorie waehlen: der Abruf
holte den ganzen Snapshot (Produkte + Ablauf + Kategorien) - schlug ein Teil
fehl, kamen auch die Kategorien leer. snapshot ist jetzt widerstandsfaehig
(Teilausfaelle egal), und die Regel-Bearbeitung holt nur noch die Kategorien.
2) Die gruene Erfolgsmeldung ist jetzt eine saubere, eingerueckte Pille mit Titel
("Eingelagert"/"Ausgelagert") und Detailzeile ("Neuer Bestand: 2 Packungen").
Die Einheit steht in der Mehrzahl (Gebinde-Plural vom Server; Basiseinheiten
wie Gramm bleiben unveraendert).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ein erkannter Textblock konnte mehrere Zeilen enthalten; die Auswahl uebernahm
dann mehrzeiligen Text. Jetzt wird strikt in einzelne Zeilen zerlegt, sodass
jede Auswahl genau eine Zeile ist.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Neuer TextScanView: beim Anlegen Name und Marke per Texterkennung einlesen
(erkannte Zeilen antippen, wie beim MHD-Scan) - kein Abtippen mehr, wenn ein
Produkt nicht in Open Food/Products Facts steht.
- BestBeforeText erkennt jetzt auch Tag.Monat ohne Jahr (z.B. "05.01" bei
Frischware / "zu verbrauchen bis") und ergaenzt das aktuelle Jahr (bzw. das
naechste, falls schon vergangen).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Das Menge-hinzufuegen/umlagern/entfernen-Sheet ging beim ersten Antippen sofort
wieder zu: das spaet fertige Laden der Entnahme-Statistik (.task im Abschnitt)
riss das gerade praesentierte Sheet weg. ObjectStockSection ist jetzt zustands-
los (bekommt removals + Sheet-Binding); Sheet und Statistik-Laden liegen in der
Detailansicht auf der stabilen Form-Ebene bzw. im reload().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Das Hinzufuegen/Umlagern/Entfernen-Sheet hing an der umschliessenden Group; bei
manchen iOS-Versionen ging es dadurch kurz auf und sofort wieder zu. Jetzt am
Abschnitt selbst (wie bei den Einzelstuecken), was stabil praesentiert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neue "Verwaltung"-Auswahl in der Detailansicht: Menge je Lagerort <->
Einzelstuecke <-> Verbrauchsgegenstand umstellbar. Wird beim Speichern per
sendMode uebertragen (individual/bulk); vorhandener Bestand wird nicht
automatisch umgerechnet (Hinweis im Footer).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neue "Mindestbestaende"-Ansicht: alle Artikel mit gesetztem Mindestbestand mit
Soll vs. Bestand (Anzeigeeinheit) und "niedrig"-Hinweis; Tippen oeffnet den
Artikel zum Bearbeiten. Kachel im Listen-Tab ergaenzt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Lagerort per QR scannen in Menge-hinzufuegen/entfernen/umlagern (Ziel), nicht
nur ueber das Dropdown suchen.
- Beim Einlagern laufen "Menge je Lagerort"-Gegenstaende jetzt ueber die Mengen-
Eingabe (ObjectAddSheet) statt durch den Charge-Weg mit MHD - Socken & Co.
fragen kein MHD mehr ab.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LabeledField war rechtsbuendig; iOS rendert bei rechtsbuendigen Feldern ein
getipptes Leerzeichen am Ende nicht, bis ein weiteres Zeichen folgt (wirkt, als
reagiere die Eingabe nicht). Linksbuendig behebt das und liest sich bei Name/
Marke ohnehin natuerlicher.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Beim Einlagern lief auch ein Einzelstueck-Produkt durch den Charge-Weg
(CheckInFormView -> Lot). Einzelstuecke zaehlen aber Items, nicht Lots - der
Bestand blieb 0. Im Einlagern-Ablauf werden Einzelstueck-Produkte jetzt ueber
ItemAddSheet als physische Exemplare (UID/QR) angelegt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Nach dem Anlegen eines Einzelstuecks im Detail zeigte die Produktliste weiter
"Bestand 0", weil sie nicht aktualisiert wurde (das Stueck war aber gespeichert).
Produkt- und Einzelstueckliste laden jetzt bei jedem Erscheinen neu (onAppear),
auch beim Zurueckkommen aus der Detailansicht.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Das Gesamt-Mindestbestand-Feld erschien nur beim Verbrauchsgegenstand; bei
Lebensmitteln fehlte es und save() schickte ihn nicht. Jetzt fuer alle foodLike
(Lebensmittel + Verbrauchsgegenstand) - in der Anzeigeeinheit, robust aus den
Basiseinheiten umgerechnet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neuer "Verwaltung"-Filter (nur in der Gegenstaende-Liste): Menge je Lagerort /
Einzelstuecke / Verbrauchsgegenstand.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bisher musste man im "Ort zuordnen"-Ablauf zuerst den QR des Einzelstuecks
scannen. Fuer Stuecke ohne eigenen Code (z.B. Kleidung) gibt es jetzt
"Einzelstueck aus Liste waehlen" (durchsuchbare Liste) als Alternative; danach
wie gehabt den Lagerort-QR scannen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Der Kategoriefilter der Einzelstueckliste zeigt jetzt einen eingerueckten Baum
und enthaelt auch die Oberkategorien (z.B. "Klamotten"), nicht nur die Blaetter
mit Exemplaren. Nach einer Oberkategorie zu filtern schliesst alle Unter-
kategorien ein (ueber category_id statt Namensgleichheit).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kategoriefilter zeigt nur noch Kategorien des Listentyps (Lebensmittel-Liste nur
Food-Kategorien, Gegenstaende-Liste nur Objekt-Kategorien) - vorher konnte man
sinnlos ueberkreuz filtern. Neue Sortierung (Name A-Z/Z-A, Zuletzt geaendert)
per Menue-Picker; Product traegt dafuer updated_at.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Im Einzelstueck-Formular (bearbeiten und anlegen) gibt es jetzt neben dem
Lagerort-Dropdown einen "Lagerort scannen"-Knopf: den QR am Regal/Fach scannen
setzt den Ort direkt. So kann man z.B. Kleidung ohne eigenen Stueck-QR einem Ort
zuordnen. Neue wiederverwendbare LocationScannerView (…/l/<Code>).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Beim Neuanlegen eines Gegenstands (z.B. T-Shirt) werden jetzt die eigenen Felder
der gewaehlten Kategorie (Groesse, Farbe, ...) geladen und angezeigt - vorher
tauchten sie erst in der Detailansicht nach dem Anlegen auf. NewProductRequest
traegt die Werte via field_values mit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Anlege-Formular bekommt eine Foto-Sektion (Kamera/Galerie) mit Vorschau. Das
Bild wird nach dem Anlegen hochgeladen (dann gibt es eine Produkt-ID). Die
JPEG-Aufbereitung ist jetzt eine freie Funktion (productImageJPEG), von der
Detail- und Anlege-Ansicht gemeinsam genutzt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mindestbestand (global + je Lagerort) nur noch bei Charge-Artikeln: das Feld
verschwindet bei Menge je Lagerort und Einzelstuecken, Alt-Werte werden beim
Speichern geleert. Angezeigt/gespeichert wird in der Anzeigeeinheit
(Milliliter/Gramm/Stueck) statt in Packungen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Listen-Tab: zwei halbe Kacheln "Lebensmittel" und "Gegenstaende" (ProductListView
mit fixedType, clientseitig ueber isObject gefiltert wie im Web); Neuanlage
uebernimmt den Typ. Einzelstuecke bleiben unveraendert (reine Gegenstands-Welt).
Verlauf und "Letzte Bewegungen" auf der Startseite: Tippen auf eine Bewegung
oeffnet ueber den neuen Lader ProductByIdView das jeweilige Produkt. Der
artikel-eigene Verlauf bleibt ohne Link.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neuer Artikel: 3-Wege-Verwaltung fuer Gegenstaende (Menge je Lagerort /
Einzelstuecke / Verbrauchsgegenstand). Verbrauchsgegenstaende laufen wie
Lebensmittel als Charge (Einheit, Packung, MHD, Gruppe, Chargen-Ansicht,
Mindestbestand mit Einheit) - foodLike = kein Gegenstand ODER bulk. Menge-
Gegenstaende koennen jetzt ebenfalls einer Gruppe zugeordnet werden.
ProductDetailView entsprechend auf foodLike umgestellt; NewProductRequest
und ProductUpdateRequest um bulk/individual/base_unit erweitert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Gegenstaende (Verbrauchsmaterial wie Sonnencreme) koennen jetzt einen Mindestbestand haben - im Web-Produktformular ein Gesamt-Feld (Stueck) plus Mindestbestand je Lagerort, auf iOS ein Gesamt-Feld in der Detailansicht. Umrechnung wie bei Lebensmitteln (fuer Stueck-Artikel = Stueckzahl); iOS sendet min_stock nur bei Gegenstaenden, damit Lebensmittel-Modi unberuehrt bleiben. Backend/Einkaufsliste unterstuetzen das bereits. Namen (Lebensmittel/Gegenstaende) bleiben.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>