diff --git a/build.sh b/build.sh index f8470c6..81ebc70 100755 --- a/build.sh +++ b/build.sh @@ -54,9 +54,10 @@ cp -r "$SOURCE_DIR/resources" "$BUILD_DIR/" cp -r "$SOURCE_DIR/routes" "$BUILD_DIR/" cp -r "$SOURCE_DIR/vendor" "$BUILD_DIR/" -# Modules directory (example module for reference) +# Modules directory — example module is dev-only and excluded from releases if [ -d "$SOURCE_DIR/modules" ]; then cp -r "$SOURCE_DIR/modules" "$BUILD_DIR/" + rm -rf "$BUILD_DIR/modules/example" fi # Documentation and legal → build root (flat, alongside code) diff --git a/schneespur/.env.example b/schneespur/.env.example index 43f7442..208abd9 100644 --- a/schneespur/.env.example +++ b/schneespur/.env.example @@ -18,9 +18,10 @@ APP_MAINTENANCE_DRIVER=file BCRYPT_ROUNDS=12 LOG_CHANNEL=stack -LOG_STACK=single +LOG_STACK=daily LOG_DEPRECATIONS_CHANNEL=null -LOG_LEVEL=debug +LOG_LEVEL=warning +LOG_DAILY_DAYS=14 DB_CONNECTION=mysql DB_HOST=127.0.0.1 diff --git a/schneespur/VERSION b/schneespur/VERSION index ee90284..90a27f9 100644 --- a/schneespur/VERSION +++ b/schneespur/VERSION @@ -1 +1 @@ -1.0.4 +1.0.5 diff --git a/schneespur/app/Http/Controllers/Admin/DsgvoAdminController.php b/schneespur/app/Http/Controllers/Admin/DsgvoAdminController.php index 0dff2d5..417946d 100644 --- a/schneespur/app/Http/Controllers/Admin/DsgvoAdminController.php +++ b/schneespur/app/Http/Controllers/Admin/DsgvoAdminController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Http\Controllers\DsgvoOnboardingController; use App\Models\DsgvoConfirmation; use App\Models\Setting; use Illuminate\Http\Request; @@ -18,7 +19,7 @@ class DsgvoAdminController extends Controller $version = (int) Setting::get('dsgvo_template_version', 1); if ($markdown === null) { - $markdown = view('dsgvo.default-template')->render(); + $markdown = view(DsgvoOnboardingController::resolveDefaultTemplateView())->render(); } $previewHtml = Str::markdown($this->replacePlaceholders($markdown), ['html_input' => 'strip']); @@ -82,25 +83,7 @@ class DsgvoAdminController extends Controller private function replacePlaceholders(string $text): string { - $companyName = Setting::get('company_name', ''); - $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); + return dsgvo_apply_company_placeholders($text); } public function showConfirmation(int $id): View diff --git a/schneespur/app/Http/Controllers/DsgvoOnboardingController.php b/schneespur/app/Http/Controllers/DsgvoOnboardingController.php index ab1726f..333b7db 100644 --- a/schneespur/app/Http/Controllers/DsgvoOnboardingController.php +++ b/schneespur/app/Http/Controllers/DsgvoOnboardingController.php @@ -66,32 +66,22 @@ class DsgvoOnboardingController extends Controller $version = (int) Setting::get('dsgvo_template_version', 1); if ($text === null) { - $text = view('dsgvo.default-template')->render(); + $text = view(self::resolveDefaultTemplateView())->render(); } 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 { - $companyName = Setting::get('company_name', ''); - $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); + return dsgvo_apply_company_placeholders($text); } } diff --git a/schneespur/app/Http/Controllers/InstallerController.php b/schneespur/app/Http/Controllers/InstallerController.php index 97b012c..d16e0c3 100644 --- a/schneespur/app/Http/Controllers/InstallerController.php +++ b/schneespur/app/Http/Controllers/InstallerController.php @@ -129,7 +129,7 @@ class InstallerController extends Controller { if ($this->preflightChecker->hasCriticalFailures()) { return redirect()->route('install.preflight') - ->withErrors(['preflight' => 'Kritische Voraussetzungen nicht erfüllt.']); + ->withErrors(['preflight' => __('install.preflight_has_failures')]); } return redirect()->route('install.database'); diff --git a/schneespur/app/Providers/AppServiceProvider.php b/schneespur/app/Providers/AppServiceProvider.php index d27e4b8..1ff4a68 100644 --- a/schneespur/app/Providers/AppServiceProvider.php +++ b/schneespur/app/Providers/AppServiceProvider.php @@ -22,6 +22,7 @@ use App\Services\ModuleManager; use App\Services\RetentionService; use App\Services\SchneespurUpdater; use App\Services\SeasonService; +use App\Services\Translation\BrandedTranslator; use App\Services\Weather\BrightSkyProvider; use App\Services\Weather\MetNorwayProvider; use App\Services\Weather\OpenMeteoApiProvider; @@ -66,6 +67,17 @@ class AppServiceProvider extends ServiceProvider if (empty(config('app.key'))) { config(['app.key' => 'base64:'.base64_encode(random_bytes(32))]); } + + $this->app->extend('translator', function ($translator, $app) { + if ($translator instanceof BrandedTranslator) { + return $translator; + } + $branded = new BrandedTranslator($translator->getLoader(), $translator->getLocale()); + if ($fallback = $translator->getFallback()) { + $branded->setFallback($fallback); + } + return $branded; + }); } /** diff --git a/schneespur/app/Services/ModuleManager.php b/schneespur/app/Services/ModuleManager.php index ebebb03..57c90d4 100644 --- a/schneespur/app/Services/ModuleManager.php +++ b/schneespur/app/Services/ModuleManager.php @@ -56,7 +56,7 @@ class ModuleManager $this->modules[$slug] = $manifest; - Log::info('ModuleManager: module discovered', [ + Log::debug('ModuleManager: module discovered', [ 'slug' => $slug, 'version' => $manifest['version'] ?? 'unknown', ]); @@ -93,6 +93,13 @@ class ModuleManager continue; } + // Reference example module — opt-in only via env var. Ensures the + // bundled dev demo never auto-loads on customer installs even if the + // old folder is still present after upgrading from older releases. + if ($slug === 'example' && ! env('EXAMPLE_MODULE_ENABLED', false)) { + continue; + } + $namespace = $manifest['namespace'] ?? null; $srcPath = ($manifest['path'] ?? '') . '/src'; @@ -128,7 +135,7 @@ class ModuleManager $provider->register(); $provider->boot(); - Log::info('ModuleManager: module booted', [ + Log::debug('ModuleManager: module booted', [ 'slug' => $slug, 'version' => $manifest['version'] ?? 'unknown', ]); @@ -165,7 +172,7 @@ class ModuleManager $this->disabledModules[] = $slug; } - Log::info('ModuleManager: module disabled', ['slug' => $slug]); + Log::debug('ModuleManager: module disabled', ['slug' => $slug]); return true; } diff --git a/schneespur/app/Services/Translation/BrandedTranslator.php b/schneespur/app/Services/Translation/BrandedTranslator.php new file mode 100644 index 0000000..1e1679a --- /dev/null +++ b/schneespur/app/Services/Translation/BrandedTranslator.php @@ -0,0 +1,26 @@ +resolveAppName(); + } + + return parent::get($key, $replace, $locale, $fallback); + } + + private function resolveAppName(): string + { + try { + return brand(); + } catch (\Throwable) { + return (string) config('app.name', 'Schneespur'); + } + } +} diff --git a/schneespur/app/helpers.php b/schneespur/app/helpers.php index 8f963be..42e194b 100644 --- a/schneespur/app/helpers.php +++ b/schneespur/app/helpers.php @@ -23,3 +23,46 @@ function brand(): string default => 'Schneespur', }; } + +/** + * Replace company/DPO placeholders in DSGVO/GDPR template markdown. + * + * Knows both German and English placeholder strings so the same Settings + * values feed into either template language. Missing settings leave the + * placeholder visible so admins notice gaps. + */ +function dsgvo_apply_company_placeholders(string $text): string +{ + $companyName = Setting::get('company_name', ''); + $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", ', '); + + $map = [ + // German placeholders (default-template.blade.php) + '[Firmenname eintragen]' => $companyName, + '[Adresse eintragen]' => $address, + '[E-Mail-Adresse eintragen]' => $email, + '[DPO-E-Mail-Adresse eintragen]' => $dpoEmail, + '[Datenschutzbeauftragter / Ansprechpartner eintragen]' => $dpo, + // English placeholders (default-template-en.blade.php) + '[Company name]' => $companyName, + '[Address]' => $address, + '[Email]' => $email, + '[DPO email]' => $dpoEmail, + '[Data Protection Officer / Contact]' => $dpo, + ]; + + foreach ($map as $token => $value) { + if ($value !== '') { + $text = str_replace($token, $value, $text); + } + } + + return $text; +} diff --git a/schneespur/config/logging.php b/schneespur/config/logging.php index b09cb25..fb30ed8 100644 --- a/schneespur/config/logging.php +++ b/schneespur/config/logging.php @@ -54,21 +54,21 @@ return [ 'stack' => [ 'driver' => 'stack', - 'channels' => explode(',', (string) env('LOG_STACK', 'single')), + 'channels' => explode(',', (string) env('LOG_STACK', 'daily')), 'ignore_exceptions' => false, ], 'single' => [ 'driver' => 'single', 'path' => storage_path('logs/laravel.log'), - 'level' => env('LOG_LEVEL', 'debug'), + 'level' => env('LOG_LEVEL', 'warning'), 'replace_placeholders' => true, ], 'daily' => [ 'driver' => 'daily', 'path' => storage_path('logs/laravel.log'), - 'level' => env('LOG_LEVEL', 'debug'), + 'level' => env('LOG_LEVEL', 'warning'), 'days' => env('LOG_DAILY_DAYS', 14), 'replace_placeholders' => true, ], diff --git a/schneespur/modules/example/module.json b/schneespur/modules/example/module.json index f35180d..1451911 100644 --- a/schneespur/modules/example/module.json +++ b/schneespur/modules/example/module.json @@ -3,7 +3,8 @@ "version": "1.0.0", "namespace": "Schneespur\\Module\\Example", "service_provider": "Schneespur\\Module\\Example\\ExampleServiceProvider", - "description": "Reference module demonstrating all extension points (nav, widget, event, settings, route).", + "description": "Reference module demonstrating all extension points (nav, widget, event, settings, route). Dev-only — opt in via EXAMPLE_MODULE_ENABLED=true in .env.", "min_schneespur_version": "1.0.0", - "requires_permissions": [] + "requires_permissions": [], + "default_enabled": false } diff --git a/schneespur/modules/example/src/ExampleServiceProvider.php b/schneespur/modules/example/src/ExampleServiceProvider.php index 3d769d1..7b23f6f 100644 --- a/schneespur/modules/example/src/ExampleServiceProvider.php +++ b/schneespur/modules/example/src/ExampleServiceProvider.php @@ -19,6 +19,10 @@ class ExampleServiceProvider extends ServiceProvider public function boot(): void { + if (! $this->shouldBoot()) { + return; + } + $this->loadViewsFrom(__DIR__ . '/../resources/views', 'example-module'); $this->registerNavigation(); @@ -28,6 +32,15 @@ class ExampleServiceProvider extends ServiceProvider $this->registerRoutes(); } + /** + * Reference module — only loads when explicitly enabled. + * Devs can enable for local exploration via .env: EXAMPLE_MODULE_ENABLED=true + */ + protected function shouldBoot(): bool + { + return (bool) env('EXAMPLE_MODULE_ENABLED', false); + } + protected function registerNavigation(): void { $nav = $this->app->make(NavigationRegistry::class); diff --git a/schneespur/resources/views/admin/jobs/manual/create.blade.php b/schneespur/resources/views/admin/jobs/manual/create.blade.php index 8318bc5..77a60e6 100644 --- a/schneespur/resources/views/admin/jobs/manual/create.blade.php +++ b/schneespur/resources/views/admin/jobs/manual/create.blade.php @@ -22,9 +22,9 @@ @endphp
{{ __('admin.page_settings') }} + @php + $cards = [ + [ + 'route' => 'admin.settings.branding', + 'title' => __('ui.branding_title'), + 'desc' => __('ui.branding_description'), + 'icon' => 'M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42', + ], + [ + 'route' => 'admin.settings.email', + 'title' => __('notification.settings_card_email'), + 'desc' => __('notification.settings_card_email_desc'), + 'icon' => 'M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75', + ], + [ + 'route' => 'admin.settings.notification-log', + 'title' => __('notification.settings_card_log'), + 'desc' => __('notification.settings_card_log_desc'), + 'icon' => 'M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z', + ], + [ + 'route' => 'admin.settings.company', + 'title' => __('settings.company_title'), + 'desc' => __('settings.company_description'), + 'icon' => 'M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3.75h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008zm0 3h.008v.008h-.008v-.008z', + ], + [ + 'route' => 'admin.settings.retention', + 'title' => __('settings.retention_title'), + 'desc' => __('settings.retention_description'), + 'icon' => 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z', + ], + [ + 'route' => 'admin.settings.weather', + 'title' => __('weather.settings_title'), + 'desc' => __('weather.settings_description'), + 'icon' => 'M2.25 15a4.5 4.5 0 004.5 4.5H18a3.75 3.75 0 001.332-7.257 3 3 0 00-3.758-3.848 5.25 5.25 0 00-10.233 2.33A4.502 4.502 0 002.25 15z', + ], + [ + 'route' => 'admin.settings.update', + 'title' => __('update.settings_title'), + 'desc' => __('update.settings_description'), + 'icon' => 'M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99', + ], + [ + 'route' => 'admin.settings.modules.index', + 'title' => __('modules.settings_card_title'), + 'desc' => __('modules.settings_card_description'), + 'icon' => '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', + ], + ]; + @endphp +