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 {
createSession, proposeAssignment, commitAssignment,
undoLast, moveEntry, removeEntry,
undoLast, moveEntry, removeEntry, nextStackId,
} from './session.js';
import { saveSession, loadSession, clearSession } from './storage.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 { askResumeSession } from './ui/resume-dialog.js';
import { renderSessionList } from './ui/session-list.js';
import { describeSpec } from './ui/describe-spec.js';
const app = document.querySelector('#app');
const store = window.localStorage;
@@ -64,22 +65,7 @@ function syncView() {
/** Kurzbeschreibung fuer die "Zuletzt"-Zeile. */
function describeEntry(spec, stackId) {
const parts = [
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}`;
return `${describeSpec(spec)} → Stapel ${stackId}`;
}
/**
@@ -124,7 +110,7 @@ async function processCapture(getFrame) {
stackId = answer.stackId;
} else {
// "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) {
const teile = [
spec.capacityGb ? `${spec.capacityGb}GB` : null,
spec.rank,
spec.speed,
spec.formFactor,
].filter(Boolean);
return teile.join(' ') || 'ohne Angaben';
}
import { describeSpec } from './describe-spec.js';
/** 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.
* Gruen bei Barcode, gelb bei OCR. Keine Eingabe noetig.
*/
export function showResult(root, { stackId, spec, confidence }, dauerMs = 1200) {
const vorherige = aktuelleRueckmeldung.get(root);
if (vorherige) {
clearTimeout(vorherige.timer);
vorherige.overlay.remove();
vorherige.resolve();
aktuelleRueckmeldung.delete(root);
export function showResult(root, { stackId, spec, confidence }, durationMs = 1200) {
const previous = currentFeedback.get(root);
if (previous) {
clearTimeout(previous.timer);
previous.overlay.remove();
previous.resolve();
currentFeedback.delete(root);
}
const overlay = document.createElement('div');
@@ -31,16 +23,16 @@ export function showResult(root, { stackId, spec, confidence }, dauerMs = 1200)
<div class="detail"></div>
<div class="pn"></div>
`;
overlay.querySelector('.detail').textContent = beschreibung(spec);
overlay.querySelector('.detail').textContent = describeSpec(spec);
overlay.querySelector('.pn').textContent = spec.partNumber ?? '';
root.appendChild(overlay);
return new Promise((resolve) => {
const timer = setTimeout(() => {
overlay.remove();
aktuelleRueckmeldung.delete(root);
currentFeedback.delete(root);
resolve();
}, dauerMs);
aktuelleRueckmeldung.set(root, { overlay, timer, resolve });
}, durationMs);
currentFeedback.set(root, { overlay, timer, resolve });
});
}
+3 -12
View File
@@ -3,16 +3,7 @@
// Sitzung. Kennt weder Kamera noch Erkennung - alles kommt ueber die
// Rueckrufe, genau wie bei scan-view.js und ambiguous-dialog.js.
/** Kurzbeschreibung eines Specs fuer Stapel-Kopfzeilen und Eintrags-Zeilen ohne Teilenummer. */
function describeSpec(spec) {
const parts = [
spec.capacityGb ? `${spec.capacityGb}GB` : '?',
spec.rank ?? '?',
spec.speed ?? '?',
spec.formFactor ?? '?',
];
return parts.join(' ');
}
import { describeSpec } from './describe-spec.js';
/**
* Kurzbezeichnung der Erkennungsquelle eines Eintrags - beim Sortieren
@@ -52,7 +43,7 @@ export function renderSessionList(root, session, { onMove, onRemove, onEndSessio
header.className = 'detail';
header.style.textAlign = 'left';
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);
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.fontSize = '14px';
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);
const sourceLabel = document.createElement('span');