diff --git a/.gitignore b/.gitignore index 291344a..ce0ff02 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ staticfiles/ .pytest_cache/ .mypy_cache/ .DS_Store +backups/ +backend/backups/ diff --git a/Makefile b/Makefile index 269a172..878034a 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ COMPOSE ?= docker compose -.PHONY: i18n-extract-en i18n-extract-de i18n-compile i18n-update-en i18n-update-de +.PHONY: i18n-extract-en i18n-extract-de i18n-compile i18n-update-en i18n-update-de backup-create backup-verify i18n-extract-en: $(COMPOSE) exec -T web django-admin makemessages -l en @@ -14,3 +14,10 @@ i18n-compile: i18n-update-en: i18n-extract-en i18n-compile i18n-update-de: i18n-extract-de i18n-compile + +backup-create: + ./scripts/backup_create.sh + +backup-verify: + @if [ -z "$(BACKUP_DIR)" ]; then echo "Usage: make backup-verify BACKUP_DIR=backups/backup_YYYYmmdd_HHMMSS"; exit 1; fi + ./scripts/backup_verify.sh "$(BACKUP_DIR)" diff --git a/README.md b/README.md index 5fc601d..8730ed8 100644 --- a/README.md +++ b/README.md @@ -78,3 +78,24 @@ Run a real workflow verification (onboarding + offboarding), including PDF check - `docker compose exec -T web python manage.py run_staging_e2e_check --email-check mailhog --mailhog-api-url http://mailhog:8025/api/v2/messages` - Skip Nextcloud existence checks: - `docker compose exec -T web python manage.py run_staging_e2e_check --skip-nextcloud` + +## Backup and restore +Use the repo-level scripts so database and media backups stay consistent. + +- Create a backup bundle: + - `make backup-create` +- Verify a backup non-destructively: + - `make backup-verify BACKUP_DIR=backend/backups/backup_YYYYmmdd_HHMMSS` +- Real restore: + - `./scripts/backup_restore.sh --yes-restore backend/backups/backup_YYYYmmdd_HHMMSS` + +What is included: +- PostgreSQL custom-format dump: `db.dump` +- media archive: `media.tar.gz` +- metadata file and SHA256 checksums +- default storage path: `backend/backups/` + +Verification behavior: +- restores the dump into a temporary verification database +- extracts media into a temporary directory +- checks that the restored DB and media structure are readable diff --git a/backend/Dockerfile b/backend/Dockerfile index 9091816..c9f8682 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -6,7 +6,7 @@ ENV PYTHONUNBUFFERED=1 WORKDIR /app RUN apt-get update \ - && apt-get install -y --no-install-recommends build-essential netcat-openbsd gettext \ + && apt-get install -y --no-install-recommends build-essential netcat-openbsd gettext postgresql-client \ && rm -rf /var/lib/apt/lists/* RUN groupadd -g 1000 app && useradd -u 1000 -g app -m app diff --git a/backend/config/settings.py b/backend/config/settings.py index 45a9181..4e4d7b2 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -140,6 +140,8 @@ PDF_TEMPLATES_DIR = MEDIA_ROOT / 'templates' PDF_TEMPLATES_DIR.mkdir(parents=True, exist_ok=True) EMAIL_TEXT_DIR = MEDIA_ROOT / 'email_text' EMAIL_TEXT_DIR.mkdir(parents=True, exist_ok=True) +BACKUP_OUTPUT_DIR = BASE_DIR / 'backups' +BACKUP_OUTPUT_DIR.mkdir(parents=True, exist_ok=True) ONBOARDING_SHARED_PDF_LINK = os.getenv('ONBOARDING_SHARED_PDF_LINK', '') SMTP_TIMEOUT_SECONDS = int(os.getenv('SMTP_TIMEOUT_SECONDS', '20')) diff --git a/backend/locale/en/LC_MESSAGES/django.mo b/backend/locale/en/LC_MESSAGES/django.mo index 46ff1ed..bb415ac 100644 Binary files a/backend/locale/en/LC_MESSAGES/django.mo and b/backend/locale/en/LC_MESSAGES/django.mo differ diff --git a/backend/locale/en/LC_MESSAGES/django.po b/backend/locale/en/LC_MESSAGES/django.po index 705a0dc..6d0b407 100644 --- a/backend/locale/en/LC_MESSAGES/django.po +++ b/backend/locale/en/LC_MESSAGES/django.po @@ -2,13 +2,59 @@ msgid "" msgstr "" "Project-Id-Version: tubco-portal\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-25 23:29+0000\n" +"POT-Creation-Date: 2026-03-26 00:10+0000\n" "PO-Revision-Date: 2026-03-24 00:00+0000\n" "Language: en\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +#: workflows/backup_ops.py:141 +msgid "Remote Backup ist deaktiviert." +msgstr "" + +#: workflows/backup_ops.py:146 +#, python-format +msgid "Zieltyp %(target)s ist vorbereitet, aber noch nicht implementiert." +msgstr "" + +#: workflows/backup_ops.py:152 +msgid "Nextcloud Backup-Verzeichnis fehlt." +msgstr "" + +#: workflows/backup_ops.py:170 +#, python-format +msgid "Upload nach Nextcloud fehlgeschlagen bei %(file)s." +msgstr "" + +#: workflows/backup_ops.py:176 +#, python-format +msgid "Nach Nextcloud hochgeladen: %(count)s Datei(en)." +msgstr "" + +#: workflows/backup_ops.py:239 workflows/backup_ops.py:318 +msgid "Backup-Dateien nicht gefunden." +msgstr "" + +#: workflows/backup_ops.py:289 +msgid "Media-Archiv enthält kein media/-Verzeichnis." +msgstr "" + +#: workflows/backup_ops.py:291 +#, python-format +msgid "" +"%(tables)s Tabellen, %(onboarding)s Onboarding, %(offboarding)s Offboarding, " +"%(media)s Mediendateien geprüft." +msgstr "" + +#: workflows/backup_ops.py:316 +msgid "Ungültiger Backup-Pfad." +msgstr "" + +#: workflows/backup_ops.py:323 +msgid "Remote Backup in Nextcloud konnte nicht gelöscht werden." +msgstr "" + #: workflows/forms.py:338 #, python-format msgid "" @@ -16,21 +62,22 @@ msgid "" "(frühestens %(date)s)." msgstr "" -#: workflows/models.py:55 workflows/views.py:180 +#: workflows/models.py:55 workflows/views.py:181 msgid "Eingereicht" msgstr "Submitted" -#: workflows/models.py:56 workflows/views.py:181 +#: workflows/models.py:56 workflows/views.py:182 msgid "In Bearbeitung" msgstr "Processing" -#: workflows/models.py:57 workflows/models.py:372 workflows/views.py:182 +#: workflows/models.py:57 workflows/models.py:372 workflows/views.py:183 msgid "Abgeschlossen" msgstr "Completed" #: workflows/models.py:58 workflows/models.py:312 -#: workflows/templates/workflows/requests_dashboard.html:226 -#: workflows/templates/workflows/welcome_emails.html:112 workflows/views.py:183 +#: workflows/templates/workflows/backup_recovery.html:70 +#: workflows/templates/workflows/requests_dashboard.html:222 +#: workflows/templates/workflows/welcome_emails.html:108 workflows/views.py:184 msgid "Fehlgeschlagen" msgstr "Failed" @@ -55,8 +102,8 @@ msgid "unbefristet" msgstr "" #: workflows/models.py:138 -#: workflows/templates/workflows/onboarding_intro_session.html:32 -#: workflows/templates/workflows/requests_dashboard.html:149 +#: workflows/templates/workflows/onboarding_intro_session.html:28 +#: workflows/templates/workflows/requests_dashboard.html:145 msgid "Abteilung" msgstr "Department" @@ -90,34 +137,34 @@ msgstr "" msgid "Automatisch" msgstr "" -#: workflows/models.py:171 workflows/views.py:86 +#: workflows/models.py:171 workflows/views.py:87 msgid "Stammdaten" msgstr "Master data" -#: workflows/models.py:172 workflows/views.py:87 +#: workflows/models.py:172 workflows/views.py:88 msgid "Vertrag" msgstr "Contract" -#: workflows/models.py:173 workflows/views.py:88 +#: workflows/models.py:173 workflows/views.py:89 msgid "IT-Setup" msgstr "IT setup" -#: workflows/models.py:174 workflows/views.py:89 +#: workflows/models.py:174 workflows/views.py:90 msgid "Abschluss" msgstr "Finish" #: workflows/models.py:177 workflows/models.py:258 -#: workflows/templates/workflows/home.html:66 +#: workflows/templates/workflows/home.html:62 #: workflows/templates/workflows/onboarding_form.html:25 #: workflows/templates/workflows/requests_dashboard.html:68 -#: workflows/templates/workflows/requests_dashboard.html:135 +#: workflows/templates/workflows/requests_dashboard.html:131 msgid "Onboarding" msgstr "Onboarding" #: workflows/models.py:178 workflows/models.py:259 -#: workflows/templates/workflows/home.html:82 +#: workflows/templates/workflows/home.html:78 #: workflows/templates/workflows/requests_dashboard.html:78 -#: workflows/templates/workflows/requests_dashboard.html:136 +#: workflows/templates/workflows/requests_dashboard.html:132 msgid "Offboarding" msgstr "Offboarding" @@ -206,22 +253,22 @@ msgid "Ist inaktiv/Nein" msgstr "inactive" #: workflows/models.py:308 -#: workflows/templates/workflows/welcome_emails.html:104 +#: workflows/templates/workflows/welcome_emails.html:100 msgid "Geplant" msgstr "Scheduled" #: workflows/models.py:309 -#: workflows/templates/workflows/welcome_emails.html:106 +#: workflows/templates/workflows/welcome_emails.html:102 msgid "Pausiert" msgstr "Paused" #: workflows/models.py:310 -#: workflows/templates/workflows/welcome_emails.html:108 +#: workflows/templates/workflows/welcome_emails.html:104 msgid "Abgebrochen" msgstr "Cancelled" #: workflows/models.py:311 -#: workflows/templates/workflows/welcome_emails.html:110 +#: workflows/templates/workflows/welcome_emails.html:106 msgid "Gesendet" msgstr "Sent" @@ -257,6 +304,20 @@ msgstr "Is no / inactive" msgid "Entwurf" msgstr "Draft" +#: workflows/models.py:391 +#, fuzzy +#| msgid "Nextcloud:" +msgid "Nextcloud" +msgstr "Nextcloud:" + +#: workflows/models.py:392 +msgid "S3" +msgstr "" + +#: workflows/models.py:393 +msgid "NFS" +msgstr "" + #: workflows/tasks.py:591 #, python-format msgid "%(item)s übergeben und Grundfunktionen erklärt" @@ -350,24 +411,24 @@ msgstr "Sign in" msgid "Bitte melden Sie sich mit Ihrem Benutzerkonto an." msgstr "Please sign in with your user account." -#: workflows/templates/registration/login.html:29 +#: workflows/templates/registration/login.html:30 #, fuzzy #| msgid "Fehlgeschlagen" msgid "Anmeldung fehlgeschlagen" msgstr "Failed" -#: workflows/templates/registration/login.html:30 +#: workflows/templates/registration/login.html:31 msgid "" "Benutzername oder Passwort sind nicht korrekt. Bitte versuchen Sie es erneut." msgstr "" -#: workflows/templates/registration/login.html:35 +#: workflows/templates/registration/login.html:37 msgid "Anmelden" msgstr "Sign in" #: workflows/templates/workflows/audit_log.html:4 #: workflows/templates/workflows/audit_log.html:15 -#: workflows/templates/workflows/home.html:124 +#: workflows/templates/workflows/home.html:120 msgid "Audit Log" msgstr "" @@ -377,15 +438,16 @@ msgstr "" #: workflows/templates/workflows/audit_log.html:23 #: workflows/templates/workflows/audit_log.html:54 -#: workflows/templates/workflows/requests_dashboard.html:197 -#: workflows/templates/workflows/welcome_emails.html:91 +#: workflows/templates/workflows/backup_recovery.html:43 +#: workflows/templates/workflows/requests_dashboard.html:193 +#: workflows/templates/workflows/welcome_emails.html:87 msgid "Aktion" msgstr "Action" #: workflows/templates/workflows/audit_log.html:25 -#: workflows/templates/workflows/requests_dashboard.html:134 -#: workflows/templates/workflows/requests_dashboard.html:142 -#: workflows/templates/workflows/requests_dashboard.html:151 +#: workflows/templates/workflows/requests_dashboard.html:130 +#: workflows/templates/workflows/requests_dashboard.html:138 +#: workflows/templates/workflows/requests_dashboard.html:147 msgid "Alle" msgstr "" @@ -413,7 +475,7 @@ msgid "Filtern" msgstr "" #: workflows/templates/workflows/audit_log.html:45 -#: workflows/templates/workflows/requests_dashboard.html:169 +#: workflows/templates/workflows/requests_dashboard.html:165 msgid "Zurücksetzen" msgstr "Reset" @@ -423,8 +485,8 @@ msgstr "" #: workflows/templates/workflows/audit_log.html:55 #: workflows/templates/workflows/request_timeline.html:62 -#: workflows/templates/workflows/requests_dashboard.html:132 -#: workflows/templates/workflows/requests_dashboard.html:192 +#: workflows/templates/workflows/requests_dashboard.html:128 +#: workflows/templates/workflows/requests_dashboard.html:188 msgid "Typ" msgstr "Type" @@ -460,12 +522,172 @@ msgstr "" msgid "Noch keine Audit-Einträge vorhanden." msgstr "No requests available yet." +#: workflows/templates/workflows/backup_recovery.html:4 +#: workflows/templates/workflows/backup_recovery.html:12 +#: workflows/templates/workflows/home.html:125 +msgid "Backup & Recovery" +msgstr "Backup & Recovery" + +#: workflows/templates/workflows/backup_recovery.html:13 +msgid "" +"Datenbank- und Media-Backups erstellen und vorhandene Bundles sicher " +"verifizieren." +msgstr "Create database and media backups and verify existing bundles safely." + +#: workflows/templates/workflows/backup_recovery.html:20 +msgid "Aktionen" +msgstr "Actions" + +#: workflows/templates/workflows/backup_recovery.html:21 +msgid "" +"Erstellung und Verifikation laufen im App-Kontext. Restore bleibt bewusst " +"CLI-only." +msgstr "Creation and verification run inside the app context. Restore intentionally remains CLI-only." + +#: workflows/templates/workflows/backup_recovery.html:23 +msgid "Neues Backup jetzt erstellen?" +msgstr "Create a new backup now?" + +#: workflows/templates/workflows/backup_recovery.html:25 +msgid "Backup erstellen" +msgstr "Create backup" + +#: workflows/templates/workflows/backup_recovery.html:31 +msgid "Verfügbare Backup-Bundles" +msgstr "Available backup bundles" + +#: workflows/templates/workflows/backup_recovery.html:37 +msgid "Bundle" +msgstr "Bundle" + +#: workflows/templates/workflows/backup_recovery.html:38 +msgid "Erstellt" +msgstr "Created" + +#: workflows/templates/workflows/backup_recovery.html:39 +#: workflows/templates/workflows/backup_recovery.html:54 +msgid "Verifiziert" +msgstr "Verified" + +#: workflows/templates/workflows/backup_recovery.html:40 +#: workflows/templates/workflows/home.html:98 +#: workflows/templates/workflows/onboarding_intro_session.html:37 +#: workflows/templates/workflows/request_timeline.html:70 +#: workflows/templates/workflows/requests_dashboard.html:136 +#: workflows/templates/workflows/welcome_emails.html:85 +msgid "Status" +msgstr "Status" + +#: workflows/templates/workflows/backup_recovery.html:41 +msgid "Inhalt" +msgstr "Contents" + +#: workflows/templates/workflows/backup_recovery.html:42 +msgid "Remote" +msgstr "Remote" + +#: workflows/templates/workflows/backup_recovery.html:56 +msgid "Nicht geprüft" +msgstr "Not verified" + +#: workflows/templates/workflows/backup_recovery.html:68 +msgid "Hochgeladen" +msgstr "Uploaded" + +#: workflows/templates/workflows/backup_recovery.html:72 +msgid "Vorbereitet" +msgstr "Prepared" + +#: workflows/templates/workflows/backup_recovery.html:74 +#, fuzzy +#| msgid "Deaktivieren" +msgid "Deaktiviert" +msgstr "Disabled" + +#: workflows/templates/workflows/backup_recovery.html:76 +msgid "Lokal" +msgstr "Local" + +msgid "Lokal gespeichert" +msgstr "Stored locally" + +msgid "Lokal nicht vorhanden" +msgstr "Not stored locally" + +#: workflows/templates/workflows/backup_recovery.html:90 +msgid "Backup jetzt verifizieren?" +msgstr "Verify backup now?" + +#: workflows/templates/workflows/backup_recovery.html:92 +msgid "Verifizieren" +msgstr "Verify" + +#: workflows/templates/workflows/backup_recovery.html:94 +msgid "Backup-Bundle wirklich löschen?" +msgstr "Delete this backup bundle?" + +#: workflows/templates/workflows/backup_recovery.html:96 +#: workflows/templates/workflows/form_builder.html:92 +#: workflows/templates/workflows/form_builder.html:107 +#: workflows/templates/workflows/integrations_setup.html:265 +#: workflows/templates/workflows/intro_builder.html:66 +#: workflows/templates/workflows/intro_builder.html:102 +#: workflows/templates/workflows/requests_dashboard.html:279 +#: workflows/templates/workflows/welcome_emails.html:70 +msgid "Löschen" +msgstr "Delete" + +#: workflows/templates/workflows/backup_recovery.html:106 +#, fuzzy +#| msgid "Noch keine Vorgänge vorhanden." +msgid "Noch keine Backup-Bundles vorhanden." +msgstr "No backup bundles available yet." + +msgid "Bitte warten" +msgstr "Please wait" + +msgid "Aktion läuft" +msgstr "Action in progress" + +msgid "Die Aktion wird im aktuellen Tab ausgeführt." +msgstr "The action is running in the current tab." + +msgid "Backup läuft" +msgstr "Backup in progress" + +msgid "Bitte warten. Die Aktion wird im aktuellen Tab ausgeführt." +msgstr "Please wait. The action is running in the current tab." + +msgid "Backup wird erstellt" +msgstr "Backup is being created" + +msgid "Bitte warten. Datenbank- und Media-Bundle werden gerade vorbereitet." +msgstr "Please wait. The database and media bundle are being prepared." + +msgid "Backup wird verifiziert" +msgstr "Backup is being verified" + +msgid "Bitte warten. Bundle, Datenbank-Dump und Media-Archiv werden geprüft." +msgstr "Please wait. The bundle, database dump, and media archive are being checked." + +msgid "Nextcloud-Test läuft" +msgstr "Nextcloud test in progress" + +msgid "Bitte warten. Verbindung und Upload in das konfigurierte Ziel werden geprüft." +msgstr "Please wait. The connection and upload to the configured target are being checked." + +msgid "SMTP-Test läuft" +msgstr "SMTP test in progress" + +msgid "Bitte warten. SMTP-Verbindung und Testversand werden geprüft." +msgstr "Please wait. The SMTP connection and test delivery are being checked." + #: workflows/templates/workflows/base_shell.html:24 msgid "Bitte bestätigen" msgstr "" #: workflows/templates/workflows/base_shell.html:28 -#: workflows/templates/workflows/welcome_emails.html:138 +#: workflows/templates/workflows/welcome_emails.html:134 msgid "Abbrechen" msgstr "Cancel" @@ -475,7 +697,7 @@ msgstr "" #: workflows/templates/workflows/form_builder.html:4 #: workflows/templates/workflows/form_builder.html:14 -#: workflows/templates/workflows/home.html:134 +#: workflows/templates/workflows/home.html:135 msgid "Form Builder" msgstr "Form Builder" @@ -483,133 +705,123 @@ msgstr "Form Builder" msgid "Felder per Drag-and-Drop sortieren und pro Schritt gruppieren." msgstr "Sort fields by drag and drop and group them by step." -#: workflows/templates/workflows/form_builder.html:33 +#: workflows/templates/workflows/form_builder.html:29 msgid "Reihenfolge speichern" msgstr "Save order" -#: workflows/templates/workflows/form_builder.html:50 +#: workflows/templates/workflows/form_builder.html:46 msgid "Fix" msgstr "Fixed" -#: workflows/templates/workflows/form_builder.html:51 +#: workflows/templates/workflows/form_builder.html:47 msgid "Ausgeblendet" msgstr "Hidden" -#: workflows/templates/workflows/form_builder.html:52 +#: workflows/templates/workflows/form_builder.html:48 msgid "Pflicht" msgstr "Required" -#: workflows/templates/workflows/form_builder.html:63 +#: workflows/templates/workflows/form_builder.html:59 msgid "Optionen verwalten" msgstr "Manage options" -#: workflows/templates/workflows/form_builder.html:66 +#: workflows/templates/workflows/form_builder.html:62 msgid "Kategorie" msgstr "Category" -#: workflows/templates/workflows/form_builder.html:79 -#: workflows/templates/workflows/form_builder.html:92 -#: workflows/templates/workflows/form_builder.html:137 +#: workflows/templates/workflows/form_builder.html:75 +#: workflows/templates/workflows/form_builder.html:88 +#: workflows/templates/workflows/form_builder.html:133 msgid "Label (DE)" msgstr "Label (DE)" -#: workflows/templates/workflows/form_builder.html:80 +#: workflows/templates/workflows/form_builder.html:76 msgid "Label (EN, optional)" msgstr "Label (EN, optional)" -#: workflows/templates/workflows/form_builder.html:81 +#: workflows/templates/workflows/form_builder.html:77 msgid "Technischer Wert (optional)" msgstr "Technical value (optional)" -#: workflows/templates/workflows/form_builder.html:82 +#: workflows/templates/workflows/form_builder.html:78 msgid "Option hinzufügen" msgstr "Add option" -#: workflows/templates/workflows/form_builder.html:91 -#: workflows/templates/workflows/intro_builder.html:62 +#: workflows/templates/workflows/form_builder.html:87 +#: workflows/templates/workflows/intro_builder.html:58 msgid "Sortierung" msgstr "Sort order" -#: workflows/templates/workflows/form_builder.html:93 -#: workflows/templates/workflows/form_builder.html:138 +#: workflows/templates/workflows/form_builder.html:89 +#: workflows/templates/workflows/form_builder.html:134 msgid "Label (EN)" msgstr "Label (EN)" -#: workflows/templates/workflows/form_builder.html:95 -#: workflows/templates/workflows/integrations_setup.html:266 -#: workflows/templates/workflows/intro_builder.html:69 +#: workflows/templates/workflows/form_builder.html:91 +#: workflows/templates/workflows/integrations_setup.html:263 +#: workflows/templates/workflows/intro_builder.html:65 msgid "Aktiv" msgstr "Active" -#: workflows/templates/workflows/form_builder.html:96 -#: workflows/templates/workflows/form_builder.html:111 -#: workflows/templates/workflows/integrations_setup.html:268 -#: workflows/templates/workflows/intro_builder.html:70 -#: workflows/templates/workflows/intro_builder.html:106 -#: workflows/templates/workflows/requests_dashboard.html:283 -#: workflows/templates/workflows/welcome_emails.html:74 -msgid "Löschen" -msgstr "Delete" - -#: workflows/templates/workflows/form_builder.html:104 +#: workflows/templates/workflows/form_builder.html:100 msgid "Ziehen zum Sortieren" msgstr "Drag to reorder" -#: workflows/templates/workflows/form_builder.html:111 +#: workflows/templates/workflows/form_builder.html:107 msgid "Option wirklich löschen?" msgstr "Delete this option?" -#: workflows/templates/workflows/form_builder.html:115 +#: workflows/templates/workflows/form_builder.html:111 msgid "Keine Optionen in dieser Kategorie." msgstr "No options in this category." -#: workflows/templates/workflows/form_builder.html:121 +#: workflows/templates/workflows/form_builder.html:117 msgid "Optionen speichern" msgstr "Save options" -#: workflows/templates/workflows/form_builder.html:128 +#: workflows/templates/workflows/form_builder.html:124 msgid "Feldtexte verwalten" msgstr "Manage field text" -#: workflows/templates/workflows/form_builder.html:136 +#: workflows/templates/workflows/form_builder.html:132 msgid "Feld" msgstr "Field" -#: workflows/templates/workflows/form_builder.html:139 +#: workflows/templates/workflows/form_builder.html:135 msgid "Hilfetext (DE)" msgstr "Help text (DE)" -#: workflows/templates/workflows/form_builder.html:140 +#: workflows/templates/workflows/form_builder.html:136 msgid "Hilfetext (EN)" msgstr "Help text (EN)" -#: workflows/templates/workflows/form_builder.html:150 +#: workflows/templates/workflows/form_builder.html:146 msgid "Fallback: Standardlabel" msgstr "Fallback: default label" -#: workflows/templates/workflows/form_builder.html:151 +#: workflows/templates/workflows/form_builder.html:147 msgid "English label" msgstr "English label" -#: workflows/templates/workflows/form_builder.html:152 +#: workflows/templates/workflows/form_builder.html:148 msgid "Optionaler Hilfetext" msgstr "Optional help text" -#: workflows/templates/workflows/form_builder.html:153 +#: workflows/templates/workflows/form_builder.html:149 msgid "Optional English help text" msgstr "Optional English help text" -#: workflows/templates/workflows/form_builder.html:156 +#: workflows/templates/workflows/form_builder.html:152 msgid "Keine Feldkonfigurationen verfügbar." msgstr "No field configurations available." -#: workflows/templates/workflows/form_builder.html:162 +#: workflows/templates/workflows/form_builder.html:158 msgid "Feldtexte speichern" msgstr "Save field text" #: workflows/templates/workflows/handbook.html:4 #: workflows/templates/workflows/handbook.html:15 -#: workflows/templates/workflows/home.html:144 +#: workflows/templates/workflows/home.html:145 msgid "Handbook" msgstr "Handbook" @@ -732,7 +944,7 @@ msgstr "" #: workflows/templates/workflows/home.html:4 #: workflows/templates/workflows/home.html:35 -#: workflows/templates/workflows/requests_dashboard.html:299 +#: workflows/templates/workflows/requests_dashboard.html:295 msgid "TUBCO Onboarding & Offboarding Portal" msgstr "TUBCO Onboarding & Offboarding Portal" @@ -770,12 +982,12 @@ msgid "Nextcloud:" msgstr "Nextcloud:" #: workflows/templates/workflows/home.html:40 -#: workflows/templates/workflows/integrations_setup.html:63 +#: workflows/templates/workflows/integrations_setup.html:60 msgid "aktiv" msgstr "active" #: workflows/templates/workflows/home.html:40 -#: workflows/templates/workflows/integrations_setup.html:63 +#: workflows/templates/workflows/integrations_setup.html:60 msgid "inaktiv" msgstr "inactive" @@ -786,12 +998,12 @@ msgid "E-Mail:" msgstr "Email:" #: workflows/templates/workflows/home.html:43 -#: workflows/templates/workflows/integrations_setup.html:125 +#: workflows/templates/workflows/integrations_setup.html:122 msgid "Testmodus" msgstr "Test mode" #: workflows/templates/workflows/home.html:43 -#: workflows/templates/workflows/integrations_setup.html:125 +#: workflows/templates/workflows/integrations_setup.html:122 msgid "Produktion" msgstr "Production" @@ -799,15 +1011,15 @@ msgstr "Production" msgid "PDF + E-Mail Workflow bereit" msgstr "PDF + Email Workflow Ready" -#: workflows/templates/workflows/home.html:59 +#: workflows/templates/workflows/home.html:55 msgid "Apps" msgstr "Apps" -#: workflows/templates/workflows/home.html:60 +#: workflows/templates/workflows/home.html:56 msgid "Wählen Sie den gewünschten Prozess." msgstr "Choose the desired process." -#: workflows/templates/workflows/home.html:67 +#: workflows/templates/workflows/home.html:63 msgid "" "Neue Mitarbeitende erfassen, PDF mit Briefkopf erstellen, Benachrichtigungen " "senden und in Nextcloud ablegen." @@ -815,19 +1027,19 @@ msgstr "" "Capture new employees, generate a PDF with letterhead, send notifications, " "and store it in Nextcloud." -#: workflows/templates/workflows/home.html:69 +#: workflows/templates/workflows/home.html:65 msgid "Mehrschritt-Formular" msgstr "Multi-step form" -#: workflows/templates/workflows/home.html:71 +#: workflows/templates/workflows/home.html:67 msgid "E-Mail Routing" msgstr "Email routing" -#: workflows/templates/workflows/home.html:75 +#: workflows/templates/workflows/home.html:71 msgid "Onboarding starten" msgstr "Start onboarding" -#: workflows/templates/workflows/home.html:83 +#: workflows/templates/workflows/home.html:79 msgid "" "Mitarbeitende suchen, Daten vorbefüllen, Offboarding-Dokumente erzeugen und " "Rückgabe-Prozess starten." @@ -835,29 +1047,29 @@ msgstr "" "Search employees, prefill data, generate offboarding documents, and start " "the return process." -#: workflows/templates/workflows/home.html:85 +#: workflows/templates/workflows/home.html:81 msgid "Profile-Suche" msgstr "Profile search" -#: workflows/templates/workflows/home.html:86 +#: workflows/templates/workflows/home.html:82 msgid "Hardware-Liste" msgstr "Hardware list" -#: workflows/templates/workflows/home.html:87 +#: workflows/templates/workflows/home.html:83 msgid "IT-Rückgabe" msgstr "IT return" -#: workflows/templates/workflows/home.html:91 +#: workflows/templates/workflows/home.html:87 msgid "Offboarding starten" msgstr "Start offboarding" -#: workflows/templates/workflows/home.html:98 +#: workflows/templates/workflows/home.html:94 #: workflows/templates/workflows/requests_dashboard.html:4 #: workflows/templates/workflows/requests_dashboard.html:33 msgid "Anfragen Dashboard" msgstr "Requests Dashboard" -#: workflows/templates/workflows/home.html:99 +#: workflows/templates/workflows/home.html:95 msgid "" "Status, Suchfunktion, PDF-Links und Verlauf aller Onboarding-/Offboarding-" "Anfragen." @@ -865,92 +1077,89 @@ msgstr "" "Status, search, PDF links, and history of all onboarding/offboarding " "requests." -#: workflows/templates/workflows/home.html:101 +#: workflows/templates/workflows/home.html:97 msgid "Suche" msgstr "Search" -#: workflows/templates/workflows/home.html:102 -#: workflows/templates/workflows/onboarding_intro_session.html:41 -#: workflows/templates/workflows/request_timeline.html:70 -#: workflows/templates/workflows/requests_dashboard.html:140 -#: workflows/templates/workflows/welcome_emails.html:89 -msgid "Status" -msgstr "Status" - -#: workflows/templates/workflows/home.html:103 +#: workflows/templates/workflows/home.html:99 msgid "PDF Zugriff" msgstr "PDF access" -#: workflows/templates/workflows/home.html:107 +#: workflows/templates/workflows/home.html:103 msgid "Dashboard öffnen" msgstr "Open dashboard" -#: workflows/templates/workflows/home.html:114 +#: workflows/templates/workflows/home.html:110 msgid "Admin Apps" msgstr "Admin Apps" -#: workflows/templates/workflows/home.html:115 +#: workflows/templates/workflows/home.html:111 msgid "Konfiguration, Tests und Steuerung." msgstr "Configuration, tests, and controls." -#: workflows/templates/workflows/home.html:119 +#: workflows/templates/workflows/home.html:115 msgid "Integrationen" msgstr "Integrations" -#: workflows/templates/workflows/home.html:120 +#: workflows/templates/workflows/home.html:116 msgid "Nextcloud- und E-Mail-Setup." msgstr "Nextcloud and email setup." -#: workflows/templates/workflows/home.html:121 -#: workflows/templates/workflows/home.html:126 -#: workflows/templates/workflows/home.html:131 -#: workflows/templates/workflows/home.html:136 -#: workflows/templates/workflows/home.html:141 -#: workflows/templates/workflows/home.html:146 -#: workflows/templates/workflows/home.html:151 +#: workflows/templates/workflows/home.html:117 +#: workflows/templates/workflows/home.html:122 +#: workflows/templates/workflows/home.html:127 +#: workflows/templates/workflows/home.html:132 +#: workflows/templates/workflows/home.html:137 +#: workflows/templates/workflows/home.html:142 +#: workflows/templates/workflows/home.html:147 +#: workflows/templates/workflows/home.html:152 msgid "Öffnen" msgstr "Open" -#: workflows/templates/workflows/home.html:125 +#: workflows/templates/workflows/home.html:121 msgid "Wichtige Admin-Aktionen nachvollziehen und prüfen." msgstr "" -#: workflows/templates/workflows/home.html:129 +#: workflows/templates/workflows/home.html:126 +msgid "Backups erstellen und sicher verifizieren." +msgstr "" + +#: workflows/templates/workflows/home.html:130 #: workflows/templates/workflows/welcome_emails.html:4 msgid "Welcome E-Mails" msgstr "Welcome Emails" -#: workflows/templates/workflows/home.html:130 +#: workflows/templates/workflows/home.html:131 msgid "Geplante Welcome Mails verwalten." msgstr "Manage scheduled welcome emails." -#: workflows/templates/workflows/home.html:135 +#: workflows/templates/workflows/home.html:136 msgid "Felder, Schritte und Optionen verwalten." msgstr "Manage fields, steps, and options." -#: workflows/templates/workflows/home.html:139 +#: workflows/templates/workflows/home.html:140 #: workflows/templates/workflows/intro_builder.html:4 #: workflows/templates/workflows/intro_builder.html:17 msgid "Einweisungs-Builder" msgstr "Introduction Builder" -#: workflows/templates/workflows/home.html:140 +#: workflows/templates/workflows/home.html:141 msgid "Checklistenpunkte für das Einweisungsprotokoll konfigurieren." msgstr "Configure checklist items for the introduction protocol." -#: workflows/templates/workflows/home.html:145 +#: workflows/templates/workflows/home.html:146 msgid "Project wiki and developer documentation in one place." msgstr "Project wiki and developer documentation in one place." -#: workflows/templates/workflows/home.html:149 +#: workflows/templates/workflows/home.html:150 msgid "Django Admin" msgstr "Django Admin" -#: workflows/templates/workflows/home.html:150 +#: workflows/templates/workflows/home.html:151 msgid "Vollständige Datenverwaltung." msgstr "Full data management." -#: workflows/templates/workflows/home.html:157 +#: workflows/templates/workflows/home.html:158 msgid "Tipp: Die letzten Vorgänge sehen Sie jederzeit im Anfragen Dashboard." msgstr "Tip: You can always see the latest requests in the Requests Dashboard." @@ -991,267 +1200,307 @@ msgstr "Email routing & templates" #: workflows/templates/workflows/integrations_setup.html:21 msgid "Workflow-Regeln" -msgstr "" +msgstr "Workflow rules" -#: workflows/templates/workflows/integrations_setup.html:57 +#: workflows/templates/workflows/integrations_setup.html:22 +msgid "Backup-Ziel" +msgstr "Backup target" + +#: workflows/templates/workflows/integrations_setup.html:54 msgid "Nextcloud speichern" msgstr "Save Nextcloud" -#: workflows/templates/workflows/integrations_setup.html:58 -#, fuzzy -#| msgid "Nextcloud-Test" +#: workflows/templates/workflows/integrations_setup.html:55 msgid "Nextcloud-Test starten" -msgstr "Nextcloud test" +msgstr "Run Nextcloud test" -#: workflows/templates/workflows/integrations_setup.html:62 -#: workflows/templates/workflows/integrations_setup.html:124 -#: workflows/templates/workflows/requests_dashboard.html:266 +#: workflows/templates/workflows/integrations_setup.html:59 +#: workflows/templates/workflows/integrations_setup.html:121 +#: workflows/templates/workflows/requests_dashboard.html:262 msgid "Status:" msgstr "Status:" -#: workflows/templates/workflows/integrations_setup.html:65 -#, fuzzy -#| msgid "Nextcloud speichern" +#: workflows/templates/workflows/integrations_setup.html:62 msgid "Nextcloud schalten" -msgstr "Save Nextcloud" +msgstr "Toggle Nextcloud" -#: workflows/templates/workflows/integrations_setup.html:71 +#: workflows/templates/workflows/integrations_setup.html:68 msgid "" "Schaltet den produktiven Nextcloud-Upload sofort für alle nachfolgenden " "Vorgänge ein oder aus." msgstr "" +"Turns productive Nextcloud upload on or off immediately for all following " +"workflows." -#: workflows/templates/workflows/integrations_setup.html:72 -#: workflows/templates/workflows/integrations_setup.html:134 +#: workflows/templates/workflows/integrations_setup.html:69 +#: workflows/templates/workflows/integrations_setup.html:131 msgid "Leeres Passwortfeld lässt das bestehende Passwort unverändert." msgstr "Leaving the password field empty keeps the current password unchanged." -#: workflows/templates/workflows/integrations_setup.html:110 -#, fuzzy -#| msgid "Absenderadresse (optional)" +#: workflows/templates/workflows/integrations_setup.html:107 msgid "Absenderadresse" -msgstr "Sender address (optional)" +msgstr "Sender address" -#: workflows/templates/workflows/integrations_setup.html:115 +#: workflows/templates/workflows/integrations_setup.html:112 msgid "SMTP SSL" msgstr "SMTP SSL" -#: workflows/templates/workflows/integrations_setup.html:116 +#: workflows/templates/workflows/integrations_setup.html:113 msgid "SMTP TLS" msgstr "SMTP TLS" -#: workflows/templates/workflows/integrations_setup.html:119 +#: workflows/templates/workflows/integrations_setup.html:116 msgid "Mail speichern" msgstr "Save mail settings" -#: workflows/templates/workflows/integrations_setup.html:120 -#, fuzzy -#| msgid "SMTP-Test" +#: workflows/templates/workflows/integrations_setup.html:117 msgid "SMTP-Test starten" -msgstr "SMTP test" +msgstr "Run SMTP test" -#: workflows/templates/workflows/integrations_setup.html:127 -#, fuzzy -#| msgid "E-Mail Modus" +#: workflows/templates/workflows/integrations_setup.html:124 msgid "E-Mail Modus schalten" -msgstr "Email mode" +msgstr "Toggle email mode" -#: workflows/templates/workflows/integrations_setup.html:133 +#: workflows/templates/workflows/integrations_setup.html:130 msgid "" "Im Testmodus werden Systemmails umgeleitet. In Produktion werden sie an die " "echten Empfänger gesendet." msgstr "" +"In test mode, system emails are redirected. In production, they are sent to " +"the real recipients." -#: workflows/templates/workflows/integrations_setup.html:147 +#: workflows/templates/workflows/integrations_setup.html:144 msgid "It onboarding email" msgstr "IT onboarding email" -#: workflows/templates/workflows/integrations_setup.html:151 +#: workflows/templates/workflows/integrations_setup.html:148 msgid "General info email" msgstr "General info email" -#: workflows/templates/workflows/integrations_setup.html:155 +#: workflows/templates/workflows/integrations_setup.html:152 msgid "Business card email" msgstr "Business card email" -#: workflows/templates/workflows/integrations_setup.html:159 +#: workflows/templates/workflows/integrations_setup.html:156 msgid "Hr works email" msgstr "HR Works email" -#: workflows/templates/workflows/integrations_setup.html:163 +#: workflows/templates/workflows/integrations_setup.html:160 msgid "Key notification email" msgstr "Key notification email" -#: workflows/templates/workflows/integrations_setup.html:167 +#: workflows/templates/workflows/integrations_setup.html:164 msgid "Diese Empfänger werden für condition-based E-Mail Routing genutzt." msgstr "These recipients are used for condition-based email routing." -#: workflows/templates/workflows/integrations_setup.html:194 +#: workflows/templates/workflows/integrations_setup.html:191 msgid "E-Mail Routing & Vorlagen speichern" msgstr "Save email routing & templates" -#: workflows/templates/workflows/integrations_setup.html:200 +#: workflows/templates/workflows/integrations_setup.html:197 msgid "Bedingungsregeln für zusätzliche E-Mails" msgstr "Conditional rules for additional emails" -#: workflows/templates/workflows/integrations_setup.html:201 +#: workflows/templates/workflows/integrations_setup.html:198 msgid "Zusätzliche Regeln laufen nach dem Standard-Routing." msgstr "Additional rules run after the standard routing flow." -#: workflows/templates/workflows/integrations_setup.html:208 -#: workflows/templates/workflows/integrations_setup.html:279 +#: workflows/templates/workflows/integrations_setup.html:205 +#: workflows/templates/workflows/integrations_setup.html:276 msgid "Regelname" msgstr "Rule name" -#: workflows/templates/workflows/integrations_setup.html:212 -#: workflows/templates/workflows/integrations_setup.html:283 +#: workflows/templates/workflows/integrations_setup.html:209 +#: workflows/templates/workflows/integrations_setup.html:280 msgid "Event" msgstr "Event" -#: workflows/templates/workflows/integrations_setup.html:220 -#: workflows/templates/workflows/integrations_setup.html:291 +#: workflows/templates/workflows/integrations_setup.html:217 +#: workflows/templates/workflows/integrations_setup.html:288 msgid "Feldname" msgstr "Field name" -#: workflows/templates/workflows/integrations_setup.html:224 -#: workflows/templates/workflows/integrations_setup.html:295 -#: workflows/templates/workflows/intro_builder.html:67 +#: workflows/templates/workflows/integrations_setup.html:221 +#: workflows/templates/workflows/integrations_setup.html:292 +#: workflows/templates/workflows/intro_builder.html:63 msgid "Operator" msgstr "Operator" -#: workflows/templates/workflows/integrations_setup.html:232 -#: workflows/templates/workflows/integrations_setup.html:303 +#: workflows/templates/workflows/integrations_setup.html:229 +#: workflows/templates/workflows/integrations_setup.html:300 msgid "Vergleichswert" msgstr "Comparison value" -#: workflows/templates/workflows/integrations_setup.html:236 -#: workflows/templates/workflows/integrations_setup.html:307 -#: workflows/templates/workflows/welcome_emails.html:87 +#: workflows/templates/workflows/integrations_setup.html:233 +#: workflows/templates/workflows/integrations_setup.html:304 +#: workflows/templates/workflows/welcome_emails.html:83 msgid "Empfänger" msgstr "Recipients" -#: workflows/templates/workflows/integrations_setup.html:240 -#: workflows/templates/workflows/integrations_setup.html:311 +#: workflows/templates/workflows/integrations_setup.html:237 +#: workflows/templates/workflows/integrations_setup.html:308 msgid "Template Key (optional)" msgstr "Template key (optional)" -#: workflows/templates/workflows/integrations_setup.html:242 -#: workflows/templates/workflows/integrations_setup.html:313 +#: workflows/templates/workflows/integrations_setup.html:239 +#: workflows/templates/workflows/integrations_setup.html:310 msgid "-- Custom Betreff/Body verwenden --" msgstr "-- Use custom subject/body --" -#: workflows/templates/workflows/integrations_setup.html:249 -#: workflows/templates/workflows/integrations_setup.html:320 +#: workflows/templates/workflows/integrations_setup.html:246 +#: workflows/templates/workflows/integrations_setup.html:317 msgid "Custom Subject (optional)" msgstr "Custom subject (optional)" -#: workflows/templates/workflows/integrations_setup.html:253 -#: workflows/templates/workflows/integrations_setup.html:324 +#: workflows/templates/workflows/integrations_setup.html:250 +#: workflows/templates/workflows/integrations_setup.html:321 msgid "Custom Body (optional)" msgstr "Custom body (optional)" -#: workflows/templates/workflows/integrations_setup.html:257 -#: workflows/templates/workflows/integrations_setup.html:328 +#: workflows/templates/workflows/integrations_setup.html:254 +#: workflows/templates/workflows/integrations_setup.html:325 msgid "Custom Subject (EN, optional)" msgstr "Custom subject (EN, optional)" -#: workflows/templates/workflows/integrations_setup.html:261 -#: workflows/templates/workflows/integrations_setup.html:332 +#: workflows/templates/workflows/integrations_setup.html:258 +#: workflows/templates/workflows/integrations_setup.html:329 msgid "Custom Body (EN, optional)" msgstr "Custom body (EN, optional)" -#: workflows/templates/workflows/integrations_setup.html:267 -#: workflows/templates/workflows/integrations_setup.html:337 +#: workflows/templates/workflows/integrations_setup.html:264 +#: workflows/templates/workflows/integrations_setup.html:334 msgid "PDF anhängen" msgstr "Attach PDF" -#: workflows/templates/workflows/integrations_setup.html:272 +#: workflows/templates/workflows/integrations_setup.html:269 msgid "Noch keine zusätzlichen Regeln vorhanden." msgstr "No additional rules configured yet." -#: workflows/templates/workflows/integrations_setup.html:276 +#: workflows/templates/workflows/integrations_setup.html:273 msgid "Neue Regel hinzufügen" msgstr "Add new rule" -#: workflows/templates/workflows/integrations_setup.html:342 +#: workflows/templates/workflows/integrations_setup.html:339 msgid "Regeln speichern" msgstr "Save rules" -#: workflows/templates/workflows/integrations_setup.html:352 -#, fuzzy -#| msgid "Hardware-Übergabetermin" +#: workflows/templates/workflows/integrations_setup.html:349 msgid "Vorlauf Hardware-Übergabe (Tage)" -msgstr "Hardware handover date" +msgstr "Hardware handover lead time (days)" -#: workflows/templates/workflows/integrations_setup.html:364 -#, fuzzy -#| msgid "Regeln speichern" +#: workflows/templates/workflows/integrations_setup.html:361 msgid "Workflow-Regeln speichern" -msgstr "Save rules" +msgstr "Save workflow rules" -#: workflows/templates/workflows/integrations_setup.html:366 +#: workflows/templates/workflows/integrations_setup.html:363 msgid "" "Steuert den Mindestvorlauf für das gewünschte Übergabedatum der Geräte im " "Onboarding-Formular." msgstr "" +"Controls the minimum lead time for the desired device handover date in the " +"onboarding form." + +#: workflows/templates/workflows/integrations_setup.html:372 +msgid "Remote Backup aktiviert" +msgstr "Remote backup enabled" + +#: workflows/templates/workflows/integrations_setup.html:374 +msgid "Remote Kopie nach lokalem Bundle erstellen" +msgstr "Create remote copy after local bundle creation" + +#: workflows/templates/workflows/integrations_setup.html:378 +msgid "Remote Backup Zieltyp" +msgstr "Remote backup target type" + +#: workflows/templates/workflows/integrations_setup.html:386 +msgid "Nextcloud Backup-Verzeichnis" +msgstr "Nextcloud backup directory" + +#: workflows/templates/workflows/integrations_setup.html:390 +msgid "S3 Bucket (optional)" +msgstr "S3 bucket (optional)" + +#: workflows/templates/workflows/integrations_setup.html:394 +msgid "NFS Pfad (optional)" +msgstr "NFS path (optional)" + +#: workflows/templates/workflows/integrations_setup.html:399 +msgid "Backup-Einstellungen speichern" +msgstr "Save backup settings" + +#: workflows/templates/workflows/integrations_setup.html:401 +msgid "" +"Empfehlung: Nextcloud als erstes Remote-Ziel verwenden. S3 und NFS sind als " +"Zieltypen vorbereitet, aber noch nicht aktiv implementiert." +msgstr "" +"Recommendation: use Nextcloud as the first remote target. S3 and NFS are " +"prepared as target types but not yet actively implemented." + +#: workflows/templates/workflows/integrations_setup.html:402 +msgid "" +"Das Backup-Verzeichnis muss getrennt vom normalen Nextcloud Dokumentenordner " +"sein, z. B. Group-on-off-boarding-backups." +msgstr "" +"The backup directory must be separate from the normal Nextcloud document " +"folder, e.g. Group-on-off-boarding-backups." #: workflows/templates/workflows/intro_builder.html:18 msgid "Checklistenpunkte für das Einweisungs- und Übergabeprotokoll verwalten." msgstr "Manage checklist items for the introduction and handover protocol." -#: workflows/templates/workflows/intro_builder.html:33 -#: workflows/templates/workflows/intro_builder.html:63 +#: workflows/templates/workflows/intro_builder.html:29 +#: workflows/templates/workflows/intro_builder.html:59 msgid "Abschnitt" msgstr "Section" -#: workflows/templates/workflows/intro_builder.html:41 -#: workflows/templates/workflows/intro_builder.html:64 +#: workflows/templates/workflows/intro_builder.html:37 +#: workflows/templates/workflows/intro_builder.html:60 msgid "Checklistenpunkt (DE)" msgstr "Checklist item (DE)" -#: workflows/templates/workflows/intro_builder.html:42 +#: workflows/templates/workflows/intro_builder.html:38 msgid "z. B. Nextcloud Ordnerstruktur erklärt" msgstr "e.g. Nextcloud folder structure explained" -#: workflows/templates/workflows/intro_builder.html:45 +#: workflows/templates/workflows/intro_builder.html:41 msgid "Checklist item (EN)" msgstr "Checklist item (EN)" -#: workflows/templates/workflows/intro_builder.html:46 +#: workflows/templates/workflows/intro_builder.html:42 msgid "e.g. Nextcloud folder structure explained" msgstr "e.g. Nextcloud folder structure explained" -#: workflows/templates/workflows/intro_builder.html:49 +#: workflows/templates/workflows/intro_builder.html:45 msgid "Punkt hinzufügen" msgstr "Add item" -#: workflows/templates/workflows/intro_builder.html:52 +#: workflows/templates/workflows/intro_builder.html:48 msgid "" "Bedingungen und Sortierung können anschließend in der Tabelle bearbeitet " "werden." msgstr "Conditions and sort order can then be edited in the table." -#: workflows/templates/workflows/intro_builder.html:65 +#: workflows/templates/workflows/intro_builder.html:61 msgid "Checklistenpunkt (EN)" msgstr "Checklist item (EN)" -#: workflows/templates/workflows/intro_builder.html:66 +#: workflows/templates/workflows/intro_builder.html:62 msgid "Feld-Bedingung" msgstr "Field condition" -#: workflows/templates/workflows/intro_builder.html:68 +#: workflows/templates/workflows/intro_builder.html:64 msgid "Wert" msgstr "Value" -#: workflows/templates/workflows/intro_builder.html:103 +#: workflows/templates/workflows/intro_builder.html:99 msgid "z. B. HR Works" msgstr "e.g. HR Works" -#: workflows/templates/workflows/intro_builder.html:106 +#: workflows/templates/workflows/intro_builder.html:102 msgid "Checklistenpunkt wirklich löschen?" msgstr "Delete this checklist item?" -#: workflows/templates/workflows/intro_builder.html:110 +#: workflows/templates/workflows/intro_builder.html:106 msgid "" "Noch keine benutzerdefinierten Checklistenpunkte angelegt. Solange die Liste " "leer ist, nutzt das System die integrierten Standardpunkte." @@ -1259,11 +1508,11 @@ msgstr "" "No custom checklist items have been created yet. As long as the list is " "empty, the system uses the built-in default items." -#: workflows/templates/workflows/intro_builder.html:115 +#: workflows/templates/workflows/intro_builder.html:111 msgid "Reihenfolge folgt derzeit der Tabellenreihenfolge beim Speichern." msgstr "Order currently follows the table order when saving." -#: workflows/templates/workflows/intro_builder.html:117 +#: workflows/templates/workflows/intro_builder.html:113 msgid "Checkliste speichern" msgstr "Save checklist" @@ -1300,7 +1549,7 @@ msgid "z. B. max.mustermann@tub.co" msgstr "e.g. john.doe@tub.co" #: workflows/templates/workflows/offboarding_form.html:33 -#: workflows/templates/workflows/requests_dashboard.html:167 +#: workflows/templates/workflows/requests_dashboard.html:163 msgid "Suchen" msgstr "Search" @@ -1343,7 +1592,7 @@ msgstr "" #: workflows/templates/workflows/offboarding_success.html:23 #: workflows/templates/workflows/onboarding_success.html:22 #: workflows/templates/workflows/request_timeline.html:104 -#: workflows/templates/workflows/requests_dashboard.html:221 +#: workflows/templates/workflows/requests_dashboard.html:217 msgid "PDF öffnen" msgstr "Open PDF" @@ -1389,7 +1638,7 @@ msgstr "Please check the highlighted fields. Invalid input was detected." #: workflows/templates/workflows/onboarding_form.html:77 #: workflows/templates/workflows/onboarding_form.html:114 #: workflows/templates/workflows/onboarding_form.html:116 -#: workflows/templates/workflows/welcome_emails.html:69 +#: workflows/templates/workflows/welcome_emails.html:65 msgid "Alle auswählen" msgstr "Select all" @@ -1436,98 +1685,98 @@ msgid "" "markieren." msgstr "" -#: workflows/templates/workflows/onboarding_intro_session.html:29 -#: workflows/templates/workflows/welcome_emails.html:86 +#: workflows/templates/workflows/onboarding_intro_session.html:25 +#: workflows/templates/workflows/welcome_emails.html:82 msgid "Mitarbeitende Person" msgstr "Employee" -#: workflows/templates/workflows/onboarding_intro_session.html:31 +#: workflows/templates/workflows/onboarding_intro_session.html:27 #: workflows/templates/workflows/request_timeline.html:66 msgid "Name" msgstr "Name" -#: workflows/templates/workflows/onboarding_intro_session.html:33 +#: workflows/templates/workflows/onboarding_intro_session.html:29 msgid "Berufsbezeichnung" msgstr "Job title" -#: workflows/templates/workflows/onboarding_intro_session.html:34 +#: workflows/templates/workflows/onboarding_intro_session.html:30 msgid "Dienstliche E-Mail" msgstr "Work email" -#: workflows/templates/workflows/onboarding_intro_session.html:35 -#: workflows/views.py:413 +#: workflows/templates/workflows/onboarding_intro_session.html:31 +#: workflows/views.py:487 msgid "Vertragsbeginn" msgstr "Contract start" -#: workflows/templates/workflows/onboarding_intro_session.html:39 +#: workflows/templates/workflows/onboarding_intro_session.html:35 msgid "Sitzungsstatus" msgstr "Session status" -#: workflows/templates/workflows/onboarding_intro_session.html:43 +#: workflows/templates/workflows/onboarding_intro_session.html:39 msgid "Abgeschlossen von" msgstr "Completed by" -#: workflows/templates/workflows/onboarding_intro_session.html:44 +#: workflows/templates/workflows/onboarding_intro_session.html:40 msgid "Abgeschlossen am" msgstr "Completed at" -#: workflows/templates/workflows/onboarding_intro_session.html:45 +#: workflows/templates/workflows/onboarding_intro_session.html:41 msgid "Letzte Änderung" msgstr "Last updated" -#: workflows/templates/workflows/onboarding_intro_session.html:53 +#: workflows/templates/workflows/onboarding_intro_session.html:49 msgid "Fortschritt der Einweisung" msgstr "Introduction progress" -#: workflows/templates/workflows/onboarding_intro_session.html:54 +#: workflows/templates/workflows/onboarding_intro_session.html:50 #, python-format msgid "%(checked)s von %(total)s Punkten erledigt" msgstr "%(checked)s of %(total)s items completed" -#: workflows/templates/workflows/onboarding_intro_session.html:78 +#: workflows/templates/workflows/onboarding_intro_session.html:74 msgid "Notizen" msgstr "Notes" -#: workflows/templates/workflows/onboarding_intro_session.html:80 +#: workflows/templates/workflows/onboarding_intro_session.html:76 msgid "" "Diese Seite bleibt bewusst einfach: echte Web-Checkboxen, Notizen und ein " "klarer Entwurf/Abschluss-Status. Kein zusätzlicher komplexer PDF-Signatur-" "Workflow." msgstr "" -#: workflows/templates/workflows/onboarding_intro_session.html:82 +#: workflows/templates/workflows/onboarding_intro_session.html:78 msgid "Als Entwurf speichern" msgstr "Save as draft" -#: workflows/templates/workflows/onboarding_intro_session.html:83 +#: workflows/templates/workflows/onboarding_intro_session.html:79 msgid "Als abgeschlossen markieren" msgstr "Mark as completed" -#: workflows/templates/workflows/onboarding_intro_session.html:84 +#: workflows/templates/workflows/onboarding_intro_session.html:80 msgid "Einweisung wirklich zurücksetzen?" msgstr "Reset the introduction session?" -#: workflows/templates/workflows/onboarding_intro_session.html:84 +#: workflows/templates/workflows/onboarding_intro_session.html:80 msgid "Alles zurücksetzen" msgstr "Reset all" -#: workflows/templates/workflows/onboarding_intro_session.html:90 -#: workflows/templates/workflows/requests_dashboard.html:239 +#: workflows/templates/workflows/onboarding_intro_session.html:86 +#: workflows/templates/workflows/requests_dashboard.html:235 msgid "Live-Protokoll" msgstr "Live protocol" -#: workflows/templates/workflows/onboarding_intro_session.html:91 +#: workflows/templates/workflows/onboarding_intro_session.html:87 msgid "" "Erzeugt das Live-Protokoll nur aus den aktuell gespeicherten Haken und " "Notizen." msgstr "" -#: workflows/templates/workflows/onboarding_intro_session.html:95 +#: workflows/templates/workflows/onboarding_intro_session.html:91 msgid "Live-Protokoll erzeugen" msgstr "Generate live protocol" -#: workflows/templates/workflows/onboarding_intro_session.html:98 -#: workflows/templates/workflows/requests_dashboard.html:243 +#: workflows/templates/workflows/onboarding_intro_session.html:94 +#: workflows/templates/workflows/requests_dashboard.html:239 msgid "Live-Protokoll öffnen" msgstr "Open live protocol" @@ -1714,7 +1963,7 @@ msgid "Request Timeline" msgstr "" #: workflows/templates/workflows/request_timeline.html:74 -#: workflows/templates/workflows/requests_dashboard.html:194 +#: workflows/templates/workflows/requests_dashboard.html:190 msgid "E-Mail" msgstr "Email" @@ -1788,108 +2037,108 @@ msgstr "Activity 14 Days" msgid "Zeitraum des visuellen Aktivitätsverlaufs in dieser Übersicht." msgstr "Time span of the visual activity timeline in this overview." -#: workflows/templates/workflows/requests_dashboard.html:118 +#: workflows/templates/workflows/requests_dashboard.html:114 msgid "Vorgänge" msgstr "Requests" -#: workflows/templates/workflows/requests_dashboard.html:119 +#: workflows/templates/workflows/requests_dashboard.html:115 msgid "" "Dokumente, Status und Einweisungsaktionen in einer verdichteten " "Arbeitsansicht." msgstr "Documents, status, and introduction actions in a condensed work view." -#: workflows/templates/workflows/requests_dashboard.html:121 +#: workflows/templates/workflows/requests_dashboard.html:117 #, python-format msgid "%(count)s Einträge sichtbar" msgstr "%(count)s entries visible" -#: workflows/templates/workflows/requests_dashboard.html:128 +#: workflows/templates/workflows/requests_dashboard.html:124 msgid "Nach Name oder E-Mail suchen" msgstr "Search by name or email" -#: workflows/templates/workflows/requests_dashboard.html:158 +#: workflows/templates/workflows/requests_dashboard.html:154 msgid "Von" msgstr "" -#: workflows/templates/workflows/requests_dashboard.html:162 +#: workflows/templates/workflows/requests_dashboard.html:158 msgid "Bis" msgstr "" -#: workflows/templates/workflows/requests_dashboard.html:176 +#: workflows/templates/workflows/requests_dashboard.html:172 #, fuzzy #| msgid "Ausgewählte Welcome-Einträge wirklich löschen?" msgid "Ausgewählte Einträge wirklich löschen?" msgstr "Delete the selected welcome entries?" -#: workflows/templates/workflows/requests_dashboard.html:179 -#: workflows/templates/workflows/welcome_emails.html:78 +#: workflows/templates/workflows/requests_dashboard.html:175 +#: workflows/templates/workflows/welcome_emails.html:74 msgid "ausgewählt" msgstr "selected" -#: workflows/templates/workflows/requests_dashboard.html:180 +#: workflows/templates/workflows/requests_dashboard.html:176 msgid "Auswahl löschen" msgstr "Delete selection" -#: workflows/templates/workflows/requests_dashboard.html:193 +#: workflows/templates/workflows/requests_dashboard.html:189 msgid "Person" msgstr "Person" -#: workflows/templates/workflows/requests_dashboard.html:195 +#: workflows/templates/workflows/requests_dashboard.html:191 msgid "Dokument" msgstr "Document" -#: workflows/templates/workflows/requests_dashboard.html:196 -#: workflows/templates/workflows/requests_dashboard.html:236 +#: workflows/templates/workflows/requests_dashboard.html:192 +#: workflows/templates/workflows/requests_dashboard.html:232 msgid "Einweisung" msgstr "Introduction" -#: workflows/templates/workflows/requests_dashboard.html:223 +#: workflows/templates/workflows/requests_dashboard.html:219 msgid "Noch nicht verfügbar" msgstr "Not available yet" -#: workflows/templates/workflows/requests_dashboard.html:241 +#: workflows/templates/workflows/requests_dashboard.html:237 msgid "Einweisung öffnen" msgstr "Open introduction" -#: workflows/templates/workflows/requests_dashboard.html:248 +#: workflows/templates/workflows/requests_dashboard.html:244 msgid "Standard-Einweisungs-PDF" msgstr "Standard introduction PDF" -#: workflows/templates/workflows/requests_dashboard.html:253 +#: workflows/templates/workflows/requests_dashboard.html:249 msgid "Neu erzeugen" msgstr "Regenerate" -#: workflows/templates/workflows/requests_dashboard.html:255 +#: workflows/templates/workflows/requests_dashboard.html:251 msgid "Standard-PDF öffnen" msgstr "Open standard PDF" -#: workflows/templates/workflows/requests_dashboard.html:259 +#: workflows/templates/workflows/requests_dashboard.html:255 msgid "PDF erzeugen" msgstr "Generate PDF" -#: workflows/templates/workflows/requests_dashboard.html:270 +#: workflows/templates/workflows/requests_dashboard.html:266 msgid "Nicht relevant" msgstr "Not relevant" -#: workflows/templates/workflows/requests_dashboard.html:274 +#: workflows/templates/workflows/requests_dashboard.html:270 msgid "Timeline" msgstr "" -#: workflows/templates/workflows/requests_dashboard.html:276 +#: workflows/templates/workflows/requests_dashboard.html:272 msgid "Eintrag erneut verarbeiten?" msgstr "" -#: workflows/templates/workflows/requests_dashboard.html:278 +#: workflows/templates/workflows/requests_dashboard.html:274 msgid "Erneut versuchen" msgstr "" -#: workflows/templates/workflows/requests_dashboard.html:281 +#: workflows/templates/workflows/requests_dashboard.html:277 #, fuzzy #| msgid "Option wirklich löschen?" msgid "Eintrag wirklich löschen?" msgstr "Delete this option?" -#: workflows/templates/workflows/requests_dashboard.html:290 +#: workflows/templates/workflows/requests_dashboard.html:286 msgid "Noch keine Vorgänge vorhanden." msgstr "No requests available yet." @@ -1905,345 +2154,397 @@ msgstr "" "Configure welcome emails and control scheduled messages (send now, pause, " "resume, cancel)." -#: workflows/templates/workflows/welcome_emails.html:27 +#: workflows/templates/workflows/welcome_emails.html:23 msgid "Verzögerung in Tagen" msgstr "Delay in days" -#: workflows/templates/workflows/welcome_emails.html:31 +#: workflows/templates/workflows/welcome_emails.html:27 msgid "Absenderadresse (optional)" msgstr "Sender address (optional)" -#: workflows/templates/workflows/welcome_emails.html:32 +#: workflows/templates/workflows/welcome_emails.html:28 msgid "Leer = System-Absender" msgstr "Empty = system sender" -#: workflows/templates/workflows/welcome_emails.html:52 +#: workflows/templates/workflows/welcome_emails.html:48 msgid "Onboarding-PDF anhängen" msgstr "Attach onboarding PDF" -#: workflows/templates/workflows/welcome_emails.html:55 +#: workflows/templates/workflows/welcome_emails.html:51 msgid "Verfügbare Keywords:" msgstr "Available keywords:" -#: workflows/templates/workflows/welcome_emails.html:61 +#: workflows/templates/workflows/welcome_emails.html:57 msgid "Welcome-Einstellungen speichern" msgstr "Save welcome settings" -#: workflows/templates/workflows/welcome_emails.html:65 +#: workflows/templates/workflows/welcome_emails.html:61 msgid "Bitte mindestens einen Welcome-Eintrag auswählen." msgstr "Please select at least one welcome entry." -#: workflows/templates/workflows/welcome_emails.html:65 +#: workflows/templates/workflows/welcome_emails.html:61 msgid "Ausgewählte Welcome-Einträge wirklich löschen?" msgstr "Delete the selected welcome entries?" -#: workflows/templates/workflows/welcome_emails.html:65 +#: workflows/templates/workflows/welcome_emails.html:61 msgid "Ausgewählte Welcome-Einträge pausieren?" msgstr "Pause the selected welcome entries?" -#: workflows/templates/workflows/welcome_emails.html:65 +#: workflows/templates/workflows/welcome_emails.html:61 msgid "Ausgewählte Welcome-Einträge sofort senden?" msgstr "Send the selected welcome entries now?" -#: workflows/templates/workflows/welcome_emails.html:72 -#: workflows/templates/workflows/welcome_emails.html:127 +#: workflows/templates/workflows/welcome_emails.html:68 +#: workflows/templates/workflows/welcome_emails.html:123 msgid "Pausieren" msgstr "Pause" -#: workflows/templates/workflows/welcome_emails.html:73 -#: workflows/templates/workflows/welcome_emails.html:121 +#: workflows/templates/workflows/welcome_emails.html:69 +#: workflows/templates/workflows/welcome_emails.html:117 msgid "Sofort senden" msgstr "Send now" -#: workflows/templates/workflows/welcome_emails.html:77 +#: workflows/templates/workflows/welcome_emails.html:73 msgid "Bulk ausführen" msgstr "Run bulk action" -#: workflows/templates/workflows/welcome_emails.html:84 +#: workflows/templates/workflows/welcome_emails.html:80 msgid "Auswahl" msgstr "Select" -#: workflows/templates/workflows/welcome_emails.html:88 +#: workflows/templates/workflows/welcome_emails.html:84 msgid "Geplant für" msgstr "Scheduled for" -#: workflows/templates/workflows/welcome_emails.html:90 +#: workflows/templates/workflows/welcome_emails.html:86 msgid "Gesendet am" msgstr "Sent at" -#: workflows/templates/workflows/welcome_emails.html:132 +#: workflows/templates/workflows/welcome_emails.html:128 msgid "Fortsetzen" msgstr "Resume" -#: workflows/templates/workflows/welcome_emails.html:145 +#: workflows/templates/workflows/welcome_emails.html:141 msgid "Keine geplanten Welcome E-Mails vorhanden." msgstr "No scheduled welcome emails available." -#: workflows/views.py:86 +#: workflows/views.py:87 msgid "Person, Rolle, Abteilung" msgstr "Person, role, department" -#: workflows/views.py:87 +#: workflows/views.py:88 msgid "Beschäftigung und Termine" msgstr "Employment and dates" -#: workflows/views.py:88 +#: workflows/views.py:89 msgid "Geräte, Software und Zugänge" msgstr "Devices, software, and access" -#: workflows/views.py:89 +#: workflows/views.py:90 msgid "Notizen und Freigabe" msgstr "Notes and approval" -#: workflows/views.py:190 +#: workflows/views.py:191 #, fuzzy #| msgid "Vorgänge" msgid "Vorgänge gelöscht" msgstr "Requests" -#: workflows/views.py:191 +#: workflows/views.py:192 msgid "Vorgang gelöscht" msgstr "" -#: workflows/views.py:192 +#: workflows/views.py:193 msgid "Vorgang erneut angestoßen" msgstr "" -#: workflows/views.py:193 +#: workflows/views.py:194 #, fuzzy #| msgid "Einweisung" msgid "Einweisungs-PDF erzeugt" msgstr "Introduction" -#: workflows/views.py:194 +#: workflows/views.py:195 #, fuzzy #| msgid "Live-Protokoll erzeugen" msgid "Live-Protokoll erzeugt" msgstr "Generate live protocol" -#: workflows/views.py:195 +#: workflows/views.py:196 #, fuzzy #| msgid "Einweisung wurde zurückgesetzt." msgid "Einweisung zurückgesetzt" msgstr "Introduction was reset." -#: workflows/views.py:196 +#: workflows/views.py:197 #, fuzzy #| msgid "Einweisung wurde als Entwurf gespeichert." msgid "Einweisung als Entwurf gespeichert" msgstr "Introduction was saved as draft." -#: workflows/views.py:197 +#: workflows/views.py:198 #, fuzzy #| msgid "Einweisung wurde als abgeschlossen gespeichert." msgid "Einweisung abgeschlossen" msgstr "Introduction was saved as completed." -#: workflows/views.py:198 +#: workflows/views.py:199 msgid "Formularoption gelöscht" msgstr "" -#: workflows/views.py:199 +#: workflows/views.py:200 #, fuzzy #| msgid "Optionen speichern" msgid "Formularoptionen gespeichert" msgstr "Save options" -#: workflows/views.py:200 +#: workflows/views.py:201 #, fuzzy #| msgid "Feldtexte speichern" msgid "Feldtexte gespeichert" msgstr "Save field text" -#: workflows/views.py:201 +#: workflows/views.py:202 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Formularlayout gespeichert" msgstr "Save offboarding request" -#: workflows/views.py:202 +#: workflows/views.py:203 msgid "Einweisungs-Checkpunkt gelöscht" msgstr "" -#: workflows/views.py:203 +#: workflows/views.py:204 msgid "Einweisungs-Checkpunkt hinzugefügt" msgstr "" -#: workflows/views.py:204 +#: workflows/views.py:205 #, fuzzy #| msgid "Checkliste speichern" msgid "Einweisungs-Checkliste gespeichert" msgstr "Save checklist" -#: workflows/views.py:205 +#: workflows/views.py:206 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail sofort ausgelöst" msgstr "Welcome Emails" -#: workflows/views.py:206 +#: workflows/views.py:207 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Welcome E-Mail Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:207 +#: workflows/views.py:208 msgid "Welcome E-Mail Sammelaktion ausgeführt" msgstr "" -#: workflows/views.py:208 +#: workflows/views.py:209 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail pausiert" msgstr "Welcome Emails" -#: workflows/views.py:209 +#: workflows/views.py:210 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail fortgesetzt" msgstr "Welcome Emails" -#: workflows/views.py:210 +#: workflows/views.py:211 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail abgebrochen" msgstr "Welcome Emails" -#: workflows/views.py:211 +#: workflows/views.py:212 #, fuzzy #| msgid "SMTP-Test" msgid "SMTP-Test gesendet" msgstr "SMTP test" -#: workflows/views.py:212 +#: workflows/views.py:213 #, fuzzy #| msgid "Nextcloud-Test" msgid "Nextcloud-Testupload ausgeführt" msgstr "Nextcloud test" -#: workflows/views.py:213 +#: workflows/views.py:214 #, fuzzy #| msgid "Nextcloud schalten" msgid "Nextcloud-Modus umgeschaltet" msgstr "Toggle Nextcloud" -#: workflows/views.py:214 +#: workflows/views.py:215 msgid "E-Mail-Modus umgeschaltet" msgstr "" -#: workflows/views.py:215 +#: workflows/views.py:216 #, fuzzy #| msgid "Integrationen Setup" msgid "Integrationen gespeichert" msgstr "Integrations Setup" -#: workflows/views.py:216 +#: workflows/views.py:217 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Nextcloud-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:217 +#: workflows/views.py:218 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Mail-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:218 +#: workflows/views.py:219 #, fuzzy #| msgid "E-Mail Routing & Vorlagen speichern" msgid "E-Mail-Routing gespeichert" msgstr "Save email routing & templates" -#: workflows/views.py:219 +#: workflows/views.py:220 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Benachrichtigungsregeln gespeichert" msgstr "Save offboarding request" -#: workflows/views.py:401 +#: workflows/views.py:221 +#, fuzzy +#| msgid "Anfrage gespeichert" +msgid "Backup erstellt" +msgstr "Request saved" + +#: workflows/views.py:222 +msgid "Backup verifiziert" +msgstr "" + +#: workflows/views.py:223 +#, fuzzy +#| msgid "Anfrage gespeichert" +msgid "Backup gelöscht" +msgstr "Request saved" + +#: workflows/views.py:224 +#, fuzzy +#| msgid "Welcome-Einstellungen speichern" +msgid "Backup-Einstellungen gespeichert" +msgstr "Save welcome settings" + +#: workflows/views.py:408 +#, python-format +msgid "Backup wurde erstellt: %(name)s" +msgstr "" + +#: workflows/views.py:410 +#, python-format +msgid "Backup konnte nicht erstellt werden: %(error)s" +msgstr "" + +#: workflows/views.py:427 +#, python-format +msgid "Backup wurde verifiziert: %(name)s" +msgstr "" + +#: workflows/views.py:429 +#, python-format +msgid "Backup-Verifikation fehlgeschlagen: %(error)s" +msgstr "" + +#: workflows/views.py:446 +#, python-format +msgid "Backup wurde gelöscht: %(name)s" +msgstr "" + +#: workflows/views.py:448 +#, python-format +msgid "Backup konnte nicht gelöscht werden: %(error)s" +msgstr "" + +#: workflows/views.py:475 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Anfrage erstellt" msgstr "Request saved" -#: workflows/views.py:403 +#: workflows/views.py:477 #, fuzzy, python-format #| msgid "Sitzungsstatus" msgid "Status: %(status)s" msgstr "Session status" -#: workflows/views.py:415 +#: workflows/views.py:489 #, fuzzy #| msgid "Geplant für" msgid "Geplanter Start" msgstr "Scheduled for" -#: workflows/views.py:425 +#: workflows/views.py:499 msgid "Geräteübergabe / Hardware-Abholung" msgstr "" -#: workflows/views.py:427 +#: workflows/views.py:501 msgid "Geplanter Hardware-Termin" msgstr "" -#: workflows/views.py:436 +#: workflows/views.py:510 #, fuzzy #| msgid "Noch nicht verfügbar" msgid "PDF verfügbar" msgstr "Not available yet" -#: workflows/views.py:462 +#: workflows/views.py:536 #, fuzzy #| msgid "Einweisung" msgid "Einweisungssitzung" msgstr "Introduction" -#: workflows/views.py:474 +#: workflows/views.py:548 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail" msgstr "Welcome Emails" -#: workflows/views.py:500 +#: workflows/views.py:574 msgid "Sie haben keine Berechtigung für diese Aktion." msgstr "You do not have permission for this action." -#: workflows/views.py:509 +#: workflows/views.py:583 msgid "Keine Einträge ausgewählt." msgstr "No entries selected." -#: workflows/views.py:552 +#: workflows/views.py:626 #, python-format msgid "%(count)s Eintrag/Einträge gelöscht." msgstr "%(count)s entry/entries deleted." -#: workflows/views.py:554 +#: workflows/views.py:628 #, python-format msgid "%(count)s Auswahl(en) konnten nicht verarbeitet werden." msgstr "%(count)s selection(s) could not be processed." -#: workflows/views.py:556 +#: workflows/views.py:630 msgid "Keine passenden Einträge gefunden." msgstr "No matching entries found." -#: workflows/views.py:776 +#: workflows/views.py:850 msgid "Einweisungs- und Übergabeprotokoll wurde erzeugt." msgstr "Introduction and handover protocol was generated." -#: workflows/views.py:794 +#: workflows/views.py:868 msgid "Einweisungsprotokoll aus Live-Status wurde erzeugt." msgstr "Introduction protocol from live status was generated." -#: workflows/views.py:824 +#: workflows/views.py:898 msgid "Einweisung wurde zurückgesetzt." msgstr "Introduction was reset." -#: workflows/views.py:838 +#: workflows/views.py:912 msgid "Einweisung wurde als abgeschlossen gespeichert." msgstr "Introduction was saved as completed." -#: workflows/views.py:851 +#: workflows/views.py:925 msgid "Einweisung wurde als Entwurf gespeichert." msgstr "Introduction was saved as draft." @@ -2255,17 +2556,9 @@ msgstr "Introduction was saved as draft." #~ msgid "Nextcloud deaktivieren" #~ msgstr "Save Nextcloud" -#, fuzzy -#~| msgid "Nextcloud schalten" -#~ msgid "Nextcloud aktivieren" -#~ msgstr "Toggle Nextcloud" - #~ msgid "Aktiv/Inaktiv direkt umschalten." #~ msgstr "Switch active/inactive directly." -#~ msgid "Deaktivieren" -#~ msgstr "Disable" - #~ msgid "Aktivieren" #~ msgstr "Enable" @@ -2293,9 +2586,6 @@ msgstr "Introduction was saved as draft." #~ msgid "Testupload und Testmail auslösen." #~ msgstr "Trigger test upload and test email." -#~ msgid "Erstellt" -#~ msgstr "Created" - #~ msgid "" #~ "Datensätze können direkt in der Tabelle gefiltert, geöffnet, geprüft oder " #~ "gelöscht werden." diff --git a/backend/workflows/backup_ops.py b/backend/workflows/backup_ops.py new file mode 100644 index 0000000..8b6ec7c --- /dev/null +++ b/backend/workflows/backup_ops.py @@ -0,0 +1,325 @@ +from __future__ import annotations + +import hashlib +import json +import os +import shutil +import subprocess +import tarfile +import tempfile +from datetime import datetime +from pathlib import Path + +from django.conf import settings +from django.utils import timezone +from django.utils.translation import gettext as _ + +from .models import WorkflowConfig +from .services import delete_from_nextcloud, upload_to_nextcloud + + +def _backup_root() -> Path: + root = Path(settings.BACKUP_OUTPUT_DIR) + root.mkdir(parents=True, exist_ok=True) + return root + + +def _metadata_path(backup_dir: Path) -> Path: + return backup_dir / 'backup_meta.json' + + +def _checksums_path(backup_dir: Path) -> Path: + return backup_dir / 'SHA256SUMS' + + +def _db_env() -> dict[str, str]: + db = settings.DATABASES['default'] + env = os.environ.copy() + env['PGPASSWORD'] = str(db['PASSWORD']) + return env + + +def _db_base_args() -> list[str]: + db = settings.DATABASES['default'] + return [ + '-h', str(db['HOST']), + '-p', str(db['PORT']), + '-U', str(db['USER']), + ] + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open('rb') as handle: + for chunk in iter(lambda: handle.read(65536), b''): + digest.update(chunk) + return digest.hexdigest() + + +def _write_checksums(backup_dir: Path, db_dump_path: Path, media_archive_path: Path) -> None: + _checksums_path(backup_dir).write_text( + f'{_sha256(db_dump_path)} {db_dump_path.name}\n{_sha256(media_archive_path)} {media_archive_path.name}\n', + encoding='utf-8', + ) + + +def _ignorable_pg_restore(stderr: str) -> bool: + text = (stderr or '').strip() + if not text: + return False + normalized = ' '.join(line.strip() for line in text.splitlines()) + return ( + 'unrecognized configuration parameter "transaction_timeout"' in normalized + and 'errors ignored on restore: 1' in normalized + ) + + +def _load_metadata(backup_dir: Path) -> dict: + path = _metadata_path(backup_dir) + if not path.exists(): + return {} + try: + return json.loads(path.read_text(encoding='utf-8')) + except json.JSONDecodeError: + return {} + + +def _save_metadata(backup_dir: Path, payload: dict) -> None: + _metadata_path(backup_dir).write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding='utf-8') + + +def list_backup_bundles() -> list[dict]: + rows: list[dict] = [] + for entry in sorted(_backup_root().glob('backup_*'), reverse=True): + if not entry.is_dir(): + continue + meta = _load_metadata(entry) + rows.append( + { + 'name': entry.name, + 'path': str(entry), + 'created_at': meta.get('created_at') or '', + 'verified_at': meta.get('verified_at') or '', + 'verify_status': meta.get('verify_status') or '', + 'summary': meta.get('verify_summary') or '', + 'remote_status': meta.get('remote_status') or '', + 'remote_summary': meta.get('remote_summary') or '', + 'remote_target_type': meta.get('remote_target_type') or '', + 'remote_path': meta.get('remote_path') or '', + 'db_dump_exists': (entry / 'db.dump').exists(), + 'media_archive_exists': (entry / 'media.tar.gz').exists(), + } + ) + return rows + + +def _remote_backup_config() -> dict: + config = WorkflowConfig.objects.filter(name='Default').order_by('-id').first() or WorkflowConfig.objects.order_by('id').first() + if not config: + return {'enabled': False, 'target_type': '', 'nextcloud_directory': ''} + return { + 'enabled': bool(config.remote_backup_enabled), + 'target_type': (config.remote_backup_target_type or '').strip(), + 'nextcloud_directory': (config.remote_backup_nextcloud_directory or '').strip().strip('/'), + 's3_bucket': (config.remote_backup_s3_bucket or '').strip(), + 'nfs_path': (config.remote_backup_nfs_path or '').strip(), + } + + +def _upload_bundle_remote(backup_dir: Path, payload: dict) -> dict: + remote = _remote_backup_config() + payload.update( + { + 'remote_status': '', + 'remote_summary': '', + 'remote_target_type': remote['target_type'], + 'remote_path': '', + } + ) + if not remote['enabled']: + payload['remote_status'] = 'disabled' + payload['remote_summary'] = str(_('Remote Backup ist deaktiviert.')) + return payload + + if remote['target_type'] != 'nextcloud': + payload['remote_status'] = 'not_implemented' + payload['remote_summary'] = _('Zieltyp %(target)s ist vorbereitet, aber noch nicht implementiert.') % {'target': remote['target_type'] or '-'} + return payload + + remote_directory = remote['nextcloud_directory'] + if not remote_directory: + payload['remote_status'] = 'failed' + payload['remote_summary'] = str(_('Nextcloud Backup-Verzeichnis fehlt.')) + return payload + + remote_bundle_dir = f'{remote_directory}/{backup_dir.name}' + files_to_upload = ['db.dump', 'media.tar.gz', 'SHA256SUMS'] + uploaded_files: list[str] = [] + for filename in files_to_upload: + local_path = backup_dir / filename + if not local_path.exists(): + continue + ok = upload_to_nextcloud( + local_path, + filename, + directory_override=remote_bundle_dir, + require_enabled=False, + ) + if not ok: + payload['remote_status'] = 'failed' + payload['remote_summary'] = _('Upload nach Nextcloud fehlgeschlagen bei %(file)s.') % {'file': filename} + payload['remote_path'] = remote_bundle_dir + return payload + uploaded_files.append(filename) + + payload['remote_status'] = 'uploaded' + payload['remote_summary'] = _('Nach Nextcloud hochgeladen: %(count)s Datei(en).') % {'count': len(uploaded_files)} + payload['remote_uploaded_at'] = timezone.now().isoformat() + payload['remote_target_type'] = 'nextcloud' + payload['remote_path'] = remote_bundle_dir + payload['remote_files'] = uploaded_files + return payload + + +def create_backup_bundle() -> dict: + timestamp = timezone.localtime().strftime('%Y%m%d_%H%M%S') + backup_dir = _backup_root() / f'backup_{timestamp}' + backup_dir.mkdir(parents=True, exist_ok=False) + + db_dump_path = backup_dir / 'db.dump' + media_archive_path = backup_dir / 'media.tar.gz' + + db = settings.DATABASES['default'] + subprocess.run( + [ + 'pg_dump', + *_db_base_args(), + '-d', str(db['NAME']), + '-Fc', + '--no-owner', + '--no-privileges', + '-f', str(db_dump_path), + ], + check=True, + env=_db_env(), + ) + + with tarfile.open(media_archive_path, 'w:gz') as archive: + archive.add(settings.MEDIA_ROOT, arcname='media') + + payload = { + 'created_at': timezone.now().isoformat(), + 'postgres_db': str(db['NAME']), + 'postgres_user': str(db['USER']), + 'db_dump_file': db_dump_path.name, + 'media_archive_file': media_archive_path.name, + 'verify_status': '', + 'verified_at': '', + 'verify_summary': '', + } + _save_metadata(backup_dir, payload) + _write_checksums(backup_dir, db_dump_path, media_archive_path) + payload = _upload_bundle_remote(backup_dir, payload) + _save_metadata(backup_dir, payload) + if payload.get('remote_status') == 'uploaded' and payload.get('remote_path'): + upload_to_nextcloud( + _metadata_path(backup_dir), + _metadata_path(backup_dir).name, + directory_override=payload['remote_path'], + require_enabled=False, + ) + return {'name': backup_dir.name, 'path': str(backup_dir)} + + +def verify_backup_bundle(backup_name: str) -> dict: + backup_dir = _backup_root() / backup_name + db_dump_path = backup_dir / 'db.dump' + media_archive_path = backup_dir / 'media.tar.gz' + if not backup_dir.exists() or not db_dump_path.exists() or not media_archive_path.exists(): + raise FileNotFoundError(_('Backup-Dateien nicht gefunden.')) + + verify_db = f'{settings.DATABASES["default"]["NAME"]}_verify_{int(timezone.now().timestamp())}' + env = _db_env() + args = _db_base_args() + meta = _load_metadata(backup_dir) + + try: + subprocess.run( + ['psql', *args, '-d', 'postgres', '-v', 'ON_ERROR_STOP=1', '-c', f'CREATE DATABASE "{verify_db}";'], + check=True, + env=env, + capture_output=True, + text=True, + ) + restore = subprocess.run( + ['pg_restore', *args, '-d', verify_db, '--no-owner', '--no-privileges', str(db_dump_path)], + env=env, + capture_output=True, + text=True, + ) + if restore.returncode != 0 and not _ignorable_pg_restore(restore.stderr): + raise subprocess.CalledProcessError( + restore.returncode, + restore.args, + output=restore.stdout, + stderr=restore.stderr, + ) + with connection.cursor() as cursor: + pass + table_count = subprocess.check_output( + ['psql', *args, '-d', verify_db, '-t', '-A', '-c', "SELECT COUNT(*) FROM pg_tables WHERE schemaname='public';"], + env=env, + text=True, + ).strip() + onboarding_count = subprocess.check_output( + ['psql', *args, '-d', verify_db, '-t', '-A', '-c', 'SELECT COUNT(*) FROM workflows_onboardingrequest;'], + env=env, + text=True, + ).strip() + offboarding_count = subprocess.check_output( + ['psql', *args, '-d', verify_db, '-t', '-A', '-c', 'SELECT COUNT(*) FROM workflows_offboardingrequest;'], + env=env, + text=True, + ).strip() + with tempfile.TemporaryDirectory(prefix='tubco_backup_verify_media_') as tmpdir: + with tarfile.open(media_archive_path, 'r:gz') as archive: + archive.extractall(tmpdir, filter='data') + media_dir = Path(tmpdir) / 'media' + if not media_dir.exists(): + raise RuntimeError(_('Media-Archiv enthält kein media/-Verzeichnis.')) + media_file_count = sum(1 for path in media_dir.rglob('*') if path.is_file()) + summary = _('%(tables)s Tabellen, %(onboarding)s Onboarding, %(offboarding)s Offboarding, %(media)s Mediendateien geprüft.') % { + 'tables': table_count, + 'onboarding': onboarding_count, + 'offboarding': offboarding_count, + 'media': media_file_count, + } + meta['verified_at'] = timezone.now().isoformat() + meta['verify_status'] = 'verified' + meta['verify_summary'] = summary + _save_metadata(backup_dir, meta) + return {'name': backup_name, 'summary': summary} + finally: + subprocess.run( + ['psql', *args, '-d', 'postgres', '-v', 'ON_ERROR_STOP=1', '-c', f'DROP DATABASE IF EXISTS "{verify_db}";'], + check=False, + env=env, + capture_output=True, + text=True, + ) + + +def delete_backup_bundle(backup_name: str) -> dict: + backup_dir = (_backup_root() / backup_name).resolve() + backup_root = _backup_root().resolve() + if backup_root not in backup_dir.parents: + raise ValueError(_('Ungültiger Backup-Pfad.')) + if not backup_dir.exists() or not backup_dir.is_dir(): + raise FileNotFoundError(_('Backup-Dateien nicht gefunden.')) + meta = _load_metadata(backup_dir) + if meta.get('remote_status') == 'uploaded' and meta.get('remote_target_type') == 'nextcloud' and meta.get('remote_path'): + ok = delete_from_nextcloud(meta['remote_path'], directory_override='') + if not ok: + raise RuntimeError(_('Remote Backup in Nextcloud konnte nicht gelöscht werden.')) + shutil.rmtree(backup_dir) + return {'name': backup_name} diff --git a/backend/workflows/migrations/0035_workflowconfig_remote_backup_enabled_and_more.py b/backend/workflows/migrations/0035_workflowconfig_remote_backup_enabled_and_more.py new file mode 100644 index 0000000..3a5067d --- /dev/null +++ b/backend/workflows/migrations/0035_workflowconfig_remote_backup_enabled_and_more.py @@ -0,0 +1,38 @@ +# Generated by Django 5.1.5 on 2026-03-26 00:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflows', '0034_workflowconfig_device_handover_lead_days'), + ] + + operations = [ + migrations.AddField( + model_name='workflowconfig', + name='remote_backup_enabled', + field=models.BooleanField(default=False, verbose_name='Remote Backup aktiviert'), + ), + migrations.AddField( + model_name='workflowconfig', + name='remote_backup_nextcloud_directory', + field=models.CharField(blank=True, max_length=255, verbose_name='Nextcloud Backup-Verzeichnis'), + ), + migrations.AddField( + model_name='workflowconfig', + name='remote_backup_nfs_path', + field=models.CharField(blank=True, max_length=255, verbose_name='NFS Pfad (optional)'), + ), + migrations.AddField( + model_name='workflowconfig', + name='remote_backup_s3_bucket', + field=models.CharField(blank=True, max_length=255, verbose_name='S3 Bucket (optional)'), + ), + migrations.AddField( + model_name='workflowconfig', + name='remote_backup_target_type', + field=models.CharField(choices=[('nextcloud', 'Nextcloud'), ('s3', 'S3'), ('nfs', 'NFS')], default='nextcloud', max_length=20, verbose_name='Remote Backup Zieltyp'), + ), + ] diff --git a/backend/workflows/models.py b/backend/workflows/models.py index b68b1f1..7f6498c 100644 --- a/backend/workflows/models.py +++ b/backend/workflows/models.py @@ -387,6 +387,12 @@ class OnboardingIntroductionSession(models.Model): class WorkflowConfig(models.Model): + REMOTE_BACKUP_TARGET_CHOICES = [ + ('nextcloud', _('Nextcloud')), + ('s3', _('S3')), + ('nfs', _('NFS')), + ] + name = models.CharField(max_length=120, default='Default', unique=True) it_onboarding_email = models.EmailField(blank=True) general_info_email = models.EmailField(blank=True) @@ -413,6 +419,16 @@ class WorkflowConfig(models.Model): nextcloud_directory_override = models.CharField(max_length=255, blank=True, verbose_name='Nextcloud Verzeichnis (Override)') sync_interval_seconds = models.PositiveIntegerField(default=60, verbose_name='Sync-Intervall (Sekunden)') device_handover_lead_days = models.PositiveIntegerField(default=5, verbose_name='Vorlauf Geräteübergabe (Tage)') + remote_backup_enabled = models.BooleanField(default=False, verbose_name='Remote Backup aktiviert') + remote_backup_target_type = models.CharField( + max_length=20, + choices=REMOTE_BACKUP_TARGET_CHOICES, + default='nextcloud', + verbose_name='Remote Backup Zieltyp', + ) + remote_backup_nextcloud_directory = models.CharField(max_length=255, blank=True, verbose_name='Nextcloud Backup-Verzeichnis') + remote_backup_s3_bucket = models.CharField(max_length=255, blank=True, verbose_name='S3 Bucket (optional)') + remote_backup_nfs_path = models.CharField(max_length=255, blank=True, verbose_name='NFS Pfad (optional)') welcome_email_delay_days = models.PositiveIntegerField(default=5, verbose_name='Welcome E-Mail Verzögerung (Tage)') welcome_sender_email = models.EmailField(blank=True, verbose_name='Welcome E-Mail Absender') welcome_include_pdf = models.BooleanField(default=True, verbose_name='Welcome E-Mail mit PDF-Anhang') diff --git a/backend/workflows/services.py b/backend/workflows/services.py index ec0882b..dfd8f8a 100644 --- a/backend/workflows/services.py +++ b/backend/workflows/services.py @@ -10,15 +10,19 @@ from .models import WorkflowConfig logger = logging.getLogger(__name__) +def _active_workflow_config() -> WorkflowConfig | None: + return WorkflowConfig.objects.filter(name='Default').order_by('-id').first() or WorkflowConfig.objects.order_by('id').first() + + def is_nextcloud_enabled() -> bool: - config = WorkflowConfig.objects.order_by('id').first() + config = _active_workflow_config() if config and config.nextcloud_enabled_override is not None: return bool(config.nextcloud_enabled_override) return bool(settings.NEXTCLOUD_ENABLED) def is_email_test_mode() -> bool: - config = WorkflowConfig.objects.order_by('id').first() + config = _active_workflow_config() if config and config.email_test_mode_override is not None: return bool(config.email_test_mode_override) return bool(settings.EMAIL_TEST_MODE) @@ -29,7 +33,7 @@ def get_email_test_redirect() -> str: def get_nextcloud_settings() -> dict[str, str]: - config = WorkflowConfig.objects.order_by('id').first() + config = _active_workflow_config() base_url = ( config.nextcloud_base_url_override.strip() if config and config.nextcloud_base_url_override.strip() @@ -58,20 +62,49 @@ def get_nextcloud_settings() -> dict[str, str]: } -def upload_to_nextcloud(local_file: Path, remote_filename: str) -> bool: - if not is_nextcloud_enabled(): +def _nextcloud_remote_url(base_url: str, directory: str, remote_path: str) -> str: + cleaned_parts = [part.strip('/') for part in [directory, remote_path] if part and part.strip('/')] + return f"{base_url}/{'/'.join(cleaned_parts)}" + + +def _ensure_nextcloud_directory(base_url: str, directory: str, auth: tuple[str, str], timeout: int) -> bool: + if not directory: + return False + current_parts: list[str] = [] + for part in [p for p in directory.split('/') if p]: + current_parts.append(part) + response = requests.request( + 'MKCOL', + f"{base_url}/{'/'.join(current_parts)}", + auth=auth, + timeout=timeout, + ) + if response.status_code in (201, 301, 405): + continue + logger.warning('Nextcloud directory ensure failed with status %s for %s', response.status_code, '/'.join(current_parts)) + return False + return True + + +def upload_to_nextcloud(local_file: Path, remote_filename: str, *, directory_override: str | None = None, require_enabled: bool = True) -> bool: + if require_enabled and not is_nextcloud_enabled(): return False nc = get_nextcloud_settings() base_url = nc['base_url'] - directory = nc['directory'] + directory_source = nc['directory'] if directory_override is None else directory_override + directory = (directory_source or '').strip('/') if not base_url or not directory: return False safe_remote_name = Path(remote_filename).name - remote_url = f"{base_url}/{directory}/{safe_remote_name}" + remote_url = _nextcloud_remote_url(base_url, directory, safe_remote_name) retries = max(0, int(getattr(settings, 'NEXTCLOUD_UPLOAD_RETRIES', 2))) timeout = max(5, int(getattr(settings, 'NEXTCLOUD_UPLOAD_TIMEOUT_SECONDS', 30))) + auth = (nc['username'], nc['password']) + + if not _ensure_nextcloud_directory(base_url, directory, auth, timeout): + return False for attempt in range(retries + 1): try: @@ -79,7 +112,7 @@ def upload_to_nextcloud(local_file: Path, remote_filename: str) -> bool: response = requests.put( remote_url, data=handle, - auth=(nc['username'], nc['password']), + auth=auth, timeout=timeout, ) if response.status_code in (200, 201, 204): @@ -102,3 +135,21 @@ def upload_to_nextcloud(local_file: Path, remote_filename: str) -> bool: if attempt < retries: time.sleep(0.6 * (attempt + 1)) return False + + +def delete_from_nextcloud(remote_path: str, *, directory_override: str | None = None) -> bool: + nc = get_nextcloud_settings() + base_url = nc['base_url'] + directory_source = nc['directory'] if directory_override is None else directory_override + directory = (directory_source or '').strip('/') + if not base_url: + return False + if directory_override is None and not directory: + return False + timeout = max(5, int(getattr(settings, 'NEXTCLOUD_UPLOAD_TIMEOUT_SECONDS', 30))) + response = requests.delete( + _nextcloud_remote_url(base_url, directory, remote_path), + auth=(nc['username'], nc['password']), + timeout=timeout, + ) + return response.status_code in (200, 204, 404) diff --git a/backend/workflows/static/workflows/css/admin_tools.css b/backend/workflows/static/workflows/css/admin_tools.css index 0849a7d..99d5c76 100644 --- a/backend/workflows/static/workflows/css/admin_tools.css +++ b/backend/workflows/static/workflows/css/admin_tools.css @@ -1,10 +1,12 @@ body { margin: 0; font-family: Arial, sans-serif; background: #f4f8ff; color: #0f172a; padding: 20px; } +[hidden] { display: none !important; } .shell { max-width: 1100px; margin: 0 auto; background: #fff; border: 1px solid #d8e3f0; border-radius: 14px; padding: 16px; } h1 { margin: 12px 0 6px; color: #000078; } .sub { margin: 0 0 12px; color: #54657c; } .app-messages { margin-bottom: 12px; } .card { border: 1px solid #d8e3f0; border-radius: 12px; background: #fbfdff; padding: 12px; margin-bottom: 14px; transition: border-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 220ms cubic-bezier(0.2, 0.8, 0.2, 1), transform 220ms cubic-bezier(0.2, 0.8, 0.2, 1); } .grid { display: grid; grid-template-columns: repeat(2, minmax(240px, 1fr)); gap: 10px; } +.backup-grid { grid-template-columns: minmax(280px, 720px); } label { display: block; margin-bottom: 4px; font-size: 12px; color: #334155; font-weight: 700; } input, select, textarea { width: 100%; box-sizing: border-box; border: 1px solid #cbd5e1; border-radius: 8px; padding: 8px 9px; background: #fff; transition: border-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 180ms cubic-bezier(0.2, 0.8, 0.2, 1), background-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1); } textarea { min-height: 120px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; } diff --git a/backend/workflows/static/workflows/css/app_chrome.css b/backend/workflows/static/workflows/css/app_chrome.css index ec01873..3129592 100644 --- a/backend/workflows/static/workflows/css/app_chrome.css +++ b/backend/workflows/static/workflows/css/app_chrome.css @@ -191,6 +191,77 @@ display: none; } +.action-progress-modal[hidden] { + display: none; +} + +.action-progress-modal { + position: fixed; + inset: 0; + z-index: 1190; +} + +.action-progress-backdrop { + position: absolute; + inset: 0; + background: rgba(16, 32, 57, 0.34); + backdrop-filter: blur(5px); + animation: confirmFadeIn var(--motion-base) var(--motion-ease); +} + +.action-progress-panel { + position: relative; + width: min(520px, calc(100% - 32px)); + margin: min(16vh, 120px) auto 0; + padding: 22px; + border: 1px solid rgba(217, 227, 238, 0.94); + border-radius: 24px; + background: linear-gradient(180deg, rgba(255,255,255,0.99), rgba(247,250,255,0.97)); + box-shadow: 0 26px 64px rgba(18, 34, 56, 0.20); + animation: confirmPopIn var(--motion-slow) var(--motion-ease); +} + +.action-progress-kicker { + margin: 0 0 8px; + color: #5a6d87; + font-size: 12px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.action-progress-title { + margin: 0 0 8px; + color: #16315b; + font-size: 24px; + letter-spacing: -0.03em; +} + +.action-progress-copy { + margin: 0 0 16px; + color: #5a6d87; + font-size: 14px; + line-height: 1.55; +} + +.action-progress-track { + overflow: hidden; + position: relative; + height: 12px; + border-radius: 999px; + background: linear-gradient(180deg, #dce6f2, #d1dce9); + box-shadow: inset 0 1px 2px rgba(16, 32, 57, 0.10); +} + +.action-progress-bar { + width: 40%; + height: 100%; + border-radius: 999px; + background: linear-gradient(90deg, #000078, #3b70ea); + box-shadow: 0 0 18px rgba(59, 112, 234, 0.28); + animation: actionProgressSlide 1.05s cubic-bezier(0.4, 0, 0.2, 1) infinite; +} + .confirm-modal { position: fixed; inset: 0; @@ -259,6 +330,11 @@ body.confirm-open { } } +@keyframes actionProgressSlide { + 0% { transform: translateX(-120%); } + 100% { transform: translateX(270%); } +} + @media (prefers-reduced-motion: reduce) { :root { --motion-fast: 1ms; diff --git a/backend/workflows/static/workflows/js/action_progress.js b/backend/workflows/static/workflows/js/action_progress.js new file mode 100644 index 0000000..00f747a --- /dev/null +++ b/backend/workflows/static/workflows/js/action_progress.js @@ -0,0 +1,35 @@ +(function () { + const modal = document.getElementById('app-action-progress'); + const title = document.getElementById('app-action-progress-title'); + const copy = document.getElementById('app-action-progress-copy'); + if (!modal || !title || !copy) return; + + function showProgress(source) { + const nextTitle = source?.dataset?.progressTitle; + const nextCopy = source?.dataset?.progressCopy; + if (nextTitle) title.textContent = nextTitle; + if (nextCopy) copy.textContent = nextCopy; + modal.hidden = false; + modal.setAttribute('aria-hidden', 'false'); + document.body.classList.add('confirm-open'); + } + + window.AppActionProgress = { + show: showProgress, + }; + + document.addEventListener('submit', function (event) { + const form = event.target; + if (!(form instanceof HTMLFormElement)) return; + + const submitter = event.submitter; + if (submitter?.dataset?.progressTitle || submitter?.dataset?.progressCopy) { + showProgress(submitter); + return; + } + + if (form.dataset.progressTitle || form.dataset.progressCopy) { + showProgress(form); + } + }); +})(); diff --git a/backend/workflows/static/workflows/js/confirm_dialog.js b/backend/workflows/static/workflows/js/confirm_dialog.js index 3041f80..ad6b06f 100644 --- a/backend/workflows/static/workflows/js/confirm_dialog.js +++ b/backend/workflows/static/workflows/js/confirm_dialog.js @@ -58,6 +58,9 @@ open(message).then(function (confirmed) { if (!confirmed) return; form.dataset.confirmBypass = '1'; + if (window.AppActionProgress && (form.dataset.progressTitle || form.dataset.progressCopy)) { + window.AppActionProgress.show(form); + } if (typeof form.requestSubmit === 'function') { form.requestSubmit(); } else { @@ -79,6 +82,9 @@ open(button.dataset.confirm).then(function (confirmed) { if (!confirmed) return; button.dataset.confirmBypass = '1'; + if (window.AppActionProgress && (button.dataset.progressTitle || button.dataset.progressCopy || form.dataset.progressTitle || form.dataset.progressCopy)) { + window.AppActionProgress.show(button.dataset.progressTitle || button.dataset.progressCopy ? button : form); + } if (typeof form.requestSubmit === 'function') { form.requestSubmit(button); } else { diff --git a/backend/workflows/templates/workflows/backup_recovery.html b/backend/workflows/templates/workflows/backup_recovery.html new file mode 100644 index 0000000..9edda46 --- /dev/null +++ b/backend/workflows/templates/workflows/backup_recovery.html @@ -0,0 +1,114 @@ +{% extends 'workflows/base_shell.html' %} +{% load static i18n %} + +{% block title %}{% trans "Backup & Recovery" %}{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block shell_body %} +{% include 'workflows/includes/app_header.html' with header_show_home=1 header_show_lang=1 header_inside_shell=1 %} +
{% trans "Datenbank- und Media-Backups erstellen und vorhandene Bundles sicher verifizieren." %}
+ +{% include 'workflows/includes/messages.html' %} + +| {% trans "Bundle" %} | +{% trans "Erstellt" %} | +{% trans "Verifiziert" %} | +{% trans "Status" %} | +{% trans "Inhalt" %} | +{% trans "Remote" %} | +{% trans "Aktion" %} | +
|---|---|---|---|---|---|---|
{{ row.name }} |
+ {% if row.created_at %}{{ row.created_at|slice:":16"|cut:"T" }}{% else %}-{% endif %} | +{% if row.verified_at %}{{ row.verified_at|slice:":16"|cut:"T" }}{% else %}-{% endif %} | ++ {% if row.verify_status == 'verified' %} + {% trans "Verifiziert" %} + {% else %} + {% trans "Nicht geprüft" %} + {% endif %} + | +
+ {% if row.db_dump_exists %}DB{% endif %}
+ {% if row.media_archive_exists %}Media{% endif %}
+ {% if row.summary %}
+ {{ row.summary }}
+ {% endif %}
+ |
+
+ {% if row.remote_status == 'uploaded' %}
+ {% trans "Hochgeladen" %}
+ {% elif row.remote_status == 'failed' %}
+ {% trans "Fehlgeschlagen" %}
+ {% elif row.remote_status == 'not_implemented' %}
+ {% trans "Vorbereitet" %}
+ {% elif row.remote_status == 'disabled' %}
+ {% trans "Deaktiviert" %}
+ {% else %}
+ {% trans "Lokal" %}
+ {% endif %}
+ {% if row.db_dump_exists or row.media_archive_exists %}
+ {% trans "Lokal gespeichert" %}
+ {% else %}
+ {% trans "Lokal nicht vorhanden" %}
+ {% endif %}
+ {% if row.remote_target_type %}
+ {{ row.remote_target_type|upper }}
+ {% endif %}
+ {% if row.remote_path %}
+ {{ row.remote_path }}{{ row.remote_summary }}
+ {% endif %}
+ |
+
+
+
+
+
+ |
+