fix(services): 500 from @disabled on <x-btn>; add R12 browser-verify rule
/services returned HTTP 500: @disabled() on a Blade *component* injects an
if/endif into the component tag and breaks compilation ("unexpected endif").
@disabled/@checked/@required only work on real HTML elements — switched to the
bound :disabled="…" attribute (the component attribute bag drops a false value).
Caught by a real browser probe (HTTP status + console errors per route), which I
should have run before. Documented that as R12 in rules.md + the CLAUDE.md verify
checklist: every touched page must load at HTTP 200 with zero console errors, in
its *loaded* (post-wire:init) state — a green Livewire::test is not sufficient.
Re-probed all six routes: dashboard/servers/detail/services/files/audit all 200,
zero console errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/v1-foundation
parent
5479896bf3
commit
5134fe740b
|
|
@ -227,7 +227,9 @@ docker compose -f docker-compose.prod.yml exec app php artisan migrate --force
|
||||||
- **Work on a feature branch, not `main`.** Commit in sensible steps.
|
- **Work on a feature branch, not `main`.** Commit in sensible steps.
|
||||||
- **Secret hygiene:** `.env*` (incl. `.env.gitea`) + keys are gitignored; never commit/echo the
|
- **Secret hygiene:** `.env*` (incl. `.env.gitea`) + keys are gitignored; never commit/echo the
|
||||||
Gitea token (read it only at push time). See `rules.md` → Appendix.
|
Gitea token (read it only at push time). See `rules.md` → Appendix.
|
||||||
- **Verify before "done":** container builds, page renders, no console errors. Test the 3 breakpoints.
|
- **Verify before "done" (R12):** load **every touched page in a real browser** (headless ok) →
|
||||||
|
**HTTP 200** (no 500) + **zero console/network errors**; for lazy `wire:init` pages check the
|
||||||
|
**loaded** state, not the skeleton. A green `Livewire::test` is **not** enough. Test the 3 breakpoints.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -243,5 +245,7 @@ docker compose -f docker-compose.prod.yml exec app php artisan migrate --force
|
||||||
7. Run all tooling **inside the container** (R8).
|
7. Run all tooling **inside the container** (R8).
|
||||||
8. UI copy **German, no emoji**; status via color/dots/pills (R9).
|
8. UI copy **German, no emoji**; status via color/dots/pills (R9).
|
||||||
9. **Reuse** the token set + Blade component kit (R10).
|
9. **Reuse** the token set + Blade component kit (R10).
|
||||||
10. **Verify** (build, render, no console errors, 3 breakpoints), then `git status` for stray
|
10. **Verify in a browser (R12)**: every touched route loads at **HTTP 200** with **zero console
|
||||||
|
errors** (loaded state of lazy pages, not the skeleton) — a green `Livewire::test` is not enough;
|
||||||
|
build + 3 breakpoints; then `git status` for stray
|
||||||
secrets, commit on the feature branch.
|
secrets, commit on the feature branch.
|
||||||
|
|
|
||||||
|
|
@ -70,9 +70,9 @@
|
||||||
|
|
||||||
{{-- R5: wire to wire-elements/modal --}}
|
{{-- R5: wire to wire-elements/modal --}}
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
<x-btn variant="secondary" wire:click="confirm('start', @js($svc['name']))" wire:loading.attr="disabled" @disabled($svc['status'] === 'online')>Starten</x-btn>
|
<x-btn variant="secondary" wire:click="confirm('start', @js($svc['name']))" wire:loading.attr="disabled" :disabled="$svc['status'] === 'online'">Starten</x-btn>
|
||||||
<x-btn variant="secondary" wire:click="confirm('stop', @js($svc['name']))" wire:loading.attr="disabled" @disabled($svc['status'] === 'offline')>Stopp</x-btn>
|
<x-btn variant="secondary" wire:click="confirm('stop', @js($svc['name']))" wire:loading.attr="disabled" :disabled="$svc['status'] === 'offline'">Stopp</x-btn>
|
||||||
<x-btn variant="accent" wire:click="confirm('restart', @js($svc['name']))" wire:loading.attr="disabled" @disabled($svc['status'] === 'offline')>Neustart</x-btn>
|
<x-btn variant="accent" wire:click="confirm('restart', @js($svc['name']))" wire:loading.attr="disabled" :disabled="$svc['status'] === 'offline'">Neustart</x-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
35
rules.md
35
rules.md
|
|
@ -21,6 +21,7 @@
|
||||||
- [ ] **R9** — UI copy is **German**, **no emoji**; status via color/dots/pills.
|
- [ ] **R9** — UI copy is **German**, **no emoji**; status via color/dots/pills.
|
||||||
- [ ] **R10** — Reused existing `@theme` tokens + Blade components (no ad-hoc colors/widgets).
|
- [ ] **R10** — Reused existing `@theme` tokens + Blade components (no ad-hoc colors/widgets).
|
||||||
- [ ] **R11** — Records exposed in URLs are addressed by **UUID**, never the integer PK.
|
- [ ] **R11** — Records exposed in URLs are addressed by **UUID**, never the integer PK.
|
||||||
|
- [ ] **R12** — every touched page loads in a **browser** at **HTTP 200** with **zero console errors** (the **loaded** state of `wire:init` pages, not just the skeleton).
|
||||||
- [ ] **Secrets** — `git status` shows **no** `.env*`, token, key, or home-dir dotfile staged.
|
- [ ] **Secrets** — `git status` shows **no** `.env*`, token, key, or home-dir dotfile staged.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -336,6 +337,40 @@ route('servers.show', $server->id); // exposes the auto-increment id
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## R12 — Browser-verified: HTTP 200, zero console errors
|
||||||
|
|
||||||
|
**Why.** A passing `Livewire::test(...)->assertOk()` only renders ONE state in PHP — it does not
|
||||||
|
catch client-side JS errors, and for a `wire:init`/lazy page it exercises the **skeleton**, not the
|
||||||
|
loaded view. Render-time Blade/PHP errors (e.g. `@disabled` on a component → "unexpected endif", a
|
||||||
|
500) only surface on the real request. A green unit test is **not** "done".
|
||||||
|
|
||||||
|
**Rule.** Before declaring any UI change done, load **every affected page in a real browser**
|
||||||
|
(headless is fine) and confirm, per page: **HTTP 200** (no 500), **zero console errors**, **zero
|
||||||
|
failed requests**. For lazy pages, verify the **loaded** state (after `wire:init`), not the skeleton.
|
||||||
|
Never use `@disabled`/`@checked`/`@required` on a Blade **component** — use the bound `:disabled="…"`
|
||||||
|
attribute (the component attribute bag drops a `false` value correctly).
|
||||||
|
|
||||||
|
✅ **Correct** — headless probe over the routes (captures console + HTTP status):
|
||||||
|
```bash
|
||||||
|
# mount the script into the image's home dir or require('puppeteer') fails
|
||||||
|
docker run --rm --network host -v /tmp/probe.js:/home/pptruser/probe.js -w /home/pptruser \
|
||||||
|
ghcr.io/puppeteer/puppeteer:latest node probe.js
|
||||||
|
```
|
||||||
|
```js
|
||||||
|
page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); });
|
||||||
|
const r = await page.goto(url, { waitUntil: 'networkidle2' }); // expect r.status() === 200
|
||||||
|
await new Promise(s => setTimeout(s, 4000)); // let wire:init load, then assert errors == 0
|
||||||
|
```
|
||||||
|
|
||||||
|
❌ **Forbidden**
|
||||||
|
```text
|
||||||
|
"Livewire::test rendered OK" → shipping without ever loading the page in a browser.
|
||||||
|
Testing only the skeleton state of a wire:init page and assuming the loaded state works.
|
||||||
|
@disabled($cond) on <x-btn …> → injects if/endif into the component tag → 500.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Appendix — Secret hygiene
|
## Appendix — Secret hygiene
|
||||||
|
|
||||||
The project lives in `/home/nexxo/clusev` (its own git repo). The Gitea push token lives in
|
The project lives in `/home/nexxo/clusev` (its own git repo). The Gitea push token lives in
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue