Skip to content

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

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

edwh wants to merge 421 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.

  • Fixometer repair records match develop. Each filter section has a "Clear filters" button. The item one leaves the powered/unpowered tab as it is; the event one puts the to date back to today. The to date starts as today. Once a filter is set, "Download filtered data" sits beside the full download, with the same filters, tab and to date.

  • Group page stats match develop. They come after the events by default; ?stats_layout=compact and split still work. The CO2e card has its "that's like..." comparison and share link beside the figure. There is no "not counting" note, and the events heading is "{group} events".

  • 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

Screenshots of the develop sync

Fixometer: "Download filtered data" beside the full download once a filter is set, and the headline with its space:

fixometer filtered download

The "Clear filters" buttons, with the to date starting as today:

fixometer clear filters

Group page: the stats after the events, achievements beside impact, and the CO2e comparison beside the figure:

group stats

The CO2e card stacked on a narrow screen:

group impact narrow

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
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarQube Cloud

💡 Need a hand with PR review? Try Gitar by Sonar!

edwh and others added 25 commits October 11, 2026 22:29
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>
…y bugs

- Dockerfile.fly: take node from the official node image instead of piping the NodeSource installer to bash; npm ci --ignore-scripts for the client build
- client/docker-entrypoint.sh: npm ci --ignore-scripts
- Playwright configs: write test output to a project-relative dir, not /tmp
- BridgeController: rebuild redirect from allowlisted prefix + RFC3986-restricted suffix
- Associate aria-labels with inputs wrapped in BFormGroup; keyboard handlers / aria-controls on TagMultiselect and CollapsibleSection

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

docker-compose keeps root unless CLIENT_UID/CLIENT_GID is set, so the host bind mount stays writable.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Backend changes come in as they are. Resolutions:
- resources/js stays deleted, including the legacy Jest tests develop changed or added; the frontend changes
  are ported to client/ separately.
- updateEventv2: develop's "only the fields sent" update, keeping the participants/volunteers counters.
- ExportController: keeps writing to a temp file outside the docroot, which is already unique per request.
- lang: develop's new CO2e label ("CO2e emissions prevented"); the strings for retired Blade views stay removed.
- The window.restartersIframe flag is dropped: nothing reads it here, because the leaf embed draws its own canvas
  instead of mounting Vue. OutboundEmbedTest checks that instead.
- ExportFilteredDevicesTest compares with the full export's columns rather than a fixed 13, since this branch's
  export has a Reference column.

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

Ports develop's #924 work to the Nuxt Fixometer:
- Each filter section has a "Clear filters" button. Clearing the item info leaves the powered/unpowered tab as it
  is; clearing the event info puts the to date back to today.
- The to date starts as today, as on develop, because later records are almost always mistyped event dates.
- "Download filtered data" appears beside the full download once a filter is set: /export/devices with the
  search's filters, the tab and the to date, without the paging and sorting.
- The space between the headline and the rest of the sentence is explicit, because the template compiler
  condenses the whitespace between the two elements.

develop's wiki filter has no counterpart on this page yet, so there is nothing for Clear to reset there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ports Neil's develop changes to the group page stats (95b4636):
- The stats go after the events by default; ?stats_layout=compact and split still work. The compact grid puts
  achievements beside impact, over items worked on beside most repaired.
- The CO2e card has its "that's like..." comparison and the share link beside the figure, split by a dashed line,
  stacked below lg.
- The "not counting" note is gone, and so is the unused explanation popover helper.
- The events heading is "{group} events", in sentence case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
groups.and was only used by the "not counting" note, and the CO2e explanation strings only by the popover helper,
both removed with the group stats layout change. translations:check fails on unused keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	resources/js/components/StatsImpact.vue
#	resources/js/components/StatsValue.vue
#	resources/js/mixins/co2equivalent.js
Port of develop's 4b0c3da: the group and event pages show what the trees in the CO2e comparison are,
in a popover from an info icon beside it. Brings back the two explanation strings this branch had removed.

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

Copy link
Copy Markdown

This branch is waiting to be deployed

1 waiting deployment
preview — 5067eb3b Waiting Oct 26, 2026 by edwh via Deploy preview #696
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.

3 participants