Skip to content
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).

## [Unreleased]

## [0.17.2] - 2026-07-24

### 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/*`).
- **Desktop (Windows) and Android release builds** *(CI)*: the v0.17.1 release shipped without the Windows `.exe`/`.msi` and the Android `.apk`. The desktop bundle step used `rsync`, which is not installed on the Windows git-bash runner (`rsync: command not found`), and the Android job's `npm ci` failed on an out-of-sync `mobile/package-lock.json` (missing `@emnapi/*`). The bundle script now uses a **portable `cp`-based copy** (works on Linux, macOS and Windows git-bash), and the mobile lock file is **regenerated in sync** — so v0.17.2 produces the full artifact set (web ZIP + desktop win/mac/linux + apk). Both fixes verified locally.

### Changed
- **Sample data now fills the production planner** *(demo)*: the "Load sample data" seeder used to scatter ~46 work orders one-every-two-days across a 3-month horizon, so any single week of the planner looked nearly empty (~9 orders spread over 5 lines × 7 days × 3 shifts). It now **densely packs the current week** — an order in most (line, day, shift) slots, anchored to `now()->startOfWeek()` so the visible week is always the busy one — with a lighter taper over the neighbouring weeks and last week's work marked DONE. The current week stays **active end-to-end** (in-progress / accepted / pending) so every day renders blocks, including days already behind "today". A freshly seeded demo now opens on a realistic, almost-full weekly board instead of a sparse one. `PrintShopDemoSeeder` only; no schema or app-logic change.

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']);
Comment on lines +21 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Assert tenant isolation, not only response shape.

This test would pass even if the snapshot returned another tenant’s rows or channel. Add a tenant-scoped fixture with two tenants and assert that the authenticated user receives only its tenant’s data, matching CollectionController::index.

As per coding guidelines, new endpoint tests must cover domain edge cases.

🧰 Tools
🪛 PHPStan (2.2.5)

[error] 23-23: Call to an undefined static method App\Models\User::factory().

(staticMethod.notFound)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/Feature/Sync/CollectionSnapshotSessionAuthTest.php` around
lines 21 - 30, Extend test_authenticated_session_gets_the_snapshot with two
tenant fixtures and tenant-specific collection data, associating the
authenticated user with one tenant. Assert the response rows and channel contain
only that tenant’s data, matching CollectionController::index behavior, rather
than checking only the JSON structure.

Source: Coding guidelines

}

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();
}
}
2 changes: 1 addition & 1 deletion desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "openmes-desktop",
"private": true,
"version": "0.17.1",
"version": "0.17.2",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
31 changes: 20 additions & 11 deletions desktop/scripts/bundle-resources.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,26 @@ fi
echo "Kopiuję backend z $BACKEND_SRC → $DEST (bez node_modules/storage/.git)…"
rm -rf "$DEST"
mkdir -p "$DEST"
# vendor JEST kopiowany (wymagany w runtime); node_modules NIE (assety są w public/build).
rsync -a --delete \
--exclude 'node_modules' \
--exclude '.git' \
--exclude 'storage/logs/*' \
--exclude 'storage/framework/cache/*' \
--exclude 'storage/framework/sessions/*' \
--exclude 'storage/framework/views/*' \
--exclude 'storage/installed' \
--exclude '.env' \
"$BACKEND_SRC/" "$DEST/"
# Portable copy — no rsync: it is not installed on the Windows git-bash CI
# runner (the desktop-windows build failed with "rsync: command not found").
# vendor IS copied (needed at runtime); node_modules is NOT (assets ship in
# public/build). Copy each top-level entry except the excluded ones, then prune
# the runtime-only storage state rsync used to skip. cp -R works on Linux,
# macOS and Windows git-bash alike.
shopt -s dotglob nullglob
for item in "$BACKEND_SRC"/*; do
case "$(basename "$item")" in
node_modules|.git|.env) continue ;;
esac
cp -R "$item" "$DEST/"
done
shopt -u dotglob nullglob
rm -rf \
"$DEST/storage/logs/"* \
"$DEST/storage/framework/cache/"* \
"$DEST/storage/framework/sessions/"* \
"$DEST/storage/framework/views/"* \
"$DEST/storage/installed"

echo "Backend: $(du -sh "$DEST" | cut -f1)"
if [ -f "$HERE/src-tauri/resources/php/php" ] || [ -f "$HERE/src-tauri/resources/php/php.exe" ]; then
Expand Down
2 changes: 1 addition & 1 deletion desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "openmes-desktop"
version = "0.17.1"
version = "0.17.2"
description = "OpenMES Desktop — lokalny serwer MES w aplikacji desktopowej"
authors = ["OpenMES"]
edition = "2021"
Expand Down
2 changes: 1 addition & 1 deletion desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "openmes-desktop",
"version": "0.17.1",
"version": "0.17.2",
"identifier": "com.openmes.desktop",
"build": {
"beforeDevCommand": "npm run dev",
Expand Down
Loading
Loading