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 <noreply@anthropic.com>
main
boban 2026-05-16 13:36:40 +02:00
parent 6c81dd1d48
commit 502a777afd
8 changed files with 499 additions and 38 deletions

View File

@ -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

View File

@ -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
<!DOCTYPE html>
<html lang="{{ str_replace('_','-', app()->getLocale()) }}" data-theme="{{ $theme }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title ?? 'Nimuli' }}</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="manifest" href="/site.webmanifest">
<style>
html { background: #0a0a0f; color: #f0f0f5; font-family: 'Inter', system-ui, sans-serif; }
html[data-theme="light"] { background: #ffffff; color: #111827; }
body { margin: 0; opacity: 0; transition: opacity 120ms; min-height: 100vh; }
body.app-ready { opacity: 1; }
</style>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
{{ $slot }}
@livewire('wire-elements-modal')
<script>
function r() { document.body.classList.add('app-ready'); }
if (document.readyState === 'complete') r();
else { window.addEventListener('load', r); setTimeout(r, 500); }
</script>
@livewireScripts
</body>
</html>
```
Theme-Toggle MUSS Cookie + localStorage setzen, sonst flash beim Reload.
### Phase 2 — Modals einbinden
`@livewire('wire-elements-modal')` MUSS im Layout vor `</body>` 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/<Name>.php` — extends `LivewireUI\Modal\ModalComponent`
- `resources/views/livewire/modals/<name>.blade.php`
- Trigger-Button auf Page mit `wire:click="$dispatch('openModal', {component: 'modals.<name>'})"`
- 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"**

View File

@ -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 });
});

View File

@ -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 });
});

View File

@ -133,8 +133,8 @@
</div>
</div>
@livewireScripts
@livewire('wire-elements-modal')
@livewireScripts
<script>
(function () {

View File

@ -51,10 +51,10 @@ if grep -q "nimuli_theme" "bootstrap/app.php"; then
else
fail "nimuli_theme not excluded from EncryptCookies"
fi
if grep -q "document.cookie.*nimuli_theme" "resources/js/app.js"; then
ok "Theme cookie written in Alpine store"
if grep -q "document.cookie.*nimuli_theme" "resources/js/bootstrap.js"; then
ok "Theme cookie written in Alpine store (bootstrap.js)"
else
fail "Alpine theme store does not write cookie"
fail "Alpine theme store does not write cookie in bootstrap.js"
fi
if grep -q "request()->cookie('nimuli_theme')" "resources/views/layouts/nimuli-app.blade.php"; then
ok "Layout reads theme cookie server-side"
@ -102,12 +102,26 @@ done
# ── 7. Tests ──────────────────────────────────
echo ""
echo "[ 7 ] Test suite"
if docker compose exec -T app php artisan test --filter "LinksModals|WorkspaceModals|DevUserSeeder" 2>&1 | grep -q "passed"; then
ok "Pest tests pass"
TEST_OUT=$(docker compose exec -T app php artisan test 2>&1 || true)
echo "$TEST_OUT" | tail -5
if echo "$TEST_OUT" | grep -qE "Tests:.*[1-9][0-9]* failed"; then
fail "Test suite has failures — see output above"
else
echo " Running full suite (this may take a moment)..."
docker compose exec -T app php artisan test 2>&1 | tail -5
fail "Check output above"
ok "All tests pass"
fi
# ── 7b. Chart.js global ───────────────────────
echo ""
echo "[ 7b ] Chart.js global"
if grep -q "window\.Chart = Chart" "resources/js/bootstrap.js"; then
ok "window.Chart registered in bootstrap.js"
else
fail "window.Chart missing from bootstrap.js"
fi
if grep -q "optimizeDeps" "vite.config.js"; then
ok "optimizeDeps configured in vite.config.js"
else
fail "optimizeDeps missing from vite.config.js"
fi
# ── 8. Build compiles ─────────────────────────

View File

@ -4,9 +4,10 @@ use App\Domains\Link\Models\Link;
use App\Domains\Subscription\Models\Plan;
use App\Domains\Subscription\Services\PlanLimitsService;
use App\Domains\Workspace\Models\Workspace;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
uses(TestCase::class);
uses(TestCase::class, RefreshDatabase::class);
it('allows link creation when under limit', function () {
$plan = Plan::factory()->create(['link_limit' => 10]);

View File

@ -16,6 +16,14 @@ export default defineConfig({
],
}),
],
optimizeDeps: {
include: [
'axios',
'laravel-echo',
'pusher-js',
'chart.js',
],
},
server: {
host: '0.0.0.0',
port: 5173,