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
parent
6c81dd1d48
commit
502a777afd
|
|
@ -48,8 +48,10 @@ server {
|
||||||
proxy_set_header X-Forwarded-Proto https;
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
proxy_read_timeout 86400;
|
proxy_read_timeout 86400s;
|
||||||
proxy_send_timeout 86400;
|
proxy_send_timeout 86400s;
|
||||||
|
proxy_connect_timeout 86400s;
|
||||||
|
proxy_buffering off;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Vite HMR WebSocket
|
# Vite HMR WebSocket
|
||||||
|
|
|
||||||
|
|
@ -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"**
|
||||||
|
|
@ -1,24 +1,2 @@
|
||||||
import '../css/app.css';
|
import '../css/app.css';
|
||||||
import './bootstrap';
|
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 });
|
|
||||||
});
|
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,54 @@ window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||||
|
|
||||||
import Echo from 'laravel-echo';
|
import Echo from 'laravel-echo';
|
||||||
import Pusher from 'pusher-js';
|
import Pusher from 'pusher-js';
|
||||||
window.Pusher = Pusher;
|
|
||||||
|
|
||||||
|
window.Pusher = Pusher;
|
||||||
window.Echo = new Echo({
|
window.Echo = new Echo({
|
||||||
broadcaster: 'reverb',
|
broadcaster: 'reverb',
|
||||||
key: import.meta.env.VITE_REVERB_APP_KEY,
|
key: import.meta.env.VITE_REVERB_APP_KEY,
|
||||||
wsHost: import.meta.env.VITE_REVERB_HOST,
|
wsHost: import.meta.env.VITE_REVERB_HOST,
|
||||||
wsPort: import.meta.env.VITE_REVERB_PORT ?? 443,
|
wsPort: 443,
|
||||||
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
|
wssPort: 443,
|
||||||
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
|
forceTLS: true,
|
||||||
enabledTransports: ['ws', 'wss'],
|
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 });
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -133,8 +133,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@livewireScripts
|
|
||||||
@livewire('wire-elements-modal')
|
@livewire('wire-elements-modal')
|
||||||
|
@livewireScripts
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,10 @@ if grep -q "nimuli_theme" "bootstrap/app.php"; then
|
||||||
else
|
else
|
||||||
fail "nimuli_theme not excluded from EncryptCookies"
|
fail "nimuli_theme not excluded from EncryptCookies"
|
||||||
fi
|
fi
|
||||||
if grep -q "document.cookie.*nimuli_theme" "resources/js/app.js"; then
|
if grep -q "document.cookie.*nimuli_theme" "resources/js/bootstrap.js"; then
|
||||||
ok "Theme cookie written in Alpine store"
|
ok "Theme cookie written in Alpine store (bootstrap.js)"
|
||||||
else
|
else
|
||||||
fail "Alpine theme store does not write cookie"
|
fail "Alpine theme store does not write cookie in bootstrap.js"
|
||||||
fi
|
fi
|
||||||
if grep -q "request()->cookie('nimuli_theme')" "resources/views/layouts/nimuli-app.blade.php"; then
|
if grep -q "request()->cookie('nimuli_theme')" "resources/views/layouts/nimuli-app.blade.php"; then
|
||||||
ok "Layout reads theme cookie server-side"
|
ok "Layout reads theme cookie server-side"
|
||||||
|
|
@ -102,12 +102,26 @@ done
|
||||||
# ── 7. Tests ──────────────────────────────────
|
# ── 7. Tests ──────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "[ 7 ] Test suite"
|
echo "[ 7 ] Test suite"
|
||||||
if docker compose exec -T app php artisan test --filter "LinksModals|WorkspaceModals|DevUserSeeder" 2>&1 | grep -q "passed"; then
|
TEST_OUT=$(docker compose exec -T app php artisan test 2>&1 || true)
|
||||||
ok "Pest tests pass"
|
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
|
else
|
||||||
echo " Running full suite (this may take a moment)..."
|
ok "All tests pass"
|
||||||
docker compose exec -T app php artisan test 2>&1 | tail -5
|
fi
|
||||||
fail "Check output above"
|
|
||||||
|
# ── 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
|
fi
|
||||||
|
|
||||||
# ── 8. Build compiles ─────────────────────────
|
# ── 8. Build compiles ─────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,10 @@ use App\Domains\Link\Models\Link;
|
||||||
use App\Domains\Subscription\Models\Plan;
|
use App\Domains\Subscription\Models\Plan;
|
||||||
use App\Domains\Subscription\Services\PlanLimitsService;
|
use App\Domains\Subscription\Services\PlanLimitsService;
|
||||||
use App\Domains\Workspace\Models\Workspace;
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
uses(TestCase::class);
|
uses(TestCase::class, RefreshDatabase::class);
|
||||||
|
|
||||||
it('allows link creation when under limit', function () {
|
it('allows link creation when under limit', function () {
|
||||||
$plan = Plan::factory()->create(['link_limit' => 10]);
|
$plan = Plan::factory()->create(['link_limit' => 10]);
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@ export default defineConfig({
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
optimizeDeps: {
|
||||||
|
include: [
|
||||||
|
'axios',
|
||||||
|
'laravel-echo',
|
||||||
|
'pusher-js',
|
||||||
|
'chart.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 5173,
|
port: 5173,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue