Nuxt 4 SPA migration: Laravel becomes API-only - #898
Open
edwh wants to merge 365 commits into
Open
Conversation
SKIP_NPM_INSTALL leaves no Vite manifest, so the Blade homepage the shared readiness check curled 500s (ViteManifestNotFoundException) — the same trap class the Phase F audit flagged for the cutover. The wait-for-core command gains a probe parameter; the build job points it at /api/v2/session.
The npm-less build job exposed two couplings: 13 legacy page-render tests throw ViteManifestNotFoundException without a built manifest (they assert content, not asset tags — withoutVite() in TestCase::setUp removes the dependency; these tests die at Phase F anyway), and phpunit starting ~12 minutes earlier hit a cold Discourse whose 5s-timeout retries made every integration-adjacent test crawl (82 tests in 45 min). The build job now waits for the Discourse web app non-fatally before phpunit. Local phpunit verification deferred until the e2e agent releases the shared DB; CI verifies in isolation meanwhile.
notifications.js moves to global/js/misc (canonical, survives cutover) with a temporary annotated copy at the old path — vite's define global->window shim (for select2) mangles any import path containing the token 'global' from resources/js, and that tree dies at F anyway. box-shadows moves into global/css/helpers; _cards.scss gets a local copy of the legacy fixometer variables instead of reaching into resources/sass. Legacy vite build verified green (also surfaced + fixed stale container deps from the #887 merge: leaflet-control-geocoder).
StatsShare rewritten as a vanilla self-initializing module under resources/global/js/widgets (own vite entry — the partner iframe shouldn't load the general global bundle). Translations baked server-side into data-t-* attributes by the Blade partial. Bug-for-bug compatible with the Vue original by design (fontSize switch fallthrough; emHeightAscent no-op highlight box) — both documented in-code as candidates for a deliberate follow-up, not silent changes to a live partner embed. leaf.blade.php no longer references resources/js/app.js. Ported by the statsshare-port agent; build re-verified (9.15kB chunk).
…ockfile repair Closes the gap the redirector work exposed: unconsented users (admin-created, pre-2018 accounts) had no path to record consents once the Blade form became a redirector. pages/user/consent.vue posts to /api/v2/auth/consent (reusing the registration lang strings) and auth.global routes consent.given===false users there — strict false check so a still-loading session never redirects. 812 vitest green. Also: vite.server.allowedHosts fixes the container-hostname rejection that silently stalled e2e iteration for hours, and the client lockfile is regenerated with STANDARD npm resolution — --legacy-peer-deps (a root-repo necessity) drops peer deps like @popperjs/core and produces npm-ci- incompatible lockfiles for the clean-npm client (third drift incident, root cause closed).
Ported event/device/admin/grouptags flows (~46 tests; grouptags keeps the legacy 34-test permission matrix incl. the 4 pure-API checks). Specs authored by the phase-cde-e2e agent; verification and fixes completed after takeover. Client bugs found by these flows, all fixed with unit coverage: - No cold-boot session bootstrap: full page reloads rendered logged-out (plugins/session.ts, awaited pre-paint). - Event-page permissions derived from the dashboard's capped your_groups: stale for new groups; now GET /api/v2/users/me/groups via the groups store. - hide-footer is not a bootstrap-vue-next prop: every modal (10 across 8 components) rendered a duplicate default footer; now no-footer. - DeviceForm sent null for empty optionals; the endpoint's validation (2025, pre-branch) requires omission — legacy client provably omitted falsy keys (store/devices.js FormData truthiness guard). - Forced device-list refreshes swapped the table for a spinner, unmounting the row being edited (photo upload closed the edit form); panel now stale-while-revalidates. - Category suggestion only reacted to typing, not to datasets arriving after; split watchers (user edits may clear, data arrival only fills). - Suggestion e2e is self-seeding (test DBs have no production item-type mappings; the legacy spec was CI-excluded for this). - e2e URL matchers: endsWith broke on BaseAPI's locale param.
… green)
user/reset + user/recover (GET only now; the API owns submission),
user/register/{hash?} (invite_hash carried through), group/party
accept-invite (DB semantics unchanged, redirect + joined/invalid query
flags replace session flash), group/party invite/{code} (session-array flow
gone entirely — the SPA claim endpoint owns it). Party::shareable_link now
points at the frontend like Group's. AcceptUserInvites middleware is now
provably unreachable (removed at F proper).
Authored by the f-redirectors agent; verified after e2e completion (two
route-removed tests needed withExceptionHandling for their 405 assertions).
The accept-invite redirectors replaced Laravel session flashes with query flags, which left groups.invite_confirmed unused in Laravel code and broke testCheckTranslations. The checker now scans client/app and client/e2e so SPA t() usage counts. The group and party view pages now toast on ?joined=1 / ?invite=invalid and strip the params, restoring the legacy flash messages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzRkdRa6w1P3ZXKH5pGDh5
The two delete tests inline the flow to assert on tag-delete-warning mid-way, so the helper was never called. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzRkdRa6w1P3ZXKH5pGDh5
fetchForEvent and addDevice created a plain entry object, assigned it into byEvent, then kept mutating the plain reference - those writes bypass Vue's proxy, so subscribers were never notified. Whether the event page's devices panel left its loading skeleton depended on the fetch beating the panel's first render: the recurring spare-parts / add-device e2e flake (deterministic 6-minute timeouts on slow CI). Assign the fallback into state first and read the proxy back. New spec pins the notification, not just the post-await value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzRkdRa6w1P3ZXKH5pGDh5
/group/map as its own route/tab: markers for every non-archived group from the names index (clustered, leaflet.markercluster), visible rows hydrated via GET /api/v2/groups/summary?ids= chunked at the API's 200-id cap - the split-fetch architecture from PR #887. Photon place search (leaflet-control-geocoder), CARTO tiles, row<->marker hover linking in GroupsTable, and pure geometry helpers split into a composable for unit testing. Deviations from legacy documented in GroupMap.vue (no initial user-area bounds - no v2 source; marker popup links to /group/view instead of the modal). 53 new vitest cases plus a groupmap e2e spec. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzRkdRa6w1P3ZXKH5pGDh5
task docker:test:playwright:client -- <file> silently ran the full suite; the args now reach npx playwright test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzRkdRa6w1P3ZXKH5pGDh5
…cation Double-run e2e gate met (57/57 twice, zero flaky). Checklist gains verified F2 details: errors pages must always use header_plain (navbar needs seven dying route names), set-lang/set-cookie/user-thumbnail have no surviving consumers, preview-deploy is kept with a session-auth DECISION flag, and the route trim uses an explicit keep-list plus an api-excluded catch-all redirect to the SPA. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzRkdRa6w1P3ZXKH5pGDh5
The Nuxt SPA in client/ is now the sole user-facing frontend. Laravel
serves /api/v2 plus a small pinned web surface (SSO bridge, emailed/shared
deep-link redirectors, anonymous exports + calendar feeds, the embeddable
stats widgets, admin preview-deploy) and a catch-all that 302-redirects any
other browser navigation to the SPA. tests/Feature/ApiOnlyRouteSurfaceTest
pins that surface.
Removed:
- routes/web.php trimmed to the API-only surface + catch-all (api/ excluded
so unknown API paths 404 as JSON).
- ~200 Blade views (all but the retained widget/error/email set),
resources/js, resources/sass.
- laravel/ui + Auth/{Login,Register,ResetPassword,ForgotPassword,
ConfirmPassword}Controllers; EnsureAPIToken, VerifyUserConsent (web),
AcceptUserInvites middleware (VerifyUserConsentApi, the API gate, stays).
- Jest + tests/Integration + legacy Playwright config; the CI
build-legacy-frontend job; jest/vue2 deps from the root package.json.
- The dead legacy-prefix web redirects (FaultCat etc.) -> nginx 301 map.
Changed:
- Authenticate::redirectTo -> FRONTEND_URL/login (route('login') is gone).
- vite.config.js builds only the widget + wiki assets; no Vue. docker_run.sh
startup npm section slimmed.
- CheckTranslations scans client/ and builds one in-memory haystack instead
of a grep per key (was ~20 min + a process-isolation deadlock); resolves
the 'en' locale explicitly. ~290 now-dead lang keys pruned across en/fr/
fr-BE. Client-side i18n gaps fixed (kg/CO2 tab suffixes, km, network alt).
Regressions the cutover had introduced, now fixed (found by test triage):
- Network logo upload had no API/UI -> POST /api/v2/networks/{id}/logo +
TusImageUpload on the network page.
- The "Add groups to a network" modal called an unimplemented endpoint ->
POST /api/v2/networks/{id}/groups.
- Group delete stopped enforcing "can't delete a group with a device" ->
archivev2 re-checks Group::canDelete().
- The "request reviews from attendees" feature was dropped -> POST
/api/v2/events/{id}/request-review + a host-only button.
- The event page's "follow group" link pointed at a removed route -> calls
the join API.
- The notifications page (profile tab target) was never built -> GET/POST
/api/v2/users/me/notifications[/read] + a /notifications page.
- The image_upload feature flag (disable writes in preview/staging) was dead
for the API path -> FixometerFile::uploadLocalFile now honours it.
- Event-photo uploads stopped notifying admins to moderate -> uploadImagev2
re-fires EventImagesUploaded.
- The groups-table filter bar (name/location/country/tags) wasn't ported ->
client GroupsTableFilters.vue + client-side filtering in GroupsTable.
Tests: the Blade-render Feature tests were superseded by the APIv2*Test
suite + client vitest/e2e. loginAsTestUser now uses a factory user +
actingAs. Dead-Blade tests were deleted where coverage moved; hybrid tests
stripped of dead-page assertions and repointed to the live API; security
tests ported to policy assertions / new trigger points. New API tests cover
every restored endpoint; new e2e specs (networks, event request-review /
follow-group, notifications) exercise the flows that previously had no
coverage. See docs/nuxt-migration/cutover-checklist.md for per-file
decisions and remaining follow-ups.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzRkdRa6w1P3ZXKH5pGDh5
Full phpunit green (772), client vitest 877, e2e 62 passed. Plus 9 real regressions restored during test triage, each with API+e2e coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzRkdRa6w1P3ZXKH5pGDh5
Live-vs-local parity review (docs/nuxt-migration/findings/g6-visual-review.md)
against the running restarters.net site found several cutover regressions:
- Profile pages (/profile, /profile/{id}) always showed "profile couldn't be
found": the client calls GET /api/v2/users/{id}, which didn't exist. Add
UserController::getPublicProfilev2 + route, returning the documented PII-safe
shape (name/avatar/role/location/groups/skills/bio, NOT email) - any logged-in
user may view any profile, matching the always-public legacy Blade page.
- The /group/all tab rendered the raw key GROUPS.ALL_GROUPS: add the
client-only key client.groups.all_tab (en/fr/fr-BE) and point GroupsTabsNav
at it. Added a label-resolution test (the old test only checked hrefs, which
is why the missing key slipped through).
- client/public/images/ was never created, so every /images/* fallback
(placeholder avatar, the icon SVGs, onboarding photos) rendered as a broken
image. Populate it.
- Bootstrap 4 spacing utilities (mr-/ml-/pl-/pr-) are dead under bootstrap-vue-
next's Bootstrap 5, so nav and footer links ran together -> me-/ms-/ps-/pe-.
Asset rationalisation (per request): raster -> WebP (placeholder-avatar 8K->4K,
onboarding photos ~280K->~38K each, via GD); the UI icons stay SVG (vector CSS
backgrounds - the "unless a reason not to" exception); line-endings LF-
normalised; kebab-case names; references updated across the client.
Tests: tests/Feature/Users/APIv2PublicProfileTest, GroupsTabsNav label test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gate
Careful review of the API surface (docs/nuxt-migration/findings/
security-review-api.md), fixing the confirmed exploitable findings:
- CRITICAL - device IDOR: PATCH /api/v2/devices/{id} checked permission on the
body eventid but overwrote the device loaded by the URL id, with no check it
belonged to that event. Device ids are sequential, so a host of event A could
PATCH a stranger's device with eventid=A and reassign/overwrite it. Now also
require edit permission on the device's current owning event (matching
deleteDevicev2). Test: APIv2DeviceUpdateAuthTest.
- HIGH - open redirect: BridgeController::safeRedirect used a bare
str_starts_with, so https://<frontend-origin>.attacker.com passed the
allowlist - and it fires right after establishing a real login session.
Match on an origin/path boundary. Test:
SsoBridgeTest::testRedirectAllowlistBlocksSuffixBypass.
- HIGH - event visibility regression: GET /api/v2/events/{id}, /attendees and
/devices had no gate, so anyone could read full venue / exact lat-long /
online-meeting link / attendee names / device notes for events on unmoderated
(spam-risk) groups. Restore the legacy userHasViewPartyPermission gate (404);
events on approved groups stay fully public. Test: APIv2EventVisibilityTest.
- MEDIUM - password-reset token replay: resetPasswordv2 never rotated recovery/
recovery_expires, so an intercepted/forwarded link stayed valid for 24h even
after use. Rotate it on reset (single-use), matching updateMyPasswordv2. Test:
AuthEndpointsTest::testResetPasswordTokenCannotBeReplayed.
Remaining findings (repair-directory read gate, dead v1 /api/groups/ table
dump, dead draft-image IDOR path, tus filename collision + disk-fill, missing
throttles, public group directory showing unapproved groups) are documented
for follow-up. The group-directory visibility question needs a product decision
and was left unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…guard
The H2 event-visibility gate (getEventv2/attendeesv2/devicesv2) used
$request->user(), which only checks the default 'web' guard. The SPA
authenticates with a sanctum bearer token, so on these auth-middleware-free
routes the acting user came back null and the gate 404'd even an admin or host
- which broke the e2e device-moderation flow (setUpApprovedEvent could no
longer load /party/view/{id} to approve an event on a freshly-created,
still-unapproved group). CI #4829 build+build-client were green; e2e-client
caught this.
Resolve the acting user across guards (web -> sanctum -> api, non-throwing) via
a new optionalUser() helper, matching the controller's own getUser(). Events on
approved groups were always public regardless, so only the moderation path was
affected.
Test: APIv2EventVisibilityTest::testUnapprovedGroupEventVisibleToAdminViaBearerToken
uses a real bearer token (no actingAs) and 404s an anonymous caller - it fails
on the old $request->user() path and passes now.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…page The event-visibility gate (H2, commit 837d454) correctly hides events on unapproved (unmoderated) groups from everyone but their host/coordinator/admin. The follow-group e2e ("Following the hosting group from the event page joins it") had a non-member (USERS.host) view an event on a still-unapproved group, so with the gate the event page now 404s and the follow button never renders (CI #4830 e2e-client: 1 failed, 62 passed). Add an approveGroup() util (mirrors approveEvent, via GroupForm's group-form-moderate select + PATCH /api/v2/groups/{id}) and approve the group in that test - matching the realistic flow: you follow a group once it's been moderated/made public. Test-only change; locally event.test.js is green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This was referenced Jul 18, 2026
| // establish, regardless of what the default guard has been set to. | ||
| if (Auth::guard('web')->check()) { | ||
| // Already have a web session (e.g. second bridge hop); no ticket needed. | ||
| return redirect($redirect); |
| Auth::guard('web')->login($user); | ||
| $request->session()->regenerate(); | ||
|
|
||
| return redirect($redirect); |
The login/dashboard templates already reference .panel / .panel__orange / .panel__blue, but those classes were never ported from the legacy resources/sass/_panels.scss - so the signature brand card (white fill, thin black border, hard 6px offset drop-shadow) and the amber/blue variants were missing, leaving flat bordered boxes vs the live site. Add client/app/assets/css/_panels.scss (ported verbatim, $brand-black -> $black) + the $brand-blue token, imported in restarters.scss alongside the other component base layers. No component-scoped CSS: the existing template classes now resolve. Fixes the login page's amber welcome panel + card shadows; the same classes back the dashboard panels (D2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The highest-severity item in the parity audit, and a consent gap rather than a styling one: "Cookie settings" was a NuxtLink to /about/cookie-policy, so a visitor could accept everything or read an article - there was no way to decline analytics. Consent was a single boolean in localStorage. develop's gdpr-cookie-notice opens a modal listing each category with its own switch and a Save button. /about/cookie-policy is a SEPARATE "statement" link inside that modal, not the destination of the settings control - we had wired the secondary link in place of the primary one. Now: a settings dialog with Essential (listed, "Always on", no switch, because the site cannot work without it), Performance, Analytics and Marketing toggles, and a Save that persists the choice. The OK button keeps develop's accept-all defaults - marketing off, everything else on. Stored as develop's cookie, not localStorage, keeping its exact name (gdprcookienotice), shape and key spellings - including "performace", which is misspelled in develop. Renaming it would silently discard the stored preference of every visitor who set one on the legacy site, and the retained Blade widgets read that cookie: header.blade.php derives window.restarters.analyticsCookieEnabled from it to gate Matomo and Sentry. useCookieConsent now exposes analyticsEnabled for the same purpose here. Strings taken verbatim from develop's gdpr-cookie-notice locale, translated for fr/fr-BE. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Ye3otsxm9RiHHDKcC3TJA
Controls that looked right and did the wrong thing - the class a screenshot
diff cannot catch, because the pixels match and the behaviour does not.
1. EventCard offered a live RSVP/cancel button on events that had already
happened. GroupEventsScrollTableActions.vue gates the whole actions cell on
`v-else-if="upcoming"`, and for an attendee shows static bold "You're
going!" text - there is no un-RSVP from this table at all. Two tests
asserted our version, including one I added earlier, so they pinned the
divergence rather than develop's behaviour; both corrected.
2. The dashboard's nearby-groups "Join" button navigated to the group page
instead of joining. Its comment said the join endpoint "lands with
B2/B4-B5"; POST /api/v2/groups/{id}/members/me has existed since then and
groupsStore.join() already wraps it. A button labelled Join that does not
join.
3. Password reset ended on an invented in-page success alert with no route
back to the sign-in form. UserController::reset() redirects to /login with
the passwords.updated flash; login now shows that flash on arrival.
4. Nearby-group join and reset both needed their specs updating rather than
worked around - the join spec asserted an href, which was the bug.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Ye3otsxm9RiHHDKcC3TJA
1. Shareable-link invites sent logged-out visitors to /login. confirmCodeInvite redirects to /user/register with the auth.login_before_using_shareable_link banner - a shareable link normally goes to someone with no account yet, so the sign-in form was the wrong step first. The banner still offers the login route. The success message also reused groups.invite_confirmed, develop's text for the unrelated email-hash accept-invite flow, and named nothing. develop flashes groups/events.you_have_joined - "You have joined <linked name>". The claim endpoint now returns the group/event name so that message can be rendered. already_member stays as our own string: develop has none here, and inventing linked copy to match would repeat the mistake this audit was about. 2. The navbar showed the full wordmark at every width. navbar.blade.php swaps to the compact power mark below md (includes/logo vs includes/logo-plain), which is part of why the header clipped on a phone. 3. brands.vue offered delete. develop has no brand delete anywhere - no route, no control, no controller method - so the workflow was invented outright. allow-delete is now false for that page; AdminCrudTable keeps the capability for the pages that legitimately have it. Recorded as accepted deviations rather than reverted: - Uppy for profile photo and network logo, where develop has a plain file input. Standing instruction from the product owner: all photo uploads go through Uppy. - AdminCrudTable's delete confirmation. develop deletes on an unconfirmed GET link; removing a confirmation step from a destructive action to match that is not a sensible reading of parity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Ye3otsxm9RiHHDKcC3TJA
GroupEventScrollTable.vue is a b-table with every column sortable. Only the date header sorted here, so the stats columns restored in b1877c9 were decorative - a host could see participants or CO2 per event but not order by them, which is most of why you would look at the table. All columns now sort, first click ascending and the active one flipping, matching b-table. Missing stats sort as -1 rather than 0: "not recorded" and "a recorded zero" are different, and interleaving them would misreport which events had nothing logged. Caught while wiring it: the existing header's `@click="toggleSort"` passed the MouseEvent as the new sort key once toggleSort took an argument, which silently broke sorting entirely. Its test failed immediately, which is the only reason it did not ship. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Ye3otsxm9RiHHDKcC3TJA
The bulk of the 105-item parity audit, fixed in twelve concurrent batches with disjoint file ownership, then a single strings pass (sole owner of lang/**) and a verification pass. Every fix was made by reading develop's actual markup/CSS for the element and matching it - the discipline whose absence produced these deviations in the first place. Highlights by root cause: RC1 control-substitution: the native controls that stood in for develop's vue-multiselect are now TagMultiselect (DashboardAddData, EventForm, EventFilters, GroupsTableFilters, DeviceForm). Date filters use vue-datepicker-next, the same library Freegle uses and which we already depend on. Every string these needed was taken verbatim from develop's lang files, not invented. RC2 orphaned CSS / rewritten markup: doodle icons restored to their native SVG dimensions (develop sizes most of them not at all); Talk-topic comic-card spacing and hover box-shadow restored from DiscourseTopic.vue; row dividers given develop's solid-black hr instead of Bootstrap grey (with the !important BS5's border utilities require); onboarding modal restyled from _onboarding.scss against its own class names. RC3 invented strings replaced with develop's verbatim: forbidden.title, moderate_approve_help (both traced to develop source - forbidden.blade.php and EventAddEdit.vue:84), and 17 keys added rather than made up. RC4 dropped behaviours restored: whole-row click targets, the onboarding close X gated to the last slide, Previous hidden not disabled on the first. Two dead keys removed, both now genuinely unreferenced: - dashboard.getting_the_most_intro - this reverts my own earlier commit 3b9d4ce, which added it as "visible at all widths". It is not: develop wraps it in a d-none d-md-block <p> whose only child is d-inline d-md-none - mobile content in a desktop-only container, invisible at every width. The identical dead-markup pattern I correctly caught for sidebar_help in the same file and then missed here. - admin.role_permissions_help - develop's role-edit modal uses a literal untranslated paragraph; no lang key is referenced there. Tests: several specs asserted our old (wrong) behaviour and were corrected to develop's, not weakened - the onboarding close-on-last-slide test, the whole-row-link test, and the native-select-to-TagMultiselect driver changes. Full client suite 1568 passing; the 2 failures are the known useStatsShareImage container-mount artifact that passes in CI. translations:check exits 0, eslint 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Ye3otsxm9RiHHDKcC3TJA
…in the 88 fixes An exhaustive verification pass re-read all 88 fixes from ccc4b84 against develop. 77 matched; 11 diverged - the fix had invented or mis-copied a value and shipped it under a plausible note, which is exactly the failure mode the whole parity effort was cleaning up, recurring in the cleanup. Sampling would have missed all 11. Corrected against develop's actual source: - DashboardOnboardingModal: nav buttons bottom:25px/left:40px/right:40px and finish bottom:32px (were 0/0); slide dots 15x15px #81CAD3 active #000 (were 8px #d8d8d8/#222); modal max-width 615px at lg (was ~500). From _onboarding.scss and _slideshow.scss. - RichTextEditor: pasted <img>/<table>/<iframe> were embedded; develop's quill-paste-smart strips every tag outside [a,b,strong,u,s,i,p,br,ul,ol,li, span,h4,h5,h6]. Added the tag allowlist matcher (text kept, embed dropped). Security-relevant - this was a real sanitisation hole, not just styling. - DevicesSearchTable brand filter: native <datalist> replaced with the custom suggestion panel DeviceForm already built and the codebase deliberately chose over datalist for parity with vue-typeahead-bootstrap. - LocaleSwitcher: restored _languages.scss's position:fixed;bottom:0 on the login/guest layout with 65px page padding. - AppNotifications: coloured pill badges (Patua One, badge-left/right padding) from _badge.scss, were transparent. - CalendarsTab: the .span-vertically-align-middle class was referenced in markup but never ported - added it (and the cog before "All events"). - networks/[id] coordinator card border #000 -> #222 and hover colour. - AdminCrudTable .edit-panel__device padding rule from _edit.scss. - Two *-icon-brand.svg assets were missing from client/public entirely, so correct markup rendered a broken image - copied from the Laravel public dir. NOT applied - recorded accepted non-match: category.vue weight/footprint/name column labels. develop's CategoriesTable.vue hardcodes English headers bypassing its own lang file; matching literally would delete our fr/fr-BE. Reverted the batch's name->'Name' change and its test to keep all three translated, consistent with 1eb9863. Full client suite 1569 passing (2 known useStatsShareImage container-mount failures), translations:check 0, eslint 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Ye3otsxm9RiHHDKcC3TJA
e2e-client was timing out - not slowly, but hanging. grouptags.test.js's
network-tag helpers waited 360s for getByTestId('tag-add-button'), never found
it, retried, and blew the whole job's wall clock, taking all e2e signal down
with it.
Root cause, and it is NOT this session's parity work: the network-tag section
was written for a modal CRUD UI (tag-add-button -> tag-create-modal ->
tag-create-submit, tag-edit-modal, tag-delete-modal) with testid-prefix "tag".
The actual component, NetworkTagsManager.vue, has always been a bespoke
inline create form (network-tags-create-*) plus edit/delete modals
(network-tags-edit-*/network-tags-delete-*). git log --all -S "tag-add-button"
finds it nowhere in client/app - it never existed. My one touch of this file
(ccc4b84) changed two icons and a background colour, no testids; the mismatch
predates the whole migration branch, which is why e2e-client was already
timing out on pipeline #4894.
Rewrote the network-tag helpers and test bodies to drive the component that
actually renders - the same fix applied to party/index.spec.js earlier this
session: address the real control, not one that was never built. Create is an
always-visible inline form (no add-button, no create-modal - the redundant
"wait for create modal to close" assertions after each create are removed).
Edit and delete are modals, remapped to their real testids. Added the one
missing testid the flow needed: network-tags-delete-cancel on the delete
modal's cancel button.
Every remapped testid verified present in NetworkTagsManager.vue /
networks/[id].vue; the create/update/delete API URLs and methods the helpers
wait on already matched the component's store actions. eslint clean.
NOT run locally: a background job holds the shared test DB, and running
Playwright into it would corrupt both. CI validates the e2e; global-tag and
group-tag helpers in the same file were already correct and untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011Ye3otsxm9RiHHDKcC3TJA
Four issues from the preview, all confirmed against develop. 1. Dead session shows a dashboard error instead of redirecting to login. The console evidence (GET /api/v2/users/me/events 401, "/" redirecting to /dashboard) is a token present in localStorage - loggedIn (=!!token) true, so the guard admits it - while the session behind it is invalid, common on a preview whose DB was reset so the token maps to a deleted user. BaseAPI only clears auth on a /session *401*; a 500, or a /session that returns a null user for a bad token, left the token in place. The cold-boot session plugin now clears the token on ANY failed resolve OR a null-user response, so the whole app - guard, navbar, the "/" redirect - consistently treats it as a guest and routes to /login. Two tests pin this. 2. Logged-out Sign in / Join buttons. develop writes plain anchors with inline styles the .btn classes were fighting: square corners (we rendered a pill radius), a 10px gap between them (they were touching), Sign in white with a 2px black border, Join solid black. All re-asserted to match, sentence case per develop's text-transform: initial. 3. Language bar was not full width - AppFooter wrapped it in Bootstrap's .container, capping it to the content max-width. Removed; the bar is now width:100% with develop's top border, matching _languages.scss. 4. Language bar sat mid-page on short pages instead of pinned to the bottom. develop sets <body class="fixed-layout"> in BOTH header.blade.php (app pages) and header_plain.blade.php (guest), and .fixed-layout .language-bar is position:fixed;bottom:0;z-index:10 - i.e. pinned on every page, with the body padded 65px so it never covers content. An earlier fix scoped this to guest pages only; generalised it to every page as develop does. The dashboard error box (#2 of the reported set) is left as-is: once the dead session redirects to /login it no longer appears in that flow, and develop server-renders the dashboard so there is no SPA error box to match against. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Ye3otsxm9RiHHDKcC3TJA
When no groups are in the map view, the count line read "There are 0 groups. Zoom out to see more." - technically true but a dead end. It now reads "If you can't see any here yet, why not find a group near you?", with "find a group" linking to /group/nearby. Not a develop parity change - develop has no group_count_map string at all, this whole count line is ours - so this is a UX improvement, and the friendly copy plus link is the requested behaviour. The link uses <i18n-t> with a #link slot rather than a raw <a> in v-html: it keeps the link's position translatable (it moves in the French sentence) and gives a real client-side NuxtLink instead of a full page reload. Same pattern as party/edit's "Editing <name>" heading. Non-zero counts keep the existing pluralised count string. fr/fr-BE added, using the map's own "Repair Cafe" wording for the link. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Ye3otsxm9RiHHDKcC3TJA
Two correctness fixes from the RES-1995 map-of-groups work (PR 887), still unmerged to develop, that the nuxt branch's own rewrites had dropped. 1. GroupSummary next_event dropped the approved filter. The base intent - Group::getNextUpcomingEvent - selects the next event WHERE approved=true. The bulk-cached summary rewrite (for the map, which fetches many groups at once) cached Party::future()->get() with no approval filter, so a pending-moderation event could surface as a group's public next event. Now filters approved=true, and the cache key is future_approved_events to match (the old key would otherwise serve stale all-events data). The comment already said "next approved event" - the code just didn't. 2. Party::scopeFuture now reorder()s before its orderBy. orderBy appends, so a parent scope's existing ORDER BY (e.g. DESC) stacked into "... DESC, ... ASC" with DESC winning, making future() return the LAST future event first - so a next-event lookup picked the wrong one. Test asserts an unapproved future event does NOT become a group's next_event; the existing tests only checked approved ones appear. The stale future_events cache key in the test is updated to the new name. Not runnable locally right now (a background job holds the shared test DB); CI validates. develop is exactly at the 887/nuxt merge-base, so 887 has not landed there yet - this brings its fix into 898 early rather than regressing the map when it does. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Ye3otsxm9RiHHDKcC3TJA
PR 887 (RES-1995 map of groups) is unmerged and develop sits exactly at its merge-base, so its wording never reached the nuxt branch - and my ac4f5a5 "drift" sync had actually reverted some of it toward the stale develop values. This brings 887's group/map copy into 898. Filter bar (used by list and map): search placeholders -> "Search by name", "Search by location", "Filter by country", "Filter by tags"; "Show filters" / "Hide filters" (sentence case). This reverses the direction of ac4f5a5 for these keys - 887, not the current develop, is where the project moved. Nearby/find tab: groups_title2 "Other groups nearby" -> "Find a group". Map count line: adopts 887's wording - "There is N group in this area. Search and zoom to find more." - and its zero-state group_count_none ("...why not find a group near you?", linking to /group/nearby). My last two commits had built ad-hoc versions of exactly these (group_count_map_empty, find_a_group, the i18n-t/NuxtLink zero-state); those keys are removed and the map page now uses 887's group_count_none verbatim, rendered via v-html since 887 carries the link inline. The rendered text is what those two commits already showed - this just converges on 887's actual keys so the two branches don't collide in lang/groups.php when both reach develop. Deliberately NOT changed: /group/all's own group_count stays a plain "There are N groups." - 887's "in this area, search and zoom" belongs to the map, and 898 split develop's single /group page into separate list/map/nearby routes where that wording would be wrong on the list. groups_title2_mobile is not added: 898 references it nowhere and translations:check rejects unused keys. fr/fr-BE taken from 887 verbatim (it uses "Repair Cafe" for groups on these pages). Specs asserting the old labels updated to 887's; full client suite 1572 passing (2 known useStatsShareImage container failures). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Ye3otsxm9RiHHDKcC3TJA
The one 887 feature 898 had deliberately dropped: clicking a map marker opens a modal with the group's next event and a "Go to group" action, rather than a bare popup link. 898's comment said a Vue modal "isn't reachable from inside a marker's click handler without extra plumbing" - this is that plumbing. The seam: markers are imperative Leaflet layers (for clustering), so GroupMap's marker click emits `select` with the group id; the page turns that into a GroupInfoModal for the selected group's summary, and clears it on close. GroupInfoModal ports develop's component to Vue 3 / bootstrap-vue-next: header (image + name + location) linked to the group, "Next event: <date> <title>" or "None planned", and Go to group / Close. Date formatted exactly like GroupsTable's dateLabel so the map and list agree. The redundant marker popup is removed (887 has the modal, not both), which also drops the now-unused escapeHtml helper. Built test-first: GroupInfoModal.spec (6 - render/next-event/none-planned/goto/ close), GroupMap.spec (marker click emits select with the id), map.spec (select opens the modal for that group; close clears it). Each failed before the implementation. goto_group/next_event lang keys added from 887 (fr/fr-BE too). Full client suite 154 files, only the 2 known useStatsShareImage container failures; eslint clean; translations:check 0. The backend approved-next-event fix (c51f096) is confirmed live: GET /groups/summary?includeNextEvent returns the seeded approved future event. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011Ye3otsxm9RiHHDKcC3TJA
The marker-info modal showed the location as {{ group.location }}, but the
summary API returns location as a GroupLocation object ({location, area,
postcode, country, country_code, lat, lng}) - so the modal leaked the whole
JSON blob into the header. develop's GroupInfoModal renders
group.location.location; match it. The unit fixture used a plain string so
the bug never surfaced in tests; browser verification (WSL chromium, GPU
disabled) caught it. Fixture now uses the real object shape and asserts no
raw object leaks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
develop had its own deploy and teardown path, separate from PR previews: a dedicated fly.dev.toml, a long-lived restarters-dev-db MySQL app, and a nightly GitHub Actions job to stop a machine that could not suspend itself (4GB + swap; swap disables Fly's suspend). Previews already had a better answer - disposable embedded database, suspend on idle, wake on request. restarters-dev is now a preview in every respect. fly.pr.toml becomes the shared template fly.preview.toml with four placeholders (__APP_NAME__, __APP_TITLE__, __BASE_URL__, __SENTRY_ENV__), substituted identically by pr-preview.yml and CircleCI's deploy-fly-dev. The develop deploy keeps its existing test gate and now polls /_preview_status, failing the build if the restore or the branch's migrations fail - previously a broken migration produced a quietly broken dev site. Deleted: fly.dev.toml, fly-mysql-dev.toml, stop-dev-nightly.yml, and the admin "Deploy Preview Branch" tooling (page, controller, routes, specs, six orphaned translation keys). That tooling dispatched preview-deploy.yml, which was deleted in 3db7b1c, so the button had been failing since. QUEUE_CONNECTION is database, not sync, for every preview. The supervisord worker already ran `queue:work database` regardless, so sync only served to block the dispatching request. Fly suspends on proxy inactivity rather than CPU, so a polling worker does not hold the machine awake. Base URL is restarters-dev.fly.dev, not restarters.dev: that hostname resolves to an unrelated host and restarters-dev holds no certificate for it, so the old fly.dev.toml's APP_URL was simply wrong. .dockerignore: `node_modules` as a bare name only matches at the repo root, so client/node_modules (~466MB) was uploaded to the builder on every deploy, CI included. Use **/node_modules, and exclude local-only clutter that took a working-tree deploy to 8.1GB across 358k files - past the builder's 8GB uncompressed image limit. Context is now 561MB / 5,468 files. Verified: 1575 client tests pass; translations:export-client --check is in sync; the deployed app reports {"phase":"ready","restore_ok":true, "migrate_ok":true} with the queue worker running and the db-backup and restart-yesterday crons stripped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0172K3nBv5WN34mB9taxLyWt
User feedback on the PR 887 map, applied to the Nuxt port: - MAX_MAP_ZOOM 14 -> 18 (CARTO raster tiles serve z18): street names were unreadable and close-together pins couldn't be told apart. - maxClusterRadius 80 -> 120 (MAP_CLUSTER_RADIUS): too many small cluster bubbles on screen at once; wider radius merges them into fewer, larger clusters. - Port Freegle's ClusterMarker.vue identical-location separation: each subsequent group at the same exact coordinates is nudged by 0.003 degrees into a display-only copy (never mutating the store's objects - Freegle's accumulation regression), with string coords coerced so the nudge is arithmetic, not concatenation. TDD: all three changes test-first (useGroupMapGeometry.spec.js, GroupMap.spec.js); full client suite green (1583 tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRv64fqMT3fAveD6NFgnmR
/group/map existed but was linked from nowhere: the tab bar deliberately omitted it, citing pre-887 legacy's three tabs, and four specs pinned its absence - while the groupmap e2e already expected groups-tab-map to be active. PR 887 reworks the groups page so the map IS one of /group's tabs, so it must be reachable from /group. Rendered as a fourth tab rather than replacing nearby/all as 887 does, because this port's /group/all carries the full-list filter bar and tag badges that 887 folded into its map list panel (not ported into pages/group/map.vue's list). Uses the already-present (en/fr/fr-BE) client.groups.map_tab key. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRv64fqMT3fAveD6NFgnmR
User feedback: the 0.003-degree nudge put co-located pins streets apart, reading as two genuinely different locations. Shrink it to 0.00015 (~28px lng / ~45px lat at z18, UK latitudes) so the pins only become distinguishable right at max zoom. That offset is well inside the 120px cluster radius, so also set disableClusteringAtZoom to the map's max zoom - otherwise co-located pins would render as a "2" cluster bubble even at z18 and never visibly split (spiderfy-on-click was the only way to separate them). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRv64fqMT3fAveD6NFgnmR
The port's /group kept pre-887 develop's three list tabs (mine/nearby/ all) with the map on an unlinked side route - the wrong baseline for a branch integrating PR 887, whose GroupsPage.vue has exactly two tabs with the map+list AS the "Find a group" tab. The previous commit's fourth "Map" tab was a misreading of the same history; this puts the structure right: - GroupsTabsNav: two tabs - "Your Groups" (/group) and "Find a group" (groups_title2, /group/map). - /group/nearby and /group/all redirect to /group/map (old links, bookmarks and map.vue's group_count_none inline link keep working); layout e2e now sweeps /group/map instead of /group/all. Known follow-up, deliberately not smuggled in here: 887's map list panel carries the full-list filter bar and tag badges (its GroupsTable gets search/show-tags); the port's map list panel doesn't yet, so the old /group/all extras (filters, tag badges, archived rows) are absent until that's ported. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRv64fqMT3fAveD6NFgnmR
User feedback (matching the same change on PR 887's branch): - Cluster bubbles now indicate how many groups they hold - 36/46/56px with a white -> amber -> orange saturation ramp at <10 / 10-99 / 100+ - via a custom iconCreateFunction, replacing markercluster's default green/yellow/orange discs (MarkerCluster.Default.css no longer imported). - Pins share the cluster's restart style (black border, white inner) as inline-SVG divIcons; the follow/hover states recolour the fill (green/red) instead of hue-rotating the stock blue PNG. TDD; full client suite green (1562 tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRv64fqMT3fAveD6NFgnmR
Same change as PR 887's branch: cluster border 5px -> 2px with the pale yellow #FFF396 / amber #FFD16A / brand orange #F18F01 tier ramp; pin SVG stroke 2.5 -> 2 with brand red #F45B69 on hover. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRv64fqMT3fAveD6NFgnmR
Follow-up to 983bb23: the brand colours now live in _variables.scss ($brand-orange-official #F18F01, $brand-red #F45B69, and the $map-cluster-*/$map-pin-* role tokens built on them) and GroupMap.vue @uses them. The navbar notification badge, which already hardcoded the same #f45b69, now reads $brand-red too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRv64fqMT3fAveD6NFgnmR
Same change as PR 887's branch, via the client's own $font-family-sans-serif token. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRv64fqMT3fAveD6NFgnmR
User feedback: with scrollWheelZoom off, the wheel scrolled the page instead of zooming the map, and people couldn't work out how to zoom. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRv64fqMT3fAveD6NFgnmR
df40a22 deleted the /group/nearby list page but left its three now-unreferenced keys (nearest_groups, nearest_groups_change, no_groups_nearest_with_location) in en/fr/fr-BE - translations:check counts unused keys as errors and Translations\CheckTest requires exit 0, so CI's build job has been red since. Client locale exports regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRv64fqMT3fAveD6NFgnmR
The "upcoming" fixtures hardcoded 2026-08-20T10:00Z, which stopped being the future mid-morning today - 15 tests flipped red with no code change. Upcoming/later fixtures now derive from Date.now() (+30/+31 days), and the header test derives its expected day-of-month from the fixture in the event's timezone instead of hardcoding "20". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRv64fqMT3fAveD6NFgnmR
Mirror of the same feature on PR 887's branch, in this stack: - GET /api/v2/users/me/profile now carries the user's geocoded lat/lng (floats, null when unset) - the client had no numeric coordinates for the signed-in user at all. - useGroupMapGeometry gains haversineKm (real km, unlike the ordering approximations - this number is shown to the user). - GroupMap emits `searched` with the landed geocode centre. - /group/map anchors to the searched place, else the profile coordinates, else hides the column; rows carry distance straight from the names index. - GroupsTable: opt-in distance column (optionalColumns.distance) with a "km" header, 887's rounding (tenths under 5 km), click-sortable with unplaceable groups pinned last, and a new initialSortKey prop so the map page opens nearest-first. TDD (12 new tests watched fail first); client suite green (1577), APIv2UserProfileTest green (29). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRv64fqMT3fAveD6NFgnmR
Same fix as PR 887's branch, on the v3 promise-based geocoder API: q=Haringey never surfaces the borough in Photon's default ranking, so the search runs a place-layer-filtered query alongside the general one and lists places first (deduped, capped at 10, failure on either side degrades to the other). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRv64fqMT3fAveD6NFgnmR
Only index.spec.js uses the second future date pair; no-unused-vars failed the build-client Lint step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRv64fqMT3fAveD6NFgnmR
The e2e-client CI job has been dying on CircleCI's 10-minute no-output rule: several tests went stale against deliberate UI changes and each burned 3 x 360s in silence. Verified locally per suite: - brands round-trip: brand delete is deliberately absent (b17158c - develop has no brand delete anywhere); the test now pins the button's absence instead of waiting for it. - global tags round-trip: delete moved into the edit form (638e285); final assertion counts the row rather than reading a table that is absent mid-refetch. - device tests: the devices panel and its tables render twice (desktop + mobile collapsibles), so bare testids trip strict mode - scoped to the desktop panel in device.test.js and utils.addDevice; category/status/spare-parts became DeviceForm's plain-element multiselects, so the helper clicks control + option instead of selectOption, and the category assertion reads data-value. - networks Add-groups: the item sits in the Actions dropdown - open it first instead of clicking an invisible menuitem forever. Also a real regression this exposed: /group/all?network=N (linked from the networks page Actions menu) redirected to /group/map WITHOUT the query. The redirects now preserve it and /group/map applies ?network= as the map's network filter. Still genuinely failing (pre-existing, own session needed): the grouptags setGroupTagAssignment cluster (a newly created network tag never appears in the group form's tag checkboxes) and the Manager network-logo-manage control. Client suite green (1584); lint clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRv64fqMT3fAveD6NFgnmR
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.




Summary
client/) that talks to Laravel exclusively via/api/v2./api/v2plus a small pinned web surface (SSO bridge, emailed/shared deep-link redirectors, anonymous exports + calendar feeds, embeddable stats widgets, admin preview-deploy) and a catch-all that 302-redirects any other browser navigation to the SPA.tests/Feature/ApiOnlyRouteSurfaceTestpins that surface.laravel/ui, the 5 auth controllers, ~200 Blade views,resources/js/resources/sass, Jest, thebuild-legacy-frontendCI job, and the legacy global jQuery (resources/global/js/app.js).What's included
Nuxt 4 SPA: Vue 3, Pinia, bootstrap-vue-next (Bootstrap 5),
@nuxtjs/i18n, Vitest, Playwright e2e. All pages ported: dashboard, groups (+ map/nearby/all/create/edit), events (+ create/view/devices/attendees), fixometer, profile (view + edit tabs), admin reference-data, user/all, networks, notifications, auth.Auth: Sanctum bearer + dual guard
auth:sanctum,api; an SSO bridge (/auth/bridge) minting a web session for Discourse SSO + MediaWiki silent login.Infra: docker-compose split into Nuxt + API containers; CI four-job topology (
build,build-client,e2e-client); client translations exported fromlang/*.php.Design parity with the live site (G6) — reviewed page-by-page against production and matched:
.panel/.panel__orangecards with the offset drop-shadow (ported the legacy_panels.scssinto the global layer — no component overrides)..content-divider; brand cards global, only page-specific layout scoped./images/*set populated.API security review (verified against the pre-migration controllers) — fixed a critical device IDOR, an open redirect in the SSO bridge, an event-visibility regression (unmoderated-group events were public), and a password-reset token replay; each with a regression test. See
docs/nuxt-migration/findings/security-review-api.md.Code Quality Review
ApiOnlyRouteSurfaceTest) and per-endpoint OpenAPI response validation guard against surface/shape drift.role/api_tokenexcluded from$fillable; Eloquent bindings — no raw SQL).wiki.js) and the embeddable widget pages' CDN libs — both outside the Vue/Nuxt app.Test Plan
buildphpunit,build-clientvitest 882 + lint + build,e2e-clientPlaywright) green on the branch head.APIv2PublicProfileTest,APIv2DeviceUpdateAuthTest,APIv2EventVisibilityTest,SsoBridgeTest,AuthEndpointsTest,DashboardAddData.spec, e2e for dashboard/device/event flows).Future Improvements
docs/nuxt-migration/findings/): the SSO bridge is irreducibly a web route; the redirectors + catch-all could move to nginx; admin preview-deploy should move into the SPA + an/api/v2endpoint (it currently can't authenticate the SPA user, which is a session-vs-token mismatch). The anonymous export/calendar feeds and embeddable stats widgets are kept for external consumers/partners.security-review-api.md(repair-directory read gate, dead v1/api/groups/dump, dead draft-image copy path, tus filename-collision + disk-fill sweep, throttle gaps). The public group directory + shareable-link exposure were reviewed and deliberately left as-is.