diff --git a/TODO.md b/TODO.md
index 6067535..b80866f 100644
--- a/TODO.md
+++ b/TODO.md
@@ -47,7 +47,7 @@
- [x] Verzeichnislayout auf dem Player festlegen
- [x] `player-agent` fachlich zuschneiden
- [x] `player-ui` fachlich zuschneiden (lokale Kiosk-Seite mit Splash + Sysinfo-Overlay)
-- [ ] Watchdog-Konzept fuer Browser und Agent definieren
+- [x] Watchdog-Konzept fuer Browser und Agent definieren
- [x] Offline-Overlay-Verhalten spezifizieren
- [x] Fehlerbehandlung fuer Web-Inhalte und Timeouts ausarbeiten
- [x] Display-Steuerung fuer An/Aus, Rotation und Neustart planen
@@ -62,12 +62,12 @@
- [x] Storage-Konzept fuer Uploads, Cache-Dateien und Screenshots festlegen
- [x] Authentifizierungskonzept festlegen
- [x] Mandantentrennung im Datenmodell und in den APIs absichern
-- [ ] Logging- und Monitoring-Konzept definieren
-- [ ] Template-Editor fuer globale Kampagnen fachlich schneiden
-- [ ] Aktivierungsoberflaeche fuer saisonale oder temporäre Kampagnen planen
-- [ ] Gruppierung oder Slot-Modell fuer monitoruebergreifende Layouts planen
+- [x] Logging- und Monitoring-Konzept definieren
+- [x] Template-Editor fuer globale Kampagnen fachlich schneiden
+- [x] Aktivierungsoberflaeche fuer saisonale oder temporäre Kampagnen planen
+- [x] Gruppierung oder Slot-Modell fuer monitoruebergreifende Layouts planen
- [x] Provisionierungs-UI fuer neue Screens fachlich und technisch schneiden
-- [ ] Jobrunner-Konzept fuer Ansible-gestuetzte Erstinstallation planen
+- [x] Jobrunner-Konzept fuer Ansible-gestuetzte Erstinstallation planen
## Phase 5 - Prototyping
@@ -89,18 +89,18 @@
- [x] Docker-Compose-Setup fuer den Server anlegen
- [x] systemd-Units fuer den Player erstellen
- [x] Chromium-Kiosk-Startskript erstellen
-- [ ] Screenshot-Erzeugung auf dem Player integrieren
+- [x] Screenshot-Erzeugung auf dem Player integrieren
- [x] Heartbeat- und Statusmeldungen integrieren
- [x] MQTT-Playlist-Change-Synchronisation mit Backend-Debounce (2s) und Agent-Debounce (3s) implementiert
- [ ] Fehler- und Wiederanlaufverhalten verifizieren
## Phase 7 - Ansible-Automatisierung
-- [ ] Rolle `signage_base` erstellen
+- [x] Rolle `signage_base` erstellen
- [x] Rolle `signage_player` erstellen
- [x] Rolle `signage_display` erstellen
-- [ ] Rolle `signage_server` erstellen
-- [ ] Rolle `signage_provision` erstellen
+- [x] Rolle `signage_server` erstellen
+- [x] Rolle `signage_provision` erstellen
- [x] Inventar-/Variablenmodell fuer mehrere Monitore entwerfen
- [x] Screen-spezifische Variablen wie `screen_id`, Rotation und Aufloesung abbilden
- [x] Erstinstallation eines neuen Players automatisieren
@@ -145,7 +145,7 @@
- [x] Screen-Online/Offline-Status in Admin-Tabelle anzeigen (aus /status-Endpoint befuellen)
- [x] Playlist-Tabelle in overflow-x Wrapper einwickeln (Responsive auf kleinen Screens)
- [x] PDF-Darstellung: Sidebar und Toolbar im Chromium PDF-Viewer ausblenden (URL-Parameter navpanes=0, toolbar=0)
-- [ ] PDF-Darstellung: PDF.js fuer automatisches Seitendurchblaettern integrieren
+- [x] PDF-Darstellung: PDF.js fuer automatisches Seitendurchblaettern integrieren
### Mittlere Prioritaet
@@ -171,6 +171,44 @@
- [x] Fix: /api/startup-token setzt Cache-Control: no-store Header (Server + Client)
- [x] Fix: TestAssetsServed Nil-Dereferenz durch tote Goroutine behoben
+## Security & Code-Review (Opus, 2026-03-23)
+
+### Kritisch — Sicherheitslücken
+
+- [x] **K2** Tenant-Isolation für `/manage/{screenSlug}/*`: `requireScreenAccess()` in allen manage-Handlern
+- [x] **K3** `DELETE /api/v1/media/{id}`: Tenant-Check via reqcontext.UserFromContext
+- [x] **K4** JSON-API Playlist-Routen (`/items`, `/playlists/*/items`, `/order`, `/duration`): `requirePlaylistAccess()` + `GetByItemID()` im Store
+- [x] **K1** CSRF-Schutz: Double-Submit-Cookie-Pattern (`httpapi/csrf.go`); JS-Injection in alle Templates; Middleware in Router
+- [x] **K6** `POST /api/v1/screens/register`: Pre-Shared-Secret via `MORZ_INFOBOARD_REGISTER_SECRET` (Header `X-Register-Secret`); Player-Agent sendet Secret mit
+- [x] **K5** Admin-Passwort aus Log entfernt — nur `[gesetzt]` wird geloggt
+
+### Wichtig — Robustheit
+
+- [x] **N5** Directory-Listing auf `/uploads/` deaktiviert via `neuteredFileSystem` (`httpapi/uploads.go`)
+- [x] **N6** Uploads nach Tenant getrennt: `fileutil.SaveUploadedFile()` legt Dateien in `uploads/{tenantSlug}/` ab
+- [x] **W1** Race Condition bei `order_index` behoben: atomare Subquery in `AddItem()`
+- [x] **W2** Graceful Shutdown implementiert: `http.Server.Shutdown()` mit 15s Timeout auf SIGTERM/SIGINT
+- [x] **W3** Upload mit `http.MaxBytesReader` begrenzt (512 MB) in allen drei Upload-Handlern
+- [x] **W4** `err.Error()` nicht mehr an den Client — generische Fehlermeldungen, Details serverseitig
+- [x] **W7** Template-Execution-Errors: `bytes.Buffer`-Rendering, erst bei Erfolg an Client senden (`renderTemplate()`)
+
+### Verbesserung — Wartbarkeit
+
+- [ ] **V3** Keine Tests für Auth, Middleware, Tenant-Handler (gesamter Phase-1-5-Code ohne Abdeckung)
+- [x] **V1** Upload-Logik konsolidiert in `internal/fileutil/fileutil.go` (`SaveUploadedFile`)
+- [x] **V5** Cookie-Name als Konstante `reqcontext.SessionCookieName` — manage/auth.go und middleware.go nutzen sie
+- [x] **V6** Strukturiertes Logging: `log/slog` mit JSON-Handler in `main.go`; `app.go` nutzt `slog.Info/slog.Error`
+- [x] **V7** DB-Pool wird im Graceful-Shutdown-Handler geschlossen (`a.dbPool.Close()`)
+
+### Nice-to-have — Features
+
+- [x] **N1** Rate-Limiting auf `/login`: In-Memory Sliding-Window (5 Versuche/Minute pro IP) via `httpapi/ratelimit.go`
+- [ ] **N2** Passwort-Änderung / Self-Service-Reset
+- [ ] **N3** Tenant-User-Management im Admin-UI
+- [ ] **N4** Session-TTL via Config-Variable steuerbar (aktuell hardcoded 8h)
+
+**Hinweis K6:** `MORZ_INFOBOARD_REGISTER_SECRET` muss in `server/.env` / `docker-compose.yml` und in der Player-Config (`MORZ_INFOBOARD_REGISTER_SECRET` oder `register_secret` in `config.json`) identisch gesetzt werden. Wenn die Variable leer ist, bleibt der Endpoint offen (Rückwärtskompatibilität).
+
## Querschnittsthemen
- [ ] Datensicherung fuer Datenbank und Medien einplanen
diff --git a/ansible/inventory.yml b/ansible/inventory.yml
index d0d2cee..36cbd38 100644
--- a/ansible/inventory.yml
+++ b/ansible/inventory.yml
@@ -5,3 +5,8 @@ all:
hosts:
info10:
info01-dev:
+ signage_servers:
+ hosts:
+ dockerbox:
+ # ansible_host: 10.0.0.70
+ # ansible_user: admin
diff --git a/ansible/roles/signage_base/defaults/main.yml b/ansible/roles/signage_base/defaults/main.yml
new file mode 100644
index 0000000..6bd7fd6
--- /dev/null
+++ b/ansible/roles/signage_base/defaults/main.yml
@@ -0,0 +1,12 @@
+---
+signage_user: morz
+signage_timezone: "Europe/Berlin"
+
+signage_base_packages:
+ - curl
+ - ca-certificates
+ - rsync
+ - htop
+ - vim-tiny
+ - bash-completion
+ - ntp
diff --git a/ansible/roles/signage_base/handlers/main.yml b/ansible/roles/signage_base/handlers/main.yml
new file mode 100644
index 0000000..2af4d77
--- /dev/null
+++ b/ansible/roles/signage_base/handlers/main.yml
@@ -0,0 +1,12 @@
+---
+- name: Restart cron
+ ansible.builtin.systemd:
+ name: cron
+ state: restarted
+ become: true
+
+- name: Restart journald
+ ansible.builtin.systemd:
+ name: systemd-journald
+ state: restarted
+ become: true
diff --git a/ansible/roles/signage_base/tasks/main.yml b/ansible/roles/signage_base/tasks/main.yml
new file mode 100644
index 0000000..c5af3a9
--- /dev/null
+++ b/ansible/roles/signage_base/tasks/main.yml
@@ -0,0 +1,55 @@
+---
+- name: Update apt cache and upgrade installed packages
+ ansible.builtin.apt:
+ update_cache: true
+ upgrade: dist
+ cache_valid_time: 3600
+ become: true
+
+- name: Install base packages
+ ansible.builtin.apt:
+ name: "{{ signage_base_packages }}"
+ state: present
+ become: true
+
+- name: Set system timezone
+ community.general.timezone:
+ name: "{{ signage_timezone }}"
+ become: true
+ notify: Restart cron
+
+- name: Ensure NTP service is enabled and running
+ ansible.builtin.systemd:
+ name: ntp
+ enabled: true
+ state: started
+ become: true
+
+- name: Ensure journald drop-in directory exists
+ ansible.builtin.file:
+ path: /etc/systemd/journald.conf.d
+ state: directory
+ owner: root
+ group: root
+ mode: "0755"
+ become: true
+
+- name: Configure journald volatile storage (RAM only, schont SD-Karte)
+ ansible.builtin.copy:
+ dest: /etc/systemd/journald.conf.d/morz-volatile.conf
+ content: |
+ [Journal]
+ Storage=volatile
+ RuntimeMaxUse=20M
+ owner: root
+ group: root
+ mode: "0644"
+ become: true
+ notify: Restart journald
+
+- name: Ensure signage user exists
+ ansible.builtin.user:
+ name: "{{ signage_user }}"
+ create_home: true
+ state: present
+ become: true
diff --git a/ansible/roles/signage_provision/defaults/main.yml b/ansible/roles/signage_provision/defaults/main.yml
new file mode 100644
index 0000000..e3ea820
--- /dev/null
+++ b/ansible/roles/signage_provision/defaults/main.yml
@@ -0,0 +1,16 @@
+---
+# Admin token used to authenticate against the server API
+# Must be overridden in group_vars, host_vars or vault.
+signage_admin_token: ""
+
+# Server base URL reachable from the Ansible controller
+signage_server_base_url: "http://10.0.0.70:8080"
+
+# SSH public key to deploy to the signage user
+signage_ssh_public_key: ""
+
+# User that Ansible should permanently manage (after bootstrapping)
+signage_user: morz
+
+# Config dir on the target (shared with signage_player role)
+signage_config_dir: /etc/signage
diff --git a/ansible/roles/signage_provision/handlers/main.yml b/ansible/roles/signage_provision/handlers/main.yml
new file mode 100644
index 0000000..346cd18
--- /dev/null
+++ b/ansible/roles/signage_provision/handlers/main.yml
@@ -0,0 +1,3 @@
+---
+# No handlers required for provisioning role.
+# Handlers are intentionally empty – provisioning tasks are one-shot.
diff --git a/ansible/roles/signage_provision/tasks/main.yml b/ansible/roles/signage_provision/tasks/main.yml
new file mode 100644
index 0000000..934f940
--- /dev/null
+++ b/ansible/roles/signage_provision/tasks/main.yml
@@ -0,0 +1,57 @@
+---
+- name: Ensure signage user exists
+ ansible.builtin.user:
+ name: "{{ signage_user }}"
+ create_home: true
+ state: present
+ become: true
+
+- name: Ensure .ssh directory exists for signage user
+ ansible.builtin.file:
+ path: "/home/{{ signage_user }}/.ssh"
+ state: directory
+ owner: "{{ signage_user }}"
+ group: "{{ signage_user }}"
+ mode: "0700"
+ become: true
+
+- name: Deploy SSH public key for signage user
+ ansible.builtin.authorized_key:
+ user: "{{ signage_user }}"
+ key: "{{ signage_ssh_public_key }}"
+ state: present
+ become: true
+ when: signage_ssh_public_key | length > 0
+
+- name: Ensure config directory exists
+ ansible.builtin.file:
+ path: "{{ signage_config_dir }}"
+ state: directory
+ owner: root
+ group: root
+ mode: "0755"
+ become: true
+
+- name: Deploy vars.yml template for player config
+ ansible.builtin.template:
+ src: vars.yml.j2
+ dest: "{{ signage_config_dir }}/vars.yml"
+ owner: root
+ group: "{{ signage_user }}"
+ mode: "0640"
+ become: true
+
+- name: Register screen at server via API
+ ansible.builtin.uri:
+ url: "{{ signage_server_base_url }}/api/v1/screens/register"
+ method: POST
+ body_format: json
+ body:
+ slug: "{{ screen_id }}"
+ name: "{{ screen_name | default(screen_id) }}"
+ orientation: "{{ screen_orientation | default('landscape') }}"
+ headers:
+ Content-Type: application/json
+ status_code: [200, 201]
+ delegate_to: localhost
+ when: screen_id is defined
diff --git a/ansible/roles/signage_provision/templates/vars.yml.j2 b/ansible/roles/signage_provision/templates/vars.yml.j2
new file mode 100644
index 0000000..cc4b12e
--- /dev/null
+++ b/ansible/roles/signage_provision/templates/vars.yml.j2
@@ -0,0 +1,16 @@
+# Managed by Ansible – signage_provision role
+# Do not edit manually on the device.
+
+screen_id: "{{ screen_id }}"
+screen_name: "{{ screen_name | default(screen_id) }}"
+screen_orientation: "{{ screen_orientation | default('landscape') }}"
+
+morz_server_base_url: "{{ morz_server_base_url | default(signage_server_base_url) }}"
+morz_mqtt_broker: "{{ morz_mqtt_broker | default('') }}"
+morz_mqtt_username: "{{ morz_mqtt_username | default('') }}"
+morz_mqtt_password: "{{ morz_mqtt_password | default('') }}"
+
+morz_heartbeat_every_seconds: {{ morz_heartbeat_every_seconds | default(30) }}
+morz_status_report_every_seconds: {{ morz_status_report_every_seconds | default(60) }}
+morz_player_listen_addr: "{{ morz_player_listen_addr | default('127.0.0.1:8090') }}"
+morz_player_content_url: "{{ morz_player_content_url | default('') }}"
diff --git a/ansible/roles/signage_server/defaults/main.yml b/ansible/roles/signage_server/defaults/main.yml
new file mode 100644
index 0000000..3f9ec0c
--- /dev/null
+++ b/ansible/roles/signage_server/defaults/main.yml
@@ -0,0 +1,26 @@
+---
+signage_server_deploy_dir: /srv/docker/info-board-neu
+signage_server_data_dir: /srv/docker/info-board-neu/data
+
+# Backend
+morz_http_addr: ":8080"
+morz_database_url: "postgres://morz_infoboard:morz_infoboard@db:5432/morz_infoboard?sslmode=disable"
+morz_upload_dir: /app/uploads
+morz_status_store_path: /app/data/status
+morz_default_tenant: morz
+morz_dev_mode: "false"
+
+# Admin password – must be overridden in group_vars or vault
+morz_admin_password: ""
+
+# MQTT
+morz_mqtt_broker: ""
+morz_mqtt_username: ""
+morz_mqtt_password: ""
+
+# Firewall
+signage_server_ufw_enabled: true
+signage_server_ufw_allow_https: true
+signage_server_ufw_allow_mqtt: true
+signage_server_mqtt_port: "1883"
+signage_server_https_port: "443"
diff --git a/ansible/roles/signage_server/handlers/main.yml b/ansible/roles/signage_server/handlers/main.yml
new file mode 100644
index 0000000..edb6d45
--- /dev/null
+++ b/ansible/roles/signage_server/handlers/main.yml
@@ -0,0 +1,7 @@
+---
+- name: Restart morz-server stack
+ community.docker.docker_compose_v2:
+ project_src: "{{ signage_server_deploy_dir }}"
+ state: present
+ pull: always
+ become: true
diff --git a/ansible/roles/signage_server/tasks/main.yml b/ansible/roles/signage_server/tasks/main.yml
new file mode 100644
index 0000000..9266a8f
--- /dev/null
+++ b/ansible/roles/signage_server/tasks/main.yml
@@ -0,0 +1,130 @@
+---
+- name: Install Docker dependencies
+ ansible.builtin.apt:
+ name:
+ - ca-certificates
+ - curl
+ - gnupg
+ state: present
+ update_cache: true
+ become: true
+
+- name: Create Docker apt keyring directory
+ ansible.builtin.file:
+ path: /etc/apt/keyrings
+ state: directory
+ owner: root
+ group: root
+ mode: "0755"
+ become: true
+
+- name: Add Docker GPG key
+ ansible.builtin.get_url:
+ url: https://download.docker.com/linux/debian/gpg
+ dest: /etc/apt/keyrings/docker.asc
+ owner: root
+ group: root
+ mode: "0644"
+ become: true
+
+- name: Add Docker apt repository
+ ansible.builtin.apt_repository:
+ repo: >-
+ deb [arch={{ ansible_architecture | replace('x86_64', 'amd64') | replace('aarch64', 'arm64') }}
+ signed-by=/etc/apt/keyrings/docker.asc]
+ https://download.docker.com/linux/debian
+ {{ ansible_distribution_release }} stable
+ state: present
+ filename: docker
+ become: true
+
+- name: Install Docker Engine and Compose plugin
+ ansible.builtin.apt:
+ name:
+ - docker-ce
+ - docker-ce-cli
+ - containerd.io
+ - docker-buildx-plugin
+ - docker-compose-plugin
+ state: present
+ update_cache: true
+ become: true
+
+- name: Ensure Docker service is enabled and running
+ ansible.builtin.systemd:
+ name: docker
+ enabled: true
+ state: started
+ become: true
+
+- name: Create server deploy directory
+ ansible.builtin.file:
+ path: "{{ signage_server_deploy_dir }}"
+ state: directory
+ owner: root
+ group: root
+ mode: "0750"
+ become: true
+
+- name: Create server data directory
+ ansible.builtin.file:
+ path: "{{ signage_server_data_dir }}"
+ state: directory
+ owner: root
+ group: root
+ mode: "0750"
+ become: true
+
+- name: Create uploads directory
+ ansible.builtin.file:
+ path: "{{ signage_server_deploy_dir }}/uploads"
+ state: directory
+ owner: root
+ group: root
+ mode: "0750"
+ become: true
+
+- name: Deploy docker-compose.yml
+ ansible.builtin.template:
+ src: docker-compose.yml.j2
+ dest: "{{ signage_server_deploy_dir }}/docker-compose.yml"
+ owner: root
+ group: root
+ mode: "0640"
+ become: true
+ notify: Restart morz-server stack
+
+- name: Deploy server environment file
+ ansible.builtin.template:
+ src: env.j2
+ dest: "{{ signage_server_deploy_dir }}/.env"
+ owner: root
+ group: root
+ mode: "0600"
+ become: true
+ notify: Restart morz-server stack
+
+- name: Allow HTTPS through ufw
+ community.general.ufw:
+ rule: allow
+ port: "{{ signage_server_https_port }}"
+ proto: tcp
+ comment: morz-infoboard HTTPS
+ become: true
+ when: signage_server_ufw_enabled and signage_server_ufw_allow_https
+
+- name: Allow MQTT through ufw
+ community.general.ufw:
+ rule: allow
+ port: "{{ signage_server_mqtt_port }}"
+ proto: tcp
+ comment: morz-infoboard MQTT
+ become: true
+ when: signage_server_ufw_enabled and signage_server_ufw_allow_mqtt
+
+- name: Pull and start morz-server stack
+ community.docker.docker_compose_v2:
+ project_src: "{{ signage_server_deploy_dir }}"
+ state: present
+ pull: always
+ become: true
diff --git a/ansible/roles/signage_server/templates/docker-compose.yml.j2 b/ansible/roles/signage_server/templates/docker-compose.yml.j2
new file mode 100644
index 0000000..6f6282f
--- /dev/null
+++ b/ansible/roles/signage_server/templates/docker-compose.yml.j2
@@ -0,0 +1,58 @@
+---
+# Managed by Ansible – signage_server role
+# Do not edit manually on the server.
+
+services:
+ backend:
+ image: git.az-it.net/az/morz-infoboard/backend:latest
+ restart: unless-stopped
+ ports:
+ - "8080:8080"
+ environment:
+ MORZ_INFOBOARD_HTTP_ADDR: "${MORZ_HTTP_ADDR}"
+ MORZ_INFOBOARD_DATABASE_URL: "${MORZ_DATABASE_URL}"
+ MORZ_INFOBOARD_UPLOAD_DIR: /app/uploads
+ MORZ_INFOBOARD_STATUS_STORE_PATH: /app/data/status
+ MORZ_INFOBOARD_MQTT_BROKER: "${MORZ_MQTT_BROKER}"
+ MORZ_INFOBOARD_MQTT_USERNAME: "${MORZ_MQTT_USERNAME}"
+ MORZ_INFOBOARD_MQTT_PASSWORD: "${MORZ_MQTT_PASSWORD}"
+ MORZ_INFOBOARD_ADMIN_PASSWORD: "${MORZ_ADMIN_PASSWORD}"
+ MORZ_INFOBOARD_DEFAULT_TENANT: "${MORZ_DEFAULT_TENANT}"
+ MORZ_INFOBOARD_DEV_MODE: "${MORZ_DEV_MODE}"
+ volumes:
+ - ./uploads:/app/uploads
+ - ./data:/app/data
+ depends_on:
+ db:
+ condition: service_healthy
+
+ db:
+ image: postgres:17-alpine
+ restart: unless-stopped
+ environment:
+ POSTGRES_USER: morz_infoboard
+ POSTGRES_PASSWORD: "${MORZ_DB_PASSWORD}"
+ POSTGRES_DB: morz_infoboard
+ volumes:
+ - db_data:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U morz_infoboard"]
+ interval: 10s
+ timeout: 5s
+ retries: 5
+
+ mqtt:
+ image: eclipse-mosquitto:2
+ restart: unless-stopped
+ ports:
+ - "1883:1883"
+ - "9001:9001"
+ volumes:
+ - ./mosquitto/config:/mosquitto/config:ro
+ - mosquitto_data:/mosquitto/data
+ - mosquitto_log:/mosquitto/log
+
+volumes:
+ db_data:
+ mosquitto_data:
+ mosquitto_log:
diff --git a/ansible/roles/signage_server/templates/env.j2 b/ansible/roles/signage_server/templates/env.j2
new file mode 100644
index 0000000..0c6d146
--- /dev/null
+++ b/ansible/roles/signage_server/templates/env.j2
@@ -0,0 +1,16 @@
+# Managed by Ansible – signage_server role
+# Do not edit manually on the server.
+
+MORZ_HTTP_ADDR={{ morz_http_addr }}
+MORZ_DATABASE_URL={{ morz_database_url }}
+MORZ_DB_PASSWORD={{ morz_db_password | default('morz_infoboard') }}
+MORZ_UPLOAD_DIR={{ morz_upload_dir }}
+MORZ_STATUS_STORE_PATH={{ morz_status_store_path }}
+MORZ_DEFAULT_TENANT={{ morz_default_tenant }}
+MORZ_DEV_MODE={{ morz_dev_mode }}
+
+MORZ_ADMIN_PASSWORD={{ morz_admin_password }}
+
+MORZ_MQTT_BROKER={{ morz_mqtt_broker }}
+MORZ_MQTT_USERNAME={{ morz_mqtt_username }}
+MORZ_MQTT_PASSWORD={{ morz_mqtt_password }}
diff --git a/ansible/site.yml b/ansible/site.yml
index 8288506..bc3363c 100644
--- a/ansible/site.yml
+++ b/ansible/site.yml
@@ -1,7 +1,33 @@
---
+# Provision a fresh player (run once per new screen)
+- name: Provision new Signage Player
+ hosts: signage_players
+ gather_facts: false
+ tags: [provision]
+ roles:
+ - signage_provision
+
+# Base system setup for all signage nodes
+- name: Base setup for Signage Players
+ hosts: signage_players
+ gather_facts: true
+ tags: [base, player]
+ roles:
+ - signage_base
+
+# Deploy Morz Infoboard Player Agent and Kiosk Display
- name: Deploy Morz Infoboard Player Agent
hosts: signage_players
gather_facts: false
+ tags: [player]
roles:
- signage_player
- signage_display
+
+# Deploy Morz Infoboard Central Server
+- name: Deploy Morz Infoboard Central Server
+ hosts: signage_servers
+ gather_facts: true
+ tags: [server]
+ roles:
+ - signage_server
diff --git a/docs/GRUPPEN-KONZEPT.md b/docs/GRUPPEN-KONZEPT.md
new file mode 100644
index 0000000..cc5818f
--- /dev/null
+++ b/docs/GRUPPEN-KONZEPT.md
@@ -0,0 +1,535 @@
+# Info-Board Neu - Gruppierungs- und Slot-Modell fuer monitoruebergreifende Layouts
+
+## Ziel
+
+Dieses Dokument definiert, wie Screens in Gruppen und Slots organisiert werden.
+
+Gruppen und Slots sind notwendig fuer:
+
+- **Massenaktionen** — mehrere Screens mit einer Kampagne ansprechen
+- **Monitorwaende** — Schriftzuege und Layouts auf mehrere Screens verteilen
+- **zukuenftige Skalierbarkeit** — neue Displays ohne Neustrukturierung hinzufuegen
+
+Siehe auch `docs/TEMPLATE-KONZEPT.md` fuer Template-Typen, die Gruppen/Slots verwenden.
+
+## 1. Screen-Gruppen
+
+### Konzept
+
+Eine Gruppe ist eine semantische Zusammenfassung mehrerer Screens.
+
+**Beispiele:**
+
+- `all` — alle Screens im System
+- `wall-all` — alle 9 Infowand-Screens
+- `wall-row-1` — die 3 Screens der ersten Reihe
+- `wall-row-2` — die 3 Screens der zweiten Reihe
+- `single-all` — alle Einzelanzeigen (z.B. Vertretungsplan-Displays)
+- `outdoor` — alle Aussenanzeigetafeln
+
+### Typen von Gruppen
+
+#### Physische Gruppen
+
+Spiegeln die **reale Anordnung** wider:
+
+- `wall-all` — alle Displays einer Infowand
+- `wall-row-1`, `wall-row-2`, `wall-row-3` — Reihen einer Wand
+- `wall-column-1`, `wall-column-2`, `wall-column-3` — Spalten einer Wand
+
+#### Funktionale Gruppen
+
+Spiegeln den **Verwendungszweck** wider:
+
+- `main-hall-all` — alle Displays im Hauptkorridor
+- `cafeteria-all` — alle Displays in der Kaffeteria
+- `info-all` — alle Informationsanzeigen
+
+#### Typen-Gruppen
+
+Spiegeln das **Geraetemodell** wider:
+
+- `portrait-all` — alle Displays im Hochformat
+- `landscape-all` — alle Displays im Querformat
+- `4k-displays` — nur 4K-Monitore
+
+#### Tenant-Gruppen (Phase 2)
+
+Spiegeln die **Mandanten-Zugehoerigkeit** wider:
+
+- `tenant-xyz-all` — alle Displays fuer Mandant XYZ
+- `tenant-xyz-public` — nur oeffentliche Displays des Mandants
+
+### Hierarchische Struktur
+
+Gruppen koennen verschachtelt sein:
+
+```
+all
+ ├── wall-all
+ │ ├── wall-row-1
+ │ │ ├── info01
+ │ │ ├── info02
+ │ │ └── info03
+ │ ├── wall-row-2
+ │ │ ├── info04
+ │ │ ├── info05
+ │ │ └── info06
+ │ └── wall-row-3
+ │ ├── info07
+ │ ├── info08
+ │ └── info09
+ ├── single-all
+ │ ├── info10 (Vertretungsplan 1)
+ │ └── info11 (Vertretungsplan 2)
+ └── fallback-displays
+ └── [none currently]
+```
+
+**Automatische Inferenz:**
+
+Ein Screen kann in mehreren Gruppen sein:
+
+```
+info01:
+ - all
+ - wall-all
+ - wall-row-1
+ - portrait-all
+ - online-displays (automatisch basierend auf Status)
+```
+
+## 2. Slot-Modell
+
+### Konzept
+
+Slots beschreiben **feste Positionen innerhalb eines Layouts**.
+
+Sie werden hauptsaechlich fuer `message_wall`-Templates verwendet, um Ausschnitte von Grossmotiven auf einzelne Screens zu verteilen.
+
+**Beispiel: 3x3 Infowand**
+
+```
+┌─────────────────────────────────┐
+│ [0,0] [0,1] [0,2] │ Slot wall-r1-c1, wall-r1-c2, wall-r1-c3
+├─────────────────────────────────┤
+│ [1,0] [1,1] [1,2] │ Slot wall-r2-c1, wall-r2-c2, wall-r2-c3
+├─────────────────────────────────┤
+│ [2,0] [2,1] [2,2] │ Slot wall-r3-c1, wall-r3-c2, wall-r3-c3
+└─────────────────────────────────┘
+```
+
+**Slot-Nomenclatur:**
+
+- `wall-r{reihe}-c{spalte}` (Zeile/Spalte im 0er-System oder 1er-System)
+- `wall-slot-{nummer}` (durchnummeriert, z.B. wall-slot-0 bis wall-slot-8)
+
+### Geometrische Definition
+
+Fuer jeden Slot wird definiert:
+
+```json
+{
+ "slot_id": "wall-r1-c1",
+ "row": 0,
+ "col": 0,
+ "layout_name": "3x3_grid",
+ "crop_x": 0,
+ "crop_y": 0,
+ "crop_width": 640,
+ "crop_height": 1080,
+ "assigned_screen_id": "info01"
+}
+```
+
+Diese Werte sind:
+
+- **serverseitig generiert** — Admin muss nicht manuell Pixel-Koordinaten eingeben
+- **automatisch skalierbar** — bei verschiedenen Aufloesungen
+
+## 3. Datenmodell
+
+### Tabelle `screen_groups`
+
+```sql
+CREATE TABLE screen_groups (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ slug TEXT NOT NULL UNIQUE,
+ name TEXT NOT NULL,
+ description TEXT,
+ group_type TEXT NOT NULL CHECK (group_type IN (
+ 'physical', 'functional', 'device_type', 'tenant', 'custom'
+ )),
+ parent_group_id UUID REFERENCES screen_groups(id),
+ active BOOLEAN NOT NULL DEFAULT true,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+```
+
+**Beispiele:**
+
+```sql
+INSERT INTO screen_groups (slug, name, group_type)
+VALUES
+ ('all', 'Alle Screens', 'custom'),
+ ('wall-all', 'Infowand - Alle', 'physical'),
+ ('wall-row-1', 'Infowand - Reihe 1', 'physical'),
+ ('single-all', 'Einzelanzeigen', 'functional'),
+ ('portrait-all', 'Hochformat', 'device_type');
+```
+
+### Tabelle `screen_group_members`
+
+```sql
+CREATE TABLE screen_group_members (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ group_id UUID NOT NULL REFERENCES screen_groups(id) ON DELETE CASCADE,
+ screen_id UUID NOT NULL REFERENCES screens(id) ON DELETE CASCADE,
+ added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ UNIQUE(group_id, screen_id)
+);
+```
+
+**Beispiel:**
+
+```sql
+INSERT INTO screen_group_members (group_id, screen_id)
+SELECT
+ (SELECT id FROM screen_groups WHERE slug = 'wall-row-1'),
+ id
+FROM screens
+WHERE slug IN ('info01', 'info02', 'info03');
+```
+
+### Tabelle `layout_definitions`
+
+```sql
+CREATE TABLE layout_definitions (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ slug TEXT NOT NULL UNIQUE,
+ name TEXT NOT NULL,
+ layout_type TEXT NOT NULL CHECK (layout_type IN (
+ '3x3_grid', '2x2_grid', '1x9_row', '9x1_column', 'custom'
+ )),
+ rows INT NOT NULL,
+ cols INT NOT NULL,
+ description TEXT,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+```
+
+**Beispiel:**
+
+```sql
+INSERT INTO layout_definitions (slug, name, layout_type, rows, cols)
+VALUES ('3x3_infowand', 'Infowand 3x3', '3x3_grid', 3, 3);
+```
+
+### Tabelle `layout_slots`
+
+```sql
+CREATE TABLE layout_slots (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ layout_id UUID NOT NULL REFERENCES layout_definitions(id) ON DELETE CASCADE,
+ slot_slug TEXT NOT NULL,
+ row INT NOT NULL,
+ col INT NOT NULL,
+ UNIQUE(layout_id, slot_slug)
+);
+```
+
+**Beispiel:**
+
+```sql
+INSERT INTO layout_slots (layout_id, slot_slug, row, col)
+SELECT
+ (SELECT id FROM layout_definitions WHERE slug = '3x3_infowand'),
+ 'wall-r' || (r) || '-c' || (c),
+ r - 1, c - 1
+FROM
+ CROSS JOIN LATERAL (SELECT GENERATE_SERIES(1, 3) AS r)
+ CROSS JOIN LATERAL (SELECT GENERATE_SERIES(1, 3) AS c);
+```
+
+### Tabelle `slot_screen_assignments`
+
+```sql
+CREATE TABLE slot_screen_assignments (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ layout_id UUID NOT NULL REFERENCES layout_definitions(id),
+ slot_id UUID NOT NULL REFERENCES layout_slots(id) ON DELETE CASCADE,
+ screen_id UUID NOT NULL REFERENCES screens(id),
+ assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ UNIQUE(layout_id, slot_id, screen_id)
+);
+```
+
+**Beispiel:**
+
+```sql
+-- Zuordnung: Slot wall-r1-c1 → Screen info01 (in 3x3-Layout)
+INSERT INTO slot_screen_assignments (layout_id, slot_id, screen_id)
+SELECT
+ l.id,
+ ls.id,
+ s.id
+FROM
+ layout_definitions l,
+ layout_slots ls,
+ screens s
+WHERE
+ l.slug = '3x3_infowand'
+ AND ls.layout_id = l.id
+ AND ls.slot_slug = 'wall-r1-c1'
+ AND s.slug = 'info01';
+```
+
+## 4. Admin-Verwaltung
+
+### Gruppen verwalten
+
+**Seite:** Admin → Gruppen
+
+```
+┌──────────────────────────────────────────┐
+│ Screen-Gruppen │
+├──────────────────────────────────────────┤
+│ │
+│ Gruppe Typ Screens│
+│────────────────────────────────────────│
+│ all custom 13 │
+│ wall-all physical 9 │
+│ wall-row-1 physical 3 │
+│ wall-row-2 physical 3 │
+│ wall-row-3 physical 3 │
+│ single-all functional 2 │
+│ portrait-all device_type 12 │
+│ │
+│ [+ Neue Gruppe] [Gruppe bearbeiten] │
+└──────────────────────────────────────────┘
+```
+
+### Gruppe erstellen/bearbeiten
+
+```
+┌──────────────────────────────────────────┐
+│ Neue Gruppe │
+├──────────────────────────────────────────┤
+│ │
+│ Name * │
+│ [ Infowand Reihe 2 __________________ ] │
+│ slug: wall-row-2 (automatisch) │
+│ │
+│ Gruppentyp * │
+│ ⦿ physical (Wand-Anordnung) │
+│ ○ functional (Verwendungszweck) │
+│ ○ device_type (Geraetetyp) │
+│ ○ tenant (Mandant) │
+│ ○ custom (benutzerdefiniert) │
+│ │
+│ Beschreibung │
+│ [ Die obere Reihe der Infowand ______ ] │
+│ │
+│ Screens hinzufuegen │
+│ [ Suchfeld: "info" ] │
+│ □ info01 ← obere Reihe │
+│ □ info02 ← obere Reihe │
+│ ☑ info03 ← obere Reihe │
+│ □ info04 │
+│ ... (nur unzugeordnete zeigen) │
+│ │
+│ Ausgewaehlte Screens │
+│ info03 (portrait, online) │
+│ [ + weitere hinzufuegen ] │
+│ │
+│ Uebergruppe │
+│ [Dropdown: all > wall-all] │
+│ (optional, zur Hierarchie) │
+│ │
+│ [Speichern] [Abbrechen] │
+└──────────────────────────────────────────┘
+```
+
+### Layout-Definition erstellen (fuer Message-Wall)
+
+**Seite:** Admin → Layouts
+
+```
+┌──────────────────────────────────────────┐
+│ Layout-Definitionen │
+├──────────────────────────────────────────┤
+│ │
+│ Layout-Name Typ Grid Slots│
+│─────────────────────────────────────────│
+│ 3x3 Infowand 3x3_grid 3x3 9 │
+│ Vertretungsplan 2x2_grid 2x2 4 │
+│ News-Lauf 1x9_row 1x9 9 │
+│ │
+│ [+ Neues Layout] [Bearbeiten] │
+└──────────────────────────────────────────┘
+```
+
+Detailseite eines Layouts:
+
+```
+Layout: 3x3 Infowand
+
+Visualisierung:
+┌─────────┬─────────┬─────────┐
+│ Slot 1 │ Slot 2 │ Slot 3 │
+├─────────┼─────────┼─────────┤
+│ Slot 4 │ Slot 5 │ Slot 6 │
+├─────────┼─────────┼─────────┤
+│ Slot 7 │ Slot 8 │ Slot 9 │
+└─────────┴─────────┴─────────┘
+
+Slot-Zuordnungen:
+Slot 1 (wall-r1-c1) → Screen info01 (portrait, 1920x1080)
+Slot 2 (wall-r1-c2) → Screen info02 (portrait, 1920x1080)
+...
+
+[Screen-Zuordnungen aendernx] [Layout loeschen]
+```
+
+## 5. Anwendung in Kampagnen
+
+### Kampagne auf Gruppe anwenden
+
+**Beispiel:** Admin aktiviert Weihnachtsmotiv auf `wall-all`:
+
+```
+Template: Weihnachtsmotiv 2025 (full_screen_media)
+
+Zielgruppe auswaehlen:
+⦿ Alle Screens
+○ Nach Gruppe:
+ [Dropdown: wall-all ]
+ oder wall-row-1, single-all, ...
+○ Einzelne Screens
+
+→ Kampagne wird auf alle 9 Screens in wall-all aktiviert
+→ Jeder Screen zeigt dasselbe Motiv
+→ (Portrait/Landscape-Varianten werden serverseitig beruecksichtigt)
+```
+
+### Message-Wall-Kampagne mit Slot-Modell
+
+**Beispiel:** Admin teilt Schriftzug auf Infowand auf:
+
+```
+Template: Schriftzug (message_wall)
+
+Layout: 3x3 Infowand
+Zielgruppe: wall-all (auto-expandiert zu Slots)
+
+Gesamte Grafik hochladen oder zeichnen
+↓
+System generiert automatisch:
+ - Slot wall-r1-c1 → Ausschnitt x0-640 y0-1080 → Screen info01
+ - Slot wall-r1-c2 → Ausschnitt 640-1280 y0-1080 → Screen info02
+ - Slot wall-r1-c3 → Ausschnitt 1280-1920 y0-1080 → Screen info03
+ - ... (9 Zuweisungen insgesamt)
+↓
+Kampagne aktivieren
+↓
+Jeder Screen ladet seinen zustaendigen Ausschnitt
+↓
+Schriftzug erscheint verteilt ueber alle 9 Screens
+```
+
+## 6. Automatische Gruppe-Inferenz
+
+Der Server kann bestimmte Gruppen automatisch generieren:
+
+```python
+# Automatisch generierte Gruppen
+
+all:
+ - alle Screens im System (manuelle Verwaltung nicht noetig)
+
+online-all:
+ - alle Screens, die gerade online sind
+ - wird alle 5 Min aktualisiert
+
+offline-all:
+ - alle Screens, die gerade offline sind
+
+portrait-all:
+ - alle Screens mit Orientierung = "portrait"
+
+landscape-all:
+ - alle Screens mit Orientierung = "landscape"
+
+device_type_*:
+ - fuer jeden konfigurieren Screen-Typ (z.B. device_type_raspberry_pi)
+
+region_*:
+ - optional: auf Basis von Geo-Daten oder Tags
+```
+
+Diese automatischen Gruppen sind **read-only** im Admin-UI, aber voll verwendbar fuer Kampagnen.
+
+## 7. Beispiel: Neuinstallation einer Infowand
+
+**Szenario:** Admin installiert neue 3x3-Infowand mit Screens info01-info09.
+
+**Schritte:**
+
+1. **Screens anlegen** (via Provisionierungs-UI oder direkt)
+ ```
+ info01, info02, ..., info09
+ Alle: Orientierung portrait, Geraetetyp "raspberry_pi"
+ ```
+
+2. **Gruppen anlegen**
+ ```
+ screen_groups:
+ - slug: wall-all, name: "Infowand Alle", type: physical
+ - slug: wall-row-1, name: "Infowand Reihe 1", type: physical
+ - slug: wall-row-2, name: "Infowand Reihe 2", type: physical
+ - slug: wall-row-3, name: "Infowand Reihe 3", type: physical
+ ```
+
+3. **Screens den Gruppen zuordnen**
+ ```
+ wall-all: info01-info09
+ wall-row-1: info01, info02, info03
+ wall-row-2: info04, info05, info06
+ wall-row-3: info07, info08, info09
+ ```
+
+4. **Layout definieren**
+ ```
+ layout_definitions:
+ - slug: 3x3_infowand, rows: 3, cols: 3
+
+ layout_slots:
+ - wall-r1-c1, wall-r1-c2, wall-r1-c3 (row 0)
+ - wall-r2-c1, wall-r2-c2, wall-r2-c3 (row 1)
+ - wall-r3-c1, wall-r3-c2, wall-r3-c3 (row 2)
+
+ slot_screen_assignments:
+ - wall-r1-c1 → info01
+ - wall-r1-c2 → info02
+ - ... (9 gesamt)
+ ```
+
+5. **Kampagnen verwenden**
+ ```
+ Template: Schriftzug
+ Zielgruppe: wall-all
+ Layout: 3x3_infowand
+ → Kampagne kann sofort aktiviert werden
+ ```
+
+## 8. Zusammenfassung
+
+Das Gruppierungs- und Slot-Modell:
+
+- **ist flexibel** — physische, funktionale und typen-basierte Gruppen
+- **ist hierarchisch** — Gruppen koennen Untergruppen enthalten
+- **ist automatisch** — Gruppen wie "all" und "online-all" werden inferiert
+- **ist geometrisch** — Slots definieren Layouts fuer verteilte Motive
+- **ist skalierbar** — neue Screens werden einfach Gruppen zugeordnet
+- **ist intuitiv** — Admin-UI zeigt Zuordnungen und Vorschauen
diff --git a/docs/KAMPAGNEN-AKTIVIERUNG.md b/docs/KAMPAGNEN-AKTIVIERUNG.md
new file mode 100644
index 0000000..4e1ad77
--- /dev/null
+++ b/docs/KAMPAGNEN-AKTIVIERUNG.md
@@ -0,0 +1,483 @@
+# Info-Board Neu - Aktivierungsoberflaeche fuer saisonale und temporaere Kampagnen
+
+## Ziel
+
+Die Aktivierungsoberflaeche ermoeglicht es dem Admin, Kampagnen zeitlich und gezielt auf Screens auszurollen — sofort oder geplant.
+
+Dieses Dokument beschreibt:
+
+- die Aktivierungs-Workflows im Admin-UI
+- zeitgesteuerte Aktivierung (Scheduler)
+- Screen-Zuordnung und Vorschau
+- Status und Kontrolle waehrend der Laufzeit
+
+Siehe auch `docs/TEMPLATE-EDITOR.md` fuer die Template-Verwaltung und `docs/TEMPLATE-KONZEPT.md` fuer konzeptionelle Grundlagen.
+
+## 1. Aktivierungs-Workflows
+
+### Workflow 1 — Schnelle Sofort-Aktivierung
+
+**Szenario:** Admin hat ein Template und will es sofort starten.
+
+**Weg:**
+
+Admin → Templates → [Template] → "Aktivieren"
+
+```
+┌──────────────────────────────────────────┐
+│ Kampagne starten: Weihnachtsmotiv 2025 │
+├──────────────────────────────────────────┤
+│ │
+│ Kampagnen-Name (eindeutig) │
+│ [ Weihnachten 2025 _________________] │
+│ Vorschau: morz_campaign_xmas2025 │
+│ │
+│ Zielgruppe pruefen │
+│ aus Template: Alle Screens (13) │
+│ [Gruppe aendernx] [Screens aendernx] │
+│ │
+│ Dauer │
+│ ⦿ Sofort starten │
+│ gueltig ab jetzt │
+│ ○ Geplant starten │
+│ [Datum/Uhrzeit auswaehlen] │
+│ │
+│ Gueltig bis │
+│ [Datum/Uhrzeit auswaehlen] │
+│ oder [ ] unbegrenzt │
+│ │
+│ Prioritaet gegenueber Playlist │
+│ [10____________] hoeher = wichtiger │
+│ Standardwert: 1 │
+│ │
+│ Auto-Deaktivierung bei Ablauf? │
+│ ⦿ Ja, danach Fallback zeigen │
+│ ○ Nein, manuell deaktivieren │
+│ │
+│ Vorschau betroffener Screens │
+│ [Screenshot-Vorschau mit Kampagnen- │
+│ Inhalt fuer ausgew. Screens] │
+│ │
+│ [Aktivieren] [Abbrechen] │
+└──────────────────────────────────────────┘
+```
+
+**Aktion:**
+
+- Server speichert Kampagne mit `active = true`, `valid_from = NOW()`
+- Server expandiert Zielgruppe in konkrete Screens
+- Alle betroffenen Screens erhalten MQTT-Signal `playlist-changed` (obwohl Playlist gleich, aber Kampagnen-Prioritaet aendert sich)
+- Screens synchonisieren und laden neue Kampagnen-Inhalte
+
+### Workflow 2 — Geplante Aktivierung
+
+**Szenario:** Admin bereitet eine Kampagne vor, soll aber erst am naechsten Tag 8:00 Uhr starten.
+
+**Weg:**
+
+Admin → Templates → [Template] → "Aktivieren" → "Geplant starten"
+
+```
+┌──────────────────────────────────────────┐
+│ Geplante Aktivierung: Ostern 2025 │
+├──────────────────────────────────────────┤
+│ │
+│ Kampagnen-Name │
+│ [ Ostern_Dekoration_2025 ____________ ] │
+│ │
+│ Startdatum und -uhrzeit │
+│ [2025-04-14] [08:00] [Kalender/Uhr] │
+│ │
+│ Enddatum und -uhrzeit (optional) │
+│ [2025-04-21] [20:00] [Kalender/Uhr] │
+│ oder [ ] Kein Enddatum │
+│ │
+│ Prioritaet │
+│ [1_____________] │
+│ │
+│ Auto-Deaktivierung? │
+│ ⦿ Ja │
+│ ○ Nein │
+│ │
+│ Status │
+│ ◯ GEPLANT — wird am 2025-04-14 08:00 │
+│ aktiviert │
+│ │
+│ Erinnerung setzen (optional) │
+│ [ ] Erinnerungs-Email 1 Tag vorher │
+│ [ ] Erinnerungs-Email 1 Stunde vorher │
+│ │
+│ [Planen & Speichern] [Abbrechen] │
+└──────────────────────────────────────────┘
+```
+
+**Aktion:**
+
+- Server speichert Kampagne mit `active = false`, `valid_from = 2025-04-14 08:00`
+- Server erstellt inneren Scheduler-Job
+- Admin sieht Kampagne in Liste mit Status "GEPLANT"
+- Um geplanten Zeitpunkt:
+ - Scheduler setzt `campaigns.active = true`
+ - MQTT-Signal an alle betroffenen Screens
+ - Optionale Erinnerungs-Email an Admin
+
+### Workflow 3 — Schnelle Deaktivierung
+
+**Szenario:** Kampagne laeuft, Admin will sie sofort stoppen.
+
+**Weg:**
+
+Admin → Kampagnen → [laufende Kampagne] → "Deaktivieren"
+
+```
+┌──────────────────────────────────────────┐
+│ Kampagne deaktivieren? │
+├──────────────────────────────────────────┤
+│ │
+│ Kampagne: Weihnachten 2025 │
+│ Status: AKTIV seit 2025-12-01 09:00 │
+│ Betroffene Screens: 13 │
+│ │
+│ Aktion: │
+│ ⦿ Sofort deaktivieren │
+│ Screens zeigen danach wieder │
+│ Tenant-Playlist oder Fallback │
+│ │
+│ ○ Mit Verzoegerung (Fade-Out) │
+│ [2 Min] [5 Min] [Uhr auswaehlen] │
+│ Nuetzlich: Licht dimmen, Musik leiser │
+│ etc. vor Inhalt-Wechsel │
+│ │
+│ [Ja, deaktivieren] [Abbrechen] │
+└──────────────────────────────────────────┘
+```
+
+**Aktion:**
+
+- Server setzt `campaigns.active = false`
+- Server sendet MQTT-Signal an Screens
+- Screens wechseln sofort (oder mit Verzoegerung) zu Fallback/Playlist
+- Kampagne verschwindet aus "Aktive Kampagnen"-Liste
+
+## 2. Zeitplanung und Scheduler
+
+### Automatisierte Scheduler-Jobs
+
+Der Server laeuft einen einfachen Scheduler als Goroutine oder als separaten Service.
+
+```go
+// Pseudocode
+type CampaignScheduler interface {
+ RegisterJob(campaignID, activateAt, deactivateAt time.Time)
+ RunScheduler(ctx context.Context)
+}
+
+// Beim Starten
+func init() {
+ scheduler := NewCampaignScheduler()
+ go scheduler.RunScheduler(ctx)
+}
+
+// Im Hintergrund
+func (s *CampaignScheduler) RunScheduler(ctx context.Context) {
+ ticker := time.NewTicker(1 * time.Minute)
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-ticker.C:
+ // Checke alle geplanten Kampagnen
+ campaigns := db.GetScheduledCampaigns()
+ for _, c := range campaigns {
+ if time.Now() >= c.ValidFrom && !c.Active {
+ // Aktiviere die Kampagne
+ s.ActivateCampaign(c.ID)
+ }
+ if c.ValidUntil != nil && time.Now() >= *c.ValidUntil && c.Active {
+ // Deaktiviere die Kampagne
+ s.DeactivateCampaign(c.ID)
+ }
+ }
+ }
+ }
+}
+```
+
+### Persistenz ueber Restart
+
+Scheduler-Jobs werden in der Datenbank gespeichert (Spalten `valid_from`, `valid_until`, `active` in `campaigns`-Tabelle).
+
+Beim Neustart des Servers:
+
+1. Server laedt alle geplanten/aktiven Kampagnen
+2. Scheduler prueft bei jedem Takt (1 Min), ob eine Aktivierung/Deaktivierung faellig ist
+3. Kein Datenverlust, kein komplexes Job-Persisting noetig
+
+### Erinnerungen und Notifications
+
+**Optional (Phase 2):**
+
+- Email-Erinnerung N Stunden vor Aktivierung
+- Webhook-Notification fuer externe Systeme
+- In-App-Benachrichtigung im Admin-Dashboard
+
+## 3. Screen-Zuordnung und Vorschau
+
+### Interaktive Zielgruppen-Auswahl
+
+Waehrend der Kampagnen-Erstellung kann der Admin entscheiden, welche Screens betroffen sein sollen.
+
+```
+Zielgruppe
+ ⦿ Alle Screens
+ ○ Nach Gruppe auswaehlen:
+ □ wall-all (9 Screens)
+ □ single-info (2 Screens)
+ □ vertretungsplan-all (2 Screens)
+ ○ Einzelne Screens:
+ [ Suchfeld: "info" ]
+ □ info01 (portrait)
+ □ info02 (portrait)
+ ☑ info03 (portrait)
+ □ info04 (portrait)
+ ...
+```
+
+### Rendering-Vorschau
+
+Admin sieht, wie die Kampagne auf verschiedenen Zielscreens aussieht:
+
+```
+Betroffene Screens: 4 ausgew.
+
+┌─────────────────────────────────────┐
+│ info01 (portrait, 1920x1080) │
+│ ┌────────────────────────────────┐ │
+│ │ │ │
+│ │ [Kampagnen-Inhalt: Bild] │ │
+│ │ (Portrait-Assets verwendet) │ │
+│ │ │ │
+│ └────────────────────────────────┘ │
+└─────────────────────────────────────┘
+
+┌─────────────────────────────────────┐
+│ info05 (landscape, 2560x1440) │
+│ ┌────────────────────────────────┐ │
+│ │ [Kampagnen-Inhalt: Bild] │ │
+│ │ (Landscape-Assets verwendet) │ │
+│ └────────────────────────────────┘ │
+└─────────────────────────────────────┘
+
+[Scrollen um weitere Screens zu sehen]
+```
+
+### Live-Uebersicht waehrend Laufzeit
+
+Wenn eine Kampagne aktiv ist, zeigt das Admin-Dashboard:
+
+```
+Kampagne: Weihnachten 2025 einfuehrung
+Status: AKTIV seit 2025-12-01 09:00
+
+Betroffene Screens: 13
+ ✓ Aktiv angezeigt: 11 (info01-info08, info10, info11, info13)
+ ◯ Wartet auf Sync: 1 (info09)
+ ✗ Offline: 1 (info12)
+
+Zuletzt geprueft: vor 30 Sekunden
+
+[Aktualisieren] [Deaktivieren] [Bearbeiten]
+```
+
+## 4. Kampagnen-Verwaltung waehrend Laufzeit
+
+### Aktive Kampagnen — Haupt-Dashboard
+
+**Seite:** Admin → Aktive Kampagnen (oder Campaigns)
+
+```
+┌─────────────────────────────────┐
+│ Aktive Kampagnen │
+├─────────────────────────────────┤
+│ │
+│ Weihnachten 2025 einfuehrung │ ▼
+│ Template: Weihnachtsmotiv 2025 │
+│ Aktiv seit: 2025-12-01 09:00 │
+│ Aktiv bis: 2025-12-26 23:59 │
+│ Betroffene: 13 Screens │
+│ Status: ✓ Auf allen Screens ok │
+│ │
+│ [Vorschau] [Bearbeiten] │
+│ [Deaktivieren] │
+│ │
+├─────────────────────────────────┤
+│ │
+│ Event-Tag 25.03 │
+│ Template: screen_specific_scene │
+│ Aktiv seit: 2025-03-25 00:00 │
+│ Aktiv bis: 2025-03-25 23:59 │
+│ Betroffene: 4 Screens │
+│ Status: ◯ 1 Screen offline │
+│ │
+│ [Vorschau] [Bearbeiten] │
+│ [Deaktivieren] │
+│ │
+└─────────────────────────────────┘
+```
+
+### Geplante Kampagnen
+
+**Seite:** Admin → Kampagnen (Alle)
+
+```
+┌─────────────────────────────────┐
+│ Geplante Kampagnen │
+├─────────────────────────────────┤
+│ │
+│ Ostern-Dekoration 2025 │ ▼
+│ Template: full_screen_media │
+│ Status: GEPLANT │
+│ Startet: 2025-04-14 08:00 │
+│ Endet: 2025-04-21 20:00 │
+│ Betroffene: 13 Screens │
+│ Erinnerung: 1 Tag vorher │
+│ │
+│ [Vorschau] [Bearbeiten] │
+│ [Jetzt aktivieren] [Loeschen] │
+│ │
+├─────────────────────────────────┤
+│ │
+│ Sommer-Kampagne │
+│ Status: GEPLANT │
+│ Startet: 2025-06-01 00:00 │
+│ │
+│ ... │
+│ │
+└─────────────────────────────────┘
+```
+
+### Abgelaufene Kampagnen
+
+**Seite:** Admin → Kampagnen (Archiv)
+
+```
+Zeigt inaktive/abgelaufene Kampagnen fuer Audit-Trail.
+
+[ Kampagne ] Zeitraum Status
+Ostern 2025 2025-04-14—04-21 Auto-Deaktiviert
+Karneval 2025-02-28—03-05 Manuell deaktiviert
+Valentinstag 2025-02-14 Auto-Deaktiviert
+```
+
+## 5. Prioritaetsverwaltung
+
+### Prio-Einstellung pro Kampagne
+
+```
+Prioritaet gegenueber Tenant-Playlist
+┌─────────────────────────────────┐
+│ Schieber oder Zahlenfeld │
+│ │
+│ [|━━━━━━━━━━━| ] 10 │
+│ 1 5 10 100 │
+│ │
+│ Bedeutung: │
+│ 1 = normale Kampagne │
+│ 10 = hohe Prioritaet (Standard) │
+│ 100 = Notfall / absolut wichtig │
+│ │
+│ Diese Prioritaet wird ueber │
+│ alle Tenant-Playlists gestellt │
+│ (falls mehrere Kampagnen) │
+│ verwendet die mit hoechster │
+│ Prioritaet │
+└─────────────────────────────────┘
+```
+
+### Konflikt-Management (mehrere Kampagnen gleichzeitig)
+
+Falls mehrere Kampagnen fuer denselben Screen aktiv sind:
+
+1. Sortierende nach Prioritaet (hoechste gewinnt)
+2. Bei gleicher Prioritaet: nach Start-Zeitstempel (neueste gewinnt)
+3. Admin sieht im Status-Dashboard einen Warning: "2 Kampagnen fuer info01 aktiv"
+
+Empfehlung: Admin sollte Zeitraeume von Kampagnen nicht ueberlappen lassen.
+
+## 6. Fehlerbehandlung
+
+### Was, wenn ein Screen offline ist?
+
+```
+Kampagne wird aktiviert, aber Screen info03 ist gerade offline:
+
+1. Server weiss, dass info03 Ziel der Kampagne ist
+2. Server loggt: "Kampagne XYZ kann nicht auf info03 ausgeliefert werden (offline)"
+3. Info03 hat letzte gueltige Kampagne gecacht
+4. Sobald info03 wieder online kommt:
+ - Player synchonisiert
+ - Server sagt: "Kampagne XYZ ist aktiv"
+ - Player ladet und rendert
+5. Status im Dashboard: "info03 — Offline, wird synchronisiert sobald online"
+```
+
+### Rollback bei fehlgeschlagener Aktivierung
+
+Falls eine Kampagne fehlerhaft ist (kaputtes Video, Renderingfehler):
+
+```
+1. Screen zeigt Fehler-Overlay
+2. Admin ist informiert (Status-API zeigt Fehler)
+3. Admin Aktion 1: Template korrigieren
+ - Fehlerhaftes Asset austauschen
+ - Kampagne aktualisieren
+ - Screens neu synchonisieren
+4. Admin Aktion 2: Schnelle Deaktivierung
+ - Kampagne abschalten
+ - Fallback/Playlist kehrt zurueck
+```
+
+## 7. Datenschutz und Audit
+
+### Audit-Trail
+
+Alle Kampagnen-Aenderungen werden protokolliert:
+
+```json
+{
+ "ts": "2025-03-25T14:22:00Z",
+ "event": "campaign_activated",
+ "campaign_id": "uuid-...",
+ "campaign_name": "Ostern-Dekoration",
+ "triggered_by_user_id": "admin123",
+ "triggered_by_email": "admin@example.com",
+ "details": {
+ "valid_from": "2025-04-14T08:00:00Z",
+ "valid_until": "2025-04-21T20:00:00Z",
+ "target_screens_count": 13
+ }
+}
+```
+
+Diese Logs sind fuer Compliance und Forensik wichtig.
+
+### Sichtbarkeitsbeschraenkung
+
+Nur Benutzer mit Admin-Rolle koennen:
+
+- Kampagnen erstellen/aendernx
+- Templates bearbeiten
+- Aktivierung planen
+
+Tenant-User sehen keine Kampagnen-Verwaltung.
+
+## 8. Zusammenfassung
+
+Die Aktivierungsoberflaeche:
+
+- **ist einsteigerfreundlich** — Multi-Step Formulare mit Vorschau
+- **unterstuetzt Sofort und Planung** — spontan oder Wochen im Voraus
+- **ist sichtbar** — Live-Status und Fehler-Reporting
+- **ist automatisiert** — Scheduler kuemmert sich um Auf-/Abschalten
+- **ist sicher** — Audit-Trail und Rollback-Moeglichkeiten
+- **ist robust** — Offline-Screens werden spaeter synchronisiert
diff --git a/docs/MONITORING-KONZEPT.md b/docs/MONITORING-KONZEPT.md
new file mode 100644
index 0000000..69e0271
--- /dev/null
+++ b/docs/MONITORING-KONZEPT.md
@@ -0,0 +1,470 @@
+# Info-Board Neu - Logging- und Monitoring-Konzept
+
+## Ziel
+
+Logging und Monitoring geben dem Betriebsteam vollstaendige Transparenz ueber:
+
+- Verhalten und Fehler auf dem Player
+- Verhalten und Fehler auf dem Server
+- Health-Status aller Screens
+- Netzwerk- und Synchronisierungsprobleme
+- Kapazitaetsauslastung und Trends
+
+Das Konzept muss robust gegen Speicherplatz-Engpaesse auf dem Raspberry Pi arbeiten und zentralisiert auf dem Server auswertbar sein.
+
+## Logging-Architektur
+
+### Allgemeine Prinzipien
+
+- **strukturiertes JSON-Logging** — nicht Freitextloggen, sondern strukturierte Felder
+- **Log-Levels**: `debug`, `info`, `warn`, `error`, `fatal`
+- **Zentrale Auswertung** — Player loggen lokal und senden auch an Server
+- **Rotation und Bereinigung** — lokale Logs werden rotiert und komprimiert
+- **Datenschutz** — keine sensiblen Inhalte (Passwoerter, API-Keys) ins Log
+
+### Komponenten und ihre Logs
+
+## 1. Player-Logs
+
+### Player-Agent
+
+Der Agent protokolliert:
+
+- **Startup/Shutdown**
+ ```json
+ {
+ "ts": "2025-03-23T14:22:00Z",
+ "level": "info",
+ "component": "agent",
+ "event": "startup",
+ "config_file": "/etc/signage/config.yml",
+ "screen_id": "info01"
+ }
+ ```
+
+- **Server-Sync**
+ ```json
+ {
+ "ts": "2025-03-23T14:22:05Z",
+ "level": "info",
+ "component": "agent.sync",
+ "event": "sync_complete",
+ "duration_ms": 342,
+ "items_synced": 15,
+ "bytes_downloaded": 4521000
+ }
+ ```
+
+- **MQTT-Ereignisse**
+ ```json
+ {
+ "ts": "2025-03-23T14:22:10Z",
+ "level": "info",
+ "component": "agent.mqtt",
+ "event": "playlist_changed",
+ "source": "mqtt",
+ "cause": "playlist-changed-event"
+ }
+ ```
+
+- **Fehler**
+ ```json
+ {
+ "ts": "2025-03-23T14:22:15Z",
+ "level": "error",
+ "component": "agent.cache",
+ "event": "download_failed",
+ "media_id": "abc123",
+ "url": "https://cdn.example.com/video.mp4",
+ "error": "connection_timeout",
+ "retry_count": 2
+ }
+ ```
+
+- **Watchdog-Ereignisse** (siehe WATCHDOG-KONZEPT.md)
+
+### Player-UI
+
+Die lokale Web-App protokolliert:
+
+- **Item-Wechsel**
+ ```json
+ {
+ "ts": "2025-03-23T14:23:00Z",
+ "level": "info",
+ "component": "ui",
+ "event": "item_change",
+ "previous_item": "img-001",
+ "current_item": "video-002",
+ "source": "campaign"
+ }
+ ```
+
+- **Rendering-Fehler**
+ ```json
+ {
+ "ts": "2025-03-23T14:23:05Z",
+ "level": "warn",
+ "component": "ui.renderer",
+ "event": "render_failed",
+ "item_id": "url-003",
+ "media_type": "webpage",
+ "error": "load_timeout",
+ "timeout_ms": 10000
+ }
+ ```
+
+- **Overlay-Status-Aenderungen**
+ ```json
+ {
+ "ts": "2025-03-23T14:23:10Z",
+ "level": "info",
+ "component": "ui.overlay",
+ "event": "status_change",
+ "old_status": "online",
+ "new_status": "offline",
+ "reason": "broker_connection_lost"
+ }
+ ```
+
+### Chromium
+
+Der Browser ist schwer zu loggable, aber systemd journal erfasst:
+
+- Startup und Argumente
+- Crash-Meldungen
+- Fehlerrückmeldungen bei Seitenladefehler
+
+## 2. Server-Logs
+
+### Backend-API
+
+Der Server protokolliert:
+
+- **HTTP-Requests** (strukturiert, nicht kompletter Request-Body)
+ ```json
+ {
+ "ts": "2025-03-23T14:22:20Z",
+ "level": "info",
+ "component": "server.http",
+ "method": "POST",
+ "path": "/api/v1/screens/info01/playlist",
+ "status": 200,
+ "duration_ms": 34,
+ "user_id": "admin123",
+ "tenant_id": "tenant01"
+ }
+ ```
+
+- **Datenbank-Operationen** (nur bei Debug-Level)
+ ```json
+ {
+ "ts": "2025-03-23T14:22:25Z",
+ "level": "debug",
+ "component": "server.db",
+ "query": "UPDATE playlists SET updated_at = NOW() WHERE screen_id = $1",
+ "duration_ms": 5,
+ "rows_affected": 1
+ }
+ ```
+
+- **Fehler und Exceptions**
+ ```json
+ {
+ "ts": "2025-03-23T14:22:30Z",
+ "level": "error",
+ "component": "server.api",
+ "event": "media_download_failed",
+ "media_id": "abc123",
+ "reason": "storage_quota_exceeded",
+ "available_bytes": 1024000,
+ "required_bytes": 50000000
+ }
+ ```
+
+- **Admin-Kommandos**
+ ```json
+ {
+ "ts": "2025-03-23T14:22:35Z",
+ "level": "info",
+ "component": "server.command",
+ "event": "command_sent",
+ "command_type": "restart_player",
+ "target_screen": "info01",
+ "triggered_by_user": "admin123"
+ }
+ ```
+
+### Provisionierungs-Worker
+
+```json
+{
+ "ts": "2025-03-23T14:22:40Z",
+ "level": "info",
+ "component": "server.provision",
+ "event": "provision_started",
+ "screen_id": "new_display_01",
+ "target_ip": "192.168.1.50",
+ "ansible_playbook": "site.yml"
+}
+```
+
+## Log-Format und Ausgabe
+
+### Struktur
+
+Alle Logs folgen diesem Schema:
+
+```json
+{
+ "ts": "2025-03-23T14:22:00Z", // ISO 8601, UTC
+ "level": "info|warn|error|debug",
+ "component": "agent|ui|server.api|server.db|server.mqtt",
+ "event": "descriptive_name",
+ "screen_id": "info01", // nur auf Player relevant
+ "tenant_id": "tenant01", // nur auf Server relevant
+ "user_id": "user123", // nur auf Server bei Auth-Events
+ "duration_ms": 342, // bei Performance-Events
+
+ // Fehler-spezifische Felder
+ "error": "error_code",
+ "error_message": "readable error",
+
+ // Domain-spezifische Felder
+ "item_id": "...",
+ "media_type": "image|video|pdf|webpage",
+ "source": "campaign|tenant_playlist|fallback",
+
+ // Sonstige beliebige Felder
+ "details": { ... }
+}
+```
+
+### Ausgabeziele
+
+#### Auf dem Player
+
+1. **stdout/stderr** mit `log/slog` JSON-Formatter
+ - erfasst von systemd journal
+ - abrufbar via `journalctl`
+
+2. **Lokale Datei** `/var/log/signage/player.log`
+ - JSON, eine Zeile pro Event
+ - Rotation auf 100 MB, 10 Archive
+
+3. **Schnelle Fehler** an Server via HTTP-POST
+ - `POST /api/v1/screens/{screenSlug}/log-event`
+ - asynchron, Fehler bei Offline ignoriert
+ - nur `error` und `fatal` Events
+
+#### Auf dem Server
+
+1. **stdout/stderr** mit strukturiertem Logging
+ - erfasst von Docker/systemd
+ - abrufbar via `docker logs` oder `journalctl`
+
+2. **PostgreSQL** (Phase 2+)
+ - wichtige Fehler und Status-Events in Tabelle `logs`
+ - Abfrage-UI im Admin-Dashboard
+
+3. **Dateispeicher** (Docker Volume)
+ - `/var/log/signage/server.log`
+ - Rotation und Verdichtung durch Container-Orchester
+
+## Log-Level-Strategie
+
+### Debug (development)
+
+- SQL-Queries
+- HTTP-Request-Details
+- interner State-Uebergaenge
+
+Bei Production: `--log-level warn` oder `--log-level info`
+
+### Info (standard)
+
+- Startup/Shutdown
+- erfolgreiche Operationen
+- Status-Wechsel
+- Synchronisierungsereignisse
+
+### Warn (aufmerksamkeit)
+
+- Timeouts
+- Retry-Versuche
+- deprecierte APIs
+- suboptimale Performance
+
+### Error (problematisch)
+
+- gescheiterte HTTP-Requests
+- Datenbankfehler
+- fehlende Ressourcen
+- Auth-Fehler
+
+### Fatal (kritisch)
+
+- nicht-wiederherstellbare Fehler
+- Prozess beendet sich danach
+
+## Monitoring-Metriken
+
+### Player-seitig
+
+Metriken, die der Agent periodisch dem Server meldet:
+
+```json
+{
+ "screen_id": "info01",
+ "ts": "2025-03-23T14:25:00Z",
+ "heartbeat": {
+ "uptime_seconds": 86400,
+ "last_sync_at": "2025-03-23T14:24:55Z",
+ "seconds_since_last_sync": 5,
+ "sync_status": "ok|failed|pending",
+ "sync_fail_count_24h": 0
+ },
+ "resources": {
+ "cpu_percent": 25,
+ "memory_percent": 45,
+ "disk_free_mb": 2048,
+ "disk_used_percent": 35
+ },
+ "network": {
+ "broker_connected": true,
+ "server_reachable": true,
+ "ip_addresses": ["192.168.1.10"],
+ "signal_strength_dbm": -55
+ },
+ "playback": {
+ "current_item_id": "img-001",
+ "source": "campaign",
+ "rendering_status": "ok",
+ "seconds_on_current_item": 23
+ },
+ "errors_last_hour": [
+ {
+ "event": "download_failed",
+ "media_id": "video-999",
+ "count": 2
+ }
+ ]
+}
+```
+
+**Uebertragung:** HTTP `POST /api/v1/screens/{screenSlug}/heartbeat` alle 60 Sekunden
+
+### Server-seitig
+
+Der Server sammelt und ueberwacht:
+
+```json
+{
+ "screen_id": "info01",
+ "status": "online|offline|degraded|error",
+ "last_heartbeat_at": "2025-03-23T14:25:00Z",
+ "seconds_since_last_heartbeat": 0,
+ "heartbeat_interval_sec": 60,
+ "offline_since_sec": null,
+
+ "screenshot": {
+ "latest_at": "2025-03-23T14:25:00Z",
+ "seconds_since_latest": 0
+ },
+
+ "sync": {
+ "latest_at": "2025-03-23T14:24:55Z",
+ "latest_duration_ms": 342,
+ "fail_count_24h": 1,
+ "last_error": null
+ },
+
+ "content": {
+ "current_item": "img-001",
+ "source": "campaign",
+ "campaign_id": "xmas-2025"
+ },
+
+ "performance": {
+ "cpu_avg_percent_1h": 22,
+ "memory_avg_percent_1h": 44,
+ "disk_free_mb": 2048
+ }
+}
+```
+
+Diese Metriken werden in PostgreSQL gespeichert und bilden Basis fuer:
+
+- Status-Dashboard
+- Alerts
+- Trend-Analysen
+- Kapazitaetsplanung
+
+## Log-Rotation auf dem Player
+
+Der Raspberry Pi hat begrenzte Speicherkapazitaet. Log-Rotation muss aggressiv sein:
+
+```yaml
+# /etc/logrotate.d/signage
+
+/var/log/signage/player.log
+{
+ size 50M
+ rotate 5
+ compress
+ delaycompress
+ missingok
+ notifempty
+ create 0644 root root
+ postrotate
+ systemctl reload signage-agent.service || true
+ endscript
+}
+
+/var/log/signage/watchdog.log
+{
+ size 20M
+ rotate 3
+ compress
+ delaycompress
+ missingok
+ notifempty
+ create 0644 root root
+}
+```
+
+Resultat:
+- `player.log`: max 50 MB * 5 = 250 MB
+- `watchdog.log`: max 20 MB * 3 = 60 MB
+- Komprimierung von alten Logs auf ~10% der urspruenglichen Groesse
+
+## Alerting-Strategie
+
+### Kriterien fuer Alerts
+
+| Bedingung | Severity | Aktion |
+|---|---|---|
+| Screen offline > 15 min | High | Email + Dashboard-Alert |
+| Screen offline > 2h | Critical | Email + SMS |
+| Sync-Fehlerquote > 50% in 1h | Medium | Email |
+| Disk Full auf Player | Critical | Email + Stop-Recording |
+| CPU > 90% fuer 5 min | Medium | Warnung + Analysis |
+| Provisioning fehlgeschlagen | High | Email an Provisioner |
+
+### Alert-Kanal (Phase 2)
+
+1. **Dashboard-Benachrichtigungen** (im Admin-UI sichtbar)
+2. **Email** an konfigurierte Admin-Adressen
+3. **Webhook** fuer externe Monitoring-Systeme (Zabbix, Grafana)
+4. **Server-API** `/api/v1/admin/alerts` fuer Polling
+
+## Zusammenfassung
+
+Das Logging- und Monitoring-Konzept:
+
+- **ist strukturiert** — JSON, nicht Freitexte
+- **ist verteilt** — lokal auf Player + zentral auf Server
+- **ist speicherbewusst** — Rotation und Kompression
+- **gibt Ueberblick** — Heartbeat + Metriken fuer jeden Screen
+- **ermoeglicht Diagnose** — detaillierte Logs im Fehlerfall
+- **skaliert** — Verfahren gilt fuer beliebig viele Player
diff --git a/docs/PROVISION-KONZEPT.md b/docs/PROVISION-KONZEPT.md
new file mode 100644
index 0000000..e92fa15
--- /dev/null
+++ b/docs/PROVISION-KONZEPT.md
@@ -0,0 +1,610 @@
+# Info-Board Neu - Jobrunner-Konzept fuer Ansible-gestuetzte Erstinstallation
+
+## Ziel
+
+Der Jobrunner fuehrt aus dem Admin-Backend heraus Provisionierungsjobs aus, die ein neues Display technisch in Betrieb nehmen.
+
+Dieses Dokument beschreibt:
+
+- wie ein Admin einen neuen Screen aus dem Web-UI provisioniert
+- wie der Server Ansible-Playbooeke orchestriert
+- wie der Fortschritt angezeigt wird
+- Sicherheits- und Fehlerbehandlung
+
+Grundlagen zur Provisionierungs-Strategie finden sich in `docs/PROVISIONIERUNGSKONZEPT.md`.
+
+## 1. Provisionierungs-Workflow im Admin-UI
+
+### Seite: Admin → Screens → Neu
+
+```
+┌──────────────────────────────────────────┐
+│ Neuen Screen provisionieren │
+├──────────────────────────────────────────┤
+│ │
+│ Schritt 1 — Grunddaten │
+│ │
+│ Screen-ID / Slug * │
+│ [ info10 ] │
+│ (muss eindeutig sein, alphanumerisch) │
+│ │
+│ Anzeigename * │
+│ [ Infowand Bottom-Left ________________ ] │
+│ │
+│ Beschreibung │
+│ [ Neue Infowand Display, pos. 7______ ] │
+│ │
+│ Device Type * │
+│ ⦿ Raspberry Pi 4 │
+│ ○ Raspberry Pi 5 │
+│ ○ x86 Linux Kiosk │
+│ │
+│ Aufloesung * │
+│ [1920 x 1080 ] Standard fuer RPi │
+│ │
+│ Orientierung * │
+│ ⦿ portrait (hochkant) │
+│ ○ landscape (quer) │
+│ │
+│ Tenant-Zuordnung │
+│ [ Dropdown: alle Tenants + "admin" ] │
+│ │
+│ [Weiter >] [Abbrechen] │
+└──────────────────────────────────────────┘
+```
+
+### Schritt 2 — Netzwerk- und SSH-Einstellung
+
+```
+┌──────────────────────────────────────────┐
+│ Schritt 2 — Zugang zur Hardware │
+│ │
+│ Ziel-IP-Adresse * │
+│ [ 192.168.1.50 ] │
+│ │
+│ SSH-Port │
+│ [ 22 ] Standard │
+│ │
+│ Bootstrap-Benutzer * │
+│ ⦿ root │
+│ ○ pi │
+│ ○ custom: [ ________________ ] │
+│ │
+│ Bootstrap-Authentifizierung * │
+│ ⦿ Passwort (initial, wird durch Key │
+│ ersetzt): │
+│ [ Passwort ____________ ] │
+│ ○ SSH-Key (nur wenn vorvorhanden): │
+│ [ Datei auswaehlen ] oder │
+│ [ PEM-Key einfuegen ] │
+│ │
+│ Test-Verbindung │
+│ [SSH Test] [PING Test] │
+│ │
+│ [Weiter >] [Zurueck] [Abbrechen] │
+└──────────────────────────────────────────┘
+```
+
+### Schritt 3 — Konfiguration und Optionen
+
+```
+┌──────────────────────────────────────────┐
+│ Schritt 3 — Konfiguration │
+│ │
+│ Fallback-Verzeichnis (lokal auf Player) │
+│ [ /var/lib/signage/fallback ] │
+│ │
+│ Snapshot-Intervall (Sekunden) │
+│ [ 300 ] 0 = deaktiviert │
+│ │
+│ MQTT-Broker-Adresse (Zielserver) │
+│ [ mqtt.example.com ] auto-gefuellt │
+│ │
+│ Server-API-Adresse │
+│ [ https://signage.example.com/api ] │
+│ auto-gefuellt │
+│ │
+│ Gruppen-Zuordnung (optional) │
+│ [ Checkboxen: wall-all, wall-row-1 ] │
+│ │
+│ Tags / Labels (optional) │
+│ [ mainfloor, hightrafficarea ] │
+│ │
+│ [Weiter >] [Zurueck] [Abbrechen] │
+└──────────────────────────────────────────┘
+```
+
+### Schritt 4 — Review und Start
+
+```
+┌──────────────────────────────────────────┐
+│ Schritt 4 — Uebersicht & Start │
+│ │
+│ Zusammenfassung: │
+│ │
+│ Screen: info10 │
+│ Name: Infowand Bottom-Left │
+│ Typ: Raspberry Pi 4 │
+│ IP: 192.168.1.50 │
+│ Aufloesung: 1920 x 1080 │
+│ Orientierung: portrait │
+│ Tenant: admin │
+│ │
+│ SSH-Verbindung wird hergestellt... │
+│ [✓] SSH-Zugang verifiziert │
+│ [✓] Pfadberechtigungen ok │
+│ [✓] Speicherplatz ausreichend (15GB) │
+│ │
+│ Provisioning-Playbook: │
+│ [ ] site.yml │
+│ ├─ signage_base (Packages, Kernel) │
+│ ├─ signage_display (X11, Chromium) │
+│ ├─ signage_player (Agent, Config) │
+│ └─ signage_provision (Setup-Jobs) │
+│ │
+│ Warnung: │
+│ ! Diesen Prozess kann nicht unterbrochen│
+│ werden. Typische Dauer: 10-15 Min. │
+│ │
+│ [Provisioning starten] [Abbrechen] │
+└──────────────────────────────────────────┘
+```
+
+## 2. Provisioning-Job: Serverseitige Orchestrierung
+
+### Architektur
+
+```
+┌─────────────────────────────────────────┐
+│ Admin-UI HTTP Request │
+│ POST /api/v1/admin/provision │
+└────────────┬────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────┐
+│ Backend API (Go) │
+│ - validiert Eingaben │
+│ - erstellt ProvisioningJob in DB │
+│ - queued Job in Job-Broker (Redis etc) │
+└────────────┬────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────┐
+│ Jobrunner Worker (Goroutine oder │
+│ separater Go-Service) │
+│ - laeuft im Server-Container │
+│ - zeigt Fortschritt via Websocket │
+└────────────┬────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────┐
+│ Ansible Executor │
+│ ansible-playbook site.yml │
+│ -i inventory.ini │
+│ -e vars.yml │
+└────────────┬────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────┐
+│ Target Device (Raspberry Pi) │
+│ SSH: root@192.168.1.50 │
+│ - installiert Packages │
+│ - startet Services │
+│ - synchonisiert Config │
+└─────────────────────────────────────────┘
+```
+
+### Provisioning-Job-Modell
+
+```sql
+CREATE TABLE provisioning_jobs (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ screen_id UUID NOT NULL REFERENCES screens(id),
+ status TEXT NOT NULL CHECK (status IN (
+ 'pending', 'running', 'completed', 'failed'
+ )),
+ started_at TIMESTAMPTZ,
+ completed_at TIMESTAMPTZ,
+
+ -- SSH/Ansible-Details
+ target_ip TEXT NOT NULL,
+ target_port INT NOT NULL DEFAULT 22,
+ target_user TEXT NOT NULL,
+
+ -- Verbrauch von Ressourcen
+ ansible_job_id TEXT, -- Job-ID aus Ansible-Executor
+
+ -- Fehlerbehandlung
+ error_log TEXT, -- bei failure
+
+ created_by_user_id TEXT NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+```
+
+### Provisioning-Log-Modell
+
+```sql
+CREATE TABLE provisioning_logs (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ job_id UUID NOT NULL REFERENCES provisioning_jobs(id) ON DELETE CASCADE,
+ line_number INT NOT NULL,
+ timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+
+ -- Quelle des Logs
+ source TEXT NOT NULL CHECK (source IN ('ansible', 'agent', 'system')),
+ level TEXT NOT NULL CHECK (level IN ('info', 'warn', 'error')),
+
+ -- Nachricht
+ message TEXT NOT NULL,
+
+ UNIQUE(job_id, line_number)
+);
+```
+
+## 3. Jobrunner-Implementierung
+
+### Job-Verarbeitung (Pseudocode)
+
+```go
+type ProvisioningJobRunner struct {
+ db *sql.DB
+ ansibleBinPath string
+ logChannel chan ProvisioningLogMessage
+}
+
+func (r *ProvisioningJobRunner) ProcessJob(ctx context.Context, jobID uuid.UUID) error {
+ // 1. Lade Job aus DB
+ job := r.db.GetProvisioningJob(jobID)
+
+ // 2. Setze Status auf "running"
+ r.db.UpdateProvisioningJob(job.ID, map[string]interface{}{
+ "status": "running",
+ "started_at": time.Now(),
+ })
+
+ // 3. Generiere Ansible-Inventar
+ inventory := r.generateInventory(job)
+ // [192.168.1.50]
+ // ansible_user=root
+ // ansible_password=***
+ // screen_id=info10
+ // ansible_become=yes
+
+ // 4. Generiere vars.yml
+ vars := r.generateVars(job)
+ // screen_id: info10
+ // display_name: "Infowand Bottom-Left"
+ // orientation: portrait
+ // mqtt_broker: mqtt.example.com
+ // etc.
+
+ // 5. Fuehre Ansible aus
+ cmd := exec.CommandContext(ctx,
+ r.ansibleBinPath,
+ "site.yml",
+ "-i", inventoryPath,
+ "-e", varsPath,
+ "-v", // verbose
+ )
+
+ // 6. Piping: Ansible-Ausgabe → Log-Dateien + Websocket
+ stdout, _ := cmd.StdoutPipe()
+ stderr, _ := cmd.StderrPipe()
+
+ go r.streamLogs(job.ID, stdout, "ansible")
+ go r.streamLogs(job.ID, stderr, "ansible")
+
+ // 7. Warte auf Completion
+ err := cmd.Run()
+
+ // 8. Aktualisiere Job-Status
+ if err != nil {
+ r.db.UpdateProvisioningJob(job.ID, map[string]interface{}{
+ "status": "failed",
+ "completed_at": time.Now(),
+ "error_log": err.Error(),
+ })
+ return err
+ }
+
+ r.db.UpdateProvisioningJob(job.ID, map[string]interface{}{
+ "status": "completed",
+ "completed_at": time.Now(),
+ })
+
+ return nil
+}
+
+func (r *ProvisioningJobRunner) streamLogs(jobID uuid.UUID, reader io.Reader, source string) {
+ scanner := bufio.NewScanner(reader)
+ lineNum := 1
+
+ for scanner.Scan() {
+ line := scanner.Text()
+
+ // Persistiere in DB
+ r.db.InsertProvisioningLog(ProvisioningLog{
+ JobID: jobID,
+ LineNumber: lineNum,
+ Source: source,
+ Level: parseLogLevel(line), // heuristic
+ Message: line,
+ })
+
+ // Schreibe ins Websocket (siehe Abschnitt "Fortschritt")
+ r.logChannel <- ProvisioningLogMessage{
+ JobID: jobID,
+ Line: line,
+ }
+
+ lineNum++
+ }
+}
+```
+
+### Ansible-Ausfuehrung mit Jumphost (optional)
+
+Falls der Server nicht direkt die Zielgeraete erreicht, kann ein Jumphost verwendet werden:
+
+```yaml
+# ansible.cfg
+[defaults]
+inventory = inventory.ini
+host_key_checking = False
+retries = 3
+
+[privilege_escalation]
+become = True
+become_method = sudo
+```
+
+```ini
+# inventory.ini fuer Jumphost-Szenario
+[targets]
+192.168.1.50 ansible_user=root ansible_password=*** \
+ ansible_ssh_common_args='-o ProxyCommand="ssh -W %h:%p jumphost@example.com"'
+```
+
+## 4. Fortschritt und Live-Updates
+
+### Websocket-Kanal fuer Echtzeit-Logs
+
+**HTTP-Upgrade zu Websocket:**
+
+```
+GET /api/v1/admin/provision/{jobID}/logs
+Upgrade: websocket
+Connection: Upgrade
+```
+
+**Server sendet kontinuierlich:**
+
+```json
+{
+ "type": "log_line",
+ "timestamp": "2025-03-25T14:22:00Z",
+ "line": "TASK [signage_base : Update package cache] **",
+ "source": "ansible",
+ "level": "info"
+}
+```
+
+```json
+{
+ "type": "progress",
+ "timestamp": "2025-03-25T14:22:15Z",
+ "current_task": "signage_base : Update package cache",
+ "task_number": 3,
+ "total_tasks": 12,
+ "percent": 25
+}
+```
+
+```json
+{
+ "type": "status_change",
+ "timestamp": "2025-03-25T14:35:00Z",
+ "status": "completed",
+ "duration_seconds": 780
+}
+```
+
+### UI-Anzeige waehrend Provisioning
+
+```
+┌──────────────────────────────────────────┐
+│ Provisioning laeuft: info10 │
+│ Gestartet: vor 5 Min. │
+│ Geschaetzte verbleibende Zeit: 8 Min. │
+├──────────────────────────────────────────┤
+│ │
+│ [████████████░░░░░░░░░░░░░░] 33% │
+│ │
+│ Aktuelle Aufgabe: │
+│ ⊙ signage_base : Update package cache │
+│ │
+│ Letzte Logs: │
+│ ├─ [14:22:00] TASK [signage_base ...] │
+│ ├─ [14:22:05] ok: [192.168.1.50] │
+│ ├─ [14:22:10] TASK [signage_display] │
+│ ├─ [14:22:15] Chromium wird installiert│
+│ └─ [14:22:20] ... │
+│ │
+│ [Auto-Refresh] [Pause] [Abbrechen] │
+│ (Abbrechen: SSH-Verbindung wird nicht │
+│ sofort getrennt, aber Job gestoppt) │
+└──────────────────────────────────────────┘
+```
+
+## 5. Fehlerbehandlung und Recovery
+
+### Fehlerszenarien
+
+| Fehler | Grund | Recovery |
+|---|---|---|
+| SSH-Verbindung fehlgeschlagen | IP falsch, Passwort falsch, Firewall | Logs zeigen SSH-Error, Admin kann Credentials korrigieren und neu starten |
+| Ansible-Playbook fehlgeschlagen | Paket-Versionskonflikt, Platz voll | Logs zeigen welcher Task fehlgeschlagen, Admin kann manuell SSH-en oder Job wiederholen |
+| Timeout nach 30 Min. | Sehr langsame Netzwerk oder Device haengt | Job wird abgebrochen, Admin kann Verbindung checken und neu starten |
+| Package-Download fehlgeschlagen | Mirror offline, Netzwerk unterbrochen | Ansible retry automatisch 3x, Logs zeigen wget-Error |
+
+### Retry-Logik
+
+```
+Strategie: Exponentieller Backoff fuer Playbook-Fehler
+ Fehler 1: Sofort wiederholen
+ Fehler 2: Warte 5s, wiederhole
+ Fehler 3: Warte 15s, wiederhole
+ Fehler 4+: Gib auf, zeige Fehler
+```
+
+### Admin-Recovery
+
+Falls ein Job fehlgeschlagen ist:
+
+```
+┌──────────────────────────────────────────┐
+│ Provisioning fehlgeschlagen: info10 │
+│ │
+│ Fehler: │
+│ ssh: Could not resolve hostname │
+│ (DNS-Fehler oder Geraet nicht erreichbar)│
+│ │
+│ Empfehlung: │
+│ 1. IP-Adresse pruefen │
+│ 2. Geraet von Hand SSH-en und testen │
+│ 3. Job neu starten: [Neuer Versuch] │
+│ │
+│ Komplette Logs herunterladen: │
+│ [logs-info10-20250325.txt] │
+│ │
+│ [Neuer Versuch] [Logs zeigen] [Zurueck]│
+└──────────────────────────────────────────┘
+```
+
+## 6. Sicherheitsaspekte
+
+### SSH-Key-Verwaltung
+
+**Phase 1 — Bootstrap mit Passwort:**
+
+```
+Admin gibt Passwort ein
+ ↓
+Server speichert Passwort NICHT
+ ↓
+Server uebergibt an Ansible nur waehrend dieser Session
+ ↓
+Ansible loggt sich ein, generiert SSH-Key
+ ↓
+SSH-Key wird auf dem Geraet als authorized_key eingetragen
+ ↓
+Passwort wird auf dem Geraet gelöscht oder deaktiviert
+```
+
+**Phase 2 — Dauerhaft mit SSH-Key:**
+
+```
+Server speichert SSH-Key in Secrets-Backend (z.B. HashiCorp Vault)
+Zukuenftige Ansible-Lauefe verwenden den Key
+```
+
+### Ansible-Vault fuer sensitive Daten
+
+```yaml
+# roles/signage_player/defaults/main.yml
+server_api_key: !vault |
+ $ANSIBLE_VAULT;1.1;AES256
+ abcd1234...
+```
+
+Die Vault-Passphrase wird:
+
+- nie im Klartext gelagert
+- vom Server nur zur Laufzeit an Ansible uebergeben
+- in Logs nicht ausgegeben
+
+### Sudo ohne Passwort
+
+Ansible erhoeht die Rechte per `sudo` ohne Passwort-Eingabe:
+
+```sudoers
+# /etc/sudoers.d/ansible-signage
+ansible ALL=(ALL) NOPASSWD: ALL
+```
+
+(Alternativ: mit Passwort, das Ansible am Anfang einmal abfragt)
+
+## 7. Verbindung zum bestehenden System
+
+### Provisioning-Trigger aus Admin-UI
+
+```
+Admin-Seite: Screens → "+ Neuer Screen"
+ ↓
+Formular sammelt Grunddaten
+ ↓
+POST /api/v1/admin/provision
+ ↓
+Backend:
+ 1. Screen in `screens` Tabelle eintragen
+ 2. ProvisioningJob in `provisioning_jobs` anlegen
+ 3. Job in Broker queuen
+ ↓
+Jobrunner:
+ 1. Holt Job aus Broker
+ 2. Startet Ansible
+ 3. Streamt Logs via Websocket
+ 4. Aktualisiert Job-Status bei Completion
+ ↓
+Admin sieht Live-Updates im UI
+```
+
+### Nach erfolgreichem Provisioning
+
+```
+Job-Status: "completed"
+ ↓
+Agent auf dem Display startet
+ ↓
+Agent registriert sich beim Server
+ ↓
+Server setzt Screen-Status auf "online"
+ ↓
+Admin sieht Screen in Tabelle mit Status "online"
+ ↓
+Admin kann sofort Kampagnen/Playlists zuweisen
+```
+
+## 8. Konfigurierbare Parameter
+
+In `/etc/signage/provision.yml`:
+
+```yaml
+jobrunner:
+ max_concurrent_jobs: 3
+ ansible_timeout_sec: 1800
+ playbook_path: "/srv/ansible/site.yml"
+ inventory_template_path: "/srv/ansible/inventory.ini.tpl"
+ vars_template_path: "/srv/ansible/vars.yml.tpl"
+
+ssh:
+ known_hosts_file: "/etc/signage/.ssh/known_hosts"
+ key_storage: "vault" # oder "filesystem"
+
+ansible:
+ verbosity: "-vv" # oder "-v", "-vvv"
+ extra_args: ""
+```
+
+## 9. Zusammenfassung
+
+Der Jobrunner:
+
+- **ist web-gesteuert** — Provisioning-UI mit Multi-Step-Wizard
+- **ist automatisiert** — Ansible Playbooks, nicht manuelle SSH-Kommandos
+- **ist transparent** — Live-Logs und Fortschritt-Anzeige
+- **ist sicher** — SSH-Keys, Ansible-Vault, keine Plaintext-Credentials in Logs
+- **ist resilient** — Retry-Logik und Error-Recovery
+- **ist erweiterbar** — neue Rollen und Tasks koennen hinzugefuegt werden ohne UI-Aenderung
diff --git a/docs/TEMPLATE-EDITOR.md b/docs/TEMPLATE-EDITOR.md
new file mode 100644
index 0000000..01ee652
--- /dev/null
+++ b/docs/TEMPLATE-EDITOR.md
@@ -0,0 +1,494 @@
+# Info-Board Neu - Template-Editor fuer globale Kampagnen
+
+## Ziel
+
+Der Template-Editor ist ein Bereich des Admin-UI fuer die fachliche Erstellung und Verwaltung globaler Templates und deren operativen Aktivierungen als Kampagnen.
+
+Dieses Dokument definiert:
+
+- Welche Schritte ein Admin unternimmt, um ein Template zu erstellen
+- Welche Felder und Optionen der Editor anbietet
+- Wie Templates zu Kampagnen aktiviert werden
+- Wie die Abbildung im Datenmodell aussieht
+
+Grundlagen zu Template-Typen, Slot-Modell und Message-Wall finden sich in `docs/TEMPLATE-KONZEPT.md`.
+
+## 1. Template-Verwaltung
+
+### Template-Liste
+
+**Seite:** Admin → Templates
+
+**Anzeige:**
+
+Tabelle mit allen Templates:
+
+| Name | Typ | Zielgruppe | Szenen | Erstellt | Status |
+|---|---|---|---|---|---|
+| Weihnachtsmotiv 2025 | full_screen_media | alle | 1 | 2025-01-15 | draft |
+| Schriftzug Infowand | message_wall | wall-all | 9 | 2025-02-01 | active |
+| Event-Tag 25.03 | screen_specific_scene | [info01, info02, ...] | 2 | 2025-03-01 | draft |
+
+**Aktionen pro Zeile:**
+
+- "Bearbeiten" — öffnet Template-Editor
+- "Kopieren" — dupliziert als neue Draft
+- "Löschen" — nur wenn keine aktiven Kampagnen
+- "Vorschau" — zeigt Layout (fuer message_wall) oder Asset-Galerien
+- "Aktivieren" — schneller Weg zu Kampagne starten
+
+### Template-Editor (Erstellung/Bearbeitung)
+
+#### Phase 1 — Grunddaten
+
+```
+┌─────────────────────────────────────────┐
+│ Neues Template erstellen │
+├─────────────────────────────────────────┤
+│ │
+│ Name * │
+│ [ Weihnachtsmotiv 2025_______________ ]│
+│ technischer slug wird automatisch │
+│ │
+│ Template-Typ * │
+│ ⦿ full_screen_media │
+│ ○ message_wall │
+│ ○ screen_specific_scene │
+│ │
+│ Beschreibung │
+│ [ Weihnachtliche Grafik fuer alle___ ] │
+│ [ Screens __________________________ ]│
+│ │
+│ Zielgruppe / Screens * │
+│ ⦿ Alle Screens │
+│ ○ Nach Gruppe auswaehlen │
+│ [Dropdown: wall-all, single-all, ...] │
+│ ○ Einzelne Screens auswaehlen │
+│ [Checkbox-Liste mit Filterung] │
+│ │
+│ [Weiter >] [Abbrechen] │
+└─────────────────────────────────────────┘
+```
+
+**Validierung:**
+
+- Name ist erforderlich
+- Name ist eindeutig
+- Template-Typ ist erforderlich
+- Zielgruppe ist erforderlich (keine leere Zuweisung)
+
+#### Phase 2 — Szenen/Inhalte
+
+Fuer `full_screen_media`:
+
+```
+┌─────────────────────────────────────────┐
+│ Szenen und Inhalte │
+├─────────────────────────────────────────┤
+│ │
+│ Szene 1: Vollbild-Grafik │
+│ │
+│ Medientyp * │
+│ ○ Bild │
+│ ○ Video │
+│ ○ PDF │
+│ ⦿ Webseite (HTML) │
+│ │
+│ Portrait-Asset (Hochformat) │
+│ [Upload oder URL] │
+│ [ Datei auswaehlen ] [Neue URL] │
+│ oder vorher gemanagte Assets: [Liste] │
+│ │
+│ Landscape-Asset (Querformat) [optional] │
+│ [ Datei auswaehlen ] [Neue URL] │
+│ │
+│ Anzeigedauer (Sekunden) │
+│ [60_____] Standard: 10 │
+│ │
+│ Load-Timeout (Sekunden) │
+│ [10_____] Standard: 10 │
+│ │
+│ gueltig ab │
+│ [ 2025-03-25 ] [ 00:00 ] │
+│ (leer = sofort gueltig) │
+│ │
+│ gueltig bis │
+│ [ 2025-04-01 ] [ 00:00 ] │
+│ (leer = unendlich) │
+│ │
+│ [+ Weitere Szene hinzufuegen] │
+│ │
+│ [Zurueck <] [Speichern & Aktivieren] │
+│ [Speichern] │
+│ [Abbrechen] │
+└─────────────────────────────────────────┘
+```
+
+Fuer `message_wall`:
+
+```
+┌─────────────────────────────────────────┐
+│ Message-Wall Layout │
+├─────────────────────────────────────────┤
+│ │
+│ Layout-Template │
+│ [Dropdown: 3x3-Grid, 2x2-Grid, ...] │
+│ │
+│ Anzeigedauer (Sekunden) │
+│ [10_____] │
+│ │
+│ Gesamt-Grafik oder Text eingeben │
+│ [Rich-Text-Editor oder Bild-Upload] │
+│ │
+│ Vorschau: [Zeigt Einteilung in Slots] │
+│ │
+│ Slot-Zuordnung: [Interaktive Zuordnung] │
+│ Slot wall-r1-c1 → Screen info01 │
+│ Slot wall-r1-c2 → Screen info02 │
+│ ... (9 Slots insgesamt) │
+│ │
+│ [+ Layout-Typ aendernx] [Speichern] │
+│ │
+│ [Zurueck <] [Speichern & Aktivieren] │
+│ [Speichern] │
+│ [Abbrechen] │
+└─────────────────────────────────────────┘
+```
+
+Fuer `screen_specific_scene`:
+
+```
+┌─────────────────────────────────────────┐
+│ Monitorindividuelle Szenen │
+├─────────────────────────────────────────┤
+│ │
+│ Szene 1: Infowand │
+│ │
+│ Zielgruppe │
+│ ⦿ Gruppe: [Dropdown: wall-all] │
+│ ○ Einzelne Screens: [Checkboxen] │
+│ │
+│ Asset │
+│ [Upload oder URL] │
+│ │
+│ Dauer, Timeout, gueltig_von/bis │
+│ [... wie oben ...] │
+│ │
+│ [+ Weitere Szene hinzufuegen] │
+│ │
+│ [Zurueck <] [Speichern & Aktivieren] │
+└─────────────────────────────────────────┘
+```
+
+## 2. Kampagnen-Verwaltung
+
+Kampagnen sind die operativen Instanzen von Templates.
+
+### Kampagnen-Liste
+
+**Seite:** Admin → Kampagnen
+
+**Anzeige:**
+
+| Name | Template | Aktiv | Zielgruppe | gueltig von | gueltig bis | Betroffene Screens |
+|---|---|---|---|---|---|---|
+| Weihnachten Dekoration | Weihnachtsmotiv 2025 | ✓ | alle | 2025-12-01 | 2025-12-26 | 13 Screens |
+| Schriftzug Januar | Schriftzug Infowand | ✗ | wall-all | 2025-01-06 | 2025-01-31 | 9 Screens |
+
+**Aktionen:**
+
+- "Bearbeiten" — Kampagnen-Eigenschaften aendern
+- "Aktivieren/Deaktivieren" — Toggle sofort
+- "Vorschau" — zeigt betroffene Screens mit Rendering
+- "Duplizieugen" — als neue Kampagne mit anderem Template
+- "Loeschen" — wenn inaktiv und abgelaufen
+
+### Neue Kampagne starten
+
+**Workflow Option 1 — Von Template aus:**
+
+Template-Liste → [Template] → "Aktivieren"
+
+```
+┌─────────────────────────────────────────┐
+│ Kampagne starten: Weihnachtsmotiv 2025 │
+├─────────────────────────────────────────┤
+│ │
+│ Kampagnen-Name │
+│ [ Weihnachten 2025 einfuehrung____ ] │
+│ │
+│ Aktiv ab sofort? │
+│ ⦿ Ja │
+│ ○ Geplant fuer: [Datum/Zeit auswaehlen]│
+│ [ 2025-12-01 ] [ 09:00 ] │
+│ │
+│ Gueltig von │
+│ [ 2025-12-01 ] [ 00:00 ] │
+│ │
+│ Gueltig bis │
+│ [ 2025-12-26 ] [ 23:59 ] │
+│ │
+│ Prioritaet (gegenueber Playlist) │
+│ [1 (hoehere Werte sind wichtiger)] ___ │
+│ │
+│ Auto-Deaktivierung bei Ablauf? │
+│ ⦿ Ja │
+│ ○ Nein (Kampagne bleibt inaktiv) │
+│ │
+│ [Kampagne starten] [Abbrechen] │
+└─────────────────────────────────────────┘
+```
+
+**Workflow Option 2 — Neue Kampagne ohne Template:**
+
+Admin → Kampagnen → "+ Neue Kampagne"
+
+```
+[Template auswaehlen] → [Grunddaten] → [Aktivierung]
+```
+
+### Kampagnen-Detailseite
+
+**Anzeige einer laufenden Kampagne:**
+
+```
+Kampagne: Weihnachten 2025 einfuehrung
+Status: AKTIV seit 2025-12-01 09:00
+
+Template: Weihnachtsmotiv 2025 (full_screen_media)
+Zielgruppe: Alle (13 Screens)
+
+Gueltig: 2025-12-01 00:00 bis 2025-12-26 23:59
+Prioritaet: 1
+
+Betroffene Screens:
+┌──────────────────────────────┐
+│ info01 online aktiv │ [Screenshot]
+│ info02 online aktiv │ [Screenshot]
+│ info03 offline ausstehend │
+│ info04 online aktiv │ [Screenshot]
+│ ... (10 weitere) ... │
+└──────────────────────────────┘
+
+Aktionen:
+[Deaktivieren] [Bearbeiten] [Vorschau aendernx]
+
+Aktivierungsverlauf:
+2025-12-01 09:00 — Kampagne gestartet von admin@...
+2025-12-01 09:05 — 9 Screens haben gerendert
+2025-12-01 10:30 — info03 ging offline, Kampagnen-Inhalt wartet auf Rueckkehr
+```
+
+## 3. Verknuepfung zur Prioritaetsregel
+
+Die Regel `campaign > tenant_playlist > fallback` ist:
+
+- **hardcoded** im Player
+- **administrierbar** ueber die Kampagnen-Aktivierung
+- **vorhersagbar** durch klare Doku
+
+### Abbildung im System
+
+```
+Fuer jeden Screen:
+ IF Kampagne fuer diesen Screen aktiv UND gueltig_von <= jetzt <= gueltig_bis
+ THEN Zeige Kampagnen-Inhalt
+ ELSE IF Tenant-Playlist hat gueltige Items
+ THEN Zeige Tenant-Playlist
+ ELSE
+ Zeige Fallback
+```
+
+Diese Logik wird:
+
+1. **Serverseitig** berechnet bei jedem Sync-Request (HTTP `/api/v1/screens/{screenSlug}/playlist`)
+2. **Spielerseitig** nochmals geprueft beim Rendering (fuer Offline-Robustheit)
+
+### Admin-Sichtbarkeit
+
+Die Admin-UI zeigt auf der Seite "Screens" fuer jeden Monitor:
+
+```
+info01
+├── Kampagne (AKTIV bis 2025-12-26)
+│ └── Weihnachten 2025 einfuehrung
+├── Fallback (wird nach Kampagnen-Ablauf gezeigt)
+└── Tenant Playlist
+ ├── Playlist A (Tenant XYZ)
+ │ ├── Bild-1 (gueltig bis 2025-04-01)
+ │ ├── Video-2 (laedt...)
+ │ └── Webseite-3
+ └── Fallback-Verzeichnis
+```
+
+Diese View zeigt, was der Screen **aktuell gerade zeigt** und warum.
+
+## 4. Datenmodell
+
+### Tabelle `templates`
+
+```sql
+CREATE TABLE templates (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ slug TEXT NOT NULL UNIQUE,
+ name TEXT NOT NULL,
+ description TEXT,
+ template_type TEXT NOT NULL CHECK (template_type IN ('message_wall', 'full_screen_media', 'screen_specific_scene')),
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ created_by_user_id TEXT NOT NULL,
+
+ -- Serializierte Konfiguration (JSON)
+ config JSONB NOT NULL DEFAULT '{}'
+ -- Beispiele:
+ -- {
+ -- "target_mode": "all_screens" | "group" | "specific_screens",
+ -- "target_group": "wall-all" (wenn target_mode = "group"),
+ -- "target_screen_ids": ["..."] (wenn target_mode = "specific_screens"),
+ -- "scenes": [
+ -- {
+ -- "media_type": "image|video|pdf|webpage|html",
+ -- "asset_id": "...",
+ -- "portrait_asset_id": "..." (optional),
+ -- "landscape_asset_id": "..." (optional),
+ -- "duration_sec": 10,
+ -- "load_timeout_sec": 10,
+ -- "valid_from": "2025-03-25T00:00:00Z",
+ -- "valid_until": "2025-04-01T23:59:59Z"
+ -- }
+ -- ]
+ -- }
+);
+```
+
+### Tabelle `campaigns`
+
+```sql
+CREATE TABLE campaigns (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ name TEXT NOT NULL,
+ template_id UUID NOT NULL REFERENCES templates(id),
+ active BOOLEAN NOT NULL DEFAULT false,
+ priority INT NOT NULL DEFAULT 1,
+ valid_from TIMESTAMPTZ NOT NULL,
+ valid_until TIMESTAMPTZ,
+ auto_deactivate BOOLEAN NOT NULL DEFAULT true,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ created_by_user_id TEXT NOT NULL,
+
+ -- ueberschreiben/erweitern Template-Zielgruppe (optional)
+ target_mode TEXT CHECK (target_mode IN ('template', 'all_screens', 'group', 'specific_screens')),
+ target_group TEXT,
+ target_screen_ids UUID[] DEFAULT '{}'::uuid[]
+);
+```
+
+### Tabelle `campaign_screen_assignments` (generiert)
+
+Diese Tabelle wird **serverseitig** generiert/gepflegt, wenn eine Kampagne aktiv wird.
+
+Sie expandiert Gruppen in konkrete Screen-IDs:
+
+```sql
+CREATE TABLE campaign_screen_assignments (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+ campaign_id UUID NOT NULL REFERENCES campaigns(id) ON DELETE CASCADE,
+ screen_id UUID NOT NULL REFERENCES screens(id) ON DELETE CASCADE,
+ assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ UNIQUE(campaign_id, screen_id)
+);
+```
+
+**Logik:**
+
+```
+IF campaign.target_mode = 'template'
+ THEN Fuelle campaign_screen_assignments aus template.config.target_screen_ids
+ELSE IF campaign.target_mode = 'group'
+ THEN Fuelle campaign_screen_assignments aus allen Screens in campaign.target_group
+ELSE IF campaign.target_mode = 'specific_screens'
+ THEN Fuelle campaign_screen_assignments aus campaign.target_screen_ids
+ELSE
+ (alle Screens)
+```
+
+## 5. Praxis-Beispiele
+
+### Beispiel 1 — Weihnachtsplakatierung (full_screen_media)
+
+**Szenario:**
+
+Admin will ab 01.12.2025 fuer 4 Wochen ein rotes Weihnachtsmotiv auf allen Screens zeigen.
+
+**Schritte:**
+
+1. Admin → Templates → "+ Neues Template"
+ - Name: `Weihnachtsmotiv 2025`
+ - Typ: `full_screen_media`
+ - Zielgruppe: `Alle Screens`
+
+2. Szene hinzufuegen:
+ - Bild hochladen (passend fuer Portrait und Landscape)
+ - Dauer: 10 Sekunden
+
+3. Speichern → Editor zeigt Draft mit Vorschau
+
+4. Admin → Templates → [Weihnachtsmotiv 2025] → "Aktivieren"
+ - Kampagnen-Name: `Weihnachten 2025 globale Dekoration`
+ - Gueltig von: 2025-12-01
+ - Gueltig bis: 2025-12-26
+ - Aktiv ab: sofort
+
+5. Kampagne speichern → Sofort sichtbar auf allen Screens
+
+### Beispiel 2 — Schriftzug ueber die Infowand (message_wall)
+
+**Szenario:**
+
+Admin hat eine neue `message_wall`-Gruppe "wall-all" mit 9 Screens. Er will ein riesiges rotes Schriftzug-Motiv aufteilen und auf allen 9 Screens verteilen.
+
+**Schritte:**
+
+1. Admin → Templates → "+ Neues Template"
+ - Name: `Rotes Schriftzug auf Infowand`
+ - Typ: `message_wall`
+ - Zielgruppe: `Gruppe: wall-all`
+
+2. Layout waehlen: `3x3-Grid` (passt zu 9 Screens)
+
+3. Gesamte Grafik hochladen (oder als Text eingeben)
+
+4. Slot-Zuordnung:
+ - System zeigt interaktive 3x3-Vorschau
+ - Admin tuen: "Slot 1 → info01", "Slot 2 → info02", ...
+ - System generiert automatisch die Crop-Regionen
+
+5. Speichern + Aktivieren
+ - Jeder Screen zeigt seinen Ausschnitt
+
+### Beispiel 3 — Deaktivierung und Fallback
+
+**Szenario:**
+
+Kampagne laueft seit 2 Wochen. Admin will sie sofort stoppen, damit Screens auf ihre normalen Playlists zurueckfallen.
+
+**Aktion:**
+
+Admin → Kampagnen → [Kampagne] → "Deaktivieren"
+
+**Folge:**
+
+- Server setzt `campaigns.active = false`
+- Bei naechstem Sync ladet jeder Player wieder die Tenant-Playlist
+- Fallback-Verzeichnis wird nur noch angezeigt, wenn tenantbezogene Playlist leer ist
+
+## 6. Zusammenfassung
+
+Der Template-Editor:
+
+- **ist zwei-stufig** — Template-Verwaltung + Kampagnen-Aktivierung
+- **ist intuitiv** — Multi-Step-Formulare mit Vorschauen
+- **unterstützt alle Template-Typen** — full_screen, message_wall, screen_specific
+- **haelt die Prioritaetsregel transparent** — Admin sieht, welche Kampagne welche Screens uebersteuert
+- **ist zukunftssicher** — Datenmodell skaliert mit neuen Template-Typen
diff --git a/docs/WATCHDOG-KONZEPT.md b/docs/WATCHDOG-KONZEPT.md
new file mode 100644
index 0000000..7d92d89
--- /dev/null
+++ b/docs/WATCHDOG-KONZEPT.md
@@ -0,0 +1,305 @@
+# Info-Board Neu - Watchdog-Konzept
+
+## Ziel
+
+Der Watchdog ueberwacht die kritischen Komponenten des Players und sorgt dafuer, dass der Display-Betrieb bei Abstuerzen oder Verhaengungen automatisch wiederhergestellt wird.
+
+Die Ueberwachung erfolgt auf zwei Ebenen:
+
+1. **Browser-Watchdog** — Ueberwachung von Chromium
+2. **Agent-Watchdog** — Ueberwachung des Player-Agents
+
+## Grundprinzipien
+
+- Watchdogs sind extern und unabhaengig von den ueberwachten Prozessen
+- Erkennung erfolgt aktiv durch Health-Checks, nicht durch Liveness-Pings
+- Restart-Strategien sind progressiv und vermeiden Restart-Schleifen
+- Logging ist strukturiert und fuer Admin-Diagnosen aussagekraeftig
+
+## Browser-Watchdog (Chromium-Ueberwachung)
+
+### Aufgaben
+
+Der Browser-Watchdog sorgt dafuer, dass:
+
+- Chromium staendig laeuft und antwortet
+- der Renderer nicht in einer Endlosschleife haengt
+- Rendering-Fehler nicht zu permanenten Schwarzbildern fuehren
+- bei Chromium-Crash oder Verhaengung schnell neugestartet wird
+
+### Health-Check-Verfahren
+
+Der Watchdog fuehrt regelmaeßig folgende Checks durch:
+
+#### 1. Prozess-Check
+
+```
+Existiert der Chromium-Prozess noch?
+ - lsof oder ps-Abfrage auf die PID
+ - Timeout: sofort bei fehlender PID
+```
+
+#### 2. HTTP-Health-Check auf localhost
+
+```
+GET http://localhost:8081/health
+Timeout: 5 Sekunden
+Erwartet: 200 OK und JSON-Antwort {status: "ok"}
+```
+
+Die `player-ui` muss einen einfachen `/health`-Endpunkt bereitstellen, der schnell antwortet, auch wenn die Playlist gerade verarbeitet wird.
+
+#### 3. Rendering-Verifizierung (optional, Phase 2)
+
+```
+Screenshot-basiert erkennen, ob der Browser:
+ - Fehlerseite zeigt
+ - komplett schwarz ist (mehr als 95% schwarze Pixel)
+ - seit mehreren Minuten denselben Content zeigt, obwohl ein Wechsel erwartet wurde
+```
+
+Diese Methode ist fuer v1 optional, wird aber fuer spaetere Verhaengungserkennung eingeplant.
+
+### Ueberwachungs-Intervall
+
+- Health-Check alle **30 Sekunden**
+- Bei Fehler: sofort Neustart pruefen (kein Warten auf naechsten Zyklus)
+
+### Restart-Strategie
+
+#### Strategie: Exponentieller Backoff mit Maximum
+
+```
+Fehlerfall:
+ Fehler 1: Sofort neustart (Wait 0s)
+ Fehler 2: Warte 2s, versuche Restart
+ Fehler 3: Warte 5s, versuche Restart
+ Fehler 4: Warte 10s, versuche Restart
+ Fehler 5+: Warte 30s, versuche Restart
+ Nach 10 aufeinanderfolgende Fehler ohne erfolgreicher Recovery:
+ - Alert an Admin (via Server-Status)
+ - Overlay auf "Error" setzen
+ - Watchdog-Loop verlangsamen auf 5 Min Intervall
+```
+
+#### Erfolg-Kriterium
+
+Wenn der Health-Check 3x hintereinander erfolgreich ist:
+
+- Backoff-Zaehler zuruecksetzen auf 0
+- naechstes Fehler wieder mit sofort-Restart starten
+
+### Logging
+
+Jeder Watchdog-Ereignis wird protokolliert:
+
+```json
+{
+ "ts": "2025-03-23T14:22:15Z",
+ "component": "browser_watchdog",
+ "event": "restart",
+ "reason": "health_check_timeout",
+ "attempt": 2,
+ "next_retry_in_ms": 5000,
+ "details": {
+ "pid_before": 1234,
+ "pid_after": 1245,
+ "http_status_before": 0
+ }
+}
+```
+
+Logging-Ziele:
+
+- strukturiert auf stdout/stderr (JSON)
+- lokal in `/var/log/signage/watchdog.log` mit Rotation
+
+## Agent-Watchdog (systemd-Integration)
+
+### Aufgaben
+
+Der Agent-Watchdog (bzw. systemd-Unit) sorgt dafuer, dass:
+
+- der Player-Agent staendig laeuft
+- nach Crash oder gewolltem Stop schnell neugestartet wird
+- Restart-Grenzen ein Verhaengungsloop verhindern
+
+### systemd-Konfiguration
+
+```ini
+[Service]
+Type=simple
+ExecStart=/usr/local/bin/player-agent
+Restart=always
+RestartSec=5
+StartLimitInterval=300
+StartLimitBurst=10
+StandardOutput=journal
+StandardError=journal
+```
+
+**Bedeutung:**
+
+- `Restart=always` — Neustart bei jedem Exit (unabhaengig vom Exit-Code)
+- `RestartSec=5` — Warte 5 Sekunden vor Neustart
+- `StartLimitInterval=300` — Zaehle Restarts in einem 300s-Fenster
+- `StartLimitBurst=10` — Mehr als 10 Restarts in 300s fuehrt zu systemd-Stop
+
+Wenn `StartLimitBurst` erreicht wird:
+
+- systemd laesst den Service stehen
+- Admin wird informiert (Status-API setzt `agent_watchdog_failed`)
+- manueller Eingriff oder Admin-Kommando noetig
+
+### Health-Check durch Agent selbst
+
+Der Agent sollte intern:
+
+- Broker-Verbindung regelmaeßig pruefen
+- Server-Sync-Status tracken
+- bei kritischen Innenfehlern nicht einfach weiterlaeufen
+
+Wenn sich der Agent selbst als unheilbar beschaedigt sieht:
+
+- strukturiert mit Exit-Code `1` beenden (systemd startet neu)
+- nicht mit `exit(0)` haengend beenden
+
+## Verhaeltnis zu systemd
+
+### Architektur-Entscheidung
+
+`systemd` uebernimmt die Prozess-Wiederbelebung fuer den Agent.
+
+Der Browser-Watchdog ist ein **separater, von systemd unabhaengiger Prozess**, weil:
+
+- Chromium staendiger Ueberwachung bedarf (Health-Checks im 30s-Rhythmus)
+- ein Systemd-Watchdog-Timer zu unverzeihlich waere (nur on/off, nicht granular)
+- der Browser-Watchdog auch die Systemd-Unit selbst monitoren kann (Defensive Architektur)
+
+### Optional: systemd WatchdogSec
+
+Fuer den Agent ist es sinnvoll, auch systemd's Watchdog-Timer zu nutzen:
+
+```ini
+[Service]
+WatchdogSec=30
+ExecStart=/usr/local/bin/player-agent
+```
+
+Der Agent muesste dann periodisch `systemd-notify --ready` senden.
+
+Das ist **optional fuer v1**, wird aber fuer spaetere Robustheit eingeplant.
+
+## Integration mit Player-Setup
+
+### Verzeichnisstruktur
+
+```
+/usr/local/bin/
+ player-agent — Go-Binary
+ browser-watchdog — Go-Binary oder Shell-Script
+
+/etc/systemd/system/
+ signage-agent.service
+ signage-browser-watchdog.service
+
+/var/lib/signage/
+ watchdog-state.json — letzter Zustand, Backoff-Counter
+
+/var/log/signage/
+ watchdog.log — strukturiertes Logging
+```
+
+### Startup-Reihenfolge
+
+1. Basis-System bootet, X11 startet
+2. `signage-agent.service` startet (systemd)
+3. Agent startet, prueft Konfiguration, startet `player-ui` HTTP-Server
+4. `signage-browser-watchdog.service` startet (systemd)
+5. Watchdog wartet initial 10s, bevor erste Checks starten
+6. Agent laesst Chromium starten
+7. Watchdog beginnt Health-Checks
+
+Dieses Ordering verhindert, dass der Watchdog versucht, den Browser zu uberwachen, bevor der Agent bereit ist.
+
+### Stopp-Reihenfolge bei Shutdown
+
+1. systemd sendet SIGTERM an Agent und Browser-Watchdog
+2. Watchdog: beendet sich, versucht nicht zu restarten
+3. Agent: beendet sich, laedt Chromium herunter
+4. Systemd wartet auf Completion
+
+## Fehlerklassifizierung und Admin-Reporting
+
+### Fehlerklassen
+
+| Fehlerklasse | Symptom | Watchdog-Aktion | Admin-Alert |
+|---|---|---|---|
+| Prozess-Crash | PID weg | Sofort neustart | Nach 3x Fehlschlag |
+| Health-Check-Timeout | HTTP timeout | Backoff-Restart | Nach 5x Fehlschlag |
+| Rendering-Fehler | Browser zeigt Fehlerseite | Neustart | Sofort sichtbar |
+| Backoff-Maximum | 10+ Fehler in 5min | Stoppen, Alert | Sofort |
+| Agent-Unhealthy | Server-Sync fehlgeschlagen | Systemd-Neustart | Nach 3x Sync-Fehler |
+
+### Admin-Oberflaeche
+
+Status-Page und Admin-Dashboard zeigen:
+
+```json
+{
+ "screen_id": "info01",
+ "browser_status": {
+ "pid": 1234,
+ "health": "ok",
+ "last_check_at": "2025-03-23T14:25:00Z",
+ "restart_count_5m": 0,
+ "last_error": null
+ },
+ "agent_status": {
+ "pid": 567,
+ "uptime_seconds": 3600,
+ "sync_status": "ok",
+ "last_sync_at": "2025-03-23T14:24:55Z",
+ "systemd_restart_count": 0
+ },
+ "watchdog_alert": null
+}
+```
+
+## Konfigurierbare Parameter
+
+In `/etc/signage/config.yml` oder Umgebungsvariablen:
+
+```yaml
+watchdog:
+ browser:
+ check_interval_sec: 30
+ health_check_timeout_sec: 5
+ restart_backoff_steps: [0, 2, 5, 10, 30] # Sekunden
+ max_consecutive_errors: 10
+ error_window_sec: 300
+ agent:
+ systemd_unit: "signage-agent.service"
+ healthcheck_timeout_sec: 10
+```
+
+## Testing und Validierung
+
+Testfaelle fuer den Watchdog:
+
+1. Chromium manuell toeten (`kill -9 PID`) — sollte innerhalb 30s neustartet werden
+2. Player-Agent starten/stoppen — systemd sollte neustart triggern
+3. Player-UI HTTP-Server abschalten — Browser-Watchdog sollte neustarten
+4. Schnelle aufeinanderfolgende Crashes — Backoff-Exponentialfunktion pruefen
+5. Admin-Kommando `restart_player` — geordneter Neustart, dann Restart-Counter nicht erhoeht
+6. Watchdog-Logs auf Struktur und Vollstaendigkeit pruefen
+
+## Zusammenfassung
+
+Der Watchdog-Ansatz ist:
+
+- **Transparent** — klare Logging und Admin-Sichtbarkeit
+- **Progressive** — Backoff statt Restart-Schleife
+- **Defensiv** — mehrere Erkennungsmethoden (Prozess, HTTP, optional Rendering)
+- **Integriert** — arbeitet mit systemd zusammen, nicht gegen es
+- **Skalierbar** — Verfahren gilt fuer alle Player unabhaengig von Standort oder Netzwerk
diff --git a/player/agent/internal/app/app.go b/player/agent/internal/app/app.go
index 45f7e68..4a5a661 100644
--- a/player/agent/internal/app/app.go
+++ b/player/agent/internal/app/app.go
@@ -16,6 +16,7 @@ import (
"git.az-it.net/az/morz-infoboard/player/agent/internal/mqttheartbeat"
"git.az-it.net/az/morz-infoboard/player/agent/internal/mqttsubscriber"
"git.az-it.net/az/morz-infoboard/player/agent/internal/playerserver"
+ "git.az-it.net/az/morz-infoboard/player/agent/internal/screenshot"
"git.az-it.net/az/morz-infoboard/player/agent/internal/statusreporter"
)
@@ -222,6 +223,14 @@ func (a *App) Run(ctx context.Context) error {
// Start polling the backend for playlist updates (60 s fallback + MQTT trigger).
go a.pollPlaylist(ctx)
+ // Phase 6: Periodische Screenshot-Erzeugung, wenn konfiguriert.
+ if a.Config.ScreenshotEvery > 0 {
+ ss := screenshot.New(a.Config.ScreenID, a.Config.ServerBaseURL, a.Config.ScreenshotEvery, a.logger)
+ go ss.Run(ctx)
+ a.logger.Printf("event=screenshot_enabled screen_id=%s interval_seconds=%d",
+ a.Config.ScreenID, a.Config.ScreenshotEvery)
+ }
+
a.emitHeartbeat()
a.mu.Lock()
a.status = StatusRunning
@@ -272,6 +281,10 @@ func (a *App) registerScreen(ctx context.Context) {
return
}
req.Header.Set("Content-Type", "application/json")
+ // K6: Register-Secret mitsenden, wenn konfiguriert.
+ if a.Config.RegisterSecret != "" {
+ req.Header.Set("X-Register-Secret", a.Config.RegisterSecret)
+ }
resp, err := http.DefaultClient.Do(req)
if err == nil {
diff --git a/player/agent/internal/config/config.go b/player/agent/internal/config/config.go
index a28b908..04b73e2 100644
--- a/player/agent/internal/config/config.go
+++ b/player/agent/internal/config/config.go
@@ -23,6 +23,13 @@ type Config struct {
PlayerListenAddr string `json:"player_listen_addr"`
// PlayerContentURL is a fallback URL shown when no playlist is available from the server.
PlayerContentURL string `json:"player_content_url"`
+ // RegisterSecret ist das Pre-Shared-Secret für POST /api/v1/screens/register (K6).
+ // Muss mit MORZ_INFOBOARD_REGISTER_SECRET auf dem Server übereinstimmen.
+ // Wenn leer, wird kein Header gesendet (kompatibel mit Servern ohne Secret).
+ RegisterSecret string `json:"register_secret"`
+ // ScreenshotEvery gibt das Intervall in Sekunden für periodische Screenshots an (Phase 6).
+ // 0 oder negativ = Screenshots deaktiviert.
+ ScreenshotEvery int `json:"screenshot_every_seconds"`
}
const defaultConfigPath = "/etc/signage/config.json"
@@ -90,6 +97,12 @@ func overrideFromEnv(cfg *Config) {
cfg.ScreenName = getenv("MORZ_INFOBOARD_SCREEN_NAME", cfg.ScreenName)
cfg.ScreenOrientation = getenv("MORZ_INFOBOARD_SCREEN_ORIENTATION", cfg.ScreenOrientation)
cfg.PlayerContentURL = getenv("MORZ_INFOBOARD_PLAYER_CONTENT_URL", cfg.PlayerContentURL)
+ cfg.RegisterSecret = getenv("MORZ_INFOBOARD_REGISTER_SECRET", cfg.RegisterSecret)
+ if value := getenv("MORZ_INFOBOARD_SCREENSHOT_EVERY", ""); value != "" {
+ var parsed int
+ _, _ = fmt.Sscanf(value, "%d", &parsed)
+ cfg.ScreenshotEvery = parsed
+ }
if value := getenv("MORZ_INFOBOARD_STATUS_REPORT_EVERY", ""); value != "" {
var parsed int
_, _ = fmt.Sscanf(value, "%d", &parsed)
diff --git a/player/agent/internal/playerserver/assets/pdf.min.js b/player/agent/internal/playerserver/assets/pdf.min.js
new file mode 100644
index 0000000..c31b6ab
--- /dev/null
+++ b/player/agent/internal/playerserver/assets/pdf.min.js
@@ -0,0 +1,22 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page
+ *
+ * Copyright 2023 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @licend The above is the entire license notice for the
+ * JavaScript code in this page
+ */
+!function webpackUniversalModuleDefinition(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=t.pdfjsLib=e():"function"==typeof define&&define.amd?define("pdfjs-dist/build/pdf",[],(()=>t.pdfjsLib=e())):"object"==typeof exports?exports["pdfjs-dist/build/pdf"]=t.pdfjsLib=e():t["pdfjs-dist/build/pdf"]=t.pdfjsLib=e()}(globalThis,(()=>(()=>{"use strict";var __webpack_modules__=[,(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0});e.VerbosityLevel=e.Util=e.UnknownErrorException=e.UnexpectedResponseException=e.TextRenderingMode=e.RenderingIntentFlag=e.PromiseCapability=e.PermissionFlag=e.PasswordResponses=e.PasswordException=e.PageActionEventType=e.OPS=e.MissingPDFException=e.MAX_IMAGE_SIZE_TO_CACHE=e.LINE_FACTOR=e.LINE_DESCENT_FACTOR=e.InvalidPDFException=e.ImageKind=e.IDENTITY_MATRIX=e.FormatError=e.FeatureTest=e.FONT_IDENTITY_MATRIX=e.DocumentActionEventType=e.CMapCompressionType=e.BaseException=e.BASELINE_FACTOR=e.AnnotationType=e.AnnotationReplyType=e.AnnotationPrefix=e.AnnotationMode=e.AnnotationFlag=e.AnnotationFieldFlag=e.AnnotationEditorType=e.AnnotationEditorPrefix=e.AnnotationEditorParamsType=e.AnnotationBorderStyleType=e.AnnotationActionEventType=e.AbortException=void 0;e.assert=function assert(t,e){t||unreachable(e)};e.bytesToString=bytesToString;e.createValidAbsoluteUrl=function createValidAbsoluteUrl(t,e=null,i=null){if(!t)return null;try{if(i&&"string"==typeof t){if(i.addDefaultProtocol&&t.startsWith("www.")){const e=t.match(/\./g);e?.length>=2&&(t=`http://${t}`)}if(i.tryConvertEncoding)try{t=stringToUTF8String(t)}catch{}}const s=e?new URL(t,e):new URL(t);if(function _isValidProtocol(t){switch(t?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return!0;default:return!1}}(s))return s}catch{}return null};e.getModificationDate=function getModificationDate(t=new Date){return[t.getUTCFullYear().toString(),(t.getUTCMonth()+1).toString().padStart(2,"0"),t.getUTCDate().toString().padStart(2,"0"),t.getUTCHours().toString().padStart(2,"0"),t.getUTCMinutes().toString().padStart(2,"0"),t.getUTCSeconds().toString().padStart(2,"0")].join("")};e.getUuid=function getUuid(){if("undefined"!=typeof crypto&&"function"==typeof crypto?.randomUUID)return crypto.randomUUID();const t=new Uint8Array(32);if("undefined"!=typeof crypto&&"function"==typeof crypto?.getRandomValues)crypto.getRandomValues(t);else for(let e=0;e<32;e++)t[e]=Math.floor(255*Math.random());return bytesToString(t)};e.getVerbosityLevel=function getVerbosityLevel(){return n};e.info=function info(t){n>=s.INFOS&&console.log(`Info: ${t}`)};e.isArrayBuffer=function isArrayBuffer(t){return"object"==typeof t&&void 0!==t?.byteLength};e.isArrayEqual=function isArrayEqual(t,e){if(t.length!==e.length)return!1;for(let i=0,s=t.length;ie?e.normalize("NFKC"):h.get(i)))};e.objectFromMap=function objectFromMap(t){const e=Object.create(null);for(const[i,s]of t)e[i]=s;return e};e.objectSize=function objectSize(t){return Object.keys(t).length};e.setVerbosityLevel=function setVerbosityLevel(t){Number.isInteger(t)&&(n=t)};e.shadow=shadow;e.string32=function string32(t){return String.fromCharCode(t>>24&255,t>>16&255,t>>8&255,255&t)};e.stringToBytes=stringToBytes;e.stringToPDFString=function stringToPDFString(t){if(t[0]>="ï"){let e;"þ"===t[0]&&"ÿ"===t[1]?e="utf-16be":"ÿ"===t[0]&&"þ"===t[1]?e="utf-16le":"ï"===t[0]&&"»"===t[1]&&"¿"===t[2]&&(e="utf-8");if(e)try{const i=new TextDecoder(e,{fatal:!0}),s=stringToBytes(t);return i.decode(s)}catch(t){warn(`stringToPDFString: "${t}".`)}}const e=[];for(let i=0,s=t.length;i=s.WARNINGS&&console.log(`Warning: ${t}`)}function unreachable(t){throw new Error(t)}function shadow(t,e,i,s=!1){Object.defineProperty(t,e,{value:i,enumerable:!s,configurable:!0,writable:!1});return i}const a=function BaseExceptionClosure(){function BaseException(t,e){this.constructor===BaseException&&unreachable("Cannot initialize BaseException.");this.message=t;this.name=e}BaseException.prototype=new Error;BaseException.constructor=BaseException;return BaseException}();e.BaseException=a;e.PasswordException=class PasswordException extends a{constructor(t,e){super(t,"PasswordException");this.code=e}};e.UnknownErrorException=class UnknownErrorException extends a{constructor(t,e){super(t,"UnknownErrorException");this.details=e}};e.InvalidPDFException=class InvalidPDFException extends a{constructor(t){super(t,"InvalidPDFException")}};e.MissingPDFException=class MissingPDFException extends a{constructor(t){super(t,"MissingPDFException")}};e.UnexpectedResponseException=class UnexpectedResponseException extends a{constructor(t,e){super(t,"UnexpectedResponseException");this.status=e}};e.FormatError=class FormatError extends a{constructor(t){super(t,"FormatError")}};e.AbortException=class AbortException extends a{constructor(t){super(t,"AbortException")}};function bytesToString(t){"object"==typeof t&&void 0!==t?.length||unreachable("Invalid argument for bytesToString");const e=t.length,i=8192;if(et.toString(16).padStart(2,"0")));e.Util=class Util{static makeHexColor(t,e,i){return`#${r[t]}${r[e]}${r[i]}`}static scaleMinMax(t,e){let i;if(t[0]){if(t[0]<0){i=e[0];e[0]=e[1];e[1]=i}e[0]*=t[0];e[1]*=t[0];if(t[3]<0){i=e[2];e[2]=e[3];e[3]=i}e[2]*=t[3];e[3]*=t[3]}else{i=e[0];e[0]=e[2];e[2]=i;i=e[1];e[1]=e[3];e[3]=i;if(t[1]<0){i=e[2];e[2]=e[3];e[3]=i}e[2]*=t[1];e[3]*=t[1];if(t[2]<0){i=e[0];e[0]=e[1];e[1]=i}e[0]*=t[2];e[1]*=t[2]}e[0]+=t[4];e[1]+=t[4];e[2]+=t[5];e[3]+=t[5]}static transform(t,e){return[t[0]*e[0]+t[2]*e[1],t[1]*e[0]+t[3]*e[1],t[0]*e[2]+t[2]*e[3],t[1]*e[2]+t[3]*e[3],t[0]*e[4]+t[2]*e[5]+t[4],t[1]*e[4]+t[3]*e[5]+t[5]]}static applyTransform(t,e){return[t[0]*e[0]+t[1]*e[2]+e[4],t[0]*e[1]+t[1]*e[3]+e[5]]}static applyInverseTransform(t,e){const i=e[0]*e[3]-e[1]*e[2];return[(t[0]*e[3]-t[1]*e[2]+e[2]*e[5]-e[4]*e[3])/i,(-t[0]*e[1]+t[1]*e[0]+e[4]*e[1]-e[5]*e[0])/i]}static getAxialAlignedBoundingBox(t,e){const i=this.applyTransform(t,e),s=this.applyTransform(t.slice(2,4),e),n=this.applyTransform([t[0],t[3]],e),a=this.applyTransform([t[2],t[1]],e);return[Math.min(i[0],s[0],n[0],a[0]),Math.min(i[1],s[1],n[1],a[1]),Math.max(i[0],s[0],n[0],a[0]),Math.max(i[1],s[1],n[1],a[1])]}static inverseTransform(t){const e=t[0]*t[3]-t[1]*t[2];return[t[3]/e,-t[1]/e,-t[2]/e,t[0]/e,(t[2]*t[5]-t[4]*t[3])/e,(t[4]*t[1]-t[5]*t[0])/e]}static singularValueDecompose2dScale(t){const e=[t[0],t[2],t[1],t[3]],i=t[0]*e[0]+t[1]*e[2],s=t[0]*e[1]+t[1]*e[3],n=t[2]*e[0]+t[3]*e[2],a=t[2]*e[1]+t[3]*e[3],r=(i+a)/2,o=Math.sqrt((i+a)**2-4*(i*a-n*s))/2,l=r+o||1,h=r-o||1;return[Math.sqrt(l),Math.sqrt(h)]}static normalizeRect(t){const e=t.slice(0);if(t[0]>t[2]){e[0]=t[2];e[2]=t[0]}if(t[1]>t[3]){e[1]=t[3];e[3]=t[1]}return e}static intersect(t,e){const i=Math.max(Math.min(t[0],t[2]),Math.min(e[0],e[2])),s=Math.min(Math.max(t[0],t[2]),Math.max(e[0],e[2]));if(i>s)return null;const n=Math.max(Math.min(t[1],t[3]),Math.min(e[1],e[3])),a=Math.min(Math.max(t[1],t[3]),Math.max(e[1],e[3]));return n>a?null:[i,n,s,a]}static bezierBoundingBox(t,e,i,s,n,a,r,o){const l=[],h=[[],[]];let c,d,u,p,g,m,f,b;for(let h=0;h<2;++h){if(0===h){d=6*t-12*i+6*n;c=-3*t+9*i-9*n+3*r;u=3*i-3*t}else{d=6*e-12*s+6*a;c=-3*e+9*s-9*a+3*o;u=3*s-3*e}if(Math.abs(c)<1e-12){if(Math.abs(d)<1e-12)continue;p=-u/d;0
{this.resolve=e=>{this.#t=!0;t(e)};this.reject=t=>{this.#t=!0;e(t)}}))}get settled(){return this.#t}};let l=null,h=null;e.AnnotationPrefix="pdfjs_internal_id_"},(__unused_webpack_module,exports,__w_pdfjs_require__)=>{Object.defineProperty(exports,"__esModule",{value:!0});exports.RenderTask=exports.PDFWorkerUtil=exports.PDFWorker=exports.PDFPageProxy=exports.PDFDocumentProxy=exports.PDFDocumentLoadingTask=exports.PDFDataRangeTransport=exports.LoopbackPort=exports.DefaultStandardFontDataFactory=exports.DefaultFilterFactory=exports.DefaultCanvasFactory=exports.DefaultCMapReaderFactory=void 0;Object.defineProperty(exports,"SVGGraphics",{enumerable:!0,get:function(){return _displaySvg.SVGGraphics}});exports.build=void 0;exports.getDocument=getDocument;exports.version=void 0;var _util=__w_pdfjs_require__(1),_annotation_storage=__w_pdfjs_require__(3),_display_utils=__w_pdfjs_require__(6),_font_loader=__w_pdfjs_require__(9),_displayNode_utils=__w_pdfjs_require__(10),_canvas=__w_pdfjs_require__(11),_worker_options=__w_pdfjs_require__(14),_message_handler=__w_pdfjs_require__(15),_metadata=__w_pdfjs_require__(16),_optional_content_config=__w_pdfjs_require__(17),_transport_stream=__w_pdfjs_require__(18),_displayFetch_stream=__w_pdfjs_require__(19),_displayNetwork=__w_pdfjs_require__(22),_displayNode_stream=__w_pdfjs_require__(23),_displaySvg=__w_pdfjs_require__(24),_xfa_text=__w_pdfjs_require__(25);const DEFAULT_RANGE_CHUNK_SIZE=65536,RENDERING_CANCELLED_TIMEOUT=100,DELAYED_CLEANUP_TIMEOUT=5e3,DefaultCanvasFactory=_util.isNodeJS?_displayNode_utils.NodeCanvasFactory:_display_utils.DOMCanvasFactory;exports.DefaultCanvasFactory=DefaultCanvasFactory;const DefaultCMapReaderFactory=_util.isNodeJS?_displayNode_utils.NodeCMapReaderFactory:_display_utils.DOMCMapReaderFactory;exports.DefaultCMapReaderFactory=DefaultCMapReaderFactory;const DefaultFilterFactory=_util.isNodeJS?_displayNode_utils.NodeFilterFactory:_display_utils.DOMFilterFactory;exports.DefaultFilterFactory=DefaultFilterFactory;const DefaultStandardFontDataFactory=_util.isNodeJS?_displayNode_utils.NodeStandardFontDataFactory:_display_utils.DOMStandardFontDataFactory;exports.DefaultStandardFontDataFactory=DefaultStandardFontDataFactory;function getDocument(t){"string"==typeof t||t instanceof URL?t={url:t}:(0,_util.isArrayBuffer)(t)&&(t={data:t});if("object"!=typeof t)throw new Error("Invalid parameter in getDocument, need parameter object.");if(!t.url&&!t.data&&!t.range)throw new Error("Invalid parameter object: need either .data, .range or .url");const e=new PDFDocumentLoadingTask,{docId:i}=e,s=t.url?getUrlProp(t.url):null,n=t.data?getDataProp(t.data):null,a=t.httpHeaders||null,r=!0===t.withCredentials,o=t.password??null,l=t.range instanceof PDFDataRangeTransport?t.range:null,h=Number.isInteger(t.rangeChunkSize)&&t.rangeChunkSize>0?t.rangeChunkSize:DEFAULT_RANGE_CHUNK_SIZE;let c=t.worker instanceof PDFWorker?t.worker:null;const d=t.verbosity,u="string"!=typeof t.docBaseUrl||(0,_display_utils.isDataScheme)(t.docBaseUrl)?null:t.docBaseUrl,p="string"==typeof t.cMapUrl?t.cMapUrl:null,g=!1!==t.cMapPacked,m=t.CMapReaderFactory||DefaultCMapReaderFactory,f="string"==typeof t.standardFontDataUrl?t.standardFontDataUrl:null,b=t.StandardFontDataFactory||DefaultStandardFontDataFactory,A=!0!==t.stopAtErrors,_=Number.isInteger(t.maxImageSize)&&t.maxImageSize>-1?t.maxImageSize:-1,v=!1!==t.isEvalSupported,y="boolean"==typeof t.isOffscreenCanvasSupported?t.isOffscreenCanvasSupported:!_util.isNodeJS,S=Number.isInteger(t.canvasMaxAreaInBytes)?t.canvasMaxAreaInBytes:-1,E="boolean"==typeof t.disableFontFace?t.disableFontFace:_util.isNodeJS,x=!0===t.fontExtraProperties,w=!0===t.enableXfa,C=t.ownerDocument||globalThis.document,T=!0===t.disableRange,P=!0===t.disableStream,M=!0===t.disableAutoFetch,k=!0===t.pdfBug,F=l?l.length:t.length??NaN,R="boolean"==typeof t.useSystemFonts?t.useSystemFonts:!_util.isNodeJS&&!E,D="boolean"==typeof t.useWorkerFetch?t.useWorkerFetch:m===_display_utils.DOMCMapReaderFactory&&b===_display_utils.DOMStandardFontDataFactory&&p&&f&&(0,_display_utils.isValidFetchUrl)(p,document.baseURI)&&(0,_display_utils.isValidFetchUrl)(f,document.baseURI),I=t.canvasFactory||new DefaultCanvasFactory({ownerDocument:C}),L=t.filterFactory||new DefaultFilterFactory({docId:i,ownerDocument:C});(0,_util.setVerbosityLevel)(d);const O={canvasFactory:I,filterFactory:L};if(!D){O.cMapReaderFactory=new m({baseUrl:p,isCompressed:g});O.standardFontDataFactory=new b({baseUrl:f})}if(!c){const t={verbosity:d,port:_worker_options.GlobalWorkerOptions.workerPort};c=t.port?PDFWorker.fromPort(t):new PDFWorker(t);e._worker=c}const N={docId:i,apiVersion:"3.11.174",data:n,password:o,disableAutoFetch:M,rangeChunkSize:h,length:F,docBaseUrl:u,enableXfa:w,evaluatorOptions:{maxImageSize:_,disableFontFace:E,ignoreErrors:A,isEvalSupported:v,isOffscreenCanvasSupported:y,canvasMaxAreaInBytes:S,fontExtraProperties:x,useSystemFonts:R,cMapUrl:D?p:null,standardFontDataUrl:D?f:null}},B={ignoreErrors:A,isEvalSupported:v,disableFontFace:E,fontExtraProperties:x,enableXfa:w,ownerDocument:C,disableAutoFetch:M,pdfBug:k,styleElement:null};c.promise.then((function(){if(e.destroyed)throw new Error("Loading aborted");const t=_fetchDocument(c,N),o=new Promise((function(t){let e;if(l)e=new _transport_stream.PDFDataTransportStream({length:F,initialData:l.initialData,progressiveDone:l.progressiveDone,contentDispositionFilename:l.contentDispositionFilename,disableRange:T,disableStream:P},l);else if(!n){e=(t=>_util.isNodeJS?new _displayNode_stream.PDFNodeStream(t):(0,_display_utils.isValidFetchUrl)(t.url)?new _displayFetch_stream.PDFFetchStream(t):new _displayNetwork.PDFNetworkStream(t))({url:s,length:F,httpHeaders:a,withCredentials:r,rangeChunkSize:h,disableRange:T,disableStream:P})}t(e)}));return Promise.all([t,o]).then((function([t,s]){if(e.destroyed)throw new Error("Loading aborted");const n=new _message_handler.MessageHandler(i,t,c.port),a=new WorkerTransport(n,e,s,B,O);e._transport=a;n.send("Ready",null)}))})).catch(e._capability.reject);return e}async function _fetchDocument(t,e){if(t.destroyed)throw new Error("Worker was destroyed");const i=await t.messageHandler.sendWithPromise("GetDocRequest",e,e.data?[e.data.buffer]:null);if(t.destroyed)throw new Error("Worker was destroyed");return i}function getUrlProp(t){if(t instanceof URL)return t.href;try{return new URL(t,window.location).href}catch{if(_util.isNodeJS&&"string"==typeof t)return t}throw new Error("Invalid PDF url data: either string or URL-object is expected in the url property.")}function getDataProp(t){if(_util.isNodeJS&&"undefined"!=typeof Buffer&&t instanceof Buffer)throw new Error("Please provide binary data as `Uint8Array`, rather than `Buffer`.");if(t instanceof Uint8Array&&t.byteLength===t.buffer.byteLength)return t;if("string"==typeof t)return(0,_util.stringToBytes)(t);if("object"==typeof t&&!isNaN(t?.length)||(0,_util.isArrayBuffer)(t))return new Uint8Array(t);throw new Error("Invalid PDF binary data: either TypedArray, string, or array-like object is expected in the data property.")}class PDFDocumentLoadingTask{static#e=0;constructor(){this._capability=new _util.PromiseCapability;this._transport=null;this._worker=null;this.docId="d"+PDFDocumentLoadingTask.#e++;this.destroyed=!1;this.onPassword=null;this.onProgress=null}get promise(){return this._capability.promise}async destroy(){this.destroyed=!0;try{this._worker?.port&&(this._worker._pendingDestroy=!0);await(this._transport?.destroy())}catch(t){this._worker?.port&&delete this._worker._pendingDestroy;throw t}this._transport=null;if(this._worker){this._worker.destroy();this._worker=null}}}exports.PDFDocumentLoadingTask=PDFDocumentLoadingTask;class PDFDataRangeTransport{constructor(t,e,i=!1,s=null){this.length=t;this.initialData=e;this.progressiveDone=i;this.contentDispositionFilename=s;this._rangeListeners=[];this._progressListeners=[];this._progressiveReadListeners=[];this._progressiveDoneListeners=[];this._readyCapability=new _util.PromiseCapability}addRangeListener(t){this._rangeListeners.push(t)}addProgressListener(t){this._progressListeners.push(t)}addProgressiveReadListener(t){this._progressiveReadListeners.push(t)}addProgressiveDoneListener(t){this._progressiveDoneListeners.push(t)}onDataRange(t,e){for(const i of this._rangeListeners)i(t,e)}onDataProgress(t,e){this._readyCapability.promise.then((()=>{for(const i of this._progressListeners)i(t,e)}))}onDataProgressiveRead(t){this._readyCapability.promise.then((()=>{for(const e of this._progressiveReadListeners)e(t)}))}onDataProgressiveDone(){this._readyCapability.promise.then((()=>{for(const t of this._progressiveDoneListeners)t()}))}transportReady(){this._readyCapability.resolve()}requestDataRange(t,e){(0,_util.unreachable)("Abstract method PDFDataRangeTransport.requestDataRange")}abort(){}}exports.PDFDataRangeTransport=PDFDataRangeTransport;class PDFDocumentProxy{constructor(t,e){this._pdfInfo=t;this._transport=e;Object.defineProperty(this,"getJavaScript",{value:()=>{(0,_display_utils.deprecated)("`PDFDocumentProxy.getJavaScript`, please use `PDFDocumentProxy.getJSActions` instead.");return this.getJSActions().then((t=>{if(!t)return t;const e=[];for(const i in t)e.push(...t[i]);return e}))}})}get annotationStorage(){return this._transport.annotationStorage}get filterFactory(){return this._transport.filterFactory}get numPages(){return this._pdfInfo.numPages}get fingerprints(){return this._pdfInfo.fingerprints}get isPureXfa(){return(0,_util.shadow)(this,"isPureXfa",!!this._transport._htmlForXfa)}get allXfaHtml(){return this._transport._htmlForXfa}getPage(t){return this._transport.getPage(t)}getPageIndex(t){return this._transport.getPageIndex(t)}getDestinations(){return this._transport.getDestinations()}getDestination(t){return this._transport.getDestination(t)}getPageLabels(){return this._transport.getPageLabels()}getPageLayout(){return this._transport.getPageLayout()}getPageMode(){return this._transport.getPageMode()}getViewerPreferences(){return this._transport.getViewerPreferences()}getOpenAction(){return this._transport.getOpenAction()}getAttachments(){return this._transport.getAttachments()}getJSActions(){return this._transport.getDocJSActions()}getOutline(){return this._transport.getOutline()}getOptionalContentConfig(){return this._transport.getOptionalContentConfig()}getPermissions(){return this._transport.getPermissions()}getMetadata(){return this._transport.getMetadata()}getMarkInfo(){return this._transport.getMarkInfo()}getData(){return this._transport.getData()}saveDocument(){return this._transport.saveDocument()}getDownloadInfo(){return this._transport.downloadInfoCapability.promise}cleanup(t=!1){return this._transport.startCleanup(t||this.isPureXfa)}destroy(){return this.loadingTask.destroy()}get loadingParams(){return this._transport.loadingParams}get loadingTask(){return this._transport.loadingTask}getFieldObjects(){return this._transport.getFieldObjects()}hasJSActions(){return this._transport.hasJSActions()}getCalculationOrderIds(){return this._transport.getCalculationOrderIds()}}exports.PDFDocumentProxy=PDFDocumentProxy;class PDFPageProxy{#i=null;#s=!1;constructor(t,e,i,s=!1){this._pageIndex=t;this._pageInfo=e;this._transport=i;this._stats=s?new _display_utils.StatTimer:null;this._pdfBug=s;this.commonObjs=i.commonObjs;this.objs=new PDFObjects;this._maybeCleanupAfterRender=!1;this._intentStates=new Map;this.destroyed=!1}get pageNumber(){return this._pageIndex+1}get rotate(){return this._pageInfo.rotate}get ref(){return this._pageInfo.ref}get userUnit(){return this._pageInfo.userUnit}get view(){return this._pageInfo.view}getViewport({scale:t,rotation:e=this.rotate,offsetX:i=0,offsetY:s=0,dontFlip:n=!1}={}){return new _display_utils.PageViewport({viewBox:this.view,scale:t,rotation:e,offsetX:i,offsetY:s,dontFlip:n})}getAnnotations({intent:t="display"}={}){const e=this._transport.getRenderingIntent(t);return this._transport.getAnnotations(this._pageIndex,e.renderingIntent)}getJSActions(){return this._transport.getPageJSActions(this._pageIndex)}get filterFactory(){return this._transport.filterFactory}get isPureXfa(){return(0,_util.shadow)(this,"isPureXfa",!!this._transport._htmlForXfa)}async getXfa(){return this._transport._htmlForXfa?.children[this._pageIndex]||null}render({canvasContext:t,viewport:e,intent:i="display",annotationMode:s=_util.AnnotationMode.ENABLE,transform:n=null,background:a=null,optionalContentConfigPromise:r=null,annotationCanvasMap:o=null,pageColors:l=null,printAnnotationStorage:h=null}){this._stats?.time("Overall");const c=this._transport.getRenderingIntent(i,s,h);this.#s=!1;this.#n();r||(r=this._transport.getOptionalContentConfig());let d=this._intentStates.get(c.cacheKey);if(!d){d=Object.create(null);this._intentStates.set(c.cacheKey,d)}if(d.streamReaderCancelTimeout){clearTimeout(d.streamReaderCancelTimeout);d.streamReaderCancelTimeout=null}const u=!!(c.renderingIntent&_util.RenderingIntentFlag.PRINT);if(!d.displayReadyCapability){d.displayReadyCapability=new _util.PromiseCapability;d.operatorList={fnArray:[],argsArray:[],lastChunk:!1,separateAnnots:null};this._stats?.time("Page Request");this._pumpOperatorList(c)}const complete=t=>{d.renderTasks.delete(p);(this._maybeCleanupAfterRender||u)&&(this.#s=!0);this.#a(!u);if(t){p.capability.reject(t);this._abortOperatorList({intentState:d,reason:t instanceof Error?t:new Error(t)})}else p.capability.resolve();this._stats?.timeEnd("Rendering");this._stats?.timeEnd("Overall")},p=new InternalRenderTask({callback:complete,params:{canvasContext:t,viewport:e,transform:n,background:a},objs:this.objs,commonObjs:this.commonObjs,annotationCanvasMap:o,operatorList:d.operatorList,pageIndex:this._pageIndex,canvasFactory:this._transport.canvasFactory,filterFactory:this._transport.filterFactory,useRequestAnimationFrame:!u,pdfBug:this._pdfBug,pageColors:l});(d.renderTasks||=new Set).add(p);const g=p.task;Promise.all([d.displayReadyCapability.promise,r]).then((([t,e])=>{if(this.destroyed)complete();else{this._stats?.time("Rendering");p.initializeGraphics({transparency:t,optionalContentConfig:e});p.operatorListChanged()}})).catch(complete);return g}getOperatorList({intent:t="display",annotationMode:e=_util.AnnotationMode.ENABLE,printAnnotationStorage:i=null}={}){const s=this._transport.getRenderingIntent(t,e,i,!0);let n,a=this._intentStates.get(s.cacheKey);if(!a){a=Object.create(null);this._intentStates.set(s.cacheKey,a)}if(!a.opListReadCapability){n=Object.create(null);n.operatorListChanged=function operatorListChanged(){if(a.operatorList.lastChunk){a.opListReadCapability.resolve(a.operatorList);a.renderTasks.delete(n)}};a.opListReadCapability=new _util.PromiseCapability;(a.renderTasks||=new Set).add(n);a.operatorList={fnArray:[],argsArray:[],lastChunk:!1,separateAnnots:null};this._stats?.time("Page Request");this._pumpOperatorList(s)}return a.opListReadCapability.promise}streamTextContent({includeMarkedContent:t=!1,disableNormalization:e=!1}={}){return this._transport.messageHandler.sendWithStream("GetTextContent",{pageIndex:this._pageIndex,includeMarkedContent:!0===t,disableNormalization:!0===e},{highWaterMark:100,size:t=>t.items.length})}getTextContent(t={}){if(this._transport._htmlForXfa)return this.getXfa().then((t=>_xfa_text.XfaText.textContent(t)));const e=this.streamTextContent(t);return new Promise((function(t,i){const s=e.getReader(),n={items:[],styles:Object.create(null)};!function pump(){s.read().then((function({value:e,done:i}){if(i)t(n);else{Object.assign(n.styles,e.styles);n.items.push(...e.items);pump()}}),i)}()}))}getStructTree(){return this._transport.getStructTree(this._pageIndex)}_destroy(){this.destroyed=!0;const t=[];for(const e of this._intentStates.values()){this._abortOperatorList({intentState:e,reason:new Error("Page was destroyed."),force:!0});if(!e.opListReadCapability)for(const i of e.renderTasks){t.push(i.completed);i.cancel()}}this.objs.clear();this.#s=!1;this.#n();return Promise.all(t)}cleanup(t=!1){this.#s=!0;const e=this.#a(!1);t&&e&&(this._stats&&=new _display_utils.StatTimer);return e}#a(t=!1){this.#n();if(!this.#s||this.destroyed)return!1;if(t){this.#i=setTimeout((()=>{this.#i=null;this.#a(!1)}),DELAYED_CLEANUP_TIMEOUT);return!1}for(const{renderTasks:t,operatorList:e}of this._intentStates.values())if(t.size>0||!e.lastChunk)return!1;this._intentStates.clear();this.objs.clear();this.#s=!1;return!0}#n(){if(this.#i){clearTimeout(this.#i);this.#i=null}}_startRenderPage(t,e){const i=this._intentStates.get(e);if(i){this._stats?.timeEnd("Page Request");i.displayReadyCapability?.resolve(t)}}_renderPageChunk(t,e){for(let i=0,s=t.length;i 0){const t=1e3*o.measureText(a).width/r*l;if(y {Object.defineProperty(t,"__esModule",{value:!0});t.Parser=t.Linearization=t.Lexer=void 0;var r=a(2),n=a(4),i=a(3),s=a(8),o=a(17),c=a(19),l=a(20),h=a(22),u=a(23),d=a(26),f=a(29),g=a(31),p=a(32),m=a(33);class Parser{constructor({lexer:e,xref:t,allowStreams:a=!1,recoveryMode:r=!1}){this.lexer=e;this.xref=t;this.allowStreams=a;this.recoveryMode=r;this.imageCache=Object.create(null);this._imageId=0;this.refill()}refill(){this.buf1=this.lexer.getObj();this.buf2=this.lexer.getObj()}shift(){if(this.buf2 instanceof n.Cmd&&"ID"===this.buf2.cmd){this.buf1=this.buf2;this.buf2=null}else{this.buf1=this.buf2;this.buf2=this.lexer.getObj()}}tryShift(){try{this.shift();return!0}catch(e){if(e instanceof i.MissingDataException)throw e;return!1}}getObj(e=null){const t=this.buf1;this.shift();if(t instanceof n.Cmd)switch(t.cmd){case"BI":return this.makeInlineImage(e);case"[":const a=[];for(;!(0,n.isCmd)(this.buf1,"]")&&this.buf1!==n.EOF;)a.push(this.getObj(e));if(this.buf1===n.EOF){if(this.recoveryMode)return a;throw new i.ParserEOFException("End of file inside array.")}this.shift();return a;case"<<":const s=new n.Dict(this.xref);for(;!(0,n.isCmd)(this.buf1,">>")&&this.buf1!==n.EOF;){if(!(this.buf1 instanceof n.Name)){(0,r.info)("Malformed dictionary: key must be a name object");this.shift();continue}const t=this.buf1.name;this.shift();if(this.buf1===n.EOF)break;s.set(t,this.getObj(e))}if(this.buf1===n.EOF){if(this.recoveryMode)return s;throw new i.ParserEOFException("End of file inside dictionary.")}if((0,n.isCmd)(this.buf2,"stream"))return this.allowStreams?this.makeStream(s,e):s;this.shift();return s;default:return t}if(Number.isInteger(t)){if(Number.isInteger(this.buf1)&&(0,n.isCmd)(this.buf2,"R")){const e=n.Ref.get(t,this.buf1);this.shift();this.shift();return e}return t}return"string"==typeof t&&e?e.decryptString(t):t}findDefaultInlineStreamEnd(e){const{knownCommands:t}=this.lexer,a=e.pos;let o,c,l=0;for(;-1!==(o=e.getByte());)if(0===l)l=69===o?1:0;else if(1===l)l=73===o?2:0;else if(32===o||10===o||13===o){c=e.pos;const a=e.peekBytes(15),i=a.length;if(0===i)break;for(let e=0;e127))){l=0;break}}if(2!==l)continue;if(!t){(0,r.warn)("findDefaultInlineStreamEnd - `lexer.knownCommands` is undefined.");continue}const h=new Lexer(new s.Stream(a.slice()),t);h._hexStringWarn=()=>{};let u=0;for(;;){const e=h.getObj();if(e===n.EOF){l=0;break}if(e instanceof n.Cmd){const a=t[e.cmd];if(!a){l=0;break}if(a.variableArgs?u<=a.numArgs:u===a.numArgs)break;u=0}else u++}if(2===l)break}else l=0;if(-1===o){(0,r.warn)("findDefaultInlineStreamEnd: Reached the end of the stream without finding a valid EI marker");if(c){(0,r.warn)('... trying to recover by using the last "EI" occurrence.');e.skip(-(e.pos-c))}}let h=4;e.skip(-h);o=e.peekByte();e.skip(h);(0,i.isWhiteSpace)(o)||h--;return e.pos-h-a}findDCTDecodeInlineStreamEnd(e){const t=e.pos;let a,n,i=!1;for(;-1!==(a=e.getByte());)if(255===a){switch(e.getByte()){case 0:break;case 255:e.skip(-1);break;case 217:i=!0;break;case 192:case 193:case 194:case 195:case 197:case 198:case 199:case 201:case 202:case 203:case 205:case 206:case 207:case 196:case 204:case 218:case 219:case 220:case 221:case 222:case 223:case 224:case 225:case 226:case 227:case 228:case 229:case 230:case 231:case 232:case 233:case 234:case 235:case 236:case 237:case 238:case 239:case 254:n=e.getUint16();n>2?e.skip(n-2):e.skip(-2)}if(i)break}const s=e.pos-t;if(-1===a){(0,r.warn)("Inline DCTDecode image stream: EOI marker not found, searching for /EI/ instead.");e.skip(-s);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return s}findASCII85DecodeInlineStreamEnd(e){const t=e.pos;let a;for(;-1!==(a=e.getByte());)if(126===a){const t=e.pos;a=e.peekByte();for(;(0,i.isWhiteSpace)(a);){e.skip();a=e.peekByte()}if(62===a){e.skip();break}if(e.pos>t){const t=e.peekBytes(2);if(69===t[0]&&73===t[1])break}}const n=e.pos-t;if(-1===a){(0,r.warn)("Inline ASCII85Decode image stream: EOD marker not found, searching for /EI/ instead.");e.skip(-n);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return n}findASCIIHexDecodeInlineStreamEnd(e){const t=e.pos;let a;for(;-1!==(a=e.getByte())&&62!==a;);const n=e.pos-t;if(-1===a){(0,r.warn)("Inline ASCIIHexDecode image stream: EOD marker not found, searching for /EI/ instead.");e.skip(-n);return this.findDefaultInlineStreamEnd(e)}this.inlineStreamSkipEI(e);return n}inlineStreamSkipEI(e){let t,a=0;for(;-1!==(t=e.getByte());)if(0===a)a=69===t?1:0;else if(1===a)a=73===t?2:0;else if(2===a)break}makeInlineImage(e){const t=this.lexer,a=t.stream,i=Object.create(null);let s;for(;!(0,n.isCmd)(this.buf1,"ID")&&this.buf1!==n.EOF;){if(!(this.buf1 instanceof n.Name))throw new r.FormatError("Dictionary key must be a name object");const t=this.buf1.name;this.shift();if(this.buf1===n.EOF)break;i[t]=this.getObj(e)}-1!==t.beginInlineImagePos&&(s=a.pos-t.beginInlineImagePos);const o=this.xref.fetchIfRef(i.F||i.Filter);let c;if(o instanceof n.Name)c=o.name;else if(Array.isArray(o)){const e=this.xref.fetchIfRef(o[0]);e instanceof n.Name&&(c=e.name)}const l=a.pos;let h,u;switch(c){case"DCT":case"DCTDecode":h=this.findDCTDecodeInlineStreamEnd(a);break;case"A85":case"ASCII85Decode":h=this.findASCII85DecodeInlineStreamEnd(a);break;case"AHx":case"ASCIIHexDecode":h=this.findASCIIHexDecodeInlineStreamEnd(a);break;default:h=this.findDefaultInlineStreamEnd(a)}if(h<1e3&&s>0){const e=a.pos;a.pos=t.beginInlineImagePos;u=function getInlineImageCacheKey(e){const t=[],a=e.length;let r=0;for(;r{a.read().then((({value:t,done:e})=>{if(e)r.streamReader=null;else if(!this._transport.destroyed){this._renderPageChunk(t,r);pump()}}),(t=>{r.streamReader=null;if(!this._transport.destroyed){if(r.operatorList){r.operatorList.lastChunk=!0;for(const t of r.renderTasks)t.operatorListChanged();this.#a(!0)}if(r.displayReadyCapability)r.displayReadyCapability.reject(t);else{if(!r.opListReadCapability)throw t;r.opListReadCapability.reject(t)}}}))};pump()}_abortOperatorList({intentState:t,reason:e,force:i=!1}){if(t.streamReader){if(t.streamReaderCancelTimeout){clearTimeout(t.streamReaderCancelTimeout);t.streamReaderCancelTimeout=null}if(!i){if(t.renderTasks.size>0)return;if(e instanceof _display_utils.RenderingCancelledException){let i=RENDERING_CANCELLED_TIMEOUT;e.extraDelay>0&&e.extraDelay<1e3&&(i+=e.extraDelay);t.streamReaderCancelTimeout=setTimeout((()=>{t.streamReaderCancelTimeout=null;this._abortOperatorList({intentState:t,reason:e,force:!0})}),i);return}}t.streamReader.cancel(new _util.AbortException(e.message)).catch((()=>{}));t.streamReader=null;if(!this._transport.destroyed){for(const[e,i]of this._intentStates)if(i===t){this._intentStates.delete(e);break}this.cleanup()}}}get stats(){return this._stats}}exports.PDFPageProxy=PDFPageProxy;class LoopbackPort{#r=new Set;#o=Promise.resolve();postMessage(t,e){const i={data:structuredClone(t,e?{transfer:e}:null)};this.#o.then((()=>{for(const t of this.#r)t.call(this,i)}))}addEventListener(t,e){this.#r.add(e)}removeEventListener(t,e){this.#r.delete(e)}terminate(){this.#r.clear()}}exports.LoopbackPort=LoopbackPort;const PDFWorkerUtil={isWorkerDisabled:!1,fallbackWorkerSrc:null,fakeWorkerId:0};exports.PDFWorkerUtil=PDFWorkerUtil;if(_util.isNodeJS&&"function"==typeof require){PDFWorkerUtil.isWorkerDisabled=!0;PDFWorkerUtil.fallbackWorkerSrc="./pdf.worker.js"}else if("object"==typeof document){const t=document?.currentScript?.src;t&&(PDFWorkerUtil.fallbackWorkerSrc=t.replace(/(\.(?:min\.)?js)(\?.*)?$/i,".worker$1$2"))}PDFWorkerUtil.isSameOrigin=function(t,e){let i;try{i=new URL(t);if(!i.origin||"null"===i.origin)return!1}catch{return!1}const s=new URL(e,i);return i.origin===s.origin};PDFWorkerUtil.createCDNWrapper=function(t){const e=`importScripts("${t}");`;return URL.createObjectURL(new Blob([e]))};class PDFWorker{static#l;constructor({name:t=null,port:e=null,verbosity:i=(0,_util.getVerbosityLevel)()}={}){this.name=t;this.destroyed=!1;this.verbosity=i;this._readyCapability=new _util.PromiseCapability;this._port=null;this._webWorker=null;this._messageHandler=null;if(e){if(PDFWorker.#l?.has(e))throw new Error("Cannot use more than one PDFWorker per port.");(PDFWorker.#l||=new WeakMap).set(e,this);this._initializeFromPort(e)}else this._initialize()}get promise(){return this._readyCapability.promise}get port(){return this._port}get messageHandler(){return this._messageHandler}_initializeFromPort(t){this._port=t;this._messageHandler=new _message_handler.MessageHandler("main","worker",t);this._messageHandler.on("ready",(function(){}));this._readyCapability.resolve();this._messageHandler.send("configure",{verbosity:this.verbosity})}_initialize(){if(!PDFWorkerUtil.isWorkerDisabled&&!PDFWorker._mainThreadWorkerMessageHandler){let{workerSrc:t}=PDFWorker;try{PDFWorkerUtil.isSameOrigin(window.location.href,t)||(t=PDFWorkerUtil.createCDNWrapper(new URL(t,window.location).href));const e=new Worker(t),i=new _message_handler.MessageHandler("main","worker",e),terminateEarly=()=>{e.removeEventListener("error",onWorkerError);i.destroy();e.terminate();this.destroyed?this._readyCapability.reject(new Error("Worker was destroyed")):this._setupFakeWorker()},onWorkerError=()=>{this._webWorker||terminateEarly()};e.addEventListener("error",onWorkerError);i.on("test",(t=>{e.removeEventListener("error",onWorkerError);if(this.destroyed)terminateEarly();else if(t){this._messageHandler=i;this._port=e;this._webWorker=e;this._readyCapability.resolve();i.send("configure",{verbosity:this.verbosity})}else{this._setupFakeWorker();i.destroy();e.terminate()}}));i.on("ready",(t=>{e.removeEventListener("error",onWorkerError);if(this.destroyed)terminateEarly();else try{sendTest()}catch{this._setupFakeWorker()}}));const sendTest=()=>{const t=new Uint8Array;i.send("test",t,[t.buffer])};sendTest();return}catch{(0,_util.info)("The worker has been disabled.")}}this._setupFakeWorker()}_setupFakeWorker(){if(!PDFWorkerUtil.isWorkerDisabled){(0,_util.warn)("Setting up fake worker.");PDFWorkerUtil.isWorkerDisabled=!0}PDFWorker._setupFakeWorkerGlobal.then((t=>{if(this.destroyed){this._readyCapability.reject(new Error("Worker was destroyed"));return}const e=new LoopbackPort;this._port=e;const i="fake"+PDFWorkerUtil.fakeWorkerId++,s=new _message_handler.MessageHandler(i+"_worker",i,e);t.setup(s,e);const n=new _message_handler.MessageHandler(i,i+"_worker",e);this._messageHandler=n;this._readyCapability.resolve();n.send("configure",{verbosity:this.verbosity})})).catch((t=>{this._readyCapability.reject(new Error(`Setting up fake worker failed: "${t.message}".`))}))}destroy(){this.destroyed=!0;if(this._webWorker){this._webWorker.terminate();this._webWorker=null}PDFWorker.#l?.delete(this._port);this._port=null;if(this._messageHandler){this._messageHandler.destroy();this._messageHandler=null}}static fromPort(t){if(!t?.port)throw new Error("PDFWorker.fromPort - invalid method signature.");const e=this.#l?.get(t.port);if(e){if(e._pendingDestroy)throw new Error("PDFWorker.fromPort - the worker is being destroyed.\nPlease remember to await `PDFDocumentLoadingTask.destroy()`-calls.");return e}return new PDFWorker(t)}static get workerSrc(){if(_worker_options.GlobalWorkerOptions.workerSrc)return _worker_options.GlobalWorkerOptions.workerSrc;if(null!==PDFWorkerUtil.fallbackWorkerSrc){_util.isNodeJS||(0,_display_utils.deprecated)('No "GlobalWorkerOptions.workerSrc" specified.');return PDFWorkerUtil.fallbackWorkerSrc}throw new Error('No "GlobalWorkerOptions.workerSrc" specified.')}static get _mainThreadWorkerMessageHandler(){try{return globalThis.pdfjsWorker?.WorkerMessageHandler||null}catch{return null}}static get _setupFakeWorkerGlobal(){const loader=async()=>{const mainWorkerMessageHandler=this._mainThreadWorkerMessageHandler;if(mainWorkerMessageHandler)return mainWorkerMessageHandler;if(_util.isNodeJS&&"function"==typeof require){const worker=eval("require")(this.workerSrc);return worker.WorkerMessageHandler}await(0,_display_utils.loadScript)(this.workerSrc);return window.pdfjsWorker.WorkerMessageHandler};return(0,_util.shadow)(this,"_setupFakeWorkerGlobal",loader())}}exports.PDFWorker=PDFWorker;class WorkerTransport{#h=new Map;#c=new Map;#d=new Map;#u=null;constructor(t,e,i,s,n){this.messageHandler=t;this.loadingTask=e;this.commonObjs=new PDFObjects;this.fontLoader=new _font_loader.FontLoader({ownerDocument:s.ownerDocument,styleElement:s.styleElement});this._params=s;this.canvasFactory=n.canvasFactory;this.filterFactory=n.filterFactory;this.cMapReaderFactory=n.cMapReaderFactory;this.standardFontDataFactory=n.standardFontDataFactory;this.destroyed=!1;this.destroyCapability=null;this._networkStream=i;this._fullReader=null;this._lastProgress=null;this.downloadInfoCapability=new _util.PromiseCapability;this.setupMessageHandler()}#p(t,e=null){const i=this.#h.get(t);if(i)return i;const s=this.messageHandler.sendWithPromise(t,e);this.#h.set(t,s);return s}get annotationStorage(){return(0,_util.shadow)(this,"annotationStorage",new _annotation_storage.AnnotationStorage)}getRenderingIntent(t,e=_util.AnnotationMode.ENABLE,i=null,s=!1){let n=_util.RenderingIntentFlag.DISPLAY,a=_annotation_storage.SerializableEmpty;switch(t){case"any":n=_util.RenderingIntentFlag.ANY;break;case"display":break;case"print":n=_util.RenderingIntentFlag.PRINT;break;default:(0,_util.warn)(`getRenderingIntent - invalid intent: ${t}`)}switch(e){case _util.AnnotationMode.DISABLE:n+=_util.RenderingIntentFlag.ANNOTATIONS_DISABLE;break;case _util.AnnotationMode.ENABLE:break;case _util.AnnotationMode.ENABLE_FORMS:n+=_util.RenderingIntentFlag.ANNOTATIONS_FORMS;break;case _util.AnnotationMode.ENABLE_STORAGE:n+=_util.RenderingIntentFlag.ANNOTATIONS_STORAGE;a=(n&_util.RenderingIntentFlag.PRINT&&i instanceof _annotation_storage.PrintAnnotationStorage?i:this.annotationStorage).serializable;break;default:(0,_util.warn)(`getRenderingIntent - invalid annotationMode: ${e}`)}s&&(n+=_util.RenderingIntentFlag.OPLIST);return{renderingIntent:n,cacheKey:`${n}_${a.hash}`,annotationStorageSerializable:a}}destroy(){if(this.destroyCapability)return this.destroyCapability.promise;this.destroyed=!0;this.destroyCapability=new _util.PromiseCapability;this.#u?.reject(new Error("Worker was destroyed during onPassword callback"));const t=[];for(const e of this.#c.values())t.push(e._destroy());this.#c.clear();this.#d.clear();this.hasOwnProperty("annotationStorage")&&this.annotationStorage.resetModified();const e=this.messageHandler.sendWithPromise("Terminate",null);t.push(e);Promise.all(t).then((()=>{this.commonObjs.clear();this.fontLoader.clear();this.#h.clear();this.filterFactory.destroy();this._networkStream?.cancelAllRequests(new _util.AbortException("Worker was terminated."));if(this.messageHandler){this.messageHandler.destroy();this.messageHandler=null}this.destroyCapability.resolve()}),this.destroyCapability.reject);return this.destroyCapability.promise}setupMessageHandler(){const{messageHandler:t,loadingTask:e}=this;t.on("GetReader",((t,e)=>{(0,_util.assert)(this._networkStream,"GetReader - no `IPDFStream` instance available.");this._fullReader=this._networkStream.getFullReader();this._fullReader.onProgress=t=>{this._lastProgress={loaded:t.loaded,total:t.total}};e.onPull=()=>{this._fullReader.read().then((function({value:t,done:i}){if(i)e.close();else{(0,_util.assert)(t instanceof ArrayBuffer,"GetReader - expected an ArrayBuffer.");e.enqueue(new Uint8Array(t),1,[t])}})).catch((t=>{e.error(t)}))};e.onCancel=t=>{this._fullReader.cancel(t);e.ready.catch((t=>{if(!this.destroyed)throw t}))}}));t.on("ReaderHeadersReady",(t=>{const i=new _util.PromiseCapability,s=this._fullReader;s.headersReady.then((()=>{if(!s.isStreamingSupported||!s.isRangeSupported){this._lastProgress&&e.onProgress?.(this._lastProgress);s.onProgress=t=>{e.onProgress?.({loaded:t.loaded,total:t.total})}}i.resolve({isStreamingSupported:s.isStreamingSupported,isRangeSupported:s.isRangeSupported,contentLength:s.contentLength})}),i.reject);return i.promise}));t.on("GetRangeReader",((t,e)=>{(0,_util.assert)(this._networkStream,"GetRangeReader - no `IPDFStream` instance available.");const i=this._networkStream.getRangeReader(t.begin,t.end);if(i){e.onPull=()=>{i.read().then((function({value:t,done:i}){if(i)e.close();else{(0,_util.assert)(t instanceof ArrayBuffer,"GetRangeReader - expected an ArrayBuffer.");e.enqueue(new Uint8Array(t),1,[t])}})).catch((t=>{e.error(t)}))};e.onCancel=t=>{i.cancel(t);e.ready.catch((t=>{if(!this.destroyed)throw t}))}}else e.close()}));t.on("GetDoc",(({pdfInfo:t})=>{this._numPages=t.numPages;this._htmlForXfa=t.htmlForXfa;delete t.htmlForXfa;e._capability.resolve(new PDFDocumentProxy(t,this))}));t.on("DocException",(function(t){let i;switch(t.name){case"PasswordException":i=new _util.PasswordException(t.message,t.code);break;case"InvalidPDFException":i=new _util.InvalidPDFException(t.message);break;case"MissingPDFException":i=new _util.MissingPDFException(t.message);break;case"UnexpectedResponseException":i=new _util.UnexpectedResponseException(t.message,t.status);break;case"UnknownErrorException":i=new _util.UnknownErrorException(t.message,t.details);break;default:(0,_util.unreachable)("DocException - expected a valid Error.")}e._capability.reject(i)}));t.on("PasswordRequest",(t=>{this.#u=new _util.PromiseCapability;if(e.onPassword){const updatePassword=t=>{t instanceof Error?this.#u.reject(t):this.#u.resolve({password:t})};try{e.onPassword(updatePassword,t.code)}catch(t){this.#u.reject(t)}}else this.#u.reject(new _util.PasswordException(t.message,t.code));return this.#u.promise}));t.on("DataLoaded",(t=>{e.onProgress?.({loaded:t.length,total:t.length});this.downloadInfoCapability.resolve(t)}));t.on("StartRenderPage",(t=>{if(this.destroyed)return;this.#c.get(t.pageIndex)._startRenderPage(t.transparency,t.cacheKey)}));t.on("commonobj",(([e,i,s])=>{if(!this.destroyed&&!this.commonObjs.has(e))switch(i){case"Font":const n=this._params;if("error"in s){const t=s.error;(0,_util.warn)(`Error during font loading: ${t}`);this.commonObjs.resolve(e,t);break}const a=n.pdfBug&&globalThis.FontInspector?.enabled?(t,e)=>globalThis.FontInspector.fontAdded(t,e):null,r=new _font_loader.FontFaceObject(s,{isEvalSupported:n.isEvalSupported,disableFontFace:n.disableFontFace,ignoreErrors:n.ignoreErrors,inspectFont:a});this.fontLoader.bind(r).catch((i=>t.sendWithPromise("FontFallback",{id:e}))).finally((()=>{!n.fontExtraProperties&&r.data&&(r.data=null);this.commonObjs.resolve(e,r)}));break;case"FontPath":case"Image":case"Pattern":this.commonObjs.resolve(e,s);break;default:throw new Error(`Got unknown common object type ${i}`)}}));t.on("obj",(([t,e,i,s])=>{if(this.destroyed)return;const n=this.#c.get(e);if(!n.objs.has(t))switch(i){case"Image":n.objs.resolve(t,s);if(s){let t;if(s.bitmap){const{width:e,height:i}=s;t=e*i*4}else t=s.data?.length||0;t>_util.MAX_IMAGE_SIZE_TO_CACHE&&(n._maybeCleanupAfterRender=!0)}break;case"Pattern":n.objs.resolve(t,s);break;default:throw new Error(`Got unknown object type ${i}`)}}));t.on("DocProgress",(t=>{this.destroyed||e.onProgress?.({loaded:t.loaded,total:t.total})}));t.on("FetchBuiltInCMap",(t=>this.destroyed?Promise.reject(new Error("Worker was destroyed.")):this.cMapReaderFactory?this.cMapReaderFactory.fetch(t):Promise.reject(new Error("CMapReaderFactory not initialized, see the `useWorkerFetch` parameter."))));t.on("FetchStandardFontData",(t=>this.destroyed?Promise.reject(new Error("Worker was destroyed.")):this.standardFontDataFactory?this.standardFontDataFactory.fetch(t):Promise.reject(new Error("StandardFontDataFactory not initialized, see the `useWorkerFetch` parameter."))))}getData(){return this.messageHandler.sendWithPromise("GetData",null)}saveDocument(){this.annotationStorage.size<=0&&(0,_util.warn)("saveDocument called while `annotationStorage` is empty, please use the getData-method instead.");const{map:t,transfers:e}=this.annotationStorage.serializable;return this.messageHandler.sendWithPromise("SaveDocument",{isPureXfa:!!this._htmlForXfa,numPages:this._numPages,annotationStorage:t,filename:this._fullReader?.filename??null},e).finally((()=>{this.annotationStorage.resetModified()}))}getPage(t){if(!Number.isInteger(t)||t<=0||t>this._numPages)return Promise.reject(new Error("Invalid page request."));const e=t-1,i=this.#d.get(e);if(i)return i;const s=this.messageHandler.sendWithPromise("GetPage",{pageIndex:e}).then((t=>{if(this.destroyed)throw new Error("Transport destroyed");const i=new PDFPageProxy(e,t,this,this._params.pdfBug);this.#c.set(e,i);return i}));this.#d.set(e,s);return s}getPageIndex(t){return"object"!=typeof t||null===t||!Number.isInteger(t.num)||t.num<0||!Number.isInteger(t.gen)||t.gen<0?Promise.reject(new Error("Invalid pageIndex request.")):this.messageHandler.sendWithPromise("GetPageIndex",{num:t.num,gen:t.gen})}getAnnotations(t,e){return this.messageHandler.sendWithPromise("GetAnnotations",{pageIndex:t,intent:e})}getFieldObjects(){return this.#p("GetFieldObjects")}hasJSActions(){return this.#p("HasJSActions")}getCalculationOrderIds(){return this.messageHandler.sendWithPromise("GetCalculationOrderIds",null)}getDestinations(){return this.messageHandler.sendWithPromise("GetDestinations",null)}getDestination(t){return"string"!=typeof t?Promise.reject(new Error("Invalid destination request.")):this.messageHandler.sendWithPromise("GetDestination",{id:t})}getPageLabels(){return this.messageHandler.sendWithPromise("GetPageLabels",null)}getPageLayout(){return this.messageHandler.sendWithPromise("GetPageLayout",null)}getPageMode(){return this.messageHandler.sendWithPromise("GetPageMode",null)}getViewerPreferences(){return this.messageHandler.sendWithPromise("GetViewerPreferences",null)}getOpenAction(){return this.messageHandler.sendWithPromise("GetOpenAction",null)}getAttachments(){return this.messageHandler.sendWithPromise("GetAttachments",null)}getDocJSActions(){return this.#p("GetDocJSActions")}getPageJSActions(t){return this.messageHandler.sendWithPromise("GetPageJSActions",{pageIndex:t})}getStructTree(t){return this.messageHandler.sendWithPromise("GetStructTree",{pageIndex:t})}getOutline(){return this.messageHandler.sendWithPromise("GetOutline",null)}getOptionalContentConfig(){return this.messageHandler.sendWithPromise("GetOptionalContentConfig",null).then((t=>new _optional_content_config.OptionalContentConfig(t)))}getPermissions(){return this.messageHandler.sendWithPromise("GetPermissions",null)}getMetadata(){const t="GetMetadata",e=this.#h.get(t);if(e)return e;const i=this.messageHandler.sendWithPromise(t,null).then((t=>({info:t[0],metadata:t[1]?new _metadata.Metadata(t[1]):null,contentDispositionFilename:this._fullReader?.filename??null,contentLength:this._fullReader?.contentLength??null})));this.#h.set(t,i);return i}getMarkInfo(){return this.messageHandler.sendWithPromise("GetMarkInfo",null)}async startCleanup(t=!1){if(!this.destroyed){await this.messageHandler.sendWithPromise("Cleanup",null);for(const t of this.#c.values()){if(!t.cleanup())throw new Error(`startCleanup: Page ${t.pageNumber} is currently rendering.`)}this.commonObjs.clear();t||this.fontLoader.clear();this.#h.clear();this.filterFactory.destroy(!0)}}get loadingParams(){const{disableAutoFetch:t,enableXfa:e}=this._params;return(0,_util.shadow)(this,"loadingParams",{disableAutoFetch:t,enableXfa:e})}}class PDFObjects{#g=Object.create(null);#m(t){return this.#g[t]||={capability:new _util.PromiseCapability,data:null}}get(t,e=null){if(e){const i=this.#m(t);i.capability.promise.then((()=>e(i.data)));return null}const i=this.#g[t];if(!i?.capability.settled)throw new Error(`Requesting object that isn't resolved yet ${t}.`);return i.data}has(t){const e=this.#g[t];return e?.capability.settled||!1}resolve(t,e=null){const i=this.#m(t);i.data=e;i.capability.resolve()}clear(){for(const t in this.#g){const{data:e}=this.#g[t];e?.bitmap?.close()}this.#g=Object.create(null)}}class RenderTask{#f=null;constructor(t){this.#f=t;this.onContinue=null}get promise(){return this.#f.capability.promise}cancel(t=0){this.#f.cancel(null,t)}get separateAnnots(){const{separateAnnots:t}=this.#f.operatorList;if(!t)return!1;const{annotationCanvasMap:e}=this.#f;return t.form||t.canvas&&e?.size>0}}exports.RenderTask=RenderTask;class InternalRenderTask{static#b=new WeakSet;constructor({callback:t,params:e,objs:i,commonObjs:s,annotationCanvasMap:n,operatorList:a,pageIndex:r,canvasFactory:o,filterFactory:l,useRequestAnimationFrame:h=!1,pdfBug:c=!1,pageColors:d=null}){this.callback=t;this.params=e;this.objs=i;this.commonObjs=s;this.annotationCanvasMap=n;this.operatorListIdx=null;this.operatorList=a;this._pageIndex=r;this.canvasFactory=o;this.filterFactory=l;this._pdfBug=c;this.pageColors=d;this.running=!1;this.graphicsReadyCallback=null;this.graphicsReady=!1;this._useRequestAnimationFrame=!0===h&&"undefined"!=typeof window;this.cancelled=!1;this.capability=new _util.PromiseCapability;this.task=new RenderTask(this);this._cancelBound=this.cancel.bind(this);this._continueBound=this._continue.bind(this);this._scheduleNextBound=this._scheduleNext.bind(this);this._nextBound=this._next.bind(this);this._canvas=e.canvasContext.canvas}get completed(){return this.capability.promise.catch((function(){}))}initializeGraphics({transparency:t=!1,optionalContentConfig:e}){if(this.cancelled)return;if(this._canvas){if(InternalRenderTask.#b.has(this._canvas))throw new Error("Cannot use the same canvas during multiple render() operations. Use different canvas or ensure previous operations were cancelled or completed.");InternalRenderTask.#b.add(this._canvas)}if(this._pdfBug&&globalThis.StepperManager?.enabled){this.stepper=globalThis.StepperManager.create(this._pageIndex);this.stepper.init(this.operatorList);this.stepper.nextBreakPoint=this.stepper.getNextBreakPoint()}const{canvasContext:i,viewport:s,transform:n,background:a}=this.params;this.gfx=new _canvas.CanvasGraphics(i,this.commonObjs,this.objs,this.canvasFactory,this.filterFactory,{optionalContentConfig:e},this.annotationCanvasMap,this.pageColors);this.gfx.beginDrawing({transform:n,viewport:s,transparency:t,background:a});this.operatorListIdx=0;this.graphicsReady=!0;this.graphicsReadyCallback?.()}cancel(t=null,e=0){this.running=!1;this.cancelled=!0;this.gfx?.endDrawing();InternalRenderTask.#b.delete(this._canvas);this.callback(t||new _display_utils.RenderingCancelledException(`Rendering cancelled, page ${this._pageIndex+1}`,e))}operatorListChanged(){if(this.graphicsReady){this.stepper?.updateOperatorList(this.operatorList);this.running||this._continue()}else this.graphicsReadyCallback||=this._continueBound}_continue(){this.running=!0;this.cancelled||(this.task.onContinue?this.task.onContinue(this._scheduleNextBound):this._scheduleNext())}_scheduleNext(){this._useRequestAnimationFrame?window.requestAnimationFrame((()=>{this._nextBound().catch(this._cancelBound)})):Promise.resolve().then(this._nextBound).catch(this._cancelBound)}async _next(){if(!this.cancelled){this.operatorListIdx=this.gfx.executeOperatorList(this.operatorList,this.operatorListIdx,this._continueBound,this.stepper);if(this.operatorListIdx===this.operatorList.argsArray.length){this.running=!1;if(this.operatorList.lastChunk){this.gfx.endDrawing();InternalRenderTask.#b.delete(this._canvas);this.callback()}}}}}const version="3.11.174";exports.version=version;const build="ce8716743";exports.build=build},(t,e,i)=>{Object.defineProperty(e,"__esModule",{value:!0});e.SerializableEmpty=e.PrintAnnotationStorage=e.AnnotationStorage=void 0;var s=i(1),n=i(4),a=i(8);const r=Object.freeze({map:null,hash:"",transfers:void 0});e.SerializableEmpty=r;class AnnotationStorage{#A=!1;#_=new Map;constructor(){this.onSetModified=null;this.onResetModified=null;this.onAnnotationEditor=null}getValue(t,e){const i=this.#_.get(t);return void 0===i?e:Object.assign(e,i)}getRawValue(t){return this.#_.get(t)}remove(t){this.#_.delete(t);0===this.#_.size&&this.resetModified();if("function"==typeof this.onAnnotationEditor){for(const t of this.#_.values())if(t instanceof n.AnnotationEditor)return;this.onAnnotationEditor(null)}}setValue(t,e){const i=this.#_.get(t);let s=!1;if(void 0!==i){for(const[t,n]of Object.entries(e))if(i[t]!==n){s=!0;i[t]=n}}else{s=!0;this.#_.set(t,e)}s&&this.#v();e instanceof n.AnnotationEditor&&"function"==typeof this.onAnnotationEditor&&this.onAnnotationEditor(e.constructor._type)}has(t){return this.#_.has(t)}getAll(){return this.#_.size>0?(0,s.objectFromMap)(this.#_):null}setAll(t){for(const[e,i]of Object.entries(t))this.setValue(e,i)}get size(){return this.#_.size}#v(){if(!this.#A){this.#A=!0;"function"==typeof this.onSetModified&&this.onSetModified()}}resetModified(){if(this.#A){this.#A=!1;"function"==typeof this.onResetModified&&this.onResetModified()}}get print(){return new PrintAnnotationStorage(this)}get serializable(){if(0===this.#_.size)return r;const t=new Map,e=new a.MurmurHash3_64,i=[],s=Object.create(null);let o=!1;for(const[i,a]of this.#_){const r=a instanceof n.AnnotationEditor?a.serialize(!1,s):a;if(r){t.set(i,r);e.update(`${i}:${JSON.stringify(r)}`);o||=!!r.bitmap}}if(o)for(const e of t.values())e.bitmap&&i.push(e.bitmap);return t.size>0?{map:t,hash:e.hexdigest(),transfers:i}:r}}e.AnnotationStorage=AnnotationStorage;class PrintAnnotationStorage extends AnnotationStorage{#y;constructor(t){super();const{map:e,hash:i,transfers:s}=t.serializable,n=structuredClone(e,s?{transfer:s}:null);this.#y={map:n,hash:i,transfers:s}}get print(){(0,s.unreachable)("Should not call PrintAnnotationStorage.print")}get serializable(){return this.#y}}e.PrintAnnotationStorage=PrintAnnotationStorage},(t,e,i)=>{Object.defineProperty(e,"__esModule",{value:!0});e.AnnotationEditor=void 0;var s=i(5),n=i(1),a=i(6);class AnnotationEditor{#S="";#E=!1;#x=null;#w=null;#C=null;#T=!1;#P=null;#M=this.focusin.bind(this);#k=this.focusout.bind(this);#F=!1;#R=!1;#D=!1;_initialOptions=Object.create(null);_uiManager=null;_focusEventsAllowed=!0;_l10nPromise=null;#I=!1;#L=AnnotationEditor._zIndex++;static _borderLineWidth=-1;static _colorManager=new s.ColorManager;static _zIndex=1;static SMALL_EDITOR_SIZE=0;constructor(t){this.constructor===AnnotationEditor&&(0,n.unreachable)("Cannot initialize AnnotationEditor.");this.parent=t.parent;this.id=t.id;this.width=this.height=null;this.pageIndex=t.parent.pageIndex;this.name=t.name;this.div=null;this._uiManager=t.uiManager;this.annotationElementId=null;this._willKeepAspectRatio=!1;this._initialOptions.isCentered=t.isCentered;this._structTreeParentId=null;const{rotation:e,rawDims:{pageWidth:i,pageHeight:s,pageX:a,pageY:r}}=this.parent.viewport;this.rotation=e;this.pageRotation=(360+e-this._uiManager.viewParameters.rotation)%360;this.pageDimensions=[i,s];this.pageTranslation=[a,r];const[o,l]=this.parentDimensions;this.x=t.x/o;this.y=t.y/l;this.isAttachedToDOM=!1;this.deleted=!1}get editorType(){return Object.getPrototypeOf(this).constructor._type}static get _defaultLineColor(){return(0,n.shadow)(this,"_defaultLineColor",this._colorManager.getHexCode("CanvasText"))}static deleteAnnotationElement(t){const e=new FakeEditor({id:t.parent.getNextId(),parent:t.parent,uiManager:t._uiManager});e.annotationElementId=t.annotationElementId;e.deleted=!0;e._uiManager.addToAnnotationStorage(e)}static initialize(t,e=null){AnnotationEditor._l10nPromise||=new Map(["editor_alt_text_button_label","editor_alt_text_edit_button_label","editor_alt_text_decorative_tooltip"].map((e=>[e,t.get(e)])));if(e?.strings)for(const i of e.strings)AnnotationEditor._l10nPromise.set(i,t.get(i));if(-1!==AnnotationEditor._borderLineWidth)return;const i=getComputedStyle(document.documentElement);AnnotationEditor._borderLineWidth=parseFloat(i.getPropertyValue("--outline-width"))||0}static updateDefaultParams(t,e){}static get defaultPropertiesToUpdate(){return[]}static isHandlingMimeForPasting(t){return!1}static paste(t,e){(0,n.unreachable)("Not implemented")}get propertiesToUpdate(){return[]}get _isDraggable(){return this.#I}set _isDraggable(t){this.#I=t;this.div?.classList.toggle("draggable",t)}center(){const[t,e]=this.pageDimensions;switch(this.parentRotation){case 90:this.x-=this.height*e/(2*t);this.y+=this.width*t/(2*e);break;case 180:this.x+=this.width/2;this.y+=this.height/2;break;case 270:this.x+=this.height*e/(2*t);this.y-=this.width*t/(2*e);break;default:this.x-=this.width/2;this.y-=this.height/2}this.fixAndSetPosition()}addCommands(t){this._uiManager.addCommands(t)}get currentLayer(){return this._uiManager.currentLayer}setInBackground(){this.div.style.zIndex=0}setInForeground(){this.div.style.zIndex=this.#L}setParent(t){if(null!==t){this.pageIndex=t.pageIndex;this.pageDimensions=t.pageDimensions}this.parent=t}focusin(t){this._focusEventsAllowed&&(this.#F?this.#F=!1:this.parent.setSelected(this))}focusout(t){if(!this._focusEventsAllowed)return;if(!this.isAttachedToDOM)return;const e=t.relatedTarget;if(!e?.closest(`#${this.id}`)){t.preventDefault();this.parent?.isMultipleSelection||this.commitOrRemove()}}commitOrRemove(){this.isEmpty()?this.remove():this.commit()}commit(){this.addToAnnotationStorage()}addToAnnotationStorage(){this._uiManager.addToAnnotationStorage(this)}setAt(t,e,i,s){const[n,a]=this.parentDimensions;[i,s]=this.screenToPageTranslation(i,s);this.x=(t+i)/n;this.y=(e+s)/a;this.fixAndSetPosition()}#O([t,e],i,s){[i,s]=this.screenToPageTranslation(i,s);this.x+=i/t;this.y+=s/e;this.fixAndSetPosition()}translate(t,e){this.#O(this.parentDimensions,t,e)}translateInPage(t,e){this.#O(this.pageDimensions,t,e);this.div.scrollIntoView({block:"nearest"})}drag(t,e){const[i,s]=this.parentDimensions;this.x+=t/i;this.y+=e/s;if(this.parent&&(this.x<0||this.x>1||this.y<0||this.y>1)){const{x:t,y:e}=this.div.getBoundingClientRect();if(this.parent.findNewParent(this,t,e)){this.x-=Math.floor(this.x);this.y-=Math.floor(this.y)}}let{x:n,y:a}=this;const[r,o]=this.#N();n+=r;a+=o;this.div.style.left=`${(100*n).toFixed(2)}%`;this.div.style.top=`${(100*a).toFixed(2)}%`;this.div.scrollIntoView({block:"nearest"})}#N(){const[t,e]=this.parentDimensions,{_borderLineWidth:i}=AnnotationEditor,s=i/t,n=i/e;switch(this.rotation){case 90:return[-s,n];case 180:return[s,n];case 270:return[s,-n];default:return[-s,-n]}}fixAndSetPosition(){const[t,e]=this.pageDimensions;let{x:i,y:s,width:n,height:a}=this;n*=t;a*=e;i*=t;s*=e;switch(this.rotation){case 0:i=Math.max(0,Math.min(t-n,i));s=Math.max(0,Math.min(e-a,s));break;case 90:i=Math.max(0,Math.min(t-a,i));s=Math.min(e,Math.max(n,s));break;case 180:i=Math.min(t,Math.max(n,i));s=Math.min(e,Math.max(a,s));break;case 270:i=Math.min(t,Math.max(a,i));s=Math.max(0,Math.min(e-n,s))}this.x=i/=t;this.y=s/=e;const[r,o]=this.#N();i+=r;s+=o;const{style:l}=this.div;l.left=`${(100*i).toFixed(2)}%`;l.top=`${(100*s).toFixed(2)}%`;this.moveInDOM()}static#B(t,e,i){switch(i){case 90:return[e,-t];case 180:return[-t,-e];case 270:return[-e,t];default:return[t,e]}}screenToPageTranslation(t,e){return AnnotationEditor.#B(t,e,this.parentRotation)}pageTranslationToScreen(t,e){return AnnotationEditor.#B(t,e,360-this.parentRotation)}#U(t){switch(t){case 90:{const[t,e]=this.pageDimensions;return[0,-t/e,e/t,0]}case 180:return[-1,0,0,-1];case 270:{const[t,e]=this.pageDimensions;return[0,t/e,-e/t,0]}default:return[1,0,0,1]}}get parentScale(){return this._uiManager.viewParameters.realScale}get parentRotation(){return(this._uiManager.viewParameters.rotation+this.pageRotation)%360}get parentDimensions(){const{parentScale:t,pageDimensions:[e,i]}=this,s=e*t,a=i*t;return n.FeatureTest.isCSSRoundSupported?[Math.round(s),Math.round(a)]:[s,a]}setDims(t,e){const[i,s]=this.parentDimensions;this.div.style.width=`${(100*t/i).toFixed(2)}%`;this.#T||(this.div.style.height=`${(100*e/s).toFixed(2)}%`);this.#x?.classList.toggle("small",t>8]>>8:e[n]*s>>16}}function composeSMask(t,e,i,s){const n=s[0],a=s[1],r=s[2]-n,o=s[3]-a;if(0!==r&&0!==o){!function genericComposeSMask(t,e,i,s,n,a,r,o,l,h,c){const d=!!a,u=d?a[0]:0,p=d?a[1]:0,g=d?a[2]:0,m="Luminosity"===n?composeSMaskLuminosity:composeSMaskAlpha,f=Math.min(s,Math.ceil(1048576/i));for(let n=0;n10&&"function"==typeof i,c=h?Date.now()+15:0;let d=0;const u=this.commonObjs,p=this.objs;let g;for(;;){if(void 0!==n&&o===n.nextBreakPoint){n.breakIt(o,i);return o}g=r[o];if(g!==s.OPS.dependency)this[g].apply(this,a[o]);else for(const t of a[o]){const e=t.startsWith("g_")?u:p;if(!e.has(t)){e.get(t,i);return o}}o++;if(o===l)return o;if(h&&++d>10){if(Date.now()>c){i();return o}d=0}}}#he(){for(;this.stateStack.length||this.inSMaskMode;)this.restore();this.ctx.restore();if(this.transparentCanvas){this.ctx=this.compositeCtx;this.ctx.save();this.ctx.setTransform(1,0,0,1,0,0);this.ctx.drawImage(this.transparentCanvas,0,0);this.ctx.restore();this.transparentCanvas=null}}endDrawing(){this.#he();this.cachedCanvases.clear();this.cachedPatterns.clear();for(const t of this._cachedBitmapsMap.values()){for(const e of t.values())"undefined"!=typeof HTMLCanvasElement&&e instanceof HTMLCanvasElement&&(e.width=e.height=0);t.clear()}this._cachedBitmapsMap.clear();this.#ce()}#ce(){if(this.pageColors){const t=this.filterFactory.addHCMFilter(this.pageColors.foreground,this.pageColors.background);if("none"!==t){const e=this.ctx.filter;this.ctx.filter=t;this.ctx.drawImage(this.ctx.canvas,0,0);this.ctx.filter=e}}}_scaleImage(t,e){const i=t.width,s=t.height;let n,a,r=Math.max(Math.hypot(e[0],e[1]),1),o=Math.max(Math.hypot(e[2],e[3]),1),l=i,h=s,c="prescale1";for(;r>2&&l>1||o>2&&h>1;){let e=l,i=h;if(r>2&&l>1){e=l>=16384?Math.floor(l/2)-1||1:Math.ceil(l/2);r/=l/e}if(o>2&&h>1){i=h>=16384?Math.floor(h/2)-1||1:Math.ceil(h)/2;o/=h/i}n=this.cachedCanvases.getCanvas(c,e,i);a=n.context;a.clearRect(0,0,e,i);a.drawImage(t,0,0,l,h,0,0,e,i);t=n.canvas;l=e;h=i;c="prescale1"===c?"prescale2":"prescale1"}return{img:t,paintWidth:l,paintHeight:h}}_createMaskCanvas(t){const e=this.ctx,{width:i,height:r}=t,o=this.current.fillColor,l=this.current.patternFill,h=(0,n.getCurrentTransform)(e);let c,d,u,p;if((t.bitmap||t.data)&&t.count>1){const e=t.bitmap||t.data.buffer;d=JSON.stringify(l?h:[h.slice(0,4),o]);c=this._cachedBitmapsMap.get(e);if(!c){c=new Map;this._cachedBitmapsMap.set(e,c)}const i=c.get(d);if(i&&!l){return{canvas:i,offsetX:Math.round(Math.min(h[0],h[2])+h[4]),offsetY:Math.round(Math.min(h[1],h[3])+h[5])}}u=i}if(!u){p=this.cachedCanvases.getCanvas("maskCanvas",i,r);putBinaryImageMask(p.context,t)}let g=s.Util.transform(h,[1/i,0,0,-1/r,0,0]);g=s.Util.transform(g,[1,0,0,1,0,-r]);const m=s.Util.applyTransform([0,0],g),f=s.Util.applyTransform([i,r],g),b=s.Util.normalizeRect([m[0],m[1],f[0],f[1]]),A=Math.round(b[2]-b[0])||1,_=Math.round(b[3]-b[1])||1,v=this.cachedCanvases.getCanvas("fillCanvas",A,_),y=v.context,S=Math.min(m[0],f[0]),E=Math.min(m[1],f[1]);y.translate(-S,-E);y.transform(...g);if(!u){u=this._scaleImage(p.canvas,(0,n.getCurrentTransformInverse)(y));u=u.img;c&&l&&c.set(d,u)}y.imageSmoothingEnabled=getImageSmoothingEnabled((0,n.getCurrentTransform)(y),t.interpolate);drawImageAtIntegerCoords(y,u,0,0,u.width,u.height,0,0,i,r);y.globalCompositeOperation="source-in";const x=s.Util.transform((0,n.getCurrentTransformInverse)(y),[1,0,0,1,-S,-E]);y.fillStyle=l?o.getPattern(e,this,x,a.PathType.FILL):o;y.fillRect(0,0,i,r);if(c&&!l){this.cachedCanvases.delete("fillCanvas");c.set(d,v.canvas)}return{canvas:v.canvas,offsetX:Math.round(S),offsetY:Math.round(E)}}setLineWidth(t){t!==this.current.lineWidth&&(this._cachedScaleForStroking[0]=-1);this.current.lineWidth=t;this.ctx.lineWidth=t}setLineCap(t){this.ctx.lineCap=h[t]}setLineJoin(t){this.ctx.lineJoin=c[t]}setMiterLimit(t){this.ctx.miterLimit=t}setDash(t,e){const i=this.ctx;if(void 0!==i.setLineDash){i.setLineDash(t);i.lineDashOffset=e}}setRenderingIntent(t){}setFlatness(t){}setGState(t){for(const[e,i]of t)switch(e){case"LW":this.setLineWidth(i);break;case"LC":this.setLineCap(i);break;case"LJ":this.setLineJoin(i);break;case"ML":this.setMiterLimit(i);break;case"D":this.setDash(i[0],i[1]);break;case"RI":this.setRenderingIntent(i);break;case"FL":this.setFlatness(i);break;case"Font":this.setFont(i[0],i[1]);break;case"CA":this.current.strokeAlpha=i;break;case"ca":this.current.fillAlpha=i;this.ctx.globalAlpha=i;break;case"BM":this.ctx.globalCompositeOperation=i;break;case"SMask":this.current.activeSMask=i?this.tempSMask:null;this.tempSMask=null;this.checkSMaskState();break;case"TR":this.ctx.filter=this.current.transferMaps=this.filterFactory.addFilter(i)}}get inSMaskMode(){return!!this.suspendedCtx}checkSMaskState(){const t=this.inSMaskMode;this.current.activeSMask&&!t?this.beginSMaskMode():!this.current.activeSMask&&t&&this.endSMaskMode()}beginSMaskMode(){if(this.inSMaskMode)throw new Error("beginSMaskMode called while already in smask mode");const t=this.ctx.canvas.width,e=this.ctx.canvas.height,i="smaskGroupAt"+this.groupLevel,s=this.cachedCanvases.getCanvas(i,t,e);this.suspendedCtx=this.ctx;this.ctx=s.context;const a=this.ctx;a.setTransform(...(0,n.getCurrentTransform)(this.suspendedCtx));copyCtxState(this.suspendedCtx,a);!function mirrorContextOperations(t,e){if(t._removeMirroring)throw new Error("Context is already forwarding operations.");t.__originalSave=t.save;t.__originalRestore=t.restore;t.__originalRotate=t.rotate;t.__originalScale=t.scale;t.__originalTranslate=t.translate;t.__originalTransform=t.transform;t.__originalSetTransform=t.setTransform;t.__originalResetTransform=t.resetTransform;t.__originalClip=t.clip;t.__originalMoveTo=t.moveTo;t.__originalLineTo=t.lineTo;t.__originalBezierCurveTo=t.bezierCurveTo;t.__originalRect=t.rect;t.__originalClosePath=t.closePath;t.__originalBeginPath=t.beginPath;t._removeMirroring=()=>{t.save=t.__originalSave;t.restore=t.__originalRestore;t.rotate=t.__originalRotate;t.scale=t.__originalScale;t.translate=t.__originalTranslate;t.transform=t.__originalTransform;t.setTransform=t.__originalSetTransform;t.resetTransform=t.__originalResetTransform;t.clip=t.__originalClip;t.moveTo=t.__originalMoveTo;t.lineTo=t.__originalLineTo;t.bezierCurveTo=t.__originalBezierCurveTo;t.rect=t.__originalRect;t.closePath=t.__originalClosePath;t.beginPath=t.__originalBeginPath;delete t._removeMirroring};t.save=function ctxSave(){e.save();this.__originalSave()};t.restore=function ctxRestore(){e.restore();this.__originalRestore()};t.translate=function ctxTranslate(t,i){e.translate(t,i);this.__originalTranslate(t,i)};t.scale=function ctxScale(t,i){e.scale(t,i);this.__originalScale(t,i)};t.transform=function ctxTransform(t,i,s,n,a,r){e.transform(t,i,s,n,a,r);this.__originalTransform(t,i,s,n,a,r)};t.setTransform=function ctxSetTransform(t,i,s,n,a,r){e.setTransform(t,i,s,n,a,r);this.__originalSetTransform(t,i,s,n,a,r)};t.resetTransform=function ctxResetTransform(){e.resetTransform();this.__originalResetTransform()};t.rotate=function ctxRotate(t){e.rotate(t);this.__originalRotate(t)};t.clip=function ctxRotate(t){e.clip(t);this.__originalClip(t)};t.moveTo=function(t,i){e.moveTo(t,i);this.__originalMoveTo(t,i)};t.lineTo=function(t,i){e.lineTo(t,i);this.__originalLineTo(t,i)};t.bezierCurveTo=function(t,i,s,n,a,r){e.bezierCurveTo(t,i,s,n,a,r);this.__originalBezierCurveTo(t,i,s,n,a,r)};t.rect=function(t,i,s,n){e.rect(t,i,s,n);this.__originalRect(t,i,s,n)};t.closePath=function(){e.closePath();this.__originalClosePath()};t.beginPath=function(){e.beginPath();this.__originalBeginPath()}}(a,this.suspendedCtx);this.setGState([["BM","source-over"],["ca",1],["CA",1]])}endSMaskMode(){if(!this.inSMaskMode)throw new Error("endSMaskMode called while not in smask mode");this.ctx._removeMirroring();copyCtxState(this.ctx,this.suspendedCtx);this.ctx=this.suspendedCtx;this.suspendedCtx=null}compose(t){if(!this.current.activeSMask)return;if(t){t[0]=Math.floor(t[0]);t[1]=Math.floor(t[1]);t[2]=Math.ceil(t[2]);t[3]=Math.ceil(t[3])}else t=[0,0,this.ctx.canvas.width,this.ctx.canvas.height];const e=this.current.activeSMask;composeSMask(this.suspendedCtx,e,this.ctx,t);this.ctx.save();this.ctx.setTransform(1,0,0,1,0,0);this.ctx.clearRect(0,0,this.ctx.canvas.width,this.ctx.canvas.height);this.ctx.restore()}save(){if(this.inSMaskMode){copyCtxState(this.ctx,this.suspendedCtx);this.suspendedCtx.save()}else this.ctx.save();const t=this.current;this.stateStack.push(t);this.current=t.clone()}restore(){0===this.stateStack.length&&this.inSMaskMode&&this.endSMaskMode();if(0!==this.stateStack.length){this.current=this.stateStack.pop();if(this.inSMaskMode){this.suspendedCtx.restore();copyCtxState(this.suspendedCtx,this.ctx)}else this.ctx.restore();this.checkSMaskState();this.pendingClip=null;this._cachedScaleForStroking[0]=-1;this._cachedGetSinglePixelWidth=null}}transform(t,e,i,s,n,a){this.ctx.transform(t,e,i,s,n,a);this._cachedScaleForStroking[0]=-1;this._cachedGetSinglePixelWidth=null}constructPath(t,e,i){const a=this.ctx,r=this.current;let o,l,h=r.x,c=r.y;const d=(0,n.getCurrentTransform)(a),u=0===d[0]&&0===d[3]||0===d[1]&&0===d[2],p=u?i.slice(0):null;for(let i=0,n=0,g=t.length;i>>8|255;i[n+2]=e<<16|s>>>16|255;i[n+3]=s<<8|255}for(let e=4*o,s=t.length;e>3,u=7&n,p=t.length;i=new Uint32Array(i.buffer);let g=0;for(let s=0;s{Object.defineProperty(e,"__esModule",{value:!0});e.GlobalWorkerOptions=void 0;const i=Object.create(null);e.GlobalWorkerOptions=i;i.workerPort=null;i.workerSrc=""},(t,e,i)=>{Object.defineProperty(e,"__esModule",{value:!0});e.MessageHandler=void 0;var s=i(1);const n=1,a=2,r=1,o=2,l=3,h=4,c=5,d=6,u=7,p=8;function wrapReason(t){t instanceof Error||"object"==typeof t&&null!==t||(0,s.unreachable)('wrapReason: Expected "reason" to be a (possibly cloned) Error.');switch(t.name){case"AbortException":return new s.AbortException(t.message);case"MissingPDFException":return new s.MissingPDFException(t.message);case"PasswordException":return new s.PasswordException(t.message,t.code);case"UnexpectedResponseException":return new s.UnexpectedResponseException(t.message,t.status);case"UnknownErrorException":return new s.UnknownErrorException(t.message,t.details);default:return new s.UnknownErrorException(t.message,t.toString())}}e.MessageHandler=class MessageHandler{constructor(t,e,i){this.sourceName=t;this.targetName=e;this.comObj=i;this.callbackId=1;this.streamId=1;this.streamSinks=Object.create(null);this.streamControllers=Object.create(null);this.callbackCapabilities=Object.create(null);this.actionHandler=Object.create(null);this._onComObjOnMessage=t=>{const e=t.data;if(e.targetName!==this.sourceName)return;if(e.stream){this.#de(e);return}if(e.callback){const t=e.callbackId,i=this.callbackCapabilities[t];if(!i)throw new Error(`Cannot resolve callback ${t}`);delete this.callbackCapabilities[t];if(e.callback===n)i.resolve(e.data);else{if(e.callback!==a)throw new Error("Unexpected callback case");i.reject(wrapReason(e.reason))}return}const s=this.actionHandler[e.action];if(!s)throw new Error(`Unknown action from worker: ${e.action}`);if(e.callbackId){const t=this.sourceName,r=e.sourceName;new Promise((function(t){t(s(e.data))})).then((function(s){i.postMessage({sourceName:t,targetName:r,callback:n,callbackId:e.callbackId,data:s})}),(function(s){i.postMessage({sourceName:t,targetName:r,callback:a,callbackId:e.callbackId,reason:wrapReason(s)})}))}else e.streamId?this.#ue(e):s(e.data)};i.addEventListener("message",this._onComObjOnMessage)}on(t,e){const i=this.actionHandler;if(i[t])throw new Error(`There is already an actionName called "${t}"`);i[t]=e}send(t,e,i){this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:t,data:e},i)}sendWithPromise(t,e,i){const n=this.callbackId++,a=new s.PromiseCapability;this.callbackCapabilities[n]=a;try{this.comObj.postMessage({sourceName:this.sourceName,targetName:this.targetName,action:t,callbackId:n,data:e},i)}catch(t){a.reject(t)}return a.promise}sendWithStream(t,e,i,n){const a=this.streamId++,o=this.sourceName,l=this.targetName,h=this.comObj;return new ReadableStream({start:i=>{const r=new s.PromiseCapability;this.streamControllers[a]={controller:i,startCall:r,pullCall:null,cancelCall:null,isClosed:!1};h.postMessage({sourceName:o,targetName:l,action:t,streamId:a,data:e,desiredSize:i.desiredSize},n);return r.promise},pull:t=>{const e=new s.PromiseCapability;this.streamControllers[a].pullCall=e;h.postMessage({sourceName:o,targetName:l,stream:d,streamId:a,desiredSize:t.desiredSize});return e.promise},cancel:t=>{(0,s.assert)(t instanceof Error,"cancel must have a valid reason");const e=new s.PromiseCapability;this.streamControllers[a].cancelCall=e;this.streamControllers[a].isClosed=!0;h.postMessage({sourceName:o,targetName:l,stream:r,streamId:a,reason:wrapReason(t)});return e.promise}},i)}#ue(t){const e=t.streamId,i=this.sourceName,n=t.sourceName,a=this.comObj,r=this,o=this.actionHandler[t.action],d={enqueue(t,r=1,o){if(this.isCancelled)return;const l=this.desiredSize;this.desiredSize-=r;if(l>0&&this.desiredSize<=0){this.sinkCapability=new s.PromiseCapability;this.ready=this.sinkCapability.promise}a.postMessage({sourceName:i,targetName:n,stream:h,streamId:e,chunk:t},o)},close(){if(!this.isCancelled){this.isCancelled=!0;a.postMessage({sourceName:i,targetName:n,stream:l,streamId:e});delete r.streamSinks[e]}},error(t){(0,s.assert)(t instanceof Error,"error must have a valid reason");if(!this.isCancelled){this.isCancelled=!0;a.postMessage({sourceName:i,targetName:n,stream:c,streamId:e,reason:wrapReason(t)})}},sinkCapability:new s.PromiseCapability,onPull:null,onCancel:null,isCancelled:!1,desiredSize:t.desiredSize,ready:null};d.sinkCapability.resolve();d.ready=d.sinkCapability.promise;this.streamSinks[e]=d;new Promise((function(e){e(o(t.data,d))})).then((function(){a.postMessage({sourceName:i,targetName:n,stream:p,streamId:e,success:!0})}),(function(t){a.postMessage({sourceName:i,targetName:n,stream:p,streamId:e,reason:wrapReason(t)})}))}#de(t){const e=t.streamId,i=this.sourceName,n=t.sourceName,a=this.comObj,g=this.streamControllers[e],m=this.streamSinks[e];switch(t.stream){case p:t.success?g.startCall.resolve():g.startCall.reject(wrapReason(t.reason));break;case u:t.success?g.pullCall.resolve():g.pullCall.reject(wrapReason(t.reason));break;case d:if(!m){a.postMessage({sourceName:i,targetName:n,stream:u,streamId:e,success:!0});break}m.desiredSize<=0&&t.desiredSize>0&&m.sinkCapability.resolve();m.desiredSize=t.desiredSize;new Promise((function(t){t(m.onPull?.())})).then((function(){a.postMessage({sourceName:i,targetName:n,stream:u,streamId:e,success:!0})}),(function(t){a.postMessage({sourceName:i,targetName:n,stream:u,streamId:e,reason:wrapReason(t)})}));break;case h:(0,s.assert)(g,"enqueue should have stream controller");if(g.isClosed)break;g.controller.enqueue(t.chunk);break;case l:(0,s.assert)(g,"close should have stream controller");if(g.isClosed)break;g.isClosed=!0;g.controller.close();this.#pe(g,e);break;case c:(0,s.assert)(g,"error should have stream controller");g.controller.error(wrapReason(t.reason));this.#pe(g,e);break;case o:t.success?g.cancelCall.resolve():g.cancelCall.reject(wrapReason(t.reason));this.#pe(g,e);break;case r:if(!m)break;new Promise((function(e){e(m.onCancel?.(wrapReason(t.reason)))})).then((function(){a.postMessage({sourceName:i,targetName:n,stream:o,streamId:e,success:!0})}),(function(t){a.postMessage({sourceName:i,targetName:n,stream:o,streamId:e,reason:wrapReason(t)})}));m.sinkCapability.reject(wrapReason(t.reason));m.isCancelled=!0;delete this.streamSinks[e];break;default:throw new Error("Unexpected stream case")}}async#pe(t,e){await Promise.allSettled([t.startCall?.promise,t.pullCall?.promise,t.cancelCall?.promise]);delete this.streamControllers[e]}destroy(){this.comObj.removeEventListener("message",this._onComObjOnMessage)}}},(t,e,i)=>{Object.defineProperty(e,"__esModule",{value:!0});e.Metadata=void 0;var s=i(1);e.Metadata=class Metadata{#ge;#me;constructor({parsedData:t,rawData:e}){this.#ge=t;this.#me=e}getRaw(){return this.#me}get(t){return this.#ge.get(t)??null}getAll(){return(0,s.objectFromMap)(this.#ge)}has(t){return this.#ge.has(t)}}},(t,e,i)=>{Object.defineProperty(e,"__esModule",{value:!0});e.OptionalContentConfig=void 0;var s=i(1),n=i(8);const a=Symbol("INTERNAL");class OptionalContentGroup{#fe=!0;constructor(t,e){this.name=t;this.intent=e}get visible(){return this.#fe}_setVisible(t,e){t!==a&&(0,s.unreachable)("Internal method `_setVisible` called.");this.#fe=e}}e.OptionalContentConfig=class OptionalContentConfig{#be=null;#Ae=new Map;#_e=null;#ve=null;constructor(t){this.name=null;this.creator=null;if(null!==t){this.name=t.name;this.creator=t.creator;this.#ve=t.order;for(const e of t.groups)this.#Ae.set(e.id,new OptionalContentGroup(e.name,e.intent));if("OFF"===t.baseState)for(const t of this.#Ae.values())t._setVisible(a,!1);for(const e of t.on)this.#Ae.get(e)._setVisible(a,!0);for(const e of t.off)this.#Ae.get(e)._setVisible(a,!1);this.#_e=this.getHash()}}#ye(t){const e=t.length;if(e<2)return!0;const i=t[0];for(let n=1;n>>8^e[i]}return-1^n}(s,n+4,a);s[a]=o>>24&255;s[a+1]=o>>16&255;s[a+2]=o>>8&255;s[a+3]=255&o}function deflateSyncUncompressed(t){let e=t.length;const i=65535,s=Math.ceil(e/i),n=new Uint8Array(2+e+5*s+4);let a=0;n[a++]=120;n[a++]=156;let r=0;for(;e>i;){n[a++]=0;n[a++]=255;n[a++]=255;n[a++]=0;n[a++]=0;n.set(t.subarray(r,r+i),a);a+=i;r+=i;e-=i}n[a++]=1;n[a++]=255&e;n[a++]=e>>8&255;n[a++]=255&~e;n[a++]=(65535&~e)>>8&255;n.set(t.subarray(r),a);a+=t.length-r;const o=function adler32(t,e,i){let s=1,n=0;for(let a=e;a>24&255;n[a++]=o>>16&255;n[a++]=o>>8&255;n[a++]=255&o;return n}function encode(e,i,s,a){const r=e.width,o=e.height;let l,h,c;const d=e.data;switch(i){case n.ImageKind.GRAYSCALE_1BPP:h=0;l=1;c=r+7>>3;break;case n.ImageKind.RGB_24BPP:h=2;l=8;c=3*r;break;case n.ImageKind.RGBA_32BPP:h=6;l=8;c=4*r;break;default:throw new Error("invalid format")}const u=new Uint8Array((1+c)*o);let p=0,g=0;for(let t=0;t{t.get(e,i)}));this.current.dependencies.push(i)}return Promise.all(this.current.dependencies)}transform(t,e,i,s,a,r){const o=[t,e,i,s,a,r];this.transformMatrix=n.Util.transform(this.transformMatrix,o);this.tgrp=null}getSVG(t,e){this.viewport=e;const i=this._initialize(e);return this.loadDependencies(t).then((()=>{this.transformMatrix=n.IDENTITY_MATRIX;this.executeOpTree(this.convertOpList(t));return i}))}convertOpList(t){const e=this._operatorIdMapping,i=t.argsArray,s=t.fnArray,n=[];for(let t=0,a=s.length;t0&&(this.current.lineWidth=t)}setLineCap(t){this.current.lineCap=l[t]}setLineJoin(t){this.current.lineJoin=h[t]}setMiterLimit(t){this.current.miterLimit=t}setStrokeAlpha(t){this.current.strokeAlpha=t}setStrokeRGBColor(t,e,i){this.current.strokeColor=n.Util.makeHexColor(t,e,i)}setFillAlpha(t){this.current.fillAlpha=t}setFillRGBColor(t,e,i){this.current.fillColor=n.Util.makeHexColor(t,e,i);this.current.tspan=this.svgFactory.createElement("svg:tspan");this.current.xcoords=[];this.current.ycoords=[]}setStrokeColorN(t){this.current.strokeColor=this._makeColorN_Pattern(t)}setFillColorN(t){this.current.fillColor=this._makeColorN_Pattern(t)}shadingFill(t){const{width:e,height:i}=this.viewport,s=n.Util.inverseTransform(this.transformMatrix),[a,r,o,l]=n.Util.getAxialAlignedBoundingBox([0,0,e,i],s),h=this.svgFactory.createElement("svg:rect");h.setAttributeNS(null,"x",a);h.setAttributeNS(null,"y",r);h.setAttributeNS(null,"width",o-a);h.setAttributeNS(null,"height",l-r);h.setAttributeNS(null,"fill",this._makeShadingPattern(t));this.current.fillAlpha<1&&h.setAttributeNS(null,"fill-opacity",this.current.fillAlpha);this._ensureTransformGroup().append(h)}_makeColorN_Pattern(t){return"TilingPattern"===t[0]?this._makeTilingPattern(t):this._makeShadingPattern(t)}_makeTilingPattern(t){const e=t[1],i=t[2],s=t[3]||n.IDENTITY_MATRIX,[a,r,o,l]=t[4],h=t[5],c=t[6],d=t[7],u="shading"+p++,[g,m,f,b]=n.Util.normalizeRect([...n.Util.applyTransform([a,r],s),...n.Util.applyTransform([o,l],s)]),[A,_]=n.Util.singularValueDecompose2dScale(s),v=h*A,y=c*_,S=this.svgFactory.createElement("svg:pattern");S.setAttributeNS(null,"id",u);S.setAttributeNS(null,"patternUnits","userSpaceOnUse");S.setAttributeNS(null,"width",v);S.setAttributeNS(null,"height",y);S.setAttributeNS(null,"x",`${g}`);S.setAttributeNS(null,"y",`${m}`);const E=this.svg,x=this.transformMatrix,w=this.current.fillColor,C=this.current.strokeColor,T=this.svgFactory.create(f-g,b-m);this.svg=T;this.transformMatrix=s;if(2===d){const t=n.Util.makeHexColor(...e);this.current.fillColor=t;this.current.strokeColor=t}this.executeOpTree(this.convertOpList(i));this.svg=E;this.transformMatrix=x;this.current.fillColor=w;this.current.strokeColor=C;S.append(T.childNodes[0]);this.defs.append(S);return`url(#${u})`}_makeShadingPattern(t){"string"==typeof t&&(t=this.objs.get(t));switch(t[0]){case"RadialAxial":const e="shading"+p++,i=t[3];let s;switch(t[1]){case"axial":const i=t[4],n=t[5];s=this.svgFactory.createElement("svg:linearGradient");s.setAttributeNS(null,"id",e);s.setAttributeNS(null,"gradientUnits","userSpaceOnUse");s.setAttributeNS(null,"x1",i[0]);s.setAttributeNS(null,"y1",i[1]);s.setAttributeNS(null,"x2",n[0]);s.setAttributeNS(null,"y2",n[1]);break;case"radial":const a=t[4],r=t[5],o=t[6],l=t[7];s=this.svgFactory.createElement("svg:radialGradient");s.setAttributeNS(null,"id",e);s.setAttributeNS(null,"gradientUnits","userSpaceOnUse");s.setAttributeNS(null,"cx",r[0]);s.setAttributeNS(null,"cy",r[1]);s.setAttributeNS(null,"r",l);s.setAttributeNS(null,"fx",a[0]);s.setAttributeNS(null,"fy",a[1]);s.setAttributeNS(null,"fr",o);break;default:throw new Error(`Unknown RadialAxial type: ${t[1]}`)}for(const t of i){const e=this.svgFactory.createElement("svg:stop");e.setAttributeNS(null,"offset",t[0]);e.setAttributeNS(null,"stop-color",t[1]);s.append(e)}this.defs.append(s);return`url(#${e})`;case"Mesh":(0,n.warn)("Unimplemented pattern Mesh");return null;case"Dummy":return"hotpink";default:throw new Error(`Unknown IR type: ${t[0]}`)}}setDash(t,e){this.current.dashArray=t;this.current.dashPhase=e}constructPath(t,e){const i=this.current;let s=i.x,a=i.y,r=[],o=0;for(const i of t)switch(0|i){case n.OPS.rectangle:s=e[o++];a=e[o++];const t=s+e[o++],i=a+e[o++];r.push("M",pf(s),pf(a),"L",pf(t),pf(a),"L",pf(t),pf(i),"L",pf(s),pf(i),"Z");break;case n.OPS.moveTo:s=e[o++];a=e[o++];r.push("M",pf(s),pf(a));break;case n.OPS.lineTo:s=e[o++];a=e[o++];r.push("L",pf(s),pf(a));break;case n.OPS.curveTo:s=e[o+4];a=e[o+5];r.push("C",pf(e[o]),pf(e[o+1]),pf(e[o+2]),pf(e[o+3]),pf(s),pf(a));o+=6;break;case n.OPS.curveTo2:r.push("C",pf(s),pf(a),pf(e[o]),pf(e[o+1]),pf(e[o+2]),pf(e[o+3]));s=e[o+2];a=e[o+3];o+=4;break;case n.OPS.curveTo3:s=e[o+2];a=e[o+3];r.push("C",pf(e[o]),pf(e[o+1]),pf(s),pf(a),pf(s),pf(a));o+=4;break;case n.OPS.closePath:r.push("Z")}r=r.join(" ");if(i.path&&t.length>0&&t[0]!==n.OPS.rectangle&&t[0]!==n.OPS.moveTo)r=i.path.getAttributeNS(null,"d")+r;else{i.path=this.svgFactory.createElement("svg:path");this._ensureTransformGroup().append(i.path)}i.path.setAttributeNS(null,"d",r);i.path.setAttributeNS(null,"fill","none");i.element=i.path;i.setCurrentPoint(s,a)}endPath(){const t=this.current;t.path=null;if(!this.pendingClip)return;if(!t.element){this.pendingClip=null;return}const e="clippath"+d++,i=this.svgFactory.createElement("svg:clipPath");i.setAttributeNS(null,"id",e);i.setAttributeNS(null,"transform",pm(this.transformMatrix));const s=t.element.cloneNode(!0);"evenodd"===this.pendingClip?s.setAttributeNS(null,"clip-rule","evenodd"):s.setAttributeNS(null,"clip-rule","nonzero");this.pendingClip=null;i.append(s);this.defs.append(i);if(t.activeClipUrl){t.clipGroup=null;for(const t of this.extraStack)t.clipGroup=null;i.setAttributeNS(null,"clip-path",t.activeClipUrl)}t.activeClipUrl=`url(#${e})`;this.tgrp=null}clip(t){this.pendingClip=t}closePath(){const t=this.current;if(t.path){const e=`${t.path.getAttributeNS(null,"d")}Z`;t.path.setAttributeNS(null,"d",e)}}setLeading(t){this.current.leading=-t}setTextRise(t){this.current.textRise=t}setTextRenderingMode(t){this.current.textRenderingMode=t}setHScale(t){this.current.textHScale=t/100}setRenderingIntent(t){}setFlatness(t){}setGState(t){for(const[e,i]of t)switch(e){case"LW":this.setLineWidth(i);break;case"LC":this.setLineCap(i);break;case"LJ":this.setLineJoin(i);break;case"ML":this.setMiterLimit(i);break;case"D":this.setDash(i[0],i[1]);break;case"RI":this.setRenderingIntent(i);break;case"FL":this.setFlatness(i);break;case"Font":this.setFont(i);break;case"CA":this.setStrokeAlpha(i);break;case"ca":this.setFillAlpha(i);break;default:(0,n.warn)(`Unimplemented graphic state operator ${e}`)}}fill(){const t=this.current;if(t.element){t.element.setAttributeNS(null,"fill",t.fillColor);t.element.setAttributeNS(null,"fill-opacity",t.fillAlpha);this.endPath()}}stroke(){const t=this.current;if(t.element){this._setStrokeAttributes(t.element);t.element.setAttributeNS(null,"fill","none");this.endPath()}}_setStrokeAttributes(t,e=1){const i=this.current;let s=i.dashArray;1!==e&&s.length>0&&(s=s.map((function(t){return e*t})));t.setAttributeNS(null,"stroke",i.strokeColor);t.setAttributeNS(null,"stroke-opacity",i.strokeAlpha);t.setAttributeNS(null,"stroke-miterlimit",pf(i.miterLimit));t.setAttributeNS(null,"stroke-linecap",i.lineCap);t.setAttributeNS(null,"stroke-linejoin",i.lineJoin);t.setAttributeNS(null,"stroke-width",pf(e*i.lineWidth)+"px");t.setAttributeNS(null,"stroke-dasharray",s.map(pf).join(" "));t.setAttributeNS(null,"stroke-dashoffset",pf(e*i.dashPhase)+"px")}eoFill(){this.current.element?.setAttributeNS(null,"fill-rule","evenodd");this.fill()}fillStroke(){this.stroke();this.fill()}eoFillStroke(){this.current.element?.setAttributeNS(null,"fill-rule","evenodd");this.fillStroke()}closeStroke(){this.closePath();this.stroke()}closeFillStroke(){this.closePath();this.fillStroke()}closeEOFillStroke(){this.closePath();this.eoFillStroke()}paintSolidColorImageMask(){const t=this.svgFactory.createElement("svg:rect");t.setAttributeNS(null,"x","0");t.setAttributeNS(null,"y","0");t.setAttributeNS(null,"width","1px");t.setAttributeNS(null,"height","1px");t.setAttributeNS(null,"fill",this.current.fillColor);this._ensureTransformGroup().append(t)}paintImageXObject(t){const e=this.getObject(t);e?this.paintInlineImageXObject(e):(0,n.warn)(`Dependent image with object ID ${t} is not ready yet`)}paintInlineImageXObject(t,e){const i=t.width,s=t.height,n=c(t,this.forceDataSchema,!!e),a=this.svgFactory.createElement("svg:rect");a.setAttributeNS(null,"x","0");a.setAttributeNS(null,"y","0");a.setAttributeNS(null,"width",pf(i));a.setAttributeNS(null,"height",pf(s));this.current.element=a;this.clip("nonzero");const r=this.svgFactory.createElement("svg:image");r.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",n);r.setAttributeNS(null,"x","0");r.setAttributeNS(null,"y",pf(-s));r.setAttributeNS(null,"width",pf(i)+"px");r.setAttributeNS(null,"height",pf(s)+"px");r.setAttributeNS(null,"transform",`scale(${pf(1/i)} ${pf(-1/s)})`);e?e.append(r):this._ensureTransformGroup().append(r)}paintImageMaskXObject(t){const e=this.getObject(t.data,t);if(e.bitmap){(0,n.warn)("paintImageMaskXObject: ImageBitmap support is not implemented, ensure that the `isOffscreenCanvasSupported` API parameter is disabled.");return}const i=this.current,s=e.width,a=e.height,r=i.fillColor;i.maskId="mask"+u++;const o=this.svgFactory.createElement("svg:mask");o.setAttributeNS(null,"id",i.maskId);const l=this.svgFactory.createElement("svg:rect");l.setAttributeNS(null,"x","0");l.setAttributeNS(null,"y","0");l.setAttributeNS(null,"width",pf(s));l.setAttributeNS(null,"height",pf(a));l.setAttributeNS(null,"fill",r);l.setAttributeNS(null,"mask",`url(#${i.maskId})`);this.defs.append(o);this._ensureTransformGroup().append(l);this.paintInlineImageXObject(e,o)}paintFormXObjectBegin(t,e){Array.isArray(t)&&6===t.length&&this.transform(t[0],t[1],t[2],t[3],t[4],t[5]);if(e){const t=e[2]-e[0],i=e[3]-e[1],s=this.svgFactory.createElement("svg:rect");s.setAttributeNS(null,"x",e[0]);s.setAttributeNS(null,"y",e[1]);s.setAttributeNS(null,"width",pf(t));s.setAttributeNS(null,"height",pf(i));this.current.element=s;this.clip("nonzero");this.endPath()}}paintFormXObjectEnd(){}_initialize(t){const e=this.svgFactory.create(t.width,t.height),i=this.svgFactory.createElement("svg:defs");e.append(i);this.defs=i;const s=this.svgFactory.createElement("svg:g");s.setAttributeNS(null,"transform",pm(t.transform));e.append(s);this.svg=s;return e}_ensureClipGroup(){if(!this.current.clipGroup){const t=this.svgFactory.createElement("svg:g");t.setAttributeNS(null,"clip-path",this.current.activeClipUrl);this.svg.append(t);this.current.clipGroup=t}return this.current.clipGroup}_ensureTransformGroup(){if(!this.tgrp){this.tgrp=this.svgFactory.createElement("svg:g");this.tgrp.setAttributeNS(null,"transform",pm(this.transformMatrix));this.current.activeClipUrl?this._ensureClipGroup().append(this.tgrp):this.svg.append(this.tgrp)}return this.tgrp}}},(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0});e.XfaText=void 0;class XfaText{static textContent(t){const e=[],i={items:e,styles:Object.create(null)};!function walk(t){if(!t)return;let i=null;const s=t.name;if("#text"===s)i=t.value;else{if(!XfaText.shouldBuildText(s))return;t?.attributes?.textContent?i=t.attributes.textContent:t.value&&(i=t.value)}null!==i&&e.push({str:i});if(t.children)for(const e of t.children)walk(e)}(t);return i}static shouldBuildText(t){return!("textarea"===t||"input"===t||"option"===t||"select"===t)}}e.XfaText=XfaText},(t,e,i)=>{Object.defineProperty(e,"__esModule",{value:!0});e.TextLayerRenderTask=void 0;e.renderTextLayer=function renderTextLayer(t){if(!t.textContentSource&&(t.textContent||t.textContentStream)){(0,n.deprecated)("The TextLayerRender `textContent`/`textContentStream` parameters will be removed in the future, please use `textContentSource` instead.");t.textContentSource=t.textContent||t.textContentStream}const{container:e,viewport:i}=t,s=getComputedStyle(e),a=s.getPropertyValue("visibility"),r=parseFloat(s.getPropertyValue("--scale-factor"));"visible"===a&&(!r||Math.abs(r-i.scale)>1e-5)&&console.error("The `--scale-factor` CSS-variable must be set, to the same value as `viewport.scale`, either on the `container`-element itself or higher up in the DOM.");const o=new TextLayerRenderTask(t);o._render();return o};e.updateTextLayer=function updateTextLayer({container:t,viewport:e,textDivs:i,textDivProperties:s,isOffscreenCanvasSupported:a,mustRotate:r=!0,mustRescale:o=!0}){r&&(0,n.setLayerDimensions)(t,{rotation:e.rotation});if(o){const t=getCtx(0,a),n={prevFontSize:null,prevFontFamily:null,div:null,scale:e.scale*(globalThis.devicePixelRatio||1),properties:null,ctx:t};for(const t of i){n.properties=s.get(t);n.div=t;layout(n)}}};var s=i(1),n=i(6);const a=30,r=.8,o=new Map;function getCtx(t,e){let i;if(e&&s.FeatureTest.isOffscreenCanvasSupported)i=new OffscreenCanvas(t,t).getContext("2d",{alpha:!1});else{const e=document.createElement("canvas");e.width=e.height=t;i=e.getContext("2d",{alpha:!1})}return i}function appendText(t,e,i){const n=document.createElement("span"),l={angle:0,canvasWidth:0,hasText:""!==e.str,hasEOL:e.hasEOL,fontSize:0};t._textDivs.push(n);const h=s.Util.transform(t._transform,e.transform);let c=Math.atan2(h[1],h[0]);const d=i[e.fontName];d.vertical&&(c+=Math.PI/2);const u=Math.hypot(h[2],h[3]),p=u*function getAscent(t,e){const i=o.get(t);if(i)return i;const s=getCtx(a,e);s.font=`${a}px ${t}`;const n=s.measureText("");let l=n.fontBoundingBoxAscent,h=Math.abs(n.fontBoundingBoxDescent);if(l){const e=l/(l+h);o.set(t,e);s.canvas.width=s.canvas.height=0;return e}s.strokeStyle="red";s.clearRect(0,0,a,a);s.strokeText("g",0,0);let c=s.getImageData(0,0,a,a).data;h=0;for(let t=c.length-1-3;t>=0;t-=4)if(c[t]>0){h=Math.ceil(t/4/a);break}s.clearRect(0,0,a,a);s.strokeText("A",0,a);c=s.getImageData(0,0,a,a).data;l=0;for(let t=0,e=c.length;t{Object.defineProperty(e,"__esModule",{value:!0});e.StampEditor=void 0;var s=i(1),n=i(4),a=i(6),r=i(29);class StampEditor extends n.AnnotationEditor{#fs=null;#bs=null;#As=null;#_s=null;#vs=null;#ys=null;#zi=null;#Ss=null;#Es=!1;#xs=!1;static _type="stamp";constructor(t){super({...t,name:"stampEditor"});this.#_s=t.bitmapUrl;this.#vs=t.bitmapFile}static initialize(t){n.AnnotationEditor.initialize(t)}static get supportedTypes(){return(0,s.shadow)(this,"supportedTypes",["apng","avif","bmp","gif","jpeg","png","svg+xml","webp","x-icon"].map((t=>`image/${t}`)))}static get supportedTypesStr(){return(0,s.shadow)(this,"supportedTypesStr",this.supportedTypes.join(","))}static isHandlingMimeForPasting(t){return this.supportedTypes.includes(t)}static paste(t,e){e.pasteEditor(s.AnnotationEditorType.STAMP,{bitmapFile:t.getAsFile()})}#ws(t,e=!1){if(t){this.#fs=t.bitmap;if(!e){this.#bs=t.id;this.#Es=t.isSvg}this.#Ki()}else this.remove()}#Cs(){this.#As=null;this._uiManager.enableWaiting(!1);this.#ys&&this.div.focus()}#Ts(){if(this.#bs){this._uiManager.enableWaiting(!0);this._uiManager.imageManager.getFromId(this.#bs).then((t=>this.#ws(t,!0))).finally((()=>this.#Cs()));return}if(this.#_s){const t=this.#_s;this.#_s=null;this._uiManager.enableWaiting(!0);this.#As=this._uiManager.imageManager.getFromUrl(t).then((t=>this.#ws(t))).finally((()=>this.#Cs()));return}if(this.#vs){const t=this.#vs;this.#vs=null;this._uiManager.enableWaiting(!0);this.#As=this._uiManager.imageManager.getFromFile(t).then((t=>this.#ws(t))).finally((()=>this.#Cs()));return}const t=document.createElement("input");t.type="file";t.accept=StampEditor.supportedTypesStr;this.#As=new Promise((e=>{t.addEventListener("change",(async()=>{if(t.files&&0!==t.files.length){this._uiManager.enableWaiting(!0);const e=await this._uiManager.imageManager.getFromFile(t.files[0]);this.#ws(e)}else this.remove();e()}));t.addEventListener("cancel",(()=>{this.remove();e()}))})).finally((()=>this.#Cs()));t.click()}remove(){if(this.#bs){this.#fs=null;this._uiManager.imageManager.deleteId(this.#bs);this.#ys?.remove();this.#ys=null;this.#zi?.disconnect();this.#zi=null}super.remove()}rebuild(){if(this.parent){super.rebuild();if(null!==this.div){this.#bs&&this.#Ts();this.isAttachedToDOM||this.parent.add(this)}}else this.#bs&&this.#Ts()}onceAdded(){this._isDraggable=!0;this.div.focus()}isEmpty(){return!(this.#As||this.#fs||this.#_s||this.#vs)}get isResizable(){return!0}render(){if(this.div)return this.div;let t,e;if(this.width){t=this.x;e=this.y}super.render();this.div.hidden=!0;this.#fs?this.#Ki():this.#Ts();if(this.width){const[i,s]=this.parentDimensions;this.setAt(t*i,e*s,this.width*i,this.height*s)}return this.div}#Ki(){const{div:t}=this;let{width:e,height:i}=this.#fs;const[s,n]=this.pageDimensions,a=.75;if(this.width){e=this.width*s;i=this.height*n}else if(e>a*s||i>a*n){const t=Math.min(a*s/e,a*n/i);e*=t;i*=t}const[r,o]=this.parentDimensions;this.setDims(e*r/s,i*o/n);this._uiManager.enableWaiting(!1);const l=this.#ys=document.createElement("canvas");t.append(l);t.hidden=!1;this.#Ps(e,i);this.#Yi();if(!this.#xs){this.parent.addUndoableEditor(this);this.#xs=!0}this._uiManager._eventBus.dispatch("reporttelemetry",{source:this,details:{type:"editing",subtype:this.editorType,data:{action:"inserted_image"}}});this.addAltTextButton()}#Ms(t,e){const[i,s]=this.parentDimensions;this.width=t/i;this.height=e/s;this.setDims(t,e);this._initialOptions?.isCentered?this.center():this.fixAndSetPosition();this._initialOptions=null;null!==this.#Ss&&clearTimeout(this.#Ss);this.#Ss=setTimeout((()=>{this.#Ss=null;this.#Ps(t,e)}),200)}#ks(t,e){const{width:i,height:s}=this.#fs;let n=i,a=s,r=this.#fs;for(;n>2*t||a>2*e;){const i=n,s=a;n>2*t&&(n=n>=16384?Math.floor(n/2)-1:Math.ceil(n/2));a>2*e&&(a=a>=16384?Math.floor(a/2)-1:Math.ceil(a/2));const o=new OffscreenCanvas(n,a);o.getContext("2d").drawImage(r,0,0,i,s,0,0,n,a);r=o.transferToImageBitmap()}return r}#Ps(t,e){t=Math.ceil(t);e=Math.ceil(e);const i=this.#ys;if(!i||i.width===t&&i.height===e)return;i.width=t;i.height=e;const s=this.#Es?this.#fs:this.#ks(t,e),n=i.getContext("2d");n.filter=this._uiManager.hcmFilter;n.drawImage(s,0,0,s.width,s.height,0,0,t,e)}#Fs(t){if(t){if(this.#Es){const t=this._uiManager.imageManager.getSvgUrl(this.#bs);if(t)return t}const t=document.createElement("canvas");({width:t.width,height:t.height}=this.#fs);t.getContext("2d").drawImage(this.#fs,0,0);return t.toDataURL()}if(this.#Es){const[t,e]=this.pageDimensions,i=Math.round(this.width*t*a.PixelsPerInch.PDF_TO_CSS_UNITS),s=Math.round(this.height*e*a.PixelsPerInch.PDF_TO_CSS_UNITS),n=new OffscreenCanvas(i,s);n.getContext("2d").drawImage(this.#fs,0,0,this.#fs.width,this.#fs.height,0,0,i,s);return n.transferToImageBitmap()}return structuredClone(this.#fs)}#Yi(){this.#zi=new ResizeObserver((t=>{const e=t[0].contentRect;e.width&&e.height&&this.#Ms(e.width,e.height)}));this.#zi.observe(this.div)}static deserialize(t,e,i){if(t instanceof r.StampAnnotationElement)return null;const s=super.deserialize(t,e,i),{rect:n,bitmapUrl:a,bitmapId:o,isSvg:l,accessibilityData:h}=t;o&&i.imageManager.isValidId(o)?s.#bs=o:s.#_s=a;s.#Es=l;const[c,d]=s.pageDimensions;s.width=(n[2]-n[0])/c;s.height=(n[3]-n[1])/d;h&&(s.altTextData=h);return s}serialize(t=!1,e=null){if(this.isEmpty())return null;const i={annotationType:s.AnnotationEditorType.STAMP,bitmapId:this.#bs,pageIndex:this.pageIndex,rect:this.getRect(0,0),rotation:this.rotation,isSvg:this.#Es,structTreeParentId:this._structTreeParentId};if(t){i.bitmapUrl=this.#Fs(!0);i.accessibilityData=this.altTextData;return i}const{decorative:n,altText:a}=this.altTextData;!n&&a&&(i.accessibilityData={type:"Figure",alt:a});if(null===e)return i;e.stamps||=new Map;const r=this.#Es?(i.rect[2]-i.rect[0])*(i.rect[3]-i.rect[1]):null;if(e.stamps.has(this.#bs)){if(this.#Es){const t=e.stamps.get(this.#bs);if(r>t.area){t.area=r;t.serialized.bitmap.close();t.serialized.bitmap=this.#Fs(!1)}}}else{e.stamps.set(this.#bs,{area:r,serialized:i});i.bitmap=this.#Fs(!1)}return i}}e.StampEditor=StampEditor}],__webpack_module_cache__={};function __w_pdfjs_require__(t){var e=__webpack_module_cache__[t];if(void 0!==e)return e.exports;var i=__webpack_module_cache__[t]={exports:{}};__webpack_modules__[t](i,i.exports,__w_pdfjs_require__);return i.exports}var __webpack_exports__={};(()=>{var t=__webpack_exports__;Object.defineProperty(t,"__esModule",{value:!0});Object.defineProperty(t,"AbortException",{enumerable:!0,get:function(){return e.AbortException}});Object.defineProperty(t,"AnnotationEditorLayer",{enumerable:!0,get:function(){return a.AnnotationEditorLayer}});Object.defineProperty(t,"AnnotationEditorParamsType",{enumerable:!0,get:function(){return e.AnnotationEditorParamsType}});Object.defineProperty(t,"AnnotationEditorType",{enumerable:!0,get:function(){return e.AnnotationEditorType}});Object.defineProperty(t,"AnnotationEditorUIManager",{enumerable:!0,get:function(){return r.AnnotationEditorUIManager}});Object.defineProperty(t,"AnnotationLayer",{enumerable:!0,get:function(){return o.AnnotationLayer}});Object.defineProperty(t,"AnnotationMode",{enumerable:!0,get:function(){return e.AnnotationMode}});Object.defineProperty(t,"CMapCompressionType",{enumerable:!0,get:function(){return e.CMapCompressionType}});Object.defineProperty(t,"DOMSVGFactory",{enumerable:!0,get:function(){return s.DOMSVGFactory}});Object.defineProperty(t,"FeatureTest",{enumerable:!0,get:function(){return e.FeatureTest}});Object.defineProperty(t,"GlobalWorkerOptions",{enumerable:!0,get:function(){return l.GlobalWorkerOptions}});Object.defineProperty(t,"ImageKind",{enumerable:!0,get:function(){return e.ImageKind}});Object.defineProperty(t,"InvalidPDFException",{enumerable:!0,get:function(){return e.InvalidPDFException}});Object.defineProperty(t,"MissingPDFException",{enumerable:!0,get:function(){return e.MissingPDFException}});Object.defineProperty(t,"OPS",{enumerable:!0,get:function(){return e.OPS}});Object.defineProperty(t,"PDFDataRangeTransport",{enumerable:!0,get:function(){return i.PDFDataRangeTransport}});Object.defineProperty(t,"PDFDateString",{enumerable:!0,get:function(){return s.PDFDateString}});Object.defineProperty(t,"PDFWorker",{enumerable:!0,get:function(){return i.PDFWorker}});Object.defineProperty(t,"PasswordResponses",{enumerable:!0,get:function(){return e.PasswordResponses}});Object.defineProperty(t,"PermissionFlag",{enumerable:!0,get:function(){return e.PermissionFlag}});Object.defineProperty(t,"PixelsPerInch",{enumerable:!0,get:function(){return s.PixelsPerInch}});Object.defineProperty(t,"PromiseCapability",{enumerable:!0,get:function(){return e.PromiseCapability}});Object.defineProperty(t,"RenderingCancelledException",{enumerable:!0,get:function(){return s.RenderingCancelledException}});Object.defineProperty(t,"SVGGraphics",{enumerable:!0,get:function(){return i.SVGGraphics}});Object.defineProperty(t,"UnexpectedResponseException",{enumerable:!0,get:function(){return e.UnexpectedResponseException}});Object.defineProperty(t,"Util",{enumerable:!0,get:function(){return e.Util}});Object.defineProperty(t,"VerbosityLevel",{enumerable:!0,get:function(){return e.VerbosityLevel}});Object.defineProperty(t,"XfaLayer",{enumerable:!0,get:function(){return h.XfaLayer}});Object.defineProperty(t,"build",{enumerable:!0,get:function(){return i.build}});Object.defineProperty(t,"createValidAbsoluteUrl",{enumerable:!0,get:function(){return e.createValidAbsoluteUrl}});Object.defineProperty(t,"getDocument",{enumerable:!0,get:function(){return i.getDocument}});Object.defineProperty(t,"getFilenameFromUrl",{enumerable:!0,get:function(){return s.getFilenameFromUrl}});Object.defineProperty(t,"getPdfFilenameFromUrl",{enumerable:!0,get:function(){return s.getPdfFilenameFromUrl}});Object.defineProperty(t,"getXfaPageViewport",{enumerable:!0,get:function(){return s.getXfaPageViewport}});Object.defineProperty(t,"isDataScheme",{enumerable:!0,get:function(){return s.isDataScheme}});Object.defineProperty(t,"isPdfFile",{enumerable:!0,get:function(){return s.isPdfFile}});Object.defineProperty(t,"loadScript",{enumerable:!0,get:function(){return s.loadScript}});Object.defineProperty(t,"noContextMenu",{enumerable:!0,get:function(){return s.noContextMenu}});Object.defineProperty(t,"normalizeUnicode",{enumerable:!0,get:function(){return e.normalizeUnicode}});Object.defineProperty(t,"renderTextLayer",{enumerable:!0,get:function(){return n.renderTextLayer}});Object.defineProperty(t,"setLayerDimensions",{enumerable:!0,get:function(){return s.setLayerDimensions}});Object.defineProperty(t,"shadow",{enumerable:!0,get:function(){return e.shadow}});Object.defineProperty(t,"updateTextLayer",{enumerable:!0,get:function(){return n.updateTextLayer}});Object.defineProperty(t,"version",{enumerable:!0,get:function(){return i.version}});var e=__w_pdfjs_require__(1),i=__w_pdfjs_require__(2),s=__w_pdfjs_require__(6),n=__w_pdfjs_require__(26),a=__w_pdfjs_require__(27),r=__w_pdfjs_require__(5),o=__w_pdfjs_require__(29),l=__w_pdfjs_require__(14),h=__w_pdfjs_require__(32)})();return __webpack_exports__})()));
\ No newline at end of file
diff --git a/player/agent/internal/playerserver/assets/pdf.worker.min.js b/player/agent/internal/playerserver/assets/pdf.worker.min.js
new file mode 100644
index 0000000..1224226
--- /dev/null
+++ b/player/agent/internal/playerserver/assets/pdf.worker.min.js
@@ -0,0 +1,22 @@
+/**
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this page
+ *
+ * Copyright 2023 Mozilla Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @licend The above is the entire license notice for the
+ * JavaScript code in this page
+ */
+!function webpackUniversalModuleDefinition(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=e.pdfjsWorker=t():"function"==typeof define&&define.amd?define("pdfjs-dist/build/pdf.worker",[],(()=>e.pdfjsWorker=t())):"object"==typeof exports?exports["pdfjs-dist/build/pdf.worker"]=e.pdfjsWorker=t():e["pdfjs-dist/build/pdf.worker"]=e.pdfjsWorker=t()}(globalThis,(()=>(()=>{"use strict";var e=[,(e,t,a)=>{Object.defineProperty(t,"__esModule",{value:!0});t.WorkerTask=t.WorkerMessageHandler=void 0;var r=a(2),n=a(3),i=a(4),s=a(6),o=a(10),c=a(68),l=a(73),h=a(104),u=a(105),d=a(72);class WorkerTask{constructor(e){this.name=e;this.terminated=!1;this._capability=new r.PromiseCapability}get finished(){return this._capability.promise}finish(){this._capability.resolve()}terminate(){this.terminated=!0}ensureNotTerminated(){if(this.terminated)throw new Error("Worker task was terminated")}}t.WorkerTask=WorkerTask;class WorkerMessageHandler{static setup(e,t){let a=!1;e.on("test",(function(t){if(!a){a=!0;e.send("test",t instanceof Uint8Array)}}));e.on("configure",(function(e){(0,r.setVerbosityLevel)(e.verbosity)}));e.on("GetDocRequest",(function(e){return WorkerMessageHandler.createDocumentHandler(e,t)}))}static createDocumentHandler(e,t){let a,f=!1,g=null;const p=new Set,m=(0,r.getVerbosityLevel)(),{docId:b,apiVersion:y}=e,w="3.11.174";if(y!==w)throw new Error(`The API version "${y}" does not match the Worker version "${w}".`);const S=[];for(const e in[])S.push(e);if(S.length)throw new Error("The `Array.prototype` contains unexpected enumerable properties: "+S.join(", ")+"; thus breaking e.g. `for...in` iteration of `Array`s.");const x=b+"_worker";let C=new h.MessageHandler(x,b,t);function ensureNotTerminated(){if(f)throw new Error("Worker was terminated")}function startWorkerTask(e){p.add(e)}function finishWorkerTask(e){e.finish();p.delete(e)}async function loadDocument(e){await a.ensureDoc("checkHeader");await a.ensureDoc("parseStartXRef");await a.ensureDoc("parse",[e]);await a.ensureDoc("checkFirstPage",[e]);await a.ensureDoc("checkLastPage",[e]);const t=await a.ensureDoc("isPureXfa");if(t){const e=new WorkerTask("loadXfaFonts");startWorkerTask(e);await Promise.all([a.loadXfaFonts(C,e).catch((e=>{})).then((()=>finishWorkerTask(e))),a.loadXfaImages()])}const[r,n]=await Promise.all([a.ensureDoc("numPages"),a.ensureDoc("fingerprints")]);return{numPages:r,fingerprints:n,htmlForXfa:t?await a.ensureDoc("htmlForXfa"):null}}function getPdfManager({data:e,password:t,disableAutoFetch:a,rangeChunkSize:i,length:o,docBaseUrl:c,enableXfa:l,evaluatorOptions:h}){const d={source:null,disableAutoFetch:a,docBaseUrl:c,docId:b,enableXfa:l,evaluatorOptions:h,handler:C,length:o,password:t,rangeChunkSize:i},f=new r.PromiseCapability;let p;if(e){try{d.source=e;p=new s.LocalPdfManager(d);f.resolve(p)}catch(e){f.reject(e)}return f.promise}let m,y=[];try{m=new u.PDFWorkerStream(C)}catch(e){f.reject(e);return f.promise}const w=m.getFullReader();w.headersReady.then((function(){if(w.isRangeSupported){d.source=m;d.length=w.contentLength;d.disableAutoFetch||=w.isStreamingSupported;p=new s.NetworkPdfManager(d);for(const e of y)p.sendProgressiveData(e);y=[];f.resolve(p);g=null}})).catch((function(e){f.reject(e);g=null}));let S=0;new Promise((function(e,t){const readChunk=function({value:e,done:a}){try{ensureNotTerminated();if(a){p||function(){const e=(0,n.arrayBuffersToBytes)(y);o&&e.length!==o&&(0,r.warn)("reported HTTP length is different from actual");try{d.source=e;p=new s.LocalPdfManager(d);f.resolve(p)}catch(e){f.reject(e)}y=[]}();g=null;return}S+=e.byteLength;w.isStreamingSupported||C.send("DocProgress",{loaded:S,total:Math.max(S,w.contentLength||0)});p?p.sendProgressiveData(e):y.push(e);w.read().then(readChunk,t)}catch(e){t(e)}};w.read().then(readChunk,t)})).catch((function(e){f.reject(e);g=null}));g=function(e){m.cancelAllRequests(e)};return f.promise}C.on("GetPage",(function(e){return a.getPage(e.pageIndex).then((function(e){return Promise.all([a.ensure(e,"rotate"),a.ensure(e,"ref"),a.ensure(e,"userUnit"),a.ensure(e,"view")]).then((function([e,t,a,r]){return{rotate:e,ref:t,userUnit:a,view:r}}))}))}));C.on("GetPageIndex",(function(e){const t=i.Ref.get(e.num,e.gen);return a.ensureCatalog("getPageIndex",[t])}));C.on("GetDestinations",(function(e){return a.ensureCatalog("destinations")}));C.on("GetDestination",(function(e){return a.ensureCatalog("getDestination",[e.id])}));C.on("GetPageLabels",(function(e){return a.ensureCatalog("pageLabels")}));C.on("GetPageLayout",(function(e){return a.ensureCatalog("pageLayout")}));C.on("GetPageMode",(function(e){return a.ensureCatalog("pageMode")}));C.on("GetViewerPreferences",(function(e){return a.ensureCatalog("viewerPreferences")}));C.on("GetOpenAction",(function(e){return a.ensureCatalog("openAction")}));C.on("GetAttachments",(function(e){return a.ensureCatalog("attachments")}));C.on("GetDocJSActions",(function(e){return a.ensureCatalog("jsActions")}));C.on("GetPageJSActions",(function({pageIndex:e}){return a.getPage(e).then((function(e){return a.ensure(e,"jsActions")}))}));C.on("GetOutline",(function(e){return a.ensureCatalog("documentOutline")}));C.on("GetOptionalContentConfig",(function(e){return a.ensureCatalog("optionalContentConfig")}));C.on("GetPermissions",(function(e){return a.ensureCatalog("permissions")}));C.on("GetMetadata",(function(e){return Promise.all([a.ensureDoc("documentInfo"),a.ensureCatalog("metadata")])}));C.on("GetMarkInfo",(function(e){return a.ensureCatalog("markInfo")}));C.on("GetData",(function(e){return a.requestLoadedStream().then((function(e){return e.bytes}))}));C.on("GetAnnotations",(function({pageIndex:e,intent:t}){return a.getPage(e).then((function(a){const r=new WorkerTask(`GetAnnotations: page ${e}`);startWorkerTask(r);return a.getAnnotationsData(C,r,t).then((e=>{finishWorkerTask(r);return e}),(e=>{finishWorkerTask(r);throw e}))}))}));C.on("GetFieldObjects",(function(e){return a.ensureDoc("fieldObjects")}));C.on("HasJSActions",(function(e){return a.ensureDoc("hasJSActions")}));C.on("GetCalculationOrderIds",(function(e){return a.ensureDoc("calculationOrderIds")}));C.on("SaveDocument",(async function({isPureXfa:e,numPages:t,annotationStorage:s,filename:c}){const h=[a.requestLoadedStream(),a.ensureCatalog("acroForm"),a.ensureCatalog("acroFormRef"),a.ensureDoc("startXRef"),a.ensureDoc("xref"),a.ensureDoc("linearization"),a.ensureCatalog("structTreeRoot")],u=[],f=e?null:(0,n.getNewAnnotationsMap)(s),[g,p,m,b,y,w,S]=await Promise.all(h),x=y.trailer.getRaw("Root")||null;let k;if(f){S?await S.canUpdateStructTree({pdfManager:a,newAnnotationsByPage:f})&&(k=S):await d.StructTreeRoot.canCreateStructureTree({catalogRef:x,pdfManager:a,newAnnotationsByPage:f})&&(k=null);const e=o.AnnotationFactory.generateImages(s.values(),y,a.evaluatorOptions.isOffscreenCanvasSupported),t=void 0===k?u:[];for(const[r,n]of f)t.push(a.getPage(r).then((t=>{const a=new WorkerTask(`Save (editor): page ${r}`);return t.saveNewAnnotations(C,a,n,e).finally((function(){finishWorkerTask(a)}))})));null===k?u.push(Promise.all(t).then((async e=>{await d.StructTreeRoot.createStructureTree({newAnnotationsByPage:f,xref:y,catalogRef:x,pdfManager:a,newRefs:e});return e}))):k&&u.push(Promise.all(t).then((async e=>{await k.updateStructureTree({newAnnotationsByPage:f,pdfManager:a,newRefs:e});return e})))}if(e)u.push(a.serializeXfaData(s));else for(let e=0;e1)return null;e=t.join("");M.push(e);let a=0;const r=y.charsToGlyphs(e);for(const e of r)a+=e.width*O;T=Math.max(T,a)}let D=1;T>k&&(D=k/T);let E=1;const N=r.LINE_FACTOR*d,R=(r.LINE_FACTOR-r.LINE_DESCENT_FACTOR)*d,L=N*F.length;L>v&&(E=v/L);const $=d*Math.min(D,E);let _,j,U;switch(g){case 0:U=[1,0,0,1];j=[f[0],f[1],k,v];_=[f[0],f[3]-R];break;case 90:U=[0,1,-1,0];j=[f[1],-f[2],k,v];_=[f[1],-f[0]-R];break;case 180:U=[-1,0,0,-1];j=[-f[2],-f[3],k,v];_=[-f[2],-f[1]-R];break;case 270:U=[0,-1,1,0];j=[-f[3],f[0],k,v];_=[-f[3],f[2]-R]}const X=["q",`${U.join(" ")} 0 0 cm`,`${j.join(" ")} re W n`,"BT",`${(0,i.getPdfColor)(u,!0)}`,`0 Tc /Helv ${(0,n.numberToString)($)} Tf`];X.push(`${_.join(" ")} Td (${(0,n.escapeString)(M[0])}) Tj`);const H=(0,n.numberToString)(N);for(let e=1,t=M.length;e{e.push((t[0].x+t[2].x)/2+" "+(t[0].y+t[2].y)/2+" m",(t[1].x+t[3].x)/2+" "+(t[1].y+t[3].y)/2+" l","S");return[t[0].x,t[1].x,t[3].y,t[1].y]}})}}else this.data.popupRef=null}}class StampAnnotation extends MarkupAnnotation{constructor(e){super(e);this.data.annotationType=r.AnnotationType.STAMP;this.data.hasOwnCanvas=this.data.noRotate}static async createImage(e,t){const{width:a,height:n}=e,i=new OffscreenCanvas(a,n),c=i.getContext("2d",{alpha:!0});c.drawImage(e,0,0);const l=c.getImageData(0,0,a,n).data,h=new Uint32Array(l.buffer),u=h.some(r.FeatureTest.isLittleEndian?e=>e>>>24!=255:e=>255!=(255&e));if(u){c.fillStyle="white";c.fillRect(0,0,a,n);c.drawImage(e,0,0)}const d=i.convertToBlob({type:"image/jpeg",quality:1}).then((e=>e.arrayBuffer())),f=s.Name.get("XObject"),g=s.Name.get("Image"),p=new s.Dict(t);p.set("Type",f);p.set("Subtype",g);p.set("BitsPerComponent",8);p.set("ColorSpace",s.Name.get("DeviceRGB"));p.set("Filter",s.Name.get("DCTDecode"));p.set("BBox",[0,0,a,n]);p.set("Width",a);p.set("Height",n);let m=null;if(u){const e=new Uint8Array(h.length);if(r.FeatureTest.isLittleEndian)for(let t=0,a=h.length;t>>24;else for(let t=0,a=h.length;t=0&&o<=1?o:null}}},(e,t,a)=>{Object.defineProperty(t,"__esModule",{value:!0});t.FakeUnicodeFont=void 0;t.createDefaultAppearance=function createDefaultAppearance({fontSize:e,fontName:t,fontColor:a}){return`/${(0,n.escapePDFName)(t)} ${e} Tf ${getPdfColor(a,!0)}`};t.getPdfColor=getPdfColor;t.parseAppearanceStream=function parseAppearanceStream(e,t,a){return new AppearanceStreamEvaluator(e,t,a).parse()};t.parseDefaultAppearance=function parseDefaultAppearance(e){return new DefaultAppearanceEvaluator(e).parse()};var r=a(4),n=a(3),i=a(2),s=a(12),o=a(13),c=a(59),l=a(57),h=a(8);class DefaultAppearanceEvaluator extends o.EvaluatorPreprocessor{constructor(e){super(new h.StringStream(e))}parse(){const e={fn:0,args:[]},t={fontSize:0,fontName:"",fontColor:new Uint8ClampedArray(3)};try{for(;;){e.args.length=0;if(!this.read(e))break;if(0!==this.savedStatesDepth)continue;const{fn:a,args:n}=e;switch(0|a){case i.OPS.setFont:const[e,a]=n;e instanceof r.Name&&(t.fontName=e.name);"number"==typeof a&&a>0&&(t.fontSize=a);break;case i.OPS.setFillRGBColor:s.ColorSpace.singletons.rgb.getRgbItem(n,0,t.fontColor,0);break;case i.OPS.setFillGray:s.ColorSpace.singletons.gray.getRgbItem(n,0,t.fontColor,0);break;case i.OPS.setFillCMYKColor:s.ColorSpace.singletons.cmyk.getRgbItem(n,0,t.fontColor,0)}}}catch(e){(0,i.warn)(`parseDefaultAppearance - ignoring errors: "${e}".`)}return t}}class AppearanceStreamEvaluator extends o.EvaluatorPreprocessor{constructor(e,t,a){super(e);this.stream=e;this.evaluatorOptions=t;this.xref=a;this.resources=e.dict?.get("Resources")}parse(){const e={fn:0,args:[]};let t={scaleFactor:1,fontSize:0,fontName:"",fontColor:new Uint8ClampedArray(3),fillColorSpace:s.ColorSpace.singletons.gray},a=!1;const n=[];try{for(;;){e.args.length=0;if(a||!this.read(e))break;const{fn:o,args:c}=e;switch(0|o){case i.OPS.save:n.push({scaleFactor:t.scaleFactor,fontSize:t.fontSize,fontName:t.fontName,fontColor:t.fontColor.slice(),fillColorSpace:t.fillColorSpace});break;case i.OPS.restore:t=n.pop()||t;break;case i.OPS.setTextMatrix:t.scaleFactor*=Math.hypot(c[0],c[1]);break;case i.OPS.setFont:const[e,o]=c;e instanceof r.Name&&(t.fontName=e.name);"number"==typeof o&&o>0&&(t.fontSize=o*t.scaleFactor);break;case i.OPS.setFillColorSpace:t.fillColorSpace=s.ColorSpace.parse({cs:c[0],xref:this.xref,resources:this.resources,pdfFunctionFactory:this._pdfFunctionFactory,localColorSpaceCache:this._localColorSpaceCache});break;case i.OPS.setFillColor:t.fillColorSpace.getRgbItem(c,0,t.fontColor,0);break;case i.OPS.setFillRGBColor:s.ColorSpace.singletons.rgb.getRgbItem(c,0,t.fontColor,0);break;case i.OPS.setFillGray:s.ColorSpace.singletons.gray.getRgbItem(c,0,t.fontColor,0);break;case i.OPS.setFillCMYKColor:s.ColorSpace.singletons.cmyk.getRgbItem(c,0,t.fontColor,0);break;case i.OPS.showText:case i.OPS.showSpacedText:case i.OPS.nextLineShowText:case i.OPS.nextLineSetSpacingShowText:a=!0}}}catch(e){(0,i.warn)(`parseAppearanceStream - ignoring errors: "${e}".`)}this.stream.reset();delete t.scaleFactor;delete t.fillColorSpace;return t}get _localColorSpaceCache(){return(0,i.shadow)(this,"_localColorSpaceCache",new c.LocalColorSpaceCache)}get _pdfFunctionFactory(){const e=new l.PDFFunctionFactory({xref:this.xref,isEvalSupported:this.evaluatorOptions.isEvalSupported});return(0,i.shadow)(this,"_pdfFunctionFactory",e)}}function getPdfColor(e,t){if(e[0]===e[1]&&e[1]===e[2]){const a=e[0]/255;return`${(0,n.numberToString)(a)} ${t?"g":"G"}`}return Array.from(e,(e=>(0,n.numberToString)(e/255))).join(" ")+" "+(t?"rg":"RG")}class FakeUnicodeFont{constructor(e,t){this.xref=e;this.widths=null;this.firstChar=1/0;this.lastChar=-1/0;this.fontFamily=t;const a=new OffscreenCanvas(1,1);this.ctxMeasure=a.getContext("2d");FakeUnicodeFont._fontNameId||(FakeUnicodeFont._fontNameId=1);this.fontName=r.Name.get(`InvalidPDFjsFont_${t}_${FakeUnicodeFont._fontNameId++}`)}get toUnicodeRef(){if(!FakeUnicodeFont._toUnicodeRef){const e="/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n/CIDSystemInfo\n<< /Registry (Adobe)\n/Ordering (UCS) /Supplement 0 >> def\n/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n1 begincodespacerange\n<0000> u&&"DeviceGray"!==this.name&&"DeviceRGB"!==this.name){const t=s<=8?new Uint8Array(u):new Uint16Array(u);for(let e=0;e=.99554525?1:this.#m(0,1,1.055*e**(1/2.4)-.055)}#m(e,t,a){return Math.max(e,Math.min(t,a))}#b(e){return e<0?-this.#b(-e):e>8?((e+16)/116)**3:e*CalRGBCS.#u}#y(e,t,a){if(0===e[0]&&0===e[1]&&0===e[2]){a[0]=t[0];a[1]=t[1];a[2]=t[2];return}const r=this.#b(0),n=(1-r)/(1-this.#b(e[0])),i=1-n,s=(1-r)/(1-this.#b(e[1])),o=1-s,c=(1-r)/(1-this.#b(e[2])),l=1-c;a[0]=t[0]*n+i;a[1]=t[1]*s+o;a[2]=t[2]*c+l}#w(e,t,a){if(1===e[0]&&1===e[2]){a[0]=t[0];a[1]=t[1];a[2]=t[2];return}const r=a;this.#d(CalRGBCS.#n,t,r);const n=CalRGBCS.#c;this.#f(e,r,n);this.#d(CalRGBCS.#i,n,a)}#S(e,t,a){const r=a;this.#d(CalRGBCS.#n,t,r);const n=CalRGBCS.#c;this.#g(e,r,n);this.#d(CalRGBCS.#i,n,a)}#r(e,t,a,r,n){const i=this.#m(0,1,e[t]*n),s=this.#m(0,1,e[t+1]*n),o=this.#m(0,1,e[t+2]*n),c=1===i?1:i**this.GR,l=1===s?1:s**this.GG,h=1===o?1:o**this.GB,u=this.MXA*c+this.MXB*l+this.MXC*h,d=this.MYA*c+this.MYB*l+this.MYC*h,f=this.MZA*c+this.MZB*l+this.MZC*h,g=CalRGBCS.#l;g[0]=u;g[1]=d;g[2]=f;const p=CalRGBCS.#h;this.#w(this.whitePoint,g,p);const m=CalRGBCS.#l;this.#y(this.blackPoint,p,m);const b=CalRGBCS.#h;this.#S(CalRGBCS.#o,m,b);const y=CalRGBCS.#l;this.#d(CalRGBCS.#s,b,y);a[r]=255*this.#p(y[0]);a[r+1]=255*this.#p(y[1]);a[r+2]=255*this.#p(y[2])}getRgbItem(e,t,a,r){this.#r(e,t,a,r,1)}getRgbBuffer(e,t,a,r,n,i,s){const o=1/((1<this.amax||this.bmin>this.bmax){(0,r.info)("Invalid Range, falling back to defaults");this.amin=-100;this.amax=100;this.bmin=-100;this.bmax=100}}#x(e){return e>=6/29?e**3:108/841*(e-4/29)}#A(e,t,a,r){return a+e*(r-a)/t}#r(e,t,a,r,n){let i=e[t],s=e[t+1],o=e[t+2];if(!1!==a){i=this.#A(i,a,0,100);s=this.#A(s,a,this.amin,this.amax);o=this.#A(o,a,this.bmin,this.bmax)}s>this.amax?s=this.amax:s.5*f.width){appendEOL();return!0}resetLastChars();flushTextContentItem();return!0}if(Math.abs(t)>f.width){appendEOL();return!0}e<=s*f.notASpace&&resetLastChars();if(e<=s*f.trackingSpaceMin)if(shouldAddWhitepsace()){resetLastChars();flushTextContentItem();pushWhitespace({height:Math.abs(e)})}else f.height+=e;else if(!addFakeSpaces(e,f.prevTransform,s))if(0===f.str.length){resetLastChars();pushWhitespace({height:Math.abs(e)})}else f.height+=e;Math.abs(t)>.25*f.width&&flushTextContentItem();return!0}const o=(a-n)/f.textAdvanceScale,c=r-i,h=Math.sign(f.width);if(o>15&1;this.clow=this.clow<<1&65535;this.ct--}while(0==(32768&c));this.a=c;e[t]=r<<1|n;return o}}},(e,t,a)=>{Object.defineProperty(t,"__esModule",{value:!0});t.JpegStream=void 0;var r=a(18),n=a(4),i=a(27),s=a(2);class JpegStream extends r.DecodeStream{constructor(e,t,a){let r;for(;-1!==(r=e.getByte());)if(255===r){e.skip(-1);break}super(t);this.stream=e;this.dict=e.dict;this.maybeLength=t;this.params=a}get bytes(){return(0,s.shadow)(this,"bytes",this.stream.getBytes(this.maybeLength))}ensureBuffer(e){}readBlock(){if(this.eof)return;const e={decodeTransform:void 0,colorTransform:void 0},t=this.dict.getArray("D","Decode");if((this.forceRGBA||this.forceRGB)&&Array.isArray(t)){const a=this.dict.get("BPC","BitsPerComponent")||8,r=t.length,n=new Int32Array(r);let i=!1;const s=(1<{Object.defineProperty(t,"__esModule",{value:!0});t.JpegImage=void 0;var r=a(2),n=a(28),i=a(3);class JpegError extends r.BaseException{constructor(e){super(`JPEG error: ${e}`,"JpegError")}}class DNLMarkerError extends r.BaseException{constructor(e,t){super(e,"DNLMarkerError");this.scanLines=t}}class EOIMarkerError extends r.BaseException{constructor(e){super(e,"EOIMarkerError")}}const s=new Uint8Array([0,1,8,16,9,2,3,10,17,24,32,25,18,11,4,5,12,19,26,33,40,48,41,34,27,20,13,6,7,14,21,28,35,42,49,56,57,50,43,36,29,22,15,23,30,37,44,51,58,59,52,45,38,31,39,46,53,60,61,54,47,55,62,63]),o=4017,c=799,l=3406,h=2276,u=1567,d=3784,f=5793,g=2896;function buildHuffmanTable(e,t){let a,r,n=0,i=16;for(;i>0&&!e[i-1];)i--;const s=[{children:[],index:0}];let o,c=s[0];for(a=0;a0;)c=s.pop();c.index++;s.push(c);for(;s.length<=a;){s.push(o={children:[],index:0});c.children[c.index]=o.children;c=o}n++}if(a+10){b--;return m>>b&1}m=e[t++];if(255===m){const r=e[t++];if(r){if(220===r&&d){t+=2;const r=(0,i.readUint16)(e,t);t+=2;if(r>0&&r!==a.scanLines)throw new DNLMarkerError("Found DNL marker (0xFFDC) while parsing scan data",r)}else if(217===r){if(d){const e=x*(8===a.precision?8:0);if(e>0&&Math.round(a.scanLines/e)>=5)throw new DNLMarkerError("Found EOI marker (0xFFD9) while parsing scan data, possibly caused by incorrect `scanLines` parameter",e)}throw new EOIMarkerError("Found EOI marker (0xFFD9) while parsing scan data")}throw new JpegError(`unexpected marker ${(m<<8|r).toString(16)}`)}}b=7;return m>>>7}function decodeHuffman(e){let t=e;for(;;){t=t[readBit()];switch(typeof t){case"number":return t;case"object":continue}throw new JpegError("invalid huffman sequence")}}function receive(e){let t=0;for(;e>0;){t=t<<1|readBit();e--}return t}function receiveAndExtend(e){if(1===e)return 1===readBit()?1:-1;const t=receive(e);return t>=1<>4==0)for(m=0;m<64;m++){x=s[m];a[x]=e[o++]}else{if(t>>4!=1)throw new JpegError("DQT - invalid table spec");for(m=0;m<64;m++){x=s[m];a[x]=(0,i.readUint16)(e,o);o+=2}}u[15&t]=a}break;case 65472:case 65473:case 65474:if(a)throw new JpegError("Only single frame JPEGs supported");o+=2;a={};a.extended=65473===g;a.progressive=65474===g;a.precision=e[o++];const C=(0,i.readUint16)(e,o);o+=2;a.scanLines=t||C;a.samplesPerLine=(0,i.readUint16)(e,o);o+=2;a.components=[];a.componentIds={};const k=e[o++];let v=0,F=0;for(p=0;p0?Math.min(r.xcb,n.PPx-1):Math.min(r.xcb,n.PPx);n.ycb_=a>0?Math.min(r.ycb,n.PPy-1):Math.min(r.ycb,n.PPy);return n}function buildPrecincts(e,t,a){const r=1<0,o=t+1=0;t--){d[t]=o[a];a=l[a]}}else d[f++]=d[0]}if(n){l[s]=u;c[s]=c[u]+1;o[s]=d[0];s++;h=s+i&s+i-1?h:0|Math.min(Math.log(s+i)/.6931471805599453+1,12)}u=e;g+=f;if(r