Drucker-Pi holt sich die Einrichtung, statt sie geschickt zu bekommen
Bisher richtete die App den Pi per SSH ein: Adresse, Benutzer und Passwort ins Webformular, Knopf druecken. Das verlangte ein SSH-Passwort in einem Browser, ein weiteres Paket (ssh2) und eine Verbindung vom Server zum Pi. Jetzt andersherum. Die App legt einen Link bereit und wartet. Auf dem Pi wird das Skript geholt und gestartet, dort wird der Drucker ausgewaehlt, und am Ende meldet sich der Pi von selbst an. Wer einen Drucker ansteckt, steht ohnehin am Pi - dort gehoert auch die Frage hin, welcher es sein soll. Die Auswahl zeigt Klartextnamen aus dem sysfs: "EPSON TM-T88IV" statt "lp0". Bei mehreren Druckern ist das der Unterschied zwischen Wissen und Raten. Das ausgelieferte Skript ist zusammengesetzt, nicht statisch: einrichten.sh ist eine Vorlage, in die setup.js Adresse, Code, print-server.py und die systemd-Unit einsetzt. Heraus kommt EINE Datei - auf dem Pi ist nichts nachzuladen, und eine halb geglueckte Uebertragung kann es nicht geben. Zwei Endpunkte muessen ohne Anmeldung auskommen, weil der Pi sich bei Authentik nicht anmelden kann. Der Einrichtungscode ist dort der einzige Schluessel: 32 Zeichen, gilt einmal und 30 Minuten, nur im Arbeitsspeicher, zeitkonstant verglichen, hinter einem eigenen Zaehler. Die Adresse des Pi wird der Verbindung entnommen und nicht dem Rumpf der Anmeldung - was der Pi ueber sich behauptet, koennte auch jemand anders behaupten. Die eigene Adresse leitet die App aus dem Host-Kopf ab und prueft sie streng, statt sie zu maskieren: der Wert landet in einem Shell-Skript. Geprueft am echten Pi (Debian 13, TM-T88IV): Link erzeugen, am Pi abholen, Drucker aus der Liste waehlen, Anmeldung, Uebernahme, Probedruck. Dazu die Abweise: verbrauchter Code, falscher Code, Abholung ohne Code, und dass die uebrige API weiterhin 401 liefert. Entfaellt: provision.js, install.sh, die Abhaengigkeit ssh2, das SSH-Formular. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8c5cdf16c4
commit
e68daee457
14 files changed
+834
-605
No files matched your search
+93
-41
@@ -14,6 +14,8 @@ const state = {
|
||||
printAutoOnStop: true, // Bon beim Beenden automatisch drucken
|
||||
tickHandle: null,
|
||||
syncHandle: null,
|
||||
setupWatch: null, // wartet auf die Anmeldung des Drucker-Pi
|
||||
setupCommand: '', // der Befehl, der auf dem Pi ausgefuehrt wird
|
||||
history: { offset: 0, limit: 50, reachedEnd: false }
|
||||
};
|
||||
|
||||
@@ -692,7 +694,7 @@ function toggleSetup() {
|
||||
box.style.display = offen ? 'none' : 'block';
|
||||
document.getElementById('setupToggleBtn').textContent =
|
||||
offen ? '🔧 Drucker einrichten' : '✕ Einrichtung schließen';
|
||||
if (!offen) document.getElementById('setupHost').focus();
|
||||
if (offen) stopSetupWatch();
|
||||
}
|
||||
|
||||
/** Protokoll der Einrichtung anzeigen - ein Kasten je Schritt. */
|
||||
@@ -711,59 +713,109 @@ function renderSetupLog(steps, laufend) {
|
||||
|
||||
el.innerHTML = zeilen + (laufend
|
||||
? `<div class="setup-step"><span class="setup-step-icon">⏳</span>
|
||||
<div><div class="setup-step-name">läuft …</div></div></div>`
|
||||
<div><div class="setup-step-name">wartet auf den Pi …</div></div></div>`
|
||||
: '');
|
||||
}
|
||||
|
||||
async function startProvision() {
|
||||
const host = document.getElementById('setupHost').value.trim();
|
||||
const port = document.getElementById('setupPort').value;
|
||||
const username = document.getElementById('setupUser').value.trim();
|
||||
const passwordEl = document.getElementById('setupPassword');
|
||||
const password = passwordEl.value;
|
||||
|
||||
if (!host) return toast('Bitte die Adresse des Pi eintragen.', 'error');
|
||||
if (!username) return toast('Bitte den Benutzernamen eintragen.', 'error');
|
||||
if (!password) return toast('Bitte das Passwort eintragen.', 'error');
|
||||
|
||||
if (state.printerConfigured && !confirm(
|
||||
'Es ist bereits ein Drucker eingerichtet.\n\n' +
|
||||
`Nach der Einrichtung druckt die App auf ${host}. Fortfahren?`
|
||||
)) return;
|
||||
|
||||
/** Einrichtungslink erzeugen und danach auf die Rueckmeldung des Pi warten. */
|
||||
async function startSetup() {
|
||||
const btn = document.getElementById('setupStartBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ Wird eingerichtet …';
|
||||
renderSetupLog([], true);
|
||||
|
||||
try {
|
||||
const res = await api('/api/printer/provision', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ host, port, username, password })
|
||||
});
|
||||
const res = await api('/api/printer/setup-code', { method: 'POST' });
|
||||
|
||||
// Das Passwort hat seinen Zweck erfuellt und hat im Formular nichts
|
||||
// mehr verloren.
|
||||
passwordEl.value = '';
|
||||
state.setupCommand = res.command;
|
||||
document.getElementById('setupCommand').textContent = res.command;
|
||||
document.getElementById('setupLink').style.display = 'block';
|
||||
|
||||
renderSetupLog(res.steps, false);
|
||||
state.printerConfigured = true;
|
||||
const bis = new Date(res.expiresAt);
|
||||
document.getElementById('setupExpiry').textContent =
|
||||
`Der Link gilt einmal und läuft um ${bis.getHours()}:` +
|
||||
`${String(bis.getMinutes()).padStart(2, '0')} Uhr ab. ` +
|
||||
'Danach hier einfach einen neuen erzeugen.';
|
||||
|
||||
if (res.ready) {
|
||||
toast('Drucker eingerichtet und erreichbar. Ein Probedruck zeigt, ob Papier drin ist.', 'success');
|
||||
} else {
|
||||
toast('Einrichtung durchgelaufen, der Drucker antwortet aber noch nicht – siehe Protokoll.', 'error');
|
||||
}
|
||||
await refreshPrinterStatus();
|
||||
await syncTimers().catch(() => {});
|
||||
renderSetupLog([{ name: 'Link erzeugt', ok: true, detail: res.url }], true);
|
||||
startSetupWatch();
|
||||
toast('Link erzeugt. Jetzt am Pi ausführen.', 'success');
|
||||
} catch (e) {
|
||||
// Die Schritte bis zum Abbruch stecken in der Fehlerantwort und sind
|
||||
// das Wertvollste am ganzen Vorgang - deshalb trotzdem anzeigen.
|
||||
renderSetupLog(e.steps, false);
|
||||
toast('Einrichtung fehlgeschlagen: ' + e.message, 'error');
|
||||
toast('Link konnte nicht erzeugt werden: ' + e.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🚀 Jetzt einrichten';
|
||||
btn.textContent = '🔗 Neuen Link erzeugen';
|
||||
}
|
||||
}
|
||||
|
||||
function copySetupCommand() {
|
||||
const text = state.setupCommand || '';
|
||||
if (!text) return;
|
||||
|
||||
// navigator.clipboard gibt es nur auf HTTPS oder localhost. Im Laden laeuft
|
||||
// das hier auch mal ueber schlichtes HTTP - dann faellt es auf den alten
|
||||
// Weg zurueck, statt wortlos nichts zu tun.
|
||||
const alterWeg = () => {
|
||||
const feld = document.createElement('textarea');
|
||||
feld.value = text;
|
||||
feld.style.position = 'fixed';
|
||||
feld.style.opacity = '0';
|
||||
document.body.appendChild(feld);
|
||||
feld.select();
|
||||
try { document.execCommand('copy'); } catch (e) { /* dann eben abtippen */ }
|
||||
document.body.removeChild(feld);
|
||||
};
|
||||
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text).then(
|
||||
() => toast('Befehl kopiert', 'success'),
|
||||
() => { alterWeg(); toast('Befehl kopiert', 'success'); }
|
||||
);
|
||||
} else {
|
||||
alterWeg();
|
||||
toast('Befehl kopiert', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auf die Anmeldung des Pi warten.
|
||||
*
|
||||
* Der Pi meldet sich bei der App, nicht umgekehrt - die App kann also nur
|
||||
* nachsehen. Alle drei Sekunden ist oft genug, um sich wie eine Rueckmeldung
|
||||
* anzufuehlen, und selten genug, um niemandem wehzutun.
|
||||
*/
|
||||
function startSetupWatch() {
|
||||
stopSetupWatch();
|
||||
state.setupWatch = setInterval(async () => {
|
||||
let z;
|
||||
try {
|
||||
z = await api('/api/printer/setup-status');
|
||||
} catch (e) {
|
||||
return; // Netz weg, Anmeldung abgelaufen - beim naechsten Mal wieder
|
||||
}
|
||||
|
||||
if (z.state === 'done') {
|
||||
stopSetupWatch();
|
||||
state.printerConfigured = true;
|
||||
renderSetupLog([
|
||||
{ name: 'Pi angemeldet', ok: true,
|
||||
detail: `${z.host}${z.device ? ' · Drucker ' + z.device : ''}` },
|
||||
{ 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');
|
||||
await refreshPrinterStatus();
|
||||
await syncTimers().catch(() => {});
|
||||
} else if (z.state === 'expired') {
|
||||
stopSetupWatch();
|
||||
renderSetupLog([{ name: 'Link abgelaufen', ok: false,
|
||||
detail: 'Es hat sich kein Pi gemeldet. Bitte einen neuen Link erzeugen.' }], false);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function stopSetupWatch() {
|
||||
if (state.setupWatch) {
|
||||
clearInterval(state.setupWatch);
|
||||
state.setupWatch = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+50
-36
@@ -644,6 +644,37 @@
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.setup-link { margin-top: 1.25rem; }
|
||||
|
||||
/* Der Befehl muss vollstaendig lesbar sein - er wird abgetippt, wenn
|
||||
das Kopieren ins Leere geht (kein HTTPS, fremdes Tablet). Deshalb
|
||||
umbrechen statt abschneiden. */
|
||||
.setup-command {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
padding: 0.85rem 1rem;
|
||||
}
|
||||
|
||||
.setup-command code {
|
||||
flex: 1;
|
||||
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.6;
|
||||
color: #d7d7ff;
|
||||
word-break: break-all;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.setup-command .btn { flex-shrink: 0; }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.setup-command { flex-direction: column; }
|
||||
}
|
||||
|
||||
/* Protokoll der Einrichtung - Schritt fuer Schritt, damit im Fehlerfall
|
||||
sichtbar ist, wie weit es gekommen ist. */
|
||||
.setup-log {
|
||||
@@ -903,47 +934,30 @@
|
||||
</div>
|
||||
|
||||
<div class="setup-box" id="setupBox" style="display: none;">
|
||||
<h3 style="margin-bottom: 0.35rem;">Neuen Drucker-Pi einrichten</h3>
|
||||
<h3 style="margin-bottom: 0.35rem;">Drucker-Pi einrichten</h3>
|
||||
<p style="color: rgba(255,255,255,0.7); font-size: 0.9rem; margin-bottom: 1.25rem;">
|
||||
Richtet den Druckdienst auf einem frisch aufgesetzten Raspberry Pi
|
||||
vollständig ein und übernimmt ihn anschließend. Der Pi braucht nur
|
||||
SSH, Python 3 und den Drucker am USB-Anschluss.
|
||||
Die App legt ein Einrichtungsskript bereit. Das wird auf dem Pi
|
||||
heruntergeladen und gestartet — dort wird auch ausgewählt, welcher
|
||||
Drucker verwendet werden soll. Danach meldet sich der Pi von selbst
|
||||
hier an. Der Pi braucht nur Python 3 und den Drucker am USB-Anschluss.
|
||||
</p>
|
||||
|
||||
<div class="setup-grid">
|
||||
<div class="form-group">
|
||||
<label for="setupHost">Adresse des Pi</label>
|
||||
<input type="text" id="setupHost" placeholder="192.168.8.157" autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="setupPort">SSH-Port</label>
|
||||
<input type="number" id="setupPort" value="22" min="1" max="65535">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setup-grid" style="margin-top: 1rem;">
|
||||
<div class="form-group">
|
||||
<label for="setupUser">Benutzer</label>
|
||||
<input type="text" id="setupUser" value="pi" autocomplete="off">
|
||||
<small>Muss auf dem Pi sudo dürfen</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="setupPassword">Passwort</label>
|
||||
<input type="password" id="setupPassword" autocomplete="new-password">
|
||||
<small>Wird auch für sudo verwendet</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-actions" style="margin-top: 1.25rem; margin-bottom: 0;">
|
||||
<div class="settings-actions" style="margin-bottom: 0;">
|
||||
<button type="button" class="btn" id="setupStartBtn"
|
||||
onclick="startProvision()">🚀 Jetzt einrichten</button>
|
||||
onclick="startSetup()">🔗 Einrichtungslink erzeugen</button>
|
||||
</div>
|
||||
|
||||
<p class="setup-hint">
|
||||
Das Passwort wird einmal verwendet und danach verworfen — gespeichert
|
||||
wird nur ein neu erzeugtes Drucker-Token. Ein vorhandener Druckdienst
|
||||
auf demselben Pi wird dabei überschrieben.
|
||||
</p>
|
||||
<div id="setupLink" class="setup-link" style="display: none;">
|
||||
<p class="setup-step-name" style="margin-bottom: 0.5rem;">
|
||||
Auf dem Pi im Terminal ausführen:
|
||||
</p>
|
||||
<div class="setup-command">
|
||||
<code id="setupCommand"></code>
|
||||
<button type="button" class="btn btn-secondary btn-sm"
|
||||
onclick="copySetupCommand()">📋 Kopieren</button>
|
||||
</div>
|
||||
<p class="setup-hint" id="setupExpiry"></p>
|
||||
</div>
|
||||
|
||||
<div class="setup-log" id="setupLog"></div>
|
||||
</div>
|
||||
@@ -966,6 +980,6 @@
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script src="/app.js?v=20260912-barcode"></script>
|
||||
<script src="/app.js?v=20260912-setup"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user