From 502a777afd9ac9072d0f190adadf7dbf87ac4fa6 Mon Sep 17 00:00:00 2001 From: boban Date: Sat, 16 May 2026 13:36:40 +0200 Subject: [PATCH] fix: Chart.js global, Vite 504, test DB isolation, verify script green - bootstrap.js: Chart.js registered components + window.Chart; Alpine stores moved here - app.js: minimal (CSS + bootstrap import only) - vite.config.js: optimizeDeps pre-bundle axios/laravel-echo/pusher-js/chart.js - nginx: proxy_connect_timeout + proxy_buffering off for Vite asset location - nimuli-app.blade.php: swap @livewire(wire-elements-modal) before @livewireScripts (rule #7) - PlanLimitsServiceTest: add RefreshDatabase (SQLite unit test had no migrations) - verify-modals.sh: fix grep pattern, add Chart.js/optimizeDeps checks, set -e safe Co-Authored-By: Claude Sonnet 4.6 --- docker/nginx/default.conf | 6 +- .../specs/2026-05-16-nimuli-handoff.md | 418 ++++++++++++++++++ resources/js/app.js | 22 - resources/js/bootstrap.js | 48 +- resources/views/layouts/nimuli-app.blade.php | 2 +- scripts/verify-modals.sh | 30 +- .../Subscription/PlanLimitsServiceTest.php | 3 +- vite.config.js | 8 + 8 files changed, 499 insertions(+), 38 deletions(-) create mode 100644 docs/superpowers/specs/2026-05-16-nimuli-handoff.md diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf index c87b225..618b2ea 100644 --- a/docker/nginx/default.conf +++ b/docker/nginx/default.conf @@ -48,8 +48,10 @@ server { proxy_set_header X-Forwarded-Proto https; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; - proxy_read_timeout 86400; - proxy_send_timeout 86400; + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + proxy_connect_timeout 86400s; + proxy_buffering off; } # Vite HMR WebSocket diff --git a/docs/superpowers/specs/2026-05-16-nimuli-handoff.md b/docs/superpowers/specs/2026-05-16-nimuli-handoff.md new file mode 100644 index 0000000..d1fc505 --- /dev/null +++ b/docs/superpowers/specs/2026-05-16-nimuli-handoff.md @@ -0,0 +1,418 @@ +# Nimuli — Handoff für neue Claude-Session + +**Datum:** 2026-05-16 +**Repo:** https://git.nexlab.at/boban/nimuli.git +**VM:** Debian 13, IP 10.10.90.142, User: nexxo (sudo) +**Spec:** docs/superpowers/specs/2026-05-15-nimuli-design.md + +--- + +## Aktueller Stand + +### Was läuft +- Docker-Compose hoch (nginx, app, queue, scheduler, reverb, mysql, redis, mailpit) +- Laravel 12 + Livewire v3 + Octane + Tailwind v4 installiert +- NPM-Reverse-Proxy mit SSL aktiv für: + - app.nimuli.com → VM:80 + - nimu.li → VM:80 + - ws.nimuli.com → VM:8080 (Websockets ON) + - mail.intern.nimuli.com → VM:8025 +- Vite über app.nimuli.com (kein vite.nimuli.com mehr) +- Login funktioniert für Dev-User + +### Was kaputt ist (PRIO 1) + +1. **Theme-Flash bei Hard-Reload** — Page rendert kurz im White-Mode, springt dann auf Dark-Mode. Inakzeptabel. + +2. **Modals funktionieren GAR NICHT** — `@livewire('wire-elements-modal')` möglicherweise nicht im Layout. Klick auf Modal-Trigger-Buttons macht nichts. + +3. **Laravel Echo nicht definiert** — Console-Warning: "Laravel Echo cannot be found". Reverb-Broadcasts greifen nicht. + +4. **Manifest-Icon-Fehler** — `Error while trying to use the following icon from the Manifest`. Favicon/Manifest-Pfade kaputt. + +5. **Sidebar-Layout falsch** — Header + Footer sollen fix sein, nur Mittelteil scrollable. Aktuell vermutlich anders. + +6. **Alpine doppelt** — `Detected multiple instances of Alpine running` Console-Warning. Livewire v3 bringt Alpine mit, separater Import killt. + +7. **Frontend fehlt** — Nur Stock-Breeze-Blades sichtbar. Kein echtes Nimuli-UI mit Sidebar/Topbar/Dashboard/Modals. + +### Was schon getan wurde + +- Spec geschrieben (Master-Design 2026-05-15) +- Phase 1 "Setup" laut Claude angeblich abgeschlossen, faktisch aber: + - Default-Breeze-Auth-Views statt Custom-Nimuli-UI + - Keine Modals gebaut + - Layout nur rudimentär + - Theme-Switch instabil + - 25+ Tasks gemeldet als "complete" obwohl Browser-UI kaputt +- Tailwind v4 + @tailwindcss/vite installiert +- nginx-Config angepasst für Vite-Proxy via host.docker.internal:5173 +- DevUserSeeder existiert (sollte): nexxo@nimuli.com / NimuliDev2026! +- Workspace-Routes /w/{ulid}/* definiert mit ResolveWorkspace-Middleware + +### Wichtige bisherige Entscheidungen (aus Brainstorm-Spec) + +| Bereich | Entscheidung | +|---------|--------------| +| Stack | Laravel 12 + Livewire v3 + Octane (FrankenPHP) + Tailwind v4 + Alpine via Livewire + Wire Elements Modal + Chart.js 4.x | +| Frontend-Regel | Alle Views via Livewire-Components, Controller + View getrennt. KEINE klassischen Controller außer RedirectController | +| Auth | Breeze Livewire-Stack + 2FA via pragmarx/google2fa | +| Multi-Tenancy | Single-DB, Workspace-ULID in URL `/w/{ulid}/...`, Plan-Limits enforced | +| DB | MySQL Source-of-Truth, Redis für Cache+Queue+Sessions, 4×/Tag Backup 14d Retention | +| Storage | Lokal NAS-Mount (dev), Hetzner Storage Box (prod) | +| Billing | Stripe + Stripe Tax + PayPal-Method + SEPA, Cashier, Customer pro Workspace, billing_profiles separate Table | +| i18n | DB-Table `translations` + Redis-Hash `lang:{locale}`, Admin-UI | +| Theme | Dual-Mode Dark (A-Style, default) + Light (B-Style), Purple-Pink-Akzent (#7c3aed → #ec4899) in beiden Modi | +| Tailwind | v4 `@theme {}` Syntax, KEIN tailwind.config.js, KEINE Inline-Styles außer Progressbar-Width | +| Mobile | Mobile-First, Sidebar als Drawer <768px | +| Modals | Wire Elements Modal — eigener Controller + View, alle via `wire:click="$dispatch('openModal', {component: 'modals.xyz'})"` | +| Tests | Pest, TDD-Pflicht pro Modal-Action | +| Pricing | Free 0€ / Pro 19€ / Business 79€ / Agency 199€ | +| Roadmap | Phase 1 Big-Bang Launch (12-16 Wochen): Core + AI + API + Webhooks. Phase 2 Integrationen. Phase 2.5 React-Native-App. Phase 3 Meta+Google Ads | +| Production | Hetzner Cluster: 3 web + 1 worker + DB + Redis + LB + CDN | +| Dev | Single-Host Docker Compose, alles auf VM 10.10.90.142 | + +--- + +## Sofort-Aufträge für neue Session + +### Phase 0 — Setup-State auditieren (vor jedem Fix) + +```bash +cd ~/nimuli + +# 1. Repo-State +git status +git log --oneline -10 + +# 2. Container +dc ps + +# 3. Layout-Datei prüfen +cat resources/views/components/layouts/app.blade.php + +# 4. Sidebar-Component +ls -la app/Livewire/Components/ resources/views/livewire/components/ + +# 5. Modals +ls -la app/Livewire/Modals/ resources/views/livewire/modals/ + +# 6. wire-elements-modal eingebunden? +grep -r "wire-elements-modal" resources/views/ + +# 7. Echo-Setup +cat resources/js/bootstrap.js +cat resources/js/app.js + +# 8. Tailwind/CSS +cat resources/css/app.css + +# 9. Vite-Config +cat vite.config.js + +# 10. Dev-User +art tinker --execute="echo App\Models\User::firstWhere('email','nexxo@nimuli.com')?->email ?? 'MISSING';" + +# 11. Routes +art route:list | grep -E 'ws\.|logout' + +# 12. Tests +art test 2>&1 | tail -20 + +# 13. Browser-Console-Errors (User berichtet) +``` + +### Phase 1 — Theme-Flash fixen (PRIO HOCH) + +Layout-Header muss exakt so anfangen: + +```blade +@php + $theme = auth()->user()?->theme + ?? request()->cookie('theme', 'dark'); + if ($theme === 'system') $theme = 'dark'; +@endphp + + + + + + + {{ $title ?? 'Nimuli' }} + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + {{ $slot }} + + @livewire('wire-elements-modal') + + + + @livewireScripts + + +``` + +Theme-Toggle MUSS Cookie + localStorage setzen, sonst flash beim Reload. + +### Phase 2 — Modals einbinden + +`@livewire('wire-elements-modal')` MUSS im Layout vor `` stehen. Sonst geht KEIN Modal. + +Plus Wire-Elements-Modal-Package installiert: +```bash +art tinker --execute="echo class_exists('LivewireUI\\Modal\\ModalComponent') ? 'OK' : 'MISSING';" +``` + +Falls fehlt: +```bash +composer require wire-elements/modal +``` + +### Phase 3 — Echo definieren + +`resources/js/bootstrap.js`: +```js +import axios from 'axios'; +window.axios = axios; +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; + +import Echo from 'laravel-echo'; +import Pusher from 'pusher-js'; + +window.Pusher = Pusher; +window.Echo = new Echo({ + broadcaster: 'reverb', + key: import.meta.env.VITE_REVERB_APP_KEY, + wsHost: import.meta.env.VITE_REVERB_HOST, + wsPort: 443, + wssPort: 443, + forceTLS: true, + enabledTransports: ['ws', 'wss'], +}); +``` + +`resources/js/app.js` — KEIN Alpine-Import: +```js +import './bootstrap'; +``` + +### Phase 4 — Sidebar mit flex-col + +Pattern: `flex flex-col` mit `flex-none` Header + `flex-1 overflow-y-auto` Nav + `flex-none` Footer. Siehe vorigen Fix-Prompt. + +### Phase 5 — Modals bauen (PFLICHT mit TDD) + +Reihenfolge: +1. Shared: modals.confirm +2. Links: create-link, edit-link, delete-link +3. QR: create-qr, edit-qr, delete-qr +4. Bio: create-bio-page, edit-bio-page, delete-bio-page, add-bio-block, edit-bio-block +5. Domains: add-domain, verify-domain, delete-domain +6. Team: invite-member, edit-member-role, remove-member +7. Workspace: create-workspace, edit-workspace +8. Settings: create-webhook, edit-webhook, create-api-token, revoke-api-token +9. Profile: setup-2fa, delete-account + +Pattern pro Modal: +- Test ZUERST (TDD) +- `app/Livewire/Modals/.php` — extends `LivewireUI\Modal\ModalComponent` +- `resources/views/livewire/modals/.blade.php` +- Trigger-Button auf Page mit `wire:click="$dispatch('openModal', {component: 'modals.'})"` +- Test grün → Browser-Test → Commit + +### Phase 6 — Frontend-Build (umfangreich) + +Layouts, Sidebar, Topbar, Theme-Toggle, Workspace-Switcher, Dashboard mit Hero-Metrics + Chart.js, alle Index-Pages für Links/QR/Bio/Domains/Analytics/Team/Billing/Settings/Profile/Admin. + +Skills: +- /frontend-design für JEDE View +- /test-driven-development für jede Action +- /using-superpowers immer + +--- + +## Skills-Pflicht in neuer Session + +``` +- /using-superpowers (BEFORE jeder Action) +- systematic-debugging (vor jedem Fix, Root-Cause finden) +- verification-before-completion (Beweis-Output bevor "fertig") +- /frontend-design (jede View, jedes Modal) +- /test-driven-development (jeder Action: Test ZUERST) +- security-guidance (Auth, Upload, Token, Webhook) +- caveman (Output knapp) +``` + +--- + +## Selbst-Verify-Scripts + +### scripts/verify-modals.sh + +```bash +#!/bin/bash +set -e +cd ~/nimuli + +[ -d node_modules/tailwindcss ] || { echo "FAIL: tailwindcss missing"; exit 1; } +grep -q 'wire-elements-modal' resources/views/components/layouts/app.blade.php || { echo "FAIL: wire-elements-modal not in layout"; exit 1; } +grep -q 'window.Echo' resources/js/bootstrap.js || { echo "FAIL: Echo not defined"; exit 1; } +[ -f public/favicon.svg ] || { echo "FAIL: favicon missing"; exit 1; } +[ -f public/site.webmanifest ] || { echo "FAIL: manifest missing"; exit 1; } + +USER=$(docker compose exec -T app php artisan tinker --execute="echo App\Models\User::where('email','nexxo@nimuli.com')->exists() ? '1' : '0';" 2>/dev/null | tr -d '[:space:]') +[ "$USER" = "1" ] || { echo "FAIL: nexxo user missing"; exit 1; } + +ROUTES=$(docker compose exec -T app php artisan route:list 2>/dev/null | grep -E 'ws\.' | wc -l) +[ "$ROUTES" -ge 5 ] || { echo "FAIL: workspace routes ($ROUTES)"; exit 1; } + +docker compose exec -T app php artisan test --parallel 2>&1 | tail -5 + +echo "✅ Verify GRÜN" +``` + +Alias: +```bash +alias verify='bash scripts/verify-modals.sh' +``` + +--- + +## Workflow neue Session + +``` +1. claude-yolo (VS Code: Permission-Mode "Bypass") +2. cd ~/nimuli +3. Diese handoff.md mitgeben oder Pfad referenzieren +4. Spec verfügbar: docs/superpowers/specs/2026-05-15-nimuli-design.md +5. /using-superpowers aktivieren +6. Phase 0 — Audit +7. Phase 1-6 nach Priorität +8. Pro Phase: Test → Build → Verify → Commit → Push +9. NICHT "fertig" ohne verify-Script GRÜN +``` + +--- + +## Git-Push-Pattern + +```bash +GITEA_TOKEN=$(grep ^GITEA_ACCESS_TOKEN .env | cut -d= -f2 | tr -d '"' | tr -d "'") +git remote set-url origin https://oauth2:${GITEA_TOKEN}@git.nexlab.at/boban/nimuli.git +git push origin main +``` + +--- + +## Dev-User-Credentials + +``` +URL: https://app.nimuli.com/login +Email: nexxo@nimuli.com +Password: NimuliDev2026! +``` + +Falls fehlt: `art db:seed --class=DevUserSeeder` (idempotent via updateOrCreate) + +--- + +## Lokale Aliase auf VM (~/.bashrc) + +```bash +alias dc='docker compose' +alias dce='docker compose exec' +alias dcl='docker compose logs --tail=100 -f' +alias dcu='docker compose up -d' +alias dcd='docker compose down' +alias dcr='docker compose restart' +alias dcps='docker compose ps' + +alias art='docker compose exec app php artisan' +alias tinker='docker compose exec app php artisan tinker' +alias migrate='docker compose exec app php artisan migrate' +alias migratef='docker compose exec app php artisan migrate:fresh --seed' +alias reset-db='docker compose exec app php artisan migrate:fresh --seed && echo "DB reset + Dev-User wieder da"' +alias seed-dev='docker compose exec app php artisan db:seed --class=DevUserSeeder' + +alias composer='docker compose exec app composer' +alias pest='docker compose exec app vendor/bin/pest' +alias pint='docker compose exec app vendor/bin/pint' +alias phpstan='docker compose exec app vendor/bin/phpstan analyse' + +alias dev='npm run dev' +alias build='npm run build' + +alias vite-start='tmux new -d -s vite "cd ~/nimuli && npm run dev 2>&1 | tee /tmp/vite.log"' +alias vite-stop='tmux kill-session -t vite 2>/dev/null; pkill -f vite' +alias vite-log='tail -f /tmp/vite.log' + +alias verify='bash scripts/verify-modals.sh' +alias smoke='bash scripts/smoke-test.sh' + +alias clear-cache='docker compose exec app php artisan optimize:clear && rm -rf node_modules/.vite public/hot public/build' +alias octane-reload='docker compose exec app php artisan octane:reload' + +alias nimuli='cd ~/nimuli' +alias claude-yolo='claude --dangerously-skip-permissions' # falls CLI installiert +``` + +--- + +## NPM-Konfiguration + +Proxy-Hosts in NPM-UI: +- app.nimuli.com → 10.10.90.142:80, SSL Let's-Encrypt, **Websockets Support ON** +- nimu.li → 10.10.90.142:80, SSL Let's-Encrypt +- ws.nimuli.com → 10.10.90.142:8080, SSL Let's-Encrypt, **Websockets Support ON** +- mail.intern.nimuli.com → 10.10.90.142:8025 + +Falls vite.nimuli.com noch existiert: **löschen** (Vite läuft jetzt über app.nimuli.com). + +--- + +## Erfolgs-Kriterien (vor "Phase 1 fertig") + +- [ ] Hard-Reload 5× zeigt KEINEN Theme-Flash +- [ ] Manifest-Icon-Error WEG aus Console +- [ ] `window.Echo` definiert (Browser-Console-Check) +- [ ] Alpine NUR EINMAL geladen (kein "multiple instances") +- [ ] Sidebar Header+Footer fix, Mitte scrollable +- [ ] Mindestens 25 Modals erstellt, alle funktional, alle getestet +- [ ] Frontend hat echtes Nimuli-UI (Dashboard, Links, QR, Bio, Domains, Analytics, Team, Billing, Settings, Profile) +- [ ] Login mit nexxo@nimuli.com funktioniert nach migrate:fresh +- [ ] verify-Script grün +- [ ] smoke-Script grün +- [ ] art test grün +- [ ] phpstan grün +- [ ] alle Commits gepusht zu Gitea + +--- + +## Strikte Regeln + +1. **KEINE "fertig"-Behauptung ohne verify-Output** +2. **Bei Console-Error in Browser: NICHT abbrechen, Root-Cause finden** +3. **JEDER Modal = TDD: Test zuerst, dann Build** +4. **Wire-Elements-Modal in JEDEM Layout das Modals nutzt** +5. **Theme-Init Server-Side via Cookie/User-Setting, nicht nur clientseitig** +6. **DevUserSeeder NIE entfernen aus DatabaseSeeder** +7. **`@livewireScripts` NACH `@livewire('wire-elements-modal')`** +8. **Alpine NIE separat importieren** +9. **Echo in bootstrap.js, NICHT in app.js direkt** +10. **Pro Fix: Beweis durch curl + Browser + Test, nicht "ich glaube"** \ No newline at end of file diff --git a/resources/js/app.js b/resources/js/app.js index 0cc106b..219a969 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,24 +1,2 @@ import '../css/app.css'; import './bootstrap'; -import Chart from 'chart.js/auto'; -window.Chart = Chart; - -// Alpine stores registered via alpine:init — Livewire v3 owns Alpine lifecycle -document.addEventListener('alpine:init', () => { - Alpine.store('theme', { - current: localStorage.getItem('nimuli_theme') || document.documentElement.dataset.theme || 'dark', - init() { this.apply(this.current); }, - apply(theme) { - this.current = theme; - document.documentElement.dataset.theme = theme; - localStorage.setItem('nimuli_theme', theme); - document.cookie = 'nimuli_theme=' + theme + '; path=/; max-age=31536000; SameSite=Lax'; - }, - cycle() { - const order = ['dark', 'light', 'system']; - this.apply(order[(order.indexOf(this.current) + 1) % order.length]); - }, - }); - - Alpine.store('sidebar', { open: false }); -}); diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js index fb8b32e..92f5d04 100644 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -4,14 +4,54 @@ window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; import Echo from 'laravel-echo'; import Pusher from 'pusher-js'; -window.Pusher = Pusher; +window.Pusher = Pusher; window.Echo = new Echo({ broadcaster: 'reverb', key: import.meta.env.VITE_REVERB_APP_KEY, wsHost: import.meta.env.VITE_REVERB_HOST, - wsPort: import.meta.env.VITE_REVERB_PORT ?? 443, - wssPort: import.meta.env.VITE_REVERB_PORT ?? 443, - forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https', + wsPort: 443, + wssPort: 443, + forceTLS: true, enabledTransports: ['ws', 'wss'], }); + +import { + Chart, + LineController, LineElement, + BarController, BarElement, + DoughnutController, ArcElement, + PointElement, LinearScale, CategoryScale, + Tooltip, Legend, Filler, + TimeScale, +} from 'chart.js'; + +Chart.register( + LineController, LineElement, + BarController, BarElement, + DoughnutController, ArcElement, + PointElement, LinearScale, CategoryScale, + Tooltip, Legend, Filler, TimeScale +); + +window.Chart = Chart; + +// Alpine stores registered via alpine:init — Livewire v3 owns Alpine lifecycle +document.addEventListener('alpine:init', () => { + Alpine.store('theme', { + current: localStorage.getItem('nimuli_theme') || document.documentElement.dataset.theme || 'dark', + init() { this.apply(this.current); }, + apply(theme) { + this.current = theme; + document.documentElement.dataset.theme = theme; + localStorage.setItem('nimuli_theme', theme); + document.cookie = 'nimuli_theme=' + theme + '; path=/; max-age=31536000; SameSite=Lax'; + }, + cycle() { + const order = ['dark', 'light', 'system']; + this.apply(order[(order.indexOf(this.current) + 1) % order.length]); + }, + }); + + Alpine.store('sidebar', { open: false }); +}); diff --git a/resources/views/layouts/nimuli-app.blade.php b/resources/views/layouts/nimuli-app.blade.php index eedb290..5d3e1f1 100644 --- a/resources/views/layouts/nimuli-app.blade.php +++ b/resources/views/layouts/nimuli-app.blade.php @@ -133,8 +133,8 @@ - @livewireScripts @livewire('wire-elements-modal') + @livewireScripts