chore(release): v0.18.0 — onboarding module selection + develop features#208
Conversation
…and restored complete vi.json)
…ipt fail-fast, and e2e spec safety
…t createBtn helper mismatches
…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>
feat: accepnt informations
feat: required operators
…#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
… (Structure) and Issues/reason codes (Maintenance) so Lightweight hides them
… -> Maintenance, net-requirements -> Structure) so Lightweight keeps only Work Order History
…Reports/Structure/Maintenance)
…ngs + backward-compat backfill
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
📝 WalkthroughWalkthroughChangesThe 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
Machine-counted production
Logistics pallet movements
Module onboarding and navigation
Supporting updates
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 winModule labels drifted between
ModuleRegistry::OPTIONALandTabRegistry::TABS. Both registries define a human-readable label per module key, and two of them disagree:structureandmaintenanceshow 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:structureis labeled'Company structure'andmaintenanceis labeled'Maintenance & QC'.backend/app/Support/TabRegistry.php#L57-73:structureis labeled'Structure'andmaintenanceis labeled'Maintenance'— align these withModuleRegistry::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 realWorkOrderstatus 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 equalWorkOrder::STATUS_*, soWorkOrdersActiveShape's terminal-status filter, controller equality checks ($workOrder->status === WorkOrder::STATUS_DONE), andcompleted_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. anALL_STATUSESconstant), 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 liftDefer or tighten validation for
webhook_forwardURLs.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 winAdd
aria-pressedto 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 valueDrop the no-op
actionsprop.
actions={() => []}is truthy, soResourceTablewill 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
⛔ Files ignored due to path filters (1)
desktop/src-tauri/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (77)
CHANGELOG.mdbackend/app/Enums/RevisionLifecycle.phpbackend/app/Http/Controllers/Web/Admin/ProductRevisionController.phpbackend/app/Http/Controllers/Web/Admin/WorkOrderManagementController.phpbackend/app/Http/Controllers/Web/Admin/WorkerController.phpbackend/app/Http/Controllers/Web/Logistics/PalletMovementController.phpbackend/app/Http/Controllers/Web/OnboardingController.phpbackend/app/Http/Controllers/Web/Operator/WorkstationController.phpbackend/app/Http/Controllers/Web/SettingsController.phpbackend/app/Http/Requests/StorePalletMovementRequest.phpbackend/app/Http/Requests/StoreWorkerRequest.phpbackend/app/Http/Requests/UpdateWorkerRequest.phpbackend/app/Http/Requests/Web/Admin/StoreProductRevisionRequest.phpbackend/app/Http/Requests/Web/Admin/StoreWorkOrderRequest.phpbackend/app/Http/Requests/Web/Admin/UpdateProductRevisionRequest.phpbackend/app/Http/Requests/Web/Admin/UpdateWorkOrderRequest.phpbackend/app/Models/PalletMovement.phpbackend/app/Models/ProductRevision.phpbackend/app/Models/WorkOrder.phpbackend/app/Models/Worker.phpbackend/app/Services/Connectivity/ActionExecutor.phpbackend/app/Services/Logistics/PalletMovementService.phpbackend/app/Services/Machine/MachineSignalIngestor.phpbackend/app/Services/WorkOrder/MachineProductionService.phpbackend/app/Services/WorkOrder/WorkOrderService.phpbackend/app/Support/ModuleRegistry.phpbackend/app/Support/SoftDeleteRegistry.phpbackend/app/Support/TabRegistry.phpbackend/app/Sync/CollectionBroadcaster.phpbackend/app/Sync/ShapeRegistry.phpbackend/app/Sync/Shapes/PalletMovementsRecentShape.phpbackend/app/Sync/Shapes/WorkOrdersActiveShape.phpbackend/database/factories/PalletMovementFactory.phpbackend/database/factories/ProductRevisionFactory.phpbackend/database/factories/WorkerFactory.phpbackend/database/migrations/2026_07_19_100000_add_is_logistics_to_workers_table.phpbackend/database/migrations/2026_07_19_100001_create_pallet_movements_table.phpbackend/database/migrations/2026_07_20_120000_add_counting_source_to_work_orders.phpbackend/database/migrations/2026_07_21_100000_create_product_revisions_table.phpbackend/database/migrations/2026_07_21_100100_add_product_revision_to_work_orders.phpbackend/database/migrations/2026_07_23_120000_expand_enabled_modules_for_granular_split.phpbackend/lang/en.jsonbackend/lang/pl.jsonbackend/resources/js/Pages/admin/pallet-movements/Index.jsxbackend/resources/js/Pages/admin/product-revisions/Create.jsxbackend/resources/js/Pages/admin/product-revisions/Edit.jsxbackend/resources/js/Pages/admin/product-revisions/Index.jsxbackend/resources/js/Pages/admin/product-revisions/fields.jsbackend/resources/js/Pages/admin/schedule/planner/panels.jsxbackend/resources/js/Pages/admin/work-orders/Create.jsxbackend/resources/js/Pages/admin/work-orders/Edit.jsxbackend/resources/js/Pages/admin/work-orders/WorkOrderForm.jsxbackend/resources/js/Pages/admin/work-orders/fields.jsbackend/resources/js/Pages/admin/workers/Create.jsxbackend/resources/js/Pages/admin/workers/Edit.jsxbackend/resources/js/Pages/admin/workers/WorkerForm.jsxbackend/resources/js/Pages/logistics/MovePallet.jsxbackend/resources/js/Pages/onboarding/Modules.jsxbackend/resources/js/Pages/onboarding/Step1.jsxbackend/resources/js/Pages/onboarding/Step2.jsxbackend/resources/js/Pages/onboarding/Step3.jsxbackend/resources/js/Pages/onboarding/Step4.jsxbackend/resources/js/Pages/operator/Queue.jsxbackend/resources/js/components/ResourceForm.jsxbackend/resources/js/layouts/AppLayout.jsxbackend/resources/js/layouts/OnboardingLayout.jsxbackend/resources/js/layouts/adminNav.jsbackend/routes/web.phpbackend/tests/Feature/Machine/MachineCountToProducedQtyTest.phpbackend/tests/Feature/OnboardingWizardTest.phpbackend/tests/Feature/Web/Admin/ProductRevisionTest.phpbackend/tests/Feature/Web/Logistics/PalletMovementTest.phpbackend/tests/Feature/Web/ModuleSelectionTest.phpdesktop/package.jsondesktop/src-tauri/Cargo.tomldesktop/src-tauri/tauri.conf.jsondocs/mqtt-connectivity.md
| 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'); | ||
| } |
There was a problem hiding this comment.
📐 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
| if (! $workOrder->allowsOperatorEntry()) { | ||
| return back()->with('error', 'This work order is machine-counted; quantities are recorded automatically from the machine.'); | ||
| } | ||
|
|
There was a problem hiding this comment.
🗄️ 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.phpRepository: 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 -200Repository: 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 -250Repository: 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.
| 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, | ||
| ]); | ||
| }); |
There was a problem hiding this comment.
🗄️ 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.
| 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.
| 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); | ||
| } |
There was a problem hiding this comment.
🗄️ 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 || trueRepository: 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 || trueRepository: 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 || trueRepository: 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.
| /** | ||
| * 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; | ||
| } |
There was a problem hiding this comment.
🗄️ 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> |
There was a problem hiding this comment.
🎯 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 toStep 2 — Production Line.backend/resources/js/Pages/onboarding/Step2.jsx#L24-L24: change the title toStep 3 — Product Type.backend/resources/js/Pages/onboarding/Step3.jsx#L80-L80: change the title toStep 4 — Process Template.backend/resources/js/Pages/onboarding/Step4.jsx#L28-L28: change the title toStep 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-L24backend/resources/js/Pages/onboarding/Step3.jsx#L80-L80backend/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”.
| public function test_modules_step_requires_admin(): void | ||
| { | ||
| $operator = User::factory()->create(); | ||
| $operator->assignRole('Operator'); | ||
|
|
||
| $this->actingAs($operator) | ||
| ->get(route('onboarding.modules')) | ||
| ->assertForbidden(); | ||
| } |
There was a problem hiding this comment.
📐 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.
| 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
| 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(); | ||
| } |
There was a problem hiding this comment.
🔒 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.
| 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
| ### 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. |
There was a problem hiding this comment.
📐 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.
| - **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
| ``` | ||
| [ machine / MQTTX / any publisher ] ──publish──▶ [ MQTT broker ] ◀──subscribe── [ OpenMES (php artisan mqtt:listen) ] | ||
| (Mosquitto) | ||
| ``` |
There was a problem hiding this comment.
📐 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
v0.18.0 (develop → main)
Brings all of
developtomain, 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
release/v0.18.0= develop (incl. onboarding via feat(onboarding): choose modules at first login (Lightweight / Advanced / Custom) #202) merged with main; 26 conflicts resolved (version files → 0.18.0, code → develop superset, api.php/seeder → main's No data shown #193/planner, lang union, CHANGELOG consolidated).Validation (pre-merge)
After merge
main(admin — protection).v0.18.0→ triggers the release build (web + desktop win/mac/linux + apk).Closes #202.
🤖 Generated with claude-flow
Summary by CodeRabbit