@freighttech/terminal-tracking 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -0
  3. package/dist/generated/entities/terminal_config/index.js +53 -0
  4. package/dist/generated/entities/terminal_config/index.js.map +7 -0
  5. package/dist/generated/entities/terminal_event/index.js +59 -0
  6. package/dist/generated/entities/terminal_event/index.js.map +7 -0
  7. package/dist/generated/entities/terminal_tracking_job/index.js +37 -0
  8. package/dist/generated/entities/terminal_tracking_job/index.js.map +7 -0
  9. package/dist/generated/entities/terminal_vessel_visit/index.js +41 -0
  10. package/dist/generated/entities/terminal_vessel_visit/index.js.map +7 -0
  11. package/dist/generated/entities.ids.generated.js +26 -0
  12. package/dist/generated/entities.ids.generated.js.map +7 -0
  13. package/dist/index.js +20 -0
  14. package/dist/index.js.map +7 -0
  15. package/dist/modules/terminal_tracking/acl.js +14 -0
  16. package/dist/modules/terminal_tracking/acl.js.map +7 -0
  17. package/dist/modules/terminal_tracking/api/openapi.js +27 -0
  18. package/dist/modules/terminal_tracking/api/openapi.js.map +7 -0
  19. package/dist/modules/terminal_tracking/api/terminal-configs/route.js +140 -0
  20. package/dist/modules/terminal_tracking/api/terminal-configs/route.js.map +7 -0
  21. package/dist/modules/terminal_tracking/api/terminal-configs/test/route.js +58 -0
  22. package/dist/modules/terminal_tracking/api/terminal-configs/test/route.js.map +7 -0
  23. package/dist/modules/terminal_tracking/api/tracking-jobs/details/route.js +109 -0
  24. package/dist/modules/terminal_tracking/api/tracking-jobs/details/route.js.map +7 -0
  25. package/dist/modules/terminal_tracking/api/tracking-jobs/poll/route.js +58 -0
  26. package/dist/modules/terminal_tracking/api/tracking-jobs/poll/route.js.map +7 -0
  27. package/dist/modules/terminal_tracking/api/tracking-jobs/route.js +189 -0
  28. package/dist/modules/terminal_tracking/api/tracking-jobs/route.js.map +7 -0
  29. package/dist/modules/terminal_tracking/api/utils.js +12 -0
  30. package/dist/modules/terminal_tracking/api/utils.js.map +7 -0
  31. package/dist/modules/terminal_tracking/api/vessel-visits/route.js +83 -0
  32. package/dist/modules/terminal_tracking/api/vessel-visits/route.js.map +7 -0
  33. package/dist/modules/terminal_tracking/backend/terminal-configs/page.js +669 -0
  34. package/dist/modules/terminal_tracking/backend/terminal-configs/page.js.map +7 -0
  35. package/dist/modules/terminal_tracking/backend/terminal-configs/page.meta.js +19 -0
  36. package/dist/modules/terminal_tracking/backend/terminal-configs/page.meta.js.map +7 -0
  37. package/dist/modules/terminal_tracking/backend/terminal-containers/page.js +275 -0
  38. package/dist/modules/terminal_tracking/backend/terminal-containers/page.js.map +7 -0
  39. package/dist/modules/terminal_tracking/backend/terminal-containers/page.meta.js +19 -0
  40. package/dist/modules/terminal_tracking/backend/terminal-containers/page.meta.js.map +7 -0
  41. package/dist/modules/terminal_tracking/ce.js +35 -0
  42. package/dist/modules/terminal_tracking/ce.js.map +7 -0
  43. package/dist/modules/terminal_tracking/commands/index.js +4 -0
  44. package/dist/modules/terminal_tracking/commands/index.js.map +7 -0
  45. package/dist/modules/terminal_tracking/commands/terminal-configs.js +107 -0
  46. package/dist/modules/terminal_tracking/commands/terminal-configs.js.map +7 -0
  47. package/dist/modules/terminal_tracking/commands/terminal-tracking.js +27 -0
  48. package/dist/modules/terminal_tracking/commands/terminal-tracking.js.map +7 -0
  49. package/dist/modules/terminal_tracking/commands/tracking-jobs.js +56 -0
  50. package/dist/modules/terminal_tracking/commands/tracking-jobs.js.map +7 -0
  51. package/dist/modules/terminal_tracking/components/ContainerDetailsDrawer.js +371 -0
  52. package/dist/modules/terminal_tracking/components/ContainerDetailsDrawer.js.map +7 -0
  53. package/dist/modules/terminal_tracking/data/entities.js +337 -0
  54. package/dist/modules/terminal_tracking/data/entities.js.map +7 -0
  55. package/dist/modules/terminal_tracking/data/validators.js +98 -0
  56. package/dist/modules/terminal_tracking/data/validators.js.map +7 -0
  57. package/dist/modules/terminal_tracking/di.js +39 -0
  58. package/dist/modules/terminal_tracking/di.js.map +7 -0
  59. package/dist/modules/terminal_tracking/encryption.js +14 -0
  60. package/dist/modules/terminal_tracking/encryption.js.map +7 -0
  61. package/dist/modules/terminal_tracking/events.js +45 -0
  62. package/dist/modules/terminal_tracking/events.js.map +7 -0
  63. package/dist/modules/terminal_tracking/i18n/de.json +175 -0
  64. package/dist/modules/terminal_tracking/i18n/en.json +178 -0
  65. package/dist/modules/terminal_tracking/i18n/es.json +175 -0
  66. package/dist/modules/terminal_tracking/i18n/pl.json +206 -0
  67. package/dist/modules/terminal_tracking/index.js +15 -0
  68. package/dist/modules/terminal_tracking/index.js.map +7 -0
  69. package/dist/modules/terminal_tracking/lib/adapters/bct/adapter.js +164 -0
  70. package/dist/modules/terminal_tracking/lib/adapters/bct/adapter.js.map +7 -0
  71. package/dist/modules/terminal_tracking/lib/adapters/bct/auth/basic.js +23 -0
  72. package/dist/modules/terminal_tracking/lib/adapters/bct/auth/basic.js.map +7 -0
  73. package/dist/modules/terminal_tracking/lib/adapters/bct/incos-semantics.js +146 -0
  74. package/dist/modules/terminal_tracking/lib/adapters/bct/incos-semantics.js.map +7 -0
  75. package/dist/modules/terminal_tracking/lib/adapters/bct/types.js +1 -0
  76. package/dist/modules/terminal_tracking/lib/adapters/bct/types.js.map +7 -0
  77. package/dist/modules/terminal_tracking/lib/adapters/gct/adapter.js +106 -0
  78. package/dist/modules/terminal_tracking/lib/adapters/gct/adapter.js.map +7 -0
  79. package/dist/modules/terminal_tracking/lib/adapters/gct/auth/token.js +120 -0
  80. package/dist/modules/terminal_tracking/lib/adapters/gct/auth/token.js.map +7 -0
  81. package/dist/modules/terminal_tracking/lib/adapters/gct/gct-semantics.js +77 -0
  82. package/dist/modules/terminal_tracking/lib/adapters/gct/gct-semantics.js.map +7 -0
  83. package/dist/modules/terminal_tracking/lib/adapters/gct/types.js +1 -0
  84. package/dist/modules/terminal_tracking/lib/adapters/gct/types.js.map +7 -0
  85. package/dist/modules/terminal_tracking/lib/adapters/index.js +12 -0
  86. package/dist/modules/terminal_tracking/lib/adapters/index.js.map +7 -0
  87. package/dist/modules/terminal_tracking/lib/adapters/n4/adapter.js +121 -0
  88. package/dist/modules/terminal_tracking/lib/adapters/n4/adapter.js.map +7 -0
  89. package/dist/modules/terminal_tracking/lib/adapters/n4/auth/ropc.js +81 -0
  90. package/dist/modules/terminal_tracking/lib/adapters/n4/auth/ropc.js.map +7 -0
  91. package/dist/modules/terminal_tracking/lib/adapters/n4/http.js +66 -0
  92. package/dist/modules/terminal_tracking/lib/adapters/n4/http.js.map +7 -0
  93. package/dist/modules/terminal_tracking/lib/adapters/n4/n4-semantics.js +95 -0
  94. package/dist/modules/terminal_tracking/lib/adapters/n4/n4-semantics.js.map +7 -0
  95. package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.js +70 -0
  96. package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.js.map +7 -0
  97. package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.js +44 -0
  98. package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.js.map +7 -0
  99. package/dist/modules/terminal_tracking/lib/adapters/n4/proxy.js +15 -0
  100. package/dist/modules/terminal_tracking/lib/adapters/n4/proxy.js.map +7 -0
  101. package/dist/modules/terminal_tracking/lib/adapters/n4/types.js +1 -0
  102. package/dist/modules/terminal_tracking/lib/adapters/n4/types.js.map +7 -0
  103. package/dist/modules/terminal_tracking/lib/adapters/zoned-time.js +27 -0
  104. package/dist/modules/terminal_tracking/lib/adapters/zoned-time.js.map +7 -0
  105. package/dist/modules/terminal_tracking/lib/availability.js +40 -0
  106. package/dist/modules/terminal_tracking/lib/availability.js.map +7 -0
  107. package/dist/modules/terminal_tracking/lib/ensure-schedule.js +51 -0
  108. package/dist/modules/terminal_tracking/lib/ensure-schedule.js.map +7 -0
  109. package/dist/modules/terminal_tracking/lib/holds.js +240 -0
  110. package/dist/modules/terminal_tracking/lib/holds.js.map +7 -0
  111. package/dist/modules/terminal_tracking/lib/logger.js +19 -0
  112. package/dist/modules/terminal_tracking/lib/logger.js.map +7 -0
  113. package/dist/modules/terminal_tracking/lib/rate-limiter.js +30 -0
  114. package/dist/modules/terminal_tracking/lib/rate-limiter.js.map +7 -0
  115. package/dist/modules/terminal_tracking/lib/stops.js +48 -0
  116. package/dist/modules/terminal_tracking/lib/stops.js.map +7 -0
  117. package/dist/modules/terminal_tracking/lib/terminal-adapter.js +1 -0
  118. package/dist/modules/terminal_tracking/lib/terminal-adapter.js.map +7 -0
  119. package/dist/modules/terminal_tracking/lib/totp.js +41 -0
  120. package/dist/modules/terminal_tracking/lib/totp.js.map +7 -0
  121. package/dist/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.js +101 -0
  122. package/dist/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.js.map +7 -0
  123. package/dist/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.js +39 -0
  124. package/dist/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.js.map +7 -0
  125. package/dist/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.js +13 -0
  126. package/dist/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.js.map +7 -0
  127. package/dist/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.js +15 -0
  128. package/dist/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.js.map +7 -0
  129. package/dist/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.js +13 -0
  130. package/dist/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.js.map +7 -0
  131. package/dist/modules/terminal_tracking/services/terminalMatcherService.js +61 -0
  132. package/dist/modules/terminal_tracking/services/terminalMatcherService.js.map +7 -0
  133. package/dist/modules/terminal_tracking/services/terminalRegistry.js +24 -0
  134. package/dist/modules/terminal_tracking/services/terminalRegistry.js.map +7 -0
  135. package/dist/modules/terminal_tracking/services/terminalTrackingService.js +687 -0
  136. package/dist/modules/terminal_tracking/services/terminalTrackingService.js.map +7 -0
  137. package/dist/modules/terminal_tracking/setup.js +23 -0
  138. package/dist/modules/terminal_tracking/setup.js.map +7 -0
  139. package/dist/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.js +22 -0
  140. package/dist/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.js.map +7 -0
  141. package/generated/entities/terminal_config/index.ts +36 -0
  142. package/generated/entities/terminal_event/index.ts +39 -0
  143. package/generated/entities/terminal_tracking_job/index.ts +28 -0
  144. package/generated/entities/terminal_vessel_visit/index.ts +30 -0
  145. package/generated/entities.ids.generated.ts +52 -0
  146. package/package.json +80 -0
  147. package/src/index.ts +44 -0
  148. package/src/modules/terminal_tracking/__integration__/TC-TRACK-301-gct-config-test-connection.spec.ts +66 -0
  149. package/src/modules/terminal_tracking/__integration__/TC-TRACK-317-bct-config-test-connection.spec.ts +68 -0
  150. package/src/modules/terminal_tracking/__integration__/helpers.ts +148 -0
  151. package/src/modules/terminal_tracking/__integration__/meta.ts +4 -0
  152. package/src/modules/terminal_tracking/acl.ts +10 -0
  153. package/src/modules/terminal_tracking/api/openapi.ts +28 -0
  154. package/src/modules/terminal_tracking/api/terminal-configs/route.ts +140 -0
  155. package/src/modules/terminal_tracking/api/terminal-configs/test/route.ts +64 -0
  156. package/src/modules/terminal_tracking/api/tracking-jobs/details/route.ts +116 -0
  157. package/src/modules/terminal_tracking/api/tracking-jobs/poll/route.ts +64 -0
  158. package/src/modules/terminal_tracking/api/tracking-jobs/route.ts +243 -0
  159. package/src/modules/terminal_tracking/api/utils.ts +10 -0
  160. package/src/modules/terminal_tracking/api/vessel-visits/route.ts +95 -0
  161. package/src/modules/terminal_tracking/backend/terminal-configs/page.meta.ts +16 -0
  162. package/src/modules/terminal_tracking/backend/terminal-configs/page.tsx +728 -0
  163. package/src/modules/terminal_tracking/backend/terminal-containers/page.meta.ts +16 -0
  164. package/src/modules/terminal_tracking/backend/terminal-containers/page.tsx +328 -0
  165. package/src/modules/terminal_tracking/ce.ts +31 -0
  166. package/src/modules/terminal_tracking/commands/index.ts +3 -0
  167. package/src/modules/terminal_tracking/commands/terminal-configs.ts +132 -0
  168. package/src/modules/terminal_tracking/commands/terminal-tracking.ts +34 -0
  169. package/src/modules/terminal_tracking/commands/tracking-jobs.ts +69 -0
  170. package/src/modules/terminal_tracking/components/ContainerDetailsDrawer.tsx +540 -0
  171. package/src/modules/terminal_tracking/data/entities.ts +386 -0
  172. package/src/modules/terminal_tracking/data/validators.ts +113 -0
  173. package/src/modules/terminal_tracking/di.ts +46 -0
  174. package/src/modules/terminal_tracking/encryption.ts +12 -0
  175. package/src/modules/terminal_tracking/events.ts +48 -0
  176. package/src/modules/terminal_tracking/i18n/de.json +175 -0
  177. package/src/modules/terminal_tracking/i18n/en.json +178 -0
  178. package/src/modules/terminal_tracking/i18n/es.json +175 -0
  179. package/src/modules/terminal_tracking/i18n/pl.json +206 -0
  180. package/src/modules/terminal_tracking/index.ts +13 -0
  181. package/src/modules/terminal_tracking/lib/__tests__/availability.test.ts +136 -0
  182. package/src/modules/terminal_tracking/lib/__tests__/ensure-schedule.test.ts +67 -0
  183. package/src/modules/terminal_tracking/lib/__tests__/holds.test.ts +73 -0
  184. package/src/modules/terminal_tracking/lib/__tests__/stops.test.ts +80 -0
  185. package/src/modules/terminal_tracking/lib/__tests__/totp.test.ts +38 -0
  186. package/src/modules/terminal_tracking/lib/adapters/bct/__tests__/adapter.test.ts +196 -0
  187. package/src/modules/terminal_tracking/lib/adapters/bct/__tests__/incos-semantics.test.ts +303 -0
  188. package/src/modules/terminal_tracking/lib/adapters/bct/adapter.ts +219 -0
  189. package/src/modules/terminal_tracking/lib/adapters/bct/auth/basic.ts +34 -0
  190. package/src/modules/terminal_tracking/lib/adapters/bct/incos-semantics.ts +291 -0
  191. package/src/modules/terminal_tracking/lib/adapters/bct/types.ts +113 -0
  192. package/src/modules/terminal_tracking/lib/adapters/gct/__tests__/adapter.test.ts +238 -0
  193. package/src/modules/terminal_tracking/lib/adapters/gct/__tests__/gct-semantics.test.ts +133 -0
  194. package/src/modules/terminal_tracking/lib/adapters/gct/adapter.ts +152 -0
  195. package/src/modules/terminal_tracking/lib/adapters/gct/auth/token.ts +219 -0
  196. package/src/modules/terminal_tracking/lib/adapters/gct/gct-semantics.ts +136 -0
  197. package/src/modules/terminal_tracking/lib/adapters/gct/types.ts +58 -0
  198. package/src/modules/terminal_tracking/lib/adapters/index.ts +16 -0
  199. package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/adapter-proxy.test.ts +69 -0
  200. package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/data-table.test.ts +179 -0
  201. package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/http.test.ts +98 -0
  202. package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/n4-semantics.test.ts +140 -0
  203. package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/proxy.test.ts +19 -0
  204. package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/vessel-data-table.test.ts +67 -0
  205. package/src/modules/terminal_tracking/lib/adapters/n4/adapter.ts +163 -0
  206. package/src/modules/terminal_tracking/lib/adapters/n4/auth/ropc.ts +104 -0
  207. package/src/modules/terminal_tracking/lib/adapters/n4/http.ts +127 -0
  208. package/src/modules/terminal_tracking/lib/adapters/n4/n4-semantics.ts +156 -0
  209. package/src/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.ts +102 -0
  210. package/src/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.ts +60 -0
  211. package/src/modules/terminal_tracking/lib/adapters/n4/proxy.ts +24 -0
  212. package/src/modules/terminal_tracking/lib/adapters/n4/types.ts +56 -0
  213. package/src/modules/terminal_tracking/lib/adapters/zoned-time.ts +65 -0
  214. package/src/modules/terminal_tracking/lib/availability.ts +119 -0
  215. package/src/modules/terminal_tracking/lib/ensure-schedule.ts +83 -0
  216. package/src/modules/terminal_tracking/lib/holds.ts +344 -0
  217. package/src/modules/terminal_tracking/lib/logger.ts +21 -0
  218. package/src/modules/terminal_tracking/lib/rate-limiter.ts +52 -0
  219. package/src/modules/terminal_tracking/lib/stops.ts +124 -0
  220. package/src/modules/terminal_tracking/lib/terminal-adapter.ts +136 -0
  221. package/src/modules/terminal_tracking/lib/totp.ts +63 -0
  222. package/src/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.ts +104 -0
  223. package/src/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.ts +40 -0
  224. package/src/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.ts +11 -0
  225. package/src/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.ts +13 -0
  226. package/src/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.ts +16 -0
  227. package/src/modules/terminal_tracking/services/__tests__/availabilityEmit.test.ts +277 -0
  228. package/src/modules/terminal_tracking/services/__tests__/createJob.test.ts +59 -0
  229. package/src/modules/terminal_tracking/services/__tests__/pollAllBatch.test.ts +147 -0
  230. package/src/modules/terminal_tracking/services/__tests__/terminalMatcher.test.ts +70 -0
  231. package/src/modules/terminal_tracking/services/__tests__/vesselResolution.test.ts +453 -0
  232. package/src/modules/terminal_tracking/services/terminalMatcherService.ts +97 -0
  233. package/src/modules/terminal_tracking/services/terminalRegistry.ts +30 -0
  234. package/src/modules/terminal_tracking/services/terminalTrackingService.ts +912 -0
  235. package/src/modules/terminal_tracking/setup.ts +24 -0
  236. package/src/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.ts +32 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -0,0 +1,27 @@
1
+ const DEFAULT_TERMINAL_TIMEZONE = "Europe/Warsaw";
2
+ function zoneOffsetMs(utcMs, timeZone) {
3
+ const parts = new Intl.DateTimeFormat("en-US", {
4
+ timeZone,
5
+ hour12: false,
6
+ year: "numeric",
7
+ month: "2-digit",
8
+ day: "2-digit",
9
+ hour: "2-digit",
10
+ minute: "2-digit",
11
+ second: "2-digit"
12
+ }).formatToParts(new Date(utcMs));
13
+ const f = {};
14
+ for (const p of parts) if (p.type !== "literal") f[p.type] = Number(p.value);
15
+ const hour = f.hour === 24 ? 0 : f.hour;
16
+ const asIfUtc = Date.UTC(f.year, f.month - 1, f.day, hour, f.minute, f.second);
17
+ return asIfUtc - utcMs;
18
+ }
19
+ function zonedNaiveToUtc(year, month, day, hour, minute, second, timeZone) {
20
+ const asUtc = Date.UTC(year, month - 1, day, hour, minute, second);
21
+ return new Date(asUtc - zoneOffsetMs(asUtc, timeZone));
22
+ }
23
+ export {
24
+ DEFAULT_TERMINAL_TIMEZONE,
25
+ zonedNaiveToUtc
26
+ };
27
+ //# sourceMappingURL=zoned-time.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/terminal_tracking/lib/adapters/zoned-time.ts"],
4
+ "sourcesContent": ["/**\n * Terminal timestamp timezone handling.\n *\n * Every terminal we integrate (BCT Gda\u0144sk via N4, GCT and BCT Gdynia) reports\n * event times as a naive wall-clock string \u2014 `2026-08-21 11:57`, with no offset.\n * Those are *terminal-local* times, not UTC. Reading them as UTC (the old\n * behaviour: append `Z`) shifted every displayed time by the zone's offset \u2014 in\n * summer Europe/Warsaw is CEST (+02:00), so a gate-out of 11:57 rendered as\n * 13:57 in the (Warsaw) UI.\n *\n * All current terminals sit in `Europe/Warsaw`; that is the default. When a\n * terminal outside Poland is onboarded, pass its IANA zone into the parsers\n * instead of relying on this constant.\n */\nexport const DEFAULT_TERMINAL_TIMEZONE = 'Europe/Warsaw'\n\n/**\n * How far ahead of UTC `timeZone` is at the instant `utcMs`, in milliseconds.\n * Uses `Intl` so DST is honoured (CET/CEST for Europe/Warsaw) without any\n * external timezone library.\n */\nfunction zoneOffsetMs(utcMs: number, timeZone: string): number {\n const parts = new Intl.DateTimeFormat('en-US', {\n timeZone,\n hour12: false,\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n }).formatToParts(new Date(utcMs))\n const f: Record<string, number> = {}\n for (const p of parts) if (p.type !== 'literal') f[p.type] = Number(p.value)\n // Some engines render midnight as hour 24 rather than 0.\n const hour = f.hour === 24 ? 0 : f.hour\n const asIfUtc = Date.UTC(f.year, f.month - 1, f.day, hour, f.minute, f.second)\n return asIfUtc - utcMs\n}\n\n/**\n * Convert a naive wall-clock datetime \u2014 its components written in `timeZone`\n * with no offset \u2014 to the correct UTC instant.\n *\n * e.g. `(2026, 8, 21, 11, 57, 0, 'Europe/Warsaw')` during CEST (+02:00) yields\n * `2026-08-21T09:57:00.000Z`.\n *\n * `month` is 1-based (January = 1). Within a DST transition (the ~1h gap or\n * overlap twice a year) the result may be off by the DST delta \u2014 acceptable for\n * terminal event timestamps, which never land inside that window in practice.\n */\nexport function zonedNaiveToUtc(\n year: number,\n month: number,\n day: number,\n hour: number,\n minute: number,\n second: number,\n timeZone: string,\n): Date {\n // First approximation: pretend the wall-clock components are already UTC,\n // then subtract the zone's offset at (approximately) that instant.\n const asUtc = Date.UTC(year, month - 1, day, hour, minute, second)\n return new Date(asUtc - zoneOffsetMs(asUtc, timeZone))\n}\n"],
5
+ "mappings": "AAcO,MAAM,4BAA4B;AAOzC,SAAS,aAAa,OAAe,UAA0B;AAC7D,QAAM,QAAQ,IAAI,KAAK,eAAe,SAAS;AAAA,IAC7C;AAAA,IACA,QAAQ;AAAA,IACR,MAAM;AAAA,IACN,OAAO;AAAA,IACP,KAAK;AAAA,IACL,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,EACV,CAAC,EAAE,cAAc,IAAI,KAAK,KAAK,CAAC;AAChC,QAAM,IAA4B,CAAC;AACnC,aAAW,KAAK,MAAO,KAAI,EAAE,SAAS,UAAW,GAAE,EAAE,IAAI,IAAI,OAAO,EAAE,KAAK;AAE3E,QAAM,OAAO,EAAE,SAAS,KAAK,IAAI,EAAE;AACnC,QAAM,UAAU,KAAK,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAG,EAAE,KAAK,MAAM,EAAE,QAAQ,EAAE,MAAM;AAC7E,SAAO,UAAU;AACnB;AAaO,SAAS,gBACd,MACA,OACA,KACA,MACA,QACA,QACA,UACM;AAGN,QAAM,QAAQ,KAAK,IAAI,MAAM,QAAQ,GAAG,KAAK,MAAM,QAAQ,MAAM;AACjE,SAAO,IAAI,KAAK,QAAQ,aAAa,OAAO,QAAQ,CAAC;AACvD;",
6
+ "names": []
7
+ }
@@ -0,0 +1,40 @@
1
+ import { resolveHolds } from "./holds.js";
2
+ import { parseTerminalStops } from "./stops.js";
3
+ const YARD_STATE = "S40_YARD";
4
+ function blockingHoldCount(impediments) {
5
+ return resolveHolds(impediments ?? null).filter((h) => h.blocksMovement).length;
6
+ }
7
+ function impedimentsChanged(prior, current) {
8
+ const norm = (l) => Array.from(new Set((l ?? []).map((c) => c.trim()).filter((c) => c !== ""))).sort();
9
+ const a = norm(prior);
10
+ const b = norm(current);
11
+ if (a.length !== b.length) return true;
12
+ return a.some((code, i) => code !== b[i]);
13
+ }
14
+ function category(e) {
15
+ const c = String(e.rawData?.["Category"] ?? "").trim().toLowerCase();
16
+ return c === "import" || c === "export" ? c : null;
17
+ }
18
+ function isEmpty(e) {
19
+ const kind = String(e.rawData?.["Frght Kind"] ?? "").trim().toUpperCase();
20
+ return kind === "EMPTY" || kind === "MTY";
21
+ }
22
+ function isInYard(e) {
23
+ return e.transitState === YARD_STATE;
24
+ }
25
+ function isRoadPickupStopped(e) {
26
+ return parseTerminalStops(e.rawData).road === true;
27
+ }
28
+ function isEmptyReady(e) {
29
+ return isEmpty(e) && isInYard(e) && blockingHoldCount(e.impediments) === 0 && !isRoadPickupStopped(e);
30
+ }
31
+ function isImportHoldsCleared(e, priorImpediments) {
32
+ return category(e) === "import" && isInYard(e) && blockingHoldCount(priorImpediments) > 0 && blockingHoldCount(e.impediments) === 0;
33
+ }
34
+ export {
35
+ blockingHoldCount,
36
+ impedimentsChanged,
37
+ isEmptyReady,
38
+ isImportHoldsCleared
39
+ };
40
+ //# sourceMappingURL=availability.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/terminal_tracking/lib/availability.ts"],
4
+ "sourcesContent": ["/**\n * Availability detection for terminal containers.\n *\n * Derives two business signals from the latest N4 `/unit` snapshot of a\n * container, reusing the hold catalogue in `./holds` to decide what actually\n * blocks a physical pickup:\n *\n * - **Empty-ready** \u2014 an empty container sitting in the yard with no\n * movement-blocking holds is releasable for collection. Per Baltic Hub's\n * Navis docs, `EMPTY PERMISSION` is applied to *every* empty regardless of\n * direction and is cleared once the carrier's EDO/booking is provided; empties\n * are handled under N4 category `Storage`, not `Export` \u2014 so this does not\n * gate on category.\n * - **Import holds-cleared** \u2014 an import container in the yard whose blocking\n * holds have just been lifted (transition blocked \u2192 clear) has become\n * collectable.\n *\n * Pure helpers \u2014 no DI, no persistence. The service owns the one-shot guard\n * (job-level `emptyReadyAt` / `holdsClearedAt`).\n */\nimport { resolveHolds } from './holds'\nimport { parseTerminalStops } from './stops'\n\n/** N4 in-yard transit state (discharged, available on the terminal). */\nconst YARD_STATE = 'S40_YARD'\n\n/** Minimal shape both a fetched event and a persisted `TerminalEvent` satisfy. */\nexport type AvailabilityInput = {\n transitState?: string | null\n impediments?: string[] | null\n rawData?: Record<string, unknown> | null\n}\n\n/** Count holds that block physical movement / loading / pickup. */\nexport function blockingHoldCount(impediments: readonly string[] | null | undefined): number {\n return resolveHolds(impediments ?? null).filter((h) => h.blocksMovement).length\n}\n\n/**\n * Whether two raw impediment lists differ as sets (order- and duplicate-\n * insensitive, whitespace-trimmed). An absent/null prior is treated as empty,\n * so a first-seen container carrying holds counts as a change. Drives the\n * `equipment.holds_updated` event (fires on every poll the hold set changes).\n */\nexport function impedimentsChanged(\n prior: readonly string[] | null | undefined,\n current: readonly string[] | null | undefined,\n): boolean {\n const norm = (l: readonly string[] | null | undefined): string[] =>\n Array.from(new Set((l ?? []).map((c) => c.trim()).filter((c) => c !== ''))).sort()\n const a = norm(prior)\n const b = norm(current)\n if (a.length !== b.length) return true\n return a.some((code, i) => code !== b[i])\n}\n\n/** N4 `Category` column, normalized to import/export (null if absent/unknown). */\nfunction category(e: AvailabilityInput): 'import' | 'export' | null {\n const c = String(e.rawData?.['Category'] ?? '').trim().toLowerCase()\n return c === 'import' || c === 'export' ? c : null\n}\n\n/**\n * Whether the container is empty, per the N4 `Frght Kind` column. Baltic Hub\n * reports empties as `Empty` (confirmed against a real /unit payload, 2026-07);\n * `MTY` is kept as a defensive alias for other N4 terminals. Full containers\n * report `FCL`/`LCL`.\n */\nfunction isEmpty(e: AvailabilityInput): boolean {\n const kind = String(e.rawData?.['Frght Kind'] ?? '').trim().toUpperCase()\n return kind === 'EMPTY' || kind === 'MTY'\n}\n\nfunction isInYard(e: AvailabilityInput): boolean {\n return e.transitState === YARD_STATE\n}\n\n/**\n * Whether the terminal has explicitly stopped road pickup for this container\n * (Stop-Road). Only an *explicit* stop counts \u2014 an absent column (`null`, the\n * pre-update world) or a `false` never blocks, keeping this purely additive.\n *\n * This gates only the **state-based** `isEmptyReady` below, where a road stop\n * simply *delays* the one-shot until it lifts (the signal is re-evaluated every\n * poll). The **transition-based** `isImportHoldsCleared` is deliberately NOT\n * gated: it fires on the holds blocked\u2192clear edge, so suppressing it during a\n * road stop would lose the event entirely once the edge has passed, rather than\n * defer it \u2014 the container's holds still genuinely cleared.\n */\nfunction isRoadPickupStopped(e: AvailabilityInput): boolean {\n return parseTerminalStops(e.rawData).road === true\n}\n\n/**\n * Empty ready for pickup: an empty container in the yard with no movement-\n * blocking holds (its `EMPTY PERMISSION` and any others have cleared). State-\n * based \u2014 evaluate on the latest snapshot. Direction-agnostic, since empties\n * sit under N4 category `Storage` rather than `Export`.\n */\nexport function isEmptyReady(e: AvailabilityInput): boolean {\n return isEmpty(e) && isInYard(e) && blockingHoldCount(e.impediments) === 0 && !isRoadPickupStopped(e)\n}\n\n/**\n * Import holds cleared: an import container in the yard whose blocking holds\n * went from present \u2192 none this poll. Transition-based, so it never fires for a\n * container that was never blocked.\n */\nexport function isImportHoldsCleared(\n e: AvailabilityInput,\n priorImpediments: readonly string[] | null | undefined,\n): boolean {\n return (\n category(e) === 'import' &&\n isInYard(e) &&\n blockingHoldCount(priorImpediments) > 0 &&\n blockingHoldCount(e.impediments) === 0\n )\n}\n"],
5
+ "mappings": "AAoBA,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AAGnC,MAAM,aAAa;AAUZ,SAAS,kBAAkB,aAA2D;AAC3F,SAAO,aAAa,eAAe,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,cAAc,EAAE;AAC3E;AAQO,SAAS,mBACd,OACA,SACS;AACT,QAAM,OAAO,CAAC,MACZ,MAAM,KAAK,IAAI,KAAK,KAAK,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,MAAM,MAAM,EAAE,CAAC,CAAC,EAAE,KAAK;AACnF,QAAM,IAAI,KAAK,KAAK;AACpB,QAAM,IAAI,KAAK,OAAO;AACtB,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,SAAO,EAAE,KAAK,CAAC,MAAM,MAAM,SAAS,EAAE,CAAC,CAAC;AAC1C;AAGA,SAAS,SAAS,GAAkD;AAClE,QAAM,IAAI,OAAO,EAAE,UAAU,UAAU,KAAK,EAAE,EAAE,KAAK,EAAE,YAAY;AACnE,SAAO,MAAM,YAAY,MAAM,WAAW,IAAI;AAChD;AAQA,SAAS,QAAQ,GAA+B;AAC9C,QAAM,OAAO,OAAO,EAAE,UAAU,YAAY,KAAK,EAAE,EAAE,KAAK,EAAE,YAAY;AACxE,SAAO,SAAS,WAAW,SAAS;AACtC;AAEA,SAAS,SAAS,GAA+B;AAC/C,SAAO,EAAE,iBAAiB;AAC5B;AAcA,SAAS,oBAAoB,GAA+B;AAC1D,SAAO,mBAAmB,EAAE,OAAO,EAAE,SAAS;AAChD;AAQO,SAAS,aAAa,GAA+B;AAC1D,SAAO,QAAQ,CAAC,KAAK,SAAS,CAAC,KAAK,kBAAkB,EAAE,WAAW,MAAM,KAAK,CAAC,oBAAoB,CAAC;AACtG;AAOO,SAAS,qBACd,GACA,kBACS;AACT,SACE,SAAS,CAAC,MAAM,YAChB,SAAS,CAAC,KACV,kBAAkB,gBAAgB,IAAI,KACtC,kBAAkB,EAAE,WAAW,MAAM;AAEzC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,51 @@
1
+ import { createHash } from "node:crypto";
2
+ import { terminalLogger } from "./logger.js";
3
+ function deterministicUuid(seed) {
4
+ const hex = createHash("sha1").update(seed).digest("hex").slice(0, 32).split("");
5
+ hex[12] = "5";
6
+ hex[16] = (parseInt(hex[16], 16) & 3 | 8).toString(16);
7
+ const s = hex.join("");
8
+ return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
9
+ }
10
+ async function ensureTerminalPollSchedule(container, scope) {
11
+ let scheduler;
12
+ try {
13
+ scheduler = container.resolve("schedulerService");
14
+ } catch {
15
+ return;
16
+ }
17
+ if (!scheduler?.register) return;
18
+ const id = deterministicUuid(`terminal_tracking:poll_all:${scope.organizationId}:${scope.tenantId}`);
19
+ try {
20
+ if (scheduler.exists && await scheduler.exists(id)) return;
21
+ } catch {
22
+ }
23
+ try {
24
+ await scheduler.register({
25
+ id,
26
+ name: "Terminal Tracking Poll",
27
+ description: "Polls all active terminal tracking jobs every 4 hours. Change the cadence in the scheduler settings.",
28
+ scopeType: "organization",
29
+ organizationId: scope.organizationId,
30
+ tenantId: scope.tenantId,
31
+ scheduleType: "cron",
32
+ scheduleValue: "0 */4 * * *",
33
+ timezone: "UTC",
34
+ targetType: "command",
35
+ targetCommand: "terminal_tracking.tracking.poll_all",
36
+ targetPayload: { tenantId: scope.tenantId, organizationId: scope.organizationId },
37
+ sourceType: "module",
38
+ sourceModule: "terminal_tracking",
39
+ isEnabled: true
40
+ });
41
+ } catch (err) {
42
+ terminalLogger.warn("Failed to ensure terminal poll schedule", {
43
+ organizationId: scope.organizationId,
44
+ message: err instanceof Error ? err.message : String(err)
45
+ });
46
+ }
47
+ }
48
+ export {
49
+ ensureTerminalPollSchedule
50
+ };
51
+ //# sourceMappingURL=ensure-schedule.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/terminal_tracking/lib/ensure-schedule.ts"],
4
+ "sourcesContent": ["import { createHash } from 'node:crypto'\nimport { terminalLogger } from './logger'\n\ntype SchedulerLike = {\n register: (registration: Record<string, unknown>) => Promise<void>\n exists?: (scheduleId: string) => Promise<boolean>\n}\n\ntype ResolveLike = { resolve: <T = unknown>(name: string) => T }\n\n/**\n * Deterministic RFC 4122 (v5-style) uuid derived from a stable seed, so the same\n * (org, tenant) always maps to the same scheduled-job id. The scheduler upserts\n * by id (idempotent `register`), and consumers validate with `z.uuid()`, so the\n * version (nibble 13 = '5') and variant (nibble 17 = 8-b) bits must be set.\n */\nfunction deterministicUuid(seed: string): string {\n const hex = createHash('sha1').update(seed).digest('hex').slice(0, 32).split('')\n hex[12] = '5' // version 5\n hex[16] = ((parseInt(hex[16], 16) & 0x3) | 0x8).toString(16) // variant 10xx -> 8..b\n const s = hex.join('')\n return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`\n}\n\n/**\n * Ensure the periodic \"poll all\" schedule exists for an org/tenant \u2014 create-only.\n *\n * The schedule is seeded with the module defaults the first time only. Once it\n * exists, this no-ops so user edits in the scheduler settings (cadence, enabled\n * state) are the source of truth and are never overwritten \u2014 `setup.ts` and the\n * config/job-save callers re-run this purely to self-heal orgs that predate the\n * module. Stays a single row thanks to the deterministic id. No-ops gracefully\n * when the optional scheduler module isn't installed.\n */\nexport async function ensureTerminalPollSchedule(\n container: ResolveLike,\n scope: { organizationId: string; tenantId: string },\n): Promise<void> {\n let scheduler: SchedulerLike\n try {\n scheduler = container.resolve<SchedulerLike>('schedulerService')\n } catch {\n return // scheduler module not installed\n }\n if (!scheduler?.register) return\n\n const id = deterministicUuid(`terminal_tracking:poll_all:${scope.organizationId}:${scope.tenantId}`)\n\n // Create-only: never overwrite an existing schedule, so UI cadence/enabled edits win.\n // (`register` is a full upsert, so without this guard every config/job save would\n // revert the row to the defaults below.)\n try {\n if (scheduler.exists && (await scheduler.exists(id))) return\n } catch {\n // Existence check is best-effort; fall through to register on failure.\n }\n\n try {\n await scheduler.register({\n id,\n name: 'Terminal Tracking Poll',\n description:\n 'Polls all active terminal tracking jobs every 4 hours. Change the cadence in the scheduler settings.',\n scopeType: 'organization',\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n scheduleType: 'cron',\n scheduleValue: '0 */4 * * *',\n timezone: 'UTC',\n targetType: 'command',\n targetCommand: 'terminal_tracking.tracking.poll_all',\n targetPayload: { tenantId: scope.tenantId, organizationId: scope.organizationId },\n sourceType: 'module',\n sourceModule: 'terminal_tracking',\n isEnabled: true,\n })\n } catch (err) {\n terminalLogger.warn('Failed to ensure terminal poll schedule', {\n organizationId: scope.organizationId,\n message: err instanceof Error ? err.message : String(err),\n })\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,kBAAkB;AAC3B,SAAS,sBAAsB;AAe/B,SAAS,kBAAkB,MAAsB;AAC/C,QAAM,MAAM,WAAW,MAAM,EAAE,OAAO,IAAI,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE,EAAE,MAAM,EAAE;AAC/E,MAAI,EAAE,IAAI;AACV,MAAI,EAAE,KAAM,SAAS,IAAI,EAAE,GAAG,EAAE,IAAI,IAAO,GAAK,SAAS,EAAE;AAC3D,QAAM,IAAI,IAAI,KAAK,EAAE;AACrB,SAAO,GAAG,EAAE,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,IAAI,EAAE,CAAC;AACpG;AAYA,eAAsB,2BACpB,WACA,OACe;AACf,MAAI;AACJ,MAAI;AACF,gBAAY,UAAU,QAAuB,kBAAkB;AAAA,EACjE,QAAQ;AACN;AAAA,EACF;AACA,MAAI,CAAC,WAAW,SAAU;AAE1B,QAAM,KAAK,kBAAkB,8BAA8B,MAAM,cAAc,IAAI,MAAM,QAAQ,EAAE;AAKnG,MAAI;AACF,QAAI,UAAU,UAAW,MAAM,UAAU,OAAO,EAAE,EAAI;AAAA,EACxD,QAAQ;AAAA,EAER;AAEA,MAAI;AACF,UAAM,UAAU,SAAS;AAAA,MACvB;AAAA,MACA,MAAM;AAAA,MACN,aACE;AAAA,MACF,WAAW;AAAA,MACX,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,MAChB,cAAc;AAAA,MACd,eAAe;AAAA,MACf,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,eAAe;AAAA,MACf,eAAe,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe;AAAA,MAChF,YAAY;AAAA,MACZ,cAAc;AAAA,MACd,WAAW;AAAA,IACb,CAAC;AAAA,EACH,SAAS,KAAK;AACZ,mBAAe,KAAK,2CAA2C;AAAA,MAC7D,gBAAgB,MAAM;AAAA,MACtB,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAAA,IAC1D,CAAC;AAAA,EACH;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,240 @@
1
+ function holdI18nKey(normalizedCode) {
2
+ return normalizedCode.toLowerCase().replace(/ /g, "_");
3
+ }
4
+ const HOLD_SEVERITY_RANK = {
5
+ critical: 3,
6
+ warning: 2,
7
+ info: 1
8
+ };
9
+ function normalizeHoldCode(rawCode) {
10
+ return rawCode.replace(/^!/, "").replace(/[_\s]+/g, " ").trim().toUpperCase();
11
+ }
12
+ function def(code, category, severity, blocksMovement, label, description, resolution) {
13
+ return { code, category, severity, blocksMovement, label, description, resolution };
14
+ }
15
+ const HOLD_CATALOGUE = {
16
+ // ── Import ────────────────────────────────────────────────────────────────
17
+ "CFS APPOINTMENT": def(
18
+ "CFS APPOINTMENT",
19
+ "import",
20
+ "warning",
21
+ true,
22
+ "CFS appointment",
23
+ "Applied while a CFS (warehouse) import order is being accepted.",
24
+ "Cleared automatically once the order is completed and warehouse work is done."
25
+ ),
26
+ "CUSTOMS IMPORT PERMISSION": def(
27
+ "CUSTOMS IMPORT PERMISSION",
28
+ "import",
29
+ "warning",
30
+ true,
31
+ "Customs import permission",
32
+ "Applied systematically to every import container pending customs release.",
33
+ "Removed automatically after the MRN number is updated via the Polish PCS codes."
34
+ ),
35
+ "CUSTOMS IMPORT HOLD": def(
36
+ "CUSTOMS IMPORT HOLD",
37
+ "import",
38
+ "critical",
39
+ true,
40
+ "Customs import hold",
41
+ "Placed manually by a customs officer for scanning, inspection or a clearance issue.",
42
+ "Only a customs officer can lift it \u2014 contact the customs office."
43
+ ),
44
+ "LINE IMPORT PERMISSION": def(
45
+ "LINE IMPORT PERMISSION",
46
+ "import",
47
+ "warning",
48
+ true,
49
+ "Line import permission",
50
+ "Applied systematically to every import container until the shipping line releases it.",
51
+ "Removed once the line releases the container via its PIN number."
52
+ ),
53
+ "OOG HOLD": def(
54
+ "OOG HOLD",
55
+ "import",
56
+ "critical",
57
+ true,
58
+ "OOG hold",
59
+ "Applied to out-of-gauge import containers that require special handling permits.",
60
+ "Must be verified by Baltic Hub before pickup; the container cannot be notified for collection until then."
61
+ ),
62
+ // ── Export ────────────────────────────────────────────────────────────────
63
+ "CFS HOLD": def(
64
+ "CFS HOLD",
65
+ "export",
66
+ "warning",
67
+ true,
68
+ "CFS hold",
69
+ "Applied while a CFS (warehouse) export order is being accepted.",
70
+ "Cleared automatically once the order is completed and warehouse work is done."
71
+ ),
72
+ "CUSTOMS EXPORT PERMISSION": def(
73
+ "CUSTOMS EXPORT PERMISSION",
74
+ "export",
75
+ "warning",
76
+ true,
77
+ "Customs export permission",
78
+ "Applied systematically to export containers pending customs release.",
79
+ "Removed automatically after the MRN update; must be cleared before the customs-clearance deadline."
80
+ ),
81
+ "CUSTOMS EXPORT HOLD": def(
82
+ "CUSTOMS EXPORT HOLD",
83
+ "export",
84
+ "critical",
85
+ true,
86
+ "Customs export hold",
87
+ "Placed manually by a customs officer for scanning, inspection or a clearance issue.",
88
+ "Only a customs officer can lift it \u2014 contact the customs office."
89
+ ),
90
+ "UNIT VGM LOAD PERMISSION": def(
91
+ "UNIT VGM LOAD PERMISSION",
92
+ "export",
93
+ "warning",
94
+ true,
95
+ "VGM load permission",
96
+ "Applied to every full export container that has no verified gross mass yet.",
97
+ "Removed when the VGM weight is uploaded to Navis."
98
+ ),
99
+ "DGD HOLD": def(
100
+ "DGD HOLD",
101
+ "export",
102
+ "critical",
103
+ true,
104
+ "DGD hold",
105
+ "Applied automatically to every export container carrying IMO (dangerous) cargo.",
106
+ "Requires the DGD (dangerous-goods declaration) document to be submitted."
107
+ ),
108
+ "SCANNING HOLD": def(
109
+ "SCANNING HOLD",
110
+ "export",
111
+ "critical",
112
+ true,
113
+ "Scanning hold",
114
+ "Placed manually by a customs officer; the container must be presented for scanning.",
115
+ "Cleared by customs after scanning (DCT_CFS_SCANNING selection)."
116
+ ),
117
+ // ── General / technical ─────────────────────────────────────────────────────
118
+ "EMPTY PERMISSION": def(
119
+ "EMPTY PERMISSION",
120
+ "general",
121
+ "warning",
122
+ true,
123
+ "Empty permission",
124
+ "Applied systematically to every empty container pending a release reference.",
125
+ "Removed once the EDO / booking number from the shipping line is provided."
126
+ ),
127
+ "MISSING AGENT PERMISSION": def(
128
+ "MISSING AGENT PERMISSION",
129
+ "general",
130
+ "info",
131
+ false,
132
+ "Missing agent permission",
133
+ "Relates to an inactive PCS payment. Does not currently block ship loading or pickup.",
134
+ "Removed by entering the shipping code in the Agent One field."
135
+ ),
136
+ "TECHNICAL FULL CONTAINER": def(
137
+ "TECHNICAL FULL CONTAINER",
138
+ "general",
139
+ "info",
140
+ false,
141
+ "Technical (full container)",
142
+ "Applied to every full import/export container to support OCR character recognition. Does not block movement.",
143
+ "No action required \u2014 informational only."
144
+ ),
145
+ "TECHNICAL LIVE REEFER": def(
146
+ "TECHNICAL LIVE REEFER",
147
+ "general",
148
+ "info",
149
+ false,
150
+ "Technical (live reefer)",
151
+ "Applied to every full reefer container to support OCR. Does not block operations.",
152
+ "No action required \u2014 informational only."
153
+ ),
154
+ // Baltic Hub's Navis docs spell this "REFFER" (double F); keep both spellings
155
+ // so the real terminal code resolves to the non-blocking definition instead of
156
+ // the blocking unknown-hold fallback.
157
+ "TECHNICAL LIVE REFFER": def(
158
+ "TECHNICAL LIVE REFFER",
159
+ "general",
160
+ "info",
161
+ false,
162
+ "Technical (live reefer)",
163
+ "Applied to every full reefer container to support OCR. Does not block operations.",
164
+ "No action required \u2014 informational only."
165
+ ),
166
+ "TERMINAL HOLD": def(
167
+ "TERMINAL HOLD",
168
+ "general",
169
+ "critical",
170
+ true,
171
+ "Terminal hold",
172
+ "Applied for a container irregularity (incorrect labelling, bent container beams, etc.).",
173
+ "Contact Baltic Hub Customer Service to resolve."
174
+ ),
175
+ "CZRM HOLD": def(
176
+ "CZRM HOLD",
177
+ "general",
178
+ "critical",
179
+ true,
180
+ "CZRM hold",
181
+ "Applied by the customs office.",
182
+ "Contact the customs office directly to have it lifted."
183
+ ),
184
+ "IZRX HOLD": def(
185
+ "IZRX HOLD",
186
+ "general",
187
+ "critical",
188
+ true,
189
+ "IZRX hold",
190
+ "Applied by the customs office.",
191
+ "Contact the customs office directly to have it lifted."
192
+ )
193
+ };
194
+ const HOLD_CODES = Object.keys(HOLD_CATALOGUE).sort((a, b) => {
195
+ const bySeverity = HOLD_SEVERITY_RANK[HOLD_CATALOGUE[b].severity] - HOLD_SEVERITY_RANK[HOLD_CATALOGUE[a].severity];
196
+ return bySeverity !== 0 ? bySeverity : a.localeCompare(b);
197
+ });
198
+ function isKnownHoldCode(code) {
199
+ return Object.prototype.hasOwnProperty.call(HOLD_CATALOGUE, normalizeHoldCode(code));
200
+ }
201
+ function getHoldInfo(rawCode) {
202
+ const key = normalizeHoldCode(rawCode);
203
+ const found = HOLD_CATALOGUE[key];
204
+ if (found) {
205
+ return { ...found, rawCode, unknown: false, i18nKey: holdI18nKey(key) };
206
+ }
207
+ const rawLabel = rawCode.replace(/^!/, "").trim();
208
+ return {
209
+ code: key,
210
+ label: rawLabel || "Unknown hold",
211
+ category: "general",
212
+ severity: "warning",
213
+ blocksMovement: true,
214
+ description: "Unrecognised terminal hold. Refer to the terminal for details.",
215
+ resolution: "Contact the terminal to determine how this hold is cleared.",
216
+ rawCode,
217
+ unknown: true,
218
+ i18nKey: "unknown"
219
+ };
220
+ }
221
+ function resolveHolds(rawCodes) {
222
+ if (!rawCodes || rawCodes.length === 0) return [];
223
+ return rawCodes.filter((c) => c != null && c.trim() !== "").map(getHoldInfo).sort((a, b) => HOLD_SEVERITY_RANK[b.severity] - HOLD_SEVERITY_RANK[a.severity]);
224
+ }
225
+ function highestHoldSeverity(rawCodes) {
226
+ const holds = resolveHolds(rawCodes);
227
+ if (holds.length === 0) return null;
228
+ return holds[0].severity;
229
+ }
230
+ export {
231
+ HOLD_CODES,
232
+ HOLD_SEVERITY_RANK,
233
+ getHoldInfo,
234
+ highestHoldSeverity,
235
+ holdI18nKey,
236
+ isKnownHoldCode,
237
+ normalizeHoldCode,
238
+ resolveHolds
239
+ };
240
+ //# sourceMappingURL=holds.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/terminal_tracking/lib/holds.ts"],
4
+ "sourcesContent": ["/**\n * Unit-impediment (\"hold\") catalogue for Navis N4 terminals (Baltic Hub / BCT).\n *\n * N4 returns the `Unit Impediments` column as a comma-separated list of codes,\n * each prefixed with `!` and delimited inconsistently \u2014 some use underscores\n * (`!TECHNICAL_FULL_CONTAINER`), others spaces (`!CUSTOMS EXPORT PERMISSION`).\n * This module normalizes a raw code and resolves it to a human explanation plus\n * a severity, so the UI can render a coloured badge with an informative tooltip.\n *\n * Source of truth for the catalogue: Baltic Hub documentation\n * https://baltichub.com/dla-klienta/navis\n *\n * Pure helper \u2014 no DI, no i18n, no React. English strings by convention; the\n * consuming component owns localization/colour mapping.\n */\n\n/**\n * How urgently a hold needs attention.\n * - `critical`: blocks movement/load/pickup and typically needs a third party\n * (customs office, terminal CS) to lift \u2014 cannot be self-served.\n * - `warning`: routine, systematic permission that is auto- or self-removed in\n * the normal flow (MRN update, VGM upload, line PIN) but currently blocks.\n * - `info`: technical/administrative marker that does NOT block operations.\n */\nexport type HoldSeverity = 'critical' | 'warning' | 'info'\n\n/** Which container flow the hold belongs to. */\nexport type HoldCategory = 'import' | 'export' | 'general'\n\nexport type HoldDefinition = {\n /** Canonical, normalized code (no `!`, spaced, upper-case). */\n code: string\n /** Short human label for the badge. */\n label: string\n category: HoldCategory\n severity: HoldSeverity\n /** Whether the hold blocks physical movement / loading / pickup. */\n blocksMovement: boolean\n /** What the hold means / why it is applied. */\n description: string\n /** How the hold is cleared and by whom. */\n resolution: string\n}\n\n/** Resolved hold: a known definition, or an `unknown: true` fallback. */\nexport type HoldInfo = HoldDefinition & {\n /** Original code as received from the terminal (with `!`, original spacing). */\n rawCode: string\n /** True when the code was not in the catalogue. */\n unknown: boolean\n /**\n * Stable slug for i18n lookup, e.g. `dgd_hold`. The `label`/`description`/\n * `resolution` fields above are English fallbacks; the consuming component\n * resolves `terminal_tracking.holds.<i18nKey>.{label,description,resolution}`\n * against the active locale, falling back to those English strings.\n */\n i18nKey: string\n}\n\n/** Derive the i18n slug for a normalized hold code (e.g. `DGD HOLD` \u2192 `dgd_hold`). */\nexport function holdI18nKey(normalizedCode: string): string {\n return normalizedCode.toLowerCase().replace(/ /g, '_')\n}\n\n/** Sort/threshold rank: higher = more urgent. */\nexport const HOLD_SEVERITY_RANK: Record<HoldSeverity, number> = {\n critical: 3,\n warning: 2,\n info: 1,\n}\n\n/**\n * Normalize a raw N4 impediment code into its canonical lookup key: drop the\n * leading `!`, trim, upper-case, and collapse runs of `_`/whitespace into a\n * single space so `TECHNICAL_FULL_CONTAINER` and `TECHNICAL FULL CONTAINER`\n * resolve to the same entry.\n */\nexport function normalizeHoldCode(rawCode: string): string {\n return rawCode\n .replace(/^!/, '')\n .replace(/[_\\s]+/g, ' ')\n .trim()\n .toUpperCase()\n}\n\nfunction def(\n code: string,\n category: HoldCategory,\n severity: HoldSeverity,\n blocksMovement: boolean,\n label: string,\n description: string,\n resolution: string,\n): HoldDefinition {\n return { code, category, severity, blocksMovement, label, description, resolution }\n}\n\n/** Catalogue keyed by normalized code. */\nconst HOLD_CATALOGUE: Record<string, HoldDefinition> = {\n // \u2500\u2500 Import \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n 'CFS APPOINTMENT': def(\n 'CFS APPOINTMENT',\n 'import',\n 'warning',\n true,\n 'CFS appointment',\n 'Applied while a CFS (warehouse) import order is being accepted.',\n 'Cleared automatically once the order is completed and warehouse work is done.',\n ),\n 'CUSTOMS IMPORT PERMISSION': def(\n 'CUSTOMS IMPORT PERMISSION',\n 'import',\n 'warning',\n true,\n 'Customs import permission',\n 'Applied systematically to every import container pending customs release.',\n 'Removed automatically after the MRN number is updated via the Polish PCS codes.',\n ),\n 'CUSTOMS IMPORT HOLD': def(\n 'CUSTOMS IMPORT HOLD',\n 'import',\n 'critical',\n true,\n 'Customs import hold',\n 'Placed manually by a customs officer for scanning, inspection or a clearance issue.',\n 'Only a customs officer can lift it \u2014 contact the customs office.',\n ),\n 'LINE IMPORT PERMISSION': def(\n 'LINE IMPORT PERMISSION',\n 'import',\n 'warning',\n true,\n 'Line import permission',\n 'Applied systematically to every import container until the shipping line releases it.',\n 'Removed once the line releases the container via its PIN number.',\n ),\n 'OOG HOLD': def(\n 'OOG HOLD',\n 'import',\n 'critical',\n true,\n 'OOG hold',\n 'Applied to out-of-gauge import containers that require special handling permits.',\n 'Must be verified by Baltic Hub before pickup; the container cannot be notified for collection until then.',\n ),\n\n // \u2500\u2500 Export \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n 'CFS HOLD': def(\n 'CFS HOLD',\n 'export',\n 'warning',\n true,\n 'CFS hold',\n 'Applied while a CFS (warehouse) export order is being accepted.',\n 'Cleared automatically once the order is completed and warehouse work is done.',\n ),\n 'CUSTOMS EXPORT PERMISSION': def(\n 'CUSTOMS EXPORT PERMISSION',\n 'export',\n 'warning',\n true,\n 'Customs export permission',\n 'Applied systematically to export containers pending customs release.',\n 'Removed automatically after the MRN update; must be cleared before the customs-clearance deadline.',\n ),\n 'CUSTOMS EXPORT HOLD': def(\n 'CUSTOMS EXPORT HOLD',\n 'export',\n 'critical',\n true,\n 'Customs export hold',\n 'Placed manually by a customs officer for scanning, inspection or a clearance issue.',\n 'Only a customs officer can lift it \u2014 contact the customs office.',\n ),\n 'UNIT VGM LOAD PERMISSION': def(\n 'UNIT VGM LOAD PERMISSION',\n 'export',\n 'warning',\n true,\n 'VGM load permission',\n 'Applied to every full export container that has no verified gross mass yet.',\n 'Removed when the VGM weight is uploaded to Navis.',\n ),\n 'DGD HOLD': def(\n 'DGD HOLD',\n 'export',\n 'critical',\n true,\n 'DGD hold',\n 'Applied automatically to every export container carrying IMO (dangerous) cargo.',\n 'Requires the DGD (dangerous-goods declaration) document to be submitted.',\n ),\n 'SCANNING HOLD': def(\n 'SCANNING HOLD',\n 'export',\n 'critical',\n true,\n 'Scanning hold',\n 'Placed manually by a customs officer; the container must be presented for scanning.',\n 'Cleared by customs after scanning (DCT_CFS_SCANNING selection).',\n ),\n\n // \u2500\u2500 General / technical \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n 'EMPTY PERMISSION': def(\n 'EMPTY PERMISSION',\n 'general',\n 'warning',\n true,\n 'Empty permission',\n 'Applied systematically to every empty container pending a release reference.',\n 'Removed once the EDO / booking number from the shipping line is provided.',\n ),\n 'MISSING AGENT PERMISSION': def(\n 'MISSING AGENT PERMISSION',\n 'general',\n 'info',\n false,\n 'Missing agent permission',\n 'Relates to an inactive PCS payment. Does not currently block ship loading or pickup.',\n 'Removed by entering the shipping code in the Agent One field.',\n ),\n 'TECHNICAL FULL CONTAINER': def(\n 'TECHNICAL FULL CONTAINER',\n 'general',\n 'info',\n false,\n 'Technical (full container)',\n 'Applied to every full import/export container to support OCR character recognition. Does not block movement.',\n 'No action required \u2014 informational only.',\n ),\n 'TECHNICAL LIVE REEFER': def(\n 'TECHNICAL LIVE REEFER',\n 'general',\n 'info',\n false,\n 'Technical (live reefer)',\n 'Applied to every full reefer container to support OCR. Does not block operations.',\n 'No action required \u2014 informational only.',\n ),\n // Baltic Hub's Navis docs spell this \"REFFER\" (double F); keep both spellings\n // so the real terminal code resolves to the non-blocking definition instead of\n // the blocking unknown-hold fallback.\n 'TECHNICAL LIVE REFFER': def(\n 'TECHNICAL LIVE REFFER',\n 'general',\n 'info',\n false,\n 'Technical (live reefer)',\n 'Applied to every full reefer container to support OCR. Does not block operations.',\n 'No action required \u2014 informational only.',\n ),\n 'TERMINAL HOLD': def(\n 'TERMINAL HOLD',\n 'general',\n 'critical',\n true,\n 'Terminal hold',\n 'Applied for a container irregularity (incorrect labelling, bent container beams, etc.).',\n 'Contact Baltic Hub Customer Service to resolve.',\n ),\n 'CZRM HOLD': def(\n 'CZRM HOLD',\n 'general',\n 'critical',\n true,\n 'CZRM hold',\n 'Applied by the customs office.',\n 'Contact the customs office directly to have it lifted.',\n ),\n 'IZRX HOLD': def(\n 'IZRX HOLD',\n 'general',\n 'critical',\n true,\n 'IZRX hold',\n 'Applied by the customs office.',\n 'Contact the customs office directly to have it lifted.',\n ),\n}\n\n/**\n * The canonical, normalized codes known to the catalogue, sorted by severity\n * (most-urgent first) then alphabetically. Consumers that offer a constrained\n * \"pick a hold\" UI (dropdown) or validate manual entry against the known set\n * use this instead of hard-coding the list.\n */\nexport const HOLD_CODES: string[] = Object.keys(HOLD_CATALOGUE).sort((a, b) => {\n const bySeverity = HOLD_SEVERITY_RANK[HOLD_CATALOGUE[b].severity] - HOLD_SEVERITY_RANK[HOLD_CATALOGUE[a].severity]\n return bySeverity !== 0 ? bySeverity : a.localeCompare(b)\n})\n\n/** True when `code` (normalized) is a known catalogue entry. */\nexport function isKnownHoldCode(code: string): boolean {\n return Object.prototype.hasOwnProperty.call(HOLD_CATALOGUE, normalizeHoldCode(code))\n}\n\n/**\n * Resolve a single raw impediment code to its explanation + severity. Unknown\n * codes return a `warning`-severity fallback that echoes the cleaned code, so a\n * never-before-seen hold still renders something meaningful and visible.\n */\nexport function getHoldInfo(rawCode: string): HoldInfo {\n const key = normalizeHoldCode(rawCode)\n const found = HOLD_CATALOGUE[key]\n if (found) {\n return { ...found, rawCode, unknown: false, i18nKey: holdI18nKey(key) }\n }\n // Unknown code: surface the raw hold itself as the label (just drop the `!`\n // marker) so an operator still sees exactly what the terminal reported.\n const rawLabel = rawCode.replace(/^!/, '').trim()\n return {\n code: key,\n label: rawLabel || 'Unknown hold',\n category: 'general',\n severity: 'warning',\n blocksMovement: true,\n description: 'Unrecognised terminal hold. Refer to the terminal for details.',\n resolution: 'Contact the terminal to determine how this hold is cleared.',\n rawCode,\n unknown: true,\n i18nKey: 'unknown',\n }\n}\n\n/**\n * Resolve a list of raw impediment codes, sorted most-urgent first. Empty,\n * null, or all-whitespace entries are dropped.\n */\nexport function resolveHolds(rawCodes: readonly string[] | null | undefined): HoldInfo[] {\n if (!rawCodes || rawCodes.length === 0) return []\n return rawCodes\n .filter((c) => c != null && c.trim() !== '')\n .map(getHoldInfo)\n .sort((a, b) => HOLD_SEVERITY_RANK[b.severity] - HOLD_SEVERITY_RANK[a.severity])\n}\n\n/** The highest severity among a list of raw codes, or `null` when there are none. */\nexport function highestHoldSeverity(\n rawCodes: readonly string[] | null | undefined,\n): HoldSeverity | null {\n const holds = resolveHolds(rawCodes)\n if (holds.length === 0) return null\n return holds[0].severity // already sorted most-urgent first\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,19 @@
1
+ const PREFIX = "[terminal-tracking]";
2
+ const terminalLogger = {
3
+ debug(message, meta) {
4
+ if (process.env.TERMINAL_TRACKING_DEBUG) console.debug(PREFIX, message, meta ?? "");
5
+ },
6
+ info(message, meta) {
7
+ console.log(PREFIX, message, meta ?? "");
8
+ },
9
+ warn(message, meta) {
10
+ console.warn(PREFIX, message, meta ?? "");
11
+ },
12
+ error(message, meta) {
13
+ console.error(PREFIX, message, meta ?? "");
14
+ }
15
+ };
16
+ export {
17
+ terminalLogger
18
+ };
19
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/terminal_tracking/lib/logger.ts"],
4
+ "sourcesContent": ["/**\n * Thin logging wrapper. Mirrors the shipment-tracking logger shape but stays\n * dependency-light (falls back to console). The host app's logger can be wired\n * in later via DI if needed.\n */\nconst PREFIX = '[terminal-tracking]'\n\nexport const terminalLogger = {\n debug(message: string, meta?: Record<string, unknown>) {\n if (process.env.TERMINAL_TRACKING_DEBUG) console.debug(PREFIX, message, meta ?? '')\n },\n info(message: string, meta?: Record<string, unknown>) {\n console.log(PREFIX, message, meta ?? '')\n },\n warn(message: string, meta?: Record<string, unknown>) {\n console.warn(PREFIX, message, meta ?? '')\n },\n error(message: string, meta?: Record<string, unknown>) {\n console.error(PREFIX, message, meta ?? '')\n },\n}\n"],
5
+ "mappings": "AAKA,MAAM,SAAS;AAER,MAAM,iBAAiB;AAAA,EAC5B,MAAM,SAAiB,MAAgC;AACrD,QAAI,QAAQ,IAAI,wBAAyB,SAAQ,MAAM,QAAQ,SAAS,QAAQ,EAAE;AAAA,EACpF;AAAA,EACA,KAAK,SAAiB,MAAgC;AACpD,YAAQ,IAAI,QAAQ,SAAS,QAAQ,EAAE;AAAA,EACzC;AAAA,EACA,KAAK,SAAiB,MAAgC;AACpD,YAAQ,KAAK,QAAQ,SAAS,QAAQ,EAAE;AAAA,EAC1C;AAAA,EACA,MAAM,SAAiB,MAAgC;AACrD,YAAQ,MAAM,QAAQ,SAAS,QAAQ,EAAE;AAAA,EAC3C;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,30 @@
1
+ import { terminalLogger } from "./logger.js";
2
+ async function checkRateLimit(cache, tenantId, terminalCode, maxRequests, windowSeconds) {
3
+ const key = `tt:ratelimit:${tenantId}:${terminalCode}`;
4
+ const now = Date.now();
5
+ const raw = await cache.get(key);
6
+ let tokens = maxRequests;
7
+ let lastRefill = now;
8
+ if (raw) {
9
+ try {
10
+ const state = JSON.parse(raw);
11
+ const elapsed = (now - state.lastRefill) / 1e3;
12
+ const refilled = Math.floor(elapsed / windowSeconds * maxRequests);
13
+ tokens = Math.min(maxRequests, state.tokens + refilled);
14
+ lastRefill = refilled > 0 ? now : state.lastRefill;
15
+ } catch {
16
+ }
17
+ }
18
+ if (tokens <= 0) {
19
+ const retryAfterSeconds = Math.ceil(windowSeconds / maxRequests);
20
+ terminalLogger.debug("Rate limited", { terminalCode, tenantId, retryAfterSeconds });
21
+ return { allowed: false, retryAfterSeconds };
22
+ }
23
+ tokens -= 1;
24
+ await cache.set(key, JSON.stringify({ tokens, lastRefill }), windowSeconds * 2);
25
+ return { allowed: true };
26
+ }
27
+ export {
28
+ checkRateLimit
29
+ };
30
+ //# sourceMappingURL=rate-limiter.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/terminal_tracking/lib/rate-limiter.ts"],
4
+ "sourcesContent": ["import { terminalLogger } from './logger'\n\nexport type RateLimitResult = {\n allowed: boolean\n retryAfterSeconds?: number\n}\n\nexport type CacheService = {\n get(key: string): Promise<string | null>\n set(key: string, value: string, ttlSeconds?: number): Promise<void>\n}\n\n/**\n * Token-bucket rate limiter backed by a cache.\n * Key: `tt:ratelimit:{tenantId}:{terminalCode}`\n */\nexport async function checkRateLimit(\n cache: CacheService,\n tenantId: string,\n terminalCode: string,\n maxRequests: number,\n windowSeconds: number,\n): Promise<RateLimitResult> {\n const key = `tt:ratelimit:${tenantId}:${terminalCode}`\n const now = Date.now()\n\n const raw = await cache.get(key)\n let tokens = maxRequests\n let lastRefill = now\n\n if (raw) {\n try {\n const state = JSON.parse(raw) as { tokens: number; lastRefill: number }\n const elapsed = (now - state.lastRefill) / 1000\n const refilled = Math.floor((elapsed / windowSeconds) * maxRequests)\n tokens = Math.min(maxRequests, state.tokens + refilled)\n lastRefill = refilled > 0 ? now : state.lastRefill\n } catch {\n // Corrupted cache entry, reset\n }\n }\n\n if (tokens <= 0) {\n const retryAfterSeconds = Math.ceil(windowSeconds / maxRequests)\n terminalLogger.debug('Rate limited', { terminalCode, tenantId, retryAfterSeconds })\n return { allowed: false, retryAfterSeconds }\n }\n\n tokens -= 1\n await cache.set(key, JSON.stringify({ tokens, lastRefill }), windowSeconds * 2)\n return { allowed: true }\n}\n"],
5
+ "mappings": "AAAA,SAAS,sBAAsB;AAgB/B,eAAsB,eACpB,OACA,UACA,cACA,aACA,eAC0B;AAC1B,QAAM,MAAM,gBAAgB,QAAQ,IAAI,YAAY;AACpD,QAAM,MAAM,KAAK,IAAI;AAErB,QAAM,MAAM,MAAM,MAAM,IAAI,GAAG;AAC/B,MAAI,SAAS;AACb,MAAI,aAAa;AAEjB,MAAI,KAAK;AACP,QAAI;AACF,YAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,YAAM,WAAW,MAAM,MAAM,cAAc;AAC3C,YAAM,WAAW,KAAK,MAAO,UAAU,gBAAiB,WAAW;AACnE,eAAS,KAAK,IAAI,aAAa,MAAM,SAAS,QAAQ;AACtD,mBAAa,WAAW,IAAI,MAAM,MAAM;AAAA,IAC1C,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,UAAU,GAAG;AACf,UAAM,oBAAoB,KAAK,KAAK,gBAAgB,WAAW;AAC/D,mBAAe,MAAM,gBAAgB,EAAE,cAAc,UAAU,kBAAkB,CAAC;AAClF,WAAO,EAAE,SAAS,OAAO,kBAAkB;AAAA,EAC7C;AAEA,YAAU;AACV,QAAM,MAAM,IAAI,KAAK,KAAK,UAAU,EAAE,QAAQ,WAAW,CAAC,GAAG,gBAAgB,CAAC;AAC9E,SAAO,EAAE,SAAS,KAAK;AACzB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,48 @@
1
+ const NO_STOPS = { vsl: null, road: null, rail: null };
2
+ const STOP_HEADERS = {
3
+ vsl: ["stopvsl", "stopvessel"],
4
+ road: ["stoproad"],
5
+ rail: ["stoprail"]
6
+ };
7
+ const FALSY_TOKENS = /* @__PURE__ */ new Set(["", "0", "n", "no", "false", "f", "-"]);
8
+ function normalizeHeader(header) {
9
+ return header.toLowerCase().replace(/[^a-z0-9]/g, "");
10
+ }
11
+ function isActiveStop(value) {
12
+ if (value == null) return false;
13
+ return !FALSY_TOKENS.has(String(value).trim().toLowerCase());
14
+ }
15
+ function parseTerminalStops(rawData) {
16
+ if (!rawData) return { ...NO_STOPS };
17
+ const byNorm = /* @__PURE__ */ new Map();
18
+ for (const [key, value] of Object.entries(rawData)) byNorm.set(normalizeHeader(key), value);
19
+ const read = (mode) => {
20
+ for (const candidate of STOP_HEADERS[mode]) {
21
+ if (byNorm.has(candidate)) return isActiveStop(byNorm.get(candidate));
22
+ }
23
+ return null;
24
+ };
25
+ return { vsl: read("vsl"), road: read("road"), rail: read("rail") };
26
+ }
27
+ function isRoadStopped(stops) {
28
+ return stops.road === true;
29
+ }
30
+ function stopsChanged(prior, next) {
31
+ const active = (v) => v === true;
32
+ return active(prior.vsl) !== active(next.vsl) || active(prior.road) !== active(next.road) || active(prior.rail) !== active(next.rail);
33
+ }
34
+ function activeStopModes(stops) {
35
+ const modes = [];
36
+ if (stops.vsl === true) modes.push("vsl");
37
+ if (stops.road === true) modes.push("road");
38
+ if (stops.rail === true) modes.push("rail");
39
+ return modes;
40
+ }
41
+ export {
42
+ NO_STOPS,
43
+ activeStopModes,
44
+ isRoadStopped,
45
+ parseTerminalStops,
46
+ stopsChanged
47
+ };
48
+ //# sourceMappingURL=stops.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/terminal_tracking/lib/stops.ts"],
4
+ "sourcesContent": ["/**\n * Load/pickup \"STOP\" flags for Navis N4 terminals (Baltic Hub / BCT).\n *\n * The N4 `/unit` endpoint gained three columns (Baltic Hub API update, Aug 2026)\n * that state, per transport mode, whether the container currently CANNOT be\n * loaded / picked up:\n *\n * - **Stop-Vsl** \u2014 no loading onto a **vessel** (export blocked).\n * - **Stop-Road** \u2014 no pickup by **road / truck**.\n * - **Stop-Rail** \u2014 no pickup by **rail**.\n *\n * Unlike the `Unit Impediments` hold list (from which blocking is *inferred* via\n * the catalogue in `./holds`), these are the terminal stating the restriction\n * directly, so they are a cleaner mode-specific signal.\n *\n * Pure helper \u2014 no DI, no i18n, no React. Reads from the N4 row's `rawData` map\n * (header \u2192 value) so the SAME parse serves the `/unit` parser, the availability\n * logic (which runs on the persisted event's `rawData`), and the details drawer.\n *\n * \u2500\u2500 Header + value format \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Confirmed against a real Baltic Hub `/unit` payload (2026-07): headers are\n * `Stop-Vsl` / `Stop-Rail` / `Stop-Road`, values are the strings `\"true\"` /\n * `\"false\"`. Header matching is still done **tolerantly** (case- and\n * punctuation-insensitive) and value parsing treats any non-empty, non-falsy\n * token as an active stop, so a minor terminal-side format change (or another\n * N4 terminal spelling the columns differently) keeps working \u2014 everything\n * keys off this one file.\n */\n\n/** Transport mode a STOP flag applies to. */\nexport type TerminalStopMode = 'vsl' | 'road' | 'rail'\n\n/**\n * The three STOP flags for a container. `null` means the terminal did not report\n * that column at all (older terminals / pre-update payloads) \u2014 distinct from\n * `false`, which means the column was present and no restriction is active.\n */\nexport type TerminalStops = {\n /** No loading onto a vessel (export). */\n vsl: boolean | null\n /** No pickup by road / truck. */\n road: boolean | null\n /** No pickup by rail. */\n rail: boolean | null\n}\n\n/** All-unknown stops (nothing reported). */\nexport const NO_STOPS: TerminalStops = { vsl: null, road: null, rail: null }\n\n/**\n * Candidate column headers per mode, in normalized form (lower-case,\n * non-alphanumerics stripped). First match wins. `stopvessel` is kept alongside\n * `stopvsl` in case the header spells the word out.\n */\nconst STOP_HEADERS: Record<TerminalStopMode, readonly string[]> = {\n vsl: ['stopvsl', 'stopvessel'],\n road: ['stoproad'],\n rail: ['stoprail'],\n}\n\n/** Values that mean \"no stop\" when the column is present. */\nconst FALSY_TOKENS = new Set(['', '0', 'n', 'no', 'false', 'f', '-'])\n\n/** Normalize a header for tolerant matching: lower-case, drop non-alphanumerics. */\nfunction normalizeHeader(header: string): string {\n return header.toLowerCase().replace(/[^a-z0-9]/g, '')\n}\n\n/** Interpret a present column value as an active stop (true) or not (false). */\nfunction isActiveStop(value: unknown): boolean {\n if (value == null) return false\n return !FALSY_TOKENS.has(String(value).trim().toLowerCase())\n}\n\n/**\n * Parse the STOP flags out of an N4 row's `rawData` (header \u2192 value). Returns\n * `null` for a mode whose column is absent, `false`/`true` when present.\n */\nexport function parseTerminalStops(rawData: Record<string, unknown> | null | undefined): TerminalStops {\n if (!rawData) return { ...NO_STOPS }\n\n const byNorm = new Map<string, unknown>()\n for (const [key, value] of Object.entries(rawData)) byNorm.set(normalizeHeader(key), value)\n\n const read = (mode: TerminalStopMode): boolean | null => {\n for (const candidate of STOP_HEADERS[mode]) {\n if (byNorm.has(candidate)) return isActiveStop(byNorm.get(candidate))\n }\n return null\n }\n\n return { vsl: read('vsl'), road: read('road'), rail: read('rail') }\n}\n\n/** True only when road pickup is explicitly stopped (not on null/false). */\nexport function isRoadStopped(stops: TerminalStops): boolean {\n return stops.road === true\n}\n\n/**\n * Whether the set of *actively-stopped* modes differs between two snapshots.\n * Drives the `equipment.stops_updated` event. Compares on truthiness, so\n * `null` (unreported) and `false` (reported, not stopped) are equivalent \u2014\n * both mean \"not stopped\" \u2014 mirroring how `impedimentsChanged` treats a null\n * and an empty hold list as equal. This keeps a first poll of containers that\n * merely carry all-`false` STOP columns from emitting spurious change events.\n */\nexport function stopsChanged(prior: TerminalStops, next: TerminalStops): boolean {\n const active = (v: boolean | null) => v === true\n return (\n active(prior.vsl) !== active(next.vsl) ||\n active(prior.road) !== active(next.road) ||\n active(prior.rail) !== active(next.rail)\n )\n}\n\n/** The modes with an active stop, in vsl \u2192 road \u2192 rail order. */\nexport function activeStopModes(stops: TerminalStops): TerminalStopMode[] {\n const modes: TerminalStopMode[] = []\n if (stops.vsl === true) modes.push('vsl')\n if (stops.road === true) modes.push('road')\n if (stops.rail === true) modes.push('rail')\n return modes\n}\n"],
5
+ "mappings": "AA+CO,MAAM,WAA0B,EAAE,KAAK,MAAM,MAAM,MAAM,MAAM,KAAK;AAO3E,MAAM,eAA4D;AAAA,EAChE,KAAK,CAAC,WAAW,YAAY;AAAA,EAC7B,MAAM,CAAC,UAAU;AAAA,EACjB,MAAM,CAAC,UAAU;AACnB;AAGA,MAAM,eAAe,oBAAI,IAAI,CAAC,IAAI,KAAK,KAAK,MAAM,SAAS,KAAK,GAAG,CAAC;AAGpE,SAAS,gBAAgB,QAAwB;AAC/C,SAAO,OAAO,YAAY,EAAE,QAAQ,cAAc,EAAE;AACtD;AAGA,SAAS,aAAa,OAAyB;AAC7C,MAAI,SAAS,KAAM,QAAO;AAC1B,SAAO,CAAC,aAAa,IAAI,OAAO,KAAK,EAAE,KAAK,EAAE,YAAY,CAAC;AAC7D;AAMO,SAAS,mBAAmB,SAAoE;AACrG,MAAI,CAAC,QAAS,QAAO,EAAE,GAAG,SAAS;AAEnC,QAAM,SAAS,oBAAI,IAAqB;AACxC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,EAAG,QAAO,IAAI,gBAAgB,GAAG,GAAG,KAAK;AAE1F,QAAM,OAAO,CAAC,SAA2C;AACvD,eAAW,aAAa,aAAa,IAAI,GAAG;AAC1C,UAAI,OAAO,IAAI,SAAS,EAAG,QAAO,aAAa,OAAO,IAAI,SAAS,CAAC;AAAA,IACtE;AACA,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,KAAK,KAAK,KAAK,GAAG,MAAM,KAAK,MAAM,GAAG,MAAM,KAAK,MAAM,EAAE;AACpE;AAGO,SAAS,cAAc,OAA+B;AAC3D,SAAO,MAAM,SAAS;AACxB;AAUO,SAAS,aAAa,OAAsB,MAA8B;AAC/E,QAAM,SAAS,CAAC,MAAsB,MAAM;AAC5C,SACE,OAAO,MAAM,GAAG,MAAM,OAAO,KAAK,GAAG,KACrC,OAAO,MAAM,IAAI,MAAM,OAAO,KAAK,IAAI,KACvC,OAAO,MAAM,IAAI,MAAM,OAAO,KAAK,IAAI;AAE3C;AAGO,SAAS,gBAAgB,OAA0C;AACxE,QAAM,QAA4B,CAAC;AACnC,MAAI,MAAM,QAAQ,KAAM,OAAM,KAAK,KAAK;AACxC,MAAI,MAAM,SAAS,KAAM,OAAM,KAAK,MAAM;AAC1C,MAAI,MAAM,SAAS,KAAM,OAAM,KAAK,MAAM;AAC1C,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=terminal-adapter.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }