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