Files
ocr_scanner/README.md
T
TanerUsluandClaude Opus 5 f0705a010c Doku: Zaehlfunktion und ihre Grenzen auf das neue Verfahren aktualisiert
Beschreibt oertlichen Kontrast statt Otsu (Normierung, Glaettung,
oertlicher Hintergrund, Massstab relativ zur groessten Flaeche) und die
neuen Grenzen: Untergrund muss sich abheben, gleichmaessige Ausleuchtung
hilft, auseinandergelegt zaehlt besser als aufgehaeuft, stark
unterschiedlich grosse Teile verfaelschen die Hochrechnung.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 16:21:35 +02:00

676 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RAM-Sortierhilfe
Browser-App, die per Handykamera gebrauchte Server-RAM-Module erkennt und
beim physischen Sortieren am Tisch anleitet: Modul vor die Kamera halten, die
App nennt den Stapel.
Vor dem Scannen wählt der Nutzer eine von vier Funktionen — Strichcode,
QR-Code, Text erkennen oder Zählen (siehe „Funktionsweise" unten). Jede
Funktion nutzt ausschließlich ihre eigene Quelle; anders als früher weicht die
App nicht mehr automatisch auf Texterkennung aus, wenn kein Code gefunden
wird. Die vierte Funktion, „Zählen", gehört nicht zum eigentlichen Sortieren:
Sie bucht nichts, sondern zeigt beim Auslegen von Kleinteilen (z. B.
Festplattenschrauben) fortlaufend deren Anzahl an.
Zwei Module gelten als zusammengehörig, wenn sie denselben Barcode-Inhalt
tragen — ein Barcode ist exakt gelesen, das ist der zuverlässigste Maßstab.
Jeder Stapel führt dafür die Menge der Barcode-Zeichenketten, die bei seinen
*aktuell* ihm zugeordneten Modulen gelesen wurden (`stack.codes`); diese Menge
wird nicht angesammelt, sondern aus den Codes der zugeordneten Einträge
(`entry.codes`) abgeleitet — genau wie der Stapelzähler (siehe „Sitzungssicherung
im Detail" unten). Sortiert der Nutzer ein Modul um oder entfernt es, verliert
der alte Stapel dessen Codes deshalb sofort wieder; ein bereinigter Fehler
zieht kein weiteres Modul mehr fälschlich an. Trägt ein neu gescanntes Modul
einen Code, den ein Stapel bereits kennt, ist dieser Stapel Kandidat — aber
nur, wenn die technischen Angaben beider Seiten sich dabei nicht widersprechen
(`specsCompatible` in `src/spec.js`, siehe unten); ein Codetreffer allein
genügt nicht, sonst könnte ein Code, der gar keine Teilenummer ist, zwei
technisch verschiedene Module zusammenführen (siehe „Grenzen"). Genau ein
solcher Kandidat gehört dorthin, auch wenn die technischen Angaben
unvollständig sind (`proposeAssignment` in `src/session.js`). Auf Etiketten
stehen oft zwei Codes, von denen einer die Seriennummer ist, die bei jedem
Modul anders ist: Sie läuft beim Codeabgleich einfach ins Leere, während die
danebenstehende Teilenummer trägt — kein Sonderfall, nur eine Konsequenz des
Mengenvergleichs.
Kennt kein Stapel einen der gelesenen Codes ohne Widerspruch (typischerweise
beim ersten Modul eines neuen Stapels, aber auch dann, wenn ein Codetreffer an
der Widerspruchsprüfung scheitert), vergleicht die App stattdessen über alle
Merkmale, die
bei Modul und Stapel bekannt sind — Kapazität, Bauform, Rank, Geschwindigkeit
und, sofern gelesen, Hersteller-Teilenummer (`specsCompatible` in
`src/spec.js`); ein auf einer Seite fehlendes Merkmal wird dabei übersprungen,
nicht als Widerspruch gewertet. Ein Stapel gilt dabei nur als Kandidat, wenn
er mit dem Modul mindestens ein tatsächlich erkanntes Merkmal teilt
(`sharesSetField` in `src/session.js`) — ein Stapel ganz ohne erkannte Angaben
(etwa aus einem manuell entschiedenen Rot-Fall) ist deshalb nie automatisch
treffend, sonst würde er lautlos jedes folgende Modul an sich ziehen. Passt
danach genau ein Stapel, wird das Modul zugewiesen; passen mehrere oder mehrere
über einen bekannten Code, fragt die App nach. Das hat eine bewusst in Kauf
genommene Folge: Konnte die Teilenummer nicht gelesen werden — der häufige
Fall bei überklebten oder beschädigten Etiketten — und passt anhand der
übrigen Merkmale nur ein Stapel, landet das Modul dort, auch wenn seine
tatsächliche Teilenummer eine andere wäre. Diese Abwägung ist Absicht: Ohne
sie müsste die App bei jedem zweiten Modul nachfragen, sobald die
Texterkennung keine Teilenummer liefert.
Die Erkennung läuft vollständig lokal im Browser — kein Server, keine Cloud,
keine Daten verlassen das Gerät.
## Funktionsweise
0. **Funktion wählen.** Vor dem ersten Scan (und jederzeit erneut über die
antippbare Anzeige in der Scan-Ansicht) wählt der Nutzer eine von vier
Funktionen. Kennung, Beschriftung, gelesene Codearten und Verhalten stehen
ausschließlich in `src/scan-modes.js` — es gibt keine zweite Stelle, an der
das steht:
| Funktion | Liest | Laufende Suche/Zählung | Texterkennung |
|---|---|---|---|
| **Strichcode** | Code128, Code39, Code93, ITF, EAN-13, EAN-8, UPC-A, UPC-E, Codabar | ja (Codesuche) | nein |
| **QR-Code** | QRCode, MicroQRCode, RMQRCode, DataMatrix, Aztec, PDF417 | ja (Codesuche) | nein |
| **Text erkennen** | keine Codes | nein | ja, auf Knopfdruck |
| **Zählen** | keine Codes | ja (Zählung, keine Buchung) | nein |
„QR-Code" heißt auf Wunsch des Auftraggebers „QR-Code", liest aber bewusst
nicht nur QR-Varianten, sondern auch DataMatrix, Aztec und PDF417. Der
Grund: Die 2D-Codes auf den RAM-Etiketten sind DataMatrix, nicht QR — eine
auf QR beschränkte Funktion fände auf dieser Ware nichts. Die interne
Kennung heißt weiterhin `qrcode` (aus historischen Gründen, siehe
`src/scan-modes.js`), und der Name ist Absicht — es ist der Wunsch des
Auftraggebers für seinen Sprachgebrauch.
„Modul scannen" erscheint nur in „Text erkennen" — in „Strichcode" und
„QR-Code" sucht die App ohnehin laufend, ein Knopf ohne Texterkennung hätte
dort nichts zu tun. Der Zielrahmen hebt sich in „Strichcode" und „QR-Code"
hervor, sobald ein passender Code im Bild ist; in „Text erkennen" bleibt er
schlicht, weil dort nichts laufend erkannt wird. Trotzdem ist der Rahmen
auch dort wirksam: „Modul scannen" liest genau den Bildausschnitt innerhalb
des Rahmens, in voller Kameraauflösung, nicht mehr das gesamte,
heruntergerechnete Kamerabild (`grabFrameRegion` in `src/camera.js`
derselbe Ausschnitt, den auch die laufende Barcode-Suche benutzt). Ein
Wechsel der Funktion während des Sortierens (z. B. bei gemischter Ware)
gilt sofort: laufende Suche startet oder stoppt, der Knopf erscheint oder
verschwindet.
Liegt beim Start eine gesicherte Sitzung vor, erscheint zuerst die Frage
nach dem Fortsetzen (siehe Punkt 6 unten) und erst danach die Funktionswahl
— die Kamera startet in jedem Fall erst, nachdem beides geklärt ist.
Technisch bekommen „Strichcode" und „QR-Code" eine Texterkennung
hineingereicht, die sofort leeren Text liefert, ohne Tesseract anzustoßen;
„Text erkennen" bekommt eine Barcode-Dekodierung hineingereicht, die sofort
eine leere Liste liefert; „Zählen" bekommt beides leer hineingereicht
(`buildRecognitionAdapters` in `src/scan-recognition.js`). Die
Erkennungs-Pipeline selbst (`src/pipeline.js`, Punkte 13 und 5 unten)
bleibt dabei unverändert und kennt keine Funktionen — die drei
code-/textbasierten Funktionen unterscheiden sich ausschließlich darin,
welche Adapter ihr übergeben werden. „Zählen" ruft diese Pipeline im
normalen Betrieb gar nicht erst auf (siehe Punkt 4 unten) — es gibt dort ja
nichts zu buchen, das leere Adapterpaar dient nur als Absicherung für den
seltenen Ersatzweg ohne Kamera (Punkt 7).
1. **Barcode-Dekodierung** (nur in „Strichcode" und „QR-Code"). Die zur
Funktion gehörenden Codearten werden laufend aus dem Kamerabild dekodiert
(`zxing-wasm`, `decodeBarcodes` in `src/barcode.js` bekommt die Codearten
der gewählten Funktion übergeben). Das ist exakt, im Gegensatz zu
Texterkennung. Werden mehrere Barcodes im Bild gefunden, die sich
widersprechen (nicht dieselbe Teilenummer), gilt das als mehrdeutig — die
App rät nicht, sondern fragt nach. Das gilt nur für Barcodes mit einem
bekannten Nummernschema; widersprechen sich mehrere Barcodes mit
unbekanntem Nummernschema, wird keine ihrer Teilenummern übernommen.
2. **Teilenummer-Decoder** (nur relevant in „Strichcode"/„QR-Code" — „Text
erkennen" liest nie einen Barcode, siehe Punkt 0). Aus einer Hersteller-PN
wie `M386A8K40BM1-CRC4Y` werden Kapazität, Bauform und Geschwindigkeit
tabellengesteuert abgeleitet. Gelingt das, entfällt OCR vollständig, das Ergebnis ist grün.
Kennt der Decoder das Nummernschema nicht (siehe „Grenzen“ unten), ist das
allein noch kein Grund für Rot: Zum Sortieren muss keine Kapazität bekannt
sein, eine exakt gelesene Teilenummer genügt, um ein Modul wiederzuerkennen
(`recognize` in `src/pipeline.js`). Findet die anschließende Texterkennung
dazu auch keine Kapazität, ist das Ergebnis trotzdem grün, nur eben ohne
abgeleitete Angaben in der Rückmeldung. Ist einer der gelesenen Codes
bereits einem Stapel dieser Sitzung bekannt, steht die Zuordnung ohnehin
fest — dann entfällt auch die Texterkennung, weil sie nichts mehr beitragen
könnte, das über die Zuordnung entscheidet. Bei einem tatsächlich neuen
Modul läuft sie dagegen wie gehabt, denn dort liefert sie die lesbare
Beschriftung des neuen Stapels.
3. **Texterkennung** (ausschließlich in „Text erkennen", ausgelöst durch
„Modul scannen"). Anders als früher ist das kein automatischer Rückfall
vom Barcode-Weg mehr, sondern die einzige Quelle dieser Funktion — sie
liest ohnehin nie einen Barcode (siehe Punkt 0). Gelesen wird der
Bildausschnitt innerhalb des Zielrahmens, in voller Kameraauflösung
(`grabFrameRegion`, siehe Punkt 0) — nicht mehr das ganze, auf 1280 Pixel
heruntergerechnete Kamerabild. Das Bild wird dafür
in Graustufen gewandelt und mit einem **Otsu-Schwellwert** in Schwarz/Weiß
aufbereitet: Der Schwellwert wird aus der Helligkeitsverteilung des
gesamten Bildes bestimmt, nicht aus einer festen Kontrastspreizung
zwischen hellstem und dunkelstem Pixel. Das macht die Aufbereitung robust
gegen einzelne Lichtreflexe auf glänzenden Metalletiketten, die eine reine
Min/Max-Spreizung leicht kippen würden. Die Zeichen-Whitelist von
Tesseract (`tessedit_char_whitelist` in `src/ocr.js`) lässt Groß- **und**
Kleinbuchstaben, Ziffern sowie die auf Etiketten üblichen Satz- und
Sonderzeichen zu — nicht mehr nur Großbuchstaben. Das OCR-Ergebnis wird
anschließend gegen die bekannten Spec-Werte abgeglichen, was die
typischen Verwechslungen (0/O, 1/I, 8/B, …) auflöst; das kann durch die
breitere Whitelist bei den Codeteilen (Kapazität, Rank, Geschwindigkeit,
Teilenummer) etwas an Genauigkeit kosten, weil Tesseract jetzt z. B.
zwischen „O“ und „o“ unterscheiden muss — eine bewusst in Kauf genommene
Abwägung, damit der vollständige Etikettentext lesbar wird. Da in dieser
Funktion nie eine per Barcode gelesene Teilenummer vorliegt, entscheiden
hier ausschließlich die aus dem erkannten Text abgeleiteten technischen
Angaben (Kapazität, Bauform, Rank, Geschwindigkeit) über die
Stapelzuordnung (`specsCompatible` in `src/spec.js`) — anders als in
„Strichcode"/„QR-Code", wo meist die exakte Teilenummer trägt.
Der vollständige, ungefilterte Erkennungstext (nicht nur die daraus
verstandenen Felder) wird zusätzlich mitgeführt (`recognize` in
`src/pipeline.js` liefert ihn als `rawText` zurück) und je Eintrag
gespeichert — rein additiv, ohne jeden Einfluss auf die Stapelzuordnung
(siehe „Grenzen" unten). In Funktion „Text erkennen" bleibt die
Treffer-Rückmeldung deshalb stehen, bis der Nutzer sie wegtippt, und zeigt
den vollen erkannten Text an, statt sich nach gut einer Sekunde von selbst
auszublenden wie in den beiden Barcode-Funktionen (`showResult` in
`src/ui/result-overlay.js`). Der Rohtext jedes Eintrags lässt sich später
auch in der Sitzungsliste nachlesen.
4. **Zählen** (ausschließlich in Funktion „Zählen"). Reine Anzeige, ohne
jeden Bezug zur Sitzung: Solange die Kamera läuft, wird der Zielrahmen
mehrmals pro Sekunde ausgewertet und die Anzahl der darin gefundenen
Fundstücke groß im Bild eingeblendet (`countObjects` in
`src/count-objects.js`, aufgerufen über denselben Zeitgeber/dieselbe
Überlappungssperre wie die Barcode-Dauersuche — siehe
`continuousSearchLoop` in `src/main.js`). Es gibt in dieser Funktion
keinen „Modul scannen"-Knopf, keine Treffer-Rückmeldung und keine
Stapel-Zuweisung; die Zahl wird nirgends gespeichert, nicht einmal
flüchtig für die Dauer der Sitzung.
Kein Bilderkennungsmodell: Der **Otsu-Schwellwert** (`threshold.js`),
den die Texterkennung weiterhin nutzt, taugt für „Zählen" nicht — er
setzt voraus, dass Objekt- und Untergrundfläche etwa gleich groß sind.
Am echten Foto des Auftraggebers (rund zwanzig silberne
Festplattenschrauben auf dunklem Leder) belegen die Schrauben unter
einem Prozent der Fläche; Otsu rutschte deshalb auf den höchsten
Helligkeitswert und erklärte nur die hellsten Punkte der Ledernarbung
zum Objekt (gemessenes Fehlverhalten: 76712 statt rund zwanzig). Über
die Helligkeit allein sind Schraube und Untergrund an diesem Foto gar
nicht trennbar.
`countObjects` (`src/count-objects.js`) verwendet stattdessen
**örtlichen Kontrast**: Der Ausschnitt wird auf 800 Bildpunkte Breite
normiert (schmalere Ausschnitte bleiben unverändert — macht alle
folgenden Maße unabhängig von der Kameraauflösung), in Graustufen
gewandelt und mit einem Kastenfilter geglättet (unterdrückt feine
Untergrundnarbung, über ein Summenbild für radius-unabhängige
Laufzeit). Ein zweiter, deutlich größerer Kastenmittelwert liefert den
**örtlichen Hintergrund** an jeder Stelle — das fängt ungleichmäßige
Ausleuchtung ab. Ein Bildpunkt gilt als Objekt, wenn er um mehr als
einen festen Betrag heller **oder** dunkler ist als sein örtlicher
Hintergrund (beide Richtungen werden berechnet; es gewinnt die mit den
meisten gefundenen Einzelflächen — Details siehe unten). Anschließend
werden zusammenhängende Objektflächen ermittelt
(Connected-Component-Labeling mit einer eigenen Arbeitsliste statt
Rekursion, damit ein rund eine Viertelmillion Bildpunkte großer
Ausschnitt nicht in den Stapelüberlauf läuft), winzige Flächen als
Rauschen verworfen und nur Flächen ab einem festen **Anteil der
größten gefundenen Fläche** behalten — bewusst relativ, nicht absolut,
damit ein näher oder weiter weg gehaltenes Kamerabild die Zählung nicht
verstellt. Auffällig große Flächen (mehrere berührende Teile) werden
auf ein Vielfaches der typischen (Median-)Einzelteilgröße
hochgerechnet. Details, alle verwendeten Schwellen und die Messung am
echten Foto: `.superpowers/sdd/counting-rework-report.md`.
5. **Stapel-Zuweisung.** Grün (Barcode) und Gelb (OCR) laufen ohne Eingabe
durch; nur bei roter Konfidenz (nichts Eindeutiges erkannt, widersprüchliche
Barcodes oder widersprüchliche unbekannte Barcodes ohne jede OCR-Kapazität)
oder mehrdeutiger Stapelzuordnung fragt die App nach.
6. **Sitzungsfortsetzung beim Start.** Findet die App beim Laden eine in
`localStorage` gesicherte, nicht leere Sitzung vor, wird sie nicht
stillschweigend übernommen, sondern zur Fortsetzung oder zum Verwerfen
angeboten (`askResumeSession` in `src/ui/resume-dialog.js`, verdrahtet über
`initResumeOffer()` in `src/main.js`). Erst „fortsetzen“ stellt Stapel,
Zähler und die „Zuletzt“-Zeile wieder her; „verwerfen“ löscht den
gesicherten Stand und die App startet leer.
7. **Ersatzweg ohne Kamera.** Lässt sich die Kamera nicht starten (z. B.
verweigerte Berechtigung), bleibt die App bedienbar: eine dauerhaft
sichtbare Schaltfläche „Bild auswählen“ öffnet stattdessen die native
Dateiauswahl (`setFilePickerVisible` in `src/ui/scan-view.js`). Die App
versucht danach nicht automatisch erneut, die Kamera zu starten - der
Kamera-Hinweistext bleibt für den Rest der Sitzung stehen und wird nicht
von der OCR-Verfügbarkeitsanzeige überschrieben.
Barcode- und OCR-Adapter sind gegen hängende Aufrufe abgesichert: Die
Erkennungs-Pipeline (`src/pipeline.js`) bricht einen Barcode-Versuch nach 10
Sekunden und einen OCR-Versuch nach 20 Sekunden ab und behandelt das wie ein
leeres Ergebnis, statt die App blockiert zu lassen. Diese Grenzen sind großzügig
bemessen (sie müssen auch das einmalige Laden der WebAssembly-/Sprachmodule
beim ersten Scan einer Sitzung abdecken) und keine Leistungserwartung.
## Installation
```bash
npm install
```
## Entwicklung
```bash
npm run dev
```
Der Server bindet an alle Interfaces; VCH stellt eine Preview-URL mit SSL
bereit. **HTTPS ist Pflicht** — Kamerazugriff (`getUserMedia`) verlangt eine
gesicherte Verbindung; über eine unverschlüsselte `http://`-Adresse verweigert
der Browser die Kamera grundsätzlich, unabhängig davon, ob die App selbst
korrekt funktioniert.
## Tests
```bash
npm test
```
165 Tests, alle grün (Stand dieses Dokuments). Getestet werden ausschließlich
die reinen Module ohne Browser-Zugriff: Teilenummer-Decoder, Spec-Normalisierung
und toleranter Vergleich, OCR-Bildaufbereitung (Otsu-Schwellwert, jetzt in
`threshold.js`) und -Feldextraktion, Stapel-Zuweisung, die Erkennungs-Pipeline
samt Zeitgrenzen, Mehrdeutigkeitsbehandlung und dem additiven OCR-Rohtext
(`rawText` wird nur zurückgegeben, wenn die Texterkennung tatsächlich lief,
und beeinflusst nachweislich nie die Stapelzuordnung), die Sitzungssicherung
samt Prüfung eines wiederhergestellten Zustands (einschließlich
`entry.rawText`), die vier Scan-Funktionen (`scan-modes.js` — Codearten je
Funktion, DataMatrix/Aztec/PDF417 in „QR-Code") sowie die je Funktion
tatsächlich aufgerufenen Adapter (`scan-recognition.js` — Texterkennung läuft
nachweislich nie in „Strichcode"/„QR-Code", Barcode-Dekodierung nachweislich
nie in „Text erkennen"/„Zählen"). Dazu die Zähl-Berechnung selbst
(`count-objects.js` — leeres Bild, einzelne und mehrere getrennte Flächen,
verworfenes Rauschen, Hochrechnung berührender Teile ohne ein nur leicht
größeres Einzelteil zu verdoppeln, helle wie dunkle Objekte, ein Bild ohne
Bildpunkte). Kamera, Barcode-/OCR-Adapter selbst, die Auswahl-Oberfläche und
die Verdrahtung in `main.js` laufen nur im echten Browser und werden dort
manuell geprüft (siehe „Am Gerät noch zu prüfen" unten) — `node --test` kennt
kein DOM.
## Produktion
Zum lokalen Nachschauen ohne Passwort:
```bash
npm run build
npm run preview
```
`preview` hört auf `PORT` aus der Umgebung (Vorgabe `4173`, falls `PORT`
nicht gesetzt ist).
Im Betrieb läuft stattdessen `server.py` — siehe nächster Abschnitt.
## Passwortschutz
Die veröffentlichte Seite ist passwortgeschützt. Geprüft wird **im Server**,
nicht im Browser: Ohne gültiges Passwort gibt `server.py` nichts heraus —
weder die Seite noch das gebündelte Skript. Ein Passwortschutz allein im
Browser wäre wirkungslos, weil das Passwort dann im ausgelieferten Skript
stünde und dort mitgelesen werden könnte.
```bash
APP_PASSWORD=dein-passwort python3 server.py
```
- Das Passwort kommt ausschließlich aus der Umgebungsvariable `APP_PASSWORD`
(siehe `.env.example`). Es steht **nicht** im Quelltext.
- **Fehlt die Variable, startet der Server nicht** und meldet das im Klartext.
Das ist Absicht: Ein vergessener Eintrag soll auffallen, statt stillschweigend
eine ungeschützte Seite ins Netz zu stellen.
- Abgefragt wird per HTTP-Basic-Auth. Der Browser fragt **einmal pro Sitzung**,
nicht bei jedem Seitenaufruf — beim Sortieren wäre das sonst unbrauchbar.
- Der **Benutzername ist beliebig**; geprüft wird nur das Passwort. Beim
Vergleich wird eine zeitkonstante Prüfung verwendet.
- `/healthz` antwortet **ohne** Passwort mit `ok`. Das braucht die Überwachung
der Hosting-Umgebung, die den geschützten Dienst sonst für ausgefallen hielte.
Der Pfad gibt nichts über die Anwendung preis.
Das Passwort wandert bei einer Basic-Auth-Anmeldung mit jeder Anfrage über die
Leitung. Das ist unbedenklich, solange die Seite über HTTPS ausgeliefert wird —
worauf du bei der Veröffentlichung achten solltest.
## Technik
- Vanilla JavaScript (ES Modules), kein Framework
- [Vite](https://vite.dev/) als Entwicklungsserver und Bündler
- [`zxing-wasm`](https://github.com/Sec-ant/zxing-wasm) zur Barcode-Dekodierung;
welche Codearten gesucht werden, gibt die gewählte Funktion vor
(`src/scan-modes.js`)
- [`tesseract.js`](https://tesseract.projectnaptha.com/) für Texterkennung
- `node:test` für Tests, ohne zusätzliche Test-Bibliothek
## Aufbau
| Datei | Verantwortung |
|---|---|
| `src/spec.js` | Spec-Objekt, bekannte Werte, Fingerabdruck, toleranter Vergleich (Verwechslungstabelle für OCR-Fehler) |
| `src/pn-tables.js` | Herstellertabellen für den Teilenummer-Decoder |
| `src/pn-decoder.js` | Teilenummer → Spec-Felder |
| `src/ocr-extract.js` | OCR-Rohtext → Spec-Felder |
| `src/session.js` | Stapel halten, vorschlagen, buchen, umsortieren, zurücknehmen — inklusive der aus den Einträgen abgeleiteten, je Stapel bekannten Barcode-Inhalte (`stack.codes`), die vor dem Vergleich der technischen Angaben über die Zuordnung entscheiden, sofern diese Angaben nicht widersprechen |
| `src/pipeline.js` | Barcode → Teilenummer → OCR → Ampelfarbe, inkl. Zeitgrenzen, Mehrdeutigkeitsbehandlung und Kurzschluss über einen bereits bekannten Barcode-Inhalt (`deps.isKnownCode`) — kennt keine Scan-Funktionen; liefert zusätzlich (additiv) den ungefilterten OCR-Rohtext (`rawText`), leer, wenn keine Texterkennung lief |
| `src/scan-modes.js` | Die vier Scan-Funktionen (Strichcode, QR-Code, Text erkennen, Zählen): Kennung, deutsche Beschriftung, gelesene Codearten, laufende Suche ja/nein, Texterkennung ja/nein, laufende Zählung ja/nein — einzige Quelle, sowohl für die Auswahl-Oberfläche als auch für `main.js` |
| `src/scan-recognition.js` | Baut die an `recognize()` übergebenen Adapter anhand der gewählten Funktion: in „Strichcode"/„QR-Code" liefert die Texterkennung sofort leeren Text ohne Tesseract anzustoßen, in „Text erkennen" liefert die Barcode-Dekodierung sofort eine leere Liste ohne `zxing-wasm` anzustoßen, in „Zählen" liefern beide sofort leer (diese Funktion ruft `recognize()` im normalen Betrieb ohnehin nicht auf) |
| `src/threshold.js` | Otsu-Schwellwertbestimmung (Graustufen, Histogramm, Schwellwert) — reines Modul ohne Browser-Zugriff, aus `ocr.js` herausgezogen; gemeinsame Grundlage für `preprocess()` (ocr.js) und `countObjects()` (count-objects.js) |
| `src/count-objects.js` | Reine Berechnung für Funktion „Zählen": zählt zusammenhängende Objektflächen in einem Bildausschnitt über örtlichen Kontrast (Normierung auf 800 Bildpunkte Breite, Glättung und örtlicher Hintergrund je über Kastenfilter/Summenbild, Maske aus geglättetem Bild vs. örtlichem Hintergrund ± Marge, beide Polaritäten, Connected-Component-Labeling mit eigener Arbeitsliste, Rauschfilter, Massstab relativ zur größten Fläche, Hochrechnung berührender Teile über den Flächen-Median) — nutzt `threshold.js`/Otsu bewusst **nicht** mehr (siehe „Funktionsweise" oben) |
| `src/storage.js` | Absturzschutz der laufenden Sitzung: sichert und lädt aus `localStorage`, verwirft beim Laden jeden in sich unstimmigen Zustand vollständig (siehe unten); `entry.rawText` ist wie `entry.codes` optional und wird bei Fehlen als leerer Rohtext wiederhergestellt |
| `src/camera.js` | Kamerastart, Einzelbildaufnahme (herunterskaliert, für die Dateiauswahl), Rahmenausschnitt in voller Auflösung (`grabFrameRegion`, für die laufende Barcode-Suche, „Modul scannen" in „Text erkennen" **und** die laufende Zählung in „Zählen"), Bilddatei-Ersatzweg |
| `src/barcode.js` | Adapter zu `zxing-wasm`; `decodeBarcodes(imageData, formats)` bekommt die Codearten übergeben, ohne Angabe wie bisher (Code128 + DataMatrix) |
| `src/ocr.js` | Bildaufbereitung (Otsu, über `threshold.js`) und Adapter zu `tesseract.js`; Zeichen-Whitelist deckt Groß- und Kleinbuchstaben, Ziffern sowie übliche Etiketten-Sonderzeichen ab |
| `src/ui/scan-view.js` | Scan-Ansicht: Kamera-Vorschau, antippbare Anzeige der gewählten Funktion (Wechsel), Scan-Knopf (nur in „Text erkennen" sichtbar), große Zählanzeige (nur in „Zählen" sichtbar), Stapel-Leiste, „Zuletzt"-Zeile, Ersatzweg-Schaltfläche bei fehlender Kamera |
| `src/ui/mode-dialog.js` | Vollbild-Auswahl der Scan-Funktion vor dem Start und beim Wechsel: vier große Flächen, eine je Funktion aus `src/scan-modes.js` |
| `src/ui/result-overlay.js` | Treffer-Rückmeldung (grün/gelb): blendet sich in „Strichcode"/„QR-Code" nach kurzer Zeit selbst aus; in „Text erkennen" bleibt sie stehen, zeigt den vollen OCR-Rohtext und wartet auf eine Eingabe, bevor sie schließt |
| `src/ui/ambiguous-dialog.js` | Rot-Dialog bei roter Konfidenz oder mehrdeutiger Stapelzuordnung |
| `src/ui/resume-dialog.js` | Dialog beim Start: gesicherte Sitzung fortsetzen oder verwerfen — läuft vor der Funktionswahl |
| `src/ui/session-list.js` | Sitzungsliste: Stapel-Übersicht, Umsortieren, Entfernen, Sitzung beenden, Anzeige des vollständigen OCR-Rohtexts je Eintrag (sofern vorhanden) |
| `src/ui/describe-spec.js` | Gemeinsame Kurzbeschreibung eines Specs (Kapazität, Rank, Geschwindigkeit, Bauform), wahlweise mit Platzhaltern für fehlende Felder; von Treffer-Rückmeldung, „Zuletzt"-Zeile und Sitzungsliste gemeinsam genutzt |
| `src/main.js` | Verdrahtung aller Module zur lauffähigen App |
| `src/styles.css` | Farbvariablen (Grün/Gelb/Rot der Ampel-Rückmeldung), Layout des Kamera-Vollbilds samt Zielrahmen, Stapel-Leiste und Aktionsknöpfe sowie die Overlays für Treffer-Rückmeldung, Rot-Dialog und Sitzungsliste |
| `server.py` | Auslieferung im Betrieb: statischer Server für `dist/` mit Passwortabfrage vor jeder Datei, plus passwortfreier Gesundheitspfad `/healthz`. Nur Python-Standardbibliothek, keine zusätzliche Abhängigkeit |
| `.vch/deploy.yaml` | Bau- und Startbefehl sowie Gesundheitspfad für die Hosting-Umgebung |
`spec`, `pn-decoder`, `ocr-extract`, `session`, `pipeline`, `storage`,
`scan-modes`, `scan-recognition`, `threshold` und `count-objects` sind reine
Funktionen ohne Browser-Zugriff (kein `window`, `document` oder
`localStorage` direkt) und deshalb vollständig mit `node:test` prüfbar.
`camera.js`, `barcode.js`, `ocr.js` und `src/ui/` brauchen einen echten
Browser und werden nur manuell geprüft.
### Sitzungssicherung im Detail
`src/storage.js` prüft eine aus `localStorage` geladene Sitzung gründlich,
bevor sie übernommen wird: Stapel- und Eintragslisten müssen die erwartete
Form haben, Stapelkennungen und Eintragsnummern müssen jeweils eindeutig
sein, jeder Eintrag muss auf einen existierenden Stapel zeigen, und
`nextEntryId` muss größer sein als jede vorhandene Eintragsnummer. Trifft
auch nur eine dieser Bedingungen nicht zu, wird die gesamte gespeicherte
Sitzung verworfen (die App startet leer), statt mit einem halb kaputten
Zustand lautlos weiterzuarbeiten. Der gespeicherte Stapelzähler selbst wird
nie übernommen, sondern beim Laden immer neu aus der tatsächlichen Zahl der
zugeordneten Einträge berechnet — genauso die Codemenge eines Stapels
(`stack.codes`): Sie wird nie aus dem gesicherten Wert übernommen, sondern
stets aus den Codes der ihm tatsächlich zugeordneten Einträge (`entry.codes`)
neu zusammengesetzt, dieselbe Ableitung, die `session.js` nach jedem
Umsortieren oder Entfernen eines Eintrags vornimmt. Sind `stack.codes` bzw.
`entry.codes` vorhanden, werden sie streng geprüft (Liste von
Zeichenketten); ein gesicherter Stand aus einer Fassung vor dieser Erweiterung
ohne diese Felder gilt dagegen nicht als unstimmig, sondern wird beim Laden
als Stapel bzw. Eintrag ohne bekannte Codes wiederhergestellt.
Nach demselben Muster ist `entry.rawText` (der volle OCR-Rohtext, siehe
„Funktionsweise" oben) optional: Ist er vorhanden, muss er eine Zeichenkette
sein; ein gesicherter Stand aus einer Fassung vor dieser Erweiterung ohne
dieses Feld gilt nicht als unstimmig und wird beim Laden als Eintrag ohne
Rohtext (leere Zeichenkette) wiederhergestellt. Anders als `stack.codes` wird
`entry.rawText` beim Laden nicht neu abgeleitet — er hängt an genau diesem
Eintrag und fließt in keine Stapel-Berechnung ein.
## Grenzen
- Keine Bestandsführung über Sitzungen hinweg, kein Export. Die Sicherung in
`localStorage` ist ausdrücklich nur ein Absturzschutz für die laufende
Sitzung, keine dauerhafte Datenhaltung.
- Das Laden der Seite braucht eine Verbindung; das Sortieren selbst nicht
(nach dem Laden läuft alles lokal im Browser).
- Der Codeabgleich (siehe oben) weiß nicht, ob ein gelesener Barcode-Inhalt
tatsächlich eine Teilenummer ist. Trägt ein Etikett daneben noch eine
Los- oder Wiederverkäufer-Kennung und teilen zwei technisch verschiedene
Module dieselbe, würde ein reiner Codetreffer sie fälschlich zusammenführen.
Dagegen schützt, dass ein Codetreffer allein nie genügt: Ein Stapel ist über
einen Code nur dann Kandidat, wenn die technischen Angaben ihm zusätzlich
nicht widersprechen (`specsCompatible`, siehe `proposeAssignment` in
`src/session.js`) — widerspricht der Stapel trotz passendem Code, fällt das
Modul auf einen eigenen Stapel zurück, statt lautlos falsch einsortiert zu
werden.
- Der Teilenummer-Decoder kennt bisher nur das Samsung-DDR4-Schema. Unbekannte
Schemata sind kein Fehler — sie führen automatisch zum OCR-Weg, und selbst
wenn dieser keine Kapazität findet, genügt die exakt gelesene Teilenummer
allein für Grün (siehe „Funktionsweise“ oben). Nur Kapazität, Bauform,
Rank und Geschwindigkeit hängen am Tabelleneintrag — die Wiedererkennung
eines bereits erfassten Moduls über seinen Barcode-Inhalt nicht.
- **Der Teilenummer-Decoder ist nur teilweise gegen reale Module geprüft.**
Belegt ist ausschließlich der Samsung-Eintrag `A8K40` (64 GB, 4DRx4) mit
dem Geschwindigkeitscode `CRC`, anhand des Referenzmoduls
`M386A8K40BM1-CRC4Y`. Alle übrigen Einträge in `src/pn-tables.js`
die Bauform-Codes (`378`, `391`, `393`, `386`, `471`, `474`) und die
übrigen Geschwindigkeitscodes (`CPB`, `CTD`, `CVF`, `CWE`) — stammen aus
der veröffentlichten Samsung-Systematik und wurden **nicht** gegen ein
reales Etikett bestätigt. Hynix- und Micron-Module haben noch gar keinen
Tabelleneintrag und laufen vollständig über den OCR-Weg. Ein falscher
Tabelleneintrag fällt auf dem Barcode-Weg nicht automatisch auf, weil dort
keine Texterkennung gegenläuft, gegen die er widersprechen könnte — die
Validierung braucht deshalb echte Module, keine Code-Prüfung.
- Farb- und Schriftgestaltung ist bewusst schlicht gehalten und kann nach dem
ersten Einsatz am Tisch nachgezogen werden.
- Der volle OCR-Rohtext (`rawText`) dient ausschließlich der Anzeige für den
Nutzer (Treffer-Rückmeldung, Sitzungsliste). Die Stapelzuordnung stützt
sich bewusst weiterhin ausschließlich auf die daraus abgeleiteten,
verstandenen Spec-Felder — roher Erkennungstext schwankt zwischen
Aufnahmen (Zeilenumbrüche, zusätzlich erkannte Wörter, Groß-/
Kleinschreibung, …) und würde als Gruppierungsmerkmal für praktisch jedes
Modul einen eigenen Stapel erzeugen.
- **Funktion „Zählen" ist eine Schätzung, keine exakte Zählung** — sie wird
ungenau, wenn eine der folgenden Bedingungen zutrifft:
- **Der Untergrund muss sich abheben.** Das Verfahren erkennt Fundstücke
über den örtlichen Kontrast zu ihrer unmittelbaren Umgebung (siehe
„Funktionsweise" oben) — es braucht also eine Stelle, an der sich das
Fundstück *lokal* von dem unterscheidet, was um es herum liegt. Ein
Fundstück, das nahezu dieselbe Helligkeit wie der Tisch/das Leder
darunter hat, wird nicht zuverlässig gefunden.
- **Gleichmäßige Ausleuchtung hilft.** Der örtliche Hintergrund wird aus
einem Fenster um jeden Bildpunkt gebildet und fängt einseitigen
Lichteinfall dadurch besser ab als ein einzelner, fester Schwellwert
für das ganze Bild. Sehr harte Schlagschatten oder Reflexe können
trotzdem eigene, dem Fundstück ähnliche Kontrastflächen erzeugen (siehe
„mehrere gefundene Flächen" unten) und die Zählung verfälschen.
- **Auseinandergelegt zählt besser als aufgehäuft/berührend.** Mehrere
Teile, die sich berühren, bilden eine einzige zusammenhängende Fläche.
Die App rechnet diese über die typische Einzelteilgröße (Median der
gefundenen Flächen) auf ein Vielfaches hoch, aber je unregelmäßiger sich
die Teile überlappen (z. B. mehrere Schrauben, die sich gegenseitig
teilweise verdecken statt sich nur zu berühren), desto ungenauer wird
die hochgerechnete Fläche — und je dichter die Teile gedrängt liegen,
desto eher sieht das Umgebungsfenster (siehe oben) an ihrer eigenen
Stelle vor allem Nachbarteile statt echten Untergrund. Liegen Teile
übereinander gestapelt statt nebeneinander ausgelegt, sieht die Kamera
von oben nur die Fläche des obersten Teils — verdeckte Teile werden
nicht mitgezählt.
- **Stark unterschiedlich große Teile verfälschen die Hochrechnung.** Die
Hochrechnung setzt voraus, dass die meisten Fundstücke ungefähr gleich
groß sind (der Median dient als Maßstab für „ein Teil"). Liegen im
selben Rahmen sowohl deutlich größere als auch deutlich kleinere Teile
derselben Sorte (oder gar unterschiedliche Teile gemischt), kann die
Rauschschwelle ein tatsächliches kleines Teil verwerfen oder die
Hochrechnung ein einzelnes großes Teil als mehrere zählen.
- Details, die verwendeten Schwellenwerte, ihre Begründung und die
Messung am echten Foto (~20 Festplattenschrauben auf Leder, für das
das alte Otsu-basierte Verfahren 76712 statt rund zwanzig meldete)
stehen in `.superpowers/sdd/counting-rework-report.md`.
## Herstellertabellen erweitern
`src/pn-tables.js` ist tabellengesteuert. Ein neuer Hersteller oder ein neuer
Dichte-/Geschwindigkeitscode braucht einen Eintrag mit `pattern`,
`formFactor`, `density` und `speed` — keine Codeänderung an `pn-decoder.js`.
Vorgehen für jedes real vorliegende Modul:
1. Hersteller-Teilenummer vom Etikett ablesen (Barcode oder Klartext) sowie
die dazugehörige Klartextzeile mit Kapazität, Rank/Bauform und
Geschwindigkeit.
2. Einen Testfall in `test/pn-decoder.test.js` ergänzen, der die erwarteten
Spec-Felder für genau diese Teilenummer festhält.
3. Den passenden Tabelleneintrag (bzw. die passende Zeile in einer
bestehenden Tabelle) in `src/pn-tables.js` ergänzen, bis der Testfall
grün ist.
4. Den Kommentar am Tabellenkopf aktualisieren, welche Einträge damit belegt
sind.
Ohne reale Module bleiben unbestätigte Einträge unbestätigt — sie sollten
nicht "auf Verdacht" ergänzt werden, weil ein falscher Eintrag auf dem
Barcode-Weg (siehe „Grenzen") nicht mehr auffällt.
## Am Gerät noch zu prüfen
Diese Umgebung hat keinen Browser und kein Handy. Die folgenden Punkte
wurden während der Umsetzung deshalb nur durch Quelltext-Lektüre, `node
--check`, `npm test` und `npm run build` abgesichert, aber nie am echten
Gerät gesehen. Sie sollten vor dem produktiven Einsatz am Tisch geprüft
werden:
**Kamera und Aufnahme**
- Preview-URL auf dem Handy öffnen (muss `https://` sein), Rückkamera-Bild
erscheint im Vorschaubereich; die Statuszeile bleibt leer, solange Kamera
und Texterkennung verfügbar sind.
- Verweigerte Kamera-Berechtigung: verständliche Fehlermeldung erscheint
*und* die Aufnahme-LED des Geräts erlischt tatsächlich wieder.
- Aufnahme-Knopf antippen, bevor das Kamerabild bereit ist: verständliche
Meldung statt technischer Fehler.
- Datei-Auswahl als Ersatzweg (ohne Kamera) öffnet den nativen Dialog und
hinterlässt keine leere Fläche im Layout.
**Funktionswahl**
- Erster Start ohne gesicherte Sitzung: Auswahl mit vier großen Flächen
(„Strichcode", „QR-Code", „Text erkennen", „Zählen") erscheint, *bevor* die
Kamera startet — kein Kamerabild sichtbar, solange keine Funktion gewählt
ist.
- Erster Start *mit* gesicherter Sitzung: erst die Frage „Gesicherte Sitzung
gefunden" (fortsetzen/verwerfen), erst danach die Funktionswahl.
- „Strichcode" wählen: laufende Suche erkennt Code-128/Code-39/EAN/UPC/…,
kein „Modul scannen"-Knopf sichtbar, Zielrahmen hebt sich bei erkanntem
Code hervor.
- „QR-Code" wählen und ein Etikett mit dem eigenen DataMatrix-Code vor die
Kamera halten: Rahmen hebt sich hervor, Treffer wird gebucht (DataMatrix
liest diese Funktion jetzt mit). Einen echten QR-Code vor dieselbe Funktion
halten: ebenfalls Treffer.
- „Text erkennen" wählen: kein automatisches Hervorheben des Rahmens (er
bleibt durchgehend schlicht), „Modul scannen"-Knopf sichtbar und löst die
Texterkennung aus. Der Rahmen ist dort trotzdem wirksam: nur der
Bildausschnitt innerhalb des Rahmens wird gelesen (nicht das ganze
Kamerabild) — mit Text außerhalb des Rahmens auf demselben Etikett prüfen,
dass er **nicht** erkannt wird, während Text innerhalb des Rahmens
vollständig erscheint.
- Anzeige der gewählten Funktion in der Scan-Ansicht antippen: Auswahl
erscheint erneut, nach der Wahl gilt der Wechsel sofort — laufende Suche
startet/stoppt, Knopf erscheint/verschwindet, **ohne** die Seite neu zu
laden.
- Anzeige der gewählten Funktion aus rund einem Meter Entfernung lesbar.
**Barcode- und OCR-Erkennung**
- Samsung-Referenzmodul in Funktion „Strichcode" scannen:
`M386A8K40BM1-CRC4Y` wird korrekt als Teilenummer übernommen, grüne
Rückmeldung mit `STAPEL A`, `64GB 4DRx4 PC4-2400 LRDIMM`.
- In Funktion „Text erkennen" ein Etikett fotografieren und „Modul scannen"
antippen: die Otsu-Aufbereitung liefert auf einem echten, glänzenden
Etikettenfoto tatsächlich brauchbaren Text (bisher nur an synthetischen
Testbildern geprüft, nie an einem echten Foto). Prüfen, dass die
Rückmeldung dabei den **vollständigen** erkannten Text zeigt (Groß- und
Kleinbuchstaben, nicht nur die von der App verstandenen Felder wie
`32GB 2Rx4 PC4-2666`), stehen bleibt, bis sie weggetippt wird, und bei
langem Text lesbar bleibt (per Scrollen erreichbar, wächst nicht aus dem
Bildschirm heraus). Danach in der Sitzungsliste nachsehen, dass derselbe
Rohtext beim Eintrag zum Nachlesen erscheint.
- Da der Bildausschnitt für die Texterkennung jetzt deutlich größer ist als
das bisher übergebene, auf 1280 Pixel herunterskalierte Vollbild
(`grabFrameRegion` statt `grabFrame`, siehe „Funktionsweise"), dauert ein
einzelner Erkennungsversuch länger als vorher — beobachten, wie lange ein
normaler Scan auf einem durchschnittlichen Handy tatsächlich braucht, und
ob er dabei spürbar unter der 20-Sekunden-Zeitgrenze der Pipeline bleibt
(siehe dazu auch den folgenden Punkt zur Ladezeit des Tesseract-Arbeiters,
die zusätzlich in dieselbe Zeitgrenze fällt).
- Ladezeit des WASM-Barcode-Moduls (`zxing-wasm`) beim allerersten Scan einer
Sitzung in Funktion „Strichcode" oder „QR-Code" beobachten — bleibt sie
deutlich unter der 10-Sekunden-Zeitgrenze der Pipeline, und braucht ein
zweiter, schnell nachfolgender Scan nicht erneut die volle Ladezeit (Beleg,
dass die Vorbereitung tatsächlich nur einmal läuft)? In Funktion „Text
erkennen" darf dieses Laden dagegen gar nicht erst anlaufen, da dort nie
`decodeBarcodes` aufgerufen wird.
**Zählen (neu)**
- „Zählen" wählen, Kamera auf ausgelegte Kleinteile richten (z. B. silberne
Festplattenschrauben auf dunklem Untergrund): Eine große Zahl erscheint im
Bild und aktualisiert sich laufend, ohne dass etwas angetippt werden muss.
Kein „Modul scannen"-Knopf, keine Treffer-Rückmeldung, kein roter Dialog,
keine Stapel-Zuweisung — der Zielrahmen bleibt sichtbar und zeigt weiterhin,
was gezählt wird.
- Die angezeigte Zahl aus normalem Bedienabstand (Armlänge) lesbar prüfen.
- Ein paar Teile wegnehmen bzw. hinzufügen, während die Kamera läuft: Die
Zahl folgt ohne spürbare Verzögerung.
- Zugänge zur Sitzungsliste und zum Funktionswechsel bleiben auch in dieser
Funktion erreichbar (Anzeige der Funktion antippen, Stapel-Leiste antippen).
- Gegenprobe der Grenzen (siehe „Grenzen" oben): Teile eng aneinanderlegen
(Hochrechnung), Teile stapeln (nur oberste Schicht zählt), einseitige
Beleuchtung, geringer Kontrast zum Untergrund — beobachten, wo die Zahl
spürbar daneben liegt, bisher nur an synthetischen Testbildern geprüft, nie
an einem echten Foto.
- Verbindung während des allerersten Scans einer Sitzung unterbrechen (WASM
lädt per CDN), danach mit wiederhergestellter Verbindung erneut scannen:
Das muss einen echten neuen Ladeversuch auslösen statt dauerhaft mit
demselben Fehler zu scheitern.
- Ladezeit des Tesseract-Arbeiters beim ersten Antippen von „Modul scannen"
in Funktion „Text erkennen" beobachten — bleibt sie auf einem normalen
Handy deutlich unter der 20-Sekunden-Zeitgrenze der Pipeline? In den
Funktionen „Strichcode"/„QR-Code" darf dieses Laden nie anlaufen, auch
nicht bei einem gänzlich unbekannten Etikett, da `runOcr` dort nie
aufgerufen wird.
- Verfügbarkeitsanzeige der Texterkennung: bleibt nach einem erfolgreichen
Ladevorgang dauerhaft "verfügbar", springt nach einem erzwungenen
Fehlschlag (z. B. Flugmodus beim ersten Laden) beim nächsten Erfolg wieder
darauf zurück?
- Stichprobe, ob die Zeichen-Whitelist der Texterkennung (Groß-/
Kleinbuchstaben, Ziffern, übliche Satz-/Sonderzeichen) auf echten Etiketten
keine tatsächlich benötigten Zeichen ausschließt, und ob die Erkennung der
Codeteile (Kapazität, Rank, Geschwindigkeit, Teilenummer) durch die jetzt
mögliche Groß-/Kleinschreibung noch verlässlich genug bleibt.
- Funktion „Strichcode"/„QR-Code": kein passender Code im Bild führt zu
Rot-Dialog (statt Fehler oder Absturz) — hier ohne jeden OCR-Versuch.
Funktion „Text erkennen": kein verwertbarer OCR-Text führt ebenfalls zu
Rot, hier ohne jeden Barcode-Versuch.
- In Funktion „Strichcode" ein Modul mit unbekanntem Nummernschema (z. B. ein
Hynix-Modul) scannen: grüne Rückmeldung statt Rot, auch wenn keine
Kapazität abgeleitet werden kann (die Texterkennung ist in dieser Funktion
ohnehin stumm geschaltet und liefert nie eine Kapazität nach). Dasselbe
Modul ein zweites Mal scannen: landet auf demselben Stapel.
- Etikett mit zwei Barcodes (Teile- *und* Seriennummer) mehrfach scannen:
beide Scans landen auf demselben Stapel, obwohl die Seriennummer bei realen
Modulen nie identisch ist — das belegt, dass die Teilenummer und nicht die
Seriennummer die Zuordnung trägt.
**Bedienung und Anzeige**
- Zielrahmen liegt sinnvoll über dem Kamerabild, `object-fit: cover` füllt
es passend.
- Stapel-Leiste (`A: 12`, `B: 4`) aus rund einem Meter Entfernung mit einer
Hand lesbar.
- In „Strichcode"/„QR-Code": Grüne/gelbe Rückmeldung erscheint nach dem Scan
und verschwindet nach rund 1200 ms von selbst; bei zwei Scans deutlich
unter 1200 ms Abstand bleibt jeweils nur eine Rückmeldung sichtbar und die
App bleibt bedienbar.
- In „Text erkennen": Rückmeldung bleibt stehen, bis sie über die
„weiter"-Schaltfläche (mindestens 56px hoch) weggetippt wird — kein
automatisches Ausblenden. Ein weiterer Scan, bevor die vorherige
Rückmeldung weggetippt wurde, ersetzt sie sofort durch die neue, ohne die
App dauerhaft zu sperren.
- Alle Bedienflächen (Scan-, Rückgängig-, Stapel-Knöpfe) mit dem Daumen
erreichbar, während die andere Hand das Modul hält.
- Rückgängig-Knopf: Screenreader liest "Letzten Scan zurücknehmen" vor.
- Rot-Dialog bei Mehrdeutigkeit: vier klar unterscheidbare Flächen ("Stapel
A/B …", "neuer Stapel", "nochmal scannen"); bei vielen Stapel-Kandidaten
bleibt alles per Scrollen erreichbar, ohne dass die Seite dahinter
mitrollt; Tastatur-/Screenreader-Fokus bleibt innerhalb des Dialogs.
**Sitzung**
- Ein Modul zweimal scannen: beide Male derselbe Stapel, Zähler steigt.
- Rücknahme-Fläche nimmt genau den zuletzt erfassten Eintrag zurück, Zähler
sinkt entsprechend.
- Seite neu laden während einer laufenden Sitzung: Dialog „Gesicherte
Sitzung gefunden“ erscheint; erst nach „fortsetzen“ bleiben Stapel und
Zähler erhalten und die „Zuletzt“-Zeile zeigt wieder den tatsächlich
letzten Eintrag; „verwerfen“ führt zu einem leeren Start.
- Sitzungsliste öffnen, Eintrag umsortieren, Eintrag entfernen: Ansicht baut
sich korrekt neu auf.
- "Sitzung beenden": Ansicht wird leer, und ein anschließendes Neuladen
stellt **keine** alte Sitzung wieder her; ein danach neu gescanntes Modul
wird aber wieder normal gegen Neuladen abgesichert.
- Schnelles Mehrfachtippen auf die Stapel-Leiste öffnet nur eine
Sitzungsliste, kein Übereinanderlegen mehrerer Listen.