Drucker wird in der App gewaehlt, nicht auf dem Pi
Deploy auf den Timer-Server / deploy (push) Successful in 20s

Das Einrichtungsskript fragte bisher am Pi, welcher Drucker bedient werden
soll. Damit stand die Einstellung an einem Ort, an den man nur mit einem
Terminal kommt - und ein zweiter Drucker am Pi bedeutete: Skript nochmal.

Jetzt fragt die App den Pi, was angeschlossen ist, und der Bediener waehlt in
den Einstellungen. Die Bruecke bekam dafuer /devices; sie liest die
Klartextnamen aus dem sysfs, damit dort "EPSON TM-T88IV" steht und nicht
"lp0" - bei zwei Druckern der Unterschied zwischen Wissen und Raten.

Der gewaehlte Pfad geht bei jedem Auftrag im Kopf X-Print-Device mit. Die
Bruecke haelt dazu bewusst keinen Zustand und das Skript schreibt kein
PRINT_DEVICE mehr: es soll genau eine Stelle geben, die weiss, welcher Drucker
gemeint ist, naemlich die, an der es bedient wird. Leer bleibt "automatisch" -
das erste gefundene Geraet, zugleich der Rueckfall, wenn aus lp0 nach dem
Umstecken ein lp1 geworden ist.

Der Pfad kommt damit von aussen und wird an beiden Enden geprueft, in
PUT /api/settings und noch einmal in der Bruecke. Eine Schranke, die nur an
einem Ende steht, ist keine: ohne die Pruefung auf dem Pi liesse sich der
Dienst dazu bringen, in eine beliebige Datei zu schreiben.

Nebenbei: ein nicht bereiter Drucker meldete sich in der Oberflaeche als
"HTTP 503". Das verriet niemandem, dass nur ein Stecker lose ist. Die 503 der
Bruecke ist kein Protokollfehler, und ihr Rumpf sagt, was fehlt - der wird
jetzt ausgewertet.

Geprueft am echten Pi (Debian 13, TM-T88IV): Einrichtung ohne Rueckfrage,
Geraeteliste mit Namen, ausdrueckliche Wahl, Druck darauf, Rueckkehr auf
automatisch, Druck darauf. Dazu die Abweise: /etc/passwd und ein Pfad mit
angehaengtem Befehl werden zurueckgewiesen, ein erlaubtes aber fehlendes
/dev/usb/lp7 ergibt eine Meldung im Klartext.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kerim (portable/claudecode)andClaude Opus 5 committed 2026-09-12 13:46:50 +02:00
1 parent e68daee457
commit 0e8d826bce
10 files changed
+412 -104

No files matched your search

+93 -1
View File
@@ -11,6 +11,7 @@ const state = {
timers: [], // {id, customerName, isRunning, baseMs, syncedAt}
rate: { rateAmount: 10, rateInterval: 10 },
printerConfigured: false, // steuert, ob die Bon-Knoepfe erscheinen
printerDevice: '', // gewaehltes Geraet am Pi, leer = automatisch
printAutoOnStop: true, // Bon beim Beenden automatisch drucken
tickHandle: null,
syncHandle: null,
@@ -564,6 +565,7 @@ async function loadSettings() {
const settings = await api('/api/settings');
state.rate = settings;
state.printerConfigured = !!(settings.printer && settings.printer.configured);
state.printerDevice = (settings.printer && settings.printer.device) || '';
state.printAutoOnStop = settings.printAutoOnStop !== false;
document.getElementById('rateAmount').value = settings.rateAmount;
@@ -578,6 +580,7 @@ async function loadSettings() {
updateSettingsPreview();
renderPrinterState(settings.printer);
refreshPrinterStatus();
loadDevices();
} catch (e) {
toast('Einstellungen konnten nicht geladen werden: ' + e.message, 'error');
}
@@ -670,6 +673,94 @@ async function refreshPrinterStatus() {
}
}
/**
* Die Druckerliste vom Pi holen.
*
* Sie kommt bewusst live und nicht aus dem Zwischenspeicher: wer hier auf
* "Suchen" tippt, hat gerade ein Kabel umgesteckt und will genau das sehen.
*
* @param {boolean} lautstark Rueckmeldung geben - beim Knopfdruck ja, beim
* stillen Laden der Einstellungen nein.
*/
async function loadDevices(lautstark = false) {
const feld = document.getElementById('printerDevice');
const gruppe = document.getElementById('printerDeviceGroup');
const hinweis = document.getElementById('printerDeviceHint');
if (!feld) return;
// Ohne eingerichteten Drucker gibt es nichts auszuwaehlen.
gruppe.style.display = state.printerConfigured ? '' : 'none';
if (!state.printerConfigured) return;
const gewaehlt = state.printerDevice || '';
try {
const res = await api('/api/printer/devices');
state.printerDevice = res.selected || '';
const automatisch = res.fallback
? `automatisch (zurzeit ${res.fallback})`
: 'automatisch';
feld.innerHTML = `<option value="">${escapeHtml(automatisch)}</option>` +
res.devices.map(g => {
const beschriftung = g.name ? `${g.path} — ${g.name}` : g.path;
return `<option value="${escapeHtml(g.path)}">${escapeHtml(beschriftung)}</option>`;
}).join('');
// Der eingestellte Drucker haengt gerade nicht dran. Er darf trotzdem
// nicht aus der Liste fallen, sonst stellt ein Speichern ihn unbemerkt
// auf "automatisch" um.
const eingestellt = res.selected || '';
if (eingestellt && !res.devices.some(g => g.path === eingestellt)) {
feld.innerHTML += `<option value="${escapeHtml(eingestellt)}">` +
`${escapeHtml(eingestellt)} — zurzeit nicht angeschlossen</option>`;
}
feld.value = eingestellt;
feld.disabled = false;
hinweis.textContent = res.devices.length
? 'Welches Gerät am Raspberry Pi bedruckt wird.'
: 'Am Pi hängt gerade kein Drucker. Kabel prüfen, dann erneut suchen.';
if (lautstark) {
toast(res.devices.length
? `${res.devices.length} Drucker gefunden`
: 'Am Pi hängt gerade kein Drucker', res.devices.length ? 'success' : 'error');
}
} catch (e) {
feld.innerHTML = `<option value="">${escapeHtml(gewaehlt || 'automatisch')}</option>`;
feld.disabled = true;
hinweis.textContent = 'Liste nicht abrufbar: ' + e.message;
if (lautstark) toast('Drucker konnten nicht abgerufen werden: ' + e.message, 'error');
}
}
/** Die Auswahl sofort speichern - ein eigener Speichern-Knopf waere hier
* eine Falle, weil das Feld weit ueber dem Knopf steht. */
async function saveDevice() {
const feld = document.getElementById('printerDevice');
const vorher = state.printerDevice || '';
const neu = feld.value;
try {
const antwort = await api('/api/settings', {
method: 'PUT',
body: JSON.stringify({
rateAmount: state.rate.rateAmount,
rateInterval: state.rate.rateInterval,
printerDevice: neu
})
});
state.printerDevice = (antwort.printer && antwort.printer.device) || '';
toast(neu ? 'Drucker gewählt: ' + neu : 'Drucker wird automatisch gewählt', 'success');
await refreshPrinterStatus();
} catch (e) {
feld.value = vorher;
toast('Auswahl nicht gespeichert: ' + e.message, 'error');
}
}
async function testPrint() {
const btn = document.getElementById('printerTestBtn');
if (btn) btn.disabled = true;
@@ -801,8 +892,9 @@ function startSetupWatch() {
{ name: 'Übernommen', ok: true,
detail: 'Die App druckt ab jetzt auf diesen Pi. Ein Probedruck zeigt, ob Papier drin ist.' }
], false);
toast('Drucker eingerichtet: ' + z.host, 'success');
toast('Drucker eingerichtet: ' + z.host + ' — jetzt oben den Drucker auswählen.', 'success');
await refreshPrinterStatus();
await loadDevices();
await syncTimers().catch(() => {});
} else if (z.state === 'expired') {
stopSetupWatch();
+29 -1
View File
@@ -220,6 +220,7 @@
input[type="password"],
input[type="number"],
input[type="date"],
select,
textarea {
width: 100%;
padding: 0.875rem 1.125rem;
@@ -233,6 +234,7 @@
}
input:focus,
select:focus,
textarea:focus {
outline: none;
border-color: #4d35a9;
@@ -644,6 +646,19 @@
line-height: 1.5;
}
/* Das Auswahlfeld erbt die Eingabefeld-Optik; die Liste selbst malt der
Browser und braucht deshalb eine eigene, dunkle Fuellfarbe. */
select option { background: #2d2d44; color: #ffffff; }
.device-row {
display: flex;
gap: 0.75rem;
align-items: center;
}
.device-row select { flex: 1; }
.device-row .btn { flex-shrink: 0; }
.setup-link { margin-top: 1.25rem; }
/* Der Befehl muss vollstaendig lesbar sein - er wird abgetippt, wenn
@@ -887,6 +902,19 @@
<div id="printerState" class="printer-state">Druckerzustand wird geprüft …</div>
<div class="form-group" id="printerDeviceGroup">
<label for="printerDevice">Drucker am Pi</label>
<div class="device-row">
<select id="printerDevice" onchange="saveDevice()">
<option value="">automatisch</option>
</select>
<button type="button" class="btn btn-secondary btn-sm"
onclick="loadDevices(true)">🔄 Suchen</button>
</div>
<small id="printerDeviceHint">Welches Gerät am Raspberry Pi bedruckt wird.
„Automatisch" nimmt den ersten gefundenen.</small>
</div>
<div class="form-group">
<label for="receiptHeader">Kopfzeilen</label>
<textarea id="receiptHeader" maxlength="240" rows="3"
@@ -980,6 +1008,6 @@
<div class="toast" id="toast"></div>
<script src="/app.js?v=20260912-setup"></script>
<script src="/app.js?v=20260912-devices"></script>
</body>
</html>