Drucker-App — Direkt-zum-Drucker (Stufe 2)
Das Problem
Office-Konverter löst Browser-Druck-Dialog. Was er nicht löst:
- Massendruck — 50 Klassenarbeiten zum Pausenkopierer schicken, ohne 50 mal Druck-Dialog zu klicken
- Drucken aus dem Smartphone/Tablet — Browser-Druck-Dialog auf iOS ist unbrauchbar
- Asynchron drucken — "drucke das heute Abend, wenn ich nicht mehr da bin"
- Audit — wer hat wann was gedruckt? Heute kein Log
- Quotas — Eltern-Briefe-Druck der KiTa: "max 200 Seiten/Monat" — geht nicht
- Druck aus Workflow — eine Kaskade soll am Ende automatisch einen Bescheid zum Sekretariat-Drucker schicken
- Mobile-Drucken aus Mein Fach — Brief unterschreiben + sofort drucken, ohne PDF-Download-Umweg
Lösung: Eine "Drucker"-App auf Basis des Konverter-Service
Aufbau auf dem Office-Konverter: das Konvertieren zu PDF passiert dort. Die Drucker-App nimmt das fertige PDF und schickt es an einen physischen Drucker.
┌────────────────────────────────────┐
│ Frontend │
│ "Drucken" → Druckziel waehlen: │
│ • Browser-Dialog (Stufe 1) │
│ • Schul-Drucker "Sekretariat" │ ← NEU (App "Drucker")
│ • Schul-Drucker "Lehrerzimmer" │ ← NEU
└─────────────┬──────────────────────┘
│ Job einreichen
▼
┌────────────────────────────────────┐
│ Platform-API │
│ POST /print/jobs │
│ → Konvertiere zu PDF (vorhanden) │
│ → Job in Queue (Redis) │
│ → Response: { jobId, status:queued }│
└─────────────┬──────────────────────┘
│ Job-Auslieferung
▼
┌─────┴──────┐
│ │
Variante 1 Variante 2
(Pull) (Push)
Print-Agent Direkt-IPP
│ │
▼ ▼
Drucker DruckerKern-Insight: Schul-Drucker hängen meist im LAN, nicht im Internet. Variante 1 (Pull) ist deshalb der pragmatische Weg.
Zwei Lieferpfade — Pull vs. Push
Variante 1 (Default): Print-Agent in der Schule
Ein kleines Prilog Print Agent-Programm läuft auf einem PC oder Raspberry Pi in der Schule:
- Login mit Tenant-Token (einmalig per QR-Code beim Setup)
- Long-Poll oder WebSocket gegen Prilog-API: "Gibt es offene Jobs?"
- Lädt PDF herunter
- Druckt lokal via System-Driver (CUPS/Windows-Spooler) — nutzt die Drucker, die dem PC ohnehin bekannt sind
- Meldet Status zurück: queued → printing → printed | error
Vorteile:
- Kein Tunneling, keine Firewall-Aufrufe nötig
- Schule muss keine Ports freigeben
- Funktioniert mit jedem Drucker den der Host-PC bedienen kann (auch USB-Drucker)
- Keine sensiblen Anmeldedaten der Schul-Drucker auf Prilog-Servern
Nachteile:
- Schule muss einen Rechner laufen lassen (typisch: Sekretariats-PC, oder ein Raspberry Pi für ~50€)
- Druck-Latenz ~5–10 Sekunden (Polling-Intervall)
Variante 2 (optional): Direkt-IPP
Für Tenants mit erreichbarem Drucker (Cloud, Praxis, Hetzner-VPN-Setup):
- Tenant gibt IPP-Endpoint, Queue, Credentials in Settings ein (
ipp://printer.intern:631/printers/sekretariat) - Backend pusht direkt via IPP (
ippnpm-Library) - Geht nur bei direkter Erreichbarkeit oder bestehendem VPN (Tailscale/WireGuard)
Realistisch nur für Tech-affine Schulen oder kommerzielle Tenants.
Datenmodell
model Printer {
id String @id @default(cuid()) @db.VarChar(50)
tenantId String @map("tenant_id") @db.VarChar(64)
name String @db.VarChar(100) // "Sekretariat"
location String? @db.VarChar(200) // "Verwaltungsbau Raum 12"
kind String @default("agent") @db.VarChar(20) // agent | ipp
agentId String? @map("agent_id") @db.VarChar(50) // bei kind=agent
ippEndpoint String? @map("ipp_endpoint") @db.VarChar(500)
ippAuth Json? @map("ipp_auth") // verschluesselt
/// Welche User/Spaces duerfen drucken?
visibility String @default("tenant") @db.VarChar(20)
visibilityScopes String[]
/// Erlaubte Optionen (a4 only? duplex? farbe?)
capabilities Json @default("{}")
active Boolean @default(true)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
jobs PrintJob[]
agent PrintAgent? @relation(fields: [agentId], references: [id])
@@index([tenantId, active])
@@map("printers")
}
model PrintAgent {
id String @id @default(cuid()) @db.VarChar(50)
tenantId String @map("tenant_id") @db.VarChar(64)
hostname String @db.VarChar(255)
agentToken String @map("agent_token") @db.VarChar(64) // bcrypt-hashed
lastSeenAt DateTime? @map("last_seen_at")
version String? @db.VarChar(50)
createdAt DateTime @default(now()) @map("created_at")
printers Printer[]
@@index([tenantId, lastSeenAt])
@@map("print_agents")
}
model PrintJob {
id String @id @default(cuid()) @db.VarChar(50)
tenantId String @map("tenant_id") @db.VarChar(64)
printerId String @map("printer_id") @db.VarChar(50)
documentId String? @map("document_id") @db.VarChar(50) // ggf. multi-doc
pdfStorageKey String? @map("pdf_storage_key") @db.VarChar(1000) // gecachtes PDF
pages Int?
copies Int @default(1)
duplex Boolean @default(false)
color Boolean @default(true)
/// queued | printing | printed | error | cancelled
status String @default("queued") @db.VarChar(20)
error String? @db.Text
requestedBy String @map("requested_by") @db.VarChar(255)
scheduledFor DateTime? @map("scheduled_for")
startedAt DateTime? @map("started_at")
finishedAt DateTime? @map("finished_at")
createdAt DateTime @default(now()) @map("created_at")
printer Printer @relation(fields: [printerId], references: [id], onDelete: Cascade)
@@index([tenantId, status])
@@index([printerId, status, createdAt])
@@map("print_jobs")
}
model PrintQuota {
/// Pro Tenant ODER pro Space ODER pro UserType: max Seiten pro Monat.
id String @id @default(cuid())
tenantId String @map("tenant_id")
scopeType String @map("scope_type") // tenant | space | userType | user
scopeId String? @map("scope_id")
pagesPerMonth Int @map("pages_per_month")
resetDay Int @default(1)
createdAt DateTime @default(now()) @map("created_at")
@@map("print_quotas")
}Backend-Endpunkte
| Methode | Pfad | Wirkung |
|---|---|---|
GET /platform/v1/printers | Liste verfügbarer Drucker für aktuellen User | |
POST /platform/v1/printers | Drucker registrieren (Admin) | |
PATCH /platform/v1/printers/:id | Drucker-Settings updaten | |
DELETE /platform/v1/printers/:id | Drucker entfernen | |
POST /platform/v1/print/jobs | Job einreichen {documentIds: [], printerId, copies, duplex} | |
GET /platform/v1/print/jobs/:id | Status-Polling | |
DELETE /platform/v1/print/jobs/:id | Job abbrechen (nur queued) | |
GET /platform/v1/print/jobs?since= | Audit-Liste | |
POST /platform/v1/print/agents | Agent-Registrierung mit Tenant-Token (QR-Setup) | |
GET /platform/v1/print/agents/jobs | Long-Poll für Agent: nächster offener Job | |
POST /platform/v1/print/agents/jobs/:id/status | Agent meldet printing/printed/error |
Auth für Agent: Bearer-Token (eigene print_agents.agent_token-Tabelle), nicht JWT. Tenant-spezifisch.
UI
"Drucken"-Button
Im DMS-Detail-Pane + Doppelklick-Menü + Mehrfach-Selektion:
[Drucken ▼]
• Druck-Dialog (Browser) ← Stufe 1, immer da
───────────────────────────────────
• Sekretariat (HP LaserJet) ← App "Drucker", aktiv
• Lehrerzimmer (Brother)
───────────────────────────────────
• Drucker konfigurieren (Admin)Nach Klick: kleines Modal mit Optionen "Kopien: 1, Duplex: ja/nein, Farbe: ja/nein" + Button "Drucken (3 Seiten)".
Druckverlauf
Im Settings → Workspace → "Drucken" → "Verlauf": Liste aller Jobs der letzten 30 Tage mit Status, Drucker, User, Seitenzahl. Filter + Re-Drucken-Action.
Setup-Wizard (Admin)
Settings → Workspace → "Drucken" → "Drucker einrichten":
- Wähle Setup-Methode: Print-Agent (empfohlen) ODER Direkt-IPP
- Bei Agent: QR-Code wird angezeigt → Schul-PC öffnet Browser → installiert Agent (Windows/Mac/Linux Installer) → scannt QR → Agent ist registriert
- Sobald Agent verbunden ist: Liste aller System-Drucker des Hosts → admin wählt welche freigegeben werden
- Pro Drucker: Anzeigename, Standort, Sichtbarkeit (welche User/Spaces dürfen), Quotas optional
Print-Agent (Programm)
Tech
- Tauri-App (~3 MB Binary) — wir haben Tauri-Erfahrung (siehe
project_desktop_sync_client.md) - Win/Mac/Linux Build via GitHub Actions
- Als System-Service installiert (autostart)
- UI: einfaches Tray-Icon mit Status + Log + Logout-Button
Funktion
Beim Start:
→ Lese gespeicherten agent_token aus OS-Keychain
→ Long-Poll GET /print/agents/jobs (Timeout 30s)
Bei Job:
→ Status melden: printing
→ PDF von Backend laden (signed URL)
→ Lokale CUPS/Spooler-API: lp -d <queue> <file>
→ Auf Spooler-Done warten
→ Status melden: printed (oder error mit Message)
→ Nächster Long-PollSetup-Flow
- User installiert Agent (.exe / .dmg / .deb)
- Agent öffnet
https://leander.prilog.team/print-agent-setup - Browser zeigt QR-Code mit
tenantId + temp-agent-secret - Agent scannt → POST
/print/agents→ bekommtagent_tokenzurück → speichert in Keychain - Liste der lokalen Drucker (
lpstat -a) → User wählt aus → registriert alsPrintermitagentId
Phasenplan
| Phase | Inhalt | Aufwand |
|---|---|---|
| A | Migration (printers, print_agents, print_jobs, print_quotas) + Backend-CRUD für Printers | ~3h |
| B | Print-Job-Submission + Queue + Konvertierung über Office-Konverter (Reuse) | ~3h |
| C | Tauri Print-Agent: Long-Poll + lokales Drucken via CUPS/Spooler + Status-Reporting | ~6h |
| D | Setup-Wizard + QR-Pairing | ~2h |
| E | Frontend: Drucken-Dropdown + Druck-Modal + Druckverlauf | ~3h |
| F | Quotas (counting + monatlicher Reset) + Quota-Anzeige im UI | ~2h |
| G | Direkt-IPP-Variante (push) — fuer Tech-affine Schulen | ~4h |
| H | Auto-Print aus Workflows/Kaskaden (Side-Effect-Type 'flow.print') | ~2h |
MVP = A+B+C+D+E (~17h, 2–3 Tage). Quotas (F), IPP (G), Workflow-Print (H) sind Komfort-Erweiterungen.
Status 2026-05-06 22:00 UTC: A+B+E+G LIVE (Direkt-IPP-Variante).
- Migration 0058: printers + print_jobs Tabellen
- Backend: CRUD
/printers, POST/print/jobsmit async IPP-Dispatch - AES-GCM-Encrypted IPP-Passwoerter (env IPP_ENC_KEY)
- Frontend: PrintButton-Dropdown mit registrierten Druckern, Settings → Workspace → "Drucker" zum Eintragen von IPP-Endpunkten
- Tauri Print-Agent (Phasen C+D) NICHT umgesetzt — separate Desktop-App noetig, ueber Nacht zu riskant. Schul-Drucker mit IPP-Endpoint im Internet oder VPN funktionieren ueber den Direkt-Pfad.
- Multi-Doc-Print (PDF-Merge) noch nicht — single Document only.
App-Vermarktung
Im Plugin-Store:
- Name: Drucken
- Icon:
print - Tagline: "Direkt aus Prilog auf den Schul-Drucker. Sicher, schnell, mit Verlauf."
- Default-Status: deaktiviert — Tenant aktiviert bewusst (Setup-Aufwand)
- Stripe-Item: Add-on, monatlich, Pauschale (drei Drucker inklusive, jeder weitere kostet extra)
- Voraussetzung: Office-Konverter-App muss aktiv sein (oder kommt mit gebundled)
Sicherheit & DSGVO
- PDF-Cache nach Job — direkt nach
printedlöschen (kein PDF in S3 herumliegen) - Audit-Log — jeder Print-Job mit User, Document, Drucker, Seitenzahl in
print_jobsTabelle, 90 Tage retained - Job-Cancel — nur queued/printing-Jobs, nach
printedist die Hardcopy auf Papier nicht mehr digital löschbar - Agent-Token — bcrypt-gehasht in DB, nur Plain-Text auf dem Agent-Host (OS-Keychain)
- Print-Agent-Auto-Update — signed Binaries, Update-Channel via GitHub Releases
- Datenfluss — Quell-Document → Konverter (cleanup nach Convert) → S3-PDF (cleanup nach Print) → Agent (Memory only) → Drucker
- Schulnetz-Isolation — Agent ist outbound-only, keine Inbound-Ports nötig
Offene Fragen
- Workflow-Print — bei Kaskaden-Auto-Print: wer ist
requestedBy? System-Bot oder Workflow-Initiator? Empfehlung: Workflow-Initiator, Audit klar. - Multi-Doc-Print — 50 Klassenarbeiten als ein Job (PDF-merge) oder 50 einzelne Jobs in Queue? Empfehlung: ein Job mit
documentIds: [], Konverter merged zu einer PDF — schneller, kein Drucker-Stau. - Mobile-Print — auf iOS hat
window.print()Limitierungen. Drucker-App umgeht das komplett (Smartphone → API → Agent → Drucker), weil keine Browser-Druck-Engine involviert. Sollte explizit in der Mobile-UI prominent sein. - Cloud-Hosting fuer Print-Agent — kann der Agent auch auf einem Hetzner-Server laufen (wenn Schul-Drucker public IPP hat)? Ja, gleicher Code. Edge-Case.
- Druck-Vorschau — soll das Modal eine PDF-Vorschau zeigen vor "Drucken"-Klick? Empfehlung: ja, weil Schule-typisch "ich druck immer das falsche". Zeigt das Konverter-PDF als kleinen Preview-Frame.
Warum so?
- Pull-Agent ist DER Schul-realistische Weg — kein Firewall-Aufruf, kein VPN-Zwang, kein public-IPP-Risiko
- Reuse Office-Konverter — kein Doppel-Code für Convert-zu-PDF, beide Apps teilen die Pipeline
- Audit + Quotas inbuilt — adressiert Compliance + Kosten-Kontrolle die Schulen heute oft per Excel/Block lösen
- App-Pricing — eine Schule die nicht druckt zahlt nichts, eine die viel druckt zahlt fair
- Workflow-Integration als Phase H — Bescheid-aus-Kaskade-zum-Drucker schafft Wow-Moment, kommt aber nach Stabilität