Skip to content

feat: Nuxt 4 SPA migration - Laravel becomes API-only - #898

Open
edwh wants to merge 411 commits into
developfrom
nuxt-client
Open

edwh wants to merge 411 commits into
developfrom
nuxt-client

Conversation

@edwh

@edwh edwh commented Jul 18, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

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 from lang/*.php. CI falls back to the stub geocoder when no geocoding key is available (forked PRs), and the dev deploy job fails unless the app is actually serving.

  • Groups map and list match develop: filtering the list (name, and tags for people who can manage them) moves the pins and reframes the map once typing stops; the map opens on the user's own area (nearest groups, or the groups in their country when they have only set a country) and the place search is preloaded with it; the list is ordered by distance and badges archived groups.

  • Network page: the Groups section is a map and filterable list of the network's groups; the groups tally counts only non-archived groups; hosts can edit their group's postcode (max 32 characters).

  • Dates formatted with moment follow the active locale (fr, and fr-BE via fr), including after a language switch.

  • Open Repair Data Standard export at GET /api/v2/repairs (Administrator only; 503 until ORDS_ID_PREFIX and ORDS_DATA_PROVIDER are set).

  • Design parity with the live site (G6) — reviewed page-by-page against production and matched:

    • Login: brand .panel/.panel__orange cards with the offset drop-shadow (ported the legacy _panels.scss into the global layer — no component overrides).
    • Dashboard: "Welcome to Restarters" hero + doodles, two-column Your Groups / Upcoming events panel, repair photo, blue info panel, amber "Getting started" call-out, and the restored Add Data group→event picker.
    • Fixometer: the teal stat-card grid + the inline searchable POWERED/UNPOWERED repair-records table with filter collapsibles.
    • CSS consolidation: the dashed content divider promoted to a global .content-divider; brand cards global, only page-specific layout scoped.
    • Assets rationalised to WebP (icons stay SVG); the missing /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.

  • Output escaping and credential rotation: the group and event audit-log endpoints escape the user name, request URL and old/new values before substituting them into the HTML log lines; the group page's unfollow banner escapes the group name; rich-text descriptions are sanitised on write (stevebauman/purify); a password reset clears the recovery code and revokes the user's tokens, and a password change revokes the others instead of minting a recovery code; CSV exports neutralise spreadsheet-formula cells.

Code Quality Review

  • The pinned web-route surface (ApiOnlyRouteSurfaceTest) and per-endpoint OpenAPI response validation guard against surface/shape drift.
  • New endpoints follow v2 conventions (explicit validated field lists — no mass assignment; role/api_token excluded from $fillable; Eloquent bindings — no raw SQL).
  • The Nuxt client is jQuery-free; jQuery survives only in the MediaWiki skin (wiki.js) and the embeddable widget pages' CDN libs — both outside the Vue/Nuxt app.

Test Plan

  • CI (build phpunit, build-client vitest + lint + build, e2e-client Playwright) run on the branch head. Locally: client vitest 1658 tests in 161 files, lint, and the Nuxt build pass.
  • New coverage for every restored/added endpoint and flow (APIv2PublicProfileTest, APIv2DeviceUpdateAuthTest, APIv2EventVisibilityTest, SsoBridgeTest, AuthEndpointsTest, PasswordResetHardeningTest, StoredXssHardeningTest, AuthorizationHardeningTest, DashboardAddData.spec, e2e for dashboard/device/event flows).

Future Improvements

  • Remaining non-API web surface (see 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/v2 endpoint (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 hardening backlog documented in 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.

🤖 Generated with Claude Code

Comment thread app/Http/Controllers/Auth/BridgeController.php Fixed
Comment thread app/Http/Controllers/Auth/BridgeController.php Fixed
Comment thread tests/Feature/Auth/DualGuardTest.php Fixed
Comment thread tests/Feature/Users/APIv2UserEventsTest.php Fixed
Comment thread client/app/stores/devices.js Fixed
@github-actions

github-actions Bot commented Jul 18, 2026 •

Copy link
Copy Markdown

Preview: https://restarters-pr-898.fly.dev

✅ Ready - restore and migrations succeeded.

  • Password-protected test environment (usual dev gate password); commit 8327f4678eaf4d5ec0c0a72966b849b3781c0a1a merged with develop.
  • If you see a "warming up" page, give it a few minutes - it refreshes itself.
  • The preview database is refreshed on every deploy and may reset at any time; anything you create here is disposable.
  • Emails go to shared Mailpit, never to real recipients. Image uploads are disabled.
  • The app suspends when idle; the first request after a pause can take a moment.

Comment thread .circleci/config.yml Fixed
Comment thread .circleci/config.yml Fixed
Comment thread .circleci/config.yml Fixed
Comment thread playwright.client.config.js Fixed
Comment thread client/Dockerfile Fixed
Comment thread playwright.parity.config.js Fixed
fetchEvents and fetchGroups were near-identical (cached-fetch with
loading/error/loaded bookkeeping, differing only in the /moderate endpoint).
Extract a module-level loadSection(store, key, force, request) helper; the two
public actions become thin wrappers. Kept it async with try/catch (not a
.then/.catch chain) so a synchronous throw from request() - e.g. a missing $api
in a test - is still caught, matching the original behaviour.

Moderation + party/group-map/networks specs green; full suite 120 files / 930.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPQW8msJ8eya5XYsqk9B2S
edwh and others added 3 commits July 19, 2026 14:02
…n set

The "Other groups nearby" tab hardcoded hasLocation = true, so it never showed
the legacy "You do not currently have a town/city set..." prompt - it always
rendered the plain "nothing nearby" copy even for a user with no location. Wire
hasLocation to the dashboard's has_location flag (the same source the dashboard
and /party pages use; the nearby endpoint itself carries no such flag), fetching
the dashboard on mount. Defaults true so the prompt never flashes while loading.

+ a spec for the no-location branch. group/nearby specs 7/7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPQW8msJ8eya5XYsqk9B2S
The plain-layout header used justify-content-between; on mobile the impact-stats
bar is hidden (d-none d-md-flex), leaving the logo alone and left-aligned.
Legacy centred it on the register/landing mobile header. Use
justify-content-center on mobile, justify-content-md-between on desktop.
LogoStatsHeader spec 4/4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPQW8msJ8eya5XYsqk9B2S
edwh and others added 7 commits October 8, 2026 16:22
The API already accepts the postcode from hosts, limited to 32 characters.
The form still made the field read-only unless the user could moderate, and
did not cap its length.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Vite only bundles the moment locales that are imported, so moment.locale('fr')
silently stayed English and every date formatted with moment was English for
every user. Import the French locale and keep moment's locale in step with the
active locale, including when the user switches language. fr-BE falls back to
fr.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Picks up develop's new groups.distance key and the networks keys removed with
the network page's count sentence.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…uses

Develop dropped the key with the Blade follow buttons it replaced with the map.
The Nuxt GroupJoinButton still shows the short Unfollow label on small screens.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… nuxt-client

The groups table and network page are Nuxt components on this branch and
already render every row, so the develop-side Vue edits are dropped with the
deleted files; the new groups.show_all_groups lang key is kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ference-nuxt

# Conflicts:
#	app/Http/Controllers/ExportController.php
#	mysql/my.cnf

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread .circleci/config.yml Fixed
Comment thread .circleci/config.yml Fixed
Comment thread .circleci/config.yml Fixed
Comment thread Dockerfile.fly
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
# Install Node 22 (Nuxt 4 / nuxi require Node >=22; matches client/Dockerfile
# node:22 and CI cimg/node:22.16. Vite 4 root build is Node-22-compatible too.)
RUN curl -fsSL --proto '=https' --tlsv1.2 https://deb.nodesource.com/setup_22.x | bash - \
Comment thread Dockerfile.fly
# Node 22 (runtime only) — runs the Nuxt Nitro server that serves the SPA origin
# (client/.output, started by supervisord). The .output targets Node >=22, same
# as the builder. This image only needs the node binary to run it.
RUN curl -fsSL --proto '=https' --tlsv1.2 https://deb.nodesource.com/setup_22.x | bash - \
// Run: task parity:capture
const config = {
testDir: 'client/parity',
outputDir: '/tmp/test-results/parity-output',
Comment thread Dockerfile.fly
# in here. Drop client/node_modules afterwards — .output is self-contained, and
# this keeps the final-stage `COPY /build` from dragging the dev install in.
WORKDIR /build/client
RUN npm ci --legacy-peer-deps
Comment thread client/app/stores/devices.js Fixed
# leaks in. Install when the volume is empty or the lockfile changed.
if [ ! -f node_modules/.package-lock.json ] || ! cmp -s package-lock.json node_modules/.package-lock.json; then
echo "Installing client dependencies..."
npm ci
Comment thread client/Dockerfile
# Nuxt client container. Dev mode (NUXT_DEV_MODE=true, the docker-compose
# default) runs the HMR dev server against the bind-mounted source; otherwise
# it serves a production build — which is what CI's Playwright job tests.
FROM node:22-bookworm-slim
edwh and others added 21 commits October 11, 2026 21:44
When migrate fails, preview-startup.sh now writes the migration that was
running, the exception class and the SQLSTATE and driver code into the public
/_preview_status detail. The exception message is left out because it can quote
restored production values.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nuxt-client

The Vuex stores are deleted on this branch, and the Nuxt stores do not use the
var loop keys the change fixes, so there is nothing to port.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ails

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Backend changes come across: the repair data export service and command, the
stricter events-held count (kept alongside this branch's events_held), the
device search filters, the networks.reporting_url migration and lang keys.
The Vue components, Blade views and NetworkController that #926 edited are
deleted on this branch; porting the reporting links, group stats layouts and
Fixometer changes to the Nuxt client is not part of this merge.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A blank SENTRY_LARAVEL_DSN made Sentry throw InvalidOptionsException while
reporting any exception, which hid the original error behind it. An empty value
now disables reporting instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…without a SQL error

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A blank or whitespace SENTRY_LARAVEL_DSN secret is not a valid DSN, and Sentry
threw InvalidOptionsException from artisan boot, so migrate failed on the
restored preview database. Anything that is not an http(s) URL now means no DSN.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
develop removed years_volunteered and device_breakdown and the merge took its
no_groups_mine, which links to a page that no longer exists here. The keys are
restored with this branch's values and the client translations regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ference-nuxt

# Conflicts:
#	app/Http/Controllers/ExportController.php
#	client/i18n/locales/en.json
#	client/i18n/locales/fr-BE.json
#	client/i18n/locales/fr.json
#	lang/en/devices.php
#	lang/fr-BE/devices.php
#	lang/fr/devices.php

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Group page: ?stats_layout= switches between compact (default), after and
split, the compact layout leaves out the not-counting note, and the cluster
breakdown is gone as on develop. Group members and hosts get a reports link in
the actions menu and under the stats, from a reporting_url on the group
resource that is null for everyone else.

Network page: reports link in the actions menu and under the impact figures;
administrators set the dashboard address on the edit page through a new
PATCH /api/v2/networks/{id}, which coordinators cannot use.

Fixometer: events held and items seen tiles, the fixed-items labels, a
further-reports link, and a download button for everyone that serves the
overnight file when it exists, from download_url and reporting_url on
homepage_data.

The reporting-link, repair-data-file and export tests now go through the API,
the export test reads the response's own temp file, and lang keys nothing uses
on this branch are removed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…filter key test and a lint error

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…eption handling in the 422 and 401 tests

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ference-nuxt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nuxt

feat: optional reference field on devices
Carries the draggable group-location map (#895) into the Nuxt group form,
drops develop's edits to Blade/Vue/web-controller files this branch removes,
and annotates the API controllers for the living-specs check (#835).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…board rejection, drop Math.random from optimistic ids

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
E Security Rating on New Code (required ≥ A)
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

This branch was successfully deployed

1 active deployment
preview — 8327f467 Deployed Oct 14, 2026 by edwh via Deploy preview #610
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

preview Deploy a Fly.io preview for this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants