Maintenance Events
A Maintenance Event captures the actual work that happened on a piece of equipment — when it started, when it ended, who did it, how much it cost, how long the asset was down, what parts were used, and (for breakdowns) why it failed.
One MaintenanceEvent entity covers all types of maintenance work — planned and unplanned. This is intentional: splitting later is cheap; merging later is expensive. The event_type field plus the planned boolean differentiate the kind.
Where it lives
- Create / list: Maintenance Hub at
/equipment/maintenance→ Events tab → + Log Event. - Auto-generated: PM schedules with
auto_generate_event = truecreateSCHEDULEDevents at 08:00 daily. - Asset history:
/equipment/:assetId→ Maintenance History tab. - Permissions:
maintenance_readto view,maintenance_writeto log/edit/start/complete.
Event types
| Type | Planned? | When to use |
|---|---|---|
PM | yes | Routine preventive maintenance from a schedule. |
INSPECTION | yes | Scheduled checks (safety, regulatory, condition monitoring). |
CALIBRATION | yes | Periodic calibration of measurement equipment. |
REPAIR | usually no | Fixing something that broke or degraded. |
BREAKDOWN | no | The asset failed unexpectedly — log first, repair-event follows. |
You can flip the planned boolean independently of event_type — e.g. a planned repair during a maintenance window.
Event lifecycle
SCHEDULED ─► IN_PROGRESS ─► COMPLETE
│
└────► CANCELLED (if no longer needed)
| Status | When | Side effects |
|---|---|---|
SCHEDULED | Created (manually or by cron) | None. Just sits on the calendar. |
IN_PROGRESS | StartMaintenanceEvent RPC | Asset flips runtime_status → MAINTENANCE. WC's rollup status updates. Optionally creates an AssetTransaction. |
COMPLETE | CompleteMaintenanceEvent RPC | Asset flips back to its previous status. downtime_minutes = ended_at - started_at. Linked schedule's last_done_at rolls forward. Spare-part stock decrements. maintenance_completed SSE fires. |
CANCELLED | Manual edit | Removes the event from active counts. No side effects. |
The state machine is enforced server-side. You can't skip from SCHEDULED to COMPLETE without going through IN_PROGRESS (which is what triggers the runtime-status flip).
Logging an event — full modal
The full event modal is wider (xl size) than typical forms because it contains several distinct sections.
Event type picker
Five chip-style buttons at the top: PM, Inspection, Calibration, Repair, Breakdown. Each chip shows its own icon. Picking a chip sets event_type + the planned default.
Basics
| Field | Notes |
|---|---|
| Doc Number | Auto-generated. Editable for legacy imports. |
| Asset | Required. FetchSelect to /asset. |
| Schedule | Optional. Links this event to a parent MaintenanceSchedule so completing it rolls the schedule's next_due_at forward. Auto-populated when generated by cron. |
| Performed by | Workforce member. Picker: /support/workforce. |
| Work Order | Optional. Link to a WorkOrder if this maintenance was triggered by a production issue. |
Timing
| Field | Notes |
|---|---|
| Scheduled start / end | When you plan to do the work. Used by the capacity calculation to subtract planned PM minutes from available capacity. |
| Started at / Ended at | When the work actually happened. downtime_minutes is computed from these on completion. |
For unplanned breakdowns, scheduled_start = scheduled_end = started_at is fine — you can't schedule what you didn't expect.
Downtime
downtime_minutes is the headline metric for MTTR / MTBF / availability reports. It's auto-computed from started_at and ended_at on completion, but editable so the technician can record actual downtime even when they forgot to start/end the event in real time.
Root cause (breakdown only)
For BREAKDOWN events, a text field for the operator's first-pass diagnosis. Don't make this exhaustive — it's the field-level signal. Detailed RCA goes in internal_notes.
Labor cost / Total cost
labor_cost— labor hours × loaded rate. Manually entered.total_cost— computed:labor_cost+ sum ofparts_used[].line_cost. Read-only.
Parts used
Dynamic line items. Each row:
| Field | Notes |
|---|---|
| Item | FetchSelect to /item filtered to is_spare_part = true. |
| Qty | Quantity consumed. |
| Unit cost | Defaults to the item's average cost. Editable. |
| Line cost | Computed: qty × unit cost. |
On CompleteMaintenanceEvent:
- Each line's qty is deducted from inventory (the item's
qty_on_handdecrements). - An
AssetTransactionof typeMAINTENANCEposts to the asset's accounting ledger withtotal_cost. - A
spare_part_low_stockSSE may fire if a part dropped below safety stock.
Internal notes
Long-form free text. Procedure deviations, follow-up needed, photos via attachment, etc.
Custom fields
The maintenanceevent entity is on the custom-field allowlist. Add custom fields (text, number, choice, date, calculation) in Settings → Custom Fields and they appear here.
Quick patterns
"Just mark it done" (planned PM, no parts, no fuss)
Use the Mark Done popover on the Equipment Hub's Today's PM Schedule widget. See Preventive Maintenance → One-click Mark Done. Two clicks total.
Log an unplanned breakdown right now
- Equipment Hub status board → click the asset's chip → Runtime Status Quick Menu → DOWN → reason text "press belt broke".
- The status change flips the asset to
DOWN. The hub pulses, the WC rolls up to its worst child. - Maintenance Hub → Events tab → + Log Event → type BREAKDOWN → asset auto-fills → started_at = now → save as
IN_PROGRESS. - When repaired: open the event → fill ended_at, root_cause, parts used → Complete.
- Asset flips back to
RUNNING(or whatever it was), schedule'snext_due_atrolls forward, parts decrement,maintenance_completedfires.
Inspection that finds nothing
- Schedule a
CALENDARPM of type INSPECTION. - When the cron fires it auto-creates a SCHEDULED event with the schedule's task list.
- Technician opens the event, ticks each task
done, hits Complete. - Asset's
last_inspection_dateupdates automatically.
Reports the events feed
| Report | Sources |
|---|---|
| MTBF (mean time between failures) | BREAKDOWN events on an asset. Interval = time between consecutive started_at values. |
| MTTR (mean time to repair) | Average downtime_minutes across REPAIR + BREAKDOWN. |
| Downtime hrs YTD | Sum of downtime_minutes / 60 where ended_at falls in the current year. |
| Repair cost YTD | Sum of total_cost where event_type ∈ {REPAIR, BREAKDOWN} in the current year. |
| Pareto by root cause | BREAKDOWN events grouped by root_cause token. Helps spot the 80/20. |
All five appear on the Equipment Detail Overview tab, with a "include descendants" toggle that rolls them up across the asset's hierarchy.
Cron interactions
| Cron | What it does that touches events |
|---|---|
support-ms 08:00 daily | Auto-creates SCHEDULED events for due schedules with auto_generate_event=true. |
support-ms 5-min | Rolls up WC current_runtime_status — affected by which assets are currently IN_PROGRESS on a maintenance event. |
build-ms 02:00 | Capacity recompute. Planned events (scheduled but not yet IN_PROGRESS) subtract from available minutes; completed events don't. |
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Modal content overflows horizontally | Reported and fixed — modal is now xl size. If it returns, check that ModalLayout still wraps it with size="xl". |
| Save fails with 400 | Required field missing (asset, event_type) or started_at > ended_at. The form's inline validation should catch most. |
| Completing the event doesn't decrement parts | Check the parts_used line items have valid item_id and qty > 0. Items not flagged is_spare_part still work but the FetchSelect filters them out by default. |
Asset's runtime_status doesn't flip back from MAINTENANCE | The event status didn't change to COMPLETE — maybe the request failed. Open the event detail and check. |
| Cron-generated event has wrong asset | Schedule's asset_id was wrong. Edit the schedule and recreate the event. |
Related
- Preventive Maintenance — what schedules generate events
- Spare Parts — items used in parts_used lines
- Asset Management — where status flips originate
- Runtime Status — quick operator action that often precedes a breakdown event