@autobusal/providers 1.41.4 → 1.42.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.
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "@autobusal/providers",
3
- "version": "1.41.4",
3
+ "version": "1.42.0",
4
4
  "author": "Ferjolt Ozuni",
5
5
  "type": "module",
6
- "main": "index.ts"
6
+ "main": "index.ts",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ }
7
10
  }
package/types/routes.ts CHANGED
@@ -217,6 +217,22 @@ export interface PriceData {
217
217
  display: string
218
218
  offer?: boolean
219
219
  value: number
220
+
221
+ /**
222
+ * What this party would have paid before a dynamic price rule reduced it.
223
+ *
224
+ * Claude - 2026-08-30 (roadmap P5, ruled by Ferjolt). Present ONLY on a
225
+ * reduction: "for a price rise, just the price; if the price drops, the
226
+ * standard fare cut through, and the actual price discounted". A surcharge
227
+ * deliberately publishes nothing here, so there is no "was" figure lying
228
+ * around for a card to render as a discount that never happened.
229
+ *
230
+ * It is a real total for the passengers being quoted - the same arithmetic
231
+ * as `value`, run against the pre-modifier fares - not a list price nobody
232
+ * was offered.
233
+ */
234
+ base?: number
235
+ base_display?: string
220
236
  from: LocationData
221
237
  to: LocationData
222
238
  adult: number
package/types/settings.ts CHANGED
@@ -131,6 +131,15 @@ export interface SettingsData {
131
131
  languages: {
132
132
  available: string[]
133
133
  default: string
134
+ /*
135
+ * Claude - 2026-08-30: every language the PLATFORM ships, as opposed to
136
+ * the ones this brand publishes in. The two answer different questions
137
+ * and a screen that confuses them gets the wrong one - `available` is
138
+ * what this site is served in, `all` is what we can WRITE in, which is
139
+ * what the operator-invitation form needs. Optional so nothing that
140
+ * never asks for it is affected.
141
+ */
142
+ all?: string[]
134
143
  }
135
144
 
136
145
  social: {
package/CHANGELOG.md DELETED
@@ -1,872 +0,0 @@
1
- ## 1.41.4 - 2026-08-29
2
-
3
- - useGetMenu SUBSCRIBES to the language rather than only keying on it (audit A6-73). `i18next.language` was read during render and nothing re-rendered on a switch, so the i18n strings changed instantly while the admin-managed menu labels stayed in the previous language until a full reload.
4
-
5
- - SettingsData.preferences gains `legal` (company, address, city, VAT number) for the footer, and FoundData gains `stops` - the itinerary between the searched cities, with midnight offsets.
6
-
7
- ## 1.41.3 - 2026-08-29
8
-
9
- - UserData gains shipping_enabled (published by Auth\Export for operator/employee accounts) so menus can hide the Shipments module honestly (audit A1-37).
10
-
11
- # Changelog
12
-
13
- ## 1.41.0 (2026-08-29)
14
-
15
- - The app-shell title is set through document.title instead of a rendered <title> element - React 19 does not deduplicate titles across components, so the shell's and the page's both reached <head>, which is what Meta's cleanup was deleting one of (Sentry BUSMAGUS-7).
16
-
17
- ## 1.40.7 (2026-08-28)
18
-
19
- - useReferralTouch: the web finally reads ?ref= - touch on landing, token in localStorage (referral_token field: the BFF strips top-level token); storedReferral() exported for checkout replay.
20
-
21
- ## 1.40.6 (2026-08-26)
22
-
23
- - settings types: addons gains `flex` and `seats` entries (both optional).
24
-
25
- ## 1.40.4
26
-
27
- ### Fixed
28
-
29
- - **The Content-Language header survives a cold load** - requests firing before i18next.init resolved carried no language at all, which is the recurring English-menu-under-/sq leak (CMS-05/CMS-07). The interceptor now falls back to the URL's locale prefix, then the stored preference.
30
-
31
- ## 1.40.3
32
-
33
- ### Fixed
34
-
35
- - **An expired session interrupted on a private page carries that page through the logout hop** (`/account/logout?return=...`), so signing back in can resume there. (Audit ADM-08a.)
36
-
37
- ## 1.40.2
38
-
39
- ### Fixed
40
-
41
- - **A missing blog/help/page slug showed the "system error" screen, not a
42
- 404.** The public content screens load through `useSuspenseQuery`, whose
43
- throw the ErrorBoundary rendered as a system error for every case; a 404
44
- (unknown slug) or 422 (the articles endpoint's "invalid slug" on an
45
- absent/unpublished article) is now shown as the styled 404. (Audit
46
- CMS-01f/CMS-03d.)
47
- - **The public navigation menu was always English.** obtapi localises the
48
- labels off `Content-Language` - sent correctly - but the menu was cached
49
- under a language-blind key, so it loaded once and a language switch never
50
- refetched it. Keyed on the active language. (Audit CMS-07b.)
51
-
52
- ## 1.37.11
53
-
54
- ### Added
55
-
56
- - Saved-passenger types and the account query, shared by the checkout chooser and the account manager so both read one shape.
57
-
58
- ## 1.37.10
59
-
60
- ### Added
61
-
62
- - `ReviewData.published_on` - the review's date as ISO 8601, beside the localized display string. Structured data needs a machine date, and reversing a printed `d/m/Y` back into one would couple the schema to a locale format.
63
-
64
- ## 1.37.9
65
-
66
- ### Added
67
-
68
- - **`Errors/Message` carries a `data-error-page` marker.** The prerenderer has to tell a rendered page from a rendered not-found, and it cannot do that by matching text when the app speaks fifteen languages - it was writing 404s to disk as if they were pages and counting them as successes.
69
-
70
- ## 1.37.8
71
-
72
- ### Added
73
-
74
- - **`SettingsData.map.tile_url`** - the basemap route maps draw on, from `/settings/get`. `null` until a commercial tile provider is configured server-side, which every client reads as "keep your built-in OpenStreetMap default". Served rather than built in because the mobile app shares this switch, and a compiled-in tile URL there could only be changed by shipping a store release.
75
-
76
- ## 1.37.7
77
-
78
- ### Added
79
-
80
- - **`OrderData.owner_category`** - `'agency' | 'operator' | 'individual'`, who actually owns an order, collapsed to what a viewer outside the account needs to know: who to contact if it needs to change. See `Libraries\Orders\View::ownerCategory()` (obtapi).
81
-
82
- ### Changed
83
-
84
- - **`OrderData.comission_affiliate_display` is now optional.** obtapi's `View::get()` hides every `comission_*_display` field on a restricted (public lookup) response - see that method's own `$restricted` parameter for why.
85
-
86
- ## 1.37.6
87
-
88
- ### Fixed
89
-
90
- - **The DOM-corruption self-heal watchdog now actually recovers every navigation it's supposed to.** It had two gaps that left users stuck exactly like the bug it exists to catch: its 10-second reload cooldown was tracked globally, so a SECOND corrupted navigation to a different URL within 10s of a first one was silently swallowed - no reload, no recovery, stuck. Scoped the cooldown per-URL instead: a fresh URL always gets a fresh recovery attempt; only a repeat failure on the SAME URL is throttled (the loop-protection this cooldown exists for in the first place). It also only listened for synchronous `error` events - a promise-rejection-based failure (plausible from the lazy()/dynamic-import machinery a route transition drives) was invisible to it and got no recovery at all. Added an `unhandledrejection` listener alongside the existing one, both routed through the same recovery check.
91
- - **`apiClient`'s 401 handler no longer redirects multiple times for one dead session.** A page boot fires several requests in parallel (settings, menu, account refresh, whatever the current page needs); on a stale/invalid session every one of them 401s within the same tick, and each independently ran its own async `/session` confirmation and its own `window.location.href` redirect - the existing pathname guard only protects against a request that 401s AFTER a redirect has already navigated away, not against several 401s racing each other before any of them has. Reproduced live: a stale session landed on `/account/logout`, and before that page's own logout mutation finished, a second queued redirect (from another request's 401, still in flight when the first one fired) reloaded the page again - repeatedly, stacking a fresh "logged out" toast for every concurrent 401 the boot had fired. Now guarded with a synchronous flag set before the async confirmation, so only the first 401 of a burst does the work; every other one concurrent with it short-circuits immediately.
92
-
93
- ## 1.37.5
94
-
95
- ### Fixed
96
-
97
- - **`input[type="search"]` now gets the same theme styling as every other text-like input.** `GlobalStyles`'s selector list named `text`/`number`/`email`/`password` but not `search` - any search box on the site (the Help Center's new one) rendered with the browser's bare default appearance instead of the theme's background/border/radius/font.
98
-
99
- ## 1.37.4
100
-
101
- ### Changed
102
-
103
- - **`CategoryData.name` dropped its dead `name_en`/`name_sq` fields** - `articles_categories.name` is JSON keyed by locale now (obtapi's categories_name_to_json migration); `CategoryData` is the resolved-string shape an article's own `category` and the admin browse list use, the admin Manage form works with the raw per-locale shape directly (see magus Admin/Categories/services.ts).
104
-
105
- ## 1.37.3
106
-
107
- ### Added
108
-
109
- - **`SettingsData.currency`** - the label's ISO 4217 code, now exposed on the public `/api/settings/get` response. The same source `Facts` already trusted for its own `Offer` schema; every price elsewhere on the SPA is a pre-formatted display string with no machine-readable currency behind it, so this closes that gap for anywhere else that needs one.
110
-
111
- ## 1.37.2
112
-
113
- ### Added
114
-
115
- - **`OperatorData` gains the payout bank detail fields** (`bank_name`, `bank_address`, `iban`, `swift`, `bank_currency`, `account_owner`, `bank_document_url`) - see `@autobusal/account-update` 1.2.1.
116
-
117
- ## 1.37.1
118
-
119
- ### Added
120
-
121
- - **`MenuData.location` gains `'footer_1' | 'footer_2'`** - the site footer's two link columns are now admin-managed Menu items (same model the header nav's left/right already use) instead of hardcoded JSX. See `@autobusal/admin-menu` 1.1.3.
122
-
123
- ## 1.37.0
124
-
125
- ### Added
126
-
127
- - **Shipments types** (`ShipmentData`, `ShipmentStatus`, `SHIPMENT_STATUSES`, `ShipmentHistoryData`, etc.) for the new operator shipping module - see `@autobusal/operator-shipments` and `@autobusal/track-shipment`.
128
- - **`OperatorData.mobile_access` / `OperatorData.shipping_enabled`** - both admin-only booleans, properly typed now rather than one of them (`mobile_access`) being narrow-cast at its one call site to avoid a cross-repo publish that this release needed anyway.
129
-
130
- ## 1.36.0
131
-
132
- ### Added
133
-
134
- - **`LabelSettings.locales`** - `{default, available}`, an admin-editable override of `preferences.languages` (which UI locales this label's site responds to, and its default). Null until the new admin-label Locales tab saves one, in which case `preferences.languages` keeps coming from the label's settings.json file exactly as before.
135
-
136
- ## 1.35.0
137
-
138
- ### Changed
139
-
140
- - **`PageData` (the admin Pages shape) is now locale-object-based, not `_en`/`_sq` columns.** `title`/`content`/`keywords`/`description` are each `LocalizedText` (`Record<string, string>`, one value per locale) - matches obtapi's `pages_content_to_json` migration, which moved `pages` off the two-content-language ceiling onto the same JSON-per-locale shape `faqs` already established. `title_en`/`title_sq`/`content_en`/`content_sq` and friends are gone from the type entirely. New `PageSaveData` covers what the admin save endpoint actually accepts (each field JSON-stringified, not the raw object) - the public page-view response (`@autobusal/page/types.ts`) is unaffected, it was already a single resolved string per field.
141
-
142
- ## 1.34.0
143
-
144
- ### Added
145
-
146
- - **`OperatorData.brand_name` / `OperatorData.display_name`** - an operator's optional public-facing trade name and the computed field (`brand_name` if set, else `company`) every customer-facing display should read. `company` stays as the registered legal name, for legal/financial contexts only.
147
-
148
- ## 1.33.0
149
-
150
- ### Added
151
-
152
- - **`LabelSettings.ai`** and **`LabelData.is_main`** - a label's per-provider AI configuration (`{providers: {openai: {status, api_key?, models?, default_model?, labels_allowed?}, ...}, default_provider}`) and whether this label IS the platform's main one, both needed by the new admin-label AI tab to know which fields to show.
153
-
154
- ## 1.32.0
155
-
156
- ### Added
157
-
158
- - **`LabelSettings.theme`** — a label's chosen theme preset (`{preset, overrides}`), matching obtapi's new `labels_settings.theme` column. Optional/nullable: absent means the label still reads its colors straight out of settings.json, exactly as before this existed.
159
-
160
- ## 1.31.1
161
-
162
- ### Fixed
163
-
164
- - **A 422 no longer stacks one toast per query.** The search results page
165
- asks three endpoints the same question, so a malformed URL rejected three
166
- times and showed three identical messages. De-duplicated by the first
167
- field error rather than by the message: Laravel's summary appends "(and N
168
- more errors)" and each endpoint validates a different rule set, so the
169
- three strings differ in that count alone while describing one problem.
170
-
171
- ## 1.31.0
172
-
173
- ### Added
174
-
175
- - **`PopularRoutesData`** - the city pairs behind the home page's route
176
- columns. `from` and `to` arrive localised and `link` is built server-side,
177
- so the search URL's date and slug rules live in one place.
178
-
179
- ## 1.30.1
180
-
181
- ### Fixed
182
-
183
- - **Only the documents 403 redirects to the documents page.** This fired on
184
- every 403 there is. The intent is sound - an account blocked by an unsigned
185
- document should be taken to the screen that unblocks it - but the other
186
- source of a 403 in this API is the email-verification gate, and sending
187
- somebody there for that told them nothing and fixed nothing. It read as a
188
- deliberate product redirect and was this line. Keyed on a `reason` the
189
- server sets rather than the message, which is translated fifteen ways, and
190
- guarded against redirecting the documents page to itself.
191
-
192
- ## 1.30.0
193
-
194
- ### Added
195
-
196
- - **`BootstrapError` tells a transient failure from a real one and recovers
197
- by itself.** A rate limit or a dropped connection is not a system error,
198
- and "something broke on our side" is wrong twice for either: it blames the
199
- wrong thing, and it hands the visitor a reload button for a situation where
200
- reloading immediately makes it worse. 429/503/504 and a request with no
201
- response at all now get their own wording and an automatic retry that backs
202
- off 5s, 10s, 20s, 30s.
203
-
204
- ## 1.29.4
205
-
206
- ### Added
207
-
208
- - **`OrderData.addons`** (`OrderAddonData[]`) - the extra services bought at
209
- checkout. There is no `data` field by design: the API withholds it because
210
- the Telegram add-on's copy holds a bind token.
211
- - **`OrderData.fare_display`** - the fare WITHOUT add-ons. Optional, because
212
- only the single-order endpoint opts into it; `amount_display` remains fare +
213
- add-ons.
214
-
215
- ## 1.29.3
216
-
217
- ### Fixed
218
-
219
- - **A failed settings fetch white-screened the entire app.** `useGetSettings`
220
- is a *suspense* query, so a failure throws during render rather than
221
- resolving to an error state — and nothing in `Providers` caught it, so the
222
- whole tree unmounted and left a blank page with no message, no retry and
223
- nothing in the console. A rate limit, a brief API blip or a restart
224
- mid-deploy all produced the same silent nothing. Reproduced in a browser,
225
- not deduced.
226
-
227
- Fixed with a new **`BootstrapError`** boundary wrapping everything. It
228
- cannot be the existing `ErrorBoundary`: that renders `Errors/Message`,
229
- which calls `useGetSettings()` itself and `useNavigate` — above the
230
- settings fetch it would rethrow inside its own fallback. An error screen
231
- for "nothing loaded" cannot depend on anything having loaded, so this one
232
- uses no settings, no i18next, no router and no theme — inline styles and
233
- the static `@lang/errors` map, with a reload button.
234
-
235
- ### Changed
236
-
237
- - **Queries retry what can plausibly differ, and only that.** `retry: false`
238
- was wrong in both directions: a single dropped connection or 500 was fatal
239
- (and on the suspense bootstrap path, fatal meant the whole app), while
240
- nothing distinguished that from a 401/403/404 — a decided answer that
241
- repeats identically while spending the rate-limit budget a 429 exists to
242
- protect. Now 5xx, 408, 425, 429 and network-level failures get two retries
243
- at 700ms then 1400ms; everything else fails at once, so a real 403 surfaces
244
- as a real 403 instead of three seconds of spinner.
245
-
246
-
247
- ## 1.29.2
248
-
249
- ### Removed
250
-
251
- - **Everything 1.29.0/1.29.1 added for multi-TLD** — `getLanguage`'s
252
- `domainLocale` argument and the module-level origin it defaulted to, the
253
- argument `Setup` passed it, and `SettingsData.domain` / `.domains` /
254
- `.locale`. The stored language preference is once again the only thing that
255
- outranks the brand default on an unprefixed URL.
256
-
257
- See `@autobusal/common@1.27.1` for why. Published forward rather than
258
- downgraded, for the same reason.
259
-
260
- ## 1.28.1
261
-
262
- ### Fixed
263
-
264
- - **The stale-chunk guard budgeted two reloads instead of one.** Measured: a
265
- failure at `/operators` reloaded, the server answered with its canonical
266
- `/operators/`, the same chunk failed again — and the raw path no longer
267
- matched the one already spent, so it reloaded a second time. Bounded, but
268
- twice what was intended, and on a genuinely broken deploy the user watched
269
- the page flash twice before the error appeared. The budget key now strips a
270
- trailing slash, so one journey gets one reload.
271
-
272
- Only visible because the retest ran against a chunk that was *permanently*
273
- missing rather than replaced — the ordinary stale-bundle case recovers on
274
- the first reload and never reaches the second.
275
-
276
- ## 1.28.0
277
-
278
- ### Added
279
-
280
- - **`registerStaleChunkRecovery()`** — a tab left running a bundle that no
281
- longer exists now reloads itself onto the page it was asked for, instead of
282
- freezing.
283
-
284
- Every route is `lazy(() => import(...))`, the build emits content-hashed
285
- chunks, and it **empties the output directory** — so a new build deletes the
286
- old chunk names outright (watched `index-DFPpSljG.js` disappear across one).
287
- A tab still holding the previous `index.html` goes on asking for those names,
288
- so the next navigation fetches a chunk that no longer resolves, the import
289
- rejects, and the routed component never mounts — while the URL has *already*
290
- changed. The address bar says one page and the screen shows the last one.
291
- From the outside: "the page does not react at all, but the url seems to
292
- change."
293
-
294
- One listener on Vite's `vite:preloadError` covers all 141 lazy routes, which
295
- is why this is a small module rather than a change to 141 call sites.
296
-
297
- **Silent, not a prompt.** Every lazy chunk in both apps is a *route* —
298
- checked, there is no lazily-loaded in-page component — so a chunk is only
299
- ever fetched while navigating, which means the user has already left the page
300
- they were on. Reloading to the destination costs them nothing the click was
301
- not already costing them, and a "new version available?" dialog would be
302
- asking permission to do the thing they just asked for.
303
-
304
- **It cannot loop.** A missing chunk is usually a stale bundle but might be a
305
- broken deploy, and reloading into a repeating failure would be far worse than
306
- the bug. Each path gets at most one reload per tab; after that the error is
307
- left to surface. `sessionStorage`, so the budget is per tab.
308
-
309
- Verified by reproducing it: a tab pinned to an older bundle, its chunk
310
- orphaned by a rebuild, then a client-side click — `vite:preloadError` fired,
311
- the document was replaced, and the tab landed on `/operators` running the new
312
- bundle.
313
-
314
- ## 1.27.0
315
-
316
- ### Added
317
-
318
- - `LabelSettings.sms` (BulkGate: status, credentials, sender) and
319
- `SettingsData.addons.sms` — the paid per-order confirmation. Both optional,
320
- so an older payload cannot crash a checkout.
321
- ## 1.26.0
322
-
323
- ### Added
324
-
325
- - `FoundData.arrival_offset` — whole days between boarding and arriving, for
326
- the overnight marker on result cards. Optional: external-provider offers do
327
- not report one, and a missing value must read as same-day rather than
328
- invent a day.
329
-
330
- ## 1.25.0
331
-
332
- ### Added
333
-
334
- - `LabelSettings.whatsapp.alert_template_name` / `alert_template_language` —
335
- the separate approved template the route+date broadcast composer sends
336
- through. Optional: a brand can confirm purchases over WhatsApp without ever
337
- broadcasting, and Meta approves a template for a stated purpose, so service
338
- alerts cannot reuse the confirmation one.
339
-
340
- ## 1.24.0
341
-
342
- **New `Setup/commerce.ts` — the booking funnel, pushed to the dataLayer.**
343
-
344
- `analytics.ts` already loaded the container and announced page views; this is what makes those page views mean something. Until now the funnel was unmeasurable for a more basic reason than missing events — every step of the wizard shared one URL, so no tool could tell a search from a checkout.
345
-
346
- Uses **GA4's own event names** (`view_item_list`, `select_item`, `begin_checkout`, `add_to_cart`, `remove_from_cart`, `purchase`) so a container wires them by picking from a list rather than needing a custom mapping per brand.
347
-
348
- `purchase` is the hard one and the reason this is a module rather than a few inline pushes: a card payment leaves the site entirely and returns on a URL carrying nothing but an order hash, and most buyers are guests with no account to read the order back from. So `stash()` remembers the value at order time and `flush()` announces it on return — clearing as it fires, and checking the hash, so neither a refresh nor a stale entry from an abandoned attempt can report a sale.
349
-
350
- Each push clears `ecommerce` first; GTM's dataLayer is cumulative, and without that a later event inherits the previous one's items.
351
-
352
- ## 1.23.0
353
-
354
- - `CountryData.timezone` — the IANA zone, so the date picker can floor itself on the departure city's date rather than ours or the buyer's. Carried on `/api/cities/browse`, which the search form already loads.
355
-
356
- ## 1.22.0
357
-
358
- **Flexible Ticket, and the `policies` rename.**
359
-
360
- - `PolicyData` is now `InformationData`, and its field `policies` is `information` — the route's free-text box is luggage, pets, boarding and ID, and never was cancellation terms. Calling it Policies invited operators to type terms into it that would then contradict the terms actually sold.
361
- - `RouteData.policies_en|sq` → `information_en|sq`.
362
- - `RouteData` gains `refund_hours` / `reschedule_hours`: the operator's own notice windows, in hours before departure. **Internal** — they reach the operator's route form and the admin, never a public payload. `null` means "not offered on this line", which is why they are nullable rather than 0.
363
- - New `FlexOffer`, the response of `GET /api/orders/flex`. Its hours are already resolved to the **stricter** leg of a return trip, so they are the numbers that will be enforced. Either may be null on its own — an operator who will reschedule but not refund is a real case.
364
-
365
- All notable changes to `@autobusal/providers` are documented here. This project
366
- adheres to [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
367
- [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
368
-
369
- > Note: 1.8.0 through 1.11.0 were published without changelog entries. The
370
- > gap is left as-is rather than reconstructed after the fact.
371
-
372
- ## 1.21.0
373
-
374
- `SettingsData.today` — the earliest bookable date as the server reckons it,
375
- handed to the calendar at startup so the picker stops flooring on the buyer's
376
- device clock.
377
-
378
- ## 1.20.0
379
-
380
- `FoundData.departs_in` — minutes until departure, for same-day searches only,
381
- null on every other date.
382
-
383
- ## 1.19.0
384
-
385
- `types/reviews.ts` — `RatingData`, `ReviewData`, `ReviewSummaryData`. `rating`
386
- added to `OperatorData` and `RouteData`, always optional and often absent.
387
-
388
- ## 1.18.0
389
-
390
- `LabelSettings.reviews` — the per-brand review moderation setting
391
- (`auto_publish`, `threshold`). Absent or false means every review waits for a
392
- human, which is the default for every brand.
393
-
394
- ## [1.40.5] - 2026-08-26
395
-
396
- ### Added
397
-
398
- - `OrderData.round_trip?` - whether the order is already half of a round
399
- trip (either leg); computed by obtapi's single-order endpoint.
400
-
401
- ## [1.17.0] - 2026-08-02
402
-
403
- ### Changed
404
-
405
- - **`FeatureData` gains `slug` and drops `name_sq`.** Amenity names are
406
- resolved server-side from obtapi's `lang/{locale}/features.php` now, so
407
- `name` already arrives in the request's language - for the SPA and the
408
- partner API alike, from one resolution point. `slug` is the stable
409
- machine-readable key: `id` is ours alone and `name` moves with the
410
- language, so it is the only thing safe to map against.
411
-
412
- ## [1.16.0] - 2026-08-02
413
-
414
- ### Changed
415
-
416
- - **SEO overrides are per language.** `LabelSettings.seo` and the location
417
- types carry a `{locale: {title, description}}` map. The label's homepage
418
- override is sent WHOLE and picked client-side, unlike the country/city
419
- ones which resolve server-side - not an inconsistency but a bootstrap fact:
420
- `/api/settings/get` is the one request that runs before i18n exists, so the
421
- server cannot know which language to answer in. Resolving it there put
422
- English copy on the Italian homepage.
423
- - **`seo_keywords` is gone** everywhere. Google has ignored the meta keywords
424
- tag since 2009; the field could only ever cost somebody an afternoon.
425
-
426
- ## [1.15.0] - 2026-08-02
427
-
428
- ### Changed
429
-
430
- - **The URL decides the language.** `getLanguage()` now reads a locale prefix
431
- off the path before falling back to the stored preference and then the
432
- brand default, and takes the brand's `available` list so an unrecognised
433
- first segment (`/city/tirana`) is never mistaken for a language. This is
434
- the point of localised URLs: a link has to decide what its recipient sees,
435
- so someone whose last visit was in Albanian still gets Italian from a
436
- shared `/it/...` link. Unprefixed URLs are unaffected.
437
-
438
- ## [1.14.0] - 2026-08-02
439
-
440
- ### Changed
441
-
442
- - **`LabelSettings.telegram`/`.whatsapp` drop `price` and `refundable`.** They
443
- moved to obtapi's `addons` table; the label manager reads them from the
444
- label's `addons` map instead (see `@autobusal/admin-label` 1.15.0). The
445
- public `settings.addons` shape is unchanged, but its contents are now
446
- filtered by who is asking and through what channel.
447
-
448
- ## [1.13.0] - 2026-08-01
449
-
450
- ### Removed
451
-
452
- - **`CityData.booking_id`.** The only thing it ever fed was the Booking.com
453
- affiliate banner on the results page, which is gone (see
454
- `@autobusal/routes-order` 1.12.0), along with the `cities.booking_id`
455
- column behind it. Type-only change; nothing read the field at runtime.
456
-
457
- ## [1.12.0] - 2026-08-01
458
-
459
- ### Added
460
-
461
- - `FoundData.stops_count` - intermediate stops between the searched from/to,
462
- where 0 means a direct trip. Optional, because external-provider offers do
463
- not report one: a filter must read "missing" as unknown, never as direct.
464
- - `FoundData.priority` - the operator's subscription priority, which obtapi
465
- was already sending but which was not typed. Used only as a tiebreak in
466
- routes-order's "recommended" ordering.
467
-
468
- ## [1.7.0] - 2026-07-31
469
-
470
- ### Added
471
-
472
- - **`LabelSettings.seo` / `SettingsData.seo`** (`types/settings.ts`): optional
473
- homepage title/description/keywords override, admin-managed via the new
474
- `@autobusal/admin-label` 1.9.0 SEO tab and persisted through obtapi's
475
- `Labels\AdminController::seo()`. `SettingsData.seo` is the public-safe
476
- subset exposed by `Settings\ViewController::get()` for `StaticHome.tsx`
477
- to prefer over its i18n translation-template values when set.
478
- - **`CountryData.seo_title`/`seo_description`/`seo_keywords` and the same
479
- three fields on `CityData`** (`types/locations.ts`): optional per-entity
480
- override for a country/city landing page's meta tags, admin-managed
481
- through the existing Countries/Cities admin forms and persisted via
482
- obtapi's `Countries\AdminController`/`Cities\AdminController`.
483
- `LocationCountry.tsx`/`LocationCity.tsx` prefer these over their i18n
484
- translation-template + live-stats content when set.
485
-
486
- ## [1.6.9] - 2026-07-31
487
-
488
- ### Added (re-added)
489
-
490
- - **`useRecoverFromDomCorruption()`'s global `removeChild`/`insertBefore`
491
- listener is back**, generalized. Removed in 1.6.8 as a backstop
492
- specifically for `GoogleReCaptchaProvider`'s remount corruption, on the
493
- assumption that removing that provider removed the trigger. It didn't:
494
- `@autobusal/common` 1.8.3 fixed a second, independent cause
495
- (`Meta.tsx`'s stale-tag cleanup racing React 19's own `<title>`/`<meta>`
496
- hoisting), and the exact same uncaught error still reproduced
497
- intermittently with BOTH fixes shipped - some remaining React/router
498
- timing edge case neither touches. Reinstated as a permanent safety net
499
- rather than a fix for one specific bug: whatever the next contributing
500
- cause turns out to be, a reload of the current URL has recovered
501
- cleanly in every reproduction so far.
502
-
503
- ## [1.6.8] - 2026-07-31
504
-
505
- ### Removed
506
-
507
- - **`Recaptcha/Recaptcha.tsx` and its `<Recaptcha>` wrapper around the
508
- app in `Providers.tsx`.** This mounted `GoogleReCaptchaProvider`
509
- (`react-google-recaptcha-v3`) as a persistent context provider inside
510
- the same Suspense tree as `Styles`/`Notifications`/`Setup`; that
511
- provider's script/badge injection is async and not remount-safe, and
512
- any remount of it left the DOM in a state that could throw an
513
- uncaught `removeChild`/`insertBefore` error on the next reconciliation
514
- anywhere near the root - the actual root cause of the frozen-
515
- navigation bug 1.6.6/1.6.7 worked around. `@autobusal/hooks` 1.3.1
516
- moved reCAPTCHA to the same on-demand, no-provider pattern Turnstile
517
- already used (`useRecaptcha`'s external API is unchanged), so nothing
518
- needs this wrapper anymore - removing it removes the remount risk at
519
- the root instead of continuing to recover from it. `useGetSettings()`
520
- et al are otherwise unaffected: `Styles`/`Notifications`/`Setup` still
521
- share the outer boundary exactly as before, just without `Recaptcha`
522
- in the tree.
523
- - **`useRecoverFromDomCorruption()`'s global `removeChild`/`insertBefore`
524
- listener**, added in 1.6.7 as a backstop specifically for the
525
- `GoogleReCaptchaProvider` remount corruption above. With that provider
526
- gone, its trigger no longer exists.
527
-
528
- ## [1.6.7] - 2026-07-31
529
-
530
- ### Added
531
-
532
- - **`Providers.tsx`: self-healing reload if the DOM ever corrupts near the
533
- root.** 1.6.6 closed the deterministic trigger for the
534
- `GoogleReCaptchaProvider` remount corruption (see 1.6.6 below), but its
535
- script/badge injection is still fundamentally async - a script tag's
536
- `onload` racing React's synchronous mount/cleanup - so a rare remount
537
- can still leave the DOM in a state that throws an uncaught
538
- `removeChild`/`insertBefore` error on the next reconciliation anywhere
539
- near the root. That class of error isn't render-phase, so no
540
- `ErrorBoundary` catches it, and it silently aborts whatever navigation
541
- was in flight: the URL updates but the page never re-renders, with no
542
- visible error and no way to navigate out. A reload of the current URL
543
- always recovers cleanly (the router's `history.pushState` already
544
- applied), so `Providers` now listens for exactly this error signature
545
- and self-heals with one reload, guarded by a 10s `sessionStorage`
546
- cooldown so a genuinely persistent error can't loop.
547
-
548
- ## [1.6.6] - 2026-07-31
549
-
550
- ### Fixed
551
-
552
- - **`Providers.tsx`: `Setup` (and its `useGetMenu()` call) now has its own
553
- inner `<Suspense>` boundary**, instead of sharing the outer one with
554
- `Recaptcha`/`Styles`/`Notifications`. `Setup` calls a second,
555
- independently-resolving `useSuspenseQuery` (`get-menu`) after
556
- `useGetSettings()`; on a cold menu cache this suspended AFTER
557
- Recaptcha's `GoogleReCaptchaProvider` had already committed and
558
- injected its script/badge - and a Suspense boundary that re-suspends
559
- discards its already-committed content and remounts it once ready.
560
- `GoogleReCaptchaProvider` isn't remount-safe (its script/badge
561
- injection is async, outside React's own DOM bookkeeping), so the
562
- remount left a duplicate, half-torn-down `.grecaptcha-badge` behind.
563
- That corrupted DOM state then threw an uncaught `TypeError: Cannot
564
- read properties of null (reading 'removeChild')` the next time React
565
- reconciled anywhere near the root - i.e. on every subsequent route
566
- change, since Recaptcha wraps the whole router. The exception isn't
567
- caught by the app's `ErrorBoundary` (render-phase only), so it
568
- silently aborted the commit: the URL would update (the router's
569
- `history.pushState` already ran) but the page itself never
570
- re-rendered - freezing client-side navigation app-wide, most visibly
571
- reproduced via the account dropdown menu (Password, Telegram, and the
572
- impersonation-aware Logout/"Return to my account" link, which is what
573
- surfaced this).
574
-
575
- ## [1.6.5] - 2026-07-31
576
-
577
- ### Added
578
-
579
- - **`Queries/apiClient.ts` sends `Content-Language`** on every request,
580
- reflecting the live active i18next language. The API's
581
- ChangeApiLanguage middleware/Languaged trait expect this to localize
582
- city/country/article/... names, but the frontend never sent it -
583
- those names always rendered in the API's own default locale
584
- regardless of the UI's actual language. Requires obtapi's
585
- Traits\Languaged fix (falls back safely for the 13 UI languages the
586
- database has no columns for) and @autobusal/bff 1.2.0 (forwards the
587
- header upstream in bff mode) to actually take effect end-to-end.
588
-
589
- ## [1.6.4] - 2026-07-30
590
-
591
- ### Added
592
-
593
- - **`types/locations.ts` `CityStats`/`CityDestination` types + `CityData.stats`/`.destinations`** - real route/operator/destination-count/price stats and reachable-destinations list from obtapi's new `cities/view` endpoint, for city hub landing pages.
594
-
595
- ## [1.6.3] - 2026-07-30
596
-
597
- ### Added
598
-
599
- - **`types/locations.ts` `CountryStats` type + `CountryData.stats`** - real
600
- route/operator/city/price-from counts from obtapi's enriched
601
- `countries/view` endpoint, for country landing pages to render actual
602
- descriptive content instead of a bare city list.
603
-
604
- ## [1.6.2] - 2026-07-30
605
-
606
- ### Fixed
607
-
608
- - **`Setup/analytics.ts` now initializes GA4 with `send_page_view: false`.**
609
- Pairs with `@autobusal/common` 1.8.0, which sends every pageview
610
- explicitly (including the first) from `Meta` instead - previously the
611
- automatic initial page_view and any future manual tracking would have
612
- double-counted the first page of every session.
613
-
614
- ## [1.6.1] - 2026-07-30
615
-
616
- ### Fixed
617
-
618
- - **`Organization`/`WebSite` JSON-LD `url` was wrong** (introduced in 1.6.0,
619
- same day) - used `data.url` (`SettingsData.url`, from `Label::url()`),
620
- which is deliberately the obtapi asset-storage path for this label's
621
- uploaded files (`https://obtapi.../storage/labels/{label}`), not the
622
- site's own public domain. Now uses `window.location.origin`.
623
-
624
- ## [1.6.0] - 2026-07-30
625
-
626
- ### Added
627
-
628
- - **Sitewide `Organization` + `WebSite` JSON-LD** (`Setup/Preload.tsx`), via
629
- the new `@autobusal/common` `JsonLd` component - the entity-grounding
630
- signal AI assistants and Google's Knowledge Graph use to resolve "who is
631
- this brand". `Organization.sameAs` is populated from
632
- `preferences.social.*`. No `WebSite.potentialAction`/SearchAction -
633
- that schema is for a single free-text query field (Google's sitelinks
634
- searchbox); this app's search is two-field (from/to), so a SearchAction
635
- here would be invalid structured data, not a working sitelinks box.
636
-
637
- ## [1.5.1] - 2026-07-30
638
-
639
- ### Fixed
640
-
641
- - **`Errors/Message.tsx` (404 / system error page) never rendered a
642
- `<title>` of its own** - relied entirely on `Preload`'s app-shell fallback
643
- (the company name), and shipped `noIndex` neither. Now renders its own
644
- `<Meta>` with the localized not-found/system-error message as the title
645
- and `noIndex={true}` - error pages have no unique content worth indexing.
646
- Pairs with `@autobusal/common` 1.5.1's title-ownership fix in the same
647
- release, which also cleans up the resulting `<title>` duplication with
648
- `Preload`'s fallback.
649
-
650
- ## [1.5.0] - 2026-07-30
651
-
652
- ### Fixed
653
-
654
- - **`Setup/Preload.tsx` migrated off `react-helmet`** to React 19's native
655
- `<title>`/`<meta>`/`<link>` hoisting, same root cause and fix as
656
- `@autobusal/common`'s `Meta.tsx` in this same release - see that
657
- package's changelog for the full diagnosis. No behavior change intended
658
- beyond making the title/favicon/theme-color tags this component sets
659
- actually reach the DOM, which they silently weren't.
660
-
661
- ## [1.4.1] - 2026-07-30
662
-
663
- ### Fixed
664
-
665
- - **`<html lang>` never reflected the active language (`Setup/languages.ts`).**
666
- `index.html` hardcodes `lang="en"` at build time and nothing updated it
667
- afterward - every one of the 15 supported languages rendered under the same
668
- `lang` attribute, a signal both search engines and screen readers rely on.
669
- `languages()` now sets `document.documentElement.lang` immediately on init
670
- (using the same `localStorage`-or-default resolution as the i18next `lng`
671
- option) and keeps it in sync via `i18next.on('languageChanged', ...)`, so it
672
- tracks every future change regardless of what triggers it.
673
-
674
- ## [1.4.0] - 2026-07-30
675
-
676
- ### Added
677
-
678
- - **Cache-busting on language file requests (`Setup/languages.ts`).** A
679
- production report showed the admin panel still displaying raw i18n keys
680
- after obtapi's translation JSON was fixed and confirmed correct
681
- server-side - even after multiple manual hard-reloads. `Cache-Control:
682
- no-cache` on obtapi's side only asks caches to revalidate before reuse,
683
- which was not reliable enough against whatever was actually caching
684
- (browser and/or an intermediate proxy) in the real deployment. The
685
- `loadPath` now appends `?v={{VITE_BUILD_ID}}`, a fresh value baked in at
686
- build time by the consuming app's `vite.config.ts` `define` - every
687
- rebuild produces a URL nothing in the chain has ever cached, independent
688
- of whether that cache actually honors Cache-Control. Falls back to a
689
- fixed `'dev'` string if the consumer hasn't defined `VITE_BUILD_ID` yet
690
- (no worse than before). **Requires consuming apps to add a
691
- `VITE_BUILD_ID` define to `vite.config.ts`** (done for magus and
692
- alvavel alongside this release) for the cache-busting to take effect -
693
- without it, this version behaves identically to 1.3.9.
694
- - Companion fix in obtapi: `public/.htaccess` strengthened from
695
- `Cache-Control: no-cache` to `no-store, no-cache, must-revalidate,
696
- max-age=0` plus `Pragma`/`Expires`, so language JSON is never stored by
697
- a compliant cache at all, not just revalidated.
698
-
699
- ## [1.3.9] - 2026-07-30
700
-
701
- ### Fixed
702
-
703
- - `Errors/Message.tsx`: the crash/404 page's logo `alt` text was hardcoded to
704
- `"Home"` in every locale, bypassing the `@lang/errors` translation
705
- dictionary used for the rest of the page. Now reads `translations.home`.
706
- Requires consuming apps' `src/lang/errors.ts` to define a `home` key per
707
- locale (added to magus and alvavel alongside this release).
708
-
709
- ## [1.3.8] - 2026-07-29
710
-
711
- ### Added
712
-
713
- - `LabelSettings.share`: admin-editable footer social-share links
714
- (Facebook/X/Instagram/TikTok/LinkedIn/Trustpilot URLs), distinct from
715
- the existing `LabelSettings.social` (OAuth login credentials). Feeds
716
- the public `SettingsData.preferences.social` shape, unchanged.
717
-
718
- ## [1.3.7] - 2026-07-27
719
-
720
- ### Added
721
-
722
- - `FoundData.external`/`FoundData.price.id` widened to admit external
723
- bus-route provider offers (Mode A: search + redirect) - `id`/`price.id`
724
- become the synthetic `ext:{provider}:{offer_id}` string, and an optional
725
- `external: {provider, mode, offer_id}` discriminator marks the row so the
726
- frontend never sends it to local-only endpoints (seat maps, coupons,
727
- checkout). Scoped to `FoundData` only - the shared `PriceData`/`RouteData`
728
- interfaces (used by operator-side price editing) are untouched.
729
-
730
- ## [1.3.6] - 2026-07-25
731
-
732
- ### Added
733
-
734
- - `SettingsData.addons` - whether the WhatsApp/Telegram checkout addon is
735
- available for this label and what it costs (never credentials), exposed
736
- by obtapi's `Settings\ViewController::get()` for the checkout SPA to
737
- render the addon selection at Step5.
738
-
739
- ## [1.3.5] - 2026-07-25
740
-
741
- ### Changed
742
-
743
- - `LabelSettings.telegram` upgraded from `{enabled: boolean}` to the same
744
- `{status: 'disabled'|'dedicated'|'main', price, ...}` shape as the new
745
- `LabelSettings.whatsapp` - both are now paid checkout addons that can
746
- route through the main label's connection instead of requiring their
747
- own.
748
-
749
- ## [1.3.4] - 2026-07-25
750
-
751
- ### Added
752
-
753
- - `LabelSettings.telegram`: admin-managed bot config (enabled/bot_token/
754
- bot_username) for the Telegram route-notifications feature.
755
-
756
- ## [1.3.3] - 2026-07-25
757
-
758
- ### Added
759
-
760
- - `LabelPayments.raiffeisen.source`: 'dedicated' | 'main' - whether this
761
- label uses its own RaiAccept account or routes through the main label's
762
- account/domain (RaiAccept only accepts one registered domain per
763
- merchant account). See obtapi RaiffeisenController::relay().
764
-
765
- ## [1.3.2] - 2026-07-25
766
-
767
- ### Fixed
768
-
769
- - `RouteData.passenger_fields` (route form submit shape) missing from 1.3.1.
770
-
771
- ## [1.3.1] - 2026-07-25
772
-
773
- ### Added
774
-
775
- - `TripData.passenger_fields` (which passenger details a route requires at
776
- checkout) and `TicketData.whatsapp`/`telegram` messaging contacts.
777
-
778
- ## [1.3.0] - 2026-07-25
779
-
780
- ### Changed
781
-
782
- - **Captcha is now a runtime, per-label setting.** `Recaptcha.tsx` reads the
783
- label's captcha config (provider + public site key) from `/api/settings/get`
784
- via `useGetSettings()` and mounts `GoogleReCaptchaProvider` ONLY when the
785
- provider is `recaptcha` (build-time `VITE_RECAPTCHA_KEY` remains as a
786
- fallback). Cloudflare Turnstile needs no app-level provider. Because the
787
- component now needs the query client, it moved INSIDE `<Queries>` in
788
- `Providers.tsx`. `SettingsData`/`LabelSettings` types gained `captcha`.
789
-
790
- ## [1.2.12] - 2026-07-24
791
-
792
- ### Fixed
793
-
794
- - The full user profile - name, email, and via the nested admin/operator/agent/subagent/employee/driver/visitor sub-object: phone, address, passport, date of birth, company/NIPT - was persisted verbatim to localStorage, readable at rest by any script on the origin. Only a display-only subset (id, name, name_display, type, status) is now persisted; the full object still lives in-memory during an active session exactly as before. A new bootstrap fetch (wired into Setup) re-hydrates the full profile once per app load using the same reduced signal, guarded so it never fires for a genuine guest.
795
-
796
- ## [1.2.11] - 2026-07-19
797
-
798
- ### Fixed
799
-
800
- - bff-mode 401 no longer logs the user out when the BFF confirms the session is still valid: the apiClient now rejects the failed request (letting react-query retry) instead of tearing down auth. A single account query 401ing (transient race or a flaky upstream call) previously bounced the user to /login even though they were logged in. Bearer mode unchanged.
801
- ## [1.2.10] - 2026-07-19
802
-
803
- ### Fixed
804
-
805
- - Resilient bff-mode auth: on a 401 the apiClient now confirms the session with the BFF (GET /bff/session) before logging out, and transparently retries the request once if still authenticated. Prevents a transient post-login 401 (an account query racing the login cookie hand-off) from bouncing the user straight back to the login page. Bearer mode is unchanged.
806
- ## [1.2.9] - 2026-07-19
807
-
808
- ### Added
809
-
810
- - **Selectable auth transport in `Queries/apiClient.ts`.** A new
811
- `VITE_AUTH_MODE` env flag (`bff` | `bearer`, defaulting to `bff`) picks how
812
- the Axios client talks to the API. In `bff` mode the client targets the
813
- same-origin `/bff` with `withCredentials`, relying on the httpOnly
814
- `bff_session` cookie so no Bearer token or CSRF token ever lives in
815
- JavaScript. `bearer` mode preserves the legacy transport (`VITE_API_OBT`
816
- base URL, `withXSRFToken`, localStorage token) for brands that cannot run a
817
- server-side component at their origin.
818
- - **`isBffMode` export** derived from `VITE_AUTH_MODE`, re-exported from the
819
- package root (`index.ts`) so consumers can branch on the active transport.
820
- - **`GatewayCredentials<T>` interface in `types/settings.ts`**, modelling the
821
- new per-gateway shape of `{ mode, sandbox, production }` so every payment
822
- gateway can store a sandbox and a production credential set plus which one
823
- is active.
824
-
825
- ### Changed
826
-
827
- - **`Queries/apiClient.ts` request interceptor is now bearer-only.** The
828
- interceptor that reads `token` from localStorage and sets the
829
- `Authorization` header is only registered when not in BFF mode; in BFF mode
830
- there is no token in JavaScript to attach.
831
- - **`types/settings.ts` `LabelPayments` gateways reshaped.** `bkt`, `stripe`,
832
- `mollie`, `raiffeisen`, and `nexi` are now wrapped in
833
- `GatewayCredentials<...>` (sandbox/production/mode). The BKT credential key
834
- was corrected from `authid` to `authId`, Stripe gained a `webhook_secret`
835
- field, and `devpos` gained `mode`, `sandbox_url`, and `production_url` so
836
- its API host is configurable per-label instead of hardcoded to the demo URL.
837
- - **`Providers.tsx` wraps the tree in an outer `<Suspense>` boundary.**
838
- `Styles` and `Setup` both call `useGetSettings()` (a suspense query) above
839
- the existing inner boundary; on a cold settings cache their suspension had
840
- no boundary to catch it. The outer boundary shows the `loading` fallback
841
- during the initial settings fetch; the module-scope QueryClient means the
842
- suspended remount reuses the same cache with no refetch loop.
843
- - **`Notifications/Notifications.tsx` error toast duration raised to 6000ms**
844
- (from the 4s default) so users are less likely to miss error notifications.
845
- - **`Setup/languages.ts` i18next `debug` is now `import.meta.env.DEV`** instead
846
- of hardcoded `true`, silencing i18next debug logging in production builds.
847
- - **`Errors/Message.tsx` logo is now a keyboard-accessible control** (`role`,
848
- `tabIndex`, `onKeyDown` for Enter/Space, `alt` text) with a matching
849
- `:focus-visible` outline and `display: inline-block` in `Errors/styles.ts`.
850
-
851
- ### Fixed
852
-
853
- - **Error page crash on non-English locales (`Errors/Message.tsx`).** The
854
- translations lookup now falls back to `errors.en` when the selected locale
855
- is absent from `errors.ts`; previously a non-English selection (e.g.
856
- `localStorage` language `sq`) made the lookup `undefined` and crashed the
857
- error page.
858
- - **Crash inside the 500 response interceptor (`Queries/apiClient.ts`).** The
859
- server-error message lookup now falls back to `languages.en` for the same
860
- reason, so a missing locale can no longer throw from within the error
861
- interceptor itself.
862
-
863
- Authored by Ferjolt Ozuni. Consolidated from the magus and alvavel whitelabel
864
- patch sets into canonical @autobusal source (eliminates per-repo patch-package
865
- divergence).
866
-
867
- ## 1.8.0
868
-
869
- `TicketData.status` ('active' | 'cancelled'). A passenger can now be
870
- cancelled without the rest of the booking going with them, so consumers need
871
- a way to tell. Optional - a missing value means 'active', which keeps older
872
- payloads and any not-yet-rebuilt consumer type-checking.