feat(train-scheduling): mid-route consist changes, audit history, safer workspace

- planned couples: loose wagons join the train at a route stop, added
  from the schedule yards tab; capacity credits them per corridor edge
  and coupling validates locomotive weight/length caps per leg
- real-cut toggle: a cut wagon permanently leaves the train build at
  its cut yard (soft cut still sits out one trip only)
- fix heaviest-leg display counting a shared slot's full cargo on
  every spanned edge (phantom pull-weight overload on S-2026-00045)
- confirmation dialogs for workspace add/load/unload/remove actions
- train-builder History and Detached-wagons tabs, backed by paginated
  endpoints; builder detaches now always write adjustment-log rows

Migrations 3660 (planned_wagon_couples, planned_wagon_real_cuts) and
3670 (adjustment log train_schedule_id nullable) — both applied to the
dev DB by hand; watch mode does not run migrations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Marshal
2026-08-23 03:55:54 +00:00
parent ba89b670c8
commit 8e6fc09aac
34 changed files with 2532 additions and 202 deletions

View File

@@ -100,6 +100,7 @@ import {
CorridorBudget,
CorridorLeg,
OverageTolerance,
addCoupledWagons,
stopYardsFor,
subtractCutWagons,
} from './corridor-capacity.util';
@@ -5064,6 +5065,8 @@ export class BookingBatchService implements OnModuleInit {
stock.byYardId,
budget.stops,
);
// Wagons staff cut mid-route are not stock past their cut stop.
ledger.debitCutWagons(stock.cutWagons ?? []);
// Debit what is already committed, per boarding yard and wagon type — the
// same bookings the corridor budget subtracted. A booking with no resolvable
// wagon type still occupies steel, so it drains any type at its yard.
@@ -5385,6 +5388,8 @@ export class BookingBatchService implements OnModuleInit {
// ponytail: the wagon-type stock ledger stays cut-blind; bucket
// builtTrainStock by (yard, reach) if mixed-type cut trains appear.
subtractCutWagons(budget, schedule.plannedWagonCutYards);
// Planned couples add a slot from their couple stop onward.
addCoupledWagons(budget, schedule.plannedWagonCouples);
for (const b of await this.committedBookings(schedule, excludeBookingIds)) {
budget.subtract(
this.needFor(b, wagonDims),