Compare commits

..

15 commits
v1.0.0 ... main

Author SHA1 Message Date
Michael
a00d7ef1c9 chore(repo): slim .gitignore and remove dev artifacts from tracking
Cleaning up the public-facing repo:
- /build.sh, /moduldoku.md, /package-lock.json (root) untracked in
  prior commit and now blocked via .gitignore
- .gitignore itself: dropped GSD baseline noise, deduped overlapping
  patterns, added the three newly-untracked dev files

The schneespur/ subdirectory (Laravel app) is what users consume;
root-level dev tooling has no business in the distributed source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 13:06:15 +00:00
Michael
382ee67868 test: Bootstrapped PHPUnit test infrastructure and wrote 9 FilterRegist…
- schneespur/phpunit.xml
- schneespur/tests/TestCase.php
- schneespur/tests/CreatesApplication.php
- schneespur/tests/Feature/FilterRegistryTest.php

GSD context:
- Milestone: M013 - Hook-Fundament — FilterRegistry, Events, NotificationChannelRegistry, Modul-Plumbing
- Slice: S01
- Task: T04 - Bootstrapped PHPUnit test infrastructure and wrote 9 FilterRegistry Feature tests covering priority ordering, stable sort, error isolation, context passing, value chaining, and two integration tests proving navigation and dashboard hooks are wired

GSD-Task: S01/T04
2026-05-21 13:04:10 +00:00
Michael
182f1b4c98 Release v1.0.5: Bugfix-Welle (Manual-Job-Form, :app_name, Logging, DSGVO-EN, Icons)
Sammelrelease nach Feedback aus der Test-Installation. Sieben unabhängige
Probleme — vom Form-Breaker über Übersetzungs-Lücken bis Log-Hygiene.

- fix(jobs): Einsatz manuell erfassen brach das HTML-Attribut, sobald
  Customer-Namen ein " enthielten (@json in x-data=""). Auf json_encode
  via {{ }} umgestellt (HTML-escaped). Alpine initialisiert wieder,
  Anlegen-Button funktioniert.
- feat(i18n): BrandedTranslator injiziert :app_name automatisch in alle
  __()-Aufrufe. Hilfetexte, Mails, Update-Strings rendern jetzt überall
  korrekt 'Schneespur' bzw. 'Wintertrace' statt rohes :app_name.
- feat(dsgvo): EN-Default-Vorlage default-template-en.blade.php mit
  UK/EU GDPR-Wording (Art. 6(1)(f), ICO-Hinweis, Subject Rights).
  Controller laden locale-aware mit Fallback auf DE. Placeholder-Helper
  kennt DE + EN Tokens.
- ui(settings): Alle 8 Settings-Karten haben jetzt Icons, nicht nur
  Module. Markup auf array-driven Loop entrümpelt.
- chore(modules): Example-Modul boot()-gated via EXAMPLE_MODULE_ENABLED
  env-Var (default false). Aus Release-ZIP komplett entfernt. Bestehende
  Installs mit altem example/-Ordner laden es nicht mehr automatisch.
- chore(logging): ModuleManager Discovery/Boot-Logs auf debug
  runtergesetzt (waren info → fluteten laravel.log bei jedem Request).
  Defaults auf daily-Rotation mit 14d Retention + LOG_LEVEL=warning für
  Production.
- fix(install): Hardcoded deutsche Fehlermeldung im InstallerController
  durch __('install.preflight_has_failures') ersetzt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 12:55:19 +00:00
Michael
ed375efb22 chore: auto-commit after execute-task
GSD-Unit: M013/S01/T03
2026-05-20 14:46:51 +00:00
Michael
ea727985ea chore: auto-commit after execute-task
GSD-Unit: M013/S01/T02
2026-05-20 14:44:33 +00:00
Michael
41878e92ef chore: auto-commit after execute-task
GSD-Unit: M013/S01/T01
2026-05-20 14:42:18 +00:00
Michael
87a84eb0ac Release v1.0.4: self-updater hotfixes (wrapper stripping + counter check)
- Self-updater extractZip: detect and strip a common top-level prefix
  folder so that release ZIPs built with a versioned wrapper (the
  build.sh convention) overlay the live install correctly instead of
  creating a schneespur-X.Y.Z/ subdirectory at the install root.
  The v1.0.3 update on the test environment surfaced this — files were
  copied, state.json was committed to 1.0.3, but the wrapper folder
  meant the live config/app.php and VERSION were never actually
  overwritten. Same defensive prefix-stripping pattern as the module
  installer.

- Counter check ordering: the same-version short-circuit now runs
  BEFORE the strict counter check. Previously, after a successful
  install, the next "prüfen" fetched the same manifest (same counter)
  and the counter check would throw "Rollback-Versuch", masking the
  intended "you're up to date" response. The counter check is still
  enforced for genuinely older manifests — it just doesn't misfire
  when the server keeps serving the same release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 14:03:12 +00:00
Michael
84c68ee35c Move moduldoku.md to project root
This is an internal authoring doc, not part of the shipped Laravel
application. Lifting it out of the schneespur/ subdirectory keeps it
out of the release build (build.sh copies only the Laravel app
directories) and clarifies the separation between code-that-ships
and meta documentation about the module system.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 13:20:33 +00:00
Michael
7e1222b022 Release v1.0.3: post-1.0.2 hotfixes (logging, customer objects, version display, module catalog & installer)
- bootstrap/app.php: restore default Laravel exception logging. The
  diagnostic reportable() callback no longer returns false unconditionally
  it only suppresses default reporting when a reporter actually handled the
  exception, so storage/logs/laravel.log shows errors again on fresh installs.

- Customer object creation: fix 500 when notify_recipients is empty (NOT NULL
  violation). Reconcile drift across migration/validation/form/lang: the field
  is now treated consistently as an enum (customer|object|both) matching the
  notification consumers; form uses a <select> instead of free-text input;
  validation tightened via in: rule; coercion in prepareForValidation keeps
  the DB invariant intact when the field is empty or missing.

- config/app.php: version is now read from the VERSION file at runtime.
  The previously hardcoded '1.0.0' caused footer, settings, and dashboard to
  show a stale version after every release. VERSION is now the single source
  of truth for display.

- Module catalog UI: fix render crash (htmlspecialchars on i18n category dict)
  and disappearing modules on 304 Not Modified responses. SchneespurModuleClient
  now has a normalizeModule() adapter that bridges server-side field naming
  (current_version, image_url, i18n category dict) to the internal shape used
  by controller and views. The catalog body is cached in state, so 304
  responses replay the cached catalog instead of falling back to the
  semantically wrong "installed" list.

- Module installer: strip common top-level prefix from module ZIPs to prevent
  modules/<slug>/<slug>/ double-nesting. The installer now detects whether all
  ZIP entries share one wrapper folder and strips it during extraction; flat
  ZIPs continue to work unchanged. Path-traversal validation runs on the
  original entry names before the strip, so the security guarantee is intact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:53:20 +00:00
Michael
7e27270626 Restructure: move source into schneespur/ subdirectory, remove vendor/release from tracking
- Root: README.md, LICENSE, INSTALL.de.md, INSTALL.en.md only
- schneespur/: all application source code
- Added .gitignore for vendor/, node_modules/, release/, .env, build artifacts
- Removed vendor/ and release/ from git tracking (15,699 files)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-18 17:02:37 +00:00
Michael
7288b93500 Release v1.0.2: diagnostic infrastructure core
Add neutral diagnostic framework for future reporting modules:
- DiagnosticReporterInterface, Registry, Manager, PayloadSanitizer
- Laravel exception hook in bootstrap/app.php
- Module permission declarations (requires_permissions in module.json)
- Core diagnostic report points (module boot/install/update failures)
- Module documentation update (moduldoku.md)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-18 16:54:11 +00:00
Michael
2c63440ed8 Revert: move code back to project root from schneespur/ subdirectory
- Reverts the schneespur/ subdirectory restructure (b8e426b)
- Restores package.json and vite.config.js (needed for npm build, were
  removed in an earlier cleanup before the restructure)
- Updates public/build/ assets with current Vite output (new content hashes)
2026-05-17 18:24:26 +00:00
Michael
53b29bd0e6 Release v1.0.1: installer locale detection from Step 1
User-facing improvements
- Installer detects browser language from the Accept-Language header
  on the very first request. de-* → Schneespur/DE, everything else
  (en-*, zh, ja, ar, it, fr, …) → Wintertrace/EN.
- DE/EN switcher in the installer layout header for manual override.
  The choice persists for the browser session.
- Step 5 (Config) now reflects the already-resolved locale instead of
  detecting again client-side, eliminating a brief "wrong language"
  flash between steps.

Under the hood
- New SetInstallerLocale middleware (Symfony's getPreferredLanguage
  with strict top-preference, list order ['en','de'] so non-matching
  browsers fall back to en/Wintertrace, not de/Schneespur).
- brand() helper resolves from the runtime locale when the app_brand
  setting is absent (pre-install). Post-install behaviour unchanged.
- Composer now declares ext-sodium explicitly (required by the Ed25519
  signature verification in the auto-update flow).

No DB migrations, no breaking changes, no config rewrites needed.
2026-05-17 17:35:02 +00:00
noschmarrn
d71e8717ec
Update README.md 2026-05-17 16:06:43 +02:00
Michael
b8e426ba2d Restructure: move code into schneespur/ subdirectory, fix Umlauts
- Move all application code into schneespur/ subdirectory for cleaner
  GitHub presentation (README, LICENSE, INSTALL guides stay in root)
- Fix German Umlaut encoding in INSTALL.de.md and README.md
  (ae→ä, oe→ö, ue→ü throughout)
- ZIP download structure remains flat (code in root) for easy deployment
2026-05-17 13:52:39 +00:00
8098 changed files with 8960 additions and 1042617 deletions

44
.gitignore vendored Normal file
View file

@ -0,0 +1,44 @@
# ── Dependencies ──
/node_modules/
/vendor/
/schneespur/node_modules/
/schneespur/vendor/
# ── Local env (keep .env.example tracked) ──
.env
.env.*
!.env.example
# ── Laravel runtime ──
/schneespur/storage/logs/*.log
/schneespur/storage/framework/cache/*
/schneespur/storage/framework/sessions/*
/schneespur/storage/framework/views/*
/schneespur/storage/testing/
# ── Build artifacts ──
/schneespur/public/build/
/schneespur/public/hot
/release/
# ── Internal dev files (not part of distributable app) ──
/build.sh
/moduldoku.md
/package-lock.json
# ── Test caches ──
.phpunit.result.cache
.phpunit.cache/
# ── IDE / OS ──
.idea/
.vscode/
*.swp
.DS_Store
Thumbs.db
# ── Local tooling ──
.gsd
.gsd-id
.mcp.json
.bg-shell/

View file

@ -27,9 +27,9 @@ Diese Anleitung beschreibt die Installation von Schneespur auf einem klassischen
| MySQL | 5.7 | 8.0+ | | MySQL | 5.7 | 8.0+ |
| MariaDB (alternativ) | 10.3 | 10.6+ | | MariaDB (alternativ) | 10.3 | 10.6+ |
### Benoetigte PHP-Erweiterungen ### Benötigte PHP-Erweiterungen
**Pflicht** (Installation schlaegt ohne diese fehl): **Pflicht** (Installation schlägt ohne diese fehl):
- `pdo_mysql` - `pdo_mysql`
- `gd` - `gd`
@ -89,9 +89,9 @@ Das Document-Root (manchmal auch „Webroot" oder „Stammverzeichnis" genannt)
**Beispiel:** Wenn Sie die Dateien nach `/schneespur/` hochgeladen haben, setzen Sie das Document-Root auf `/schneespur/public/`. **Beispiel:** Wenn Sie die Dateien nach `/schneespur/` hochgeladen haben, setzen Sie das Document-Root auf `/schneespur/public/`.
So geht das bei gaengigen Hostern: So geht das bei gängigen Hostern:
- **Strato:** Paket-Verwaltung → Domain-Verwaltung → Umleitung/Ziel → Pfad aendern - **Strato:** Paket-Verwaltung → Domain-Verwaltung → Umleitung/Ziel → Pfad ändern
- **IONOS:** Hosting → Domains → Document-Root bearbeiten - **IONOS:** Hosting → Domains → Document-Root bearbeiten
- **All-Inkl:** Domain-Einstellungen → Ordnerzuordnung - **All-Inkl:** Domain-Einstellungen → Ordnerzuordnung
@ -101,7 +101,7 @@ So geht das bei gaengigen Hostern:
## 4. Datenbank anlegen ## 4. Datenbank anlegen
Erstellen Sie ueber das Verwaltungspanel Ihres Hosters eine neue MySQL-Datenbank. Notieren Sie sich: Erstellen Sie über das Verwaltungspanel Ihres Hosters eine neue MySQL-Datenbank. Notieren Sie sich:
- **Host** (z. B. `localhost` oder `rdbms.strato.de`) - **Host** (z. B. `localhost` oder `rdbms.strato.de`)
- **Port** (Standard: `3306`) - **Port** (Standard: `3306`)
@ -109,27 +109,27 @@ Erstellen Sie ueber das Verwaltungspanel Ihres Hosters eine neue MySQL-Datenbank
- **Benutzername** - **Benutzername**
- **Passwort** - **Passwort**
Diese Daten benoetigen Sie im naechsten Schritt. Diese Daten benötigen Sie im nächsten Schritt.
--- ---
## 5. Installations-Assistent ## 5. Installations-Assistent
Oeffnen Sie Ihre Domain im Browser. Schneespur erkennt automatisch, dass noch keine Installation vorliegt, und startet den Assistenten. Öffnen Sie Ihre Domain im Browser. Schneespur erkennt automatisch, dass noch keine Installation vorliegt, und startet den Assistenten.
### Schritt 1: Willkommen ### Schritt 1: Willkommen
Der Assistent prueft die Grundvoraussetzungen und erzeugt die Konfigurationsdatei (`.env`) sowie den Anwendungsschluessel (`APP_KEY`). Der Assistent prüft die Grundvoraussetzungen und erzeugt die Konfigurationsdatei (`.env`) sowie den Anwendungsschlüssel (`APP_KEY`).
### Schritt 2: Datenbank ### Schritt 2: Datenbank
Geben Sie die Zugangsdaten aus Schritt 4 ein. Der Assistent testet die Verbindung, bevor er fortfaehrt. Geben Sie die Zugangsdaten aus Schritt 4 ein. Der Assistent testet die Verbindung, bevor er fortfährt.
> Falls die `.env`-Datei nicht beschreibbar ist (selten bei Shared-Hosting), zeigt der Assistent eine Anleitung zum manuellen Bearbeiten per FTP an. > Falls die `.env`-Datei nicht beschreibbar ist (selten bei Shared-Hosting), zeigt der Assistent eine Anleitung zum manuellen Bearbeiten per FTP an.
### Schritt 3: Systemcheck ### Schritt 3: Systemcheck
Der Assistent prueft PHP-Version, Erweiterungen und Schreibrechte auf wichtige Verzeichnisse (`storage/`, `bootstrap/cache/`). Fehlende Erweiterungen werden als Pflicht oder Empfehlung markiert. Der Assistent prüft PHP-Version, Erweiterungen und Schreibrechte auf wichtige Verzeichnisse (`storage/`, `bootstrap/cache/`). Fehlende Erweiterungen werden als Pflicht oder Empfehlung markiert.
### Schritt 4: Datenbank-Migration ### Schritt 4: Datenbank-Migration
@ -145,7 +145,7 @@ Legen Sie fest:
### Schritt 6: Speicher & Caches ### Schritt 6: Speicher & Caches
Der Assistent erstellt die Verknuepfung zum oeffentlichen Speicher (`storage:link`) und baut Caches auf. Falls die Verknuepfung auf Ihrem Hoster nicht funktioniert, wird eine Anleitung zum manuellen Anlegen per FTP angezeigt. Der Assistent erstellt die Verknüpfung zum öffentlichen Speicher (`storage:link`) und baut Caches auf. Falls die Verknüpfung auf Ihrem Hoster nicht funktioniert, wird eine Anleitung zum manuellen Anlegen per FTP angezeigt.
### Schritt 7: Admin-Konto ### Schritt 7: Admin-Konto
@ -153,17 +153,17 @@ Erstellen Sie Ihr Administrator-Konto (Name, E-Mail, Passwort mit mindestens 8 Z
### Schritt 8: E-Mail-Konfiguration (optional) ### Schritt 8: E-Mail-Konfiguration (optional)
Richten Sie SMTP-Versand ein, damit Schneespur Benachrichtigungen senden kann. Dieser Schritt kann uebersprungen und spaeter in den Einstellungen nachgeholt werden. Richten Sie SMTP-Versand ein, damit Schneespur Benachrichtigungen senden kann. Dieser Schritt kann übersprungen und später in den Einstellungen nachgeholt werden.
### Fertig ### Fertig
Nach Abschluss sehen Sie eine Zusammenfassung. Sie koennen sich jetzt mit Ihren Admin-Zugangsdaten anmelden. Nach Abschluss sehen Sie eine Zusammenfassung. Sie können sich jetzt mit Ihren Admin-Zugangsdaten anmelden.
--- ---
## 6. Cron-Job einrichten ## 6. Cron-Job einrichten
Schneespur benoetigt einen Cron-Job, der einmal pro Minute den Laravel-Scheduler ausfuehrt. Dieser verarbeitet die Auftragswarteschlange (z. B. Wetterdaten abrufen, Benachrichtigungen senden). Schneespur benötigt einen Cron-Job, der einmal pro Minute den Laravel-Scheduler ausführt. Dieser verarbeitet die Auftragswarteschlange (z. B. Wetterdaten abrufen, Benachrichtigungen senden).
### Cron-Befehl ### Cron-Befehl
@ -171,7 +171,7 @@ Schneespur benoetigt einen Cron-Job, der einmal pro Minute den Laravel-Scheduler
* * * * * /usr/local/bin/php /pfad/zu/schneespur/artisan schedule:run >> /dev/null 2>&1 * * * * * /usr/local/bin/php /pfad/zu/schneespur/artisan schedule:run >> /dev/null 2>&1
``` ```
> **Wichtig:** Ersetzen Sie `/pfad/zu/schneespur/` durch den tatsaechlichen Pfad auf Ihrem Webspace und `/usr/local/bin/php` durch den PHP-Pfad Ihres Hosters (haeufig auch `/usr/bin/php` oder `/usr/bin/php8.3`). > **Wichtig:** Ersetzen Sie `/pfad/zu/schneespur/` durch den tatsächlichen Pfad auf Ihrem Webspace und `/usr/local/bin/php` durch den PHP-Pfad Ihres Hosters (häufig auch `/usr/bin/php` oder `/usr/bin/php8.3`).
### So richten Sie den Cron-Job ein ### So richten Sie den Cron-Job ein
@ -179,13 +179,13 @@ Schneespur benoetigt einen Cron-Job, der einmal pro Minute den Laravel-Scheduler
- **IONOS:** Hosting → Cron-Jobs → Cronjob anlegen - **IONOS:** Hosting → Cron-Jobs → Cronjob anlegen
- **All-Inkl:** Tools → Cronjobs → Neuer Cronjob - **All-Inkl:** Tools → Cronjobs → Neuer Cronjob
Stellen Sie die Ausfuehrung auf **jede Minute** oder das kuerzeste verfuegbare Intervall. Stellen Sie die Ausführung auf **jede Minute** oder das kürzeste verfügbare Intervall.
### Warum ist der Cron-Job noetig? ### Warum ist der Cron-Job nötig?
Ohne Cron-Job werden keine Hintergrundaufgaben verarbeitet: Ohne Cron-Job werden keine Hintergrundaufgaben verarbeitet:
- Wetterdaten werden nicht automatisch zu Einsaetzen hinzugefuegt - Wetterdaten werden nicht automatisch zu Einsätzen hinzugefügt
- E-Mail-Benachrichtigungen werden nicht versendet - E-Mail-Benachrichtigungen werden nicht versendet
- Geplante Aufgaben laufen nicht - Geplante Aufgaben laufen nicht
@ -193,18 +193,18 @@ Ohne Cron-Job werden keine Hintergrundaufgaben verarbeitet:
## 7. OwnTracks einrichten ## 7. OwnTracks einrichten
OwnTracks ist die GPS-Tracking-App, mit der Ihre Fahrer die Einsaetze aufzeichnen. Jeder Fahrer benoetigt die App auf seinem Smartphone. OwnTracks ist die GPS-Tracking-App, mit der Ihre Fahrer die Einsätze aufzeichnen. Jeder Fahrer benötigt die App auf seinem Smartphone.
### Kurzanleitung ### Kurzanleitung
1. **App installieren:** OwnTracks aus dem App Store (iOS) oder Google Play Store (Android) herunterladen. 1. **App installieren:** OwnTracks aus dem App Store (iOS) oder Google Play Store (Android) herunterladen.
2. **Zugangsdaten erzeugen:** Melden Sie sich als Admin in Schneespur an, oeffnen Sie die Fahrer-Uebersicht und klicken Sie beim jeweiligen Fahrer auf „Zugangsdaten". Schneespur erzeugt automatisch Benutzername und Passwort. 2. **Zugangsdaten erzeugen:** Melden Sie sich als Admin in Schneespur an, öffnen Sie die Fahrer-Übersicht und klicken Sie beim jeweiligen Fahrer auf „Zugangsdaten". Schneespur erzeugt automatisch Benutzername und Passwort.
3. **QR-Code scannen:** Auf der Zugangsdaten-Seite wird ein QR-Code angezeigt. Der Fahrer scannt diesen mit der OwnTracks-App, und die Verbindung wird automatisch konfiguriert. 3. **QR-Code scannen:** Auf der Zugangsdaten-Seite wird ein QR-Code angezeigt. Der Fahrer scannt diesen mit der OwnTracks-App, und die Verbindung wird automatisch konfiguriert.
4. **Manuell konfigurieren** (falls QR-Code nicht funktioniert): 4. **Manuell konfigurieren** (falls QR-Code nicht funktioniert):
- Modus: **HTTP** - Modus: **HTTP**
- URL: `https://ihre-domain.de/api/owntracks/report` - URL: `https://ihre-domain.de/api/owntracks/report`
- Benutzername und Passwort: wie in Schneespur angezeigt - Benutzername und Passwort: wie in Schneespur angezeigt
5. **Testen:** Oeffnen Sie in Schneespur unter „OwnTracks" die Uebersicht. Sobald der Fahrer die App startet, sollte dort ein gruener Status erscheinen. 5. **Testen:** Öffnen Sie in Schneespur unter „OwnTracks" die Übersicht. Sobald der Fahrer die App startet, sollte dort ein grüner Status erscheinen.
--- ---
@ -213,17 +213,17 @@ OwnTracks ist die GPS-Tracking-App, mit der Ihre Fahrer die Einsaetze aufzeichne
### Vor dem Update ### Vor dem Update
1. Erstellen Sie ein Backup (siehe [Backup](#9-backup)). 1. Erstellen Sie ein Backup (siehe [Backup](#9-backup)).
2. Aktivieren Sie den Wartungsmodus: Oeffnen Sie `https://ihre-domain.de/down` im Browser oder fuehren Sie `php artisan down` per SSH/Cron aus. 2. Aktivieren Sie den Wartungsmodus: Öffnen Sie `https://ihre-domain.de/down` im Browser oder führen Sie `php artisan down` per SSH/Cron aus.
### Update durchfuehren ### Update durchführen
1. Laden Sie das neue Release herunter. 1. Laden Sie das neue Release herunter.
2. Ueberschreiben Sie alle Dateien per FTP. Ueberspringen Sie dabei **nicht** die `.env`-Datei — diese wird beim Upload ohnehin nicht ueberschrieben, solange Sie nur die Release-Dateien hochladen. 2. Überschreiben Sie alle Dateien per FTP. Überspringen Sie dabei **nicht** die `.env`-Datei — diese wird beim Upload ohnehin nicht überschrieben, solange Sie nur die Release-Dateien hochladen.
3. Fuehren Sie die Datenbank-Migration aus. Dafuer gibt es zwei Wege: 3. Führen Sie die Datenbank-Migration aus. Dafür gibt es zwei Wege:
- **Ueber den Browser:** Oeffnen Sie `https://ihre-domain.de/admin/settings` und pruefen Sie, ob eine Update-Migration angeboten wird. - **Ueber den Browser:** Öffnen Sie `https://ihre-domain.de/admin/settings` und prüfen Sie, ob eine Update-Migration angeboten wird.
- **Per Cron/SSH:** `php artisan migrate --force` - **Per Cron/SSH:** `php artisan migrate --force`
4. Leeren Sie die Caches: `php artisan config:cache && php artisan view:cache` 4. Leeren Sie die Caches: `php artisan config:cache && php artisan view:cache`
5. Deaktivieren Sie den Wartungsmodus: Oeffnen Sie `https://ihre-domain.de/up` oder fuehren Sie `php artisan up` aus. 5. Deaktivieren Sie den Wartungsmodus: Öffnen Sie `https://ihre-domain.de/up` oder führen Sie `php artisan up` aus.
--- ---
@ -239,9 +239,9 @@ OwnTracks ist die GPS-Tracking-App, mit der Ihre Fahrer die Einsaetze aufzeichne
### Empfohlener Rhythmus ### Empfohlener Rhythmus
- **Datenbank:** woechentlich oder vor jedem Update - **Datenbank:** wöchentlich oder vor jedem Update
- **Dateien:** vor jedem Update - **Dateien:** vor jedem Update
- **Konfiguration:** nach jeder Aenderung und vor Updates - **Konfiguration:** nach jeder Änderung und vor Updates
--- ---
@ -249,47 +249,47 @@ OwnTracks ist die GPS-Tracking-App, mit der Ihre Fahrer die Einsaetze aufzeichne
### Installations-Assistent erscheint nicht ### Installations-Assistent erscheint nicht
- Pruefen Sie, ob das Document-Root korrekt auf `/public` zeigt. - Prüfen Sie, ob das Document-Root korrekt auf `/public` zeigt.
- Pruefen Sie, ob die `.htaccess`-Datei im `public/`-Ordner vorhanden ist. - Prüfen Sie, ob die `.htaccess`-Datei im `public/`-Ordner vorhanden ist.
- Stellen Sie sicher, dass `mod_rewrite` (Apache) aktiviert ist. - Stellen Sie sicher, dass `mod_rewrite` (Apache) aktiviert ist.
### Datenbankverbindung schlaegt fehl ### Datenbankverbindung schlägt fehl
- Pruefen Sie Host, Port, Datenbankname, Benutzername und Passwort. - Prüfen Sie Host, Port, Datenbankname, Benutzername und Passwort.
- Bei Strato lautet der Host oft `rdbms.strato.de`, nicht `localhost`. - Bei Strato lautet der Host oft `rdbms.strato.de`, nicht `localhost`.
- Stellen Sie sicher, dass der Datenbankbenutzer Zugriff auf die angegebene Datenbank hat. - Stellen Sie sicher, dass der Datenbankbenutzer Zugriff auf die angegebene Datenbank hat.
### Seite zeigt „500 Internal Server Error" ### Seite zeigt „500 Internal Server Error"
- Pruefen Sie die Schreibrechte: `storage/` und `bootstrap/cache/` muessen beschreibbar sein (Rechte 755 oder 775). - Prüfen Sie die Schreibrechte: `storage/` und `bootstrap/cache/`ssen beschreibbar sein (Rechte 755 oder 775).
- Schauen Sie in `storage/logs/laravel.log` nach der Fehlermeldung. - Schauen Sie in `storage/logs/laravel.log` nach der Fehlermeldung.
### GPS-Daten kommen nicht an ### GPS-Daten kommen nicht an
- Pruefen Sie in OwnTracks, ob der Modus auf „HTTP" steht (nicht MQTT). - Prüfen Sie in OwnTracks, ob der Modus auf „HTTP" steht (nicht MQTT).
- Pruefen Sie die URL: `https://ihre-domain.de/api/owntracks/report` - Prüfen Sie die URL: `https://ihre-domain.de/api/owntracks/report`
- Pruefen Sie Benutzername und Passwort in der OwnTracks-App. - Prüfen Sie Benutzername und Passwort in der OwnTracks-App.
- Oeffnen Sie die OwnTracks-Uebersicht in Schneespur — dort wird der letzte Verbindungsstatus angezeigt. - Öffnen Sie die OwnTracks-Übersicht in Schneespur — dort wird der letzte Verbindungsstatus angezeigt.
### Wetterdaten fehlen bei Einsaetzen ### Wetterdaten fehlen bei Einsätzen
- Stellen Sie sicher, dass der Cron-Job laeuft (siehe [Cron-Job einrichten](#6-cron-job-einrichten)). - Stellen Sie sicher, dass der Cron-Job läuft (siehe [Cron-Job einrichten](#6-cron-job-einrichten)).
- Wetterdaten werden ueber Open-Meteo abgerufen. Pruefen Sie, ob Ihr Server ausgehende HTTPS-Verbindungen erlaubt. - Wetterdaten werden über Open-Meteo abgerufen. Prüfen Sie, ob Ihr Server ausgehende HTTPS-Verbindungen erlaubt.
### E-Mails werden nicht versendet ### E-Mails werden nicht versendet
- Pruefen Sie die SMTP-Einstellungen unter Einstellungen → E-Mail. - Prüfen Sie die SMTP-Einstellungen unter Einstellungen → E-Mail.
- Nutzen Sie die Test-E-Mail-Funktion in den Einstellungen. - Nutzen Sie die Test-E-Mail-Funktion in den Einstellungen.
- Schauen Sie in `storage/logs/laravel.log` nach Fehlermeldungen. - Schauen Sie in `storage/logs/laravel.log` nach Fehlermeldungen.
### Cron-Job funktioniert nicht ### Cron-Job funktioniert nicht
- Pruefen Sie den PHP-Pfad: Fuehren Sie `which php` aus oder fragen Sie Ihren Hoster. - Prüfen Sie den PHP-Pfad: Führen Sie `which php` aus oder fragen Sie Ihren Hoster.
- Pruefen Sie den Pfad zur `artisan`-Datei. - Prüfen Sie den Pfad zur `artisan`-Datei.
- Testen Sie den Befehl manuell: `php /pfad/zu/schneespur/artisan schedule:run` - Testen Sie den Befehl manuell: `php /pfad/zu/schneespur/artisan schedule:run`
--- ---
## Hilfe ## Hilfe
Bei Fragen nutzen Sie die integrierte Hilfe im Admin-Bereich (Menue → Hilfe) oder erstellen Sie ein Issue im GitHub-Repository. Bei Fragen nutzen Sie die integrierte Hilfe im Admin-Bereich (Menü → Hilfe) oder erstellen Sie ein Issue im GitHub-Repository.

View file

@ -1,5 +1,5 @@
<p align="center"> <p align="center">
<img src="public/pwa-icon-512x512.png" alt="Schneespur" width="120"> <img src="schneespur/public/pwa-icon-512x512.png" alt="Schneespur" width="120">
</p> </p>
<h1 align="center">Schneespur</h1> <h1 align="center">Schneespur</h1>
@ -9,34 +9,39 @@
GPS-Tracks &middot; Wetterdaten &middot; Fotos &middot; rechtsfester Einsatznachweis GPS-Tracks &middot; Wetterdaten &middot; Fotos &middot; rechtsfester Einsatznachweis
</p> </p>
<p align="center">
<a href="https://schneespur.de">schneespur.de</a> &middot;
<a href="https://wintertrace.com">wintertrace.com</a>
</p>
<p align="center"> <p align="center">
<a href="#english">English</a> &middot; <a href="#english">English</a> &middot;
<a href="INSTALL.de.md">Installation (DE)</a> &middot; <a href="INSTALL.de.md">Installation (DE)</a> &middot;
<a href="INSTALL.en.md">Installation (EN)</a> &middot; <a href="INSTALL.en.md">Installation (EN)</a> &middot;
<a href="https://jenni.noschmarrn.dev">Download</a> <a href="https://schneespur.de/download/">Download</a>
</p> </p>
--- ---
## Was ist Schneespur? ## Was ist Schneespur?
Schneespur dokumentiert Raeum- und Streueinsaetze fuer kleine Winterdienst-Betriebe — vollstaendig, automatisch und rechtssicher. Die Software laeuft auf jedem guenstigen Shared-Webhosting (Strato, IONOS, All-Inkl, ...) und braucht weder SSH noch Docker. Schneespur dokumentiert Räum- und Streueinsätze für kleine Winterdienst-Betriebe — vollständig, automatisch und rechtssicher. Die Software läuft auf jedem günstigen Shared-Webhosting (Strato, IONOS, All-Inkl, ...) und braucht weder SSH noch Docker.
**Kernversprechen:** Wenn ein Passant auf einer gestreuten Flaeche ausrutscht und der Betreiber nachweisen muss, dass er seiner Verkehrssicherungspflicht nachgekommen ist, liefert Schneespur den Beleg — mit GPS-Track, Wetterlage, Fotos und Zeitstempeln. **Kernversprechen:** Wenn ein Passant auf einer gestreuten Fläche ausrutscht und der Betreiber nachweisen muss, dass er seiner Verkehrssicherungspflicht nachgekommen ist, liefert Schneespur den Beleg — mit GPS-Track, Wetterlage, Fotos und Zeitstempeln.
### Funktionen ### Funktionen
- **GPS-Tracking** via [OwnTracks](https://owntracks.org)-App (iOS/Android) — kein eigener Tracking-Client noetig - **GPS-Tracking** via [OwnTracks](https://owntracks.org)-App (iOS/Android) — kein eigener Tracking-Client nötig
- **Automatische Wetterdokumentation** — Temperatur, Niederschlag, Wind, Schneelage zum Einsatzzeitpunkt (Open-Meteo, BrightSky, Met.no) - **Automatische Wetterdokumentation** — Temperatur, Niederschlag, Wind, Schneelage zum Einsatzzeitpunkt (Open-Meteo, BrightSky, Met.no)
- **Foto-Dokumentation** — Bilder direkt aus der Fahrer-App hochladen - **Foto-Dokumentation** — Bilder direkt aus der Fahrer-App hochladen
- **PDF-Einsatznachweise** — einzeln oder als Sammelreport pro Kunde und Zeitraum - **PDF-Einsatznachweise** — einzeln oder als Sammelreport pro Kunde und Zeitraum
- **Kundenportal** — Kunden koennen ihre Einsaetze selbst einsehen - **Kundenportal** — Kunden können ihre Einsätze selbst einsehen
- **Fahrer-App (PWA)** — funktioniert offline, synchronisiert automatisch bei Verbindung - **Fahrer-App (PWA)** — funktioniert offline, synchronisiert automatisch bei Verbindung
- **Kunden- und Objektverwaltung** — mehrere Objekte pro Kunde, Zuordnung zu Einsaetzen - **Kunden- und Objektverwaltung** — mehrere Objekte pro Kunde, Zuordnung zu Einsätzen
- **Fahrzeugverwaltung** — Fuhrpark mit Kennzeichen und Fahrzeugtyp - **Fahrzeugverwaltung** — Fuhrpark mit Kennzeichen und Fahrzeugtyp
- **DSGVO-konform** — Fahrer-Anonymisierung, Datenexport, konfigurierbare Aufbewahrungsfristen - **DSGVO-konform** — Fahrer-Anonymisierung, Datenexport, konfigurierbare Aufbewahrungsfristen
- **Automatische Updates** — kryptographisch signiert (Ed25519), ein Klick im Admin-Panel - **Automatische Updates** — kryptographisch signiert (Ed25519), ein Klick im Admin-Panel
- **Modulsystem**Erweiterbar ueber Module aus dem Schneespur-Modulkatalog - **Modulsystem**erweiterbar über Module aus dem Schneespur-Modulkatalog
### Systemanforderungen ### Systemanforderungen
@ -50,10 +55,10 @@ Schneespur dokumentiert Raeum- und Streueinsaetze fuer kleine Winterdienst-Betri
### Schnellstart ### Schnellstart
1. [Download](https://jenni.noschmarrn.dev) der aktuellen Version (ZIP) 1. [Download](https://schneespur.de/download/) der aktuellen Version (ZIP)
2. ZIP entpacken und per FTP auf den Webserver laden 2. ZIP entpacken und per FTP auf den Webserver laden
3. Document Root auf den `public/`-Ordner setzen 3. Document Root auf den `public/`-Ordner setzen
4. Im Browser die Domain aufrufen — der Installations-Assistent fuehrt durch die Einrichtung 4. Im Browser die Domain aufrufen — der Installations-Assistent führt durch die Einrichtung
Detaillierte Anleitung: **[INSTALL.de.md](INSTALL.de.md)** Detaillierte Anleitung: **[INSTALL.de.md](INSTALL.de.md)**
@ -110,7 +115,7 @@ Schneespur (German) / Wintertrace (international) is an open-source, self-hosted
### Quick Start ### Quick Start
1. [Download](https://jenni.noschmarrn.dev) the latest release (ZIP) 1. [Download](https://wintertrace.com/download/) the latest release (ZIP)
2. Extract and upload via FTP to your web server 2. Extract and upload via FTP to your web server
3. Set the document root to the `public/` directory 3. Set the document root to the `public/` directory
4. Open the domain in your browser — the installation wizard guides you through setup 4. Open the domain in your browser — the installation wizard guides you through setup

View file

@ -1 +0,0 @@
1.0.0

View file

@ -1,18 +0,0 @@
<?php
use App\Models\Setting;
function brand(): string
{
$slug = Setting::get('app_brand', 'schneespur');
return match ($slug) {
'wintertrace' => 'Wintertrace',
default => 'Schneespur',
};
}
function brand_slug(): string
{
return Setting::get('app_brand', 'schneespur');
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
if(!self.define){let e,s={};const r=(r,n)=>(r=new URL(r+".js",n).href,s[r]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=r,e.onload=s,document.head.appendChild(e)}else e=r,importScripts(r),s()}).then(()=>{let e=s[r];if(!e)throw new Error(`Module ${r} didnt register its module`);return e}));self.define=(n,i)=>{const o=e||("document"in self?document.currentScript.src:"")||location.href;if(s[o])return;let t={};const l=e=>r(e,o),c={module:{uri:o},exports:t,require:l};s[o]=Promise.all(n.map(e=>c[e]||l(e))).then(e=>(i(...e),t))}}define(["./workbox-466e78f2"],function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"registerSW.js",revision:"2d094791c49e920331981a2d203b8cdb"},{url:"assets/marker-icon-hN30_KVU.png",revision:null},{url:"assets/layers-BWBAp2CZ.png",revision:null},{url:"assets/layers-2x-Bpkbi35X.png",revision:null},{url:"assets/app-CQAECC6q.css",revision:null},{url:"assets/app-Bwe1Adxb.js",revision:null}],{}),e.cleanupOutdatedCaches(),e.registerRoute(/\/(login|register|forgot-password|reset-password|verify-email|confirm-password)/,new e.NetworkOnly,"GET"),e.registerRoute(/\.(?:woff2?|ttf|eot|otf)$/,new e.CacheFirst({cacheName:"fonts-cache",plugins:[new e.ExpirationPlugin({maxEntries:30,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/\/driver\/.*/,new e.NetworkFirst({cacheName:"driver-pages-cache",plugins:[new e.ExpirationPlugin({maxEntries:50,maxAgeSeconds:86400})]}),"GET")});

File diff suppressed because one or more lines are too long

View file

@ -1,47 +0,0 @@
<x-admin-layout>
<x-slot name="header">{{ __('admin.page_settings') }} <x-help-icon topic="settings" /></x-slot>
<div class="max-w-2xl space-y-4">
<a href="{{ route('admin.settings.branding') }}" class="block bg-white shadow-sm rounded-lg p-6 hover:bg-gray-50 transition-colors">
<h3 class="text-sm font-medium text-gray-900">{{ __('ui.branding_title') }}</h3>
<p class="mt-1 text-sm text-gray-500">{{ __('ui.branding_description') }}</p>
</a>
<a href="{{ route('admin.settings.email') }}" class="block bg-white shadow-sm rounded-lg p-6 hover:bg-gray-50 transition-colors">
<h3 class="text-sm font-medium text-gray-900">{{ __('notification.settings_card_email') }}</h3>
<p class="mt-1 text-sm text-gray-500">{{ __('notification.settings_card_email_desc') }}</p>
</a>
<a href="{{ route('admin.settings.notification-log') }}" class="block bg-white shadow-sm rounded-lg p-6 hover:bg-gray-50 transition-colors">
<h3 class="text-sm font-medium text-gray-900">{{ __('notification.settings_card_log') }}</h3>
<p class="mt-1 text-sm text-gray-500">{{ __('notification.settings_card_log_desc') }}</p>
</a>
<a href="{{ route('admin.settings.company') }}" class="block bg-white shadow-sm rounded-lg p-6 hover:bg-gray-50 transition-colors">
<h3 class="text-sm font-medium text-gray-900">{{ __('settings.company_title') }}</h3>
<p class="mt-1 text-sm text-gray-500">{{ __('settings.company_description') }}</p>
</a>
<a href="{{ route('admin.settings.retention') }}" class="block bg-white shadow-sm rounded-lg p-6 hover:bg-gray-50 transition-colors">
<h3 class="text-sm font-medium text-gray-900">{{ __('settings.retention_title') }}</h3>
<p class="mt-1 text-sm text-gray-500">{{ __('settings.retention_description') }}</p>
</a>
<a href="{{ route('admin.settings.weather') }}" class="block bg-white shadow-sm rounded-lg p-6 hover:bg-gray-50 transition-colors">
<h3 class="text-sm font-medium text-gray-900">{{ __('weather.settings_title') }}</h3>
<p class="mt-1 text-sm text-gray-500">{{ __('weather.settings_description') }}</p>
</a>
<a href="{{ route('admin.settings.update') }}" class="block bg-white shadow-sm rounded-lg p-6 hover:bg-gray-50 transition-colors">
<h3 class="text-sm font-medium text-gray-900">{{ __('update.settings_title') }}</h3>
<p class="mt-1 text-sm text-gray-500">{{ __('update.settings_description') }}</p>
</a>
<a href="{{ route('admin.settings.modules.index') }}" class="block bg-white shadow-sm rounded-lg p-6 hover:bg-gray-50 transition-colors">
<div class="flex items-center gap-3">
<div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg bg-indigo-50 text-indigo-600">
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />
</svg>
</div>
<div>
<h3 class="text-sm font-medium text-gray-900">{{ __('modules.settings_card_title') }}</h3>
<p class="mt-1 text-sm text-gray-500">{{ __('modules.settings_card_description') }}</p>
</div>
</div>
</a>
</div>
</x-admin-layout>

View file

@ -18,9 +18,10 @@ APP_MAINTENANCE_DRIVER=file
BCRYPT_ROUNDS=12 BCRYPT_ROUNDS=12
LOG_CHANNEL=stack LOG_CHANNEL=stack
LOG_STACK=single LOG_STACK=daily
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=warning
LOG_DAILY_DAYS=14
DB_CONNECTION=mysql DB_CONNECTION=mysql
DB_HOST=127.0.0.1 DB_HOST=127.0.0.1

1
schneespur/VERSION Normal file
View file

@ -0,0 +1 @@
1.0.5

View file

@ -4,6 +4,7 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Module; use App\Models\Module;
use App\Services\ModuleManager;
use App\Services\SchneespurModuleClient; use App\Services\SchneespurModuleClient;
use App\Services\SchneespurModuleInstaller; use App\Services\SchneespurModuleInstaller;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
@ -22,12 +23,7 @@ class AdminModuleController extends Controller
try { try {
$catalog = $client->fetchCatalog(); $catalog = $client->fetchCatalog();
if ($catalog !== null) {
$catalogModules = $catalog['modules'] ?? []; $catalogModules = $catalog['modules'] ?? [];
} else {
$state = $client->loadState();
$catalogModules = $state['installed'] ?? [];
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
Log::warning('schneespur-modules: catalog fetch failed in admin UI', [ Log::warning('schneespur-modules: catalog fetch failed in admin UI', [
'error' => $e->getMessage(), 'error' => $e->getMessage(),
@ -58,6 +54,7 @@ class AdminModuleController extends Controller
'download_url' => $catModule['download_url'] ?? null, 'download_url' => $catModule['download_url'] ?? null,
'sha256' => $catModule['sha256'] ?? null, 'sha256' => $catModule['sha256'] ?? null,
'size_bytes' => $catModule['size_bytes'] ?? null, 'size_bytes' => $catModule['size_bytes'] ?? null,
'requires_permissions' => $catModule['requires_permissions'] ?? [],
]; ];
} }
@ -80,6 +77,7 @@ class AdminModuleController extends Controller
'download_url' => null, 'download_url' => null,
'sha256' => null, 'sha256' => null,
'size_bytes' => null, 'size_bytes' => null,
'requires_permissions' => $this->resolveLocalPermissions($slug),
]; ];
} }
@ -230,6 +228,16 @@ class AdminModuleController extends Controller
->with('success', __('modules.disabled', ['slug' => $slug])); ->with('success', __('modules.disabled', ['slug' => $slug]));
} }
private function resolveLocalPermissions(string $slug): array
{
try {
$manager = app(ModuleManager::class);
return $manager->getPermissions($slug);
} catch (\Throwable) {
return [];
}
}
public function remove(string $slug, SchneespurModuleInstaller $installer): RedirectResponse public function remove(string $slug, SchneespurModuleInstaller $installer): RedirectResponse
{ {
$module = Module::where('slug', $slug)->first(); $module = Module::where('slug', $slug)->first();

View file

@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Setting; use App\Models\Setting;
use App\Services\Extension\DashboardWidgetRegistry; use App\Services\Extension\DashboardWidgetRegistry;
use App\Services\Extension\FilterRegistry;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\View\View; use Illuminate\View\View;
@ -17,9 +18,10 @@ class DashboardController extends Controller
return redirect()->route('admin.dashboard'); return redirect()->route('admin.dashboard');
} }
public function index(DashboardWidgetRegistry $widgetRegistry): View public function index(DashboardWidgetRegistry $widgetRegistry, FilterRegistry $filterRegistry): View
{ {
$widgets = $widgetRegistry->getWidgets(); $widgets = $widgetRegistry->getWidgets();
$widgets = $filterRegistry->apply('schneespur.dashboard.kpis', $widgets);
return view('admin.dashboard', compact('widgets')); return view('admin.dashboard', compact('widgets'));
} }

View file

@ -3,6 +3,7 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Controllers\DsgvoOnboardingController;
use App\Models\DsgvoConfirmation; use App\Models\DsgvoConfirmation;
use App\Models\Setting; use App\Models\Setting;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -18,7 +19,7 @@ class DsgvoAdminController extends Controller
$version = (int) Setting::get('dsgvo_template_version', 1); $version = (int) Setting::get('dsgvo_template_version', 1);
if ($markdown === null) { if ($markdown === null) {
$markdown = view('dsgvo.default-template')->render(); $markdown = view(DsgvoOnboardingController::resolveDefaultTemplateView())->render();
} }
$previewHtml = Str::markdown($this->replacePlaceholders($markdown), ['html_input' => 'strip']); $previewHtml = Str::markdown($this->replacePlaceholders($markdown), ['html_input' => 'strip']);
@ -82,25 +83,7 @@ class DsgvoAdminController extends Controller
private function replacePlaceholders(string $text): string private function replacePlaceholders(string $text): string
{ {
$companyName = Setting::get('company_name', ''); return dsgvo_apply_company_placeholders($text);
$street = Setting::get('company_street', '');
$zip = Setting::get('company_zip', '');
$city = Setting::get('company_city', '');
$email = Setting::get('company_email', '');
$dpo = Setting::get('dpo_contact', '');
$dpoEmail = Setting::get('dpo_email', '');
$address = trim("$street, $zip $city", ', ');
$replacements = [
'[Firmenname eintragen]' => $companyName ?: '[Firmenname eintragen]',
'[Adresse eintragen]' => $address ?: '[Adresse eintragen]',
'[E-Mail-Adresse eintragen]' => $email ?: '[E-Mail-Adresse eintragen]',
'[DPO-E-Mail-Adresse eintragen]' => $dpoEmail ?: '[DPO-E-Mail-Adresse eintragen]',
'[Datenschutzbeauftragter / Ansprechpartner eintragen]' => $dpo ?: '[Datenschutzbeauftragter / Ansprechpartner eintragen]',
];
return str_replace(array_keys($replacements), array_values($replacements), $text);
} }
public function showConfirmation(int $id): View public function showConfirmation(int $id): View

View file

@ -66,32 +66,22 @@ class DsgvoOnboardingController extends Controller
$version = (int) Setting::get('dsgvo_template_version', 1); $version = (int) Setting::get('dsgvo_template_version', 1);
if ($text === null) { if ($text === null) {
$text = view('dsgvo.default-template')->render(); $text = view(self::resolveDefaultTemplateView())->render();
} }
return [$text, $version]; return [$text, $version];
} }
public static function resolveDefaultTemplateView(): string
{
$locale = app()->getLocale();
$localized = "dsgvo.default-template-{$locale}";
return view()->exists($localized) ? $localized : 'dsgvo.default-template';
}
private function replacePlaceholders(string $text): string private function replacePlaceholders(string $text): string
{ {
$companyName = Setting::get('company_name', ''); return dsgvo_apply_company_placeholders($text);
$street = Setting::get('company_street', '');
$zip = Setting::get('company_zip', '');
$city = Setting::get('company_city', '');
$email = Setting::get('company_email', '');
$dpo = Setting::get('dpo_contact', '');
$dpoEmail = Setting::get('dpo_email', '');
$address = trim("$street, $zip $city", ', ');
$replacements = [
'[Firmenname eintragen]' => $companyName ?: '[Firmenname eintragen]',
'[Adresse eintragen]' => $address ?: '[Adresse eintragen]',
'[E-Mail-Adresse eintragen]' => $email ?: '[E-Mail-Adresse eintragen]',
'[DPO-E-Mail-Adresse eintragen]' => $dpoEmail ?: '[DPO-E-Mail-Adresse eintragen]',
'[Datenschutzbeauftragter / Ansprechpartner eintragen]' => $dpo ?: '[Datenschutzbeauftragter / Ansprechpartner eintragen]',
];
return str_replace(array_keys($replacements), array_values($replacements), $text);
} }
} }

View file

@ -27,6 +27,17 @@ class InstallerController extends Controller
private InstallLockManager $lockManager, private InstallLockManager $lockManager,
) {} ) {}
// --- Locale switcher (works on any installer step) ---
public function switchLocale(Request $request, string $locale): RedirectResponse
{
if (in_array($locale, ['de', 'en'], true)) {
$request->session()->put('installer_locale', $locale);
}
return redirect($request->headers->get('referer') ?: route('install.welcome'));
}
// --- Step 1: Welcome --- // --- Step 1: Welcome ---
public function showWelcome(Request $request): View public function showWelcome(Request $request): View
@ -118,7 +129,7 @@ class InstallerController extends Controller
{ {
if ($this->preflightChecker->hasCriticalFailures()) { if ($this->preflightChecker->hasCriticalFailures()) {
return redirect()->route('install.preflight') return redirect()->route('install.preflight')
->withErrors(['preflight' => 'Kritische Voraussetzungen nicht erfüllt.']); ->withErrors(['preflight' => __('install.preflight_has_failures')]);
} }
return redirect()->route('install.database'); return redirect()->route('install.database');

View file

@ -0,0 +1,36 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\View;
use Symfony\Component\HttpFoundation\Response;
class SetInstallerLocale
{
// Order matters: Symfony's getPreferredLanguage falls back to the FIRST list
// entry when no browser language matches, so 'en' must lead — that makes
// zh-CN, ja, ar, etc. resolve to en/Wintertrace instead of de/Schneespur.
private const SUPPORTED = ['en', 'de'];
private const FALLBACK = 'en';
public function handle(Request $request, Closure $next): Response
{
$session = $request->session()->get('installer_locale');
if (in_array($session, self::SUPPORTED, true)) {
$locale = $session;
} else {
$locale = $request->getPreferredLanguage(self::SUPPORTED) ?: self::FALLBACK;
$request->session()->put('installer_locale', $locale);
}
App::setLocale($locale);
View::share('installerLocale', $locale);
return $next($request);
}
}

View file

@ -19,6 +19,14 @@ class StoreCustomerObjectRequest extends FormRequest
'price_amount' => str_replace(',', '.', $this->price_amount), 'price_amount' => str_replace(',', '.', $this->price_amount),
]); ]);
} }
// notify_recipients is a mode enum consumed as customer|object|both
// (see SendJobCompletedNotification / SendCustomerReportEmail).
// Fall back to the DB default when the field is missing or empty
// so the NOT NULL column never receives a null value.
if (! $this->filled('notify_recipients')) {
$this->merge(['notify_recipients' => 'customer']);
}
} }
/** /**
@ -44,7 +52,7 @@ class StoreCustomerObjectRequest extends FormRequest
'lon' => ['nullable', 'numeric', 'between:-180,180'], 'lon' => ['nullable', 'numeric', 'between:-180,180'],
'auto_notify_email' => ['boolean'], 'auto_notify_email' => ['boolean'],
'notification_email' => ['nullable', 'required_if:auto_notify_email,1', 'email', 'max:200'], 'notification_email' => ['nullable', 'required_if:auto_notify_email,1', 'email', 'max:200'],
'notify_recipients' => ['nullable', 'string', 'max:1000'], 'notify_recipients' => ['required', 'in:customer,object,both'],
]; ];
} }

Some files were not shown because too many files have changed in this diff Show more