Skip to content

chore(release): v0.18.0 — onboarding module selection + develop features#208

Merged
jakub-przepiora merged 66 commits into
mainfrom
release/v0.18.0
Jul 24, 2026
Merged

chore(release): v0.18.0 — onboarding module selection + develop features#208
jakub-przepiora merged 66 commits into
mainfrom
release/v0.18.0

Conversation

@jakub-przepiora

@jakub-przepiora jakub-przepiora commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

v0.18.0 (develop → main)

Brings all of develop to main, merged with the 0.17.x hotfixes already on main, as a minor release.

Added (highlights)

Plus the 0.17.x sections (#193 live-sync fix, planner sample data, Windows/apk build fixes, MQTT hotfix) preserved from main.

Integration

Validation (pre-merge)

  • Backend suite: 1973 passed
  • Frontend: build ✓ + vitest 9 passed
  • Playwright E2E: 20 passed (0 failed) on a fresh onboarding-branch instance
  • Desktop version bumped 0.17.2 → 0.18.0

After merge

  1. Merge to main (admin — protection).
  2. Tag v0.18.0 → triggers the release build (web + desktop win/mac/linux + apk).

Closes #202.

🤖 Generated with claude-flow

Summary by CodeRabbit

  • New Features
    • Added onboarding module selection with Lightweight, Advanced, and Custom presets.
    • Added product revision management with draft, released, and obsolete statuses.
    • Added machine-counted work orders and configurable counting sources.
    • Added logistics operator roles, pallet movement recording, and movement history.
    • Added released revision selection to work orders, with production safeguards.
    • Improved navigation visibility based on enabled modules.
  • Documentation
    • Clarified MQTT broker and browser-based WebSocket setup instructions.
  • Release
    • Updated the application version to 0.18.0.

Svannte and others added 30 commits June 23, 2026 10:01
jakub-przepiora and others added 27 commits July 10, 2026 12:21
…and-priority

feat: finish up whole feature
Resolve conflicts from the merged customer-management & priority feature (#173):
- CHANGELOG.md: keep both the read-confirmation and customer/priority entries.
- lang/en.json, lang/pl.json: union of both features' translation keys (parity preserved).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#175)

* feat(workstation): shop-floor device registration + live admin roster

Workstation client PCs self-register against the MAIN app by IP and
heartbeat; Admin → Structure → Workstation Devices shows them live
(online/offline derived from last_seen_at) via an Electric shape.

- Public, rate-limited API: POST /api/workstations/register + /heartbeat
- WorkstationDevice model (soft-delete + audit), migration with a partial
  unique index on device_uuid, factory
- Web admin roster page (ResourceTable) + forget (soft-delete)
- Registered in SoftDeleteRegistry + ShapeRegistry; i18n en/pl parity
- Tests: registration/heartbeat/validation, admin authz + soft-delete

* docs(roadmap): add workflow/UX orchestration items from field feedback

Operator/supervisor/scheduler workflow items surfaced by a high-volume
plant's feedback (unified production+scrap reporting, role-based entry
points, machine-counter reporting, shift board, start-of-shift + multi-day
Gantt scheduling, made-to-order vs stock order entry). Tagged 'probable'.
…187)

Extends the release workflow so a tag/dispatch produces installers for
every platform, all attached to the same GitHub Release:

- web: existing self-contained ZIP (unchanged) — creates the release
- desktop: Tauri matrix (windows .exe/NSIS, macOS .dmg, linux .deb/.rpm),
  each bundling PHP + the backend; PHP runtime fetched per-OS, backend
  built (composer + vite) and injected via bundle-resources + --config
- mobile: Expo prebuild + Gradle assembleRelease -> Android .apk

Jobs fan out from the web job (needs: web) and upload to the release by
tag, so a desktop/mobile failure never blocks the web artifact.
#186)

* feat(desktop): OpenMES Desktop app (Tauri) + unattended install preset

Brings the desktop platform app to develop alongside mobile/. A Tauri
(Rust + webview) shell that bundles PHP + the backend and runs OpenMES as
a local desktop app / LAN MES server, with an in-app self-updater.

- desktop/: Tauri app (src-tauri Rust, TS control panel, icons, build
  scripts for self-contained Windows .exe and Linux .deb/.rpm)
- Backend INSTALLER_PRESET mode: unattended install (skips env/db wizard
  steps, straight to admin creation) that the desktop shell drives —
  InstallController + install views + bootstrap/app.php
- InstallPresetTest covers the preset behaviour

The shop-floor workstation feature lands separately in #175.

* fix(desktop): address CodeRabbit review on #186

- installer: stop leaking raw PDO exception messages to pre-auth visitors
  (log instead); reject path-traversal in sqlite db_database before it hits
  storage_path(); move admin-step validation into a CreateAdminRequest Form
  Request (per project convention)
- tauri: harden config — withGlobalTauri=false + a CSP (permissive only for
  localhost so the served MES app's websockets/SSE keep working)
- panel: AbortController in ensureDbMode so boot retries don't stack duplicate
  DB-choice listeners
- updater: snapshot the SQLite DB before migrating and restore it on rollback,
  so restored old code never runs against the migrated schema
…185)

* feat(work-orders): add elapsed-time "Age" column to work-order lists

Add an "Age" column (now - created_at) to the admin and supervisor work-order
lists, showing a compact human-readable duration (just now / 5m / 3h / 2d / 1y)
with the exact creation time on hover.

- New reusable elapsed() formatter in lib/i18n.js
- Opt-in `live: true` column flag on ResourceTable: a shared 30s clock ticks
  live cells via context, so tables without one schedule no timer
- Opt-in `sortAccessor` column flag so "Age" sorts by actual age (ascending =
  youngest first) while global search keeps using created_at
- Vitest setup covering the duration formatting; wired into CI + pre-commit

created_at was already synced on the work-order Electric shapes, so no schema
or shape change.

Closes #100

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* perf(work-orders): isolate live-clock ticks to a provider component

Move the 30s clock state out of ResourceTable into a small LiveClockProvider
wrapper, so a tick re-renders only the provider and the NowContext consumers
(the live cells) rather than the whole ResourceTable body. Previously the tick
re-ran the live query/filter and handed DataTable freshly allocated props
(visibleRows, the inline rangeLabel), defeating its memoization and reconciling
the entire grid every 30s.

Addresses CodeRabbit review on #185.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…c report ids (#188)

- Remove the WIP `__e2e__` test-control route group and its import: it
  referenced App\Http\Controllers\TestControl\TenantController, the
  EnsureE2eEnabled middleware and config/e2e.php — none of which exist in the
  repo (leaked from 'feat: custom fields wip'). It broke `php artisan
  route:list` and would 500 on /__e2e__/* instead of the intended 404. No spec
  references it; reintroduce complete (routes + controller + middleware + config)
  if the isolated-tenant harness is wanted.
- Constrain reports.show / cost-reports.show to `whereNumber('workOrder')` so a
  non-numeric id (e.g. /admin/reports/net-requirements) 404s instead of hitting
  the controller with a string id and 500ing on an invalid-integer query.
* feat(bom): select multi bom in orders

* fix(refactor): code fixes reported in code review by CodeRabbit

---------

Co-authored-by: jakub-przepiora <jakub.przepioraa@gmail.com>
…pec (#189)

The spec was missing the final `});` closing its `test.describe(...)` block,
so Playwright failed to parse it — and because collection aborts on the first
unparseable file, this broke the ENTIRE E2E suite (0 specs could run). E2E
isn't part of the CI test gate, so it slipped onto develop unnoticed.
Adds the missing close; the file now parses and lists its test.
#197)

* feat: Identification of logistics operators / forklift drivers on physical movements

* fix(code review): fix bugs recived from CodeRabbit
…r snapshot (#180) (#196)

* feat(product): product revision management with lifecycle + work-order snapshot (#180)

* fix(product): register product-revisions routes (missing from prior commit, #180)
…ing (#174) (#198)

* fix(mqtt): stop Live Message Log crashing on first message (recursive formatTime) (#174)

* feat(mqtt): supervise all MQTT connections in one listener, auto-pick up new ones (#174)
… (Structure) and Issues/reason codes (Maintenance) so Lightweight hides them
… -> Maintenance, net-requirements -> Structure) so Lightweight keeps only Work Order History
feat(onboarding): choose modules at first login (Lightweight / Advanced / Custom)
# Conflicts:
#	CHANGELOG.md
#	backend/app/Http/Controllers/Web/Admin/WorkOrderManagementController.php
#	backend/app/Http/Requests/Web/Admin/StoreWorkOrderRequest.php
#	backend/app/Http/Requests/Web/Admin/UpdateWorkOrderRequest.php
#	backend/app/Models/WorkOrder.php
#	backend/app/Services/WorkOrder/WorkOrderService.php
#	backend/app/Support/SoftDeleteRegistry.php
#	backend/app/Sync/ShapeRegistry.php
#	backend/database/seeders/PrintShopDemoSeeder.php
#	backend/lang/en.json
#	backend/lang/pl.json
#	backend/resources/js/Pages/admin/schedule/planner/panels.jsx
#	backend/resources/js/Pages/admin/work-orders/Create.jsx
#	backend/resources/js/Pages/admin/work-orders/Edit.jsx
#	backend/resources/js/Pages/admin/work-orders/WorkOrderForm.jsx
#	backend/resources/js/Pages/admin/work-orders/fields.js
#	backend/resources/js/components/ResourceForm.jsx
#	backend/resources/js/layouts/adminNav.js
#	backend/routes/api.php
#	backend/routes/web.php
#	desktop/package.json
#	desktop/scripts/bundle-resources.sh
#	desktop/src-tauri/Cargo.lock
#	desktop/src-tauri/Cargo.toml
#	desktop/src-tauri/tauri.conf.json
#	mobile/package-lock.json
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The release adds product revision lifecycle management, machine-counted work-order production, logistics pallet movement tracking, granular module onboarding and navigation gating, new synchronization shapes, admin and operator interfaces, migrations, tests, translations, and documentation updates.

Product revision lifecycle

Layer / File(s) Summary
Revision contracts and administration
backend/app/Enums/RevisionLifecycle.php, backend/app/Models/ProductRevision.php, backend/app/Http/Controllers/Web/Admin/ProductRevisionController.php, backend/resources/js/Pages/admin/product-revisions/*
Adds draft, released, and obsolete revision states with CRUD, release, obsolescence, validation, soft deletion, and admin UI flows.
Work-order revision integration
backend/app/Services/WorkOrder/WorkOrderService.php, backend/app/Http/Controllers/Web/Admin/WorkOrderManagementController.php, backend/resources/js/Pages/admin/work-orders/*
Allows released revisions on work orders, snapshots revision metadata, and blocks revision changes after production begins.

Machine-counted production

Layer / File(s) Summary
Counting-source behavior
backend/app/Models/WorkOrder.php, backend/app/Services/WorkOrder/MachineProductionService.php, backend/app/Services/Machine/MachineSignalIngestor.php
Adds operator, machine, and combined counting modes, machine count ingestion, quantity updates, and automatic work-order status transitions.
Connectivity and operator integration
backend/app/Services/Connectivity/ActionExecutor.php, backend/app/Http/Controllers/Web/Operator/WorkstationController.php, backend/resources/js/Pages/operator/Queue.jsx
Routes connectivity quantity actions through the production service and prevents manual quantity entry for machine-counted orders.

Logistics pallet movements

Layer / File(s) Summary
Movement recording
backend/app/Services/Logistics/PalletMovementService.php, backend/app/Http/Controllers/Web/Logistics/PalletMovementController.php, backend/app/Http/Requests/StorePalletMovementRequest.php, backend/resources/js/Pages/logistics/MovePallet.jsx
Adds transactional pallet relocation recording with active logistics-worker and non-shipped-pallet validation.
Movement history
backend/app/Sync/Shapes/PalletMovementsRecentShape.php, backend/resources/js/Pages/admin/pallet-movements/Index.jsx
Synchronizes and displays pallet movements from a rolling ninety-day window.

Module onboarding and navigation

Layer / File(s) Summary
Module selection
backend/app/Support/ModuleRegistry.php, backend/app/Http/Controllers/Web/OnboardingController.php, backend/resources/js/Pages/onboarding/Modules.jsx, backend/resources/js/layouts/OnboardingLayout.jsx
Adds lightweight, advanced, and custom module presets as a new onboarding step and migrates legacy module selections.
Module-gated navigation
backend/app/Support/TabRegistry.php, backend/resources/js/layouts/AppLayout.jsx, backend/resources/js/layouts/adminNav.js
Adds granular tab metadata, longest-prefix tab resolution, and descendant-aware sidebar filtering.

Supporting updates

Layer / File(s) Summary
Release, localization, and documentation
CHANGELOG.md, backend/lang/*.json, docs/mqtt-connectivity.md, desktop/*
Updates release notes, UI translations, MQTT testing guidance, and desktop package versions.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • Mes-Open/OpenMes#196: Overlaps with the product revision lifecycle, work-order integration, and revision snapshotting paths.
  • Mes-Open/OpenMes#197: Overlaps with logistics-worker flags, pallet movement storage, services, routes, UI, and tests.
  • Mes-Open/OpenMes#183: Shares work-order creation and process snapshot integration points.

Suggested reviewers: jankolo04

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the release and key feature set, especially the onboarding module selection work.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/v0.18.0

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jakub-przepiora
jakub-przepiora merged commit 03a1c1c into main Jul 24, 2026
1 of 2 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
backend/app/Support/ModuleRegistry.php (1)

1-1: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Module labels drifted between ModuleRegistry::OPTIONAL and TabRegistry::TABS. Both registries define a human-readable label per module key, and two of them disagree: structure and maintenance show different text depending on which screen renders them (onboarding "Modules" step / Settings → Modules vs. the Access matrix / sidebar).

  • backend/app/Support/ModuleRegistry.php#L58-67: structure is labeled 'Company structure' and maintenance is labeled 'Maintenance & QC'.
  • backend/app/Support/TabRegistry.php#L57-73: structure is labeled 'Structure' and maintenance is labeled 'Maintenance' — align these with ModuleRegistry::OPTIONAL's labels (or vice versa) so the same feature reads identically across screens.
🤖 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/app/Support/ModuleRegistry.php` at line 1, Align the labels for the
structure and maintenance module keys between ModuleRegistry::OPTIONAL and
TabRegistry::TABS so both registries use identical human-readable text across
all screens; update only the mismatched label definitions and preserve the
existing module keys.
backend/app/Services/Connectivity/ActionExecutor.php (2)

224-247: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

setWorkOrderStatus's allowed-status list doesn't match the real WorkOrder status enum.

WorkOrder's status constants are uppercase (STATUS_PENDING = 'PENDING', STATUS_IN_PROGRESS = 'IN_PROGRESS', STATUS_DONE = 'DONE', STATUS_PAUSED = 'PAUSED', STATUS_REJECTED = 'REJECTED'), but this handler validates against ['pending', 'accepted', 'in_progress', 'completed', 'paused', 'rejected']. 'completed' doesn't correspond to any real status at all (should be 'DONE'), and every other entry is the wrong case. Any status this action successfully writes will never equal WorkOrder::STATUS_*, so WorkOrdersActiveShape's terminal-status filter, controller equality checks ($workOrder->status === WorkOrder::STATUS_DONE), and completed_at/side-effect logic elsewhere will silently misbehave for orders driven through this MQTT action.

🐛 Suggested fix
-        $allowed = ['pending', 'accepted', 'in_progress', 'completed', 'paused', 'rejected'];
-        if (! in_array($status, $allowed)) {
+        $status = is_string($status) ? strtoupper($status) : $status;
+        if (! in_array($status, WorkOrder::COUNTING_SOURCES_STATUSES ?? WorkOrder::TERMINAL_STATUSES, true)) {
             throw new \RuntimeException("Invalid work order status: {$status}");
         }

Replace the placeholder constant reference with the actual list of valid WorkOrder::STATUS_* values (e.g. an ALL_STATUSES constant), and normalize incoming values to match.

🤖 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/app/Services/Connectivity/ActionExecutor.php` around lines 224 - 247,
Update setWorkOrderStatus to validate against the actual WorkOrder::STATUS_*
values, preferably via the model’s existing ALL_STATUSES collection, and
normalize resolved or static input to the enum’s uppercase representation before
validation and update. Preserve the existing lookup and response behavior while
replacing the lowercase/incorrect “completed” values so persisted statuses match
WorkOrder constants.

260-278: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Defer or tighten validation for webhook_forward URLs.

This guard is still bypassable: gethostbyname() is IPv4-only, so IPv6-only hosts skip the check, and the request later re-resolves DNS independently, allowing rebinding or redirect targets to be unchecked. Use a resolver that covers both A/AAAA records, fails on any private/reserved/metadata address, and sends the request to the validated IP (or disables/re-validates redirects).

🤖 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/app/Services/Connectivity/ActionExecutor.php` around lines 260 - 278,
Harden the SSRF validation in the webhook URL handling around the current
gethostbyname block: resolve both A and AAAA records, reject any private,
reserved, loopback, or metadata address, and fail closed when resolution is
incomplete or invalid. Ensure the subsequent request uses the validated address
or disables and re-validates redirects so DNS rebinding and redirect targets
cannot bypass validation.
🧹 Nitpick comments (2)
backend/resources/js/Pages/logistics/MovePallet.jsx (1)

52-70: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add aria-pressed to the operator toggle buttons.

Selection state is conveyed only via color; screen reader users get no indication of which operator is currently selected.

♿ Proposed fix
                                     <button
                                         type="button"
                                         key={op.id}
                                         onClick={() => setData('worker_id', op.id)}
+                                        aria-pressed={active}
                                         className={`rounded-om-sm border px-3 py-3 text-left transition-colors ${
🤖 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/resources/js/Pages/logistics/MovePallet.jsx` around lines 52 - 70,
Add an aria-pressed attribute to each operator button rendered in the
operators.map callback, using the existing active boolean so assistive
technologies receive the current selection state while preserving the existing
styling and click behavior.
backend/resources/js/Pages/admin/pallet-movements/Index.jsx (1)

56-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the no-op actions prop.

actions={() => []} is truthy, so ResourceTable will always render an empty "Actions" column header for this read-only history list. Omitting the prop avoids the unused column.

♻️ Proposed fix
             <ResourceTable
                 shape="pallet_movements"
                 title={__('Pallet Movements')}
                 columns={columns}
                 orderBy="moved_at"
                 orderDir="desc"
-                actions={() => []}
                 emptyText={__('No pallet movements recorded yet.')}
             />
🤖 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/resources/js/Pages/admin/pallet-movements/Index.jsx` around lines 56
- 64, Remove the no-op actions prop from the ResourceTable instance in the
pallet movements Index component so the read-only table does not render an empty
Actions column; leave the remaining table configuration unchanged.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@backend/app/Http/Controllers/Web/OnboardingController.php`:
- Around line 42-56: Extract the inline validation from
OnboardingController::storeModules into a dedicated
StoreOnboardingModulesRequest Form Request, preserving the existing preset and
enabled_modules rules and authorization behavior. Update storeModules to
type-hint and use the Form Request, obtaining validated input through its
validated method before calling ModuleRegistry::save.

In `@backend/app/Http/Controllers/Web/Operator/WorkstationController.php`:
- Around line 260-263: Update WorkOrderManagementController::complete() to check
allowsOperatorEntry() before applying the submitted manual quantity or
completing the work order. When the check fails, return back with the existing
machine-counted error message, matching the guard already used in the operator
flow; preserve the current completion behavior for work orders that allow
operator entry.

In `@backend/app/Services/Logistics/PalletMovementService.php`:
- Around line 27-44: Re-check the locked pallet’s shipment status immediately
after `Pallet::...->lockForUpdate()->firstOrFail()` in the transaction before
updating its location or creating the `PalletMovement`. Reject shipped pallets
using the existing domain validation/error mechanism, while preserving the
current move and ledger behavior for eligible pallets.

In `@backend/app/Services/WorkOrder/MachineProductionService.php`:
- Around line 51-104: Make produced_qty writes in recordGoodCount and
recordAbsoluteCount concurrency-safe by wrapping the read-modify-write flow in a
database transaction and locking the work-order row with lockForUpdate before
calculating or persisting the value. Ensure setProducedQty uses the
locked/current model state and preserves the existing status side effects,
preventing parallel updates from overwriting each other.

In `@backend/app/Services/WorkOrder/WorkOrderService.php`:
- Around line 66-93: Update attachRevisionSnapshot() to validate the selected
ProductRevision before persisting its snapshot, matching the defensive
validation performed by resolveBomTemplates(). Reject or throw for missing
revisions, incompatible product_type_id values, and revisions with an invalid
lifecycle_status, rather than silently returning or attaching them; reuse the
existing validation rules or exception behavior from resolveBomTemplates().

In `@backend/app/Support/ModuleRegistry.php`:
- Around line 107-121: Update the docblock for ModuleRegistry::modulesForPreset
to describe that only the 'custom' preset uses the filtered explicit selection,
while unknown preset names fall back to all optional keys. Keep the
implementation unchanged.

In `@backend/app/Sync/CollectionBroadcaster.php`:
- Line 73: Update the pallet_movements entry in CollectionBroadcaster’s shape
membership configuration to use the same WINDOW_DAYS recent-window predicate as
other rolling shapes instead of null. Ensure live membership checks exclude
backdated or otherwise out-of-window ledger rows.

In `@backend/app/Sync/ShapeRegistry.php`:
- Around line 58-61: Update the product_revisions entry in ShapeRegistry to add
the established soft-delete predicate for ProductRevision records, excluding
rows whose deletion timestamp is set while retaining active revisions in the
sync shape. Ensure the index consuming this shape no longer exposes actions for
soft-deleted revisions.

In `@backend/app/Sync/Shapes/PalletMovementsRecentShape.php`:
- Around line 48-53: Update PalletMovementsRecentShape::where to include the
authenticated User’s tenant_id in the server-side predicate alongside the
existing moved_at window, ensuring only that tenant’s pallet_movements are
exposed; otherwise remove this shape from ShapeRegistry.

In `@backend/database/factories/ProductRevisionFactory.php`:
- Around line 25-40: Update the released() and obsolete() states in
ProductRevisionFactory to assign a valid process_template_id matching the
revision’s product type, so both represent releasable lifecycle states. If
retaining null is necessary for specific tests, move that behavior to a
separately named invalid-state helper instead of these states.

In
`@backend/database/migrations/2026_07_19_100001_create_pallet_movements_table.php`:
- Around line 17-24: Update the pallet_id foreign-key definition in the
pallet_movements table migration to preserve movement records when a pallet is
deleted, replacing cascade deletion with the appropriate null-on-delete behavior
and making the column nullable if required. Keep the worker_id relationship and
the ledger’s existing immutable-history design unchanged.

In `@backend/lang/en.json`:
- Around line 4848-4849: Remove the redundant "just now" entry from the
translation object in en.json, preserving the existing declaration elsewhere and
its intended translation.

In `@backend/resources/js/Pages/admin/work-orders/fields.js`:
- Around line 55-67: Update the work-order edit flow and the product revision
field definition around the revision selector to accept a server-authoritative
revisionLocked flag, then disable or omit the product_revision_id selector when
locked, matching the existing BOM picker behavior. Ensure the flag is propagated
from the backend through the edit form rather than derived solely from client
state.

In `@backend/resources/js/Pages/onboarding/Step1.jsx`:
- Line 24: Update the stale Head titles to match the visible onboarding
sequence: in backend/resources/js/Pages/onboarding/Step1.jsx at lines 24-24 use
“Step 2 — Production Line”; in backend/resources/js/Pages/onboarding/Step2.jsx
at lines 24-24 use “Step 3 — Product Type”; in
backend/resources/js/Pages/onboarding/Step3.jsx at lines 80-80 use “Step 4 —
Process Template”; and in backend/resources/js/Pages/onboarding/Step4.jsx at
lines 28-28 use “Step 5 — Work Order”.

In `@backend/tests/Feature/OnboardingWizardTest.php`:
- Around line 271-279: Add a dedicated guest-authorization test alongside
test_modules_step_requires_admin that requests route('onboarding.modules')
without actingAs and asserts the established unauthenticated response, such as a
redirect to login. Keep the existing wrong-role authorization test unchanged.

In `@backend/tests/Feature/Web/Logistics/PalletMovementTest.php`:
- Around line 163-168: The
test_user_without_operator_role_cannot_access_terminal test only covers GET
access; extend it to POST logistics.movements.store with the relevant pallet
movement payload, assert a forbidden response, and verify no movement record is
created and the pallet location remains unchanged.

In `@CHANGELOG.md`:
- Line 15: Fix the nested Markdown code spans in the changelog entry’s
machine-count attribution text: keep the surrounding symbol sequence readable
while ensuring BatchStep is not wrapped in backticks inside an already-open code
span. Preserve the existing wording and links, and ensure all code spans are
properly opened and closed for MD038 compliance.

In `@docs/mqtt-connectivity.md`:
- Around line 17-20: Update the fenced ASCII diagram in the MQTT connectivity
documentation to specify the text fence language, preserving the diagram content
unchanged.

---

Outside diff comments:
In `@backend/app/Services/Connectivity/ActionExecutor.php`:
- Around line 224-247: Update setWorkOrderStatus to validate against the actual
WorkOrder::STATUS_* values, preferably via the model’s existing ALL_STATUSES
collection, and normalize resolved or static input to the enum’s uppercase
representation before validation and update. Preserve the existing lookup and
response behavior while replacing the lowercase/incorrect “completed” values so
persisted statuses match WorkOrder constants.
- Around line 260-278: Harden the SSRF validation in the webhook URL handling
around the current gethostbyname block: resolve both A and AAAA records, reject
any private, reserved, loopback, or metadata address, and fail closed when
resolution is incomplete or invalid. Ensure the subsequent request uses the
validated address or disables and re-validates redirects so DNS rebinding and
redirect targets cannot bypass validation.

In `@backend/app/Support/ModuleRegistry.php`:
- Line 1: Align the labels for the structure and maintenance module keys between
ModuleRegistry::OPTIONAL and TabRegistry::TABS so both registries use identical
human-readable text across all screens; update only the mismatched label
definitions and preserve the existing module keys.

---

Nitpick comments:
In `@backend/resources/js/Pages/admin/pallet-movements/Index.jsx`:
- Around line 56-64: Remove the no-op actions prop from the ResourceTable
instance in the pallet movements Index component so the read-only table does not
render an empty Actions column; leave the remaining table configuration
unchanged.

In `@backend/resources/js/Pages/logistics/MovePallet.jsx`:
- Around line 52-70: Add an aria-pressed attribute to each operator button
rendered in the operators.map callback, using the existing active boolean so
assistive technologies receive the current selection state while preserving the
existing styling and click behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b6694e87-fb92-42b6-8bbc-7fdce32ee308

📥 Commits

Reviewing files that changed from the base of the PR and between 630ce2d and f934641.

⛔ Files ignored due to path filters (1)
  • desktop/src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (77)
  • CHANGELOG.md
  • backend/app/Enums/RevisionLifecycle.php
  • backend/app/Http/Controllers/Web/Admin/ProductRevisionController.php
  • backend/app/Http/Controllers/Web/Admin/WorkOrderManagementController.php
  • backend/app/Http/Controllers/Web/Admin/WorkerController.php
  • backend/app/Http/Controllers/Web/Logistics/PalletMovementController.php
  • backend/app/Http/Controllers/Web/OnboardingController.php
  • backend/app/Http/Controllers/Web/Operator/WorkstationController.php
  • backend/app/Http/Controllers/Web/SettingsController.php
  • backend/app/Http/Requests/StorePalletMovementRequest.php
  • backend/app/Http/Requests/StoreWorkerRequest.php
  • backend/app/Http/Requests/UpdateWorkerRequest.php
  • backend/app/Http/Requests/Web/Admin/StoreProductRevisionRequest.php
  • backend/app/Http/Requests/Web/Admin/StoreWorkOrderRequest.php
  • backend/app/Http/Requests/Web/Admin/UpdateProductRevisionRequest.php
  • backend/app/Http/Requests/Web/Admin/UpdateWorkOrderRequest.php
  • backend/app/Models/PalletMovement.php
  • backend/app/Models/ProductRevision.php
  • backend/app/Models/WorkOrder.php
  • backend/app/Models/Worker.php
  • backend/app/Services/Connectivity/ActionExecutor.php
  • backend/app/Services/Logistics/PalletMovementService.php
  • backend/app/Services/Machine/MachineSignalIngestor.php
  • backend/app/Services/WorkOrder/MachineProductionService.php
  • backend/app/Services/WorkOrder/WorkOrderService.php
  • backend/app/Support/ModuleRegistry.php
  • backend/app/Support/SoftDeleteRegistry.php
  • backend/app/Support/TabRegistry.php
  • backend/app/Sync/CollectionBroadcaster.php
  • backend/app/Sync/ShapeRegistry.php
  • backend/app/Sync/Shapes/PalletMovementsRecentShape.php
  • backend/app/Sync/Shapes/WorkOrdersActiveShape.php
  • backend/database/factories/PalletMovementFactory.php
  • backend/database/factories/ProductRevisionFactory.php
  • backend/database/factories/WorkerFactory.php
  • backend/database/migrations/2026_07_19_100000_add_is_logistics_to_workers_table.php
  • backend/database/migrations/2026_07_19_100001_create_pallet_movements_table.php
  • backend/database/migrations/2026_07_20_120000_add_counting_source_to_work_orders.php
  • backend/database/migrations/2026_07_21_100000_create_product_revisions_table.php
  • backend/database/migrations/2026_07_21_100100_add_product_revision_to_work_orders.php
  • backend/database/migrations/2026_07_23_120000_expand_enabled_modules_for_granular_split.php
  • backend/lang/en.json
  • backend/lang/pl.json
  • backend/resources/js/Pages/admin/pallet-movements/Index.jsx
  • backend/resources/js/Pages/admin/product-revisions/Create.jsx
  • backend/resources/js/Pages/admin/product-revisions/Edit.jsx
  • backend/resources/js/Pages/admin/product-revisions/Index.jsx
  • backend/resources/js/Pages/admin/product-revisions/fields.js
  • backend/resources/js/Pages/admin/schedule/planner/panels.jsx
  • backend/resources/js/Pages/admin/work-orders/Create.jsx
  • backend/resources/js/Pages/admin/work-orders/Edit.jsx
  • backend/resources/js/Pages/admin/work-orders/WorkOrderForm.jsx
  • backend/resources/js/Pages/admin/work-orders/fields.js
  • backend/resources/js/Pages/admin/workers/Create.jsx
  • backend/resources/js/Pages/admin/workers/Edit.jsx
  • backend/resources/js/Pages/admin/workers/WorkerForm.jsx
  • backend/resources/js/Pages/logistics/MovePallet.jsx
  • backend/resources/js/Pages/onboarding/Modules.jsx
  • backend/resources/js/Pages/onboarding/Step1.jsx
  • backend/resources/js/Pages/onboarding/Step2.jsx
  • backend/resources/js/Pages/onboarding/Step3.jsx
  • backend/resources/js/Pages/onboarding/Step4.jsx
  • backend/resources/js/Pages/operator/Queue.jsx
  • backend/resources/js/components/ResourceForm.jsx
  • backend/resources/js/layouts/AppLayout.jsx
  • backend/resources/js/layouts/OnboardingLayout.jsx
  • backend/resources/js/layouts/adminNav.js
  • backend/routes/web.php
  • backend/tests/Feature/Machine/MachineCountToProducedQtyTest.php
  • backend/tests/Feature/OnboardingWizardTest.php
  • backend/tests/Feature/Web/Admin/ProductRevisionTest.php
  • backend/tests/Feature/Web/Logistics/PalletMovementTest.php
  • backend/tests/Feature/Web/ModuleSelectionTest.php
  • desktop/package.json
  • desktop/src-tauri/Cargo.toml
  • desktop/src-tauri/tauri.conf.json
  • docs/mqtt-connectivity.md

Comment on lines +42 to 56
public function storeModules(Request $request)
{
$validated = $request->validate([
'preset' => ['required', Rule::in(['light', 'advanced', 'custom'])],
'enabled_modules' => ['nullable', 'array'],
'enabled_modules.*' => ['string', Rule::in(ModuleRegistry::optionalKeys())],
]);

ModuleRegistry::save(ModuleRegistry::modulesForPreset(
$validated['preset'],
$validated['enabled_modules'] ?? [],
));

return redirect()->route('onboarding.step1');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Move storeModules validation into a Form Request.

storeModules validates inline via $request->validate([...]). The coding guidelines mandate Form Requests for controller validation; this new method reintroduces the pattern already present elsewhere in this controller, so it's a good point to start migrating.

As per coding guidelines, "Validation must use Form Requests, never validate inline in controllers. Frontend validation is UX only; the backend rule set is authoritative."

♻️ Extract into a Form Request
// backend/app/Http/Requests/Web/StoreOnboardingModulesRequest.php
class StoreOnboardingModulesRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true; // gated by the 'onboarding' route group's role:Admin middleware
    }

    public function rules(): array
    {
        return [
            'preset' => ['required', Rule::in(['light', 'advanced', 'custom'])],
            'enabled_modules' => ['nullable', 'array'],
            'enabled_modules.*' => ['string', Rule::in(ModuleRegistry::optionalKeys())],
        ];
    }
}
-    public function storeModules(Request $request)
-    {
-        $validated = $request->validate([
-            'preset' => ['required', Rule::in(['light', 'advanced', 'custom'])],
-            'enabled_modules' => ['nullable', 'array'],
-            'enabled_modules.*' => ['string', Rule::in(ModuleRegistry::optionalKeys())],
-        ]);
+    public function storeModules(StoreOnboardingModulesRequest $request)
+    {
+        $validated = $request->validated();

         ModuleRegistry::save(ModuleRegistry::modulesForPreset(
             $validated['preset'],
             $validated['enabled_modules'] ?? [],
         ));

         return redirect()->route('onboarding.step1');
     }
🤖 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/app/Http/Controllers/Web/OnboardingController.php` around lines 42 -
56, Extract the inline validation from OnboardingController::storeModules into a
dedicated StoreOnboardingModulesRequest Form Request, preserving the existing
preset and enabled_modules rules and authorization behavior. Update storeModules
to type-hint and use the Form Request, obtaining validated input through its
validated method before calling ModuleRegistry::save.

Source: Coding guidelines

Comment on lines +260 to +263
if (! $workOrder->allowsOperatorEntry()) {
return back()->with('error', 'This work order is machine-counted; quantities are recorded automatically from the machine.');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "allowsOperatorEntry" backend/app/Http/Controllers/Web/Admin/WorkOrderManagementController.php
rg -n "function complete" -A 40 backend/app/Http/Controllers/Web/Admin/WorkOrderManagementController.php

Repository: Mes-Open/OpenMes

Length of output: 938


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== WorkOrderController method around lines 245-270 =="
sed -n '245,270pL' backend/app/Http/Controllers/Web/Operator/WorkstationController.php 2>/dev/null || true

echo "== WorkOrderManagementController complete method ="
sed -n '340,380pL' backend/app/Http/Controllers/Web/Admin/WorkOrderManagementController.php

echo "== allowsOperatorEntry definitions/usages =="
rg -n "allowsOperatorEntry|counting_source|machine-counted|recording_method|operator_entry" backend/app backend/ -E '(\.php$|\.blade\.php$)' | head -200

Repository: Mes-Open/OpenMes

Length of output: 321


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg 'backend/app/Http/Controllers/Web/(Operator|Admin)/WorkstationController.php|backend/app/Http/Controllers/Web/Admin/WorkOrderManagementController.php|backend/app/WorkOrder.php|backend/app/Models/WorkOrder.php' || true

echo "== Operator WorkstationController relevant lines =="
wc -l backend/app/Http/Controllers/Web/Operator/WorkstationController.php
sed -n '245,270p' backend/app/Http/Controllers/Web/Operator/WorkstationController.php

echo "== Admin WorkOrderManagementController complete method ==="
wc -l backend/app/Http/Controllers/Web/Admin/WorkOrderManagementController.php
sed -n '330,380p' backend/app/Http/Controllers/Web/Admin/WorkOrderManagementController.php

echo "== allowsOperatorEntry definitions/usages =="
rg -n "allowsOperatorEntry|counting_source|machine-counted|recording_method|operator_entry" backend/app backend/ -g '*.php' -g '*.blade.php' | head -250

Repository: Mes-Open/OpenMes

Length of output: 11548


Guard admin work-order completion by allowsOperatorEntry().

WorkOrderManagementController::complete() still lets an admin overwrite produced_qty and mark a machine-counted work order as done, while only the operator flow blocks manual entry for counting_source === 'machine'. Add the same guard and return the existing machine-counted message before applying the manual quantity.

🤖 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/app/Http/Controllers/Web/Operator/WorkstationController.php` around
lines 260 - 263, Update WorkOrderManagementController::complete() to check
allowsOperatorEntry() before applying the submitted manual quantity or
completing the work order. When the check fails, return back with the existing
machine-counted error message, matching the guard already used in the operator
flow; preserve the current completion behavior for work orders that allow
operator entry.

Comment on lines +27 to +44
return DB::transaction(function () use ($pallet, $operator, $toLocation, $recordedBy, $notes) {
// Lock + re-read so the from_location we capture is the real current
// location even under concurrent moves of the same pallet.
$locked = Pallet::whereKey($pallet->getKey())->lockForUpdate()->firstOrFail();

$fromLocation = $locked->location;
$locked->update(['location' => $toLocation]);

return PalletMovement::create([
'pallet_id' => $locked->id,
'worker_id' => $operator->id,
'from_location' => $fromLocation,
'to_location' => $toLocation,
'moved_at' => now(),
'notes' => $notes,
'performed_by' => $recordedBy?->id,
]);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Pallet eligibility (not-shipped) isn't re-checked under the lock.

StorePalletMovementRequest validates that the pallet isn't Shipped at request-validation time, but this service is the one place that actually locks the row (line 30) — it never re-verifies that status before mutating location and appending the ledger row. A pallet shipped by a concurrent request between validation and this transaction would still get "moved."

🔒️ Proposed fix
+use App\Enums\PalletStatus;
+
 class PalletMovementService
 {
     public function record(
         Pallet $pallet,
         Worker $operator,
         string $toLocation,
         ?User $recordedBy = null,
         ?string $notes = null,
     ): PalletMovement {
         return DB::transaction(function () use ($pallet, $operator, $toLocation, $recordedBy, $notes) {
             $locked = Pallet::whereKey($pallet->getKey())->lockForUpdate()->firstOrFail();
+
+            if ($locked->status === PalletStatus::Shipped->value) {
+                throw new \RuntimeException('Pallet has already been shipped.');
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return DB::transaction(function () use ($pallet, $operator, $toLocation, $recordedBy, $notes) {
// Lock + re-read so the from_location we capture is the real current
// location even under concurrent moves of the same pallet.
$locked = Pallet::whereKey($pallet->getKey())->lockForUpdate()->firstOrFail();
$fromLocation = $locked->location;
$locked->update(['location' => $toLocation]);
return PalletMovement::create([
'pallet_id' => $locked->id,
'worker_id' => $operator->id,
'from_location' => $fromLocation,
'to_location' => $toLocation,
'moved_at' => now(),
'notes' => $notes,
'performed_by' => $recordedBy?->id,
]);
});
use App\Enums\PalletStatus;
return DB::transaction(function () use ($pallet, $operator, $toLocation, $recordedBy, $notes) {
// Lock + re-read so the from_location we capture is the real current
// location even under concurrent moves of the same pallet.
$locked = Pallet::whereKey($pallet->getKey())->lockForUpdate()->firstOrFail();
if ($locked->status === PalletStatus::Shipped->value) {
throw new \RuntimeException('Pallet has already been shipped.');
}
$fromLocation = $locked->location;
$locked->update(['location' => $toLocation]);
return PalletMovement::create([
'pallet_id' => $locked->id,
'worker_id' => $operator->id,
'from_location' => $fromLocation,
'to_location' => $toLocation,
'moved_at' => now(),
'notes' => $notes,
'performed_by' => $recordedBy?->id,
]);
});
🧰 Tools
🪛 PHPStan (2.2.5)

[error] 30-30: Call to an undefined static method App\Models\Pallet::whereKey().

(staticMethod.notFound)


[error] 35-35: Call to an undefined static method App\Models\PalletMovement::create().

(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/app/Services/Logistics/PalletMovementService.php` around lines 27 -
44, Re-check the locked pallet’s shipment status immediately after
`Pallet::...->lockForUpdate()->firstOrFail()` in the transaction before updating
its location or creating the `PalletMovement`. Reject shipped pallets using the
existing domain validation/error mechanism, while preserving the current move
and ledger behavior for eligible pallets.

Comment on lines +51 to +104
public function recordGoodCount(WorkOrder $workOrder, float $delta): bool
{
if ($delta <= 0 || ! $workOrder->isMachineCounted()) {
return false;
}

$this->setProducedQty($workOrder, (float) $workOrder->produced_qty + $delta);

return true;
}

/**
* Set produced_qty to an absolute machine-reported value, honouring
* counting_source. Used by the legacy MQTT path when a mapping reports the
* cumulative total rather than a delta.
*/
public function recordAbsoluteCount(WorkOrder $workOrder, float $value): bool
{
if (! $workOrder->isMachineCounted()) {
return false;
}

$this->setProducedQty($workOrder, $value);

return true;
}

/**
* Persist a new produced_qty and mirror the operator flow's status side
* effects: auto-start a not-yet-started order, auto-complete once produced
* reaches planned. Uses update() (not increment()) so model events fire and
* the Electric shape broadcast happens automatically.
*/
private function setProducedQty(WorkOrder $workOrder, float $newProduced): void
{
if (in_array($workOrder->status, WorkOrder::TERMINAL_STATUSES, true)) {
return;
}

$newProduced = max(0.0, $newProduced);
$planned = (float) $workOrder->planned_qty;
$updates = ['produced_qty' => $newProduced];

if (in_array($workOrder->status, [WorkOrder::STATUS_PENDING, WorkOrder::STATUS_ACCEPTED], true)) {
$updates['status'] = WorkOrder::STATUS_IN_PROGRESS;
}

if ($planned > 0 && $newProduced >= $planned) {
$updates['status'] = WorkOrder::STATUS_DONE;
$updates['completed_at'] = now();
}

$workOrder->update($updates);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
fd -a MachineProductionService.php . || true

echo "== outline =="
if [ -f backend/app/Services/WorkOrder/MachineProductionService.php ]; then
  ast-grep outline backend/app/Services/WorkOrder/MachineProductionService.php || true
  echo "== relevant source =="
  cat -n backend/app/Services/WorkOrder/MachineProductionService.php | sed -n '1,160p'
fi

echo "== usages =="
rg -n "MachineProductionService|recordGoodCount|recordAbsoluteCount|setProducedQty|produced_qty|isMachineCounted" backend -S || true

Repository: Mes-Open/OpenMes

Length of output: 43223


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== MachineSignalIngestor =="
ast-grep outline backend/app/Services/Machine/MachineSignalIngestor.php || true
cat -n backend/app/Services/Machine/MachineSignalIngestor.php | sed -n '1,240p'

echo "== ActionExecutor relevant =="
cat -n backend/app/Services/Connectivity/ActionExecutor.php | sed -n '1,230p'

echo "== WorkOrder counters relevant =="
cat -n backend/app/Models/WorkOrder.php | sed -n '20,40p;240,270p;380,450p'

echo "== route/controller ingestion relevant =="
rg -n "MachineSignalIngestor|machine.*signal|signal.*ingest|good_count|ProducedQuantity|ProducedQuantityReport|recordGoodCount|recordAbsoluteCount" backend/routes backend/app -S || true

Repository: Mes-Open/OpenMes

Length of output: 25867


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== MachineGatewayController =="
cat -n backend/app/Http/Controllers/Api/V1/MachineGatewayController.php | sed -n '1,240p'

echo "== Modbus poll command =="
cat -n backend/app/Console/Commands/ModbusPollCommand.php | sed -n '1,220p'

echo "== TopicMapping/MachineTopic references =="
rg -n "executeAll|MachineTopic|TopicMapping|action_type|update_work_order_qty|qty_increment" backend -S || true

Repository: Mes-Open/OpenMes

Length of output: 21618


Lock produced_qty updates or make them atomic.

This service is the shared entry point for machine counter deltas and MQTT-workorder-quantity mappings. recordGoodCount reads the currently loaded $workOrder->produced_qty, adds $delta, then update()s; the absolute path also re-fetches and writes. Without a row lock/transaction around that read-modify-write, parallel readings for the same work order can overlap and one update can overwrite the other, losing produced quantity. Use an atomic update/LockForUpdate path for the delta/absolute write.

🤖 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/app/Services/WorkOrder/MachineProductionService.php` around lines 51
- 104, Make produced_qty writes in recordGoodCount and recordAbsoluteCount
concurrency-safe by wrapping the read-modify-write flow in a database
transaction and locking the work-order row with lockForUpdate before calculating
or persisting the value. Ensure setProducedQty uses the locked/current model
state and preserves the existing status side effects, preventing parallel
updates from overwriting each other.

Comment on lines +66 to +93
/**
* Merge an immutable `revision` block into the process snapshot for the given
* product revision (#180). Returns the snapshot unchanged when no revision is
* selected; initialises an array snapshot when the order has no BOM/template.
*/
private function attachRevisionSnapshot(?array $snapshot, ?int $revisionId): ?array
{
if (! $revisionId) {
return $snapshot;
}

$revision = \App\Models\ProductRevision::find($revisionId);
if (! $revision) {
return $snapshot;
}

$snapshot ??= [];
$snapshot['revision'] = [
'revision_id' => $revision->id,
'revision_code' => $revision->revision_code,
'lifecycle_at_release' => $revision->lifecycle_status?->value,
'process_template_id' => $revision->process_template_id,
'released_at' => $revision->released_at?->toIso8601String(),
'snapshotted_at' => now()->toIso8601String(),
];

return $snapshot;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

attachRevisionSnapshot() doesn't validate the revision like its BOM sibling does.

Unlike resolveBomTemplates(), which throws when a selected BOM id is invalid or belongs to another product type, attachRevisionSnapshot() silently accepts any ProductRevision::find($revisionId) result — regardless of lifecycle_status or product_type_id match — and it gets persisted onto the work order. Currently guarded only by HTTP request validation; a non-HTTP caller of this public service method (or a future request-rule change) would bypass that.

🛡️ Suggested defensive check
     private function attachRevisionSnapshot(?array $snapshot, ?int $revisionId): ?array
     {
         if (! $revisionId) {
             return $snapshot;
         }

-        $revision = \App\Models\ProductRevision::find($revisionId);
-        if (! $revision) {
+        $revision = \App\Models\ProductRevision::find($revisionId);
+        if (! $revision || ! $revision->isReleased()) {
             return $snapshot;
         }
🧰 Tools
🪛 PHPStan (2.2.5)

[error] 77-77: Call to an undefined static method App\Models\ProductRevision::find().

(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/app/Services/WorkOrder/WorkOrderService.php` around lines 66 - 93,
Update attachRevisionSnapshot() to validate the selected ProductRevision before
persisting its snapshot, matching the defensive validation performed by
resolveBomTemplates(). Reject or throw for missing revisions, incompatible
product_type_id values, and revisions with an invalid lifecycle_status, rather
than silently returning or attaching them; reuse the existing validation rules
or exception behavior from resolveBomTemplates().

<>
<Head title={__('Step 1 — Production Line')} />
<div className="font-mono text-[9.5px] uppercase tracking-[0.08em] text-om-faint mb-2">Step 1/4</div>
<div className="font-mono text-[9.5px] uppercase tracking-[0.08em] text-om-faint mb-2">Step 2/5</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Update the stale browser-title step numbers. The visible counters reflect the five-step flow, but each page’s <Head> title still uses its former number.

  • backend/resources/js/Pages/onboarding/Step1.jsx#L24-L24: change the title to Step 2 — Production Line.
  • backend/resources/js/Pages/onboarding/Step2.jsx#L24-L24: change the title to Step 3 — Product Type.
  • backend/resources/js/Pages/onboarding/Step3.jsx#L80-L80: change the title to Step 4 — Process Template.
  • backend/resources/js/Pages/onboarding/Step4.jsx#L28-L28: change the title to Step 5 — Work Order.
📍 Affects 4 files
  • backend/resources/js/Pages/onboarding/Step1.jsx#L24-L24 (this comment)
  • backend/resources/js/Pages/onboarding/Step2.jsx#L24-L24
  • backend/resources/js/Pages/onboarding/Step3.jsx#L80-L80
  • backend/resources/js/Pages/onboarding/Step4.jsx#L28-L28
🤖 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/resources/js/Pages/onboarding/Step1.jsx` at line 24, Update the stale
Head titles to match the visible onboarding sequence: in
backend/resources/js/Pages/onboarding/Step1.jsx at lines 24-24 use “Step 2 —
Production Line”; in backend/resources/js/Pages/onboarding/Step2.jsx at lines
24-24 use “Step 3 — Product Type”; in
backend/resources/js/Pages/onboarding/Step3.jsx at lines 80-80 use “Step 4 —
Process Template”; and in backend/resources/js/Pages/onboarding/Step4.jsx at
lines 28-28 use “Step 5 — Work Order”.

Comment on lines +271 to +279
public function test_modules_step_requires_admin(): void
{
$operator = User::factory()->create();
$operator->assignRole('Operator');

$this->actingAs($operator)
->get(route('onboarding.modules'))
->assertForbidden();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add a guest-authorization case for the new modules step.

test_modules_step_requires_admin only covers the wrong-role case; there's no assertion that an unauthenticated request to onboarding.modules is rejected (e.g., redirected to login), which the test-coverage guideline calls for alongside the wrong-role case.

As per coding guidelines, "Tests are mandatory for new endpoints/business logic: happy path, validation 422, authorization (guest + wrong role), domain edge cases."

✅ Add a guest case
     public function test_modules_step_requires_admin(): void
     {
         $operator = User::factory()->create();
         $operator->assignRole('Operator');

         $this->actingAs($operator)
             ->get(route('onboarding.modules'))
             ->assertForbidden();
     }
+
+    public function test_modules_step_requires_authentication(): void
+    {
+        $this->get(route('onboarding.modules'))
+            ->assertRedirect(route('login'));
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function test_modules_step_requires_admin(): void
{
$operator = User::factory()->create();
$operator->assignRole('Operator');
$this->actingAs($operator)
->get(route('onboarding.modules'))
->assertForbidden();
}
public function test_modules_step_requires_admin(): void
{
$operator = User::factory()->create();
$operator->assignRole('Operator');
$this->actingAs($operator)
->get(route('onboarding.modules'))
->assertForbidden();
}
public function test_modules_step_requires_authentication(): void
{
$this->get(route('onboarding.modules'))
->assertRedirect(route('login'));
}
🧰 Tools
🪛 PHPStan (2.2.5)

[error] 273-273: 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/OnboardingWizardTest.php` around lines 271 - 279, Add a
dedicated guest-authorization test alongside test_modules_step_requires_admin
that requests route('onboarding.modules') without actingAs and asserts the
established unauthenticated response, such as a redirect to login. Keep the
existing wrong-role authorization test unchanged.

Source: Coding guidelines

Comment on lines +163 to +168
public function test_user_without_operator_role_cannot_access_terminal(): void
{
$plain = User::factory()->create(); // no roles

$this->actingAs($plain)->get(route('logistics.move-pallet'))->assertForbidden();
}

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 | 🟠 Major | ⚡ Quick win

Cover wrong-role access to the movement POST endpoint.

This only proves a roleless user cannot open the terminal; add a forbidden POST to logistics.movements.store and assert that no movement is written and the pallet location is unchanged.

Proposed test addition
 public function test_user_without_operator_role_cannot_access_terminal(): void
 {
     $plain = User::factory()->create(); // no roles
+    $worker = Worker::factory()->logistics()->create();
+    $pallet = Pallet::factory()->create(['location' => 'A-01']);
 
     $this->actingAs($plain)->get(route('logistics.move-pallet'))->assertForbidden();
+
+    $this->actingAs($plain)->post(route('logistics.movements.store'), [
+        'pallet_id' => $pallet->id,
+        'worker_id' => $worker->id,
+        'to_location' => 'B-01',
+    ])->assertForbidden();
+
+    $this->assertDatabaseCount('pallet_movements', 0);
+    $this->assertSame('A-01', $pallet->fresh()->location);
 }

As per coding guidelines, “Tests are mandatory for new endpoints/business logic: happy path, validation 422, authorization (guest + wrong role), domain edge cases.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function test_user_without_operator_role_cannot_access_terminal(): void
{
$plain = User::factory()->create(); // no roles
$this->actingAs($plain)->get(route('logistics.move-pallet'))->assertForbidden();
}
public function test_user_without_operator_role_cannot_access_terminal(): void
{
$plain = User::factory()->create(); // no roles
$worker = Worker::factory()->logistics()->create();
$pallet = Pallet::factory()->create(['location' => 'A-01']);
$this->actingAs($plain)->get(route('logistics.move-pallet'))->assertForbidden();
$this->actingAs($plain)->post(route('logistics.movements.store'), [
'pallet_id' => $pallet->id,
'worker_id' => $worker->id,
'to_location' => 'B-01',
])->assertForbidden();
$this->assertDatabaseCount('pallet_movements', 0);
$this->assertSame('A-01', $pallet->fresh()->location);
}
🧰 Tools
🪛 PHPStan (2.2.5)

[error] 165-165: 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/Web/Logistics/PalletMovementTest.php` around lines 163
- 168, The test_user_without_operator_role_cannot_access_terminal test only
covers GET access; extend it to POST logistics.movements.store with the relevant
pallet movement payload, assert a forbidden response, and verify no movement
record is created and the pallet location remains unchanged.

Source: Coding guidelines

Comment thread CHANGELOG.md
### Added
- **Module selection at first-login onboarding (Lightweight / Advanced / Custom)**: the setup wizard now opens with a **"Choose your modules"** step so a new admin turns on only the feature areas they need instead of getting the full menu. Three one-click choices over the existing module system: **Lightweight** (core production tracking + the **Work-Order History** report only — recommended for small shops), **Advanced** (adds reporting, materials & tracing, product engineering, companies, quality, maintenance, connectivity and packaging — the shop-floor operations set), or **Custom** (tick exactly which optional modules to enable). Core areas (Dashboard, Orders, Production essentials, Admin) are always on. The choice writes to the existing `system_settings.enabled_modules` via `ModuleRegistry`, so disabled areas are hidden from the nav and their routes 404 — and it's **changeable anytime** in Settings → System → Modules. New onboarding **Modules** step (now step 1 of 5) with `ModuleRegistry::PRESETS` + `modulesForPreset()`; reuses `TabRegistry`/`TabAccessMiddleware`.
The optional-module catalog is now **fine-grained** so an install can keep, say, **Materials** without **Companies**, or the **Work-Order History** report without the **cost/scrap/non-conformance analytics**. Twelve independent toggles — **Reports**, **Advanced reports**, **Materials & tracing**, **Product engineering**, **Companies**, **Issues & reasons**, **Company structure**, **HR** (incl. employee scheduling), **Maintenance & QC**, **Connectivity**, **Packaging**, **Webhooks** — each maps to its own `TabRegistry` tab and role-access-matrix row, even when its pages render under the (core) Production or Reports nav groups. Existing installs that had explicitly customised their module set are backfilled on upgrade so previously-core areas stay visible (`2026_07_23_120000_expand_enabled_modules_for_granular_split`); installs that never customised default to all-on and are unaffected.
- **Machine counter signals drive work-order progress** *([#46](https://github.com/Mes-Open/OpenMes/issues/46))*: a work order now carries a **counting source** — **Operator (manual)**, **Machine (automatic)** or **Both** — chosen on the admin/supervisor create & edit form (default **Operator**, so existing orders are unchanged). On a **machine-counted** order, good-count deltas from the Modbus / OPC UA / MQTT **signal pipeline** now flow through to `work_orders.produced_qty`: the count is attributed to the order via the **batch step currently in progress at the reporting workstation** (`MachineTag → workstation → in-progress `BatchStep` → batch → work order`), and the same **auto-start / auto-complete** side effects fire as for operator entry. The legacy MQTT topic-mapping path (`ActionExecutor::updateWorkOrderQty`) and the new pipeline now funnel through one shared `MachineProductionService`, which **honours the counting source** — so a machine mapping and operator entry can no longer both add to the same order (the **double-count** #46 described). On a machine-counted order the operator's manual **complete / shift-entry** is blocked (the machine is the single source of truth), and the operator queue shows a **"Machine" badge**; reject counts stay in the event log for now (scrap wiring is a follow-up). New `work_orders.counting_source` column (nullable, defaults to `operator`); `App\Services\WorkOrder\MachineProductionService`; `counting_source` added to the work-order Electric shapes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the nested Markdown code spans.

The BatchStep backticks are nested inside a larger code span, so the rendered changelog is malformed and triggers MD038.

Proposed fix
-(`MachineTag → workstation → in-progress `BatchStep` → batch → work order`)
+(`MachineTag → workstation → in-progress BatchStep → batch → work order`)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **Machine counter signals drive work-order progress** *([#46](https://github.com/Mes-Open/OpenMes/issues/46))*: a work order now carries a **counting source** — **Operator (manual)**, **Machine (automatic)** or **Both** — chosen on the admin/supervisor create & edit form (default **Operator**, so existing orders are unchanged). On a **machine-counted** order, good-count deltas from the Modbus / OPC UA / MQTT **signal pipeline** now flow through to `work_orders.produced_qty`: the count is attributed to the order via the **batch step currently in progress at the reporting workstation** (`MachineTag → workstation → in-progress `BatchStep` → batch → work order`), and the same **auto-start / auto-complete** side effects fire as for operator entry. The legacy MQTT topic-mapping path (`ActionExecutor::updateWorkOrderQty`) and the new pipeline now funnel through one shared `MachineProductionService`, which **honours the counting source** — so a machine mapping and operator entry can no longer both add to the same order (the **double-count** #46 described). On a machine-counted order the operator's manual **complete / shift-entry** is blocked (the machine is the single source of truth), and the operator queue shows a **"Machine" badge**; reject counts stay in the event log for now (scrap wiring is a follow-up). New `work_orders.counting_source` column (nullable, defaults to `operator`); `App\Services\WorkOrder\MachineProductionService`; `counting_source` added to the work-order Electric shapes.
**Machine counter signals drive work-order progress** *([`#46`](https://github.com/Mes-Open/OpenMes/issues/46))*: a work order now carries a **counting source** — **Operator (manual)**, **Machine (automatic)** or **Both** — chosen on the admin/supervisor create & edit form (default **Operator**, so existing orders are unchanged). On a **machine-counted** order, good-count deltas from the Modbus / OPC UA / MQTT **signal pipeline** now flow through to `work_orders.produced_qty`: the count is attributed to the order via the **batch step currently in progress at the reporting workstation** (`MachineTag → workstation → in-progress BatchStep → batch → work order`), and the same **auto-start / auto-complete** side effects fire as for operator entry. The legacy MQTT topic-mapping path (`ActionExecutor::updateWorkOrderQty`) and the new pipeline now funnel through one shared `MachineProductionService`, which **honours the counting source** — so a machine mapping and operator entry can no longer both add to the same order (the **double-count** `#46` described). On a machine-counted order the operator's manual **complete / shift-entry** is blocked (the machine is the single source of truth), and the operator queue shows a **"Machine" badge**; reject counts stay in the event log for now (scrap wiring is a follow-up). New `work_orders.counting_source` column (nullable, defaults to `operator`); `App\Services\WorkOrder\MachineProductionService`; `counting_source` added to the work-order Electric shapes.
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 15-15: Spaces inside code span elements

(MD038, no-space-in-code)


[warning] 15-15: Spaces inside code span elements

(MD038, no-space-in-code)

🤖 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 `@CHANGELOG.md` at line 15, Fix the nested Markdown code spans in the changelog
entry’s machine-count attribution text: keep the surrounding symbol sequence
readable while ensuring BatchStep is not wrapped in backticks inside an
already-open code span. Preserve the existing wording and links, and ensure all
code spans are properly opened and closed for MD038 compliance.

Source: Linters/SAST tools

Comment thread docs/mqtt-connectivity.md
Comment on lines +17 to +20
```
[ machine / MQTTX / any publisher ] ──publish──▶ [ MQTT broker ] ◀──subscribe── [ OpenMES (php artisan mqtt:listen) ]
(Mosquitto)
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Specify the diagram fence language.

The ASCII diagram uses an untyped fenced block, triggering MD040.

Proposed fix
-```
+```text
 [ machine / MQTTX / any publisher ] ──publish──▶ [ MQTT broker ] ◀──subscribe── [ OpenMES  (php artisan mqtt:listen) ]
                                                  (Mosquitto)

</details>

<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion

🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 17-17: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@docs/mqtt-connectivity.md` around lines 17 - 20, Update the fenced ASCII
diagram in the MQTT connectivity documentation to specify the text fence
language, preserving the diagram content unchanged.

Source: Linters/SAST tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants