Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).

## [Unreleased]

### Fixed
- **Data saved but admin lists show nothing** *([#193](https://github.com/orgs/Mes-Open/discussions/193))*: on some deployments the admin list pages (work orders, lines, product types, …) rendered **empty even though the data existed** — you could pick a line in the work-order form and save, the planner showed sample data, but the lists stayed blank. The lists are fed by a live-sync snapshot (`GET /api/collections/{name}`) that lived in the **api route group** and authenticated via Sanctum **stateful-domain matching** (Origin/Referer vs the app host). Behind a reverse proxy, or when `APP_URL` didn't cover the host the app was actually served on, that check failed → the snapshot returned **401** → the frontend **silently swallowed the error into an empty list** while the rest of the SPA (which uses the session cookie directly) kept working. The snapshot now lives in the **web route group**, so it authenticates with the **plain session cookie** exactly like every Inertia page — host-independent, no `SANCTUM_STATEFUL_DOMAINS` tuning required. The client also now **logs a console warning** when a snapshot fails instead of failing silently. No API contract or URL change (`GET /api/collections/{name}` is unchanged); mobile/token clients are unaffected (they use `/api/v1/*`).

## [0.17.1] - 2026-07-23

### Fixed
Expand Down
7 changes: 5 additions & 2 deletions backend/resources/js/lib/realtimeCollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,11 @@ export function realtimeCollection(name, getKey = (row) => row.id) {
if (!alive) return;
if (!channelName && channel) subscribe(channel);
apply(rows);
} catch {
// leave whatever we have; don't hang the UI
} catch (e) {
// Leave whatever we have; don't hang the UI. Surface the
// reason so a failed snapshot (e.g. a 401 auth issue) is
// diagnosable instead of a silently empty list. #193
console.warn(`[live-sync] snapshot for "${name}" failed:`, e?.message ?? e);
} finally {
if (alive) markReady();
}
Expand Down
8 changes: 3 additions & 5 deletions backend/routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,9 @@
->middleware('throttle:120,1')
->name('api.workstations.heartbeat');

// Reverb sync: initial snapshot for a collection (live deltas arrive via the
// CollectionChanged broadcast on the channel).
Route::get('/collections/{name}', [\App\Http\Controllers\Api\CollectionController::class, 'index'])
->middleware('auth:web,sanctum')
->name('api.collections.index');
// NOTE: the live-sync snapshot GET /api/collections/{name} lives in routes/web.php
// so it authenticates via the session cookie (host-independent) instead of Sanctum
// stateful-domain matching. See the "#193" comment there.

// Authentication routes (no auth required)
Route::prefix('auth')->group(function () {
Expand Down
9 changes: 9 additions & 0 deletions backend/routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,15 @@
// Logout
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');

// SPA live-sync snapshot — initial rows for a Reverb-synced collection (live
// deltas then arrive on the channel). Served in the *web* group so it
// authenticates via the plain session cookie, exactly like every Inertia
// page. The api-group equivalent relied on Sanctum stateful-domain matching,
// which can fail behind a reverse proxy or on a host APP_URL doesn't cover —
// leaving admin lists silently empty while the rest of the SPA works. #193
Route::get('/api/collections/{name}', [\App\Http\Controllers\Api\CollectionController::class, 'index'])
->name('collections.index');

// Maintenance upcoming count (for reminder polling — all authenticated users)
Route::get('/maintenance/upcoming-count', function () {
$count = \App\Models\MaintenanceEvent::whereIn('status', ['pending', 'in_progress'])
Expand Down
46 changes: 46 additions & 0 deletions backend/tests/Feature/Sync/CollectionSnapshotSessionAuthTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Tests\Feature\Sync;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

/**
* The live-sync snapshot GET /api/collections/{name} feeds every admin list
* (ResourceTable). It must authenticate via the plain web session — the same
* cookie every Inertia page uses — so it works on any host / behind any reverse
* proxy, without depending on Sanctum stateful-domain matching. Regression guard
* for #193 ("data saved but lists show nothing"): the snapshot 401'd on hosts
* APP_URL didn't cover, and the frontend swallowed the error into an empty list.
*/
class CollectionSnapshotSessionAuthTest extends TestCase
{
use RefreshDatabase;

public function test_authenticated_session_gets_the_snapshot(): void
{
$user = User::factory()->create();

// No Origin / Referer / stateful-domain set — pure session auth, as it
// would be for any host the deployment is actually served on.
$this->actingAs($user)
->getJson('/api/collections/issue_types')
->assertOk()
->assertJsonStructure(['rows', 'channel', 'at']);
}

public function test_guest_is_rejected(): void
{
$this->getJson('/api/collections/issue_types')->assertUnauthorized();
}

public function test_unknown_collection_is_not_found(): void
{
$user = User::factory()->create();

$this->actingAs($user)
->getJson('/api/collections/definitely-not-a-shape')
->assertNotFound();
}
}
Loading