chore: auto-commit after execute-task

GSD-Unit: M013/S01/T01
This commit is contained in:
Michael 2026-05-20 14:42:18 +00:00
parent 87a84eb0ac
commit 41878e92ef
10 changed files with 14904 additions and 16 deletions

24
.gitignore vendored
View file

@ -38,3 +38,27 @@ Thumbs.db
# Misc
*.bak
*.orig
# ── GSD baseline (auto-generated) ──
.gsd
.gsd-id
.mcp.json
.bg-shell/
*~
*.code-workspace
.env.*
!.env.example
node_modules/
.next/
dist/
build/
__pycache__/
*.pyc
.venv/
venv/
target/
vendor/
*.log
coverage/
.cache/
tmp/

214
build.sh Executable file
View file

@ -0,0 +1,214 @@
#!/usr/bin/env bash
set -euo pipefail
VERSION="${1:-1.0.0}"
PRODUCT="schneespur"
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
SOURCE_DIR="${PROJECT_DIR}/${PRODUCT}"
BUILD_DIR="${PROJECT_DIR}/release/${PRODUCT}-${VERSION}"
ZIP_FILE="${PROJECT_DIR}/release/${PRODUCT}-${VERSION}.zip"
echo "════════════════════════════════════════════"
echo " Building ${PRODUCT} v${VERSION}"
echo "════════════════════════════════════════════"
cd "$PROJECT_DIR"
# ── Clean previous build ──
rm -rf "$BUILD_DIR" "$ZIP_FILE"
mkdir -p "$BUILD_DIR"
# ── 1. Frontend build ──
echo ""
echo "▸ Installing npm dependencies..."
(cd "$SOURCE_DIR" && (npm ci --silent 2>/dev/null || npm install --silent))
echo "▸ Building frontend assets..."
(cd "$SOURCE_DIR" && npm run build)
# ── 2. Composer production install ──
echo ""
echo "▸ Installing composer dependencies (production)..."
(cd "$SOURCE_DIR" && composer install --no-dev --optimize-autoloader --no-interaction --quiet)
# ── 3. Copy files ──
echo ""
echo "▸ Copying project files..."
# Core Laravel → build root (flat layout, like 1.0.0)
cp "$SOURCE_DIR/artisan" "$BUILD_DIR/"
cp "$SOURCE_DIR/composer.json" "$BUILD_DIR/"
cp "$SOURCE_DIR/composer.lock" "$BUILD_DIR/"
cp "$SOURCE_DIR/.env.example" "$BUILD_DIR/"
cp "$SOURCE_DIR/.editorconfig" "$BUILD_DIR/" 2>/dev/null || true
cp "$SOURCE_DIR/.htaccess" "$BUILD_DIR/" 2>/dev/null || true
# Application code → build root
cp -r "$SOURCE_DIR/app" "$BUILD_DIR/"
cp -r "$SOURCE_DIR/bootstrap" "$BUILD_DIR/"
cp -r "$SOURCE_DIR/config" "$BUILD_DIR/"
cp -r "$SOURCE_DIR/database" "$BUILD_DIR/"
cp -r "$SOURCE_DIR/lang" "$BUILD_DIR/"
cp -r "$SOURCE_DIR/public" "$BUILD_DIR/"
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)
if [ -d "$SOURCE_DIR/modules" ]; then
cp -r "$SOURCE_DIR/modules" "$BUILD_DIR/"
fi
# Documentation and legal → build root (flat, alongside code)
cp "$PROJECT_DIR/README.md" "$BUILD_DIR/" 2>/dev/null || true
cp "$PROJECT_DIR/LICENSE" "$BUILD_DIR/" 2>/dev/null || true
cp "$PROJECT_DIR/INSTALL.de.md" "$BUILD_DIR/" 2>/dev/null || true
cp "$PROJECT_DIR/INSTALL.en.md" "$BUILD_DIR/" 2>/dev/null || true
# ── 4. Prepare storage structure (empty, writable) ──
echo "▸ Preparing storage structure..."
rm -rf "$BUILD_DIR/storage"
mkdir -p "$BUILD_DIR/storage/app/private"
mkdir -p "$BUILD_DIR/storage/app/public"
mkdir -p "$BUILD_DIR/storage/framework/cache/data"
mkdir -p "$BUILD_DIR/storage/framework/sessions"
mkdir -p "$BUILD_DIR/storage/framework/testing"
mkdir -p "$BUILD_DIR/storage/framework/views"
mkdir -p "$BUILD_DIR/storage/logs"
for dir in "$BUILD_DIR/storage/app/private" \
"$BUILD_DIR/storage/app/public" \
"$BUILD_DIR/storage/framework/cache/data" \
"$BUILD_DIR/storage/framework/sessions" \
"$BUILD_DIR/storage/framework/testing" \
"$BUILD_DIR/storage/framework/views" \
"$BUILD_DIR/storage/logs"; do
touch "$dir/.gitkeep"
done
# ── 5. Remove dev/unnecessary files ──
echo "▸ Cleaning up dev files..."
# Remove installed.lock (fresh install!)
rm -f "$BUILD_DIR/storage/app/installed.lock"
# Remove public/storage symlink (installer creates it)
rm -f "$BUILD_DIR/public/storage"
# Remove bootstrap cache (regenerated on first run)
rm -f "$BUILD_DIR/bootstrap/cache/"*.php 2>/dev/null || true
# Remove test files
rm -rf "$BUILD_DIR/tests"
# Remove dev config/tooling
rm -f "$BUILD_DIR/phpunit.xml"
rm -f "$BUILD_DIR/.styleci.yml"
rm -f "$BUILD_DIR/.gitignore"
rm -f "$BUILD_DIR/.gitattributes"
rm -f "$BUILD_DIR/package.json"
rm -f "$BUILD_DIR/package-lock.json"
rm -f "$BUILD_DIR/vite.config.js"
# Remove GSD/AI/editor artifacts
rm -rf "$BUILD_DIR/.gsd"
rm -f "$BUILD_DIR/.gsd-id"
rm -f "$BUILD_DIR/CLAUDE.md"
rm -f "$BUILD_DIR/gpt.md"
rm -f "$BUILD_DIR/module.md"
rm -f "$BUILD_DIR/site.md"
rm -f "$BUILD_DIR/.mcp.json"
# Remove test/scratch files
rm -f "$BUILD_DIR/test.txt"
rm -f "$BUILD_DIR/test-file.txt"
rm -f "$BUILD_DIR/app/test.php"
# Remove database factories/seeders (not needed in production)
rm -rf "$BUILD_DIR/database/factories"
rm -rf "$BUILD_DIR/database/seeders"
# ── 6. Slim vendor directory ──
echo "▸ Slimming vendor directory..."
# Remove .git directories from vendor packages (source installs)
find "$BUILD_DIR/vendor" -type d -name ".git" -exec rm -rf {} + 2>/dev/null || true
# Remove tests, docs, and other non-runtime directories
find "$BUILD_DIR/vendor" -type d \( \
-name "tests" -o \
-name "Tests" -o \
-name "test" -o \
-name "Test" -o \
-name "test_files" -o \
-name "docs" -o \
-name "doc" -o \
-name "examples" -o \
-name "example" -o \
-name ".github" \
\) -exec rm -rf {} + 2>/dev/null || true
# Remove non-runtime files
find "$BUILD_DIR/vendor" -type f \( \
-name "*.md" -o \
-name "*.markdown" -o \
-name "CHANGELOG*" -o \
-name "CHANGE_LOG*" -o \
-name "CHANGES*" -o \
-name "UPGRADING*" -o \
-name "UPGRADE*" -o \
-name "SECURITY*" -o \
-name "CONTRIBUTING*" -o \
-name "CODE_OF_CONDUCT*" -o \
-name ".editorconfig" -o \
-name ".gitignore" -o \
-name ".gitattributes" -o \
-name ".php-cs-fixer*" -o \
-name ".php_cs*" -o \
-name "phpunit.xml*" -o \
-name "phpstan*" -o \
-name ".styleci.yml" -o \
-name ".travis.yml" -o \
-name "Makefile" -o \
-name "Dockerfile" -o \
-name "docker-compose*" \
\) -delete 2>/dev/null || true
# ── 7. Write version file ──
echo "▸ Writing version file..."
cat > "$BUILD_DIR/VERSION" << EOF
${VERSION}
EOF
# ── 8. Write initial update state (prevents self-update to same version) ──
echo "▸ Writing initial update state..."
cat > "$BUILD_DIR/storage/app/schneespur_update_state.json" << EOF
{"current_version":"${VERSION}","last_counter":1}
EOF
# ── 9. Create ZIP ──
echo ""
echo "▸ Creating ZIP archive..."
rm -f "${ZIP_FILE}.filepart"
(cd "${BUILD_DIR}" && zip -r -q "${ZIP_FILE}" .)
# ── 10. Summary ──
ZIP_SIZE=$(du -h "$ZIP_FILE" | cut -f1)
FILE_COUNT=$(find "$BUILD_DIR" -type f | wc -l)
echo ""
echo "════════════════════════════════════════════"
echo " ✓ Build complete!"
echo ""
echo " Version: ${VERSION}"
echo " Files: ${FILE_COUNT}"
echo " ZIP: ${ZIP_FILE} (${ZIP_SIZE})"
echo "════════════════════════════════════════════"
echo ""
echo " The ZIP is ready for distribution."
echo " Users: unzip → FTP upload → open in browser → installer starts"
# ── 11. Restore dev dependencies ──
echo ""
echo "▸ Restoring dev composer dependencies..."
(cd "$SOURCE_DIR" && composer install --no-interaction --quiet)

View file

@ -167,7 +167,9 @@ class MeinModulServiceProvider extends ServiceProvider
slug: 'mein-modul',
label: 'Mein Modul',
route: 'admin.mein-modul.settings',
icon: 'heroicon-o-puzzle-piece',
// Wichtig: ROHE SVG-Path-Geometrie (`d=...`), KEIN Heroicon-Name.
// Mehrere Pfade mit `||` trennen. Siehe Abschnitt "Navigation" unten.
icon: 'M2.25 12l8.954-8.955a1.126 1.126 0 011.591 0L21.75 12...',
order: 200,
);
}
@ -195,7 +197,9 @@ class MeinModulServiceProvider extends ServiceProvider
protected function registerRoutes(): void
{
Route::middleware(['web', 'auth'])
// Für Admin-Routen: `'admin'`-Alias (EnsureAdmin) ist Pflicht.
// Sonst kann jeder eingeloggte Nutzer (auch Fahrer/Kunden) die Seite öffnen.
Route::middleware(['web', 'auth', 'admin'])
->prefix('admin/mein-modul')
->name('admin.mein-modul.')
->group(function () {
@ -223,7 +227,9 @@ $nav->addItem(
slug: 'mein-modul', // Eindeutiger Bezeichner
label: 'Mein Modul', // Anzeige-Label
route: 'admin.mein-modul.settings', // Laravel-Route-Name
icon: 'heroicon-o-cog-6-tooth', // Heroicon-Bezeichner
icon: 'M2.25 12l8.954-8.955...', // ROHE SVG-Path-Geometrie (`d=...`),
// KEIN Heroicon-Name. Mehrere Pfade mit `||` trennen.
// Beispiele: AppServiceProvider.php der Schneespur-Core-Items.
order: 200, // Sortierung (höher = weiter unten)
permission: null, // Optional: Berechtigungsprüfung
routeCheck: null, // Optional: Route-Existenzprüfung
@ -300,18 +306,31 @@ $widgets->registerWidget('mein-widget', [
**Blade-View des Widgets:**
Das View erhält die Daten aus `dataCallback` als `$data`-Variable:
Das View erhält das gesamte Widget-Config-Array als `$widget`-Variable.
Die Daten aus `dataCallback` liegen unter `$widget['data']`. **Achtung:**
nicht `$data` — diese Variable existiert im Widget-Render-Kontext nicht.
```blade
{{-- resources/views/widgets/status-card.blade.php --}}
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
<h3 class="text-sm font-medium text-gray-900">Mein Widget</h3>
@if(isset($data['anzahl']))
<p class="text-2xl font-bold">{{ $data['anzahl'] }}</p>
@if(isset($widget['data']['anzahl']))
<p class="text-2xl font-bold">{{ $widget['data']['anzahl'] }}</p>
@endif
</div>
```
Bei häufigem Zugriff lohnt sich ein lokaler Alias:
```blade
@php $data = $widget['data'] ?? []; @endphp
{{-- ab hier wie gewohnt $data['...'] verwenden --}}
```
Schneespurs eigene Dashboard-Widgets (`resources/views/admin/dashboard/widgets/*.blade.php`)
greifen einheitlich über `$widget['data']` zu — guter Referenzort für
Beispiele.
### 3. Wetter-Provider (WeatherProviderRegistry)
Module können eigene Wetterdatenquellen registrieren.
@ -452,8 +471,9 @@ use Illuminate\Support\Facades\Route;
protected function registerRoutes(): void
{
// Admin-Bereich (authentifiziert)
Route::middleware(['web', 'auth'])
// Admin-Bereich: `'admin'`-Alias (EnsureAdmin) ist Pflicht, sonst können
// auch Fahrer und Kunden mit Portal-Login die Route aufrufen.
Route::middleware(['web', 'auth', 'admin'])
->prefix('admin/mein-modul')
->name('admin.mein-modul.')
->group(function () {
@ -469,11 +489,16 @@ protected function registerRoutes(): void
| Kontext | Middleware | Prefix |
|---------|-----------|--------|
| Admin-Seiten | `['web', 'auth']` | `admin/mein-modul` |
| Admin-Seiten | `['web', 'auth', 'admin']` | `admin/mein-modul` |
| API-Endpoints | `['api']` | `api/mein-modul` |
| Öffentlich | `['web']` | Nach Bedarf |
**Empfehlung:** Für Admin-Routen immer `auth`-Middleware verwenden. Route-Namen mit `admin.mein-modul.` prefixen, damit die Navigation korrekt markiert wird.
**Empfehlung:** Für Admin-Routen **immer** `'admin'`-Alias zusätzlich zu `'auth'`
verwenden. `'auth'` allein lässt jeden eingeloggten Nutzer durch — auch
Fahrer und Kunden mit Portal-Login. Der Alias `'admin'` ist in
`bootstrap/app.php` auf `App\Http\Middleware\EnsureAdmin::class` gemappt.
Route-Namen mit `admin.mein-modul.` prefixen, damit die Navigation
korrekt markiert wird.
### 6. Views laden
@ -490,10 +515,25 @@ return view('mein-modul::settings', ['key' => 'value']);
'view' => 'mein-modul::widgets.status-card',
```
Der View-Namespace (`mein-modul`) isoliert die Templates vom Rest der Anwendung. In Blade-Templates können alle Schneespur-Layouts und Components verwendet werden:
Der View-Namespace (`mein-modul`) isoliert die Templates vom Rest der Anwendung. In Blade-Templates können alle Schneespur-Layouts und Components verwendet werden.
**Wichtig — richtiges Layout wählen:**
| Layout | Wann | Liefert |
|--------|------|---------|
| `<x-admin-layout>` | Admin-Seiten (alles unter `/admin/...`) | Admin-Sidebar, Top-Bar, Schneespur-Chrome |
| `<x-app-layout>` | Allgemeine eingeloggte Seiten (Breeze-Standard) | Nur Top-Bar, **ohne** Admin-Sidebar |
| `<x-driver-layout>` | Fahrer-Bereich | Fahrer-Chrome |
| `<x-portal-layout>` | Kunden-Portal | Kunden-Chrome |
Für ein **Admin-Modul** ist `<x-admin-layout>` praktisch immer richtig.
`<x-app-layout>` zu nehmen ist ein häufiger Stolperfall — die Seite
rendert ohne Sidebar, sieht aus als wäre sie aus dem Admin-Bereich
herausgefallen.
```blade
<x-app-layout>
{{-- Admin-Seite: <x-admin-layout> — Slot-Signaturen sind identisch zu app-layout --}}
<x-admin-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
Mein Modul — Einstellungen
@ -505,7 +545,7 @@ Der View-Namespace (`mein-modul`) isoliert die Templates vom Rest der Anwendung.
{{-- Modul-Inhalte --}}
</div>
</div>
</x-app-layout>
</x-admin-layout>
```
---

7283
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -15,6 +15,7 @@ use App\Services\Diagnostic\DiagnosticManager;
use App\Services\Diagnostic\DiagnosticPayloadSanitizer;
use App\Services\Diagnostic\DiagnosticReporterRegistry;
use App\Services\Extension\DashboardWidgetRegistry;
use App\Services\Extension\FilterRegistry;
use App\Services\Extension\NavigationRegistry;
use App\Services\ForecastService;
use App\Services\ModuleManager;
@ -46,6 +47,7 @@ class AppServiceProvider extends ServiceProvider
{
$this->app->singleton(AlertService::class);
$this->app->singleton(DashboardWidgetRegistry::class);
$this->app->singleton(FilterRegistry::class);
$this->app->singleton(NavigationRegistry::class);
$this->app->singleton(DiagnosticPayloadSanitizer::class);
$this->app->singleton(DiagnosticReporterRegistry::class, fn ($app) => new DiagnosticReporterRegistry($app));

View file

@ -0,0 +1,43 @@
<?php
namespace App\Services\Extension;
use Illuminate\Support\Facades\Log;
class FilterRegistry
{
private array $hooks = [];
private int $insertionCounter = 0;
public function register(string $hook, callable $callback, int $priority = 100): void
{
$this->hooks[$hook][] = [$priority, $this->insertionCounter++, $callback];
}
public function apply(string $hook, mixed $value, mixed ...$context): mixed
{
if (empty($this->hooks[$hook])) {
return $value;
}
$callbacks = $this->hooks[$hook];
usort($callbacks, fn (array $a, array $b) => $a[0] <=> $b[0] ?: $a[1] <=> $b[1]);
foreach ($callbacks as $entry) {
$previousValue = $value;
try {
$value = $entry[2]($value, ...$context);
} catch (\Throwable $e) {
Log::warning('FilterRegistry: callback failed', [
'hook' => $hook,
'index' => $entry[1],
'error' => $e->getMessage(),
]);
$value = $previousValue;
}
}
return $value;
}
}

7283
schneespur/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -12,7 +12,7 @@
"src": "node_modules/leaflet/dist/images/marker-icon.png"
},
"resources/css/app.css": {
"file": "assets/app-DTM5xC6O.css",
"file": "assets/app-Gkl9XGUK.css",
"src": "resources/css/app.css",
"isEntry": true,
"name": "app",

View file

@ -1 +1 @@
if(!self.define){let e,s={};const n=(n,r)=>(n=new URL(n+".js",r).href,s[n]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=s,document.head.appendChild(e)}else e=n,importScripts(n),s()}).then(()=>{let e=s[n];if(!e)throw new Error(`Module ${n} didnt register its module`);return e}));self.define=(r,i)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let o={};const l=e=>n(e,t),c={module:{uri:t},exports:o,require:l};s[t]=Promise.all(r.map(e=>c[e]||l(e))).then(e=>(i(...e),o))}}define(["./workbox-d7f7d914"],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-GNqTWY09.js",revision:null},{url:"assets/app-DTM5xC6O.css",revision:null},{url:"manifest.webmanifest",revision:"d9cbc35793758c64f87f24da203c23b4"}],{}),e.cleanupOutdatedCaches(),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")});
if(!self.define){let e,s={};const n=(n,r)=>(n=new URL(n+".js",r).href,s[n]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=s,document.head.appendChild(e)}else e=n,importScripts(n),s()}).then(()=>{let e=s[n];if(!e)throw new Error(`Module ${n} didnt register its module`);return e}));self.define=(r,i)=>{const t=e||("document"in self?document.currentScript.src:"")||location.href;if(s[t])return;let o={};const l=e=>n(e,t),c={module:{uri:t},exports:o,require:l};s[t]=Promise.all(r.map(e=>c[e]||l(e))).then(e=>(i(...e),o))}}define(["./workbox-d7f7d914"],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-Gkl9XGUK.css",revision:null},{url:"assets/app-GNqTWY09.js",revision:null},{url:"manifest.webmanifest",revision:"d9cbc35793758c64f87f24da203c23b4"}],{}),e.cleanupOutdatedCaches(),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")});