Konsolidiere Spec-Kurzbeschreibung in ein gemeinsames Modul

main.js, result-overlay.js und session-list.js loesten dieselbe Aufgabe
(Spec-Felder zu einer Lesehilfe zusammenfassen) bislang mit drei leicht
unterschiedlichen Implementierungen - unterschiedliche Feldauswahl und
unterschiedlicher Umgang mit fehlenden Werten. Neues src/ui/describe-spec.js
buendelt das in describeSpec(spec, { placeholders }): ohne Platzhalter fuer
die kurze Treffer-Rueckmeldung und die "Zuletzt"-Zeile, mit Platzhalter fuer
die Sitzungsliste, wo feste Feldreihenfolge Wiedererkennung schafft.

Nutzt bei main.js zugleich die bereits vorhandene, jetzt exportierte
nextStackId aus session.js statt einer Zeile-fuer-Zeile-Kopie derselben
Buchstaben-Vergabe (nextFreeStackId entfaellt komplett).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
vchuser
2026-07-28 18:24:07 +02:00
co-authored by Claude Opus 5
parent 8082c4172b
commit 7fc7feb1ed
4 changed files with 59 additions and 51 deletions
+4 -18
View File
@@ -6,7 +6,7 @@ import { runOcr, isOcrAvailable } from './ocr.js';
import { recognize } from './pipeline.js'; import { recognize } from './pipeline.js';
import { import {
createSession, proposeAssignment, commitAssignment, createSession, proposeAssignment, commitAssignment,
undoLast, moveEntry, removeEntry, undoLast, moveEntry, removeEntry, nextStackId,
} from './session.js'; } from './session.js';
import { saveSession, loadSession, clearSession } from './storage.js'; import { saveSession, loadSession, clearSession } from './storage.js';
import { renderScanView } from './ui/scan-view.js'; import { renderScanView } from './ui/scan-view.js';
@@ -14,6 +14,7 @@ import { showResult } from './ui/result-overlay.js';
import { askForStack } from './ui/ambiguous-dialog.js'; import { askForStack } from './ui/ambiguous-dialog.js';
import { askResumeSession } from './ui/resume-dialog.js'; import { askResumeSession } from './ui/resume-dialog.js';
import { renderSessionList } from './ui/session-list.js'; import { renderSessionList } from './ui/session-list.js';
import { describeSpec } from './ui/describe-spec.js';
const app = document.querySelector('#app'); const app = document.querySelector('#app');
const store = window.localStorage; const store = window.localStorage;
@@ -64,22 +65,7 @@ function syncView() {
/** Kurzbeschreibung fuer die "Zuletzt"-Zeile. */ /** Kurzbeschreibung fuer die "Zuletzt"-Zeile. */
function describeEntry(spec, stackId) { function describeEntry(spec, stackId) {
const parts = [ return `${describeSpec(spec)} → Stapel ${stackId}`;
spec.capacityGb ? `${spec.capacityGb}GB` : '?',
spec.speed ?? '?',
spec.formFactor ?? '',
].filter(Boolean);
return `${parts.join(' ')} → Stapel ${stackId}`;
}
/** Naechster noch nicht vergebener Stapel-Buchstabe - fuer den Fall, dass der Nutzer im Rot-Dialog ausdruecklich einen neuen Stapel waehlt. */
function nextFreeStackId(targetSession) {
const used = new Set(targetSession.stacks.map((stack) => stack.id));
for (const letter of 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') {
if (!used.has(letter)) return letter;
}
// Nach Z weiter mit A2, B2, ... - in der Praxis nie erreicht (siehe session.js).
return `A${targetSession.stacks.length}`;
} }
/** /**
@@ -124,7 +110,7 @@ async function processCapture(getFrame) {
stackId = answer.stackId; stackId = answer.stackId;
} else { } else {
// "neuer Stapel": unabhaengig von jeder Vermutung der naechste freie Buchstabe. // "neuer Stapel": unabhaengig von jeder Vermutung der naechste freie Buchstabe.
stackId = nextFreeStackId(targetSession); stackId = nextStackId(targetSession);
} }
} }
+39
View File
@@ -0,0 +1,39 @@
// Rein darstellend: baut aus Spec-Feldern eine kurze, deutsche Lesehilfe.
// Kein window/document noetig - liefert nur eine Zeichenkette.
const PLACEHOLDER = '?';
/**
* Fasst Kapazitaet, Rank, Geschwindigkeit und Bauform eines Specs zu einer
* kurzen Lesehilfe zusammen. Frueher gab es davon drei leicht
* unterschiedliche Fassungen (main.js, result-overlay.js, session-list.js) -
* mit unterschiedlichen Feldern und unterschiedlichem Umgang mit fehlenden
* Werten. Diese eine Fassung deckt beide Beduerfnisse ueber einen Schalter ab:
*
* - `placeholders: false` (Vorgabe): fehlende Felder werden weggelassen,
* ein vollstaendig leeres Spec ergibt "ohne Angaben". Passend fuer die
* kurz eingeblendete Treffer-Rueckmeldung und die "Zuletzt"-Zeile, wo ein
* Platzhalter pro fehlendem Feld mehr verwirrt als hilft.
* - `placeholders: true`: jedes Feld erscheint, fehlende als "?". Passend
* fuer die Sitzungsliste, wo die feste Reihenfolge der vier Felder ueber
* viele Stapel hinweg Wiedererkennung schafft.
*
* @param {object} spec
* @param {{placeholders?: boolean}} [options]
* @returns {string}
*/
export function describeSpec(spec, { placeholders = false } = {}) {
const fields = [
spec.capacityGb ? `${spec.capacityGb}GB` : null,
spec.rank ?? null,
spec.speed ?? null,
spec.formFactor ?? null,
];
if (placeholders) {
return fields.map((value) => value ?? PLACEHOLDER).join(' ');
}
const present = fields.filter(Boolean);
return present.length > 0 ? present.join(' ') : 'ohne Angaben';
}
+13 -21
View File
@@ -1,27 +1,19 @@
function beschreibung(spec) { import { describeSpec } from './describe-spec.js';
const teile = [
spec.capacityGb ? `${spec.capacityGb}GB` : null,
spec.rank,
spec.speed,
spec.formFactor,
].filter(Boolean);
return teile.join(' ') || 'ohne Angaben';
}
/** Haelt pro Root die aktuell sichtbare Rueckmeldung samt Zeitgeber fest. */ /** Haelt pro Root die aktuell sichtbare Rueckmeldung samt Zeitgeber fest. */
const aktuelleRueckmeldung = new WeakMap(); const currentFeedback = new WeakMap();
/** /**
* Blendet die Treffer-Rueckmeldung ein und nach kurzer Zeit wieder aus. * Blendet die Treffer-Rueckmeldung ein und nach kurzer Zeit wieder aus.
* Gruen bei Barcode, gelb bei OCR. Keine Eingabe noetig. * Gruen bei Barcode, gelb bei OCR. Keine Eingabe noetig.
*/ */
export function showResult(root, { stackId, spec, confidence }, dauerMs = 1200) { export function showResult(root, { stackId, spec, confidence }, durationMs = 1200) {
const vorherige = aktuelleRueckmeldung.get(root); const previous = currentFeedback.get(root);
if (vorherige) { if (previous) {
clearTimeout(vorherige.timer); clearTimeout(previous.timer);
vorherige.overlay.remove(); previous.overlay.remove();
vorherige.resolve(); previous.resolve();
aktuelleRueckmeldung.delete(root); currentFeedback.delete(root);
} }
const overlay = document.createElement('div'); const overlay = document.createElement('div');
@@ -31,16 +23,16 @@ export function showResult(root, { stackId, spec, confidence }, dauerMs = 1200)
<div class="detail"></div> <div class="detail"></div>
<div class="pn"></div> <div class="pn"></div>
`; `;
overlay.querySelector('.detail').textContent = beschreibung(spec); overlay.querySelector('.detail').textContent = describeSpec(spec);
overlay.querySelector('.pn').textContent = spec.partNumber ?? ''; overlay.querySelector('.pn').textContent = spec.partNumber ?? '';
root.appendChild(overlay); root.appendChild(overlay);
return new Promise((resolve) => { return new Promise((resolve) => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
overlay.remove(); overlay.remove();
aktuelleRueckmeldung.delete(root); currentFeedback.delete(root);
resolve(); resolve();
}, dauerMs); }, durationMs);
aktuelleRueckmeldung.set(root, { overlay, timer, resolve }); currentFeedback.set(root, { overlay, timer, resolve });
}); });
} }
+3 -12
View File
@@ -3,16 +3,7 @@
// Sitzung. Kennt weder Kamera noch Erkennung - alles kommt ueber die // Sitzung. Kennt weder Kamera noch Erkennung - alles kommt ueber die
// Rueckrufe, genau wie bei scan-view.js und ambiguous-dialog.js. // Rueckrufe, genau wie bei scan-view.js und ambiguous-dialog.js.
/** Kurzbeschreibung eines Specs fuer Stapel-Kopfzeilen und Eintrags-Zeilen ohne Teilenummer. */ import { describeSpec } from './describe-spec.js';
function describeSpec(spec) {
const parts = [
spec.capacityGb ? `${spec.capacityGb}GB` : '?',
spec.rank ?? '?',
spec.speed ?? '?',
spec.formFactor ?? '?',
];
return parts.join(' ');
}
/** /**
* Kurzbezeichnung der Erkennungsquelle eines Eintrags - beim Sortieren * Kurzbezeichnung der Erkennungsquelle eines Eintrags - beim Sortieren
@@ -52,7 +43,7 @@ export function renderSessionList(root, session, { onMove, onRemove, onEndSessio
header.className = 'detail'; header.className = 'detail';
header.style.textAlign = 'left'; header.style.textAlign = 'left';
header.style.marginTop = '12px'; header.style.marginTop = '12px';
header.textContent = `Stapel ${stack.id}${stack.count}x ${describeSpec(stack.spec)}`; header.textContent = `Stapel ${stack.id}${stack.count}x ${describeSpec(stack.spec, { placeholders: true })}`;
body.appendChild(header); body.appendChild(header);
for (const entry of session.entries.filter((candidate) => candidate.stackId === stack.id)) { for (const entry of session.entries.filter((candidate) => candidate.stackId === stack.id)) {
@@ -65,7 +56,7 @@ export function renderSessionList(root, session, { onMove, onRemove, onEndSessio
label.style.flex = '1'; label.style.flex = '1';
label.style.fontSize = '14px'; label.style.fontSize = '14px';
label.style.textAlign = 'left'; label.style.textAlign = 'left';
label.textContent = entry.spec.partNumber ?? describeSpec(entry.spec); label.textContent = entry.spec.partNumber ?? describeSpec(entry.spec, { placeholders: true });
row.appendChild(label); row.appendChild(label);
const sourceLabel = document.createElement('span'); const sourceLabel = document.createElement('span');