Skip to content

Add first-class Trailer operations - #309

Open
roncodes wants to merge 23 commits into
mainfrom
feature/first-class-trailers
Open

Add first-class Trailer operations#309
roncodes wants to merge 23 commits into
mainfrom
feature/first-class-trailers

Conversation

@roncodes

@roncodes roncodes commented Sep 3, 2026

Copy link
Copy Markdown
Member

Summary

Trailer becomes a first-class Fleet-Ops resource: its own navigation entry directly below Vehicles, list/grid/details/create/edit flows, effective-dated towing connections with Vehicles, polymorphic Device and Equipment attachment, telematics ingestion, maintenance integration, import/export, search, reporting, AI query resources, permissions, and full localization in all nine locales.

Architecture

  • Model. Trailer extends Asset on the shared assets table with an asset_class = trailer global scope, its own trailer_ public-id namespace and a trailer payload identity, so every internal envelope is { "trailers": [...], "meta": {...} } / { "trailer": {...} } and never assets. Shared status, type and ownership constants back validation, import and the console.
  • Towing. asset_connections stores effective-dated connections (connector Vehicle, connected Trailer, towing position, source, audit). Unique active_connected_uuid / active_connector_position columns make single-attachment and position rules database-enforced; attach and detach are idempotent and company-scoped; attaching to another vehicle or an occupied position answers 409.
  • Devices and equipment. Device::attachTo() and the Equipment assignment accept fleet-ops:trailer; device_installations keeps installation history. Trailer telemetry flows through TelematicService (device events, with stale-event rejection) and the public track endpoint (stale and null-island observations acknowledged but ignored), persists real spatial positions, and broadcasts trailer.location_changed.
  • Public API. /v1/trailers follows the Vehicle conventions from Expand public Fleet, Vehicle, and Driver API contracts #311: additive relation identifiers (category_id, vendor_id, warranty_id, photo_id, vehicle_id), with/expand expansions for category, vendor, warranty, photo, positions; towing state, devices and equipment are part of the base representation; no private identifiers anywhere (the public telematics snapshot drops device/event UUIDs). GET /v1/vehicles/{id}/trailers and GET /v1/vehicles/{id}?with=trailers expose the vehicle side.
  • Internal API. fleetbaseRoutes('trailers') plus attach/detach vehicle, device and equipment, statuses/types, export and import. Deleting an attached trailer is refused (single and bulk); vehicles gain attach/detach equipment endpoints.
  • Console. Vehicle-convention form (grouped sections, localized labels/placeholders/help, unit-aware dimension, weight and temperature fields driven by the measurement system, conditional reefer and lease sections, money/date/currency controls), details overview, and dedicated Positions, Devices, Equipment, Towing history, Schedules, Work orders and Maintenance tabs. Index with localized enum cells, towing-vehicle link, multi-option/date/model filters mapped to backend params, bulk delete, column customization and layout switch. Vehicle details gain Trailers and Equipment tabs. All mutations update the open panel without a page reload.

Migrations

  • assets: nullable unique public_id (historical Asset rows are not rewritten), asset_class, photo_uuid, online, last_online_at, telematics, purchased_at, dimension/weight/running-gear/reefer columns and a company/class/status index.
  • asset_connections and device_installations tables.

Review fixes on top of the original branch

  • Extension setup resolves Intl through app.lookup('service:intl'); the Trailer sidebar item renders directly below Vehicles after priority sorting (asserted from the rendered menu).
  • The console never received attachment_state (missing Ember attribute), the Devices tab posted to the vehicle route, EquipmentFilter::equipable used whereHas on a morphTo relation and matched nothing, modal/tab components lacked the app/ re-exports the host needs, and the modal classes imported a non-existent ember-core base.
  • Towing conflicts returned framework abort() HTML, relation filters ignored console UUIDs and multi-option arrays, internal attach responses leaked class-name morph types, the public snapshot leaked device UUIDs, and the post-save transition was aborted.

UI polish round (owner review feedback)

  • Badges: attached, detached, in_use, out_of_service and retired had no entry in ember-ui's badge palette and rendered unstyled. They are now styled in the engine stylesheet with the same 800/700/100 convention.
  • Card view: the trailer card body is an image stage plus a padded summary strip (status and connectivity badges, "Towed by " link) instead of badges floating on the body.
  • Details panel scroll: the details TabNavigation now uses the tab-navigation-fill layout so the tab content is the scroll container and the last sections (Dimensions & capacity onwards) are reachable. Applied to Vehicles too, which had the same clipping.
  • Tab overflow: route-backed tabs carry stable ids so ember-ui's overflow bookkeeping can compute the "More" menu; the root cause (identity keyed on a missing id) is fixed upstream in fix(tab-navigation): overflow menu for route-backed tabs without ids ember-ui#171.
  • Devices / equipment / trailers / towing history share one AttachmentList component (header with description and primary action, uniform rows with a pill, status badge and row actions, one empty state). New Equipment::Pill and Trailer::Pill complement Device::Pill. Towing history is a compact timeline (vehicle pill, connected/disconnected rows, position, duration, source, state badge) instead of a seven-column table.
  • Placeholder image: trailers use an inline SVG trailer silhouette (trailer-placeholder-image helper); a console can override it through defaultValues.trailerImage.
  • Map popovers: the live vehicles feed eager-loads current trailers, the index resource emits a compact trailers list, and both map providers (Leaflet template and Google info window builder) show Trailers and Devices cells with an online dot per row. Names are HTML-escaped in the builder.
  • Resource cards (driver, vehicle, trailer) share one structure modelled on the driver card: title row with the online indicator, image body on the card background (no extra layers), footer with the status badge beside the actions, then meta rows that style linked records identically (fleetops-card-meta*). The vehicle card lists the assigned driver and the trailers it tows; the trailer card lists the towing vehicle and connectivity; the driver card links its assigned vehicle. The internal vehicle payload carries a compact trailers list (shared TrailerSummary helper, also used by the live-map resource) and the vehicles index requests currentTrailers.
  • Grid views for vendors, contacts and customers: table/grid layout switch remembered per listing, plus Vendor::Card and Contact::Card (customers reuse the contact card through the customer actions service).
  • Third review round: driver status badge titleized; vehicle and trailer images fill the card body; vehicle index gains driver/trailer/device multi-select filters (new filter/multi-model), "trailer attached / driver assigned / device attached" yes-no filters and a batch internal-id tag input; trailer index gains a devices filter and the refrigerated checkbox loses its redundant label and moves last; orders internal-id filter is a tag input for batch scanning (Feature Request: Support multi-Internal ID filtering in orders table view fleetbase#576). Backend filters accept comma-separated lists and presence flags.
  • Device re-attach guard (fleetops#177 follow-up): Device::attachTo throws DeviceAlreadyAttachedException when the device is installed on another asset; the internal device, vehicle and trailer attach endpoints and the public device API answer 409 naming the asset. The attach modal already lists only unattached devices, so the UI and API now agree.
  • Translations for the new strings in all nine locales.
  • Branch merged with main at v0.6.64 (nested-resource expansion fix); trailer and vehicle contract suites re-run green after the merge.

Verified in the in-app browser pane against the local stack: attached badge colour, card view, details scroll to the last section, More menu listing Schedules/Work orders/Maintenance, Devices/Equipment/Towing history tabs with and without rows, Vehicle Trailers tab, and the CEN-01 popover listing its trailer and device; vehicles, drivers, trailers, vendors, contacts and customers grid views with the unified cards.

Testing

  • PHP: composer test:lint clean for the touched files; Trailer suites (TrailerControllerLifecycleTest, TrailerSupportCoverageTest, TrailerTest, TelematicServiceTelemetryTest) pass; a coverage slice reports 100% line coverage for every changed server file. composer test:unit (the CI runner, every test file in its own process) passes: 441 files, exit 0. Repository-wide php-cs-fixer --dry-run only reports pre-existing files outside this change.
  • Ember: pnpm lint:js, pnpm lint:hbs, pnpm lint:css, pnpm lint:intl and pnpm build pass. New unit/integration tests cover rendered navigation order, store.query('trailer') normalization, trailer-actions, the details controller, the index route params, the form and the translated-value cell. They were validated only partially here: the engine's dummy app cannot boot ember-core's universe services because ember-core does not declare its tracked-built-ins dependency (see below), and the browser test runner was stopped at the user's request.

Manual verification (local stack, console + API)

Extension startup clean; Trailers directly below Vehicles; empty index state; create a reefer via the UI (type selection reveals the reefer section); index with localized type/status/attachment/connectivity badges; details overview and every tab; attach to a vehicle from the overview (modal with towing position) and see it from the Vehicle's Trailers tab; attach and detach equipment and a device from the tabs; edit and save; public API lifecycle via curl (create flatbed, expansions, position conflict 409, re-attach idempotent, attach elsewhere 409, delete-while-attached 409, vehicle trailer list, with=trailers, track, stale and null-island observations, connections, detach twice); device-event telemetry ingestion updates the trailer. No feature-related console errors.

Not re-verified after the last two front-end fixes (post-save transition, image fallback), and not exercised: delete from the UI, index filtering/sorting from the UI, dark mode and narrow viewports. See the final review notes.

Known limitations

  • ember-core 0.3.24 imports tracked-built-ins without declaring it; any setupTest in this engine's dummy app fails until that is fixed upstream (CI does not run ember test for this package).
  • Repository-wide PHPStan reports the existing baseline (not part of CI).
  • Two pre-existing untranslated host keys (common.search-input, select-resource-filter-by) surface on the index as they do on Vehicles.

Related PRs

@roncodes
roncodes force-pushed the feature/first-class-trailers branch from c313a8d to dbb29dc Compare September 3, 2026 11:11
@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (4008bea) to head (461d2ef).

Additional details and impacted files
@@              Coverage Diff              @@
##                main      #309     +/-   ##
=============================================
  Coverage     100.00%   100.00%             
- Complexity     10007     10274    +267     
=============================================
  Files            531       545     +14     
  Lines          38592     39684   +1092     
=============================================
+ Hits           38592     39684   +1092     
Flag Coverage Δ
backend 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Public API
- Adopt the public relation contract: category_id, vendor_id, warranty_id,
  photo_id and vehicle_id are always present; category, vendor, warranty,
  photo and positions expand through with/expand like Vehicles.
- Strip device and event UUIDs from the public telematics snapshot.
- Return 409 responses instead of aborting inside transactions for towing
  conflicts; resolve vehicles by public or internal id; fall back to the
  container request in find(); ignore null-island track observations.
- Expose towing connections with vehicle_id/trailer_id identifiers and
  full models for the console.
- Expose currently attached trailers on Vehicles via with=trailers.

Internal API
- Consistent error responses, delete guard for attached trailers (single
  and bulk), statuses/types endpoints for console filters, resource-wrapped
  attach/detach responses, and vehicle attach/detach equipment endpoints.
- Console filters submit arrays and UUIDs: TrailerFilter accepts both and
  resolves vehicle/vendor/category identifiers; EquipmentFilter::equipable
  resolves the asset instead of whereHas() on a morphTo relation, which
  silently matched nothing.
- Always embed connections/devices/equipments for the console so Ember
  Data stops warning about missing embedded relationships.
- Share status/type/ownership constants between validation, import and
  the model; make the test fixtures re-boot models after replacing the
  event dispatcher so hooks survive cross-file runs.
- Rebuild the trailer form with grouped sections, localized labels,
  placeholders and help text, unit-aware dimension/weight/temperature
  fields driven by the measurement system, select controls for type,
  status, coupling, brakes, ownership and odometer unit, money/date/currency
  inputs, and conditional reefer and lease sections.
- Rebuild the details overview (identity, towing connection with attach/
  detach, telematics, dimensions, running gear, refrigeration, ownership,
  notes/timestamps) and add dedicated Towing history, Equipment, Schedules,
  Work orders and Maintenance tabs with tables, empty states and actions.
- Rebuild the index with localized enum cells (new cell/translated-value),
  towing vehicle link, multi-option/date/model filters mapped to backend
  params, bulk delete, and strip empty query params before querying.
- Rework trailer-actions (panel/modal APIs, attach/detach vehicle with
  towing position, device and equipment attach/detach with confirmations,
  locate, maintenance shortcuts) and add attach trailer/equipment actions
  to vehicle-actions with matching Vehicle detail tabs.
- Make Device::Manager target the resource's own endpoint so trailer device
  attachment no longer posts to the vehicle route.
- Add the missing app/ re-exports for trailer components, controllers,
  routes and templates; modal components no longer import a non-existent
  ember-core base.
- Localize every trailer string in all nine locales (translated, with
  placeholder parity), plus vehicle attach-trailer/equipment strings.
…ole flows

- Assert the rendered Resources menu places Trailers directly after
  Vehicles (stubbing the console-only universe services the dummy app
  lacks) instead of inspecting component source.
- Cover store.query/queryRecord normalization of the trailers/trailer
  envelopes, including the empty collection.
- Cover trailer-actions defaults, panel tabs, and the attach/detach
  requests; the details controller tabs and attach/detach actions; the
  index route query params against the controller columns; the trailer
  form's placeholders, unit labels and conditional sections; and the
  translated-value cell.
- Provide an inert stand-in for the console extensions module so the
  dummy app can boot the universe services.
…elds

The public resource contract test inspects the resource source for
$this->when(Http::isInternalRequest(), ...) on every uuid-bearing field;
caching the check in a local broke that inspection.
- Style the attached/detached, in-use, out-of-service and retired badges,
  which ember-ui's palette does not define.
- Rebuild the trailer card body as an image stage plus a padded summary strip
  (status, connectivity, towing vehicle link) instead of badges on the raw body.
- Make the details tab content the scroll container (tab-navigation-fill) so
  the last overview sections are reachable; apply the same to Vehicles.
- Give route-backed detail tabs stable ids so TabNavigation can compute its
  More menu instead of clipping the trailing tabs.
- Introduce a shared AttachmentList component plus Equipment and Trailer
  pills, and use them for the devices, equipment, vehicle trailers and towing
  history tabs so they share one header, row and empty-state structure. Towing
  history becomes a compact timeline rather than a seven-column table.
- Ship an inline trailer placeholder image (overridable through
  defaultValues.trailerImage) instead of falling back to the vehicle photo.
- Translate the new strings in all locales; add attachment-list and
  placeholder tests.
The live vehicles feed eager-loads each vehicle's current trailers next to its
devices, and the index resource emits a compact trailers list. Both map
providers render Trailers and Devices cells with an online dot per row: the
Leaflet marker popup/tooltip templates and the Google info-window builder,
which escapes names before injecting them as HTML. Covered by resource, live
endpoint and popover-builder tests.
Share one asset-card structure between vehicles and trailers: an image stage
with the resource placeholder, a padded summary strip with status and
online/offline badges plus the assigned-driver link (or the no-driver state),
and footer actions with tooltips and permissions. Adds the common.offline and
vehicle.fields.updated-at strings the card uses in every locale.
Cards now share one structure: a title row with the online indicator, an image
body on the card background, and a footer with the status badge beside the
actions followed by meta rows that style linked records the same way. The
vehicle card lists the assigned driver and the trailers it is towing; the
trailer card lists the towing vehicle and connectivity; the driver card links
its assigned vehicle. The internal vehicle payload carries a compact list of
current trailers (shared TrailerSummary helper, also used by the live map
resource) and the vehicles index requests it. Adds the shared common strings
in every locale.
Adds the table/grid layout switch (remembered per listing) and card components
for vendors and contacts, reusing the shared resource-card structure. Customers
reuse the contact card through the customer actions service.
The repository ignores `vendor` directories for Composer output, which also
hid the new Vendor::Card files from the grid-view commit.
The driver badge rendered its raw status value through the block form, which
skips the badge's humanizer. Vehicle and trailer images now cover the body
like the driver photo instead of sitting padded inside it.
Attaching a device that is already installed on another vehicle or trailer
silently moved it, so an operator could take a tracker off one asset with no
feedback (fleetops#177). Device::attachTo now throws
DeviceAlreadyAttachedException and every attach endpoint (internal device,
vehicle and trailer controllers, public device API) answers 409 with the
asset the device is still attached to. Re-attaching to the same asset stays
idempotent.
Vehicles: filter by one or more drivers, trailers or devices (new
filter/multi-model component over ModelSelectMultiple), by whether a trailer,
driver or device is attached (with the inverse), and by several internal ids
at once; the vendor placeholder resolves through the common namespace.
Trailers: filter by devices; the refrigerated checkbox drops its redundant
group label and moves to the end of the panel.
Orders: the internal id filter is a tag input so a batch of scanned or pasted
ids matches in one query (fleetbase#576). Backend filters accept the
comma-separated lists and relation presence flags, with contract tests.
The small form-input height capped the multiple-select trigger at 34px with an
internal scroller, so the placeholder was clipped before anything was chosen.
The trigger now starts at the input height, wraps its chips and grows with
them.
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.

1 participant