Skip to content

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      Drucker

Kern-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 (ipp npm-Library)
  • Geht nur bei direkter Erreichbarkeit oder bestehendem VPN (Tailscale/WireGuard)

Realistisch nur für Tech-affine Schulen oder kommerzielle Tenants.


Datenmodell

prisma
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

MethodePfadWirkung
GET /platform/v1/printersListe verfügbarer Drucker für aktuellen User
POST /platform/v1/printersDrucker registrieren (Admin)
PATCH /platform/v1/printers/:idDrucker-Settings updaten
DELETE /platform/v1/printers/:idDrucker entfernen
POST /platform/v1/print/jobsJob einreichen {documentIds: [], printerId, copies, duplex}
GET /platform/v1/print/jobs/:idStatus-Polling
DELETE /platform/v1/print/jobs/:idJob abbrechen (nur queued)
GET /platform/v1/print/jobs?since=Audit-Liste
POST /platform/v1/print/agentsAgent-Registrierung mit Tenant-Token (QR-Setup)
GET /platform/v1/print/agents/jobsLong-Poll für Agent: nächster offener Job
POST /platform/v1/print/agents/jobs/:id/statusAgent 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":

  1. Wähle Setup-Methode: Print-Agent (empfohlen) ODER Direkt-IPP
  2. Bei Agent: QR-Code wird angezeigt → Schul-PC öffnet Browser → installiert Agent (Windows/Mac/Linux Installer) → scannt QR → Agent ist registriert
  3. Sobald Agent verbunden ist: Liste aller System-Drucker des Hosts → admin wählt welche freigegeben werden
  4. Pro Drucker: Anzeigename, Standort, Sichtbarkeit (welche User/Spaces dürfen), Quotas optional

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-Poll

Setup-Flow

  1. User installiert Agent (.exe / .dmg / .deb)
  2. Agent öffnet https://leander.prilog.team/print-agent-setup
  3. Browser zeigt QR-Code mit tenantId + temp-agent-secret
  4. Agent scannt → POST /print/agents → bekommt agent_token zurück → speichert in Keychain
  5. Liste der lokalen Drucker (lpstat -a) → User wählt aus → registriert als Printer mit agentId

Phasenplan

PhaseInhaltAufwand
AMigration (printers, print_agents, print_jobs, print_quotas) + Backend-CRUD für Printers~3h
BPrint-Job-Submission + Queue + Konvertierung über Office-Konverter (Reuse)~3h
CTauri Print-Agent: Long-Poll + lokales Drucken via CUPS/Spooler + Status-Reporting~6h
DSetup-Wizard + QR-Pairing~2h
EFrontend: Drucken-Dropdown + Druck-Modal + Druckverlauf~3h
FQuotas (counting + monatlicher Reset) + Quota-Anzeige im UI~2h
GDirekt-IPP-Variante (push) — fuer Tech-affine Schulen~4h
HAuto-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/jobs mit 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 printed löschen (kein PDF in S3 herumliegen)
  • Audit-Log — jeder Print-Job mit User, Document, Drucker, Seitenzahl in print_jobs Tabelle, 90 Tage retained
  • Job-Cancel — nur queued/printing-Jobs, nach printed ist 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

  1. Workflow-Print — bei Kaskaden-Auto-Print: wer ist requestedBy? System-Bot oder Workflow-Initiator? Empfehlung: Workflow-Initiator, Audit klar.
  2. 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.
  3. 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.
  4. 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.
  5. 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