fix: eliminate FOUC, Echo, modal duplicate, icons, test DB isolation
- FIX 1: Remove duplicate modal container (keep @livewire directive, drop <x-modal-container>) - FIX 2: Add Laravel Echo + Pusher to bootstrap.js with Reverb config - FIX 3: Cookie-based theme — exclude nimuli_theme from encryption, write cookie in Alpine store, read server-side in layout - FIX 4: Generate missing PNG favicons (16/32/180/192/512px) via GD - FIX 5: Create scripts/verify-modals.sh (27 checks, all green) - BUG: Docker OS env DB_CONNECTION=mysql was overriding phpunit.xml sqlite config, causing RefreshDatabase to migrate:fresh production MySQL — fixed by forcing DB vars in tests/bootstrap.php alongside existing APP_ENV fix Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
ba67f67d60
commit
6c81dd1d48
|
|
@ -17,6 +17,8 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
$middleware->trustProxies(at: '*');
|
$middleware->trustProxies(at: '*');
|
||||||
|
|
||||||
|
$middleware->encryptCookies(except: ['nimuli_theme']);
|
||||||
|
|
||||||
$middleware->web(append: [
|
$middleware->web(append: [
|
||||||
LocaleFromUser::class,
|
LocaleFromUser::class,
|
||||||
SecurityHeaders::class,
|
SecurityHeaders::class,
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 618 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 569 B |
Binary file not shown.
|
After Width: | Height: | Size: 145 B |
Binary file not shown.
|
After Width: | Height: | Size: 171 B |
|
|
@ -12,6 +12,7 @@ document.addEventListener('alpine:init', () => {
|
||||||
this.current = theme;
|
this.current = theme;
|
||||||
document.documentElement.dataset.theme = theme;
|
document.documentElement.dataset.theme = theme;
|
||||||
localStorage.setItem('nimuli_theme', theme);
|
localStorage.setItem('nimuli_theme', theme);
|
||||||
|
document.cookie = 'nimuli_theme=' + theme + '; path=/; max-age=31536000; SameSite=Lax';
|
||||||
},
|
},
|
||||||
cycle() {
|
cycle() {
|
||||||
const order = ['dark', 'light', 'system'];
|
const order = ['dark', 'light', 'system'];
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,17 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
window.axios = axios;
|
window.axios = axios;
|
||||||
|
|
||||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
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: import.meta.env.VITE_REVERB_PORT ?? 443,
|
||||||
|
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
|
||||||
|
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
|
||||||
|
enabledTransports: ['ws', 'wss'],
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ app()->getLocale() }}" data-theme="{{ auth()->user()?->theme ?? 'dark' }}">
|
<html lang="{{ app()->getLocale() }}" data-theme="{{ request()->cookie('nimuli_theme') ?? auth()->user()?->theme ?? 'dark' }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
try {
|
try {
|
||||||
var saved = localStorage.getItem('nimuli_theme') || '{{ auth()->user()?->theme ?? 'dark' }}';
|
var saved = localStorage.getItem('nimuli_theme') || '{{ request()->cookie('nimuli_theme') ?? auth()->user()?->theme ?? 'dark' }}';
|
||||||
var theme = saved === 'system'
|
var theme = saved === 'system'
|
||||||
? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
|
||||||
: saved;
|
: saved;
|
||||||
|
|
@ -133,9 +133,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<x-modal-container />
|
|
||||||
|
|
||||||
@livewireScripts
|
@livewireScripts
|
||||||
|
@livewire('wire-elements-modal')
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PASS=0; FAIL=0
|
||||||
|
|
||||||
|
ok() { echo " ✓ $*"; PASS=$((PASS+1)); }
|
||||||
|
fail(){ echo " ✗ $*"; FAIL=$((FAIL+1)); }
|
||||||
|
|
||||||
|
APP_URL="${APP_URL:-https://nimuli.com}"
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "═══════════════════════════════════════════"
|
||||||
|
echo " Nimuli — Modal & Layout Verify"
|
||||||
|
echo " $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
echo "═══════════════════════════════════════════"
|
||||||
|
|
||||||
|
# ── 1. PHP / Composer ─────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[ 1 ] Dependencies"
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
docker compose exec -T app php artisan --version &>/dev/null && ok "Laravel reachable" || fail "Laravel not reachable"
|
||||||
|
|
||||||
|
# ── 2. wire-elements-modal present once ───────
|
||||||
|
echo ""
|
||||||
|
echo "[ 2 ] Modal container in layout"
|
||||||
|
LAYOUT="resources/views/layouts/nimuli-app.blade.php"
|
||||||
|
MODAL_COUNT=$(grep -c "wire-elements-modal\|livewire-ui-modal" "$LAYOUT" || true)
|
||||||
|
if [ "$MODAL_COUNT" -eq 1 ]; then
|
||||||
|
ok "Exactly 1 modal container in layout"
|
||||||
|
elif [ "$MODAL_COUNT" -eq 0 ]; then
|
||||||
|
fail "No modal container found in layout"
|
||||||
|
else
|
||||||
|
fail "Duplicate modal containers ($MODAL_COUNT) — remove one"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 3. Echo in bootstrap.js ───────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[ 3 ] Laravel Echo"
|
||||||
|
if grep -q "new Echo" "resources/js/bootstrap.js"; then
|
||||||
|
ok "Echo instantiated in bootstrap.js"
|
||||||
|
else
|
||||||
|
fail "Echo missing from bootstrap.js"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 4. Cookie theme ───────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[ 4 ] Theme cookie"
|
||||||
|
if grep -q "nimuli_theme" "bootstrap/app.php"; then
|
||||||
|
ok "nimuli_theme excluded from cookie encryption"
|
||||||
|
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"
|
||||||
|
else
|
||||||
|
fail "Alpine theme store does not write cookie"
|
||||||
|
fi
|
||||||
|
if grep -q "request()->cookie('nimuli_theme')" "resources/views/layouts/nimuli-app.blade.php"; then
|
||||||
|
ok "Layout reads theme cookie server-side"
|
||||||
|
else
|
||||||
|
fail "Layout does not read theme cookie server-side"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 5. PNG icons ──────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[ 5 ] Manifest icons"
|
||||||
|
for f in favicon-16x16.png favicon-32x32.png apple-touch-icon.png android-chrome-192x192.png android-chrome-512x512.png; do
|
||||||
|
if [ -f "public/$f" ]; then
|
||||||
|
ok "$f present"
|
||||||
|
else
|
||||||
|
fail "$f missing from public/"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── 6. Modal component files ──────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[ 6 ] Modal Livewire components"
|
||||||
|
MODALS=(
|
||||||
|
"app/Livewire/Modals/CreateLink.php"
|
||||||
|
"app/Livewire/Modals/EditLink.php"
|
||||||
|
"app/Livewire/Modals/DeleteLink.php"
|
||||||
|
"app/Livewire/Modals/CreateWorkspace.php"
|
||||||
|
"app/Livewire/Modals/DeleteWorkspace.php"
|
||||||
|
"app/Livewire/Modals/InviteMember.php"
|
||||||
|
"app/Livewire/Modals/EditMemberRole.php"
|
||||||
|
"app/Livewire/Modals/RemoveMember.php"
|
||||||
|
"app/Livewire/Modals/CreateWebhook.php"
|
||||||
|
"app/Livewire/Modals/DeleteWebhook.php"
|
||||||
|
"app/Livewire/Modals/CreateApiToken.php"
|
||||||
|
"app/Livewire/Modals/RevokeApiToken.php"
|
||||||
|
"app/Livewire/Modals/ConfirmAction.php"
|
||||||
|
)
|
||||||
|
for modal in "${MODALS[@]}"; do
|
||||||
|
if [ -f "$modal" ]; then
|
||||||
|
ok "$(basename "$modal" .php)"
|
||||||
|
else
|
||||||
|
fail "MISSING: $modal"
|
||||||
|
fi
|
||||||
|
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"
|
||||||
|
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"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 8. Build compiles ─────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[ 8 ] Vite build"
|
||||||
|
if npm run build 2>&1 | grep -q "built in"; then
|
||||||
|
ok "Vite build clean"
|
||||||
|
else
|
||||||
|
fail "Vite build errors — check output"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 9. Seeders ────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[ 9 ] Dev user seeder"
|
||||||
|
DB_USER=$(sed -n 's/^DB_USERNAME=//p' "$PROJECT_DIR/.env")
|
||||||
|
DB_PASS=$(sed -n 's/^DB_PASSWORD=//p' "$PROJECT_DIR/.env")
|
||||||
|
DB_NAME=$(sed -n 's/^DB_DATABASE=//p' "$PROJECT_DIR/.env")
|
||||||
|
USER_CNT=$(docker compose exec -T mysql mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" \
|
||||||
|
-se "SELECT COUNT(*) FROM users WHERE email='nexxo@nimuli.com';" 2>/dev/null || echo "0")
|
||||||
|
if [ "${USER_CNT:-0}" -gt 0 ]; then
|
||||||
|
ok "nexxo@nimuli.com exists in DB"
|
||||||
|
else
|
||||||
|
fail "nexxo@nimuli.com not found — run: php artisan db:seed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Summary ───────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "═══════════════════════════════════════════"
|
||||||
|
TOTAL=$((PASS + FAIL))
|
||||||
|
echo " Passed: $PASS / $TOTAL"
|
||||||
|
if [ "$FAIL" -gt 0 ]; then
|
||||||
|
echo " Failed: $FAIL"
|
||||||
|
echo "═══════════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo " All checks passed ✓"
|
||||||
|
echo "═══════════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
@ -1,9 +1,17 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
// Force APP_ENV=testing before anything else loads
|
// Force test env vars before anything else loads.
|
||||||
// This overrides Docker container OS env (APP_ENV=local) so CSRF is disabled in tests
|
// Docker OS env overrides phpunit.xml <env> directives unless we set them here first.
|
||||||
putenv('APP_ENV=testing');
|
putenv('APP_ENV=testing');
|
||||||
$_ENV['APP_ENV'] = 'testing';
|
$_ENV['APP_ENV'] = 'testing';
|
||||||
$_SERVER['APP_ENV'] = 'testing';
|
$_SERVER['APP_ENV'] = 'testing';
|
||||||
|
|
||||||
|
putenv('DB_CONNECTION=sqlite');
|
||||||
|
$_ENV['DB_CONNECTION'] = 'sqlite';
|
||||||
|
$_SERVER['DB_CONNECTION'] = 'sqlite';
|
||||||
|
|
||||||
|
putenv('DB_DATABASE=:memory:');
|
||||||
|
$_ENV['DB_DATABASE'] = ':memory:';
|
||||||
|
$_SERVER['DB_DATABASE'] = ':memory:';
|
||||||
|
|
||||||
require __DIR__.'/../vendor/autoload.php';
|
require __DIR__.'/../vendor/autoload.php';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue