Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.axiomancer.io/llms.txt

Use this file to discover all available pages before exploring further.

For every completed vessel visit, Axiom Overwatch decomposes the arrival-to-departure window into the time spent in each operational state. This answers the “where did the time actually go” question that a single berth_hours figure cannot.

When to use this

  • Quantify anchorage queues vs. berth productivity at a port
  • Benchmark turnaround performance across vessels and operators
  • Surface idle time inside a visit that does not map to anchor or berth
  • Build congestion analytics that distinguish “waiting outside” from “alongside but slow”
If you only need aggregate berth utilization or congestion indices, the Berth utilization and Congestion endpoints are the right tools. Use this dataset when you need a per-visit, per-state breakdown.

The breakdown

Each closed vessel visit produces one row keyed by visit_id:
FieldDescription
total_hoursFull window from arrived_at to departed_at.
approach_hoursTime from the entered event to the first anchored, berthed, or departed event.
anchor_wait_hoursSum of segments where the vessel was in the anchored state.
berth_hoursSum of segments where the vessel was in the berthed state.
idle_other_hoursResidual inside the visit window not attributed above (typically pre-first-event slack).
classification_methodevent_segments when anchor or berth events are present, fallback_total when only entry/exit are known.
computed_atTimestamp the row was written.
approach_hours + anchor_wait_hours + berth_hours + idle_other_hours reconciles to total_hours (within rounding).

How it’s computed

The classifier walks port_events between arrived_at and departed_at and attributes the inter-event delta to the earlier event’s state. A vessel that is anchored at 02:00 and berthed at 10:00 contributes 8 hours to anchor_wait_hours. When a visit has neither an anchored nor a berthed event — usually old or sparse data — the entire window is booked to approach_hours and classification_method is set to fallback_total. Filter on this column when you need only fully-classified visits. The TypeScript classifier runs on every departure close, so live visits get an allocation row as soon as the visit closes. Historical visits without a row are filled in by a SQL-side backfill (see below).
Canal transit time is currently bucketed into approach_hours. A dedicated canal_hours column is planned once canal polygons are wired into the event stream.

Querying

The data lives in the vessel_visit_time_allocation table, indexed for (port_id, departed_at DESC) and (imo_number, departed_at DESC).
-- Average berth vs anchor wait at a port over the last 90 days,
-- excluding visits that lacked enough events to classify.
select
  date_trunc('week', departed_at) as week,
  round(avg(approach_hours)::numeric, 2)   as avg_approach_h,
  round(avg(anchor_wait_hours)::numeric, 2) as avg_anchor_h,
  round(avg(berth_hours)::numeric, 2)       as avg_berth_h,
  round(avg(idle_other_hours)::numeric, 2)  as avg_idle_h,
  count(*)                                  as visits
from vessel_visit_time_allocation
where port_id = $1
  and departed_at >= now() - interval '90 days'
  and classification_method = 'event_segments'
group by 1
order by 1 desc;
-- Per-vessel turnaround at a single port.
select v.name, v.imo_number, vva.*
from vessel_visit_time_allocation vva
join vessels v on v.imo_number = vva.imo_number
where vva.port_id = $1
order by vva.departed_at desc
limit 50;

Backfilling historical visits

Visits that closed before this feature shipped do not have allocation rows. The backfill_visit_time_allocation(p_limit) RPC processes a bounded batch of unclassified departed visits using the same segmentation logic as the live writer. It is idempotent — existing rows are not overwritten.
-- One-shot top-up. Default p_limit is 500.
select backfill_visit_time_allocation();

-- Larger batch, callable by service-role contexts.
select backfill_visit_time_allocation(5000);
Returns the integer count of visits processed. Schedule it as a periodic top-up if you do not want to wait for the next live departure to populate a given port.

Field reconciliation

If you previously joined vessel_visits.berth_hours for productivity dashboards, that column still exists and is unchanged. The allocation table is a sibling, not a replacement — vessel_visits stays narrow. Joining on visit_id gives you both views in one query:
select vv.id, vv.port_id, vv.berth_hours, vva.anchor_wait_hours, vva.approach_hours, vva.idle_other_hours
from vessel_visits vv
join vessel_visit_time_allocation vva on vva.visit_id = vv.id
where vv.port_id = $1 and vv.status = 'departed'
order by vv.departed_at desc;