feat: Nuxt 4 SPA migration - Laravel becomes API-only - #898
Open
edwh wants to merge 383 commits into
Open
Conversation
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
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>
Preview: https://restarters-pr-898.fly.dev❌ Deploy completed but startup reported a failure: migrate failed - check machine logs.
|
Replicate the live restarters.net dashboard and Fixometer, porting the legacy Vue 2 components (resources/js/components/Dashboard*/Fixometer*, read from 07e6abd^) faithfully into the Nuxt client. Dashboard (D2): - "Welcome to Restarters" hero flanked by the arrow/confetti doodles; Your Groups + Upcoming events in a two-column .panel; a right sidebar with the repair photo, blue "global community" info panel and the amber "Getting started" call-out (hand doodle + arrow-doodle bullets) - all using the global .panel/.panel__orange/.panel__blue classes from _panels.scss. - Restored 11 sidebar lang keys the Phase F prune dropped (values from history). Fixometer (D3): - ImpactStats rewritten as the teal stat-CARD grid (near-black #222 border + offset shadow, teal values - matching the live cards), LatestRepairs as the teal hero card; "Our Global Impact" intro; the repair-records table (DevicesSearchTable) embedded INLINE below the stats with the POWERED/ UNPOWERED tabs + ITEM & REPAIR INFO / EVENT INFO filter collapsibles + Download-all-data (admin) - matching the legacy single-page layout. /device/search still renders the same component standalone. - Restored 8 fixometer lang keys from history. CSS consistency (per review): the dashed content divider was re-declared in 6 components - promoted to a global .content-divider utility (_type.scss); normalised LatestRepairs' #000 -> #222. Brand cards stay global (.panel); only genuinely page-specific layout is scoped. Also fixes 6 placeholder-avatar refs (.png -> .webp) missed when the asset rationalisation landed - HEAD still pointed at the deleted .png. Assets: dashboard + fixometer doodles/photos/icons copied into client/public/images. Tests: vitest 880/880, dashboard e2e green, eslint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
app.js (+ its imports components/dropdown.js and misc/notifications.js) was the old Blade app's global jQuery: Bootstrap-tab hash sync, *-search form hash injection, a custom dropdown toggle and notification read-toggling. Post API-only cutover the only pages that still load it (via footer.blade.php's @Vite) are the error pages, the embeddable stats widgets and admin preview-deploy - none of which have the tab/search/dropdown/notification markup those handlers target, so the code is dead. Removed the @Vite include from footer.blade.php, dropped app.js from vite.config.js's input, and deleted app.js + dropdown.js + notifications.js. Vite build still green (stats-share.js + wiki.js + CSS). The Nuxt client is already jQuery-free. jQuery now survives only in resources/wiki/js/wiki.js (the MediaWiki skin, which runs inside MediaWiki and uses its jQuery) and the header_plain CDN libs (Bootstrap 4 / ekko / leaflet) that the embeddable widget/stats Blade pages still load - both outside the Vue/Nuxt app. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The live dashboard has an "Add Data" section (legacy DashboardAddData.vue): a
group -> event picker that jumps to the chosen event's view where the user adds
repair data. The Nuxt dashboard had dropped it (and the fixometer's "Add Data"
button just linked here), so the function was missing.
Add DashboardAddData.vue (prop-driven, matching the sibling dashboard
components): group select -> event select (that group's events, newest first)
-> Add button linking to /party/view/{id}, shown only when the user has an
event. The dashboard page fetches GET /api/v2/users/me/events and passes it in
alongside your_groups. Restored the 3 pruned lang keys (add_data_heading /
see_your_impact / add_data_add) from history in all 3 locales.
Test: DashboardAddData.spec.js (group filtering, newest-first ordering, add
link). vitest 882/882, eslint clean, visually confirmed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The D2 dashboard port restored all 11 legacy dashboard.sidebar_* keys, but sidebar_kit1/kit2/help/let_us_know are never used: in the legacy they sit inside a `d-none d-md-block` panel yet are themselves `d-md-none`, so they render on neither mobile nor desktop (dead markup) - which is why the live desktop sidebar shows only sidebar_intro_1. The Nuxt port faithfully omits them, so CheckTranslations (CI #4832 build job) flagged them as unused. Remove the 4 keys from en/fr/fr-BE and regenerate the client locale JSONs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e scss test CI #4833 build-client (vitest) failed for two reasons introduced/exposed by the Add Data work: - dashboard.vue's onMounted now also calls eventsStore.fetchMyEvents(); the dashboard page test only mocked dashboardStore.fetch, so the un-mocked call hit the (absent) $api and threw an unhandled rejection -> non-zero vitest exit (the assertions still "passed", which is why a bare test-count check missed it locally). Mock fetchMyEvents + stub BFormSelect. - restarters-scss.spec.js compiles the whole stylesheet with sass.compile in each of its 3 tests, but only the first had the 60s timeout the others need; the 5s default flaked out under load. Give all three 60_000. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Blade admin/preview-deploy page sat behind Laravel's web-session `auth` guard and checked Auth::user(), but the post-cutover SPA admin holds a sanctum bearer token and has no web session - so the page redirected to login and never loaded (the controller comment flagged this as a follow-up). Moved it to GET/POST /api/v2/admin/preview-deploys (API\PreviewDeployController, Administrator-gated, GitHub PAT stays server-side) driven by client/app/pages/admin/preview-deploy.vue + a nav link. Deleted the Blade controller, view and web routes; dropped them from ApiOnlyRouteSurfaceTest. Also removes two more routes from the non-API web surface. Tests: APIv2PreviewDeployTest (faked GitHub calls: admin-gates, PR list, missing PAT, workflow dispatch, validation). vitest 882, phpunit + route-surface green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… redirect)
The preview (and prod) image only ran Laravel, so post-cutover the catch-all
redirected / to FRONTEND_URL — which defaults to http://localhost:3000 and was
unset in the fly configs — and the SPA was never built or served anywhere.
Make the deploy image serve the Nuxt SPA and reach Laravel only at /api:
- Dockerfile.fly: builder runs `nuxt build` in client/ (standalone .output);
final image gets Node 18 to run the Nitro server.
- supervisord-fly.conf: run `node client/.output/server/index.mjs` on :3000.
- nginx-fly.conf: /api + the retained server-rendered surface (auth/bridge,
discourse, export, calendar, outbound, {group,admin,party}/stats) -> php-fpm;
everything else -> the Nitro server via `try_files $uri @nuxt`. /images falls
back to Nitro so SPA and widget images coexist. The emailed-link redirectors
drop away — the SPA owns those exact paths now.
- fly.{pr,,dev}.toml: set FRONTEND_URL + NUXT_PUBLIC_API_BASE to each app's own
origin.
Touches only the Fly deploy path (compose/CI use docker/nginx.conf + the client
container), so CI is unaffected; validate on a preview deploy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPQW8msJ8eya5XYsqk9B2S
The client build ran `nuxt prepare` under Node 18, but nuxi's @clack/core imports styleText from node:util, added in Node 20.12; client engines are >=22. Match client/Dockerfile (node:22) and CI (cimg/node:22.16). The root Vite 4 build is Node-22-compatible, so the shared builder bump is safe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPQW8msJ8eya5XYsqk9B2S
…ew migrate) The preview restore surfaced a real migrate failure that would also break the prod deploy: production has the personal_access_tokens table (created out-of-band) with no matching migrations row, so Sanctum's unguarded vendor migration re-ran on the restored copy and failed with "SQLSTATE[42S01] 1050 Table 'personal_access_tokens' already exists". Disable the vendor migration (Sanctum::ignoreMigrations) and ship our own copy under the same name, guarded with Schema::hasTable so it is a no-op when the table already exists but still creates it on fresh installs (CI, local, phpunit). Adds a regression test asserting up() is idempotent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPQW8msJ8eya5XYsqk9B2S
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
e2e-client fails on "Invite volunteers modal opens from the event view
page": getByTestId('event-view-invite') finds nothing. Nothing renders
that testid - it appears only in the test. The file's own header
described a UI that isn't there, saying the EVENT ACTIONS dropdown was
gone and a single invite button had replaced it.
EventActionsDropdown.vue is very much still there, with the invite item
under data-testid=event-actions-invite, and it deliberately mirrors
EventActions.vue's gating of isAttending && upcoming && approved rather
than the canedit the comment claimed. Open the dropdown and click the
item that exists.
No join step is needed despite the isAttending gate, and the corrected
comment now says why: createEventv2 writes an EventsUsers row for the
creator with role HOST, so whoever made the event is already attending
it. An RSVP step would have failed, since the RSVP item only renders
when !isAttending.
Every testid the test now uses was checked against the components:
event-actions-dropdown, event-actions-invite, event-invite-modal,
event-invite-emails, event-invite-submit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same fault as the invite test: "Host can request reviews on a finished event" and "Following the hosting group from the event page joins it" wait on event-view-request-review and event-view-follow-group, and nothing renders either. Both live in EventActionsDropdown.vue as event-actions-request-review and event-actions-follow-group, so open the dropdown and use those. The gating matches what each test already sets up: request-review sits under v-if="finished" in the canedit branch, and the test approves a past event as admin; follow-group is v-if="hasGroup && !inGroup", and the test switches to a host who isn't in the group. Every testid event.test.js uses is now present in client/app. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eight grouptags tests failed looking for `.form-check` checkboxes inside group-form-tags. GroupForm passes tagOptions to GroupMultiSelect, not to a checkbox group: selected tags are chips, and unselected ones only exist in a dropdown rendered while the search box has focus. Two things were needed, not one. Selecting through the dropdown got the chip added but the tests still timed out waiting for the PATCH, because select() clears the search box without closing the dropdown - and the dropdown is absolutely positioned over the rest of the form, including the submit button underneath it. Blurring the input closes it, which is the component's own @blur behaviour. Verified locally: the two "add and then remove a tag" tests now pass in ~14s each, having previously hit the 6 minute test timeout, and six of the eight are green. The two Admin-variant failures that remain are a different fault, still being looked at. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/group/view/[id] reads `group.is_member ?? isMember(id)`, and
GET /api/v2/groups/{id} always sends is_member for a logged-in caller, so
a defined `false` wins and the store's optimistic memberIds flip never
reaches the page. Joining or leaving from the group view left the
Join/Leave item unchanged until a reload.
join() and leave() now mirror the flip onto the loaded group and undo it
on the same paths that revert memberIds.
The e2e test caught this but was looking for GroupJoinButton's
group-join-{id}/group-leave-{id}, which the view page never renders - it
uses GroupActions, whose join and leave are dropdown items with fixed
testids, so the menu has to be reopened to see the change.
Also corrects the comment above fetchCurrent, which said the response has
no is_member flag and contradicted the class doc a few lines above.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pages/networks/[id].vue and pages/networks/[id]/edit.vue together make
the former the parent of the latter's route, and [id].vue has no
<NuxtPage/>, so the child never rendered: /networks/{id}/edit served the
network view page instead. The edit screen, and the logo uploader it
owns, could not be reached at all.
Moving the parent to [id]/index.vue gives the two pages sibling routes,
which is what they are.
The test looked for network-logo-manage - which nothing renders - on
/networks/{id}, where there is no upload control even when the page
works. It now opens the edit page and checks the uploader is there and
that the forbidden alert is not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moving pages/networks/[id].vue into [id]/index.vue to unnest the edit route left show.spec.js importing a file that no longer exists, so the whole spec file failed to load and build-client went red - 1568 tests passed and one file never ran. Also fixes the path in a comment in group/view/[id].vue that refers to the same page. Verified: vitest 154 files / 1584 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # resources/js/app.js # resources/js/constants.js # resources/js/mixins/map.js # resources/views/layouts/header.blade.php # resources/views/layouts/header_plain.blade.php
resources/js died at the Nuxt cutover and jest no longer runs on this branch, so the test merged in from develop covers a helper in a file this branch deletes. The client has its own map constants.
#902 keyed the tile URL on the Laravel side, but the client carries its own copy in utils/mapConstants.js - annotated "no API key required", which stopped being true - so every map in the SPA would have come back watermarked once this branch lands. GroupMap, EventVenueMap and GroupForm all read it. useLeafletTiles() appends the key from runtimeConfig.public and returns the bare URL without one, so an unconfigured environment degrades to the watermark rather than a broken map. Runtime config rather than a compiled-in constant for the same reason as the Laravel side: the image is built long before the deployed environment's secrets exist, and Dockerfile.fly already relies on this for apiBase ("runtime-overridable ... so no API URL is baked"). The key falls back to CARTO_API_KEY, which the machine already carries as a Fly secret and Nitro shares the environment with, so no second secret is needed anywhere - production, previews or FLY_PREVIEW_SECRETS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I put the useLeafletTiles import and its call in the middle of the import block, so every import after it tripped import/first and Lint failed with nine errors. Import alongside the other composables, call it once the imports are done. eslint: 0 errors. vitest: 155 files / 1588 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The image upload test drove .uppy-Dashboard-input, but GroupForm renders <TusImageUpload compact> and the compact path skips @uppy/dashboard entirely, so no such input exists - it has its own hidden file input. The tag filter test wants network-show-tag-filter, which nothing renders and legacy never had. The filter bar it needs is real but mounted nowhere: /group/all is now a redirect to /group/map, and that passes neither show-filters nor show-tags. pages/group/all.vue records the gap itself - the full-list filter bar is 887 functionality still to be ported - so skip it with the trail written down rather than build UX over someone's deliberate defaults. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/group/map shows administrators and NCs a groups-requiring-moderation queue above the map, and that is a second GroupsTable. Its rows are deliberately untouched by the map's search box, but the test matched group-row-link- across the whole page, so it demanded moderation rows contain the search term too and failed on the first one. The search was working all along: typing narrowed the list from 7 rows to 4, of which the one in the map's list was the match and the other three were moderation rows. Scope to the map's list, which renders after the moderation panel and is the only table when the viewer isn't a moderator, and assert the filtered list isn't empty so a silently empty list can't pass the loop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
curl followed whatever it was given, so a redirect could have downgraded the Node install script to plain HTTP. --proto '=https' --tlsv1.2 pins it. Flagged by SonarCloud on both the builder and runtime stages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SonarCloud reliability findings, all genuine:
- The four share-stats iframes had no title, so a screen reader announced
them as unlabelled frames. Titled from the headings already above them.
- font-family: Asap and 'Patua One' had no generic fallback. Asap is
already $font-family-third ('Asap', sans-serif) and _footer.scss
already pairs Patua One with serif, so both now match the rest.
- .nav-wrapper .dropdown-menu declared border twice; the first was dead.
- fillCentredText took an x it overwrote on its first line. Removed the
parameter rather than the reassignment, and updated its six callers.
eslint 0 errors, vitest 155 files / 1588 tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SonarCloud reported 56 inputs without a label. 37 were false positives - BFormGroup label-for renders a real <label for>, which its HTML analyser cannot see through - and are marked as such. These 19 were real. Three kinds: - Visible labels that were never associated: GroupNetworkData's per-field BFormGroup and its new-field <label>, and NetworkTagsManager's edit modal. Given label-for/for and matching ids rather than an aria-label, so clicking the label focuses the field. - Placeholder-only search and filter boxes (groups table name/location/ country/network, the map's search, the new-tag fields). A placeholder is not an accessible name - it is announced inconsistently and vanishes on input - so they carry an aria-label with the same wording. - Read-only calendar URLs and the mobile date input, named from the heading each sits under. The attendance stepper's heading sits above it rather than being tied to the number field, so it takes a label prop, passed the same string the heading shows. eslint 0 errors, vitest 155 files / 1588 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nine sortable table headers, collapsible section titles and gallery thumbnails carried role="button" and a click handler but no tabindex and no key handler, so a keyboard or screen-reader user could not reach or activate them - they announced as buttons and then did nothing. Each now takes focus and responds to Enter and Space, matching what the role already promised. Only elements that declare role="button" are touched; real <button>/<BButton> elements are left alone, since they handle both keys natively and would otherwise fire twice. eslint 0 errors, vitest 155 files / 1588 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Groups keep ending up with the same repair event twice - a second host
adds one not knowing the first did, or someone re-submits after fixing a
typo instead of editing what is there.
Before a create, the form asks the API what the group already has around
that date and scores each against the one being posted:
certain same place, same instant
likely same place, same day, within an hour
same name, same day, whatever the time
same name, saved by this group in the last 15 minutes - the
double submit, where the first post landed unseen
possible same place, same day at another time
same instant somewhere else
An online event and one in a room are never compared: whatever else
lines up they are not the same event. "Same day" is the day in the
event's own timezone, so an evening in Auckland and the small hours after
it are correctly different days.
Anything found is shown in a modal with its date, time and place, and
three ways out: edit that event, post anyway because it really is
different, or go back. Advisory throughout - these are heuristics over
text and times, so it never blocks, and a lookup that fails is swallowed
rather than stopping someone posting.
Strings added for en, fr and fr-BE.
TDD: criteria and modal specs written first (23 cases), then the five
covering the form - shows instead of posting, posts on "post anyway",
stays put on cancel, posts when nothing matches, and posts when the check
itself errors. eslint 0 errors, vitest 157 files / 1616 tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
4 tasks
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.