Commit Graph
188 Commits
Author SHA1 Message Date
ScarriffleandClaude Opus 5 254737086e Sitzungsdauer einstellbar, bis hin zu unbegrenzt
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>
2026-09-24 23:15:46 +02:00
ScarriffleandClaude Opus 5 2b9b98dd14 Etiketten-Vorlagen: die ganze Zeile klappt auf und zu
Aufklappen ging nur ueber ein kleines Stift-Symbol am rechten Rand - und dass
derselbe Knopf auch wieder schliesst, sah man ihm nicht an.

Jetzt ist die Zeile selbst der Schalter, mit einem Pfeil links, der den Zustand
zeigt (wie in der Einzelstueck-Tabelle). Der Stift faellt damit weg; die
Knoepfe rechts - CSV, .lbx, Loeschen - bleiben eigenstaendig und loesen das
Auf- und Zuklappen nicht mit aus.

Dafuer eine kleine Klasse .zeilen-schalter: .link-btn waere naheliegend gewesen,
faerbt aber alles im Akzent - Name und graues Beiwerk sollen ihre Farben
behalten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 17:30:03 +02:00
ScarriffleandClaude Opus 5 c741df5963 Etiketten-Vorlagen: gespeicherte Exporte mit frei waehlbaren Spalten
Der Etikettenexport konnte genau eine Sache, und zwar unveraenderlich: sechs
feste Spalten ueber Einzelstuecke. Was fehlte, war genau das, was ein Etikett
brauchbar macht - die eigenen Felder. Eine Kabellaenge oder die Kapazitaet einer
Powerbank steht laengst am Artikel, kam aber in keiner CSV vor. Und jede
Zusammenstellung war beim naechsten Mal wieder weg: Kategorien im React-State,
Trennzeichen im localStorage, Spalten hartkodiert.

Jetzt gibt es benannte Vorlagen mit eigener Spaltenwahl und Reihenfolge, fuer
Einzelstuecke, Artikel und Lagerorte. Eine Tabelle nach dem Vorbild von
DashboardLayout: benannt, Nutzlast als JSON-Text. Ohne user_id - eine Vorlage
gehoert allen, nicht einem Konto.

Spaltenschluessel fuer eigene Felder ist die ID, nicht der key: FieldDefinition.key
ist nur JE KATEGORIE eindeutig, zwei Kategorien duerfen beide "kapazitaet" fuehren.

Die CSV entsteht jetzt im Server statt im Browser. Mit waehlbaren Spalten,
eigenen Feldern und drei Bezugsarten gehoert das an eine Stelle - und zwar
dorthin, wo es sich pruefen laesst. Formel-Injektionsschutz, BOM und CRLF sind
aus der Browser-Fassung mitgenommen. Der Grund, aus dem der QR-Inhalt bisher im
Browser entstand ("nur die Oberflaeche kennt die oeffentliche Adresse"), gilt
allein fuer Lagerorte; dafuer reicht sie ihre Herkunft als Parameter durch.

Die beiden alten Abschnitte weichen den Vorlagen. Beim ersten Start entstehen
zwei mitgelieferte, die genau die bisherigen Spalten fuehren - wer nichts
einstellt, bekommt dasselbe Ergebnis wie vorher.

Dazu laesst sich eine P-touch-Datei hinterlegen. Geprueft wird ueber Endung und
ZIP-Inhalt (label.xml), nicht ueber den Content-Type: Fuer .lbx meldet der
Browser meist application/octet-stream. Eine .lbx merkt sich den Pfad zu ihrer
Datenquelle - auf einem fremden Rechner muss die CSV in P-touch einmal neu
verknuepft werden. Das laesst sich von hier aus nicht loesen; die Datei
dabeizuhaben erspart trotzdem das Suchen.

Sicherung auf Version 4: Vorlagen kommen mit, Kategorien als Pfad und eigene
Felder als Label, weil IDs zwischen zwei Instanzen nicht dieselben sind. Die
.lbx als Base64 - das erste Binaerformat in dieser Sicherung, vertretbar bei
wenigen Kilobyte und einer Vorlage, die ohne ihre Datei nur eine halbe waere.

23 neue Tests (343 gesamt), dazu im Browser durchgespielt: Kabel-Kategorie mit
eigenem Feld, Spalte hinzugefuegt, CSV mit "Länge (m)" und dem Wert, .lbx
hoch- und wieder heruntergeladen (Pruefsumme identisch), PNG und Nicht-ZIP
sauber abgelehnt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 16:07:49 +02:00
ScarriffleandClaude Opus 5 7372590171 Zubehoer: anklickbar, mit Lagerort, und mehr Platz
Die Liste war eine Sackgasse - man sah einen Namen und eine UID, kam aber
nirgends hin. Jetzt fuehrt der Name zum Artikel und die UID zu genau diesem
Stueck; beide Fragen kommen vor ("was ist das fuer ein Netzteil?" gegen "wo ist
dieses eine?").

Dazu der Lagerort je Teil. ItemKurz trug ihn schon, angezeigt wurde er nie -
dabei ist das bei Zubehoer die haeufigste Frage ueberhaupt.

Ab zwei Teilen nimmt der Block zwei Rasterspalten. In einer einzigen Spalte
(minmax 240px) brach schon ein mittellanger Name um.

Und ein Fehler von mir: Die "Gehoert zu"-Zeile war ein rohes <a href> und lud
damit die ganze Seite neu, statt im Router zu navigieren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 15:01:04 +02:00
ScarriffleandClaude Opus 5 a82405b92a Zubehoer-Liste: langer Name schiebt den Loesen-Knopf nicht mehr um
.clean-list li bricht bewusst um (flex-wrap), damit lange Inhalte nicht aus der
Karte laufen. In der Zubehoer-Liste traf das den Loesen-Knopf: Bei einem Namen
wie "Shooter Grip fuer WII Controller" landete das x allein in einer zweiten
Zeile unter dem Namen.

Jetzt waechst der Textblock (flex:1, min-width:0) und alles rechts davon bleibt
stehen. Der Name bricht innerhalb seines Blocks um, das x bleibt rechts und
mittig. overflow-wrap: anywhere fuer den Fall, dass ein einzelnes Wort breiter
ist als die Spalte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 14:58:46 +02:00
ScarriffleandClaude Opus 5 8a56b13036 Einzelstueck-Details: "Gekauft bei" steht in einer Reihe mit den Nachbarn
Im aufgeklappten Bereich stehen die Beschriftungen ueber den Feldern. Beim
Auswahlfeld "Gekauft bei" klebte sie stattdessen links daneben, waehrend
"Gekauft am" und "Garantie bis" darueber standen - die Zeile sah dadurch
verrutscht aus.

Ursache war nicht das Markup, das ist identisch zu den Nachbarn, sondern
td select { width: auto } (styles.css). Die Regel ist fuer die kompakte
Tabellenzeile richtig, im Detailbereich schrumpft sie das Feld aber auf
Inhaltsbreite, sodass es neben seine Beschriftung passt.

Nur direkte Kinder eines label: Waehrung und Preisvorschlag sitzen in eigenen
Zeilen-Behaeltern und bleiben schmal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 14:22:51 +02:00
ScarriffleandClaude Opus 5 1553e36b78 Artikelformular: Typ und Verwaltung als Auswahlliste
Zwei Umschalter mit zusammen fuenf Knoepfen beanspruchten zwei volle Zeilen im
Formular - der dreifache ("Menge je Lagerort / Einzelstuecke /
Verbrauchsgegenstand") brauchte dafuer sogar eine eigene Umbruch-Regel. Als
Auswahllisten stehen sie kompakt untereinander und sehen aus wie die
Kategorie-Auswahl direkt dazwischen.

Am Verhalten aendert sich nichts: changeModus und setObjMode bekommen denselben
Wert, nur aus onChange statt onClick.

.seg-wrap wird dadurch nirgends mehr gebraucht und faellt weg. .segmented und
.seg-label bleiben - die nutzen Mindestbestaende, Produkte und Kategorien
weiterhin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 14:16:01 +02:00
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 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 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 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 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 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 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 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 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 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 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
ScarriffleandClaude Opus 4.8 749848360b Web: Text wieder markierbar/kopierbar, nur der I-Beam-Cursor bleibt weg
user-select:none war zu viel - es hat das Kopieren von Artikel-/Markennamen aus
Tabellen verhindert. Cursor und Markierbarkeit sind unabhaengig: cursor:default
(aus 6f4f887) nimmt den I-Beam weg, Text bleibt dank Standard-user-select aber
markierbar. Also Pfeil-Cursor UND kopierbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 10:04:52 +02:00
ScarriffleandClaude Opus 4.8 69adc6d910 Web/nginx: index.html nicht cachen, gehashte Assets dauerhaft
Ohne Cache-Header cachte der Browser die alte index.html -> neue Deploys (neue
Asset-Hashes) kamen nie an, obwohl der Code laengst stimmte. Jetzt: index.html
Cache-Control:no-cache (immer revalidieren), gehashte JS/CSS/Bilder immutable +
1 Jahr. Deploys sind damit sofort sichtbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 09:59:32 +02:00
ScarriffleandClaude Opus 4.8 6895d8ec1a Web: reiner UI-Text nicht mehr markierbar (fuehlt sich nicht mehr wie Eingabefeld an)
Cursor-Fix allein reichte nicht: das Blau-Hinterlegen beim Markieren wirkte weiter
wie ein Eingabefeld. Jetzt user-select:none auf body; Eingabefelder, textarea,
contenteditable sowie Code/Token (und .selectable) bleiben markierbar/kopierbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 09:56:15 +02:00
ScarriffleandClaude Opus 4.8 6f4f8871c3 Web: Text-Cursor (I-Beam) nur noch auf echten Eingabefeldern
body bekommt cursor:default (vererbt) -> ueber Text erscheint der Pfeil statt des
I-Beams, damit nicht-editierbarer Text nicht wie ein Eingabefeld aussieht. I-Beam
nur noch auf Text-/Zahlenfeldern & textarea/contenteditable; Hand fuer Links,
Selects, Checkbox/Radio und deren Labels. Klickbares mit eigener Regel (Buttons,
Sortier-Header, Kacheln, klickbare Zeilen) bleibt unveraendert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 09:48:07 +02:00
ScarriffleandClaude Opus 4.8 9cc3e325bb Web: Kassenzettel-Trefferschwelle in den Einstellungen
Prozent-Feld fuer receipt_match_threshold (Default 45) analog zur Ablaufwarnung;
speichert ueber den generischen Settings-Endpunkt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 18:26:20 +02:00
ScarriffleandClaude Opus 4.8 d05727779b Web: Ring-Karten – Legende in eigenem Scroll-Bereich (Ring bleibt stehen)
align-self:stretch an der <ul> war zu unzuverlaessig (u.a. wegen flex-wrap). Jetzt
kanonisches Muster: Ring (feste Groesse, mittig) + eigener .chart-legend-scroll
(flex:1 1 0; min-height:0; overflow-y:auto) in einem align-items:stretch-Container
ohne Umbruch. Nur die Legende scrollt, der Ring bleibt stehen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 18:17:10 +02:00
ScarriffleandClaude Opus 4.8 30e570095f Web: Dashboard-Karten fuellen die Rasterzelle (.dash-card height:100%)
Ohne definierte Kartenhoehe reichte die feste Rasterzellen-Hoehe nie an Body/
Chart/Legende durch - die Flex-Kette (min-height:0/overflow) hatte nichts zum
Andocken, weshalb die ganze Karte scrollte statt nur der Legende. Mit height:100%
greift die Legenden-Scroll-Regel: der Anteils-Ring bleibt stehen, nur die Legende
scrollt. Betrifft auch die internen Tabellen-Scrolls der anderen Karten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 18:10:58 +02:00
ScarriffleandClaude Opus 4.8 2f6c88ce08 Web: Anteils-Ringe – Ring bleibt stehen, nur Legende scrollt; Ohne-MHD-Schalter in die Konfig
Bei vielen Kategorien scrollte die ganze Karte (Ring wanderte weg). Jetzt fuellt
die Legende die Hoehe des Ring-Bereichs und scrollt in sich; der Ring bleibt
mittig fixiert.

Ablauf-Anteile: der Ohne-MHD-Schalter sass als Checkbox auf der Karte (localStorage).
Jetzt als boolean-Konfig im Zahnrad-Dialog (Vorgabe an), wie die anderen Optionen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 17:47:46 +02:00
ScarriffleandClaude Opus 4.8 9b0771ae0e Kategorien-Anteile: einstellbare Kategorie-Tiefe (Unterkategorien zusammenfassen)
/dashboard/by-category bekommt depth: rollt jede Artikel-Kategorie auf ihren
Vorfahren der gewuenschten Stufe hoch (1 = oberste Ebene), ohne depth zaehlt
jede zugeordnete Kategorie einzeln (feinste). Neue Karten-Konfig select
Kategorie-Tiefe (Feinste / oberste / bis 2. / bis 3. Stufe). KartenConfig zeigt
jetzt auch bei select den Hinweis.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 17:39:30 +02:00
ScarriffleandClaude Opus 4.8 1cddb4432c Web: Kategorien-Anteile-Karte umschaltbar auf Detailansicht (keine Gruppe Andere)
Neue Karten-Konfig-Option Detailansicht: mitFarben(..., { alle:true }) faerbt
jede Kategorie einzeln (Festpalette vorne, danach erzeugte Farbtoene) statt den
Rest zu Andere zu buendeln. Umschaltbar ueber das Zahnrad der Karte.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 17:34:27 +02:00
ScarriffleandClaude Opus 4.8 55db1d1363 Web: Einkaufsliste-Karte zum Abhaken (wie die volle Seite)
Die Karte zeigte die Bedarfe als Tabelle ohne Haekchen. Jetzt als abhakbare
Checklist (Gesamt + je Lagerort) mit Durchstreichen beim Abhaken - dieselbe
Bedienung wie die Einkaufslisten-Seite. Abhaken ist sitzungslokal (wie dort
auch, noch nicht dauerhaft gespeichert).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 14:36:04 +02:00
ScarriffleandClaude Opus 4.8 3e10e254c8 Web: Dashboard-Karten wie HomeAssistant konfigurieren + Einkaufsliste je Lagerort
Karten-Optionen (Artikel, Lagerort, Zeitraum) sassen als einzelne Dropdowns im
Kartenkopf. Jetzt: deklaratives config-Schema je Kartenart und ein Zahnrad-Dialog
(KartenConfig) im Anordnen-Modus - der Kopf zeigt sonst nur noch Titel/Papierkorb.
Felder: product, location, zeitraum (erweiterbar: select, boolean). Werte bleiben
in props/tage, keine Migration.

Einkaufsliste-Karte laedt zusaetzlich shoppingByLocation und zeigt Gesamt + Bedarfe
je Lagerort. Ueber die Karten-Konfig waehlbarer Lagerort: leer = alle Orte, gesetzt
= Gesamt + nur dieser Ort.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 14:29:09 +02:00
ScarriffleandClaude Opus 4.8 f3bab15362 Mindestbestaende: Bestand je Lagerort anzeigen (nicht nur Gesamt)
Per-Lagerort-Zeilen (Produkt und Gruppe) zeigten '-' beim Bestand, weil die Seite
nur den Gesamtbestand kannte. Jetzt liefert LocationMinStockOut.stock den Bestand
AN DEM Ort (inkl. Unterorte) mit - bei Produkten in Basiseinheiten, bei Gruppen in
der Gruppen-Einheit (analog zu ProductOut.stock/GroupOut.stock). Berechnet ueber
location_subtree_stock_base; die Weboberflaeche zeigt ihn wie beim Gesamt-Bestand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 13:57:40 +02:00
ScarriffleandClaude Opus 4.8 4ed33e221f Web: Chargen-Seite faerbt Zeilen nach MHD (gelb = bald ablaufend, rot = abgelaufen)
Wiederverwendung von expiryRowClass + .row-warn/.row-danger wie auf der
Artikelseite. Warnfrist kommt aus der Einstellung expiry_warning_days (Default 7).
Die MHD-Faerbung bleibt auch bei ausgewaehlter/gehoverter Zeile sichtbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 13:45:19 +02:00
ScarriffleandClaude Opus 4.8 f981f41d94 Einzelstueck-QR ueber Custom-Scheme statt Universal Link
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>
2026-07-30 11:34:37 +02:00
ScarriffleandClaude Opus 4.8 48d47f1d39 Web: Mindestbestand-Ort per Dropdown verschieben + OFF-Panel-Regression behoben
MinStock: die ORT-Spalte ist jetzt ein Dropdown (Gesamt/Lagerort). Auswahl
verschiebt den Bedarf (moveRow) statt ihn zu duplizieren - mit Rueckfrage, falls
der Zielort schon belegt ist. Produktwerte werden dabei umgerechnet, Gruppen nicht.

OFF-Panel: meine schmalere Formularspalte hatte den Vergleichs-Button abgeschnitten
und die OFF-Tabelle aus der Karte laufen lassen. Tabelle jetzt in .table-wrap
(scrollt/stapelt), Button-Text darf umbrechen, Formularspalte etwas breiter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 09:32:45 +02:00