V1.0
This commit is contained in:
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
.env
|
||||||
|
*.md
|
||||||
|
.DS_Store
|
||||||
25
Dockerfile
Normal file
25
Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
FROM node:18-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# Copy application files
|
||||||
|
COPY server.js ./
|
||||||
|
COPY html/ ./html/
|
||||||
|
COPY css/ ./css/
|
||||||
|
COPY js/ ./js/
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||||
|
CMD node -e "require('http').get('http://localhost:8080', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
CMD ["node", "server.js"]
|
||||||
224
README.md
Normal file
224
README.md
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
# PDF Unterschrift System
|
||||||
|
|
||||||
|
Ein Zwei-Seiten-System für digitale PDF-Unterschriften mit Echtzeit-Synchronisation.
|
||||||
|
|
||||||
|
## 🎯 Funktionen
|
||||||
|
|
||||||
|
- **Master-Seite (PC)**: PDF hochladen und unterschriebene PDF herunterladen
|
||||||
|
- **Signatur-Seite (Tablet/Smartphone)**: Permanent offen, zeigt automatisch jede neue PDF mit Vorschau
|
||||||
|
- **PDF-Vorschau**: Echte PDF-Anzeige auf beiden Seiten mit pdf.js
|
||||||
|
- **Verschiebbare Unterschrift**: Unterschrift per Drag & Drop positionieren
|
||||||
|
- **Skalierbare Unterschrift**: Größe der Unterschrift anpassen
|
||||||
|
- **Automatische Synchronisation**: Keine Session-IDs, keine QR-Codes nötig
|
||||||
|
- **Echtzeit-Übertragung**: WebSocket-basierte direkte Kommunikation
|
||||||
|
- **Mehrfachnutzung**: Signatur-Seite einmal öffnen, beliebig viele PDFs unterschreiben
|
||||||
|
|
||||||
|
## 📋 Voraussetzungen
|
||||||
|
|
||||||
|
- Node.js (Version 14 oder höher)
|
||||||
|
- Ein moderner Webbrowser (Chrome, Firefox, Safari, Edge)
|
||||||
|
|
||||||
|
## 🚀 Installation & Start
|
||||||
|
|
||||||
|
### 1. Dependencies installieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Server starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
oder
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node server.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Server läuft nun auf `http://localhost:8080`
|
||||||
|
|
||||||
|
## 💻 Verwendung
|
||||||
|
|
||||||
|
### Schritt 1: Signatur-Seite öffnen und offen lassen (Tablet/Smartphone)
|
||||||
|
|
||||||
|
1. Öffne im Browser: `http://localhost:8080/signature.html`
|
||||||
|
2. **WICHTIG**: Lasse diese Seite permanent geöffnet!
|
||||||
|
3. Die Seite zeigt "Warte auf PDF..." - das ist normal
|
||||||
|
|
||||||
|
### Schritt 2: Master-Seite öffnen (PC)
|
||||||
|
|
||||||
|
1. Öffne im Browser: `http://localhost:8080/master.html`
|
||||||
|
2. Klicke auf "PDF auswählen" und wähle eine PDF-Datei aus
|
||||||
|
|
||||||
|
### Schritt 3: Automatische Anzeige
|
||||||
|
|
||||||
|
1. Die PDF wird **automatisch** auf der Signatur-Seite angezeigt
|
||||||
|
2. Du siehst eine **echte Vorschau** des PDFs
|
||||||
|
3. Keine QR-Codes, keine Links - alles passiert automatisch!
|
||||||
|
|
||||||
|
### Schritt 4: Unterschreiben
|
||||||
|
|
||||||
|
1. Auf der Signatur-Seite: Zeichne deine Unterschrift
|
||||||
|
2. Klicke auf "Unterschrift senden"
|
||||||
|
3. Die Unterschrift erscheint automatisch auf der Master-Seite
|
||||||
|
|
||||||
|
### Schritt 5: Unterschrift positionieren
|
||||||
|
|
||||||
|
1. Auf der Master-Seite: Die Unterschrift erscheint als **verschiebbare Box** über dem PDF
|
||||||
|
2. **Ziehe** die Unterschrift an die gewünschte Position
|
||||||
|
3. **Skaliere** die Unterschrift mit dem Griff rechts unten
|
||||||
|
4. **Optional**: Entferne die Unterschrift mit dem × falls nötig (zum Neuzeichnen)
|
||||||
|
5. **Klicke auf "Unterschrift platzieren & PDF erstellen"**
|
||||||
|
6. Die Unterschrift wird ins PDF eingefügt und die finale Version wird angezeigt
|
||||||
|
|
||||||
|
### Schritt 6: PDF herunterladen
|
||||||
|
|
||||||
|
- Auf der Master-Seite: Klicke "Unterschriebene PDF herunterladen"
|
||||||
|
- Die PDF wird mit der Unterschrift an der gewählten Position gespeichert
|
||||||
|
|
||||||
|
### Für die nächste PDF:
|
||||||
|
|
||||||
|
- Lade einfach eine neue PDF auf der Master-Seite hoch
|
||||||
|
- Sie wird automatisch auf der (noch offenen) Signatur-Seite angezeigt
|
||||||
|
- Unterschreiben → Positionieren → Platzieren → Download
|
||||||
|
- **Keine neue Session nötig!**
|
||||||
|
|
||||||
|
## 🌐 Netzwerk-Zugriff
|
||||||
|
|
||||||
|
### Für lokale Nutzung:
|
||||||
|
- Alle Geräte müssen im selben Netzwerk sein
|
||||||
|
- Verwende die IP-Adresse deines PCs statt `localhost`
|
||||||
|
|
||||||
|
### IP-Adresse finden:
|
||||||
|
|
||||||
|
**Windows:**
|
||||||
|
```bash
|
||||||
|
ipconfig
|
||||||
|
```
|
||||||
|
(Suche nach "IPv4-Adresse")
|
||||||
|
|
||||||
|
**Mac/Linux:**
|
||||||
|
```bash
|
||||||
|
ifconfig
|
||||||
|
```
|
||||||
|
oder
|
||||||
|
```bash
|
||||||
|
ip addr show
|
||||||
|
```
|
||||||
|
|
||||||
|
Dann verwende: `http://[DEINE-IP]:8080/master.html`
|
||||||
|
|
||||||
|
## 📁 Dateien
|
||||||
|
|
||||||
|
```
|
||||||
|
pdf-signature-system/
|
||||||
|
├── master.html # Master-Seite (PDF hochladen)
|
||||||
|
├── signature.html # Signatur-Seite (unterschreiben)
|
||||||
|
├── server.js # WebSocket Server
|
||||||
|
├── package.json # Node.js Dependencies
|
||||||
|
└── README.md # Diese Datei
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Technische Details
|
||||||
|
|
||||||
|
### Verwendete Technologien:
|
||||||
|
- **Frontend**: HTML5, CSS3, JavaScript
|
||||||
|
- **PDF-Vorschau**: PDF.js (Mozilla)
|
||||||
|
- **PDF-Verarbeitung**: PDF-Lib (Client-seitig)
|
||||||
|
- **Kommunikation**: WebSockets
|
||||||
|
- **Server**: Node.js mit `ws` Package
|
||||||
|
- **Interaktivität**: Drag & Drop, Resize Handles
|
||||||
|
|
||||||
|
### Architektur:
|
||||||
|
1. Signatur-Seite öffnen und permanent offen lassen
|
||||||
|
2. Master-Seite lädt PDF hoch
|
||||||
|
3. WebSocket-Server leitet PDF direkt an Signatur-Seite weiter
|
||||||
|
4. Unterschrift wird über WebSocket zurück an Master gesendet
|
||||||
|
5. Master-Seite fügt Unterschrift ins PDF ein
|
||||||
|
6. Für nächste PDF: Einfach neue PDF hochladen - alles andere bleibt wie es ist!
|
||||||
|
|
||||||
|
## 🛠️ Anpassungen
|
||||||
|
|
||||||
|
### Standard-Position der Unterschrift ändern:
|
||||||
|
|
||||||
|
In `master.html`, Zeile ~20:
|
||||||
|
```javascript
|
||||||
|
let signaturePos = { x: 0.7, y: 0.1 }; // x: 0-1 (links-rechts), y: 0-1 (oben-unten)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Standard-Größe der Unterschrift ändern:
|
||||||
|
|
||||||
|
Zeile ~21:
|
||||||
|
```javascript
|
||||||
|
let signatureScale = 0.3; // 0.3 = 30% der PDF-Breite
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port ändern:
|
||||||
|
|
||||||
|
In `server.js`, Zeile ~158:
|
||||||
|
```javascript
|
||||||
|
const PORT = 8080; // Ändere auf gewünschten Port
|
||||||
|
```
|
||||||
|
|
||||||
|
Dann auch in beiden HTML-Dateien die WebSocket-URL anpassen.
|
||||||
|
|
||||||
|
## ❗ Troubleshooting
|
||||||
|
|
||||||
|
### "WebSocket Verbindung fehlgeschlagen"
|
||||||
|
- Stelle sicher, dass der Server läuft
|
||||||
|
- Prüfe ob Port 8080 verfügbar ist
|
||||||
|
- Bei Firewall: Erlaube Port 8080
|
||||||
|
|
||||||
|
### "Warte auf PDF..." bleibt stehen
|
||||||
|
- Stelle sicher, dass beide Seiten mit dem Server verbunden sind
|
||||||
|
- Prüfe Browser-Konsole auf Fehlermeldungen
|
||||||
|
- Server neustarten und beide Seiten neu laden
|
||||||
|
|
||||||
|
### PDF-Vorschau wird nicht angezeigt
|
||||||
|
- Stelle sicher, dass pdf.js korrekt geladen wurde (siehe Browser-Konsole)
|
||||||
|
- Versuche eine andere PDF-Datei
|
||||||
|
- Browser-Cache leeren und Seite neu laden
|
||||||
|
|
||||||
|
### Unterschrift lässt sich nicht verschieben
|
||||||
|
- Stelle sicher, dass die Unterschrift vollständig geladen ist
|
||||||
|
- Klicke direkt auf die Unterschrift (nicht auf die Griffe)
|
||||||
|
- Browser neu laden
|
||||||
|
|
||||||
|
### Unterschrift wird nicht angezeigt
|
||||||
|
- Prüfe Browser-Konsole auf Fehlermeldungen
|
||||||
|
- Stelle sicher, dass beide Seiten verbunden sind
|
||||||
|
- Server neustarten
|
||||||
|
|
||||||
|
### PDF-Download funktioniert nicht
|
||||||
|
- Warte bis Status "Unterschrift erhalten" angezeigt wird
|
||||||
|
- Prüfe ob PDF-Lib korrekt geladen wurde
|
||||||
|
- Browser-Cache leeren
|
||||||
|
|
||||||
|
## 🔒 Sicherheitshinweise
|
||||||
|
|
||||||
|
⚠️ **Wichtig**: Dieses System ist für den lokalen Gebrauch gedacht!
|
||||||
|
|
||||||
|
Für den Produktiveinsatz beachten:
|
||||||
|
- HTTPS verwenden (WSS statt WS)
|
||||||
|
- Authentifizierung implementieren
|
||||||
|
- Session-Timeouts einrichten
|
||||||
|
- Input-Validierung verstärken
|
||||||
|
- Rate-Limiting hinzufügen
|
||||||
|
|
||||||
|
## 📝 Lizenz
|
||||||
|
|
||||||
|
MIT License - Frei verwendbar für private und kommerzielle Zwecke
|
||||||
|
|
||||||
|
## 🤝 Support
|
||||||
|
|
||||||
|
Bei Fragen oder Problemen:
|
||||||
|
1. Prüfe die Konsole (F12 in Browser)
|
||||||
|
2. Stelle sicher, dass Node.js installiert ist
|
||||||
|
3. Verifiziere Netzwerk-Verbindung zwischen Geräten
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Viel Erfolg mit dem PDF Unterschrift System! ✍️📄
|
||||||
328
css/master.css
Normal file
328
css/master.css
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-section {
|
||||||
|
border: 3px dashed #667eea;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
background: #f8f9ff;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-section:hover {
|
||||||
|
border-color: #764ba2;
|
||||||
|
background: #f0f2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-section input[type="file"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-button {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 15px 40px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-button:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.waiting {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.ready {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.signed {
|
||||||
|
background: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status {
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 8px;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.connected {
|
||||||
|
background: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator.disconnected {
|
||||||
|
background: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px solid #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pageInfo {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.2em;
|
||||||
|
color: #667eea;
|
||||||
|
min-width: 150px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-selector label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 2px solid #667eea;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-selector label:hover {
|
||||||
|
background: #667eea15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-selector input[type="checkbox"] {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-preview {
|
||||||
|
margin-top: 30px;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-preview.scrollable {
|
||||||
|
max-height: 70vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-preview canvas {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-page-separator {
|
||||||
|
text-align: center;
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 8px;
|
||||||
|
background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-overlay {
|
||||||
|
position: absolute;
|
||||||
|
border: 3px dashed #667eea;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 10px;
|
||||||
|
cursor: move;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||||
|
display: none;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-overlay.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-overlay img {
|
||||||
|
display: block;
|
||||||
|
max-width: 300px;
|
||||||
|
height: auto;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-overlay .handle {
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
right: -10px;
|
||||||
|
background: #667eea;
|
||||||
|
color: white;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-overlay .resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 5px;
|
||||||
|
right: 5px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: #667eea;
|
||||||
|
cursor: nwse-resize;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-controls {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-controls.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-controls p {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-button {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
padding: 15px 40px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 20px;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-button:disabled {
|
||||||
|
background: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discard-button {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
padding: 15px 40px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 20px;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.discard-button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
196
css/signature.css
Normal file
196
css/signature.css
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
width: 100%;
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-section {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-pad {
|
||||||
|
border: 3px solid #11998e;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: crosshair;
|
||||||
|
touch-action: none;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 15px 30px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.2s, opacity 0.2s;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-clear {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-submit {
|
||||||
|
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||||
|
color: white;
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
background: #e7f9f5;
|
||||||
|
border-left: 4px solid #11998e;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
display: none;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #11998e;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-display {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-display.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-display h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #11998e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-preview-canvas {
|
||||||
|
max-width: 100%;
|
||||||
|
border: 2px solid #11998e;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
display: block;
|
||||||
|
margin: 10px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-separator {
|
||||||
|
text-align: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 10px;
|
||||||
|
background: linear-gradient(135deg, #11998e15 0%, #38ef7d15 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #11998e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting-message {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.waiting-message .icon {
|
||||||
|
font-size: 3em;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
25
docker-compose.yml
Normal file
25
docker-compose.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
pdf-signature:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: pdf-signature-system
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
networks:
|
||||||
|
- pdf-signature-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "require('http').get('http://localhost:8080', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
start_period: 5s
|
||||||
|
|
||||||
|
networks:
|
||||||
|
pdf-signature-network:
|
||||||
|
driver: bridge
|
||||||
106
html/master.html
Normal file
106
html/master.html
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Master - PDF Unterschrift System</title>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf-lib/1.17.1/pdf-lib.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="../css/master.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>📄 Master Station</h1>
|
||||||
|
<p>PDF hochladen und Unterschrift empfangen</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="connection-status" id="connectionStatus" style="display: none;">
|
||||||
|
<span class="status-indicator disconnected" id="statusIndicator"></span>
|
||||||
|
<span id="connectionText">Warte auf Signatur-Station...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="upload-section" id="uploadSection">
|
||||||
|
<h2>PDF Dokument auswählen</h2>
|
||||||
|
<p style="margin: 20px 0; color: #666;">Wähle eine PDF-Datei aus, die unterschrieben werden soll</p>
|
||||||
|
<input type="file" id="pdfInput" accept=".pdf" disabled>
|
||||||
|
<button class="upload-button" id="pdfSelectButton" onclick="document.getElementById('pdfInput').click()" disabled>
|
||||||
|
PDF auswählen
|
||||||
|
</button>
|
||||||
|
<p id="pdfSelectHint" style="margin-top: 15px; color: #856404; font-style: italic; display: block;">
|
||||||
|
⚠️ Bitte warten Sie, bis die Signatur-Station verbunden ist.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="statusSection" style="display: none;">
|
||||||
|
|
||||||
|
<div class="status waiting" id="statusMessage">
|
||||||
|
Warte auf Unterschrift von der Signatur-Station...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page Navigation -->
|
||||||
|
<div class="page-navigation" id="pageNavigation" style="display: none;">
|
||||||
|
<button class="nav-button" id="prevPageBtn" onclick="changePage(-1)">◀ Zurück</button>
|
||||||
|
<span id="pageInfo">Seite 1 von 1</span>
|
||||||
|
<button class="nav-button" id="nextPageBtn" onclick="changePage(1)">Weiter ▶</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pdf-preview" id="pdfPreview">
|
||||||
|
<div id="pdfCanvasContainer">
|
||||||
|
<canvas id="pdfCanvas"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="signature-overlay" id="signatureOverlay">
|
||||||
|
<div class="handle" onclick="removeSignatureOverlay()" title="Unterschrift entfernen">×</div>
|
||||||
|
<img id="signatureImage" src="" alt="Unterschrift">
|
||||||
|
<div class="resize-handle" id="resizeHandle"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="signature-controls" id="signatureControls">
|
||||||
|
<p><strong>📝 Unterschrift positionieren:</strong> Ziehe die Unterschrift an die gewünschte Position im PDF. Nutze den Griff rechts unten zum Skalieren.</p>
|
||||||
|
|
||||||
|
<div class="page-selector" id="pageSelector" style="display: none; margin: 15px 0; padding: 15px; background: #f0f2ff; border-radius: 8px;">
|
||||||
|
<p style="margin-bottom: 10px;"><strong>Auf welchen Seiten platzieren?</strong></p>
|
||||||
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||||
|
<label style="cursor: pointer;">
|
||||||
|
<input type="checkbox" id="currentPageOnly" checked onchange="updatePageSelection()">
|
||||||
|
Nur aktuelle Seite (<span id="currentPageNum">1</span>)
|
||||||
|
</label>
|
||||||
|
<label style="cursor: pointer;">
|
||||||
|
<input type="checkbox" id="allPages" onchange="updatePageSelection()">
|
||||||
|
Alle Seiten
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 15px; margin-top: 15px;">
|
||||||
|
<button class="upload-button" onclick="removeSignatureOverlay()" style="flex: 1; background: #dc3545;">
|
||||||
|
🗑️ Unterschrift entfernen
|
||||||
|
</button>
|
||||||
|
<button class="upload-button" id="placeSignatureButton" style="flex: 2; background: linear-gradient(135deg, #28a745 0%, #20c997 100%);">
|
||||||
|
✓ Unterschrift platzieren & PDF erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<div style="display: flex; gap: 15px; justify-content: center; flex-wrap: wrap;">
|
||||||
|
<button class="download-button" id="downloadButton" disabled>
|
||||||
|
⬇ Unterschriebene PDF herunterladen
|
||||||
|
</button>
|
||||||
|
<button class="discard-button" id="discardButton">
|
||||||
|
🗑️ Verwerfen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p id="downloadHint" style="margin-top: 10px; color: #666; display: none;">
|
||||||
|
PDF wurde erstellt und ist bereit zum Download
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../js/master.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
73
html/signature.html
Normal file
73
html/signature.html
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Unterschrift - PDF Signatur</title>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="../css/signature.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>✍️ Unterschriften-Station</h1>
|
||||||
|
<p>Bereit für Unterschriften</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div id="loadingSection" class="loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>Verbinde mit Server...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="waitingSection" class="waiting-message" style="display: none;">
|
||||||
|
<div class="icon">📄</div>
|
||||||
|
<h2>Warte auf PDF...</h2>
|
||||||
|
<p>Bereit zum Unterschreiben. Lade ein PDF auf der Master-Station hoch.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="signatureSection" style="display: none;">
|
||||||
|
<div class="pdf-display" id="pdfDisplay">
|
||||||
|
<h3>📄 Zu unterschreibendes Dokument</h3>
|
||||||
|
<div id="pageInfoSignature" style="margin: 10px 0; font-weight: bold; color: #11998e; font-size: 1.1em;">
|
||||||
|
Seite 1 von 1
|
||||||
|
</div>
|
||||||
|
<div id="pdfPagesContainer" style="max-height: 60vh; overflow-y: auto; border: 2px solid #11998e; border-radius: 8px; padding: 10px; background: white;">
|
||||||
|
<!-- Pages will be inserted here dynamically -->
|
||||||
|
</div>
|
||||||
|
<p style="margin-top: 15px; color: #666; font-size: 0.9em;">Scrolle nach unten um alle Seiten zu sehen</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<strong>📝 Anleitung:</strong> Zeichnen Sie Ihre Unterschrift mit dem Finger oder der Maus im Feld unten. Drücken Sie dann auf "Unterschrift senden".
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="signature-section">
|
||||||
|
<h3 style="margin-bottom: 15px;">Unterschriftsfeld:</h3>
|
||||||
|
<canvas id="signatureCanvas" class="signature-pad" width="740" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<button class="button button-clear" id="clearButton">
|
||||||
|
🗑️ Löschen
|
||||||
|
</button>
|
||||||
|
<button class="button button-submit" id="submitButton" disabled>
|
||||||
|
✓ Unterschrift senden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="success-message" id="successMessage">
|
||||||
|
<h2>✅ Erfolgreich!</h2>
|
||||||
|
<p>Ihre Unterschrift wurde erfolgreich übertragen.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="error-message" id="errorMessage">
|
||||||
|
❌ Fehler beim Senden der Unterschrift. Bitte versuchen Sie es erneut.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../js/signature.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
757
js/master.js
Normal file
757
js/master.js
Normal file
@@ -0,0 +1,757 @@
|
|||||||
|
const { PDFDocument, rgb } = PDFLib;
|
||||||
|
|
||||||
|
// Configure pdf.js worker
|
||||||
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
||||||
|
|
||||||
|
let originalPdfBytes = null;
|
||||||
|
let signedPdfBytes = null;
|
||||||
|
let pdfFileName = '';
|
||||||
|
let pdfDoc = null;
|
||||||
|
let currentPage = null;
|
||||||
|
let currentPageNum = 1;
|
||||||
|
let totalPages = 1;
|
||||||
|
let signatureDataUrl = null;
|
||||||
|
|
||||||
|
// Signature position and size (relative to PDF)
|
||||||
|
let signaturePos = { x: 0.5, y: 0.1 }; // Relative position (0-1)
|
||||||
|
let signatureScale = 0.3;
|
||||||
|
|
||||||
|
// WebSocket connection
|
||||||
|
let ws = null;
|
||||||
|
|
||||||
|
function updateConnectionStatus(isConnected) {
|
||||||
|
const statusIndicator = document.getElementById('statusIndicator');
|
||||||
|
const connectionText = document.getElementById('connectionText');
|
||||||
|
const pdfInput = document.getElementById('pdfInput');
|
||||||
|
const pdfSelectButton = document.getElementById('pdfSelectButton');
|
||||||
|
const pdfSelectHint = document.getElementById('pdfSelectHint');
|
||||||
|
|
||||||
|
if (isConnected) {
|
||||||
|
statusIndicator.classList.remove('disconnected');
|
||||||
|
statusIndicator.classList.add('connected');
|
||||||
|
connectionText.textContent = '✓ Verbunden mit Signatur-Station';
|
||||||
|
|
||||||
|
// Enable PDF selection
|
||||||
|
pdfInput.disabled = false;
|
||||||
|
pdfSelectButton.disabled = false;
|
||||||
|
pdfSelectHint.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
statusIndicator.classList.remove('connected');
|
||||||
|
statusIndicator.classList.add('disconnected');
|
||||||
|
connectionText.textContent = '⏳ Warte auf Signatur-Station...';
|
||||||
|
|
||||||
|
// Disable PDF selection
|
||||||
|
pdfInput.disabled = true;
|
||||||
|
pdfSelectButton.disabled = true;
|
||||||
|
pdfSelectHint.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectWebSocket() {
|
||||||
|
// Use current host (works for localhost, IP addresses, and reverse proxy)
|
||||||
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
const wsHost = window.location.hostname;
|
||||||
|
// If using HTTPS (reverse proxy), use same port as page (usually 443, no port needed)
|
||||||
|
// If using HTTP directly, use port 8080
|
||||||
|
const wsPort = window.location.protocol === 'https:' ? '' : ':8080';
|
||||||
|
const wsUrl = `${wsProtocol}//${wsHost}${wsPort}`;
|
||||||
|
console.log('Verbinde WebSocket zu:', wsUrl);
|
||||||
|
ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('WebSocket verbunden');
|
||||||
|
// Show connection status section
|
||||||
|
document.getElementById('connectionStatus').style.display = 'block';
|
||||||
|
// Register as master
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'register_master'
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = async (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (data.type === 'signature') {
|
||||||
|
console.log('Unterschrift empfangen');
|
||||||
|
signatureDataUrl = data.signature;
|
||||||
|
await showSignatureOverlay(data.signature);
|
||||||
|
} else if (data.type === 'signature_station_connected') {
|
||||||
|
console.log('Signatur-Station verbunden');
|
||||||
|
updateConnectionStatus(true);
|
||||||
|
} else if (data.type === 'signature_station_disconnected') {
|
||||||
|
console.log('Signatur-Station getrennt');
|
||||||
|
updateConnectionStatus(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket Fehler:', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log('WebSocket getrennt');
|
||||||
|
setTimeout(connectWebSocket, 1000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('pdfInput').addEventListener('change', async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// Store filename for download
|
||||||
|
pdfFileName = file.name.replace('.pdf', '');
|
||||||
|
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
|
||||||
|
// Store TWO copies - one for rendering, one for pdf-lib
|
||||||
|
originalPdfBytes = new Uint8Array(arrayBuffer);
|
||||||
|
const renderCopy = new Uint8Array(arrayBuffer.slice(0));
|
||||||
|
|
||||||
|
// Create array for sending BEFORE any async operations
|
||||||
|
const pdfArray = Array.from(originalPdfBytes);
|
||||||
|
|
||||||
|
// Show status section
|
||||||
|
document.getElementById('uploadSection').style.display = 'none';
|
||||||
|
document.getElementById('statusSection').style.display = 'block';
|
||||||
|
|
||||||
|
// Render PDF preview using the copy
|
||||||
|
await renderPdfPreview(renderCopy);
|
||||||
|
|
||||||
|
// Send PDF to server using the pre-created array
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'pdf',
|
||||||
|
pdf: pdfArray
|
||||||
|
}));
|
||||||
|
console.log('PDF gesendet, Größe:', pdfArray.length);
|
||||||
|
|
||||||
|
// Send initial page info after a short delay (to ensure signature station received PDF)
|
||||||
|
setTimeout(() => {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN && totalPages > 0) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'page_change',
|
||||||
|
pageNum: 1,
|
||||||
|
totalPages: totalPages
|
||||||
|
}));
|
||||||
|
console.log('Initiale Seiteninformation gesendet: Seite 1 von', totalPages);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function renderPdfPreview(pdfBytes, pageNum = 1) {
|
||||||
|
try {
|
||||||
|
// ALWAYS create a fresh copy to prevent detachment
|
||||||
|
// pdf.js consumes/detaches the buffer during rendering
|
||||||
|
let safeCopy;
|
||||||
|
if (pdfBytes instanceof Uint8Array) {
|
||||||
|
safeCopy = new Uint8Array(pdfBytes.buffer.slice(0));
|
||||||
|
} else {
|
||||||
|
safeCopy = new Uint8Array(pdfBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadingTask = pdfjsLib.getDocument({ data: safeCopy });
|
||||||
|
pdfDoc = await loadingTask.promise;
|
||||||
|
totalPages = pdfDoc.numPages;
|
||||||
|
currentPageNum = pageNum;
|
||||||
|
|
||||||
|
currentPage = await pdfDoc.getPage(pageNum);
|
||||||
|
|
||||||
|
const canvas = document.getElementById('pdfCanvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
const viewport = currentPage.getViewport({ scale: 1.5 });
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
|
||||||
|
const renderContext = {
|
||||||
|
canvasContext: ctx,
|
||||||
|
viewport: viewport
|
||||||
|
};
|
||||||
|
|
||||||
|
await currentPage.render(renderContext).promise;
|
||||||
|
|
||||||
|
// Update page navigation
|
||||||
|
updatePageNavigation();
|
||||||
|
|
||||||
|
console.log('PDF Seite', pageNum, 'erfolgreich gerendert');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Rendern des PDFs:', error);
|
||||||
|
alert('Fehler beim Anzeigen der PDF: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderAllPages(pdfBytes) {
|
||||||
|
try {
|
||||||
|
// Create a fresh copy
|
||||||
|
let safeCopy;
|
||||||
|
if (pdfBytes instanceof Uint8Array) {
|
||||||
|
safeCopy = new Uint8Array(pdfBytes.buffer.slice(0));
|
||||||
|
} else {
|
||||||
|
safeCopy = new Uint8Array(pdfBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Rendere ALLE Seiten, Größe:', safeCopy.length, 'bytes');
|
||||||
|
|
||||||
|
const loadingTask = pdfjsLib.getDocument({ data: safeCopy });
|
||||||
|
const doc = await loadingTask.promise;
|
||||||
|
const numPages = doc.numPages;
|
||||||
|
|
||||||
|
console.log('PDF hat', numPages, 'Seiten');
|
||||||
|
|
||||||
|
// Get container and clear it
|
||||||
|
const container = document.getElementById('pdfCanvasContainer');
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
// Make pdf-preview scrollable
|
||||||
|
const pdfPreview = document.getElementById('pdfPreview');
|
||||||
|
pdfPreview.classList.add('scrollable');
|
||||||
|
|
||||||
|
// Render all pages
|
||||||
|
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
|
||||||
|
|
||||||
|
// Add page separator
|
||||||
|
const separator = document.createElement('div');
|
||||||
|
separator.className = 'pdf-page-separator';
|
||||||
|
separator.textContent = `Seite ${pageNum} von ${numPages}`;
|
||||||
|
container.appendChild(separator);
|
||||||
|
|
||||||
|
// Create canvas for this page
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.className = 'pdf-page-canvas';
|
||||||
|
canvas.id = `pdf-page-${pageNum}`;
|
||||||
|
container.appendChild(canvas);
|
||||||
|
|
||||||
|
// Render page
|
||||||
|
const page = await doc.getPage(pageNum);
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
const viewport = page.getViewport({ scale: 1.5 });
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
|
||||||
|
const renderContext = {
|
||||||
|
canvasContext: ctx,
|
||||||
|
viewport: viewport
|
||||||
|
};
|
||||||
|
|
||||||
|
await page.render(renderContext).promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide page navigation after placing
|
||||||
|
document.getElementById('pageNavigation').style.display = 'none';
|
||||||
|
|
||||||
|
console.log('Alle', numPages, 'Seiten erfolgreich gerendert');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Rendern aller Seiten:', error);
|
||||||
|
alert('Fehler beim Anzeigen der PDF: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePageNavigation() {
|
||||||
|
const pageNav = document.getElementById('pageNavigation');
|
||||||
|
const pageInfo = document.getElementById('pageInfo');
|
||||||
|
const prevBtn = document.getElementById('prevPageBtn');
|
||||||
|
const nextBtn = document.getElementById('nextPageBtn');
|
||||||
|
const pageSelector = document.getElementById('pageSelector');
|
||||||
|
const currentPageNumSpan = document.getElementById('currentPageNum');
|
||||||
|
|
||||||
|
if (totalPages > 1) {
|
||||||
|
pageNav.style.display = 'flex';
|
||||||
|
pageInfo.textContent = `Seite ${currentPageNum} von ${totalPages}`;
|
||||||
|
prevBtn.disabled = currentPageNum <= 1;
|
||||||
|
nextBtn.disabled = currentPageNum >= totalPages;
|
||||||
|
|
||||||
|
// Show page selector if signature is visible
|
||||||
|
if (document.getElementById('signatureOverlay').classList.contains('show')) {
|
||||||
|
pageSelector.style.display = 'block';
|
||||||
|
currentPageNumSpan.textContent = currentPageNum;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pageNav.style.display = 'none';
|
||||||
|
pageSelector.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changePage(direction) {
|
||||||
|
const newPage = currentPageNum + direction;
|
||||||
|
if (newPage < 1 || newPage > totalPages) return;
|
||||||
|
|
||||||
|
await renderPdfPreview(originalPdfBytes, newPage);
|
||||||
|
|
||||||
|
// Send page change to signature station
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'page_change',
|
||||||
|
pageNum: newPage,
|
||||||
|
totalPages: totalPages
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePageSelection() {
|
||||||
|
const currentOnly = document.getElementById('currentPageOnly');
|
||||||
|
const allPages = document.getElementById('allPages');
|
||||||
|
|
||||||
|
// Toggle logic
|
||||||
|
if (currentOnly.checked && allPages.checked) {
|
||||||
|
if (event.target === currentOnly) {
|
||||||
|
allPages.checked = false;
|
||||||
|
} else {
|
||||||
|
currentOnly.checked = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure at least one is checked
|
||||||
|
if (!currentOnly.checked && !allPages.checked) {
|
||||||
|
currentOnly.checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showSignatureOverlay(signatureDataUrl) {
|
||||||
|
const overlay = document.getElementById('signatureOverlay');
|
||||||
|
const img = document.getElementById('signatureImage');
|
||||||
|
|
||||||
|
img.src = signatureDataUrl;
|
||||||
|
overlay.classList.add('show');
|
||||||
|
|
||||||
|
document.getElementById('signatureControls').classList.add('show');
|
||||||
|
|
||||||
|
// Show page selector if multi-page
|
||||||
|
if (totalPages > 1) {
|
||||||
|
document.getElementById('pageSelector').style.display = 'block';
|
||||||
|
document.getElementById('currentPageNum').textContent = currentPageNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('statusMessage').className = 'status signed';
|
||||||
|
document.getElementById('statusMessage').textContent = '✅ Unterschrift erhalten! Wähle die Seite(n) und positioniere sie.';
|
||||||
|
document.getElementById('downloadButton').disabled = true;
|
||||||
|
document.getElementById('downloadHint').style.display = 'none';
|
||||||
|
|
||||||
|
// Position signature initially (use clientWidth for displayed size)
|
||||||
|
const canvas = document.getElementById('pdfCanvas');
|
||||||
|
overlay.style.left = (canvas.clientWidth * signaturePos.x) + 'px';
|
||||||
|
overlay.style.top = (canvas.clientHeight * signaturePos.y) + 'px';
|
||||||
|
|
||||||
|
// Make signature draggable
|
||||||
|
makeDraggable(overlay);
|
||||||
|
makeResizable(overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDraggable(element) {
|
||||||
|
let isDragging = false;
|
||||||
|
let startX, startY, initialX, initialY;
|
||||||
|
|
||||||
|
element.addEventListener('mousedown', (e) => {
|
||||||
|
if (e.target.classList.contains('resize-handle') || e.target.classList.contains('handle')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isDragging = true;
|
||||||
|
startX = e.clientX;
|
||||||
|
startY = e.clientY;
|
||||||
|
initialX = element.offsetLeft;
|
||||||
|
initialY = element.offsetTop;
|
||||||
|
element.style.cursor = 'grabbing';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', (e) => {
|
||||||
|
if (!isDragging) return;
|
||||||
|
|
||||||
|
const dx = e.clientX - startX;
|
||||||
|
const dy = e.clientY - startY;
|
||||||
|
|
||||||
|
element.style.left = (initialX + dx) + 'px';
|
||||||
|
element.style.top = (initialY + dy) + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', () => {
|
||||||
|
if (isDragging) {
|
||||||
|
isDragging = false;
|
||||||
|
element.style.cursor = 'move';
|
||||||
|
updateSignaturePosition();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeResizable(element) {
|
||||||
|
const resizeHandle = document.getElementById('resizeHandle');
|
||||||
|
let isResizing = false;
|
||||||
|
let startX, startWidth;
|
||||||
|
|
||||||
|
resizeHandle.addEventListener('mousedown', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
isResizing = true;
|
||||||
|
startX = e.clientX;
|
||||||
|
startWidth = element.querySelector('img').width;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', (e) => {
|
||||||
|
if (!isResizing) return;
|
||||||
|
|
||||||
|
const dx = e.clientX - startX;
|
||||||
|
const newWidth = Math.max(100, startWidth + dx);
|
||||||
|
element.querySelector('img').style.maxWidth = newWidth + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseup', () => {
|
||||||
|
if (isResizing) {
|
||||||
|
isResizing = false;
|
||||||
|
updateSignatureScale();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSignaturePosition() {
|
||||||
|
const overlay = document.getElementById('signatureOverlay');
|
||||||
|
const canvas = document.getElementById('pdfCanvas');
|
||||||
|
|
||||||
|
signaturePos.x = overlay.offsetLeft / canvas.width;
|
||||||
|
signaturePos.y = overlay.offsetTop / canvas.height;
|
||||||
|
console.log('Neue Position:', signaturePos);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSignatureScale() {
|
||||||
|
const img = document.getElementById('signatureImage');
|
||||||
|
const canvas = document.getElementById('pdfCanvas');
|
||||||
|
|
||||||
|
signatureScale = img.width / canvas.width;
|
||||||
|
console.log('Neue Skalierung:', signatureScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addSignatureToPdf() {
|
||||||
|
try {
|
||||||
|
if (!originalPdfBytes || originalPdfBytes.length === 0) {
|
||||||
|
console.error('originalPdfBytes ist nicht verfügbar');
|
||||||
|
alert('Fehler: PDF-Daten nicht verfügbar. Bitte laden Sie die PDF erneut hoch.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine which pages to add signature to
|
||||||
|
const currentOnly = document.getElementById('currentPageOnly').checked;
|
||||||
|
const allPagesChecked = document.getElementById('allPages').checked;
|
||||||
|
|
||||||
|
let pagesToSign = [];
|
||||||
|
if (allPagesChecked) {
|
||||||
|
// All pages
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
pagesToSign.push(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Current page only
|
||||||
|
pagesToSign.push(currentPageNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('=== FÜGE UNTERSCHRIFTEN HINZU ===');
|
||||||
|
console.log('Seiten zum Signieren:', pagesToSign.join(', '));
|
||||||
|
|
||||||
|
// Load PDF with pdf-lib
|
||||||
|
// Use ignoreEncryption to handle encrypted PDFs
|
||||||
|
const pdfLibDoc = await PDFDocument.load(originalPdfBytes, { ignoreEncryption: true });
|
||||||
|
const pages = pdfLibDoc.getPages();
|
||||||
|
|
||||||
|
// Get signature position and size from overlay
|
||||||
|
const overlay = document.getElementById('signatureOverlay');
|
||||||
|
const overlayLeft = overlay.offsetLeft;
|
||||||
|
const overlayTop = overlay.offsetTop;
|
||||||
|
const overlayWidth = overlay.querySelector('img').width;
|
||||||
|
|
||||||
|
// Embed signature image once
|
||||||
|
const signatureImage = await pdfLibDoc.embedPng(signatureDataUrl);
|
||||||
|
|
||||||
|
// Add signature to each selected page
|
||||||
|
for (const pageNum of pagesToSign) {
|
||||||
|
const page = pages[pageNum - 1]; // 0-indexed
|
||||||
|
const { width, height } = page.getSize();
|
||||||
|
|
||||||
|
console.log('--- Bearbeite Seite', pageNum, '---');
|
||||||
|
console.log('Seite Dimensionen:', width, 'x', height);
|
||||||
|
|
||||||
|
// Get canvas display size
|
||||||
|
const canvas = document.getElementById('pdfCanvas');
|
||||||
|
const canvasDisplayWidth = canvas.clientWidth;
|
||||||
|
const canvasDisplayHeight = canvas.clientHeight;
|
||||||
|
|
||||||
|
const scaleX = width / canvasDisplayWidth;
|
||||||
|
const scaleY = height / canvasDisplayHeight;
|
||||||
|
|
||||||
|
console.log('Scale Faktoren - X:', scaleX, 'Y:', scaleY);
|
||||||
|
console.log('Overlay Position:', overlayLeft, overlayTop);
|
||||||
|
console.log('Overlay Breite:', overlayWidth);
|
||||||
|
|
||||||
|
// Calculate signature dimensions in PDF coordinates
|
||||||
|
const sigWidth = overlayWidth * scaleX;
|
||||||
|
const sigHeight = sigWidth * (signatureImage.height / signatureImage.width);
|
||||||
|
|
||||||
|
// Convert position to PDF coordinates (bottom-left origin)
|
||||||
|
const x = overlayLeft * scaleX;
|
||||||
|
const y = height - (overlayTop * scaleY) - sigHeight;
|
||||||
|
|
||||||
|
console.log('PDF Position:', x, y, 'Größe:', sigWidth, 'x', sigHeight);
|
||||||
|
|
||||||
|
// Add signature to page
|
||||||
|
page.drawImage(signatureImage, {
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
width: sigWidth,
|
||||||
|
height: sigHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✓ Unterschrift zu Seite', pageNum, 'hinzugefügt');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save PDF
|
||||||
|
console.log('Speichere PDF...');
|
||||||
|
const pdfBytes = await pdfLibDoc.save();
|
||||||
|
console.log('PDF gespeichert, Typ:', pdfBytes.constructor.name, 'Größe:', pdfBytes.length);
|
||||||
|
|
||||||
|
if (!pdfBytes || pdfBytes.length === 0) {
|
||||||
|
console.error('FEHLER: pdfDoc.save() hat leeres Array zurückgegeben!');
|
||||||
|
alert('Fehler beim Speichern der PDF. Bitte versuchen Sie es erneut.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store as Uint8Array
|
||||||
|
signedPdfBytes = new Uint8Array(pdfBytes);
|
||||||
|
console.log('signedPdfBytes gespeichert, Typ:', signedPdfBytes.constructor.name, 'Größe:', signedPdfBytes.length);
|
||||||
|
console.log('=== UNTERSCHRIFTEN ERFOLGREICH HINZUGEFÜGT ===');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Hinzufügen der Unterschrift:', error);
|
||||||
|
console.error('Stack:', error.stack);
|
||||||
|
alert('Fehler beim Verarbeiten der Unterschrift: ' + error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSignatureOverlay() {
|
||||||
|
// Only remove the overlay, keep signedPdfBytes intact
|
||||||
|
document.getElementById('signatureOverlay').classList.remove('show');
|
||||||
|
document.getElementById('signatureControls').classList.remove('show');
|
||||||
|
signatureDataUrl = null;
|
||||||
|
console.log('removeSignatureOverlay() aufgerufen - signedPdfBytes:', signedPdfBytes ? signedPdfBytes.length + ' bytes' : 'NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSignature() {
|
||||||
|
// Complete reset including signedPdfBytes
|
||||||
|
document.getElementById('signatureOverlay').classList.remove('show');
|
||||||
|
document.getElementById('signatureControls').classList.remove('show');
|
||||||
|
document.getElementById('statusMessage').className = 'status waiting';
|
||||||
|
document.getElementById('statusMessage').textContent = 'Warte auf Unterschrift von der Signatur-Station...';
|
||||||
|
document.getElementById('downloadButton').disabled = true;
|
||||||
|
document.getElementById('downloadHint').style.display = 'none';
|
||||||
|
signatureDataUrl = null;
|
||||||
|
console.log('removeSignature() aufgerufen - SETZE signedPdfBytes auf NULL');
|
||||||
|
signedPdfBytes = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place signature button
|
||||||
|
document.getElementById('placeSignatureButton').addEventListener('click', async () => {
|
||||||
|
if (!signatureDataUrl) {
|
||||||
|
console.error('Keine signatureDataUrl vorhanden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('=== PLATZIERE UNTERSCHRIFT ===');
|
||||||
|
console.log('signatureDataUrl vorhanden:', signatureDataUrl ? 'Ja' : 'Nein');
|
||||||
|
console.log('originalPdfBytes vorhanden:', originalPdfBytes ? 'Ja (' + originalPdfBytes.length + ' bytes)' : 'Nein');
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
const btn = document.getElementById('placeSignatureButton');
|
||||||
|
const originalText = btn.textContent;
|
||||||
|
btn.textContent = '⏳ PDF wird erstellt...';
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
// Add signature to PDF
|
||||||
|
const success = await addSignatureToPdf();
|
||||||
|
|
||||||
|
console.log('addSignatureToPdf Ergebnis:', success);
|
||||||
|
console.log('signedPdfBytes nach Bearbeitung:', signedPdfBytes ? signedPdfBytes.length + ' bytes' : 'NULL');
|
||||||
|
|
||||||
|
if (success && signedPdfBytes) {
|
||||||
|
// Hide signature overlay (but keep signedPdfBytes!)
|
||||||
|
removeSignatureOverlay();
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
document.getElementById('statusMessage').className = 'status ready';
|
||||||
|
document.getElementById('statusMessage').textContent = '✅ PDF wurde erstellt! Bereit zum Download.';
|
||||||
|
|
||||||
|
// Enable download
|
||||||
|
document.getElementById('downloadButton').disabled = false;
|
||||||
|
document.getElementById('downloadHint').style.display = 'block';
|
||||||
|
|
||||||
|
console.log('Download-Button Status:', document.getElementById('downloadButton').disabled ? 'DEAKTIVIERT' : 'AKTIVIERT');
|
||||||
|
console.log('signedPdfBytes für Download:', signedPdfBytes ? signedPdfBytes.length + ' bytes' : 'NULL');
|
||||||
|
|
||||||
|
// Send confirmation to signature station that signature was placed
|
||||||
|
console.log('=== SENDE PLATZIERUNGS-BESTÄTIGUNG ===');
|
||||||
|
console.log('WebSocket vorhanden:', ws ? 'Ja' : 'Nein');
|
||||||
|
console.log('WebSocket readyState:', ws ? ws.readyState : 'N/A');
|
||||||
|
console.log('WebSocket OPEN:', ws && ws.readyState === WebSocket.OPEN ? 'Ja' : 'Nein');
|
||||||
|
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
try {
|
||||||
|
const message = JSON.stringify({
|
||||||
|
type: 'signature_placed'
|
||||||
|
});
|
||||||
|
ws.send(message);
|
||||||
|
console.log('✅ Platzierungs-Bestätigung an Server gesendet');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Fehler beim Senden der Bestätigung:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('❌ WebSocket nicht verfügbar oder nicht geöffnet - Bestätigung kann nicht gesendet werden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the FINAL PDF with signature - show ALL pages
|
||||||
|
console.log('Rendere finale PDF mit Unterschrift - ALLE Seiten...');
|
||||||
|
const renderCopy = new Uint8Array(signedPdfBytes);
|
||||||
|
await renderAllPages(renderCopy);
|
||||||
|
console.log('Finale PDF angezeigt - alle Seiten');
|
||||||
|
console.log('signedPdfBytes nach Rendering:', signedPdfBytes ? signedPdfBytes.length + ' bytes' : 'NULL');
|
||||||
|
console.log('=== PLATZIERUNG ERFOLGREICH ===');
|
||||||
|
} else {
|
||||||
|
// Reset button on error
|
||||||
|
console.error('Platzierung fehlgeschlagen! success:', success, 'signedPdfBytes:', signedPdfBytes ? 'vorhanden' : 'NULL');
|
||||||
|
btn.textContent = originalText;
|
||||||
|
btn.disabled = false;
|
||||||
|
alert('Fehler beim Platzieren der Unterschrift. Bitte versuchen Sie es erneut.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('downloadButton').addEventListener('click', async () => {
|
||||||
|
console.log('=== DOWNLOAD BUTTON GEKLICKT ===');
|
||||||
|
console.log('signedPdfBytes:', signedPdfBytes ? signedPdfBytes.length + ' bytes' : 'NULL/UNDEFINED');
|
||||||
|
console.log('signedPdfBytes type:', typeof signedPdfBytes);
|
||||||
|
|
||||||
|
if (!signedPdfBytes || signedPdfBytes.length === 0) {
|
||||||
|
console.error('FEHLER: signedPdfBytes ist nicht verfügbar!');
|
||||||
|
alert('Fehler: Kein signiertes PDF verfügbar. Bitte platzieren Sie die Unterschrift zuerst.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('Starte Download, PDF Größe:', signedPdfBytes.length, 'bytes');
|
||||||
|
|
||||||
|
// Ensure signedPdfBytes is Uint8Array
|
||||||
|
const pdfData = signedPdfBytes instanceof Uint8Array ? signedPdfBytes : new Uint8Array(signedPdfBytes);
|
||||||
|
console.log('PDF Daten konvertiert, Typ:', pdfData.constructor.name, 'Größe:', pdfData.length);
|
||||||
|
|
||||||
|
// Download with _signed suffix
|
||||||
|
const blob = new Blob([pdfData], { type: 'application/pdf' });
|
||||||
|
console.log('Blob erstellt, Größe:', blob.size);
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
console.log('Blob URL erstellt:', url);
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = pdfFileName + '_signed.pdf';
|
||||||
|
console.log('Download Dateiname:', a.download);
|
||||||
|
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
console.log('PDF heruntergeladen als:', pdfFileName + '_signed.pdf');
|
||||||
|
console.log('=== DOWNLOAD ERFOLGREICH ===');
|
||||||
|
|
||||||
|
// Reset for next PDF
|
||||||
|
document.getElementById('uploadSection').style.display = 'block';
|
||||||
|
document.getElementById('statusSection').style.display = 'none';
|
||||||
|
|
||||||
|
// Reset file input
|
||||||
|
document.getElementById('pdfInput').value = '';
|
||||||
|
|
||||||
|
// Reset place signature button
|
||||||
|
const placeBtn = document.getElementById('placeSignatureButton');
|
||||||
|
placeBtn.textContent = '✓ Unterschrift platzieren & PDF erstellen';
|
||||||
|
placeBtn.disabled = false;
|
||||||
|
|
||||||
|
// Reset page navigation and scrollable state
|
||||||
|
currentPageNum = 1;
|
||||||
|
totalPages = 1;
|
||||||
|
document.getElementById('pageNavigation').style.display = 'none';
|
||||||
|
document.getElementById('pageSelector').style.display = 'none';
|
||||||
|
document.getElementById('pdfPreview').classList.remove('scrollable');
|
||||||
|
|
||||||
|
// Reset canvas container to single canvas
|
||||||
|
const container = document.getElementById('pdfCanvasContainer');
|
||||||
|
container.innerHTML = '<canvas id="pdfCanvas"></canvas>';
|
||||||
|
|
||||||
|
removeSignature();
|
||||||
|
originalPdfBytes = null;
|
||||||
|
signedPdfBytes = null;
|
||||||
|
pdfFileName = '';
|
||||||
|
pdfDoc = null;
|
||||||
|
currentPage = null;
|
||||||
|
|
||||||
|
console.log('Alles zurückgesetzt für nächste PDF');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Download:', error);
|
||||||
|
alert('Fehler beim Herunterladen: ' + error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetToStart() {
|
||||||
|
console.log('=== ZURÜCK ZUM ANFANG ===');
|
||||||
|
|
||||||
|
// Show upload section, hide status section
|
||||||
|
document.getElementById('uploadSection').style.display = 'block';
|
||||||
|
document.getElementById('statusSection').style.display = 'none';
|
||||||
|
|
||||||
|
// Reset file input
|
||||||
|
document.getElementById('pdfInput').value = '';
|
||||||
|
|
||||||
|
// Reset place signature button
|
||||||
|
const placeBtn = document.getElementById('placeSignatureButton');
|
||||||
|
placeBtn.textContent = '✓ Unterschrift platzieren & PDF erstellen';
|
||||||
|
placeBtn.disabled = false;
|
||||||
|
|
||||||
|
// Reset download button
|
||||||
|
document.getElementById('downloadButton').disabled = true;
|
||||||
|
document.getElementById('downloadHint').style.display = 'none';
|
||||||
|
|
||||||
|
// Reset page navigation and scrollable state
|
||||||
|
currentPageNum = 1;
|
||||||
|
totalPages = 1;
|
||||||
|
document.getElementById('pageNavigation').style.display = 'none';
|
||||||
|
document.getElementById('pageSelector').style.display = 'none';
|
||||||
|
document.getElementById('pdfPreview').classList.remove('scrollable');
|
||||||
|
|
||||||
|
// Reset canvas container to single canvas
|
||||||
|
const container = document.getElementById('pdfCanvasContainer');
|
||||||
|
container.innerHTML = '<canvas id="pdfCanvas"></canvas>';
|
||||||
|
|
||||||
|
// Reset status message
|
||||||
|
document.getElementById('statusMessage').className = 'status waiting';
|
||||||
|
document.getElementById('statusMessage').textContent = 'Warte auf Unterschrift von der Signatur-Station...';
|
||||||
|
|
||||||
|
// Clear all data
|
||||||
|
removeSignature();
|
||||||
|
originalPdfBytes = null;
|
||||||
|
signedPdfBytes = null;
|
||||||
|
pdfFileName = '';
|
||||||
|
pdfDoc = null;
|
||||||
|
currentPage = null;
|
||||||
|
signatureDataUrl = null;
|
||||||
|
|
||||||
|
console.log('Zurück zum Anfang - bereit für neue PDF');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discard button event listener
|
||||||
|
document.getElementById('discardButton').addEventListener('click', () => {
|
||||||
|
if (confirm('Möchten Sie wirklich verwerfen? Alle Änderungen gehen verloren.')) {
|
||||||
|
// Notify signature station to reset
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'discard'
|
||||||
|
}));
|
||||||
|
console.log('Verwerfen-Nachricht an Signatur-Station gesendet');
|
||||||
|
}
|
||||||
|
resetToStart();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect WebSocket on load
|
||||||
|
connectWebSocket();
|
||||||
300
js/signature.js
Normal file
300
js/signature.js
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
// Configure pdf.js worker
|
||||||
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
||||||
|
|
||||||
|
const canvas = document.getElementById('signatureCanvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
let isDrawing = false;
|
||||||
|
let hasSignature = false;
|
||||||
|
let ws = null;
|
||||||
|
let currentPdf = null;
|
||||||
|
let pdfDoc = null;
|
||||||
|
let currentPageNum = 1;
|
||||||
|
let totalPages = 1;
|
||||||
|
|
||||||
|
connectWebSocket();
|
||||||
|
|
||||||
|
function connectWebSocket() {
|
||||||
|
// Use current host (works for localhost, IP addresses, and reverse proxy)
|
||||||
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
const wsHost = window.location.hostname;
|
||||||
|
// If using HTTPS (reverse proxy), use same port as page (usually 443, no port needed)
|
||||||
|
// If using HTTP directly, use port 8080
|
||||||
|
const wsPort = window.location.protocol === 'https:' ? '' : ':8080';
|
||||||
|
const wsUrl = `${wsProtocol}//${wsHost}${wsPort}`;
|
||||||
|
console.log('Verbinde WebSocket zu:', wsUrl);
|
||||||
|
ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('WebSocket verbunden');
|
||||||
|
document.getElementById('loadingSection').style.display = 'none';
|
||||||
|
document.getElementById('waitingSection').style.display = 'block';
|
||||||
|
|
||||||
|
// Register as signature station
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'register_signature'
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = async (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (data.type === 'pdf') {
|
||||||
|
console.log('PDF empfangen');
|
||||||
|
currentPdf = new Uint8Array(data.pdf);
|
||||||
|
|
||||||
|
// Show PDF notification
|
||||||
|
document.getElementById('waitingSection').style.display = 'none';
|
||||||
|
document.getElementById('signatureSection').style.display = 'block';
|
||||||
|
document.getElementById('pdfDisplay').classList.add('show');
|
||||||
|
|
||||||
|
// Render ALL pages of PDF
|
||||||
|
await renderPdfPreview(currentPdf);
|
||||||
|
|
||||||
|
// Clear previous signature if any
|
||||||
|
clearSignature();
|
||||||
|
} else if (data.type === 'page_change') {
|
||||||
|
currentPageNum = data.pageNum;
|
||||||
|
totalPages = data.totalPages;
|
||||||
|
|
||||||
|
// Scroll to the specific page
|
||||||
|
const pageCanvas = document.getElementById(`page-canvas-${data.pageNum}`);
|
||||||
|
if (pageCanvas) {
|
||||||
|
pageCanvas.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
} else if (data.type === 'signature_placed') {
|
||||||
|
console.log('✅ Unterschrift wurde platziert! Reset in 5 Sekunden...');
|
||||||
|
console.log('Empfangene Daten:', JSON.stringify(data));
|
||||||
|
|
||||||
|
// Show immediate feedback
|
||||||
|
const successMsg = document.getElementById('successMessage');
|
||||||
|
successMsg.classList.add('show');
|
||||||
|
|
||||||
|
// Reset after 5 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('Auto-Reset wird durchgeführt...');
|
||||||
|
|
||||||
|
// Clear signature
|
||||||
|
clearSignature();
|
||||||
|
|
||||||
|
// Hide success message
|
||||||
|
document.getElementById('successMessage').classList.remove('show');
|
||||||
|
|
||||||
|
// Hide PDF and signature section, show waiting
|
||||||
|
document.getElementById('signatureSection').style.display = 'none';
|
||||||
|
document.getElementById('pdfDisplay').classList.remove('show');
|
||||||
|
document.getElementById('waitingSection').style.display = 'block';
|
||||||
|
|
||||||
|
// Reset PDF data
|
||||||
|
currentPdf = null;
|
||||||
|
pdfDoc = null;
|
||||||
|
currentPageNum = 1;
|
||||||
|
totalPages = 1;
|
||||||
|
|
||||||
|
console.log('Bereit für nächste Unterschrift');
|
||||||
|
}, 5000);
|
||||||
|
} else if (data.type === 'discard') {
|
||||||
|
console.log('🔄 Verwerfen-Nachricht empfangen, setze zurück...');
|
||||||
|
resetSignatureStation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket Fehler:', error);
|
||||||
|
document.getElementById('loadingSection').innerHTML =
|
||||||
|
'<p style="color: #dc3545;">❌ Verbindung zum Server fehlgeschlagen.</p>';
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log('WebSocket getrennt');
|
||||||
|
setTimeout(connectWebSocket, 1000);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderPdfPreview(pdfBytes, pageNum = null) {
|
||||||
|
try {
|
||||||
|
// Create fresh copy to prevent detachment
|
||||||
|
const safeCopy = new Uint8Array(pdfBytes.buffer.slice(0));
|
||||||
|
|
||||||
|
console.log('renderPdfPreview aufgerufen mit', pdfBytes.length, 'bytes');
|
||||||
|
const loadingTask = pdfjsLib.getDocument({ data: safeCopy });
|
||||||
|
pdfDoc = await loadingTask.promise;
|
||||||
|
totalPages = pdfDoc.numPages;
|
||||||
|
|
||||||
|
// Update page info
|
||||||
|
document.getElementById('pageInfoSignature').textContent = `${totalPages} Seite${totalPages > 1 ? 'n' : ''}`;
|
||||||
|
|
||||||
|
// Get container
|
||||||
|
const container = document.getElementById('pdfPagesContainer');
|
||||||
|
container.innerHTML = ''; // Clear existing content
|
||||||
|
|
||||||
|
// Render all pages
|
||||||
|
for (let pageNum = 1; pageNum <= totalPages; pageNum++) {
|
||||||
|
|
||||||
|
// Add page separator if not first page
|
||||||
|
if (pageNum > 1) {
|
||||||
|
const separator = document.createElement('div');
|
||||||
|
separator.className = 'page-separator';
|
||||||
|
separator.textContent = `Seite ${pageNum}`;
|
||||||
|
container.appendChild(separator);
|
||||||
|
} else {
|
||||||
|
// First page indicator
|
||||||
|
const separator = document.createElement('div');
|
||||||
|
separator.className = 'page-separator';
|
||||||
|
separator.textContent = `Seite 1`;
|
||||||
|
container.appendChild(separator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create canvas for this page
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.className = 'pdf-preview-canvas';
|
||||||
|
canvas.id = `page-canvas-${pageNum}`;
|
||||||
|
container.appendChild(canvas);
|
||||||
|
|
||||||
|
// Render page
|
||||||
|
const page = await pdfDoc.getPage(pageNum);
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
const viewport = page.getViewport({ scale: 1.2 });
|
||||||
|
canvas.width = viewport.width;
|
||||||
|
canvas.height = viewport.height;
|
||||||
|
|
||||||
|
const renderContext = {
|
||||||
|
canvasContext: ctx,
|
||||||
|
viewport: viewport
|
||||||
|
};
|
||||||
|
|
||||||
|
await page.render(renderContext).promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Alle', totalPages, 'Seiten erfolgreich gerendert');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Rendern der PDF Vorschau:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Canvas setup
|
||||||
|
ctx.strokeStyle = '#000';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.lineCap = 'round';
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
|
||||||
|
function startDrawing(e) {
|
||||||
|
isDrawing = true;
|
||||||
|
const pos = getPosition(e);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(pos.x, pos.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw(e) {
|
||||||
|
if (!isDrawing) return;
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const pos = getPosition(e);
|
||||||
|
ctx.lineTo(pos.x, pos.y);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
hasSignature = true;
|
||||||
|
document.getElementById('submitButton').disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDrawing() {
|
||||||
|
isDrawing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPosition(e) {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const scaleX = canvas.width / rect.width;
|
||||||
|
const scaleY = canvas.height / rect.height;
|
||||||
|
|
||||||
|
if (e.touches && e.touches.length > 0) {
|
||||||
|
return {
|
||||||
|
x: (e.touches[0].clientX - rect.left) * scaleX,
|
||||||
|
y: (e.touches[0].clientY - rect.top) * scaleY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: (e.clientX - rect.left) * scaleX,
|
||||||
|
y: (e.clientY - rect.top) * scaleY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse events
|
||||||
|
canvas.addEventListener('mousedown', startDrawing);
|
||||||
|
canvas.addEventListener('mousemove', draw);
|
||||||
|
canvas.addEventListener('mouseup', stopDrawing);
|
||||||
|
canvas.addEventListener('mouseout', stopDrawing);
|
||||||
|
|
||||||
|
// Touch events
|
||||||
|
canvas.addEventListener('touchstart', startDrawing);
|
||||||
|
canvas.addEventListener('touchmove', draw);
|
||||||
|
canvas.addEventListener('touchend', stopDrawing);
|
||||||
|
|
||||||
|
// Clear button
|
||||||
|
document.getElementById('clearButton').addEventListener('click', () => {
|
||||||
|
clearSignature();
|
||||||
|
});
|
||||||
|
|
||||||
|
function clearSignature() {
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
hasSignature = false;
|
||||||
|
document.getElementById('submitButton').disabled = true;
|
||||||
|
document.getElementById('successMessage').classList.remove('show');
|
||||||
|
document.getElementById('errorMessage').classList.remove('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSignatureStation() {
|
||||||
|
console.log('=== SIGNATUR-STATION ZURÜCKSETZEN ===');
|
||||||
|
|
||||||
|
// Clear signature canvas
|
||||||
|
clearSignature();
|
||||||
|
|
||||||
|
// Hide success and error messages
|
||||||
|
document.getElementById('successMessage').classList.remove('show');
|
||||||
|
document.getElementById('errorMessage').classList.remove('show');
|
||||||
|
|
||||||
|
// Hide PDF and signature section, show waiting
|
||||||
|
document.getElementById('signatureSection').style.display = 'none';
|
||||||
|
document.getElementById('pdfDisplay').classList.remove('show');
|
||||||
|
document.getElementById('waitingSection').style.display = 'block';
|
||||||
|
|
||||||
|
// Clear PDF pages container
|
||||||
|
const container = document.getElementById('pdfPagesContainer');
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset PDF data
|
||||||
|
currentPdf = null;
|
||||||
|
pdfDoc = null;
|
||||||
|
currentPageNum = 1;
|
||||||
|
totalPages = 1;
|
||||||
|
|
||||||
|
console.log('Signatur-Station zurückgesetzt - bereit für neue PDF');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit button
|
||||||
|
document.getElementById('submitButton').addEventListener('click', async () => {
|
||||||
|
if (!hasSignature || !ws || ws.readyState !== WebSocket.OPEN) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get signature as data URL
|
||||||
|
const signatureDataUrl = canvas.toDataURL('image/png');
|
||||||
|
|
||||||
|
// Send to server (no session ID needed)
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'signature',
|
||||||
|
signature: signatureDataUrl
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
document.getElementById('successMessage').classList.add('show');
|
||||||
|
document.getElementById('submitButton').disabled = true;
|
||||||
|
|
||||||
|
console.log('Unterschrift gesendet, warte auf Platzierungs-Bestätigung...');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Senden:', error);
|
||||||
|
document.getElementById('errorMessage').classList.add('show');
|
||||||
|
}
|
||||||
|
});
|
||||||
97
master.html
Normal file
97
master.html
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Master - PDF Unterschrift System</title>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf-lib/1.17.1/pdf-lib.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="master.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>📄 Master Station</h1>
|
||||||
|
<p>PDF hochladen und Unterschrift empfangen</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="upload-section" id="uploadSection">
|
||||||
|
<h2>PDF Dokument auswählen</h2>
|
||||||
|
<p style="margin: 20px 0; color: #666;">Wähle eine PDF-Datei aus, die unterschrieben werden soll</p>
|
||||||
|
<input type="file" id="pdfInput" accept=".pdf">
|
||||||
|
<button class="upload-button" onclick="document.getElementById('pdfInput').click()">
|
||||||
|
PDF auswählen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="statusSection" style="display: none;">
|
||||||
|
<div class="connection-status">
|
||||||
|
<span class="status-indicator connected" id="statusIndicator"></span>
|
||||||
|
<span id="connectionText">Verbunden mit Signatur-Station</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status waiting" id="statusMessage">
|
||||||
|
Warte auf Unterschrift von der Signatur-Station...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page Navigation -->
|
||||||
|
<div class="page-navigation" id="pageNavigation" style="display: none;">
|
||||||
|
<button class="nav-button" id="prevPageBtn" onclick="changePage(-1)">◀ Zurück</button>
|
||||||
|
<span id="pageInfo">Seite 1 von 1</span>
|
||||||
|
<button class="nav-button" id="nextPageBtn" onclick="changePage(1)">Weiter ▶</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pdf-preview" id="pdfPreview">
|
||||||
|
<div id="pdfCanvasContainer">
|
||||||
|
<canvas id="pdfCanvas"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="signature-overlay" id="signatureOverlay">
|
||||||
|
<div class="handle" onclick="removeSignatureOverlay()" title="Unterschrift entfernen">×</div>
|
||||||
|
<img id="signatureImage" src="" alt="Unterschrift">
|
||||||
|
<div class="resize-handle" id="resizeHandle"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="signature-controls" id="signatureControls">
|
||||||
|
<p><strong>📝 Unterschrift positionieren:</strong> Ziehe die Unterschrift an die gewünschte Position im PDF. Nutze den Griff rechts unten zum Skalieren.</p>
|
||||||
|
|
||||||
|
<div class="page-selector" id="pageSelector" style="display: none; margin: 15px 0; padding: 15px; background: #f0f2ff; border-radius: 8px;">
|
||||||
|
<p style="margin-bottom: 10px;"><strong>Auf welchen Seiten platzieren?</strong></p>
|
||||||
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||||
|
<label style="cursor: pointer;">
|
||||||
|
<input type="checkbox" id="currentPageOnly" checked onchange="updatePageSelection()">
|
||||||
|
Nur aktuelle Seite (<span id="currentPageNum">1</span>)
|
||||||
|
</label>
|
||||||
|
<label style="cursor: pointer;">
|
||||||
|
<input type="checkbox" id="allPages" onchange="updatePageSelection()">
|
||||||
|
Alle Seiten
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 15px; margin-top: 15px;">
|
||||||
|
<button class="upload-button" onclick="removeSignatureOverlay()" style="flex: 1; background: #dc3545;">
|
||||||
|
🗑️ Unterschrift entfernen
|
||||||
|
</button>
|
||||||
|
<button class="upload-button" id="placeSignatureButton" style="flex: 2; background: linear-gradient(135deg, #28a745 0%, #20c997 100%);">
|
||||||
|
✓ Unterschrift platzieren & PDF erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<button class="download-button" id="downloadButton" disabled>
|
||||||
|
⬇ Unterschriebene PDF herunterladen
|
||||||
|
</button>
|
||||||
|
<p id="downloadHint" style="margin-top: 10px; color: #666; display: none;">
|
||||||
|
PDF wurde erstellt und ist bereit zum Download
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="master.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
43
package-lock.json
generated
Normal file
43
package-lock.json
generated
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"name": "pdf-signature-system",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "pdf-signature-system",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^8.14.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ws": {
|
||||||
|
"version": "8.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
|
"requires": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
package.json
Normal file
12
package.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "pdf-signature-system",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Zwei-Seiten PDF Unterschrift System",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ws": "^8.14.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
361
server.js
Normal file
361
server.js
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
const WebSocket = require('ws');
|
||||||
|
const http = require('http');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Create HTTP server
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
// CORS headers
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||||
|
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serve files
|
||||||
|
let urlPath = req.url;
|
||||||
|
|
||||||
|
// Remove query string if present
|
||||||
|
urlPath = urlPath.split('?')[0];
|
||||||
|
|
||||||
|
// Handle root path
|
||||||
|
if (urlPath === '/' || urlPath === '/index.html') {
|
||||||
|
urlPath = '/html/signature.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map URL paths to file system paths based on file type
|
||||||
|
let cleanPath = urlPath.startsWith('/') ? urlPath.substring(1) : urlPath;
|
||||||
|
|
||||||
|
// Route requests to appropriate directories
|
||||||
|
// Only add directory prefix if path doesn't already have it
|
||||||
|
if (cleanPath.endsWith('.html')) {
|
||||||
|
// HTML files from html/ directory
|
||||||
|
const fileName = path.basename(cleanPath);
|
||||||
|
if (!cleanPath.startsWith('html/')) {
|
||||||
|
cleanPath = 'html/' + fileName;
|
||||||
|
}
|
||||||
|
} else if (cleanPath.endsWith('.css')) {
|
||||||
|
// CSS files from css/ directory
|
||||||
|
const fileName = path.basename(cleanPath);
|
||||||
|
if (!cleanPath.startsWith('css/')) {
|
||||||
|
cleanPath = 'css/' + fileName;
|
||||||
|
}
|
||||||
|
} else if (cleanPath.endsWith('.js')) {
|
||||||
|
// JS files from js/ directory
|
||||||
|
const fileName = path.basename(cleanPath);
|
||||||
|
if (!cleanPath.startsWith('js/')) {
|
||||||
|
cleanPath = 'js/' + fileName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security: Prevent directory traversal attacks
|
||||||
|
if (cleanPath.includes('..')) {
|
||||||
|
res.writeHead(403, { 'Content-Type': 'text/html' });
|
||||||
|
res.end('<h1>403 - Zugriff verweigert</h1>', 'utf-8');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to filesystem path
|
||||||
|
const filePath = path.join(process.cwd(), cleanPath);
|
||||||
|
|
||||||
|
// Security: Ensure resolved path is within current directory
|
||||||
|
const currentDir = process.cwd();
|
||||||
|
const resolvedPath = path.resolve(filePath);
|
||||||
|
|
||||||
|
if (!resolvedPath.startsWith(currentDir)) {
|
||||||
|
res.writeHead(403, { 'Content-Type': 'text/html' });
|
||||||
|
res.end('<h1>403 - Zugriff verweigert</h1>', 'utf-8');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extname = String(path.extname(filePath)).toLowerCase();
|
||||||
|
const mimeTypes = {
|
||||||
|
'.html': 'text/html; charset=utf-8',
|
||||||
|
'.js': 'text/javascript; charset=utf-8',
|
||||||
|
'.css': 'text/css; charset=utf-8',
|
||||||
|
'.json': 'application/json',
|
||||||
|
'.png': 'image/png',
|
||||||
|
'.jpg': 'image/jpeg',
|
||||||
|
'.jpeg': 'image/jpeg',
|
||||||
|
'.gif': 'image/gif',
|
||||||
|
'.svg': 'image/svg+xml',
|
||||||
|
'.pdf': 'application/pdf',
|
||||||
|
'.ico': 'image/x-icon'
|
||||||
|
};
|
||||||
|
|
||||||
|
const contentType = mimeTypes[extname] || 'application/octet-stream';
|
||||||
|
|
||||||
|
// Log file requests for debugging
|
||||||
|
console.log(`📄 Request: ${req.url} -> ${filePath} (${contentType})`);
|
||||||
|
|
||||||
|
fs.readFile(filePath, (error, content) => {
|
||||||
|
if (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
console.log(`❌ 404: Datei nicht gefunden: ${filePath}`);
|
||||||
|
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||||
|
res.end(`<h1>404 - Datei nicht gefunden</h1><p>Gesuchte Datei: ${filePath}</p>`, 'utf-8');
|
||||||
|
} else {
|
||||||
|
console.error(`❌ Server Fehler bei ${filePath}:`, error);
|
||||||
|
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||||
|
res.end(`<h1>500 - Server Fehler</h1><p>${error.code}</p>`, 'utf-8');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.writeHead(200, { 'Content-Type': contentType });
|
||||||
|
res.end(content, 'utf-8');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create WebSocket server
|
||||||
|
const wss = new WebSocket.Server({ server });
|
||||||
|
|
||||||
|
// Store connections
|
||||||
|
let masterConnection = null;
|
||||||
|
let signatureConnection = null;
|
||||||
|
|
||||||
|
wss.on('connection', (ws) => {
|
||||||
|
console.log('Neue WebSocket Verbindung');
|
||||||
|
|
||||||
|
ws.on('message', (message) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(message);
|
||||||
|
console.log('📨 Nachricht empfangen:', data.type);
|
||||||
|
console.log(' Von:', ws === masterConnection ? 'Master' : ws === signatureConnection ? 'Signatur-Station' : 'Unbekannt');
|
||||||
|
|
||||||
|
switch (data.type) {
|
||||||
|
case 'register_master':
|
||||||
|
handleMasterRegister(ws);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'register_signature':
|
||||||
|
handleSignatureRegister(ws);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'pdf':
|
||||||
|
handlePdfUpload(data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'signature':
|
||||||
|
handleSignature(data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'page_change':
|
||||||
|
handlePageChange(data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'signature_placed':
|
||||||
|
console.log('🔄 signature_placed Nachricht empfangen, leite weiter...');
|
||||||
|
handleSignaturePlaced();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'discard':
|
||||||
|
console.log('🔄 discard Nachricht empfangen, leite weiter...');
|
||||||
|
handleDiscard();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log('⚠️ Unbekannter Nachrichtentyp:', data.type);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Fehler beim Verarbeiten der Nachricht:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('close', () => {
|
||||||
|
console.log('WebSocket Verbindung geschlossen');
|
||||||
|
if (ws === masterConnection) {
|
||||||
|
masterConnection = null;
|
||||||
|
console.log('Master getrennt');
|
||||||
|
}
|
||||||
|
if (ws === signatureConnection) {
|
||||||
|
signatureConnection = null;
|
||||||
|
console.log('Signatur-Station getrennt');
|
||||||
|
|
||||||
|
// Notify master that signature station disconnected
|
||||||
|
if (masterConnection && masterConnection.readyState === WebSocket.OPEN) {
|
||||||
|
masterConnection.send(JSON.stringify({
|
||||||
|
type: 'signature_station_disconnected'
|
||||||
|
}));
|
||||||
|
console.log('Master über Signatur-Station Trennung informiert');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleMasterRegister(ws) {
|
||||||
|
masterConnection = ws;
|
||||||
|
console.log('Master registriert');
|
||||||
|
|
||||||
|
// Notify master about current signature station connection status
|
||||||
|
if (signatureConnection && signatureConnection.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'signature_station_connected'
|
||||||
|
}));
|
||||||
|
console.log('Master über Signatur-Station Verbindung informiert');
|
||||||
|
} else {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'signature_station_disconnected'
|
||||||
|
}));
|
||||||
|
console.log('Master über fehlende Signatur-Station informiert');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSignatureRegister(ws) {
|
||||||
|
signatureConnection = ws;
|
||||||
|
console.log('Signatur-Station registriert');
|
||||||
|
|
||||||
|
// Notify master that signature station is now connected
|
||||||
|
if (masterConnection && masterConnection.readyState === WebSocket.OPEN) {
|
||||||
|
masterConnection.send(JSON.stringify({
|
||||||
|
type: 'signature_station_connected'
|
||||||
|
}));
|
||||||
|
console.log('Master über Signatur-Station Verbindung informiert');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePdfUpload(data) {
|
||||||
|
const { pdf } = data;
|
||||||
|
|
||||||
|
if (!signatureConnection || signatureConnection.readyState !== WebSocket.OPEN) {
|
||||||
|
console.log('Keine Signatur-Station verbunden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward PDF to signature station
|
||||||
|
signatureConnection.send(JSON.stringify({
|
||||||
|
type: 'pdf',
|
||||||
|
pdf: pdf
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('PDF an Signatur-Station weitergeleitet');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSignature(data) {
|
||||||
|
const { signature } = data;
|
||||||
|
|
||||||
|
if (!masterConnection || masterConnection.readyState !== WebSocket.OPEN) {
|
||||||
|
console.log('Keine Master-Verbindung');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send signature to master
|
||||||
|
masterConnection.send(JSON.stringify({
|
||||||
|
type: 'signature',
|
||||||
|
signature: signature
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('Unterschrift an Master gesendet');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageChange(data) {
|
||||||
|
const { pageNum, totalPages } = data;
|
||||||
|
|
||||||
|
if (!signatureConnection || signatureConnection.readyState !== WebSocket.OPEN) {
|
||||||
|
console.log('Keine Signatur-Station verbunden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward page change to signature station
|
||||||
|
signatureConnection.send(JSON.stringify({
|
||||||
|
type: 'page_change',
|
||||||
|
pageNum: pageNum,
|
||||||
|
totalPages: totalPages
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('Seitenwechsel an Signatur-Station weitergeleitet: Seite', pageNum, 'von', totalPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSignaturePlaced() {
|
||||||
|
console.log('handleSignaturePlaced() aufgerufen');
|
||||||
|
console.log('signatureConnection vorhanden:', signatureConnection ? 'Ja' : 'Nein');
|
||||||
|
console.log('signatureConnection.readyState:', signatureConnection ? signatureConnection.readyState : 'N/A');
|
||||||
|
|
||||||
|
if (!signatureConnection || signatureConnection.readyState !== WebSocket.OPEN) {
|
||||||
|
console.log('⚠️ Keine Signatur-Station verbunden - Bestätigung kann nicht gesendet werden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Forward signature placed confirmation to signature station
|
||||||
|
const message = JSON.stringify({
|
||||||
|
type: 'signature_placed'
|
||||||
|
});
|
||||||
|
signatureConnection.send(message);
|
||||||
|
console.log('✅ Unterschrift-Platzierung-Bestätigung an Signatur-Station gesendet');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Fehler beim Senden der Bestätigung:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDiscard() {
|
||||||
|
console.log('handleDiscard() aufgerufen');
|
||||||
|
console.log('signatureConnection vorhanden:', signatureConnection ? 'Ja' : 'Nein');
|
||||||
|
console.log('signatureConnection.readyState:', signatureConnection ? signatureConnection.readyState : 'N/A');
|
||||||
|
|
||||||
|
if (!signatureConnection || signatureConnection.readyState !== WebSocket.OPEN) {
|
||||||
|
console.log('⚠️ Keine Signatur-Station verbunden - Verwerfen-Nachricht kann nicht gesendet werden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Forward discard message to signature station
|
||||||
|
const message = JSON.stringify({
|
||||||
|
type: 'discard'
|
||||||
|
});
|
||||||
|
signatureConnection.send(message);
|
||||||
|
console.log('✅ Verwerfen-Nachricht an Signatur-Station gesendet');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Fehler beim Senden der Verwerfen-Nachricht:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PORT = 8080;
|
||||||
|
const HOST = '0.0.0.0'; // Listen on all network interfaces
|
||||||
|
|
||||||
|
server.listen(PORT, HOST, () => {
|
||||||
|
const os = require('os');
|
||||||
|
const networkInterfaces = os.networkInterfaces();
|
||||||
|
let ipAddresses = [];
|
||||||
|
|
||||||
|
// Get all IPv4 addresses
|
||||||
|
Object.keys(networkInterfaces).forEach((interfaceName) => {
|
||||||
|
networkInterfaces[interfaceName].forEach((iface) => {
|
||||||
|
if (iface.family === 'IPv4' && !iface.internal) {
|
||||||
|
ipAddresses.push(iface.address);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`
|
||||||
|
╔════════════════════════════════════════════════════════════╗
|
||||||
|
║ PDF Unterschrift System gestartet! ║
|
||||||
|
╠════════════════════════════════════════════════════════════╝
|
||||||
|
║
|
||||||
|
║ Lokaler Zugriff:
|
||||||
|
║ Master-Seite: http://localhost:${PORT}/master.html
|
||||||
|
║ Signatur-Seite: http://localhost:${PORT}/signature.html
|
||||||
|
║ `);
|
||||||
|
|
||||||
|
if (ipAddresses.length > 0) {
|
||||||
|
console.log(`║
|
||||||
|
║ Netzwerk-Zugriff (für iPhone/Tablet): `);
|
||||||
|
ipAddresses.forEach(ip => {
|
||||||
|
console.log(`║ Master-Seite: http://${ip}:${PORT}/master.html `);
|
||||||
|
console.log(`║ Signatur-Seite: http://${ip}:${PORT}/signature.html `);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`║
|
||||||
|
║ WICHTIG: Öffne die Signatur-Seite einmal und lasse
|
||||||
|
║ sie offen. Jede neue PDF wird dort automatisch
|
||||||
|
║ angezeigt!
|
||||||
|
║
|
||||||
|
║ WebSocket Server läuft auf Port ${PORT}
|
||||||
|
║
|
||||||
|
╚═════════════════════════════════════════════════════════════
|
||||||
|
`);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user