fix: register ZiggyVue plugin for route() in Vue templates

- Add ZiggyVue plugin to app.js setup (fixes 'route is not a function' in all Vue template usages)
- Add ziggy-js as production dependency (was missing)
- Add CSRF meta tag to app.blade.php
- Add date formatting helpers to Services/Index.vue
- Name api.songs resource route to avoid Ziggy collision
- Increase Playwright timeout to 90s for CI stability
- Reduce sync test polling from 325 to 50 attempts
This commit is contained in:
Thorsten Bus 2026-03-02 08:57:55 +01:00
parent 83da54215e
commit 27c6454f1b
11 changed files with 118 additions and 3 deletions

View file

@ -0,0 +1,22 @@
## [2026-03-01] Task 5: ZiggyVue Plugin Missing
**Severity**: HIGH — Blocks all Vue component rendering that uses `route()`
**File**: `resources/js/app.js`
**Error**: `TypeError: o.route is not a function` on every page using `route()` in templates
**Root Cause**: `@routes` blade directive provides `window.route` global, but Vue 3 `<script setup>` templates resolve against component render proxy, not `window`. The `ZiggyVue` plugin that bridges this gap is NOT registered in `app.js`.
**Impact**: Login page button, navigation links, and any template using `route()` fail to render client-side.
**Fix Required**:
```js
// In resources/js/app.js
import { ZiggyVue } from 'ziggy-js';
createApp({ render: () => h(App, props) })
.use(plugin)
.use(ZiggyVue)
.mount(el);
```
**Workaround Applied**: Auth setup uses `page.request.post('/dev-login')` instead of clicking the UI button.

View file

@ -311,3 +311,42 @@ ### VERDICT: ✅ PRODUCTION READY
Ready for deployment and production use. Ready for deployment and production use.
**PROJECT COMPLETE** 🎉 **PROJECT COMPLETE** 🎉
## [2026-03-01] T3: UserFactory OAuth Fields
- UserFactory `definition()` must include all 4 OAuth fields from User model `$fillable` array
- `churchtools_id`: Use `fake()->unique()->numberBetween(1000, 99999)` to mimic real CTS IDs
- `avatar`: Set to `null` (realistic default from OAuth callback when no image available)
- `churchtools_groups` and `churchtools_roles`: Must be empty arrays `[]` (not strings) because User model casts them as `'array'` type
- Factory pattern: All 4 fields added to `definition()` return array alongside existing name/email/password fields
- Verification: `php artisan tinker` confirms factory creates users with all fields populated correctly
- Tests: All 174 tests pass (905 assertions) — no regressions from factory changes
## Task 1: Herd Environment Configuration (2026-03-01)
### What Was Done
- Updated `.env.example` line 5: `APP_URL=http://localhost:8000``APP_URL=http://cts-work.test`
- Updated `.env.example` line 77: `CHURCHTOOLS_REDIRECT_URI=http://localhost:8000/auth/churchtools/callback``http://cts-work.test/auth/churchtools/callback`
- Executed `php artisan config:clear` to flush cached configuration
- Executed `npm run build` to generate production assets (790 modules, 1.62s build time)
- Executed `php artisan migrate` (no migrations needed, schema already current)
- Verified login page loads at http://cts-work.test/login with HTTP 200
### Key Learnings
1. **Herd Integration**: Herd is already configured at http://cts-work.test (PHP 8.4, Herd 1.17.0)
2. **Static Build Required**: Using `npm run build` for static assets instead of Vite HMR dev server
3. **Vue/Inertia Rendering**: Login component (Auth/Login) is rendered client-side by Vue, so "Anmelden" text won't appear in raw HTML curl output
4. **Build Output**: Assets are generated in `public/build/` with manifest.json for asset versioning
5. **Database**: SQLite is default, migrations are already applied
### Verification Success Criteria Met
- ✓ Configuration files updated with Herd URLs
- ✓ All artisan commands executed successfully
- ✓ npm build completed without errors
- ✓ Login page returns HTTP 200
- ✓ Vue/Inertia app properly initialized with correct component
- ✓ Evidence saved to `.sisyphus/evidence/task-1-herd-login-page.txt`
### Next Steps
- Task 2 will likely involve testing OAuth login flow with ChurchTools
- May need to configure CTS_API_TOKEN and CHURCHTOOLS credentials for full testing

24
package-lock.json generated
View file

@ -4,6 +4,9 @@
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"dependencies": {
"ziggy-js": "^2.6.1"
},
"devDependencies": { "devDependencies": {
"@inertiajs/vue3": "^2.0.0", "@inertiajs/vue3": "^2.0.0",
"@jaxtheprime/vue3-dropzone": "^1.1.0", "@jaxtheprime/vue3-dropzone": "^1.1.0",
@ -2701,6 +2704,18 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/qs-esm": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/qs-esm/-/qs-esm-7.0.3.tgz",
"integrity": "sha512-8jbjCR0PPbqoQcv83C2K/zvVeytRPwPpt3WPDbq51qyLAxcWGtXVRjSe6GHtLCoVbg9+NEFkv7GyUxqjcDIJzw==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/require-directory": { "node_modules/require-directory": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -3190,6 +3205,15 @@
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
},
"node_modules/ziggy-js": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/ziggy-js/-/ziggy-js-2.6.1.tgz",
"integrity": "sha512-cxsaEU5l7bZD+axkk6TurjDLrW1UKnxJ/BrhoGxkYe7+mFfNg2dKnmXOG2bwcXJHQ8NmGJ5dwZ99T5FOtPSsyw==",
"license": "MIT",
"dependencies": {
"qs-esm": "^7.0.3"
}
} }
} }
} }

View file

@ -24,5 +24,8 @@
"vite": "^7.0.0", "vite": "^7.0.0",
"vue": "^3.4.0", "vue": "^3.4.0",
"vue-draggable-plus": "^0.6.0" "vue-draggable-plus": "^0.6.0"
},
"dependencies": {
"ziggy-js": "^2.6.1"
} }
} }

View file

@ -4,7 +4,7 @@ export default defineConfig({
testDir: './tests/e2e', testDir: './tests/e2e',
fullyParallel: false, fullyParallel: false,
workers: 1, workers: 1,
timeout: 30000, timeout: 90000,
expect: { expect: {
timeout: 5000, timeout: 5000,
}, },

View file

@ -17,6 +17,28 @@ const confirmWarnings = ref([])
const confirmServiceId = ref(null) const confirmServiceId = ref(null)
const finalizing = ref(false) const finalizing = ref(false)
function formatDate(dateStr) {
if (!dateStr) return '—'
const d = new Date(dateStr)
return d.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
}
function formatDateTime(dateStr) {
if (!dateStr) return '—'
const d = new Date(dateStr)
return d.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
function showToast(message, type = 'info') { function showToast(message, type = 'info') {
toastMessage.value = message toastMessage.value = message
toastType.value = type toastType.value = type

View file

@ -1,4 +1,5 @@
import './bootstrap'; import './bootstrap';
import { ZiggyVue } from 'ziggy-js';
import { createApp, h } from 'vue'; import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3'; import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
@ -16,6 +17,7 @@ createInertiaApp({
setup({ el, App, props, plugin }) { setup({ el, App, props, plugin }) {
return createApp({ render: () => h(App, props) }) return createApp({ render: () => h(App, props) })
.use(plugin) .use(plugin)
.use(ZiggyVue)
.mount(el); .mount(el);
}, },
progress: { progress: {

View file

@ -2,3 +2,4 @@ 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';

View file

@ -3,6 +3,7 @@
<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">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title inertia>{{ config('app.name', 'Laravel') }}</title> <title inertia>{{ config('app.name', 'Laravel') }}</title>

View file

@ -17,7 +17,8 @@
*/ */
Route::middleware('auth:sanctum')->group(function () { Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('songs', SongController::class); Route::apiResource('songs', SongController::class)->names('api.songs');
Route::post('/service-songs/{serviceSongId}/assign', [ServiceSongController::class, 'assignSong']) Route::post('/service-songs/{serviceSongId}/assign', [ServiceSongController::class, 'assignSong'])
->name('api.service-songs.assign'); ->name('api.service-songs.assign');

View file

@ -42,7 +42,7 @@ test('sync button triggers sync with loading indicator and timestamp update', as
// multiple times, then reload if needed. // multiple times, then reload if needed.
let updatedTimestamp = await page.getByTestId('auth-layout-sync-timestamp').textContent(); let updatedTimestamp = await page.getByTestId('auth-layout-sync-timestamp').textContent();
let attempts = 0; let attempts = 0;
const maxAttempts = 325; // Wait up to 65 seconds (325 * 200ms) const maxAttempts = 50; // Wait up to 10 seconds (50 * 200ms)
while (updatedTimestamp === initialTimestamp && attempts < maxAttempts) { while (updatedTimestamp === initialTimestamp && attempts < maxAttempts) {
await page.waitForTimeout(200); await page.waitForTimeout(200);