TanerUsluandClaude Opus 5 5e64d260c5 Reine Zaehl-Berechnung fuer Fundstuecke in einem Bildausschnitt
Neues Modul src/count-objects.js: Otsu-Schwellwert (threshold.js) teilt den
Ausschnitt in zwei Klassen, die flaechenmaessig kleinere gilt als Objekt
(funktioniert fuer helle wie dunkle Teile auf beliebigem Untergrund).
Connected-Component-Labeling ueber eine eigene Arbeitsliste statt Rekursion
(vermeidet Stapelueberlauf bei ~1 Mio. Bildpunkten), Rauschflaechen werden
verworfen, beruehrende Teile anhand des Flaechen-Medians hochgerechnet.

Test-Driven: leeres Bild, einzelne/mehrere getrennte Flaechen, verworfenes
Rauschen, Hochrechnung ohne ein nur leicht groesseres Einzelteil zu
verdoppeln, helle wie dunkle Objekte, Bild ohne Bildpunkte, diagonal statt
flaechig beruehrende Teile bleiben getrennt.

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

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 drei Funktionen — Strichcode, QR-Code oder Text erkennen (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.

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

  1. Funktion wählen. Vor dem ersten Scan (und jederzeit erneut über die antippbare Anzeige in der Scan-Ansicht) wählt der Nutzer eine von drei 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 Texterkennung
    Strichcode Code128, Code39, Code93, ITF, EAN-13, EAN-8, UPC-A, UPC-E, Codabar ja nein
    QR-Code QRCode, MicroQRCode, RMQRCode, DataMatrix, Aztec, PDF417 ja nein
    Text erkennen keine Codes nein ja, auf Knopfdruck

    „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 5 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 (buildRecognitionAdapters in src/scan-recognition.js). Die Erkennungs-Pipeline selbst (src/pipeline.js, Punkte 14 unten) bleibt dabei unverändert und kennt keine Funktionen — die drei Funktionen unterscheiden sich ausschließlich darin, welche Adapter ihr übergeben werden.

  2. 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.

  3. 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.

  4. 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.

  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

npm install

Entwicklung

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

npm test

154 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) 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 drei 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"). 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:

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.

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 als Entwicklungsserver und Bündler
  • zxing-wasm zur Barcode-Dekodierung; welche Codearten gesucht werden, gibt die gewählte Funktion vor (src/scan-modes.js)
  • tesseract.js 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 drei Scan-Funktionen (Strichcode, QR-Code, Text erkennen): Kennung, deutsche Beschriftung, gelesene Codearten, laufende Suche ja/nein, Texterkennung 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
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 und für „Modul scannen" in „Text erkennen"), 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) 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), 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: drei 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 und scan-recognition 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.

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 (neu)

  • Erster Start ohne gesicherte Sitzung: Auswahl mit drei großen Flächen („Strichcode", „QR-Code", „Text erkennen") 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.
  • 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.
S
Description
No description provided
Readme
687 KiB
Languages
JavaScript 95.4%
CSS 2.6%
Python 1.8%
HTML 0.2%