Compare commits

..

279 Commits

Author SHA1 Message Date
Maurice 3e90ece087 fix(settings): list the phone plugin version date in the feature pairs 2026-11-04 00:28:49 +00:00
Maurice 65e5d4ab72 fix(settings): format the date format files prettier flagged 2026-11-03 23:39:06 +00:00
Maurice 154620b99a fix(settings): format two settings tests and keep the plugin panel at its size 2026-11-03 23:38:52 +00:00
Maurice adbe3a40f6 fix(settings): lint, size and test follow-ups for the date format
The admin reset link is a component of its own instead of one made during render, the version line of the plugin panels moved into small modules, the new settings rows hand their save on as void, the date picker takes an accessible name for fields without a visible label (the Vacay hire date), the Vacay and poll tests type through the shared picker, and the theme, size and lint baselines are lowered to what the files hold now.
2026-11-03 23:38:52 +00:00
Maurice 44c6fbf372 fix(settings): let every date follow a date format of the user's choice
Every date in TREK was written in the interface language's format, and
English is en-US, so an English interface always showed and read
mm/dd/yyyy with no way to change it. A few native date inputs followed
the browser instead, so one screen could mix two formats.

date_format is now a user setting with an admin default, next to the
time format: automatic (the language's format, as before), YYYY-MM-DD,
YYYY/MM/DD, DD.MM.YYYY, DD/MM/YYYY or MM/DD/YYYY. The date picker shows,
pre-fills, hints and reads typed dates in it, and every date written in
numbers across the app (lists, cards, admin panels, CSV and PDF exports)
goes through one formatter. Dates written with a month name keep the
language's wording. The remaining native date inputs are the shared
picker now. The display settings MCP tool reads and writes the key.

Closes #2971
2026-11-03 23:38:37 +00:00
Maurice 45690ecd14 chore(costs): keep the money helpers used only by settlementFx module-private 2026-11-03 23:21:12 +00:00
Maurice c6149cd64d fix(costs): move the settlement currency setup into the money helpers and hold the size baselines 2026-11-03 22:16:41 +00:00
Maurice 4587cf2099 Drop the unused within import and lower the test assertion baseline 2026-11-03 22:16:41 +00:00
Maurice 4585988dfd fix(costs): keep convertBooked at five parameters and update the tests to as-typed display rows
The display-currency check is its own helper (inDisplayCurrency), used by the Costs model and the shared trip page. The tests that pinned #2525's reading of a display-currency bill through the trip currency now pin the as-typed reading, which the balances use too; read in the trip currency, such a bill and its repayment may stay a euro cent apart.
2026-11-03 22:16:41 +00:00
Maurice a37a9c0fa8 fix(costs): count an expense in the display currency as typed
On a yen trip read in euros, an expense entered in euros was booked through the yen at the rate frozen on entry and shown back at today's rate, so 1,081.09 EUR read 1,078.86 EUR a day later, and repaying exactly what was typed left 2.23 EUR owed.

Rows in the display currency now net apart in whole display cents, as entered, and the lists show them as typed. A payment in the display currency first pays what is owed between the two in such rows; only the rest goes through the trip currency at its frozen rate, so a trip-currency debt settled in the display currency stays settled when rates move (#1445). Every other row still nets in trip cents and converts once (#1382), so the balances add up to zero in both halves.

Closes #2965
2026-11-03 22:16:41 +00:00
Maurice 0cde099c7f style(assistant): format the blind-call hint 2026-11-03 22:39:24 +01:00
Maurice e798eb712e fix(assistant): run a blind call, load its group, and say why when its arguments fail
Refusing every call to a tool of a group not loaded in the turn also stopped calls whose arguments were fine. The call now runs as before; its group is loaded at once, so the next step offers the schema, and when the arguments fail the MCP check the error tells the model the schema is there now and to send the call again.
2026-11-03 22:09:54 +01:00
Maurice 0dae908e05 fix(assistant): send back a call to a tool whose group the turn has not loaded
The model could call a tool it knew from an earlier turn without loading its group in this one. TREK ran it, but the model had never seen its schema in the turn, so numbers, arrays and objects came as strings and required arguments were missing, and the MCP check refused it with -32602 (mostly the Tours tools).

Such a call no longer runs: its group is loaded for the rest of the turn and the call is answered with a tool error asking for it again with the schema the next step now offers.

Closes #2967
2026-11-03 21:48:37 +01:00
Maurice 01c7bc9de2 fix(tours): plural toast, repo route and test updates for the waypoint import
The places toast is a plural group in every language, the points import goes through tourRepo instead of src/api, the hook and dialog keep their complexity, and the tests that checked the old confirm call or the GPX left-out count follow the new behaviour.
2026-11-03 20:42:14 +01:00
Maurice 2e683f8609 fix(tours): keep the waypoints of a GPX/KML file that also holds a track
With the Tours addon on, a file picked or dropped on the places column goes
into the Tours import as soon as it holds one line, and that import reads
lines only. A Sygic or Trip-o-matic export (a route plus its stops) came in
as one tour and every place was dropped without a word.

The preview now counts a GPX file's waypoints as it already counted a KML
file's pins, and the dialog offers to add those points as places of the
trip, on by default. Confirming runs the Tours import for the lines and the
places import (points only) for the same files. A file whose tracks the
trip already holds can still bring its points.

Closes #2969
2026-11-03 20:19:43 +01:00
Maurice f8e51cb83d fix(pdf): hand the trip PDF to Safari from the iOS Home Screen app
An iPhone or iPad running TREK from the Home Screen ignores print(), in
the page and in an iframe alike, so "Save as PDF" in the preview did
nothing there. Mobile Safari prints the same preview fine.

In the installed iOS app the preview now shows "Open in Safari" in
place of the dead button, with a line saying why. The link opens the
trip with ?export=pdf, and both shells open their export dialog or
sheet for it, so the PDF is one tap away once Safari is up. The
parameter is dropped once read and survives the login redirect.
Everywhere else the preview prints as before.

The preview overlay moves out of TripPDF.tsx into pdfPreview.ts, which
TripPDF had no room left for under lint:size, and its backdrop and
stacking now use the --overlay and --z-modal tokens.
2026-11-03 09:14:55 +01:00
Maurice eedfb08597 Managed mode: list the phone OIDC card and drop the map model from the default settings pair
- MAdminOidcCard is markup over the settings section's state, so it is
  listed under mobileOnly with that reason.
- The default user settings views take their presets from
  useDefaultUserSettings now and import only a type from mapSettingsModel,
  so that pair names the map settings views alone.
2026-11-03 08:44:45 +01:00
Maurice 0aee0a019e Managed mode: lower the test any baseline the journey helper brought down 2026-11-03 08:44:45 +01:00
Maurice 91bc1944dc Managed mode: keep the admin settings and addons ratchets green
- The phone's OIDC card moves into MAdminOidcCard, so the settings section
  is back within its complexity limit after the managed-mode conditions.
- enabledPhotoProviderIds reads the enabled ids straight from the
  repository and has a test of its own, which brings the addons coverage
  back to its threshold.
2026-11-03 08:44:45 +01:00
Maurice f99b40cd81 Managed mode: keep the journey page suite within its size baseline
- The owner sign-in and the photo provider the suite runs with move into
  tests/helpers/journeyDetail.ts, so the page test is back under its line
  baseline, which drops to 4028.
- Drop help/registry.base.ts, an unused copy of the help registry whose key
  builders the i18n key check could not resolve.
2026-11-03 08:44:45 +01:00
Maurice 59e68d81b2 Managed mode: satisfy the strict, loop and test ratchets
- enabledPhotoProviderIds skips rows without an id instead of widening the type.
- The listed events read each addon once, in parallel, not in a loop.
- The narrowing test reuses the suite's service and swaps the addon answers.
- PlaceContribPanel keeps its complexity by naming the standalone check.
2026-11-03 08:44:45 +01:00
Maurice 86513fda0d Managed mode: hand place search, routing, plugins and docs to the operator
On a managed install the operator runs place search and routing, the plugin
runtime, the integrations and the documentation, so the admin and the users
no longer see switches and guides for them:

- The place search and routing card, the transit provider and the search log
  are gone from the admin settings (desktop and mobile). places_provider is a
  managed-locked setting, and the transit provider, search log and wanderer
  routes refuse writes there. Share place details stays the admin's, as a card
  of its own, hidden when TREK_PLACES_CONTRIBUTE switches it off.
- File types and trip reminders move to the right column, so the columns stay
  balanced.
- Plugins disappear from the admin and the user settings, wanderer from Tours.
- The Help & Docs page and its links are not offered. The help center stays and
  leaves out guides about what the operator runs (keys, OIDC, backups, storage,
  releases, plugins, document sync) and about integrations that are off.
- Mapbox GL is offered only with the operator's token, Amap tiles only where
  the operator set Amap up.
- The mobile admin no longer shows OIDC and the WebAuthn relying party fields,
  matching the desktop tab.

For every install:

- Notification matrices list only events the install can raise: no plugin row
  without the plugin runtime, no Vacay, collection, photo, Synology, wanderer or
  packing rows while those are off. On a managed install the release and
  replica alerts are left out.
- The journey entry editor offers External photos only where a photo provider
  is switched on, as the phone already did.
- app-config is asked again after a failed answer. A page opened while the
  server restarted kept every default for the session, which on a managed
  install brought back the operator's settings.
2026-11-03 08:44:45 +01:00
Maurice 61378559ca chore(deps): runtime security floors and the CI action updates
- sharp 0.35.5 (librsvg CVE), with the musl pins that ship in the image.
- follow-redirects 1.16.2, postcss-selector-parser 6.1.5 and
  serialize-javascript 7.1.3, from the open advisories.
- GitHub Actions: github-script 9 and the minor-and-patch group.
- plugin-sdk: @types/update-notifier 6.0.9.

The grouped npm update is left out: with it the server build runs out of
memory in tsc, and the culprit is not found yet.
2026-11-02 21:58:16 +01:00
Maurice 84df69c18a Revert "chore(deps): take the safe updates from the open Dependabot PRs"
The server build runs out of memory in tsc with the grouped updates. Back to
the previous dependencies until the update that causes it is found.
2026-11-02 21:56:33 +01:00
Maurice 6194a658d9 chore(deps): take the safe updates from the open Dependabot PRs
- The npm minor-and-patch group (#2952) without mapbox-gl: 3.34 drops
  setRTLTextPlugin from its typings, and the RTL plugin is what joins Arabic,
  Hebrew and Persian labels. react and react-dom move to 19.3.1 together,
  overrides included, so the workspace keeps one React.
- Security floors from the open alerts: sharp 0.35.5, vite 8.3.5,
  follow-redirects 1.16.2 (#2927), postcss-selector-parser 6.1.5 (#2865) and
  serialize-javascript 7.1.3.
- GitHub Actions: github-script 9 (#2951) and the minor-and-patch group (#2950).
- plugin-sdk: @types/update-notifier 6.0.9 (#2948).
2026-11-02 21:30:17 +01:00
Maurice 97188c16d1 fix(place-contrib): mark the independent writes and use TREK's OSM id form in the catch-up test 2026-11-02 14:11:27 +01:00
Maurice 247d5fc519 feat(services): get the key at startup and share earlier place details
An installation now asks for its TREK services key a few minutes after
it starts, and the daily check registers a missing key as well as
renewing a due one. Until now nothing happened before someone searched
for a place or opened the admin settings, so "Share place details"
stayed unavailable on a fresh or updated instance.

When an admin agrees to share place details, what users typed into
places since TREK 5.1.0 goes out too, not only what they type from then
on. The origin of every field is recorded since 5.1.0 whatever the
decision, so a catch-up walks those places page by page before each
send run: only values that are still the ones a user typed, only for
fields switched on, only for places the service can key, and only
while the queue holds fewer than 200 values. It goes on where it
stopped after a restart, never replaces a newer value a save queued,
and ends on switching off, declining or withdrawing everything. A field
switched on later catches up as well, under an agreement given with the
new prompt sentence that says so; an agreement from 5.1.0 does not.

Queuing moves into PlaceContribQueue so a save and the catch-up take
the same path.
2026-11-02 13:56:32 +01:00
Maurice 8659262e59 fix(services): let slow handshakes reach the TREK services
Node gives each address of a dual-stack name 250 ms before it moves on.
From Australia that is shorter than one round trip to Europe, so on a
network without IPv6 the IPv4 handshake to places.liketrek.com was cut
off, the IPv6 attempt failed at once and every request ended in
ETIMEDOUT. The installation never got its services key, sharing place
details stayed unavailable and searches fell back to OpenStreetMap.

Raise the per-address attempt timeout to 2 s at startup, keeping a
larger value an operator already set, and log the reason behind
undici's bare "fetch failed" for each address tried.
2026-11-02 12:12:48 +01:00
Maurice 327ebc354f fix(push): accept Google's jmt17.google.com push endpoints
Chrome on Android increasingly hands out push subscriptions on
jmt17.google.com, which the push service allowlist did not know, so
turning push on failed with a 400. The host is added as an exact entry,
next to the two FCM hosts.

Closes #2939
2026-11-01 22:51:27 +01:00
roopesh 1625a10424 fix(server): refuse unwritable database in maintenance scripts (#2933) 2026-11-01 22:26:40 +01:00
Raz Erlich 94c2ef35b5 fix(planner): keep places selection bar visible with long lists 2026-11-01 22:08:45 +01:00
Maurice bf8e04741d test(storage): follow the release queue in the tests that pinned an immediate delete
Replacing a cover or an avatar and deleting a note file now hand the old
bytes to the release queue instead of deleting them in the request; the
integration tests check the queue row and that the bytes are still
there. Release mocks answer with a count, the reclaim e2e sends its
refused bodies one after another, and a stale-count reload that fails is
told to the admin instead of swallowed.
2026-11-01 18:46:58 +01:00
Maurice 0b598680ec test(storage): spell the pending-reset case title without a stray quote 2026-11-01 18:32:43 +01:00
Maurice 13332cf06b fix(storage): a pass on a database this installation does not recognise only reports
While a restore's reset is still pending, or the database names another
installation, the nightly pass judges and reports but deletes nothing,
whatever the restored switch says.
2026-11-01 18:32:33 +01:00
Maurice e6d52cb061 docs(wiki): say every restore resets the cleanup state and what the release queue gives up
A restore of this installation's own backup now resets it too, through the .trek-reclaim-reset file. Files dropped from the release queue after 30 days, or refused by a full queue, are only removed by the nightly cleanup once deletion is on.
2026-11-01 18:31:45 +01:00
Maurice bef8fc20f1 fix(storage): reset reclaim state after any restore, re-check owners often, share release ticks
- A restore (admin panel or RESTORE_FROM_BACKUP) now leaves
  data/.trek-reclaim-reset before it swaps the database in. Boot reconcile
  resets the reclaim state when it finds the file, whatever install id the
  database names, and removes it afterwards; a successful reopen removes it
  too. A restart after a failed reopen, or a boot restore of this
  installation's own backup, no longer keeps the old switch, marks and
  confirmations. The id mismatch reset stays.
- The release job skips a tick (install_mismatch) while the database names
  another installation or a reset is still pending.
- An allowed owner verdict is cached for 15 minutes, a refusal for 26 hours.
  The local driver reads a list prefix as a folder, so the category root is
  still listed whole, at most once per category per window.
- Each tick reads every allowed category's due rows separately, an equal
  share of the 200 each, so a category whose deletes keep failing cannot
  starve the others.
- Rows dropped after 30 days overdue and releases refused by a full queue
  are logged at warn with their counts.
2026-11-01 18:31:41 +01:00
Maurice 9d2ffc91af feat(storage): confirm the count shown, and say a backup or a missing install id held the cleanup
Confirm sends the count the admin saw and waits for its answer; a stale
count reloads the state. A running backup is named as a skip reason, a
missing installation id and a stale count as errors, and the switch's
hint no longer promises removal right away: deleted files go a day
later, once no undo can bring them back. Strings in all 28 languages.
2026-11-01 18:14:49 +01:00
Maurice 10ad64d170 docs(wiki): describe the day-later release, the install id file and the confirmation margin
Covers what a clone must do (its own data dir, or delete .trek-install-id),
that an older TREK writes no owner marker and cannot be detected, and the
new backup_running reason.
2026-11-01 18:11:47 +01:00
Maurice 1c6a175ebc fix(storage): queue what a delete left unnamed past every undo, and tell a copied database apart
A delete or a replace no longer deletes inside the request. The owner's call
queues the keys in storage_release_queue, due after RELEASE_DELAY_MS (26 h:
the assistant's 24 h approval window plus a margin), and StorageReleaseJob
takes the due rows every ten minutes, at most 200 a tick and 50 keys a
question, asks every reference source again and deletes what none names.
It refuses what the nightly pass refuses (StorageReclaimGuardService: the
ledger, the required sources, a shared location, another installation's
owner marker, cached per category), and waits while a restore or a backup
runs or another storage job holds the slot. An assistant undo or a late
write that names the old value again keeps its object.

The install id lives in data/.trek-install-id, outside any backup and never
written by a restore. At boot and after every reopen the database is
reconciled with it: a database naming another installation (a boot restore,
a restart after a failed restore, a copied travel.db) loses its switch,
confirmations, reports, marks and queue before anything reads them.

A confirmation now stores the count the admin was shown and when; it covers
up to one and a half times that count (at least 100 more) for 14 days, is
used up once the share is back under a fifth, and a count the last report
no longer holds is refused as reclaim_confirm_stale. The admin state folds
a live confirmation in, so the prompt goes at once.

A pass reads each source once for every category and drops each category's
keys once judged; right before a batch it reads the category's sources
again, whole and in memory, instead of a LIKE per key. A narrowed read holds
at most 50 LIKE terms. A pass held off by a backup reports backup_running,
and a category the ledger never reclaims reports not_reclaimed even after a
stop.
2026-11-01 18:11:43 +01:00
Maurice 0a25de2c6d feat(storage): confirm the reclaim count the admin was shown, name a running backup as a skip, bound a trip cover
The confirm request now carries the unreferenced count the admin saw, so the
server can refuse a count its last report no longer holds. A pass that found
objects due while a backup read the uploads reports backup_running for them.
A trip cover is bounded to 2048 characters on REST, RPC and the MCP tool, as a
place image already is, since a replaced cover is queued for release by every
upload path it spells out.
2026-11-01 18:11:25 +01:00
Maurice 59a6d92cea fix(storage): type the key search run, split the reclaim view per category
The longest-run helper typed its reduce as never, reclaimView grew past
the complexity limit (one function per category line now), and a test
read a promise as a boolean.
2026-11-01 16:37:00 +01:00
Maurice e0982e708f feat(storage): show running checks, confirmations and the nightly result in Admin > Storage
The panels follow the reworked reclaim state: Check now starts a check
in the background and the panel polls until it is done, a category with
an unusually large share of unused files offers Confirm, Freed comes
from the last run that deleted, a stopped check says why and no longer
reads as nothing found, the switch waits for its answer and a poll that
was in flight cannot flip it back, and refusals name their reason in
the operator's language. Strings updated in all 28 languages.
2026-11-01 16:25:38 +01:00
Maurice d17457de56 fix(storage): delete what a delete or a replace left unnamed right away again
Leaving every replaced cover, place image and deleted journey photo to the
nightly pass kept them publicly fetchable by URL for a week at least, and
for good while deletion was off. Owners now hand the values a delete or a
replace left behind to StorageReleaseService once their write committed.
It asks every registered reference source, with the same parser as the
nightly pass, and deletes at once what no row names any more: an object a
trip copy, a journey or a saved place still shows stays, which the old
inline deletes got wrong.

The release holds the pass's guards (reclaimable categories at their root
only, never an owner marker, never while a restore runs, never with a
required source missing or an overlapping location), never fails the
request and logs at debug. It runs for trip covers (update, upload, trip
delete), collection covers and saved-place images, place images (update,
bulk update, delete), journey covers and journey or gallery photos
(their thumbnail goes with a file that went), avatars (change, delete,
account deletion), trip files (permanent delete, empty trash) and collab
note and chat attachments. A trip file row now goes before its bytes, so a
failing backend no longer fails the delete. The objects of a deleted trip's
places and files are left to the nightly pass: their rows are other
domains'.

PlacesService drops the collection places repository nothing read any
more. Size, warning and boundary baselines follow the shrunk files.
2026-11-01 16:18:21 +01:00
Maurice 790fccb92c fix(storage): judge unreferenced files without a brake anyone can trip
The 20 % brake skipped a category and wiped its marks every night once a
fifth of it looked unreferenced. Any user could push a small install over
that line, a backlog from #2924 started braked, and a second install on the
same bucket below the line was not caught at all. It is gone:

- Each pass leaves a trek-owner-<install id> marker at the root of every
  category it judges. A category holding another install's marker is
  skipped as foreign_install, one where the marker cannot be written as
  owner_unknown. Backups leave the markers out.
- The install id lives in app_settings outside the storage.* prefix. The
  switch is stored as that id, so a database from another install reads as
  off, and every reopen (a restore) writes this install's id back, turns
  deletion off and drops the marks, confirmations and reports.
- More than a fifth unreferenced still marks and reports, but deletes only
  after an admin confirmed the count shown (POST .../reclaim/confirm). A
  larger count later waits again.

A skip no longer touches the marks, so a failed listing or lookup does not
restart the week. Check now runs in the background and answers 202; the
admin state says running and keeps the last deleting report apart, so a
report-only check never resets what was freed. A pass that deletes writes a
system audit entry, a pass that stops or fails keeps its partial counts and
marks the rest interrupted, a backend error stops deleting in its category,
and deleting stops for the rest of the pass once the switch is off or a
backup is reading the uploads. The usage scan waits for a pass still
running.

Sources read each category once per pass and answer the pre-delete
re-check with a query narrowed to the keys' plain runs. Bare names keep
their raw spelling (a '#', '?' or trailing blank is part of an upload's
name), and local roots are resolved through their symlinks before the
overlap check. A pass lists only a category's root.
2026-11-01 16:18:08 +01:00
Maurice e59992d56d chore(ci): lower the knip baseline the reclaim branch shrank
storage-registry.service.ts no longer exports a type nothing uses.
2026-11-01 14:55:54 +01:00
Maurice 382797febc fix(storage): let the app boot with the reclaim routes and keep the ratchets where they were
The two admin routes go on the managed allow list, without which buildApp
refused to start. The reclaim calls move to their own api module so
client.ts stays at its size, the phone card sizes its text through the
type tiers, and the tests drop a hand-built service, a non-null assertion
and a promise read as a boolean.
2026-11-01 14:44:53 +01:00
Maurice a1d9cdc8f2 feat(storage): show unreferenced files in Admin > Storage, with the switch and a check
Desktop and phone show what the last check found (can be freed, unused for
less than a week, freed), why a category was skipped, the switch that lets
the nightly run delete and a report-only check on demand, through the
shared storage panel hook. All 28 languages, and the Admin-Storage wiki
page explains the check, the week's wait and when to leave it off.
2026-11-01 14:30:01 +01:00
Maurice b31666d1c9 fix(storage): re-check a batch right before its delete, and drop reads nothing calls
The last question to the sources now comes after the object's own check,
so nothing slower than the delete sits between the answer and the delete,
and it is asked once per batch instead of once per object: a pass read
every source's columns for each object it was about to remove. The
collection reads the removed inline reclaims used are gone with them.
2026-11-01 14:23:04 +01:00
Maurice 19e4f0dadf fix(storage): keep the reclaim pass and a backup restore apart
A restore wipes and rehydrates the uploads under a swapped database, but it
never took the storage job slot, so a reclaim pass could start or carry on
beside it, and a delete already past the generation check could land on an
object the restore had just put back. BackupService now marks a restore as
running on StorageJobsService and waits for a pass in flight to let go; a
pass refuses to start while a restore runs and stops at its next check when
one begins. Backfills and migrations are not affected. The service test also
reads its fake source sets through a helper instead of non-null assertions.
2026-11-01 14:20:11 +01:00
Maurice 8fde643347 fix(storage): make the reclaim state on the admin storage state nullable
The shared contract made reclaim a required field, so every client fixture
typed StorageAdminState stopped compiling. It is nullable now, as planned,
and the five base fixtures carry reclaim: null.
2026-11-01 14:20:11 +01:00
Maurice f44dd92ee0 test(storage): run the reclaim e2e and source check on the whole app
The reclaim e2e and the source-registration check mocked src/db/database,
which lint:test-mocks holds at zero for new files; both now boot buildApp
against the worker's snapshot db. The key parser drops its non-null
assertions (the indexed reads are typed string already), and the e2e sends
its refused bodies at once instead of awaiting inside a loop, so the eslint
ratchet stays where it was.
2026-11-01 14:20:10 +01:00
Maurice db0fbc1ccd fix(storage): stop deleting covers, place images and journey photos another row may still show
Replacing a trip or list cover, changing or deleting a place or saved-place
image and deleting a journey photo removed the bytes at once, without asking
whether a trip copy, a journey, a saved place or another photo row still
named them (#2924). Those paths now leave the object in place and the reclaim
job removes it once no row names it. A journey photo the user deletes takes
its local photo row with it when nothing else links it, so its bytes become
reclaimable instead of being kept by a row nobody sees.

A place image copied from a trip file is still dropped at once when the
update that would name it fails: that copy was never anyone else's.

Size and warning baselines follow the shrunk files.
2026-11-01 13:59:28 +01:00
Maurice 095bed2db8 feat(storage): let every owner of an upload column tell the reclaim pass what it still names
Trips, collections, places, the contribution queue, journeys, photos, trip
files and users each register a reference source from their own domain. A
source reads its own columns and parses every spelling they hold through the
storage helpers, and the free-form columns claim every reclaimable category,
since a cover or an image can point into any of them. The contribution queue
joins the ledger as its own source, because place-contrib owns that table.
2026-11-01 13:59:19 +01:00
Maurice fb2429ebb2 feat(storage): find and reclaim upload objects no row names any more
Domains whose rows name stored objects register a reference source with
StorageReclaimRegistryService; storage never imports them. A ledger in
storage decides per category whether it is reclaimed at all and which
sources must have answered first, and a category whose resolved location
overlaps another one is left alone.

A nightly pass at 04:05 lists the root level of files, journey, covers,
avatars and places, skips objects younger than a day or without a
modification time, asks every source in pages of 500 and marks the rest.
Only with deletion switched on, and only for marks a week old, it asks every
source about the single key again and deletes through StorageService. A
category is skipped and its marks start over when a source is missing or
fails, the listing fails, or more than a fifth of its objects look
unreferenced. A pass shares the one-job slot with backfills and migrations,
never runs on a demo instance and stops when the database is reopened.

Admins switch deletion and run a pass on demand under
/api/admin/storage/reclaim; both are audited, and GET /api/admin/storage
carries the switch and the last report. Deletion is off after install and
upgrade.
2026-11-01 13:37:53 +01:00
Maurice 29298dcfe5 feat(storage): keep a mark per stored object no row names
storage_reclaim_marks records since when the reclaim pass has seen an object
unreferenced, keyed by category and key. A mark is never moved forward, so
its age is how long the object has been unreferenced without a break; it goes
once the object is named again or gone. Owned by the storage domain.
2026-11-01 13:37:44 +01:00
Maurice 630f15b5ea feat(storage): describe the reclaim report, its switch and the run request in the shared contract
The admin state now carries the reclaim switch and the last pass's report.
A report covers every served category, the legacy photos folder included,
with the objects a pass looked at, the unreferenced, eligible and deleted
counts and bytes, and the reason a category was skipped.
2026-11-01 13:37:37 +01:00
Maurice 04debce79e fix(planner): keep the lint and size ratchets where they were
The zone helpers of the day order take the merged row's own type instead
of an explicit any, and the day list's size baseline follows it down.
2026-11-01 11:41:56 +01:00
Maurice 4003769a5c fix(planner): keep the date whole and rename the title where it stands with the date first
With "date first in day headings" a long day title squeezed the date in
the day list to "Th…" (#2920); the name now gives way and the date stays
whole. The day panel and the mobile day sheet kept the rename pencil on
the heading, which then is the date, and the input replaced it (#2921);
pencil and input now go with the title line under the date.
2026-11-01 11:41:56 +01:00
Maurice 7faf771f9c fix(docker): look at the volume roots only, and say when chown is refused
The ownership check still read every inode of the volumes on each start.
It now looks at the dirs and what sits directly in them, hands a volume
over once when it is not node's, and logs a warning instead of walking a
volume that refuses chown (NFS with root_squash).
2026-11-01 11:41:56 +01:00
Maurice 51b991f851 fix(docker): the container starts on large volumes and under runAsUser 1000
The entrypoint no longer runs chown -R over data and uploads on every start;
it only hands over files that are not node's yet, and logs when it does.
Started as a non-root user, it skips chown and gosu and runs Node directly.
The chart exposes podSecurityContext and securityContext for that setup.
2026-11-01 11:41:56 +01:00
Maurice 8692b8790c fix(planner): judge a move by the order a day is drawn in across time zones
The checks behind a drag and the up/down buttons read each booking's raw
clock, so on a day whose flights were put in the order they happen every
move of a timed stop asked to change its time (desktop) or was refused
(mobile). They now read the same key the day is ordered by.
2026-11-01 11:41:56 +01:00
Maurice 1b6a156969 fix(planner): order flights across time zones by when they actually leave
A booking's time is local to its own airport, so a flight leaving
Auckland at 20:20 and landing in Los Angeles at 12:25 the same date
was drawn below the 17:00 flight on from Los Angeles. Bookings whose
dated time has a known zone now take their clock slots in real order.
2026-11-01 11:41:55 +01:00
Maurice f076cb82da docs(readme): link dataforest cloud wherever the sponsor section names it 2026-11-01 10:20:21 +01:00
Maurice 073db83421 fix(assistant): answer the calls of a stopped step and ask for the words behind wordless follow-ups
A step stopped while its calls ran (the user left the page) kept the
provider's whole record of it but answers only for the calls that ran, so
Anthropic and the Responses API refused that thread with HTTP 400 from then
on. Replaying the thread now answers every recorded call that never ran,
which also mends threads already stuck that way.

Claude Sonnet 5.5 at times ends a turn on suggest_follow_ups alone, without
writing its answer, and the turn failed as empty_answer. That step now goes
back to the model whole, its follow-ups answered that the user saw nothing
yet, and their chips come under the answer it then writes. The prompts no
longer say the call goes alone in its step.
2026-11-01 10:18:27 +01:00
Maurice cafc8dd46e fix: link dataforest cloud with /en again
The 5.1.0 popup, the sponsor panel in Settings > About and the README had
switched to https://cloud.dataforest.net/?mtm_campaign=trek. The link stays
https://cloud.dataforest.net/en?mtm_campaign=trek everywhere.
2026-10-31 12:26:36 +01:00
github-actions[bot] dc36c21c08 chore: bump version to 5.1.0 [skip ci] 2026-10-31 11:12:52 +00:00
Maurice 175413b3c1 ci(release): take ci-ok from an earlier commit when main only changed workflows [skip ci]
A fix to a release workflow changes nothing that the tests cover, yet the
gate wanted a full new test run on it. With ci_from the release takes ci-ok
from the commit main's code comes from, provided that commit is an ancestor
of main, everything outside .github/ is identical, and one of its ci-ok runs
is green.
2026-10-31 12:10:35 +01:00
Maurice 030918cfa9 ci(release): check the dialect with node's types
TypeScript 6 includes no @types unless a file imports from them, and the
issued dialect imports nothing from node. Type-checked on its own it did
not know Buffer, so the 5.1.0 release threw away a valid dialect as one
that does not type-check. The check config names node now.
2026-10-31 12:09:32 +01:00
Maurice 75d65ccb1b test(release-notes): build the new cases without non-null assertions
The four cases added with the 5.1.0 popup patched the fixture through
n.release! and read the support card through card!, six warnings over the
tests baseline of the eslint ratchet. They go through a withRelease()
helper and optional chaining instead.
2026-10-31 11:31:13 +01:00
Maurice def8b8a24d release notes: the 5.1.0 popup, and dataforest cloud in Settings > About
The release popup now shows 5.1.0: place search, shared place details
and faster routing as three cards side by side, each with its own
drawing. The support band is set as a card like the promise above it
(optional supportLabelKey; a release without it keeps the plain line),
and the two support buttons get the same glass look in their colours.

Settings > About gets a full-width panel for dataforest cloud, who
sponsor the servers behind place search, address search and routing.
Both logos ship in client/public/brands, so opening Settings reaches no
third party. Wherever a line names dataforest cloud it links to
https://cloud.dataforest.net/?mtm_campaign=trek, the link dataforest
gave us; the README moves to it too.

All 28 locales carry the new copy, and the old 5.0 card keys are gone.
2026-10-31 11:04:19 +01:00
Maurice f4e524f3c3 place-enrichment: the details lookup is a standalone cache fill
The service reads a community place's details before the result cache and
every other place's after it, so the lookup has two call sites of which one
runs. It only fills the details row cache and turns a failure into no
details, so it is tagged @txStandalone and the enrich baseline drops to the
four writes it holds without it.
2026-10-31 06:45:13 +00:00
Maurice 682ae19143 test(place-enrichment): a cached OSM place still reads its details for the community version
An OSM element can hold community values, so the service reads its details
before the cache to learn the version it is served under. The second request
reads them once and then answers from the row cache without another lookup.
2026-10-31 06:27:31 +00:00
Maurice 255607b750 place-contrib: format the settle filters of the outbox repository
The id and cid lists the settle writes match on come from two small helpers,
which keeps each filter on one line in Prettier's layout.
2026-10-31 06:27:31 +00:00
Maurice ce7437a46f i18n: translate the place sharing, services card and refresh pill strings into Hebrew
The Hebrew locale arrived on dev while this branch was open, so its 80 new
admin.contribute, admin.servicesCard and places.refresh keys were missing
there.
2026-10-31 03:25:56 +00:00
Maurice 36d8cc2e7b wiki: the New place details pill offers the description too 2026-10-31 03:25:56 +00:00
Maurice dc76efc418 admin: a withdrawal that went through is reported even when the state re-read fails 2026-10-31 03:25:56 +00:00
Maurice 314b087f98 planner: the refresh pill compares with the place picked anew in the search box
After another place was picked while editing, the pill still offered the
saved place's details and would have written them onto the new one.
2026-10-31 03:25:56 +00:00
Maurice 2f689ea9ca maps: stop reading a community picture at its size cap
An answer without a content-length was buffered whole before its size was
checked. The read now stops at the cap.
2026-10-31 03:25:56 +00:00
Maurice 41edbcf67a maps: validate the community values and OSM elements the TREK Places API answers
Both answers were taken as typed. A value outside the published shape, such
as hours that are no text, reached the hours parser and failed the details
request. A place outside the shape is now left out of /v1/community, and an
element outside it sends the lookup to Overpass.
2026-10-31 03:25:56 +00:00
Maurice 6945b53c10 place-enrichment: a community summary the request carried stays out of the shared cache
Like an OpenStreetMap summary, a summary credited to the community is read
straight off the request's details, so any caller could have it served to
every user of the instance for a week under the community's name.
2026-10-31 03:25:56 +00:00
Maurice 7ade99c8ce place-contrib: audit a switch-off whose withdrawal the service did not confirm
Switching sharing off commits before the withdrawal asks the service. When
the service did not confirm, the decision had taken effect but left no audit
row. It is now audited with withdrawFailed, and the admin still gets the
error.
2026-10-31 03:25:56 +00:00
Maurice 593df53993 places: a picture route records the origin of the picture only
The upload and the trip-file routes vouched for every field that changed
between their read and their answer. A website the assistant or a
collaborator changed while a large picture uploaded was then recorded as
typed by the user and queued for the TREK Places API. Those writers now name
the one field they write, and other changes keep the origin their own writer
recorded.
2026-10-31 03:25:56 +00:00
Maurice aa41c67a20 place-contrib: settle a sent value only while its queue row still holds it
A value saved while the request for its predecessor was out rewrote the same
outbox row with a new cid; the answer for the old value then deleted the row,
or charged it an attempt, and the newer value was never sent. The settle
writes now match the cid that was sent as well as the id, so such a row stays
queued for the next run. A replaced value also counts its age from the
replacement, so a long pause cannot expire a value queued days ago.
2026-10-31 03:25:56 +00:00
Maurice c10a698b9f planner: keep a place's enrichment in the tab for ten minutes, not for good
A saved place reopened for editing has no community version in its key, so
the answer the tab kept the first time stayed until the tab closed: a picture
another installation contributed since never showed. Kept answers now carry
when they were fetched and are asked again after ten minutes, the same span
the server keeps an empty answer. The key version moves to 5 so open tabs drop
what they kept in the old shape.
2026-10-31 03:25:56 +00:00
Maurice e2756dab22 places: a picture another installation contributed shows on the next edit
Two gaps kept it out. The service writes absolute picture URLs on its own
host, and the community service only took relative paths, so every picture
was dropped; it now takes the path of a URL on the configured service and
nothing from another host. And a saved place opened for editing sends no
details, so no community version, and the cached answer kept for a week; for
an index or OSM place the details are now read before the cache, so a new
version is a new answer.
2026-10-31 03:25:56 +00:00
Maurice 330644e88b places: show the community's published pictures first in Pick a picture
The service delivers the pictures review approved for a place, but TREK never
read them. A new TrekCommunityService in the maps domain asks the TREK Places
API for them (the index row for a gers place, /v1/community for an OSM one),
accepts only paths of the service's picture store, and fetches the bytes with
the installation's key, capped at 4 MB. Enrichment puts them ahead of Google
and Commons, through the same photo cache, credited as TREK Community under
CC BY 4.0. The enrichment cache now follows the community version for OSM
places too, so a newly approved picture shows on the next open.

Also lowers the client's no-non-null-assertion baseline to 380: moving Use
this text into the description card removed two.
2026-10-31 03:25:56 +00:00
Maurice 0b86a13b51 admin: fold the transmission log to its newest five rows
A busy log pushed the rest of the card down. It shows the newest five rows,
a Show more toggle opens the rest, and the page buttons appear once it is
open.
2026-10-31 03:25:56 +00:00
Maurice 8d8629516b places: offer a newer description in the refresh pill
The pill compared website, phone, e-mail and hours only, so a description
the community published never showed up on a saved place. It now reads the
index description or the OSM summary, compares it as plain words against
the editor's markup, and shows it clamped to three lines.
2026-10-31 03:25:56 +00:00
Maurice 5822910f40 places: put Use this text right under the description it copies
The button was pinned to the foot of the details column, away from the text
it adopts. It now sits inside the description card, under the source line,
with the reason it is blocked directly beneath it.
2026-10-31 03:25:56 +00:00
Maurice ce5ec7132f places: a description the community shared is credited to the TREK community
A community description laid over OSM details or an index row showed up as
OpenStreetMap under ODbL, or as the place's own website. It now has a source of
its own, community, credited as TREK Community under CC BY 4.0, like the
service publishes it.
2026-10-31 03:25:56 +00:00
Maurice 7f2a3b6039 planner: keep REFRESH_FIELDS inside its module; docker: leave shared/node_modules out of the build context
A checkout where npm install ran once carried shared/node_modules into the
image build, and its stale packages broke the client build with a vite that
lacked vite/internal.
2026-10-31 03:25:56 +00:00
Maurice 3e860dd9ca place-contrib: tag enqueue's send run as independent of its queue write 2026-10-31 03:25:56 +00:00
Maurice c28604b891 planner: the details refresh reads through a repository, and the form drops what moved out
The fresh lookup behind New place details goes through placeDetailsRepo
(online-only, like the service identity) instead of api/ directly. The form
loses the Translate type and the comment that moved with the head band's
pills, which takes it under its line limit and out of the size baseline; the
maps service suite's test-size entry follows its shorter count.
2026-10-31 03:25:56 +00:00
Maurice aba329b1b1 planner: a saved place offers newer details from its provider, field by field
Editing a saved place looks it up again. Where the lookup has a website,
phone, e-mail or opening hours that differ from the place, a New place details
pill in the form's head band shows how many, and opens what the place holds
beside what the lookup has. Empty fields are ticked, filled ones are left to
the person and say they would replace; Take over writes only the ticked
fields, with the provider as their origin, and nothing the lookup does not
know is ever emptied.

The head band's pills move to PlaceFormHeaderPills.tsx, which brings the form
from 1157 to 1002 lines.
2026-10-31 03:25:56 +00:00
Maurice 04458bee12 place-contrib: community hours in the details' own shape, and no origin rows for collection copies
Community opening hours laid over OSM details now go through the same parser
as OSM's own, so they arrive as one line per day with open-now and the periods,
and a community description lands in the details' summary. A place copied from
a collection no longer records its values as an import: without an origin row a
value is never sent anyway, and a later edit marks only the fields it changes.
2026-10-31 03:25:56 +00:00
Maurice f01ffc2c67 maps: OSM details come from TREK's own copy of OpenStreetMap first
Opening an OSM place asked the public Overpass servers for its tags, which
took five seconds and more and answered nothing while they were down. Where
the TREK services are in use, the details now come from the Places API's
details layer (/v1/osm), with the community values in the same answer, and
Overpass is asked only for an element the layer does not hold.
2026-10-31 03:25:56 +00:00
Maurice 5cfe925b78 place-contrib: no promise in the job's condition; the OSM details test counts the community call 2026-10-31 03:25:55 +00:00
Maurice a2414685ad wiki: what the API Keys card still saves 2026-10-31 03:25:55 +00:00
Maurice a1ca1aae44 admin: place search and routing get a card of their own, the keys keep theirs
The TREK API, the transit provider, the place search provider and the search
log move out of API Keys into a card of their own, which keeps them together
and saves as they change. API Keys holds only Google Maps, Unsplash and Amap,
and is not shown on a managed install, where the keys are the operator's.
Allowed File Types moves to the left column, which had room for it.
2026-10-31 03:25:55 +00:00
Maurice 56536ae803 admin: the allowance and the sharing blocks take the settings' own card head
Both blocks in the TREK API card now use SettingsCard like Single Sign-On and
two-factor: an icon tile, the title with its state as a badge, the note under
it and the switch or badges on the right, with a flush variant for a card
inside a card. A figure badge no longer cuts the last pixels of its value, and
the withdrawal toast names how many values it took back.
2026-10-31 03:25:55 +00:00
Maurice 59154b34c9 maps: OSM details carry the values installations published for the element
A place found through Photon or Overpass, a relation or a way, is no row of the
TREK index, so the community values the index lays over its own rows never
reached it. Opening its details now asks /v1/community for the element and
lays what is published over the record, with the version, where the TREK index
is in use. No answer leaves the details as they were.
2026-10-31 03:25:55 +00:00
Maurice b64f4535fd admin: set the service card's state and its one action apart from the figures
The state and its badges sit in a head band, the new-identity button in a foot
band, each with a rule and the card's own surface, so neither reads as part of
the allowance tiles between them.
2026-10-31 03:25:55 +00:00
Maurice ad2916903c place-contrib: send a queued value at once instead of minutes later
A value is due the moment it is queued, and the save starts a send run right
away; the two-minute tick stays for the retries. One run goes at a time, and a
run asked for meanwhile follows right after it. Only where the schedules run, so
tests and replicas send nothing.
2026-10-31 03:25:55 +00:00
Maurice 71cd6c2de4 place-contrib: record origins for collection copies and booking imports
A place copied from a collection and a venue a booking import creates now
record their details as an import, so a later edit compares against what was
really written. Dawarich and the background enrichment write none of the
shareable fields, so they need nothing.

The admin panel asks again for a few seconds while a fresh installation is
still fetching its key, so the prompt appears without a reload.
2026-10-31 03:25:55 +00:00
Maurice 3f2dec6c47 place-contrib: sort the request id's parts with an explicit code-unit compare 2026-10-31 03:25:55 +00:00
Maurice 668df8c62e place-contrib: seed the repository tests through the row factories, not raw SQL 2026-10-31 03:25:55 +00:00
Maurice 294ba0a4fe place-contrib: repository tests for the log, the outbox and the field origins
Every method of the three new repositories, the empty-list shortcuts and the
filters included, so the repositories keep their 99 % floor.
2026-10-31 03:25:55 +00:00
Maurice 36c1599a09 place-contrib: settle the second CI round
- The plugin RPC fallback tests build PlacesRpc with its origin bookkeeping.
- The e2e guard and contract cases send their requests one after another; the
  test server reset connections opened all at once.
- The job's two cron registrations are tagged @txIndependent: neither writes.
- The panel draws its fold head itself instead of importing it from pages/.
2026-10-31 03:25:55 +00:00
Maurice 65abea6910 place-contrib: settle the first CI round
- The pause setting's type comes from its schema, so nullable fields read the
  same under the server's compiler settings.
- The place form passes the origin of a field through to the hook.
- Serial steps run through inOrder (src/nest/common/in-order.ts) and
  independent ones through Promise.all, instead of an await inside a loop. The
  bulk update in places.mcp.ts goes the same way, so its baseline drops by one.
- The status a call without a key gets is a named constant, not an envelope
  literal.
- A picture test no longer inherits the previous test's mock answer.
- The entity counts include the three new tables.
- The log keeps its failure per query instead of resetting it in the effect,
  and usePlaceForm sets its picture through one helper.
2026-10-31 03:25:55 +00:00
Maurice 18dc121132 wiki: sharing place details with the TREK Places API
What is sent and when, what never is, how values are checked before anyone
sees them, and where an admin switches it off or takes it back. The new
TREK_PLACES_CONTRIBUTE variable is listed with the compose file and the chart.
2026-10-31 03:25:55 +00:00
Maurice 013aa3954d admin: ask once, then show and steer what is shared of place details
The first time an admin opens the settings on an installation that can share,
a dialog asks for the whole installation, with every field switched on and a
plain list of what never goes along. Nothing is sent before Agree; Not now
asks again in 14 days, No keeps the question away until the terms change, and
closing it decides nothing.

Inside the TREK Places API card, Share place details then holds the switch for
the installation and one per field, the figures of the last 30 days, the
transmission log and the way to withdraw everything sent. The place form
reports for each field whether the user typed it or a provider filled it.
2026-10-31 03:25:55 +00:00
Maurice 182ed56fa4 places: let an admin share the details users typed with the TREK Places API
Every shareable field of a place now records where its value came from: typed
by a user, filled by a provider, written by the assistant, a plugin or an
import. Only values a user typed are ever sent, and only after an admin agreed
to the current terms for the whole installation, field by field.

The new place-contrib domain keeps the decision in app settings, queues values
in an outbox a cron job sends from, logs what the service made of each one and
can withdraw everything this installation sent. Development, test and demo
instances, an own TREK_PLACES_URL, TREK_SERVICES=off and
TREK_PLACES_CONTRIBUTE=off never send anything. Index results carry the
community version, so the details caches refresh when it changes.
2026-10-31 03:25:55 +00:00
Vini f56f6ae11e perf(atlas): keep the admin0 country index as Int16 arrays and send countries/geo from disk 2026-10-31 03:25:13 +00:00
Vini c94ff7421a keep looked-up regions as packed Int32 arrays under a 16 MiB budget 2026-10-31 03:25:12 +00:00
Maurice 2dbcc39832 chore(lint): lower the no-explicit-any baseline after the typed region lookup 2026-10-31 03:25:09 +00:00
Maurice 47dad114df test(atlas): cover the per-country region route in the e2e suite 2026-10-31 00:40:23 +00:00
Vini 7277e35a96 serve each country's regions from its own file, so the server no longer holds them all
The Atlas server no longer keeps the admin-1 regions of every country in memory. Each country's regions are now a pre-gzipped file of their own, sent to the map as they are.

**Why:** `GET /addons/atlas/regions/geo?countries=…` built `admin1Store` (every country's region text, kept until restart), then joined the requested countries, `JSON.parse`d them, and Nest stringified them again. On the 512 MB – 1 GB boxes TREK runs on, the first z≥5 view could OOM the server (same shape as #1576).

**What changes:**
- `server/assets/atlas/admin1/<ISO_A2>.geojson.gz`: 198 files, 14.85 MB, the same 3,447 features as `admin1.geojson.gz`.
- New `GET /addons/atlas/regions/geo/:code` with a Zod contract (`regionGeoParamsSchema` in `@trek/shared`: two letters, any case). It sends the country's file as it is, with `Content-Encoding: gzip` and the current 1-day `Cache-Control`, as `countries/geo` does. A country without regions gets an empty FeatureCollection. Anything that is not two letters gets a 400.
- `useAtlas.ts` fetches one URL per uncached country and still rebuilds the layer once. Each country has a stable URL, so a country evicted by `REGION_CACHE_MAX` comes back from the browser cache.
- `getRegionFeatures` reads only the country it needs. `admin1Store` is removed.
- `?countries=` keeps working for cached PWA clients. It joins the per-country files as bytes, without a parse.
- The migrations are untouched. `admin1.geojson.gz` still ships, only for `Migration20200101021500` and `Migration20200101031500`. This costs ~15 MB of disk in the image and no RAM. `build-atlas-geo.mjs` writes both, and ATLAS-BUNDLE-008 keeps them equal.

**Memory** (real `atlas-geo` module, first z≥5 view over Europe = 49 country codes, JS heap + Buffers):

| | peak | kept after |
|---|---|---|
| before, first view (builds the store) | +372 MB | +61 MB |
| before, later views | +115 MB | 0 |
| after, `/regions/geo/:code` | +4.7 MB | 0 |
| after, legacy `?countries=` | +59 MB | 0 |

**Tests:**
- New: `atlas-region-files.test.ts` (moved from `atlas.service.test.ts`, plus join and dedupe cases), the `regionGeoParamsSchema` spec, ATLAS-BUNDLE-007 (the file format the byte join depends on), ATLAS-BUNDLE-008 (bundle and per-country files are equal), ATLAS-016/017/018 (the new route over HTTP, including the 400).
- The `getVisitedRegions` tests (ATLAS-UNIT-019..024) used to pass only when `getRegionGeo`'s `beforeAll` had built the store first. They now also pass on their own.
- The shared, server and client gates pass. Baselines go down only where this change removed code (`no-console`, `no-explicit-any`, `no-swallowed-catch`, file sizes).

**Trade-offs:**
- A first zoom over Europe sends ~49 small parallel requests instead of 1. Over HTTP/1.1 the browser queues them 6 at a time. The total bytes are the same (4.4 MB gz).
- The per-country URLs are unversioned and cached for a day, like `countries/geo`. After an upgrade that changes a country's regions, a browser can show the old shapes for up to 24 h.

Approved on Discord in #github-pr: <paste the thread link here>
Related: #1576, #2559

- [x] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update

- [ ] I have read the [Contributing Guidelines](https://github.com/liketrek/TREK/wiki/Contributing)
- [ ] My branch is [up to date with `dev`](https://github.com/liketrek/TREK/wiki/Development-environment#3-keep-your-fork-up-to-date)
- [x] This PR targets the `dev` branch, not `main` *(wiki-only PRs are exempt)*
- [x] I have tested my changes locally
- [x] I have added/updated tests that prove my fix is effective or that my feature works
- [x] I have updated documentation if needed
2026-10-31 00:40:23 +00:00
Maurice 70f27085b9 feat(i18n): bring the Hebrew locale up to date with dev and list it in the docs
Translate the 55 keys dev added since the branch was cut (TREK services
allowance, Valhalla routing service, PDF routing progress, overview
distances, Unsplash key hint, tour day limit) and drop the two reservation
toasts dev removed, so he matches en key for key again. Refresh the two
admin strings and the Valhalla hint whose English changed.

Check he in the placeholder spec and the RTL test, and count 28 languages
with Hebrew in the README and the Languages, Environment Variables,
Contributing and Help Center wiki pages.
2026-10-31 00:40:23 +00:00
Dolev Benezri 156dc4a6c2 feat(i18n): complete Hebrew locale for dev
Fill in every domain added since the first Hebrew pass (help, tours,
assistant*, studio*, wanderer*, journey, demo, ferratas and the rest),
retranslate strings whose English changed, and give every count group
its Hebrew one/two forms. Register he as a non-Latin locale for the
untranslated ratchet, add the Hebrew 429 message, and count 28 locales
in the tours locale spec.
2026-10-31 00:40:23 +00:00
Dolev Benezri d159aec78b feat(i18n): add Hebrew (he) locale with RTL support
Full Hebrew translation of every UI domain plus email/notification
templates. Registered in SUPPORTED_LANGUAGES, marked right-to-left,
wired into the client locale loader and the rate-limit message table.
2026-10-31 00:40:23 +00:00
bear 547f0b84b0 fix(mcp): file a transit fare under the transport cost category
transit is not one of the fixed Costs categories, so typeToCostCategory
would put an uncategorised fare under other. Default to transport, the
category every rail and bus booking maps to.
2026-10-31 00:40:23 +00:00
bear 8d10bff616 feat(mcp): price on create_transit_journey, reservation_id on create_budget_item
- create_transit_journey takes price / budget_category, as create_transport
  does: a fare > 0 files the linked expense in the booking's own transaction
  and rides along in metadata.price. The tool now writes through
  ReservationWritesService.createBooking, so its events and booking notice are
  the ones every other booking write sends
- create_budget_item and create_budget_item_with_members take reservation_id,
  the field POST /budget already accepts; create_budget_item now runs
  BudgetWritesService.createItem like its sibling, so a linked booking's
  mirrored price is worked out again before budget:created goes out
- bookingPriceSchema / bookingBudgetCategorySchema in @trek/shared back the
  price fields of all three booking tools (mcp-inline-zod baseline lowered)
- wiki: MCP-Tools-and-Resources describes the new params
2026-10-31 00:40:23 +00:00
Maurice e5048643f8 test(assistant): pin the clock in FE-AI-PICK-004
Trip 7 ran until 2026-10-30, so from 2026-10-31 on the hook ranked it as
past and the test failed. Pin the date to the file's TODAY.
2026-10-31 00:39:38 +00:00
Maurice a156f5be32 fix(chart): keep a blank line before the HTTPRoute section in NOTES.txt
The trimming if swallowed the blank line, so the HTTPRoute note ran
straight on from the Persistence section. Move the blank line inside
the conditional so it only appears with the section.
2026-10-30 23:32:43 +00:00
Demisu 431122d253 refactor(README): remove HTTPRoute configuration section for clarity 2026-10-30 23:32:43 +00:00
Demisu bbc8f141e3 feat(helm): add optional Gateway API HTTPRoute support
Add disabled-by-default HTTPRoute support with configurable parent references,
hostnames, paths, annotations and optional timeouts. Route to the existing TREK
Service and keep Ingress support unchanged, including simultaneous use.

Document configuration and extend the existing Helm Chart & Compose job with
HTTPRoute render variants and strict validation using a pinned CRDs catalog.

Approved on Discord:
https://discord.com/channels/1488298068427411591/1553537637455761500

Validation: strict Helm lint, 37 manifests validated without errors or skipped
resources, and both Compose configuration checks passed locally.
Live Gateway WebSocket and streamed assistant response checks were not performed
because no Kubernetes test cluster was configured.
2026-10-30 23:32:43 +00:00
Maurice 01681f57c7 docs(chart): document extraVolumes and extraVolumeMounts in the Helm install guide
The install guide lists extraEnv and extraEnvFrom but had no word on the
new volume values. Add a Further volumes section with the KItinerary
extractor example and point the Reservations-and-Bookings page at it.
2026-10-30 23:30:54 +00:00
aaronspruit 553808eb83 feat(chart): add extraVolumes and extraVolumeMounts
The chart mounts only the data and uploads volumes, and no value adds
another one. Add two lists that go into the Deployment as written:
extraVolumes for the pod, and extraVolumeMounts for the trek container,
next to the data and uploads mounts. Both lists are empty by default,
so the default render does not change.

The values file shows one use: a ConfigMap of KItinerary extractor
scripts, mounted at /usr/local/share/kitinerary/extractors. The chart
README and the Reservations-and-Bookings wiki page tell Helm and Compose
users how to mount extractors there.

The variants render in the Helm Chart & Compose job sets both values,
so kubeconform checks them.
2026-10-30 23:30:54 +00:00
Maurice 324ee69151 fix(transit): mark the plan fallback's quota counts as independent writes
The empty-result fallback can call Google after Transitous, so lint:tx now sees two googleQuota.record() calls in TransitService.plan. Only one of them runs per request, and each is a call counter written after its own upstream request, so it cannot share a transaction with it.
2026-10-30 23:36:35 +01:00
Maurice c4c63ffd62 docs(transit): describe the empty-result fallback between transit providers
Admin settings, transport, environment and MCP pages still said Transitous
is the only fallback and that Google is billed only when picked. Document
that an empty plan asks the other backend, that the Google fallback uses
only an instance key within the daily limit, and mention it in the
search_transit_routes tool description.
2026-10-30 23:01:17 +01:00
Raz Erlich 209f8fe5cc feat(transit): fall back between configured public transit providers 2026-10-28 08:01:53 +02:00
Maurice 9f155a3191 fix(airports): refresh the bundled airports from OurAirports without dropping shipped codes
Logroño-Agoncillo (RJL) and fifteen other airports gained their IATA code in
OurAirports after the bundled list was built, so the airport search and the
code lookup of create_transport could not find them. The list is rebuilt from
the current data. The build now keeps every code the previous list carried
that upstream no longer lists (a scheduled-service flag flipped back, PBI moved
to a new record), since bookings and MCP calls still name them.

Refs #2897
2026-10-30 22:06:07 +01:00
Maurice 067ab89430 fix(calendar): give transport events a LOCATION and GEO from their endpoints
A car or other transport booking carried its route only in the description, so
a calendar app had no place to navigate to. Without a location of its own, a
transport event now takes the endpoint the traveller has to get to: the
destination of a drive of their own (car, bicycle) and the departure of a ride
they board, the side the per-leg events already use. A car's pickup and
drop-off events are located at their own endpoints. A location the booking
states keeps winning.

Closes #2909
2026-10-30 21:54:19 +01:00
Maurice 019c668744 fix(llm-parse): ask Anthropic models for the extraction tool instead of forcing it
Claude Sonnet 5.5, Opus 5.5 and Fable 5.1 reject tool_choice type "tool" and
"any" with a 400, so every booking import through the Anthropic provider failed
on them. The request now sends tool_choice auto with parallel tool use off,
which every Anthropic model and Messages-compatible gateway accepts, and the
system prompt tells the model to answer through the emit tool exactly once. A
model that still answers in text is reported as before.

Closes #2902
2026-10-30 21:54:19 +01:00
Maurice b953ece4b3 fix(journey): stop the entry views from shifting while photos and providers load
Opening an entry on the share page showed the hero at no height until the
original arrived, then pushed the story down, and the lightbox stayed black
until the full photo had loaded. The hero now reserves the photo's proportions
(4:3 when none were recorded) with its thumbnail behind it, and the lightbox
shows the thumbnail and a spinner until the original is in.

The phone entry sheet probed the photo providers again on every open and added
the External photos button once the answer came, squeezing the icons of the
other two buttons. The journey screen already knows the connected providers, so
it hands them to the sheet, and the icons keep their size.

The picked neutral mood was a grey tint on a grey sheet; a picked mood is now
framed in its full colour and announced as pressed.

Closes #2907
Closes #2908
Closes #2905
2026-10-30 21:15:38 +01:00
Maurice 94a9cbf070 fix(journey): draw the live Dawarich trail in the colour of its day
The live trail arrived in Dawarich's own day palette, counted from the first
recorded day, while a journey colours its days by the entries. A day's line and
its pins therefore wore different colours. The trail of a day that has entries
now takes that day's colour, the way the kept recordings already did.

Closes #2906
2026-10-30 21:15:37 +01:00
Maurice 8eebcea9f5 fix(journey): keep GL photo markers absolutely positioned so thumbnails sit on their coordinates
The journey map's photo thumbnails set position:relative inline on the marker
element. That overrides the engine's .mapboxgl-marker/.maplibregl-marker
position:absolute, so every thumbnail entered the normal flow below the ones
drawn before it and the engine's translate was added on top: the further down
the list, the further the photo drifted from its spot. The location dot had the
same inline position. Both now leave position to the engine's class.

Closes #2910
2026-10-30 21:15:37 +01:00
Maurice 5e00282309 maps: drop indexHitsOnly, which the search no longer needs, and keep isHit inside its module 2026-10-30 17:26:56 +01:00
Maurice 3b675eed60 airports: publish the airport of a code as a contract the place search reads
The search turned an airport code into its airport by reaching into the
airports domain's data module. airports.contract.ts publishes that lookup
instead, as the import boundaries ask.
2026-10-30 17:14:55 +01:00
Maurice 51bbd337c9 i18n: format the admin strings of the TREK API card 2026-10-30 16:57:52 +01:00
Maurice 4203dcd91f maps: keep the merge and Photon's geocoding in modules of their own
maps.helpers.ts and osm.client.ts had grown past the 1000-line limit. The
merge behind the search and suggestion lists, with its lead rules, moves to
maps-merge.helpers.ts; the search records and Photon's half of the
OpenStreetMap geocoding (search, suggestions, airport codes, identity,
reverse, element details) move to osm-geocoding.ts. OsmClient keeps the
same methods and hands them on, so no caller changes.
2026-10-30 16:52:12 +01:00
Maurice 1e5314d847 maps, routing, i18n: settle what the checks found
- The lead is Photon's own row first; a row of its name inside the lead
  only stands in where Photon's row went as the same place. Rows without a
  line below the name are never folded as reading the same.
- A miss of Photon where it is the geocoder no longer asks it a second time
  on the way to an empty answer.
- The transport words of the identity match use a Unicode word boundary
  (straße, estación) and match 駅 anywhere.
- Opening an OSM place reads the coordinate it was placed at explicitly.
- The ferry spans of a Valhalla leg move to valhallaFerries.ts, and a day
  cut into pieces is asked with one options object.
- "Limit" on the allowance tiles is "Daily limit", which no locale left
  as an English copy.
- Tests follow the new behaviour: no Nominatim behind Photon, zoom 13,
  and the expectations of the merge and edit-distance cases.
2026-10-30 16:36:22 +01:00
Maurice b98e530ffc wiki: describe the address search, the merged result list and the routing of the TREK services 2026-10-30 16:16:10 +01:00
Maurice deb125bedc admin: show the TREK API as its three services
The card names place search, address search and routing with what each
answers and what is left of today's allowance. Each service is a tile with
what is left as its figure, the day's use as a bar and used and limit as
badges; the state, the kind of installation, a key from the environment
and the time the count starts again sit as badges in the header. The
sources add the terrain tiles the routes take their heights from.
2026-10-30 16:16:09 +01:00
Maurice c508bb9758 routing: a slow route is not a down relay, and the relay takes whole days
A request to the TREK Valhalla that runs out its deadline (a long bike
route) is answered 504 and no longer rests the relay for every user or a
tab for ten minutes; that one day goes to OSRM. Only an unreachable host
or a server error rests it.

A day goes to the relay with up to twenty stops per request and no pause
between pieces; a request that ends up at the public FOSSGIS Valhalla is
cut to ten and paced, as before. The 1.1 s spacing of the server applies
to the public hosts only, not to an admin's own OSRM or Valhalla. The
relay keeps its connections open and asks for maneuvers only, which the
relay contract now accepts.
2026-10-30 16:16:09 +01:00
Maurice a55d4e786f maps: lead with the place a name stands for, and leave Nominatim to installs without the services
The suggestions and the full search now share one merge. Photon's hits are
ranked with places that have a Wikidata link before their unlinked
namesakes, and its context bias is narrower (zoom 13). The list opens on a
lead where Photon's answer has one: a country or region always; a town,
island or sight when it is called what was typed (or a near typo of it),
lies within 1000 km of where the traveller is looking and no place of that
name ranks above it. Rows of the lead's name inside it, the same element
under two ids and rows that read the same are shown once. A three-letter
code in capitals is asked for as the airport of the bundled list.

Where Photon is the geocoder it answers alone: an empty answer and a miss
are the answer for search and imports, the reverse lookup asks again
within 50 km, a Google place is matched inside a box through Photon, and
an opened OpenStreetMap place is placed by Overpass and named by Photon.
Nominatim stays the search of installations without the TREK services or
with an operator's own Nominatim.

Measured on 5000 places from Wikidata: the right place first while typing
48 % before, 65 % now; full search 53 % to 68 %; no repeated rows.
2026-10-30 16:15:59 +01:00
Maurice 5a270abc05 fix(booking-import): give the fare keeper reduce an initial value 2026-10-29 21:40:03 +01:00
Maurice f50a1dabe5 chore(ci): lower the knip baseline for the kitinerary types 2026-10-29 21:25:13 +01:00
Maurice 1058849388 test(booking-import): read the endpoints without a non-null assertion and lower the any baseline 2026-10-29 21:12:58 +01:00
Maurice 299beb65b1 fix(roadtrip): hand a hire car without a return day back on its pick-up day
A car booking without an end day is a one-day booking everywhere else in
the plan, but the road trip read it as a car never handed back: only its
pick-up desk stood on the road. Two one-way car bookings on one day
(Bologna to Villach, then Villach to Klagenfurt) therefore drove from the
first pick-up to the second and on to whatever came before, counting the
road between the two cities twice and never reaching Klagenfurt. This
applies to the planner, the phone and calculate_roadtrip alike, since all
three seat the desks through the shared carrierSeam.

Such a car now has its return desk on the pick-up day. A return without a
time goes back before the next car of that day is collected rather than
closing the day behind it, so each one-way car drives its own road once.
Bookings with a return day and cars without a located return desk are
unchanged.

Closes #2894
2026-10-29 20:51:28 +01:00
Maurice 23b7f6d71c fix(bookings): count a multi-segment rail ticket's fare once
A rail, bus or ferry ticket with changes arrived as one booking per
segment, each with a linked cost for the full fare, so the trip total
was the fare times the number of segments.

Import: the KItinerary mapper now groups consecutive train segments
under one booking reference that connect (same station by name or
within 500 m, no return, within a day) into one train booking with
stops and metadata.legs, the way connecting flights already were. The
connection rule is shared between flights and ground transport, and a
stop carries the onward departure for flights as for trains, as the
planner form and the MCP tools write it. The three copied ground
mappers are folded into one.

Within one such booking a fare repeated on every segment is booked
once, and fares that differ per segment are added in whole cents. That
summing now applies to connecting flights too, which used to keep the
first leg's price only.

Segments of one order that do not join into one booking (an outward
and a return trip, a gap, a change of station named two ways without
coordinates, bus and boat tickets, Rail&Fly) are compared across the
document: when every priced transport booking of a reference carries
the same amount in the same currency, the first to depart keeps it and
the others lose it. Amounts that differ stay. This runs on what
KItinerary extracted only.

AI import: the repeated-total safety net from #2651 now covers train,
bus and ferry tickets and any mix of them with flights, still only
when the document prints that amount on a total line.

MCP and assistant: create_transport says that a ticket with several
segments is one booking with endpoints[] and legs[], and the price
fields say they hold the whole booking's cost. When a new booking
repeats the price an earlier booking with the same confirmation number
already carries in the trip's currency (its own currency, or the
trip's when it names none), create_transport and create_reservation
still save it and return a warning naming the earlier booking. REST is
unchanged.

Existing duplicated costs are left alone: they cannot be told apart
from two tickets that really cost the same, and deleting expenses
automatically would change settled balances. They can be removed in
the Costs tab.

Closes #2895
2026-10-29 20:51:28 +01:00
Maurice f85b9dc3ff fix(service-identity): call the dialect self-check's scrypt input inputSha256
A field named passwordSha256 next to a hex literal reads as a hard-coded
password to static analysis.
2026-10-29 20:04:40 +01:00
Maurice 3c38a87271 chore(knip): treat the registration dialect as an entry, the release job loads it by path 2026-10-29 20:04:40 +01:00
Maurice 12e22c258a ci(release): stop on a stale dialect, check it like CI would, give prereleases their own
A stable release without a dialect of its own now ships the one on main only while that one has 90 days left; dialect 0 or less stops the run before anything is committed, unless allow_stale_dialect is ticked. Fetching and checking moved into registration-dialect.yml, which never fails its caller and also type-checks the module under the strict config and lints it with --max-warnings 0, since the bump commit skips CI. The bump and everything after it run whatever became of the dialect job. Prereleases ask the issuer for a dialect of their -pre.N version and build it into the image without committing it. The release check reads the module token by token against an allowlist of keywords, globals and member names, refuses any assignment to a property, computed access and regular expressions, and names Object, JSON and defineProperty among the forbidden. The issuer's kitchen-sink sample is gone; the tests use a hand-written dialect instead.
2026-10-29 20:04:40 +01:00
Maurice cd5ce4b1d9 docs(wiki): say how long a key renews and what a new registration needs 2026-10-29 20:04:40 +01:00
Maurice 6d245ab4da ci(release): ship the registration dialect with the version bump
The stable release asks the issuer for the registration dialect of the
version it cuts and commits it in the bump commit. Fetching and checking run
in a job of their own that holds no write token and keeps no git
credentials: scripts/ci/check-registration-dialect.mjs refuses a file that
imports or names anything beyond hashing, then runs it on its self-check
inputs. Only a file that passes reaches the bump as an artifact. When the
issuer is unreachable or its answer fails the check, the release goes ahead
with the dialect already on main and says so in the log and the summary.

Prereleases get no dialect of their own; dev takes the stable one through
the merge of main into dev after a release, and docker-dev.yml warns while
dev still speaks the original registration or a dialect older than 150 days.
2026-10-29 20:04:40 +01:00
Maurice 3ca73d432b feat(service-identity): register through the dialect of the running release
Registration now goes through a registration dialect module: the paths of
the challenge and the registration, the bytes scrypt runs over and the body
and headers of the request. Each stable release will carry the dialect the
TREK services issuer writes for it; until the first such release the module
speaks the original registration, so nothing changes on the wire.

An issuer that no longer takes the release's dialect answers 410. TREK then
says so once per process, waits a day (or what Retry-After asks) and keeps
searching through the shared pool. The issuer client only lets a dialect
register at the issuer's own paths, with X-TREK-* headers that are not the
gate's and a body of registration size.

Renewal needs no dialect, and an expired key may now be renewed for a year
instead of 30 days, the same period the issuer allows, so an installation
that was offline for long comes back without an update. The key format
ignores the new optional dialect claim; the shared vectors gain one key that
carries it.
2026-10-29 20:04:40 +01:00
Maurice e6c8fee3e5 fix(journey): keep the card-click ref in JourneyMap and type the pin event test
The marker effect reads the card handler through the component's own ref
again, so exhaustive-deps sees a ref it knows, and the card hook takes it
alongside the map's other refs. The pin event assertion types the mock
calls it maps under strict mode.
2026-10-29 17:50:43 +01:00
Maurice 2dbaf6b029 fix(journey): hold the client gates on the journey map changes
The phone card logic moves out of JourneyMap into useEntryCard, which brings
the component back under the line limit. The new card tests reach the
card element through a guard instead of non-null assertions, the public
hook test reads the hook through renderHook, the GL card tests use
mock.lastCall where Array#at is outside the client's lib, and the
complexity baseline drops to the count the branch now holds.
2026-10-29 17:39:37 +01:00
Maurice c14b572038 refactor(server): read the share link's journey entries through Kysely
listPublicEntries changed for #2886, and the repository rules move a
touched QueryBuilder call to the entity methods or Kysely. Kysely keeps
the method's answer what it was, the stored row as SELECT * reads it,
which the entity methods would hand back as entities with a journey
reference in place of journey_id. The query-api baseline drops to 8.
2026-10-29 17:26:51 +01:00
Maurice c0e951fc8e chore(journey): keep the map stop and connection types to their files
ConnectionStop, EntryConnections and JourneyMapStop are only used in the
signatures of their own modules, and knip counts an export nothing
imports. The connection tests take the FE- prefix and header the other
journey tests carry.
2026-10-29 17:26:51 +01:00
Maurice c71ecc8f76 fix(journey): drive the phone's Leaflet card from the active pin
The card a phone host opens its entry from was still a tooltip bound to
the pin, so Leaflet opened and closed it on the pin's hover and focus. A
tap on the card reaches a phone browser as the pointer leaving the pin
first: the pin's mouseout closed the card between the tap and its click,
and on iOS a content change in that phase can drop the click entirely.

Where a host wires onMarkerCardClick, the pins now bind no tooltip. One
card per map stands over the active pin, the way the GL popup does: it
follows highlightMarker, leaves when no entry is active or the entry
pins are switched off, and only a change of entry moves it. A press on
it never reaches the map. Desktop hosts keep the hover label on each pin.

The card tests run against the handlers the pins really get and the
card the map really adds, and the file parses again: two test names held
an unescaped apostrophe.
2026-10-29 17:26:51 +01:00
Maurice a1c7ea209b fix(journey): open the entry when its marker card is tapped on the phone
On the phone journey screen and the public share page on a phone, a tap on
a numbered pin brings up the entry's card above it, but the card could not
be tapped: the GL popup and the Leaflet tooltip both let the pointer fall
through to the map, and no phone host gave the card anything to do.

The map now takes onMarkerCardClick, and both renderers make the card open
that entry once a host wires it. The GL popup handles it in its delegated
listener next to the photo strip, which keeps opening the photos where the
desktop asks for that. Leaflet handles it on the tooltip element itself:
a press that reached the map container let Leaflet focus the map, which
blurred the pin and closed the card before its click landed. The phone
screen and the timeline open the entry through the carousel hook, the
same way a tap on the active card does. The desktop asides keep the card
as a hover label.

The unused hideMarkerTooltip prop goes, together with the docs that still
said the phone hides the card.

Closes #2885
2026-10-29 17:26:51 +01:00
Maurice fe24dd03c5 fix(journey): number map stops the same way in the journal and on the share link
The share link has counted its map stops through the whole journey since
#1962, while the owner's journal still restarted the count every day, so
the same stop read 66 on the link and 6 in the journal.

Both views now take their stops from one helper, journeyMapStops: located,
non-skeleton entries in the server's reading order (date, sort_order, id),
numbered across the journey. A draft keeps its pin in the journal but gets
no number, so the published stops after it carry exactly the numbers the
share link shows. The public entry query gains the id tiebreak the owner
query already had, so entries sharing a sort_order come back in the same
order on both sides.

Closes #2886
2026-10-29 17:26:51 +01:00
Maurice 66bff5c342 fix(journey): drop the entry connection where a recorded day shows the way
The journey map joins every located entry with a faint dashed line in the
order of its pins. It drew that line through everything, also across days
whose real route is on the map as a kept recording or the live Dawarich
trail, so straight guesses fanned out over the recorded track. The phone
has no layer switches, so the line could not even be hidden there.

A recorded track now carries the day it belongs to (an optional `date` on
the track contract, set by every builder of recorded lines: kept days, the
shared page's recordings, the live Dawarich trail and the day import
preview). A pure model splits the connection into the stretches still open
and those whose days were all recorded, the night between two recorded days
included. Both renderers draw only the open stretches with the entries; the
covered ones sit on a layer of their own that shows while the reader hides
what was recorded, so the guess comes back exactly where nothing else
explains the way.

The live trail is now matched to a kept day by that date instead of reading
the track name as one.

Closes #2888
2026-10-29 17:26:50 +01:00
Maurice 55b11f792a fix(journey): keep map photo and mark clusters stable while panning
The journey map grouped its photo thumbnails and its plan and stay marks
on a grid laid over container pixels. Every pan shifted that grid against
the map, so photos near a cell edge changed groups after each move: the
count badges jumped and the thumbnail switched to another photo, although
the zoom never changed.

Both renderers now bucket in world pixels at the current zoom, Leaflet
through its own CRS (so GCJ-02 basemaps stay right) and GL through a
plain Web Mercator projection that ignores centre, bearing and pitch.
Panning keeps every group as it is; only a zoom regroups.

Closes #2889
2026-10-29 17:26:50 +01:00
Maurice e8fbba236d refactor(bookings): hand the notes and link to the shared lower part as form fields
The booking and transport dialogs each spelled out the notes and the link
as four value and change props, and with the saving flag the two calls
became one copied block. The shared lower part now takes the form and its
setter, as the dialogs hold them, and writes the two fields itself.
2026-10-29 15:50:09 +01:00
Maurice 97455da6ca test: type the follow-up steps and the Atlas visit filter as they are used
The editor follow-up always hands the planner an afterSave, and its type now
says so, so the hook test calls it under strictNullChecks. The travel stats
test reads a country without a status as visited, as the standing helper
does for the hand-marked list. The client baseline holds the one non-null
assertion the count missed.
2026-10-29 15:36:37 +01:00
Maurice 2a51a1a842 test: hold the branch's tests to the type and lint gates
The booking save helper answers a Reservation the way the dialogs' onSave
does, and takes the payload the sheets read back. The export tests resolve
their routing promise without non-null assertions, the autocomplete test
reads its own console spy, and the Photon tally test asks its five dropped
questions together instead of awaiting in a loop. The Photon address line
falls back to empty when a feature has none.

The repair test now expects the dashboard to place the cleared beach in
Germany through the borders, as the Atlas map does since #2875. The client
lint baseline follows the counts this branch lowered.
2026-10-29 15:24:49 +01:00
Maurice 724857fc73 docs(atlas): say what the country resolver costs the dashboard, and why
The resolver's comment claimed the places of a city trip share their
coordinates, which they do not. It now says what an uncached place costs
per count and why the dashboard does not queue the Atlas region lookup:
that lookup falls back to Nominatim, and a dashboard load must not send a
user's places to a third party.

The #2875 cases continue the file's AUTH-DB numbering.
2026-10-29 15:11:39 +01:00
Maurice 28a577df5e fix(maps): keep a slow Photon from holding back the suggestions
The suggestions waited for Photon beside the index for up to its full
2.5 s deadline on every keystroke, and their misses and spent allowance
paused Photon for the search and the reverse lookup as well.

Once the index has answered, Photon now gets 300 ms more and is then
called off; with nothing from the index it is still waited for. The
keystroke question has a shorter deadline and its own tally of misses and
pauses, a question called off by the list counts as no miss, and a pause
of the search still holds the suggestions back.

Refs #2877
2026-10-29 15:11:39 +01:00
Maurice 64f370fd0e fix(pdf): give up a routing export once its dialog or sheet is gone
A PDF of a long trip routes for minutes, and only the skip button could
stop it. Closing the export dialog, hiding the phone sheet or leaving the
trip left the legs going out and opened the document later on whatever
page the user was on. Closing now stops the routing and the document never
opens; skipping still prints without the remaining legs.

The plan filter moves from TripPDF to pdfScope beside the other plan
predicate, which keeps TripPDF under its size entry. The routing test name
gets its quotes back, and the export dialog and sheet tests cover the leg
count, the skip button and closing while routing.
2026-10-29 15:11:32 +01:00
Maurice 00ac889139 fix(bookings): add the next imported booking instead of writing over the last one
The follow-up kept the booking a save stored until the editor session
changed, and on the phone the session was only the open flag. The import
review swaps the draft without closing the sheet, so the second booking of
the same kind went out as an update of the first and lost its linked cost.

The stored booking now lives only as long as a failed try needs it, and
the phone sheets name their session the way the desktop dialogs do, by the
booking or draft they hold. The sheets also seed their form from that
session, so the next draft of the review shows its own fields.

The follow-up hook gets its own test, the notes block test passes the
saving flag the component now takes, and the swallowed-catch baseline
follows the one catch the save merge removed.
2026-10-29 15:11:25 +01:00
Maurice 9ab2fc30ff fix(atlas): count a place's country on the dashboard the way the Atlas map does
The passport card and the last/next trip countries read place countries
from the place_regions cache alone, while the Atlas map falls back to the
bundled borders for a place without a cached region. A place in Vatican
City never gets a region row (the region bundle has no VA features and
Nominatim names no region there), so the map counted VA and the card did
not. The same gap hid any country whose place was still waiting for its
background lookup, or whose lookup had failed.

Both views now resolve each place through one helper: the cached country
first, the borders otherwise, once per location. The dashboard reads every
place of a started trip with its cached country instead of the distinct
cached codes, and the trip countries count resolved places the same way.
The MCP travel stats tool and the public stats API use the same service
methods and are fixed with it.

Closes #2875
2026-10-29 14:52:47 +01:00
Maurice 3238fbc517 fix(pdf): route every leg of the trip map and mark what is left unrouted
The PDF export routed the whole trip under an 8 s deadline while the router
is asked one leg at a time, a second apart. A cold month-long trip got about
seven legs routed per export; the rest printed as straight lines at "0 m"
under a total that looked final, and every further export routed a few more
days from the device cache.

The export now waits for every leg, bounded by the request pacing, the
rate-limit retries and a per-leg timeout. The export dialog and the phone
sheet count the legs while they route and offer to print without the rest.
Any leg left without a road is marked in the document: the day reads "not
routed" or "at least ...", the total says it is a floor, and a note names
how many legs are missing. The planner overview shares the same wording.

The PDF also routes what the planner map routes now: the road trip's vias,
the multi-day tour stages and the reader's distance unit.

Closes #2876
2026-10-29 14:48:13 +01:00
Maurice c03c40d287 fix(mobile): line the day rail up on today once the trip splash lifts
The opening day is seeded while the loading splash still covers the shell,
so the rail's auto-scroll ran before any chip existed and never ran again:
its dependencies did not change when the rail first mounted. A trip opened
mid-way showed today's day below a rail stuck at day 1.

The rail element is now a dependency of the alignment, so the active chip
is brought into view when the rail appears, including after re-entering
the trip. The first alignment jumps instead of gliding across a rail
nobody has seen yet, and runs before paint; later day changes still glide.

Closes #2880
2026-10-29 14:28:22 +01:00
Maurice b06e3e6266 fix(bookings): keep the editor open until a booking's files are uploaded
Saving a booking or a transport closed the editor and reported success
before the travellers and picked files were written. A failed upload left
a booking without some of its PDFs, no list to retry from, and a create
form that would add a duplicate when saved again. On desktop a file picked
on an existing booking went up at once and vanished when that failed.

The planner's save now takes the editor's follow-up as a step it waits
for: it stores the booking, runs the traveller write and the uploads, and
only then says so and closes. Each file that lands leaves the list, so a
failure leaves exactly the files still to send, and the editor keeps the
stored booking so the retry updates it instead of creating another one,
without a second linked cost or stay. All four editors (desktop booking
and transport dialogs, both phone sheets) share this through
useBookingFollowUp, an import review only moves on once an item's files
are up, and desktop files picked on a saved booking wait for Update like
on the phone.

Closes #2883
2026-10-29 14:20:57 +01:00
Maurice 7d669cb5c3 feat(maps): ask Photon beside the index for search-as-you-type suggestions
The suggestion list asked only the TREK index and its OpenStreetMap layer,
because Nominatim's usage policy rules out autocomplete. The layer is a slice
of OpenStreetMap, so many places showed up only once the full search was sent
(#2877). Photon is built for this keystroke: the suggestions now ask it in
parallel with the index and interleave its rows, dropping any the list
already holds, by OSM id or by the search list's same-place rule.

Without Photon (services off, an operator's own Nominatim, a paused or failed
answer) the list reads exactly as before. A failing index no longer throws
away what Photon found, and the Google-only switch skips Photon as it skips
the index.

Refs #2877
2026-10-29 13:50:37 +01:00
Maurice 81b3657528 fix(mcp): name the tuple check for what it holds, describe a restored map point as [lat, lng]
MCP-SCHEMA-005 finds tuple keywords, it does not validate against the 2020-12 metaschema, so the name now says that. A restored map point lost its range and order to the tuple fix; the description keeps the order visible to a model that builds a page itself.
2026-10-29 13:49:41 +01:00
Maurice 6f4d7d9066 fix(mcp): keep restore_book_page free of tuples so ChatGPT accepts the tool list
With the Journey addon on, ChatGPT refused every TREK tool: restore_book_page
took a map's path and roads as [lat, lng] tuples, which the SDK writes as the
draft-07 array form of items, and JSON Schema 2020-12 refuses that form.

The restore now takes a map point as two numbers and parses each page as a
spread before it is put back, so lat and lng stay checked and a point out of
range is an invalid_op instead of a thrown parse. MCP-SCHEMA-005 holds every
tool's input schema free of tuple keywords.

Closes #2879
2026-10-29 13:45:53 +01:00
Maurice 00684f4202 chore(client): lower the complexity baseline to the 219 the source holds 2026-10-29 13:34:28 +01:00
Maurice c4a39171f2 fix(client): narrow the route profile and Valhalla answer types, keep the stop check reading the whole log
The built-in profile check returned the open profile key and the run request
returned an unnarrowed answer, both of which the client typecheck refused. The
new road trip tests asserted with non-null assertions where optional calls do.

The graceful stop step piped docker logs into grep -q under pipefail, so grep
leaving early after its match could fail the step with SIGPIPE although the
server had logged SIGTERM.
2026-10-29 13:34:28 +01:00
Maurice 10e0e59c3f fix(server): keep no OSRM stand-in for a day the TREK Valhalla routes
A road trip day OSRM drew while the relay could not answer was cached for five
minutes, and a key added or removed kept serving the old engine's answer. The
stand-in is no longer cached and the key's availability is part of the cache
key. A walk kept off the boats may still go to FOSSGIS, which is now said and
tested.
2026-10-29 13:34:28 +01:00
Maurice e3a8c6fb8a fix(server): tell a user over the routing allowance when to ask the relay again
The per-user 429 carried no Retry-After, so the planner left the relay alone for
its full ten minutes and routed every day with OSRM meanwhile. It now answers
like the installation limit does, with the minute the window takes.
2026-10-29 13:34:28 +01:00
Maurice 7dc313c567 fix(maps): say which engine drew a day and keep Valhalla days found while the relay rests
The route cache key followed the relay's rest, so after one failed relay request
(always the case offline) every day the device held only from Valhalla missed
the cache for ten minutes. The key now follows the configured provider, and a
resting relay only stops the question from going out.

A route now names the engine that drew it. A day OSRM drew because Valhalla
could not answer is OSRM's on the rail, and its other ways are asked of OSRM,
instead of being labelled Valhalla's and compared with Valhalla offers.

An own Valhalla gets the relay's twelve second deadline, and a day that runs it
out goes to OSRM without a second wait. A day that avoids something keeps its
old cache key, so the days a device already holds stay found. The leg segment
and the offer types move to modules both engines import.
2026-10-29 13:34:28 +01:00
Maurice e3f542c8b7 docs(wiki): describe Valhalla routing every day and OSRM taking over a day it cannot answer 2026-10-29 13:34:28 +01:00
Maurice ba6428d7f1 feat(admin): say that the routing service calculates every route with OSRM taking over
The hint under Routing service now explains that the TREK services and an
own Valhalla calculate every route, that OSRM calculates a day they cannot
answer, and that FOSSGIS keeps Valhalla to what OSRM cannot do. In all 27
languages.
2026-10-29 13:34:28 +01:00
Maurice 29dc967d29 feat(server): route the road trip days through Valhalla on the server as the planner does
Where the TREK services or an own Valhalla route the days, the MCP road trip
router asks Valhalla for a day that avoids nothing, through the relay alone,
and OSRM routes the whole day when it cannot answer. Requests carry twenty
locations unless they may end up at FOSSGIS, which keeps ten.
2026-10-29 13:34:28 +01:00
Maurice 8fd025e003 feat(client): route every day through Valhalla where the TREK services or an own Valhalla answer
With the TREK services or an own Valhalla, the day plan, the road trip and
the other ways of a leg are routed by Valhalla on foot, by bike and by car,
twenty locations a request. A day it cannot answer (relay refusal, time out,
no way for a leg) goes to OSRM whole and without a word; it is never handed
to FOSSGIS unless it avoids something only Valhalla can avoid. With FOSSGIS
nothing changes: OSRM routes the days and Valhalla the avoidances.

A Valhalla answer is cached under its own key beside the OSRM one, so
switching engines never serves a day from the other engine. The Valhalla
day routing moves out of RouteCalculator into valhallaDayRoute.ts.
2026-10-29 13:34:28 +01:00
Maurice ec8df01f85 feat(shared): name the Valhalla costing of each profile and the providers that route every day
VALHALLA_COSTING maps driving, walking and cycling to auto, pedestrian and
bicycle once for the planner and the server. valhallaRoutesEveryDay says
whether the resolved provider carries every day: the TREK services and an
own Valhalla do, FOSSGIS and no Valhalla leave the days to OSRM.
2026-10-29 13:34:28 +01:00
Maurice 79260ad737 fix: name the routing and allowance labels where the key check finds them and keep the new tests within the lint baselines 2026-10-29 13:34:28 +01:00
Maurice 0426ce7f01 docs(server): name TREK_PHOTON_URL in the services config and the alternates limit of the TREK Valhalla 2026-10-29 13:34:28 +01:00
Maurice f76b8d5d9c fix(server): put a Photon house number before its street as Nominatim does 2026-10-29 13:34:28 +01:00
Maurice 8c11833fb1 fix(server): ask Nominatim when Photon finds nothing and keep bulk lookups off Photon
An empty Photon search, or one without a hit that has an OpenStreetMap id, is asked again at Nominatim as the reverse lookup already was. The background lane goes to Nominatim alone, a reverse lookup leaves Nominatim half of the caller's deadline, and a 429 with Retry-After pauses Photon for that long.
2026-10-29 13:34:28 +01:00
Maurice 45727b3d3b fix(client): ask FOSSGIS when the routing relay times out, refuses the body or is missing
A route or height question through the relay now gives up after 12 s. A 400 without Valhalla's error_code and a 404 or 405 count as the relay unable to answer instead of no way or no Valhalla, and a Retry-After sets how long the tab leaves the relay alone.
2026-10-29 13:34:28 +01:00
Maurice 686592437a fix(server): rest the routing relay after a failure and keep it under the installation's minute
A hung TREK Valhalla cost every leg, height chunk and trace the full 30 s before FOSSGIS answered. The relay now waits 10 s for a route or heights, rests a minute after an error or timeout and answers 503 at once meanwhile, follows a 429's Retry-After (an hour at most), passes that header on to the browser, and holds the installation to 100 questions a minute below the gate's 120 (60 per user).
2026-10-29 13:34:28 +01:00
Maurice 1da7d0627a fix(server): judge the address search in the admin status by TREK_PHOTON_URL and an own NOMINATIM_URL
The status asked a fixed Photon origin and compared NOMINATIM_URL with the public address, so a moved Photon or an explicitly set public Nominatim showed the wrong service as in use. It now reads the same photonUrl and nominatimOwn the Photon client decides by. TREK_VALHALLA_URL joins the commented compose variables.
2026-10-29 13:34:28 +01:00
Maurice bc62eb0d6f feat(admin): show the allowance of place search, address search and routing in the TREK API card
GET /api/admin/service-identity lists places, photon and valhalla, each with
today's allowance read fresh from /v1/quota of its own host (cached for 20
seconds per host) and whether this instance asks it right now: Photon only
beside the public Nominatim, the TREK Valhalla only where the routing choice
comes to it. Photon and Valhalla are asked only with a key and only while
TREK_SERVICES_HOSTS lists them. The last allowance an answer stated is kept
per host for when a host does not answer.

The card lists the three services under each other in the look it had,
a service this instance does not ask dimmed and named as such.
2026-10-29 13:34:28 +01:00
Maurice 091ebed8e4 feat(routing): let the admin pick FOSSGIS, the TREK services or an own Valhalla
routing_valhalla_provider (fossgis, trek or custom) sits beside the routing
URLs under Admin, Default user settings, admin only and locked on a managed
install. Without a choice an own Valhalla is custom, an own OSRM alone asks
no Valhalla, and otherwise the TREK services answer.

The planner asks them through the same-origin relay, so no connect-src
changes. A relay that answers 401 (a shared page without a sign-in), 403,
413, 429 or 5xx sends the question to FOSSGIS without a word and is left
alone for ten minutes. A long line to describe goes as a polyline.
2026-10-29 13:34:28 +01:00
Maurice 80183bc986 feat(server): relay Valhalla routing to the TREK services with the installation key
POST /api/routing/valhalla/route, /trace_attributes and /height ask the
Valhalla at TREK_VALHALLA_URL (default https://valhalla.liketrek.com) for a
signed-in user. The body is held to the shared contract (64 KB, at most 20
locations), every question counts against 120 a minute per user, and the
installation key goes along only to a host TREK_SERVICES_HOSTS lists.
Valhalla's answer is passed on as it came, refusals included.

The road trip router asks the same relay when the routing choice lands on
the TREK services, so the MCP road trip, city walk and hike tools share its
limits, and hands a question it cannot answer to FOSSGIS.
2026-10-29 13:34:28 +01:00
Maurice 250a7d4eb7 docs(wiki): describe the Photon address search and TREK_PHOTON_URL 2026-10-29 13:34:28 +01:00
Maurice 256f8c08e2 feat(server): ask the TREK services' Photon before Nominatim for search and reverse lookups
An installation that holds a key for the TREK services and has no
NOMINATIM_URL of its own now sends the OpenStreetMap search and the
reverse lookups to photon.liketrek.com first. The answers are read into
Nominatim's shape, so the place records stay the same, and every miss
(refused key, allowance spent, timeout, error, foreign body) falls back
to Nominatim, with a one minute pause after four misses in a row.

TREK_PHOTON_URL points at another copy, and the default of
TREK_SERVICES_HOSTS now lists photon.liketrek.com and
valhalla.liketrek.com beside places.liketrek.com.
2026-10-29 13:34:28 +01:00
Maurice 4ab3244f8f fix(server): mark the identity reset as writing its new key on its own
Regenerating clears the stored key at once and writes the new one only after the issuer answered in the background, so the two writes cannot share a transaction.
2026-10-29 13:34:28 +01:00
Maurice cb546e4a72 refactor(server): move the identified Places request into a file of its own
The instance token and the request that carries the installation key leave trek-places.client.ts, which had grown past the 1000-line limit.
2026-10-29 13:34:28 +01:00
Maurice eeae904867 fix(server): solve the proof of work and read the revocation notice without awaiting in a loop
The proof of work and the bounded read of a 403 body recurse instead of looping, the background-work checks compare against null instead of testing a promise, and the tests repeat their requests through oneByOne. The renewal test loads the stored key before its first request, and the panel test picks the last confirm button without Array.prototype.at.
2026-10-29 13:34:28 +01:00
Maurice 414a7ad98f fix(server): fetch a refused registration challenge once more instead of backing off 2026-10-29 13:34:28 +01:00
Maurice 58e006cfd9 docs(wiki): say that requests are counted per installation and per network 2026-10-29 13:34:28 +01:00
Maurice 476dbef7dc fix(server): renew service keys on the next request and stop sending expired ones
An expired key is no longer sent; the request goes through the shared pool
while a due key, or an expired one within its grace, is renewed in the
background. Refusals in one burst count once, a refused renewal counts like
a refused search, and a Places request whose key is refused is sent once
more without it.
2026-10-29 13:34:28 +01:00
Maurice b53751344b fix(server): ask the issuer for the allowance of a key only when TREK_SERVICES_HOSTS lists it 2026-10-29 13:34:28 +01:00
Maurice 01c0f8b64c fix(server): rotate the stored service key with ENCRYPTION_KEY and pin every encrypted app setting 2026-10-29 13:34:28 +01:00
Maurice 44add57109 fix(client): hide the new identity action on a managed install 2026-10-29 13:34:28 +01:00
Maurice afacf00c9f docs: document TREK_SERVICES, TREK_SERVICES_KEY and the daily allowance
docker-compose.yml and the Helm chart name the switch and the key (the key in the Secret, never the ConfigMap), and the wiki explains the installation key, what it carries, where it goes and the admin view of the allowance.
2026-10-29 13:34:28 +01:00
Maurice baf69da27f feat(client): show the TREK services allowance on the TREK API card
A calm panel under the card's facts: a dot with the state, the tier, a bar of today's use against the limit, what is left and the local time the count starts again. It reads when the card comes into view and every 30 seconds while it stays there and the tab is in front, says why when the services are off, revoked or cannot be read, and offers a new identity behind a confirmation unless the key comes from the environment.
2026-10-29 13:34:28 +01:00
Maurice 01ae91c939 feat(server): give each installation a key for the TREK services and report its allowance
A new service-identity domain registers the installation with places.liketrek.com on its first Places request, in the background and behind a light scrypt proof of work, keeps the key encrypted in app_settings, renews it from day 60 through a daily job, drops it after three 401s in a row with up to six hours of jitter, and freezes it on a revocation until an admin starts over. TREK_SERVICES_KEY wins and turns registration off; TREK_SERVICES=off sends no key; a mirror set with TREK_PLACES_URL never registers and never sees the key, which goes only to the hosts in TREK_SERVICES_HOSTS over https.

The Places client takes its headers from the identity port, sends a TREK/<version> User-Agent and keeps the instance token. A 401 or 403 does not open its breaker. GET /api/admin/service-identity reports the state, tier and today's allowance, asking the issuer at most every 20 seconds; POST .../regenerate starts a new identity.
2026-10-29 13:34:28 +01:00
Maurice f19f0a33b2 feat(shared): add the service identity contract and the allowance texts of the TREK API card
Zod schemas for what the key issuer answers (challenge, key grant, quota) and for GET /api/admin/service-identity. The card no longer claims there is no quota: every locale now says there is no key to enter, a fair daily allowance per installation, and that only the number of requests is counted.
2026-10-29 13:34:28 +01:00
Maurice e5fc735e13 docs(readme): close the gap above the sponsor line 2026-10-29 11:38:54 +01:00
Maurice d2ec9eabde docs(readme): describe TREK as the trip ecosystem it has become 2026-10-29 11:38:54 +01:00
Maurice 9128039c38 docs(readme): let the four status badges continue the badge row 2026-10-29 11:38:54 +01:00
Maurice dd6f41f768 docs(readme): give the license, release, pulls and stars badges the style of the row above 2026-10-29 11:38:54 +01:00
Maurice 1a73b22810 docs(readme): show the TREK logo alone at the top 2026-10-29 11:38:54 +01:00
Maurice c1d413f31a docs(readme): set the sponsor logo right under its line and the server hint right under the button 2026-10-29 11:38:54 +01:00
Maurice 8316119293 docs(readme): thank dataforest cloud as infrastructure sponsor, drop the third showcase and the star history 2026-10-29 11:38:54 +01:00
Maurice 74abaa9d7c test(unsplash): check both refusals without awaiting in a loop 2026-10-28 16:30:54 +01:00
Maurice 091505d17c fix(unsplash): tell a server Unsplash turns away that the free key helps
Without an Access Key the image search goes through Unsplash's web endpoint,
which refuses datacenter addresses with a 401 or 403 or answers with a
challenge page. The search then said only "Unsplash search unavailable",
although an admin can already enter a free key in the settings. Such a
refusal now answers 502 with that hint and the code unsplash_key_needed, and
the cover search shows it in the user's language.

Closes #1449
2026-10-28 16:30:54 +01:00
Maurice aee275e2b0 fix(places): read an imported row for the dedup set in one helper
The candidate a GPX or KML row is checked as is built in lineCandidate, so
places.service.ts stays within its size baseline, and the atomic import test
imports the same Ridge line it later expects to be skipped.
2026-10-28 15:43:52 +01:00
Maurice ef86bf2660 fix(places): keep two different tracks of the same name apart on import
Two GPX tracks a watch named alike (two "Afternoon Hike"s) were taken for one:
the import compared a track only by its name, so the second was skipped as
already in the trip. A track now matches a track of that name only when it
also starts, ends and reaches as far as that one, to about a hundred metres,
so the same file imported again is still skipped. The GPX, KML and tour
imports and the tour preview all read the same rule.

Closes #2870
2026-10-28 15:43:52 +01:00
Maurice 0139c46fb4 test(i18n): count the new planned total key in the tours locales 2026-10-28 15:18:07 +01:00
Maurice a0a7fcdbb9 fix(tours): say why a tour over a day cannot be saved
A tour whose walking time and breaks came to more than 24 hours a stage
only greyed out Save, with nothing saying why. The planner now marks the
planned total and says what to do: make it a multi-day tour by ending a
stage at a stop, or enter a shorter planned total. The details section
opens by itself so the reason is in sight.

Closes #2869
2026-10-28 14:52:01 +01:00
Maurice 186dd94b61 fix(assistant): chat with an own server entered without /v1 at /v1
The assistant sent an own server's chat to the address as entered plus
/chat/completions. Ollama, entered as http://host:11434 the way its own docs
write it, answers that with a 404, so every message ended in "something went
wrong" while connecting had worked: the model list is found under the root
at /api/tags. A bare server address now chats under /v1, where Ollama, LM
Studio, vLLM, llama.cpp and LiteLLM all answer; an address with a path, such
as https://api.groq.com/openai/v1, is used as entered.
2026-10-28 12:17:54 +01:00
Maurice ad721a1ac6 test(studio): the auto layout lays out a long journey whole 2026-10-28 12:26:06 +01:00
Maurice 87441f269d fix(studio): lay out a book as long as its journey, with no page limit
A book held at most 150 spreads. The auto layout cut everything past that
without a word, so a long journey's book ended about three quarters of the
way through, with its last page and back cover gone, and adding, copying or
applying a designed page to a full book was refused.

The spread cap is gone from the book schema, the page operations, the
design composer and its apply, the assistant's book card and the design tool
contracts. A design's own page budget (pages.max) still bounds what it lays
out. The save route's 8 MB body still bounds a book's size, which at two to
five kilobytes an auto laid page is a few thousand pages.

The templated design digests move because their counter ids shift by two;
the pages, their order and their elements are unchanged.
2026-10-28 12:06:50 +01:00
github-actions[bot] a0943f8226 chore: bump version to 5.0.1 [skip ci] 2026-10-28 10:43:52 +00:00
Maurice 6741e8ca0d fix(help): give the close button a row of its own
A wider gutter left the button floating in the corner over the first card and the scrollbar. It now sits in its own row above the scrolling content, level with the Help title, and the content keeps symmetric padding.
2026-10-28 10:24:08 +01:00
Maurice c14f179e39 fix(help): keep the help center's content clear of its close button
The close button floats over the scrolling content, which only kept a 40px gutter on the end side while the button reaches 52px in, so the first card ran right up to it. The content now keeps a wider gutter on that side.
2026-10-28 10:24:08 +01:00
Maurice 3bfef73a46 refactor(maps): move the place clusters out of MapViewGL
The cluster source, its layers, the expansion zoom and the DOM bubbles
now live in placeClusterLayers.ts with their own tests, which brings
MapViewGL back under its size baseline instead of growing it. Lowers the
size and eslint baselines that this change made smaller.
2026-10-28 10:11:56 +01:00
Maurice 7e790c3fb7 fix(maps): draw the GL cluster bubbles like the Leaflet ones
The MapLibre and Mapbox maps painted their place clusters as a canvas
circle plus a symbol layer, so the count used whatever glyphs the map
style ships and the bubble had no hover. They are now DOM pins built by
the same markup and CSS as the Leaflet bubbles, with the same sizes,
font and hover scale. The canvas layers stay, invisible, as the hit
area the track and route layers check a click against.
2026-10-28 10:11:56 +01:00
Maurice 228db8541c chore(notices): show 5.0.1 in the release popup
The release popup still named 5.0.0 and linked the 5.0.0 release notes. It now names 5.0.1 and links its tag.
2026-10-28 09:52:18 +01:00
Maurice a729a6935f fix(journey): keep the photo picker's calendar inside the screen
The date picker of the external photo picker hung absolutely under its
trigger, inside a dialog that clips its content. The last week of a
six-week month fell below the dialog, and for the end date in the
right-hand column the calendar opened past the dialog's right edge, so
the last weekday and the next-month arrow could not be reached.

The calendar now opens in a portal, pinned to its trigger through
useAnchoredPosition: it flips above the trigger when below is too short,
scrolls when neither side holds it, and is kept inside the viewport
sideways.

Closes #2858
2026-10-28 09:25:53 +01:00
Maurice 6c0e39e287 fix(transit): name and record the backend that answered, not always Transitous
With Google as the transit provider, the result list still credited
Transitous, and a route added from the panel or through
create_transit_journey was saved with provider 'transitous'. The panel
now credits "Google Maps" (untranslated, as Google's terms ask) or
Transitous after the backend that answered and saves that one; the MCP
journey records the backend that plans routes for the caller, and
search_transit_routes reports it.
2026-10-27 21:30:18 +01:00
Maurice 22574a2270 fix(assistant): send Gemini's thought signatures back with its tool calls
Gemini 3 signs a tool call on its OpenAI-compatible endpoint
(extra_content.google.thought_signature) and refuses the next request
with a 400 when the call comes back without it, so every turn in which
the model called a tool failed. The Chat Completions client now keeps
the signature per call as the step's provider state and sends it back
with the call, to the model that signed it only.

An own server entered with Gemini's endpoint now also gets Gemini's
narrower tool schemas, which it refused before.
2026-10-27 21:06:29 +01:00
Maurice 1d880cb881 style(tours): format the route file reader 2026-10-27 15:29:00 +01:00
Maurice 3d076704f9 test: keep the new day-note and document-sync tests inside the lint and type gates 2026-10-27 15:16:50 +01:00
Maurice 1b0f043506 fix(roadtrip): stop pacing the joins between days that come back from the route cache
With connected days, every join between two days kept its one-request-a-second
slot even when the route came back from the device's cache, so a long road
trip still waited about a second per join on every open. A join answered from
the cache now gives its slot back; requests that go out stay spaced as before.

Refs #2794
2026-10-27 15:02:24 +01:00
Maurice c383b11648 fix(assistant): propose a route file's track from before Tours instead of skipping it
The Tours import takes over a same-named track a trip kept from 4.x, but a
route file attached in the assistant chat was read with the plain duplicate
rule only, so that track was reported as already in the trip and never
proposed. The read now leaves such a track to the import, which takes it
over as a tour like the Tours dialog does.

Refs #2810
2026-10-27 15:02:24 +01:00
Maurice fed564e7c0 fix(assistant): answer the model's own calls of server tools when a thread is replayed
remember_for_trip runs at once during a turn, and read_attached_file and
keep_attached_file are answered by the server, but all three are calls the
model made. Rebuilding a thread from its stored steps left them unanswered,
and ChatGPT, the OpenAI reasoning models and Anthropic replay their own record
of each step, call included, so the provider refused the conversation with a
400: the continuation after applied changes failed, and every later message of
the thread with it. A step that kept a note also sent an empty user message.
Every call the model made is now answered as in the live turn, the attached
file's outcome as the answer of its call; threads that broke this way work
again on their next message. A refusal's log line names the refused field's
path, never its message.

Closes #2845
2026-10-27 14:53:13 +01:00
Maurice 9e216f5ae4 fix(docsync): let the trip owner edit or remove a stored document-store connection
Once a store was connected for a trip, its address and login could only be
entered again after a sync run had been refused, and the stored connection
could not be removed at all: the server's DELETE route had no way in from the
app. A binding's settings now open the connection at any time, the folder
picker leads back to it before a folder is chosen, and the connection form
removes a stored connection after a confirmation, on the desktop panel and the
phone sheet alike. Removing it drops the trip's bindings through it; documents
stay on both sides.

Closes #2842
2026-10-27 14:45:17 +01:00
Maurice 0f0232491b fix(mcp): describe day-note title and body and allow the planner's 2000-character body
create_day_note and update_day_note described `time` as a clock label and capped
it at 250 characters, while the planner shows it as the Markdown body under the
title and allows 2000. The assistant put long text in the title and had to cut
notes it was asked to edit. Both tools now take text and time from the REST
contract with descriptions naming title and body, and an update still refuses an
empty title. The assistant's change card labels the two fields title and content
instead of text and time.

Closes #2840
2026-10-27 14:38:30 +01:00
Maurice 35fb59e64e chore(lint): lower the no-misused-promises baseline the packing fix brought down 2026-10-26 22:02:04 +01:00
Maurice ee3fff1dab fix(packing): Enter on the first item keeps the typing going in the add field
A new list starts with a '...' placeholder row. Typing the first item there
and pressing Enter saved it but left the cursor nowhere, while the "Add item"
field puts it straight back for the next item. Enter on a filled placeholder
row now opens the category's add field and focuses it. A blur still just
saves, so clicking elsewhere does not pull the focus back.

Closes #2825
2026-10-26 21:50:37 +01:00
Maurice 3bc75d2b1c fix(airports): keep airports OurAirports flags as having no scheduled service
The airport picker never offered Little Cayman (LYB): OurAirports marks it as
having no scheduled service although it has daily flights, and the build
dropped every airport on that flag. The same flag also hid Vestmannaeyjar,
Western Sydney and around 1,280 other large and medium airports with an IATA
code. The build now applies the flag to small airports only, where it keeps
private airstrips out.

The bundled list gains exactly those airports. Every entry it already carried
stays unchanged, because a fresh OurAirports export currently renames PBI to
DJT and drops nine other codes that saved bookings point at.

Closes #2820
2026-10-26 20:21:13 +01:00
Maurice 094426debb revert(places): keep the Tracks choice out of the pool while Tours is on, as B4A decided 2026-10-26 14:26:09 +01:00
Maurice 313198ba63 fix(places): read the dedup tracks off the dedup inputs, the places files stay within their size entries 2026-10-26 14:18:59 +01:00
Maurice e677d58f91 fix(places): a track is a duplicate only of a track, never of a sight of that name
A GPX or KML import skipped every row whose name was already a place of the
trip. A watch names a hike after where it went, so "Hokitika Gorge" was
refused as already in the trip because the gorge itself was a planned
place, with no track to show for it (#2810). A track now matches only the
tracks of that name; a pin still matches every place of its name.

The Places filter also hid its "Tracks" choice whenever Tours was on, on the
grounds that tracks are tours then. A track imported before Tours is a
plain place and stays in the list, so the choice now counts the tracks that
are no tour.
2026-10-26 14:08:56 +01:00
Maurice bc60aea839 test(studio): the book words read the new heading 2026-10-26 13:29:35 +01:00
Maurice b5cd951bd2 fix(studio): the page of stops without photos or words reads as along the way
Auto layout gathers journal entries with no photo and no text, one after the
other, onto one list page instead of giving each an empty page. Its heading
said "Stations" (German "Stationen"), which read as train stations and gave
no hint why those places were on it. It now says "Along the way" in every
language; each stop still carries its date.
2026-10-26 13:16:35 +01:00
Maurice fd818050bd fix(tours): pair the tracks to take over in the tours domain, off the places list 2026-10-26 12:47:09 +01:00
Maurice 0313597b10 fix(tours): read the tracks to take over through the typed query API, seed the tests through the factories 2026-10-26 12:35:19 +01:00
Maurice 2c867673bf test(tours): count the new preview key, lower the await-in-loop entry, type the adopt rows 2026-10-26 12:23:08 +01:00
Maurice 39ebf0976c fix(tours): a tour import takes over a track imported before Tours
A GPX track imported in 4.x is a plain place: a line, no tours row. It never
showed under Tours, and importing the same file there was refused as a
duplicate by its name, so there was no way to turn it into a tour (#2810).

The tour import now takes over a track of the trip that has the same name
and is no tour yet: the place keeps its name and line and gets the tours
row, with the recorded times the file adds. The preview says so instead of
"Already in the trip". New and taken-over tracks commit in one transaction.
2026-10-26 12:17:15 +01:00
Maurice 1ecc449f3e test(ai): fix the model-not-found pattern, guard the fetch mocks, and read the booking in an ORM context 2026-10-26 10:20:47 +01:00
Maurice b8990782cc fix(ai): connect and read bookings through any OpenAI-compatible own server
Connecting an own server asked Ollama's /api/tags and gave up on any answer
that was not a 404, and every failure, a refused key included, read as "not
responding". The model list is now looked for at /api/tags, at the address as
entered and at /v1/models, an answer only counts when it is a list, and a
refused key or a server that answered without a list say so (#2808).

AI parsing through an own server always called Ollama's /api/chat, which an
OpenAI-compatible server such as Groq answers with 404. A server without it
now gets the same extraction over /chat/completions at the address as
entered (#2806).
2026-10-26 10:20:46 +01:00
Maurice 8ba7add541 fix(plugins): give the runtime test's plugins table the accepted-range columns, and move the theme entry with the menu code 2026-10-26 09:49:22 +01:00
Maurice a3664e7117 fix(plugins): lower the unused-key entry the moved components reach again, and test the marked install 2026-10-26 09:49:22 +01:00
Maurice 64ed2175a5 fix(plugins): keep the API client and the panel test within their size entries, drop an unused import 2026-10-26 09:49:22 +01:00
Maurice 447ae54b93 fix(i18n): translate the plugin filter button where it read as English 2026-10-26 09:49:22 +01:00
Maurice 0067b0ff19 fix(plugins): let an admin run a plugin outside its TREK range, and tidy the plugins panel
TREK 5.0 left most registry plugins blocked because their authors still declare
"<5.0.0", and the only way past that was TREK_PLUGINS_IGNORE_TREK_RANGE for every
plugin at once. An admin can now install or switch on one such plugin after a
warning; the consent is kept on its row for that plugin version and TREK major,
so an update or a new major asks again. The environment switch works as before.

Discover marks a plugin not confirmed for this TREK on its card, and Install opens
the warning first. Admin > Plugins reads cleaner: one row of alike facts per plugin
with each state that needs attention on its own line, type, status and order behind
one Filter button, and the containment notes in a dialog behind the shield in the
header. The row, the filter menu and the dialog moved into their own files.
2026-10-26 09:49:22 +01:00
Maurice d589da59f2 fix(wanderer): find the author of an account whose one actor wanderer expands as an object
PocketBase answers a back-relation with a single record as the object itself,
not a list, so the user lookup failed as an answer TREK did not understand and
every search with it.

Closes #2799
2026-10-26 01:03:35 +01:00
Allan Braun 69ff295184 fix(assistant): answer Brazilian Portuguese users in Portuguese, not Breton
TREK's code for Brazilian Portuguese is 'br', which Intl reads as Breton.
The assistant passed it straight to Intl.DisplayNames for the system
prompt's answer language and to the card formatters, so the model was told
to answer in Breton. Convert TREK codes to their BCP-47 tag with
getIntlLanguage before handing them to Intl.

Closes #2800
2026-10-26 00:04:56 +01:00
Maurice d07c17ded6 fix(assistant): finish an OpenRouter sign-in whose code came back after a second question mark
The return address already carries a query, so a code appended with ? instead
of & ended up inside the ai marker and the page did nothing, without a word.
2026-10-25 19:36:00 +01:00
Maurice 44509723fd fix(bookings): keep the review's map status within the complexity and strict-null budgets
The status hides itself for a stay linked to a trip place instead of each form
branching around it, and a lookup takes a missing name.
2026-10-25 18:59:55 +01:00
Maurice b80fdf27f2 fix(bookings): show where an imported stay lands on the map before it is saved
An imported hotel was looked up once, by its name and its address together.
A booking site's romanized address (Agoda for a stay in Japan) often matches
nothing and sank the hotel's name with it, so the place was created without
coordinates and silently never showed on the map.

The review now finds the stay right away (name and address, then the name,
then the address) and says under the address whether it is on the map and
where, or that it is not yet, with a way to search again. Picking an address
suggestion sets the spot by hand. The save uses that spot, falls back the same
way when there is none, and a stay saved without a spot says so in a toast.
Desktop dialog and phone sheet share the lookup through useReservationForm.
2026-10-25 18:59:55 +01:00
Maurice a0b7b4d196 chore(lint): lower the complexity baseline the route store move took down 2026-10-25 18:59:49 +01:00
Maurice aa24a9eff7 fix(map): keep routed roads on the device so a whole-trip route opens drawn
With "Show whole trip" on, every leg of every day went to the router one at a
time, a second apart, and the route cache lived in memory only, so each visit
started over: a trip of a month took 30 to 45 seconds before its route was
drawn, on every open.

Routes the router answered are now also kept on the device (Dexie v14,
routeCache by RouteCalculator's cache key, a month at most, the oldest give way
past 3000). A leg found there needs no request and no pause, so a trip whose
legs did not change opens with its roads at once; only new or changed legs are
routed. Plugin routes stay in memory only, since they depend on trip and day.
The cache moves out of RouteCalculator into routeStore.ts.

Closes #2794
2026-10-25 18:59:49 +01:00
Maurice de5417f9f0 style(i18n): wrap the wanderer no_author line in the CJK locales 2026-10-25 18:59:43 +01:00
Maurice a48dd7645f fix(wanderer): say so when wanderer names no author for the token's account
A token whose account has no local ActivityPub author was reported as
invalid_response, which the settings show as 'The answer did not come from
wanderer', although wanderer had answered fine. It now has its own code,
no_author, with a message in every language that says what is missing.
2026-10-25 18:59:43 +01:00
Maurice 217a576e16 fix(wanderer): log why an answer from wanderer was not understood
A failed wanderer call kept its reason in WandererError.detail (the field that
did not match, or what came instead of JSON), and the client comment said it
stays in the log, but nothing wrote it anywhere. The log only had the generic
'wanderer answered with something TREK does not understand', so neither an
admin nor we could tell what wanderer had sent. Every refusal now logs its
code, message and detail.
2026-10-25 18:59:43 +01:00
Maurice f6976d5cec fix(assistant): send OpenAI reasoning models on an API key to the Responses API
gpt-5.6-sol, gpt-6.1-sol and the other reasoning models refuse function tools
on Chat Completions, so every assistant message on such a model came back as
HTTP 400. An OpenAI connection whose model is from the o-series or GPT-5 and
later now goes through the Responses API, the client a ChatGPT plan already
uses, with the key as its bearer token and no token refresh on a 401. Older
models and the -chat variants stay on Chat Completions.

Closes #2792
2026-10-25 16:55:39 +01:00
Maurice c1c2bbf146 fix(assistant): replay an Anthropic answer that thought with its thinking blocks
Sonnet 5.5 and Opus 5.5 think by default and write what they say between tool
calls into thinking blocks, which they want back unchanged with the rest of
the turn. The client dropped those blocks (and their signatures) and rebuilt
the turn from its text and calls, so after a tool result the next step came
back empty and the assistant gave up with empty_answer. A turn that thought
is now replayed block by block like one that searched the web; the cache mark
before the question skips a thinking block, and replayed blocks are copies so
a mark never sticks to the stored answer.

Closes #2789
2026-10-25 16:49:46 +01:00
Maurice c05349f7c4 fix(docker): let dumb-init forward the stop signal in the shipped compose file
The compose file drops every capability and adds back CHOWN, SETUID and
SETGID. dumb-init runs as root and node as the node user, and signalling a
process of another user needs KILL, so docker stop never reached node and
always ended in SIGKILL (exit 137). KILL only reaches processes inside the
container's own PID namespace. CI starts the image with the same set and now
checks that docker stop exits 0 after a logged SIGTERM.

Closes #2790
2026-10-25 16:47:22 +01:00
1956 changed files with 73139 additions and 15603 deletions
+1
View File
@@ -1,6 +1,7 @@
node_modules
client/node_modules
server/node_modules
shared/node_modules
client/dist
shared/dist
data
@@ -14,7 +14,7 @@ jobs:
timeout-minutes: 45
steps:
- name: Close stale invalid-title issues
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const badTitles = [
@@ -15,7 +15,7 @@ jobs:
timeout-minutes: 45
steps:
- name: Close stale wrong-base-branch PRs
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const { data: pulls } = await github.rest.pulls.list({
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
timeout-minutes: 45
steps:
- name: Flag or redirect issue
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const title = context.payload.issue.title.trim();
+66 -3
View File
@@ -5,6 +5,13 @@ name: Build & Push Docker Image (Prerelease)
# :latest-pre, and tags the commit. Nothing moves :latest. Like the stable
# release it refuses a commit whose ci-ok is not green. docker.yml with
# bump=auto later finalizes the line once main carries it.
#
# A prerelease asks the TREK services issuer for a registration dialect of its
# own -pre.N version (registration-dialect.yml, as the stable release does)
# and builds it into the image; nothing of it is committed, dev keeps the
# module of the last stable release, which it gets when main is merged back
# into dev. Without one the image carries dev's module, with a warning when
# that is the original one or old.
on:
workflow_dispatch:
@@ -89,10 +96,23 @@ jobs:
echo "VERSION=$NEW_VERSION" >> "$GITHUB_OUTPUT"
echo "$STABLE to $NEW_VERSION"
# The registration dialect of this prerelease, fetched and checked in a job
# without write access. It never fails the build.
dialect:
needs: version-bump
uses: ./.github/workflows/registration-dialect.yml
with:
version: ${{ needs.version-bump.outputs.version }}
ref: ${{ needs.version-bump.outputs.sha }}
secrets:
TREK_DIALECT_TOKEN: ${{ secrets.TREK_DIALECT_TOKEN }}
build:
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
needs: version-bump
needs: [version-bump, dialect]
# Whatever became of the dialect job: without its artifact the image carries dev's module.
if: ${{ !cancelled() && needs.version-bump.result == 'success' }}
strategy:
fail-fast: false
matrix:
@@ -111,9 +131,51 @@ jobs:
with:
ref: ${{ needs.version-bump.outputs.sha }}
- name: Take over the prerelease's dialect
id: take
uses: actions/download-artifact@v7
continue-on-error: true
with:
name: registration-dialect
path: ${{ runner.temp }}/dialect
# Into the build context only; the commit stays as it is. Without a
# dialect of its own the image speaks dev's module, which the issuer
# takes for new installations for 180 days after it was made (longer
# while it is the newest stable release's). That is warned about, never
# stopped on, and a date that cannot be read only warns as well.
- name: Place the dialect
env:
VERSION: ${{ needs.version-bump.outputs.version }}
TAKEN: ${{ steps.take.outcome }}
REPORT: ${{ matrix.platform == 'linux/amd64' }}
run: |
set -u
target=server/src/nest/service-identity/registration-dialect.ts
fetched="$RUNNER_TEMP/dialect/registration-dialect.ts"
if [ "$TAKEN" = success ] && [ -f "$fetched" ]; then
cp "$fetched" "$target"
echo "$VERSION registers in dialect $(sed -n "s/^const DIALECT = '\(.*\)';$/\1/p" "$target") of its own."
exit 0
fi
[ "$REPORT" = true ] || exit 0
id="$(sed -n "s/^const DIALECT = '\(.*\)';$/\1/p" "$target")"
created="$(sed -n "s/^ createdAt: '\(.*\)',$/\1/p" "$target")"
if [ "$id" = 0 ]; then
message="$VERSION got no dialect of its own and registers in the original one, which the issuer accepts only for a transition period."
elif made="$(date -u -d "$created" +%s 2>/dev/null)"; then
age=$(( ($(date -u +%s) - made) / 86400 ))
message="$VERSION got no dialect of its own and registers in dev's dialect $id from $created, $age days old."
[ "$age" -gt 150 ] || { echo "$message"; exit 0; }
else
message="$VERSION got no dialect of its own, and the date of dev's dialect cannot be read from $target."
fi
echo "::warning::$message"
printf '### Registration dialect\n%s\n' "$message" >> "$GITHUB_STEP_SUMMARY"
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
- uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -156,6 +218,7 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 120
needs: [version-bump, build]
if: ${{ !cancelled() && needs.build.result == 'success' }}
steps:
- uses: actions/checkout@v6
with:
@@ -173,7 +236,7 @@ jobs:
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
- uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
+182 -23
View File
@@ -2,17 +2,29 @@ name: Build & Push Docker Image
# The stable release. Dispatched by hand on main, it:
# 1. refuses to run unless ci-ok passed on the main commit it releases,
# 2. works out the version from the tags and the bump input, commits the
# bump to package.json, the lockfile and Chart.yaml, tags it and pushes,
# 3. builds both platforms from exactly that bump commit (never from
# 2. works out the version from the tags and the bump input,
# 3. asks the TREK services issuer for the registration dialect of that
# version and checks it, in a job without write access
# (registration-dialect.yml); without one the release ships the dialect
# already on main, but only while that one still has 90 days to run,
# 4. commits the bump to package.json, the lockfile, Chart.yaml and the
# dialect, tags it and pushes,
# 5. builds both platforms from exactly that bump commit (never from
# whatever main points to by then) and publishes :VERSION,
# :MAJOR.MINOR, :MAJOR and :latest,
# 4. publishes the Helm chart from the same commit.
# 6. publishes the Helm chart from the same commit.
#
# Big or risky changes go out as a prerelease first (docker-dev.yml, tagged
# :latest-pre and :<next>-pre.N); bump=auto here then finalizes that line.
# Without a prerelease in flight there is no default: the run stops until
# someone picks patch, minor or major.
#
# Each stable release registers new installations in a dialect of its own,
# which the issuer accepts for 180 days, and for as long as it is the newest
# release's; renewing a key needs none. dev gets the module when main is merged
# back into dev after the release; a prerelease asks the issuer for one of its
# own (docker-dev.yml). The issuer's side, and what to set up before the first
# release with a dialect, is in the operations guide of the TREK services.
on:
workflow_dispatch:
@@ -26,6 +38,14 @@ on:
description: "Type MAJOR (all caps) to confirm a major release"
type: string
default: ''
ci_from:
description: 'Take ci-ok from this earlier commit instead of the one on main, when main differs from it only under .github/ (a fix to a release workflow needs no second test run)'
type: string
default: ''
allow_stale_dialect:
description: 'Ship without a dialect of its own even if the one on main is the original one or has under 90 days left'
type: boolean
default: false
permissions:
contents: write
@@ -36,13 +56,16 @@ concurrency:
cancel-in-progress: false
jobs:
version-bump:
version:
if: github.repository == 'liketrek/TREK'
runs-on: ubuntu-24.04
timeout-minutes: 120
timeout-minutes: 30
permissions:
contents: read
checks: read
outputs:
version: ${{ steps.bump.outputs.VERSION }}
sha: ${{ steps.bump.outputs.SHA }}
base: ${{ steps.base.outputs.SHA }}
steps:
- name: Release from main only
if: github.ref != 'refs/heads/main'
@@ -50,17 +73,17 @@ jobs:
echo "::error::Dispatch this workflow on main, not on $GITHUB_REF."
exit 1
- uses: actions/create-github-app-token@v3
id: app-token
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
- uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true
token: ${{ steps.app-token.outputs.token }}
persist-credentials: false
# The bump job checks out exactly this commit, so what ci-ok was checked
# on is what gets released, however long the dialect takes.
- name: Note the commit being released
id: base
run: echo "SHA=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
# The dev push, the main push and any manual run each leave a ci-ok on
# this commit. scripts/ci/ci-ok-verdict.mjs counts the newest attempt of
@@ -68,6 +91,7 @@ jobs:
# finished last to be green, so a later green run supersedes a flaky or
# cancelled one. It prints the link of every run it counted.
- name: Require a green ci-ok on the commit being released
if: inputs.ci_from == ''
env:
GH_TOKEN: ${{ github.token }}
run: |
@@ -77,14 +101,43 @@ jobs:
gh api "repos/$GITHUB_REPOSITORY/commits/$sha/check-runs?check_name=ci-ok&filter=all&per_page=100" > "$runs"
node scripts/ci/ci-ok-verdict.mjs "$runs" "$sha"
- name: Determine bump type and update version
# ci_from: main carries only workflow changes on top of a commit that
# already passed. That commit must be an ancestor of main, everything
# outside .github/ must be identical, and one of its ci-ok runs must be
# green (its dev push, typically).
- name: Require a green ci-ok on the commit main's code comes from
if: inputs.ci_from != ''
env:
GH_TOKEN: ${{ github.token }}
CI_FROM: ${{ inputs.ci_from }}
run: |
set -euo pipefail
from="$(git rev-parse --verify "$CI_FROM^{commit}")"
if ! git merge-base --is-ancestor "$from" HEAD; then
echo "::error::$from is not an ancestor of main."
exit 1
fi
outside="$(git diff --name-only "$from" HEAD -- . ':(exclude).github/**')"
if [ -n "$outside" ]; then
echo "::error::main differs from $from outside .github/:"
echo "$outside"
exit 1
fi
green="$(gh api "repos/$GITHUB_REPOSITORY/commits/$from/check-runs?check_name=ci-ok&filter=all&per_page=100" \
--jq '[.check_runs[] | select(.status == "completed" and .conclusion == "success")] | length')"
if [ "$green" -lt 1 ]; then
echo "::error::$from has no green ci-ok."
exit 1
fi
echo "main differs from $from only under .github/, and $from has $green green ci-ok run(s)."
- name: Determine bump type and version
id: bump
env:
BUMP_INPUT: ${{ inputs.bump || 'auto' }}
CONFIRM_MAJOR: ${{ inputs.confirm_major }}
run: |
set -eu
git fetch --tags
# Derive version from git tags, no package.json dependency
STABLE_TAG=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | grep -v '\-pre\.' | sort -V | tail -1)
@@ -143,6 +196,107 @@ jobs:
echo "VERSION=$NEW_VERSION" >> "$GITHUB_OUTPUT"
echo "$STABLE to $NEW_VERSION ($BUMP)"
# The registration dialect of this version, fetched and checked in a job
# without write access. It never fails; without an artifact from it the
# bump decides below whether the dialect on main may ship.
dialect:
needs: version
uses: ./.github/workflows/registration-dialect.yml
with:
version: ${{ needs.version.outputs.version }}
ref: ${{ needs.version.outputs.base }}
secrets:
TREK_DIALECT_TOKEN: ${{ secrets.TREK_DIALECT_TOKEN }}
# Runs whatever became of the dialect job, a timeout or a lost runner
# included: no artifact means the dialect on main, if it may still ship.
version-bump:
needs: [version, dialect]
if: ${{ !cancelled() && needs.version.result == 'success' }}
runs-on: ubuntu-24.04
timeout-minutes: 120
outputs:
sha: ${{ steps.commit.outputs.SHA }}
steps:
- uses: actions/create-github-app-token@v3
id: app-token
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
- uses: actions/checkout@v6
with:
ref: ${{ needs.version.outputs.base }}
fetch-depth: 0
fetch-tags: true
token: ${{ steps.app-token.outputs.token }}
- name: Take over the checked dialect
id: take
uses: actions/download-artifact@v7
continue-on-error: true
with:
name: registration-dialect
path: ${{ runner.temp }}/dialect
# Without a dialect of its own the release ships the one on main, but
# only while the issuer takes new registrations in it for 90 more days:
# dialect 0 closes on the issuer's transition date, and a dialect with
# little left would shut out new installations of this release soon after
# it is out. Then this stops before anything is committed, unless
# allow_stale_dialect says to ship anyway. The issuer keeps the newest
# release's dialect open past its date, so the date here is the least it
# has, not the most.
- name: Place the dialect
env:
VERSION: ${{ needs.version.outputs.version }}
TAKEN: ${{ steps.take.outcome }}
ALLOW_STALE: ${{ inputs.allow_stale_dialect }}
run: |
set -u
target=server/src/nest/service-identity/registration-dialect.ts
fetched="$RUNNER_TEMP/dialect/registration-dialect.ts"
dialect_id() { sed -n "s/^const DIALECT = '\(.*\)';$/\1/p" "$1"; }
report() {
echo "$2$1"
printf '### Registration dialect\n%s\n' "$1" >> "$GITHUB_STEP_SUMMARY"
}
if [ "$TAKEN" = success ] && [ -f "$fetched" ]; then
cp "$fetched" "$target"
report "$VERSION registers in dialect $(dialect_id "$target"), written by the issuer for it." ""
exit 0
fi
id="$(dialect_id "$target")"
created="$(sed -n "s/^ createdAt: '\(.*\)',$/\1/p" "$target")"
left=""
if [ -n "$id" ] && [ "$id" != 0 ] && made="$(date -u -d "$created" +%s 2>/dev/null)"; then
left=$(( (made + 180 * 86400 - $(date -u +%s)) / 86400 ))
fi
if [ -n "$left" ] && [ "$left" -ge 90 ]; then
report "$VERSION ships without a dialect of its own and registers in dialect $id from $created, which the issuer takes for new installations for $left more days at least. Its operator can write one by hand for the next patch." "::warning::"
exit 0
fi
if [ "$id" = 0 ]; then
what="the original registration, which the issuer accepts only until the end of its transition period"
elif [ -n "$left" ]; then
what="dialect $id from $created, which has $left days left"
else
what="a dialect whose id or date cannot be read from $target"
fi
if [ "$ALLOW_STALE" = true ]; then
report "$VERSION ships without a dialect of its own and registers in $what, as allow_stale_dialect asked." "::warning::"
exit 0
fi
report "$VERSION got no dialect of its own and would register in $what. Nothing was committed or pushed. Make sure the issuer runs with its dialect seed and release token and that TREK_DIALECT_TOKEN is set, then run the release again, or tick allow_stale_dialect to ship anyway." "::error::"
exit 1
- name: Update the version, commit, tag and push
id: commit
env:
NEW_VERSION: ${{ needs.version.outputs.version }}
run: |
set -eu
# Update all workspace + root package.json files and the root lockfile in one shot
npm version "$NEW_VERSION" --workspaces --include-workspace-root --no-git-tag-version
sed -i "s/^version: .*/version: $NEW_VERSION/" charts/trek/Chart.yaml
@@ -153,7 +307,8 @@ jobs:
# landed on main in the meantime.
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json package-lock.json server/package.json client/package.json shared/package.json charts/trek/Chart.yaml
git add package.json package-lock.json server/package.json client/package.json shared/package.json \
charts/trek/Chart.yaml server/src/nest/service-identity/registration-dialect.ts
git commit -m "chore: bump version to $NEW_VERSION [skip ci]"
git tag -a "v$NEW_VERSION" -m "v$NEW_VERSION"
git push origin HEAD:main --follow-tags
@@ -164,7 +319,9 @@ jobs:
build:
runs-on: ${{ matrix.runner }}
timeout-minutes: 120
needs: version-bump
needs: [version, version-bump]
# version-bump may run after a failed dialect job; that alone must not skip what follows it.
if: ${{ !cancelled() && needs.version-bump.result == 'success' }}
strategy:
fail-fast: false
matrix:
@@ -185,7 +342,7 @@ jobs:
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
- uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -205,7 +362,7 @@ jobs:
outputs: type=image,name=mauriceboe/trek,push-by-digest=true,name-canonical=true,push=true
no-cache: true
build-args: |
APP_VERSION=${{ needs.version-bump.outputs.version }}
APP_VERSION=${{ needs.version.outputs.version }}
labels: |
org.opencontainers.image.revision=${{ steps.meta.outputs.revision }}
org.opencontainers.image.created=${{ steps.meta.outputs.created }}
@@ -227,7 +384,8 @@ jobs:
merge:
runs-on: ubuntu-24.04
timeout-minutes: 120
needs: [version-bump, build]
needs: [version, version-bump, build]
if: ${{ !cancelled() && needs.build.result == 'success' }}
steps:
- uses: actions/checkout@v6
with:
@@ -243,7 +401,7 @@ jobs:
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
- uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -252,7 +410,7 @@ jobs:
working-directory: /tmp/digests
run: |
set -euo pipefail
VERSION="${{ needs.version-bump.outputs.version }}"
VERSION="${{ needs.version.outputs.version }}"
MAJOR_TAG="$(echo "$VERSION" | cut -d. -f1)"
# :MAJOR.MINOR takes the patches of one minor line and nothing else,
# the conservative pin for an install that wants to wait out a
@@ -292,6 +450,7 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 120
needs: version-bump
if: ${{ !cancelled() && needs.version-bump.result == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v6
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
steps:
- name: Flag or clear wrong base branch
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
const base = context.payload.pull_request.base.ref;
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
- name: Comment on PR if checks failed
if: steps.checks.outcome == 'failure'
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
script: |
await github.rest.issues.createComment({
-191
View File
@@ -1,191 +0,0 @@
name: Native apps release
# Builds the signed store builds of the iOS and Android apps and uploads them:
# the Android App Bundle to a Google Play testing track, the iOS archive to
# App Store Connect (TestFlight). Promotion to production stays a manual step
# in both consoles.
#
# Runs only in liketrek/TREK, only by hand or on a version tag, and only after a
# required reviewer approves the `store` environment, which holds every secret
# (see wiki/Native-Apps.md for the list). Pull requests never reach it.
#
# Both stores get the same numbers: the version from client/package.json
# without a prerelease suffix, and a build number that only grows. A prerelease
# reuses its X.Y.Z with a higher build number and belongs on the internal track
# and in TestFlight only.
on:
workflow_dispatch:
inputs:
track:
description: Google Play track
type: choice
options: [internal, alpha, beta]
default: internal
push:
tags: ['v*']
permissions:
contents: read
concurrency:
group: native-release
cancel-in-progress: false
jobs:
android:
if: github.repository == 'liketrek/TREK'
runs-on: ubuntu-24.04
timeout-minutes: 40
environment: store
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: npm
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
cache: gradle
- name: Version and build number
id: version
run: |
echo "version=$(node -p "require('./client/package.json').version.split('-')[0]")" >> "$GITHUB_OUTPUT"
echo "build=$((GITHUB_RUN_NUMBER + 1000))" >> "$GITHUB_OUTPUT"
- name: Install
run: npm ci --workspace=shared --workspace=client
- name: Build shared
run: npm run build --workspace=shared
- name: Build the address screen and sync it into the shell
working-directory: client
run: npm run cap:sync -- android
- name: Upload key
env:
KEYSTORE_B64: ${{ secrets.ANDROID_UPLOAD_KEYSTORE_B64 }}
run: printf '%s' "$KEYSTORE_B64" | base64 -d > "$RUNNER_TEMP/upload.jks"
- name: Build the signed App Bundle
working-directory: client/android
env:
ORG_GRADLE_PROJECT_trekKeystorePassword: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ORG_GRADLE_PROJECT_trekKeyAlias: ${{ secrets.ANDROID_KEY_ALIAS }}
ORG_GRADLE_PROJECT_trekKeyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: >-
./gradlew bundleRelease --no-daemon
-PtrekVersionName=${{ steps.version.outputs.version }}
-PtrekVersionCode=${{ steps.version.outputs.build }}
-PtrekKeystore="$RUNNER_TEMP/upload.jks"
- uses: actions/upload-artifact@v6
with:
name: trek-android-${{ steps.version.outputs.version }}-${{ steps.version.outputs.build }}
path: client/android/app/build/outputs/bundle/release/app-release.aab
retention-days: 30
# Play only accepts API uploads once one bundle was uploaded by hand
# (wiki/Native-Apps.md). `draft` until the app has left draft in the console.
- name: Upload to Google Play
uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
packageName: com.liketrek.trek
releaseFiles: client/android/app/build/outputs/bundle/release/app-release.aab
track: ${{ inputs.track || 'internal' }}
status: draft
- name: Remove the upload key
if: always()
run: rm -f "$RUNNER_TEMP/upload.jks"
ios:
if: github.repository == 'liketrek/TREK'
runs-on: macos-15
timeout-minutes: 60
environment: store
steps:
- uses: actions/checkout@v6
# App Store uploads must be built with Xcode 26; the image's default is older.
- name: Select Xcode 26
run: |
XCODE=$(ls -d /Applications/Xcode_26*.app | sort -V | tail -1)
test -n "$XCODE"
sudo xcode-select -s "$XCODE"
xcodebuild -version
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: npm
- name: Version and build number
id: version
run: |
echo "version=$(node -p "require('./client/package.json').version.split('-')[0]")" >> "$GITHUB_OUTPUT"
echo "build=$((GITHUB_RUN_NUMBER + 1000))" >> "$GITHUB_OUTPUT"
- name: Install
run: npm ci --workspace=shared --workspace=client
- name: Build shared
run: npm run build --workspace=shared
- name: Build the address screen and sync it into the shell
working-directory: client
run: npm run cap:sync -- ios
- name: App Store Connect API key
env:
ASC_KEY_P8_B64: ${{ secrets.ASC_KEY_P8_B64 }}
run: printf '%s' "$ASC_KEY_P8_B64" | base64 -d > "$RUNNER_TEMP/AuthKey.p8"
- name: Archive
working-directory: client/ios/App
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
run: >-
xcodebuild archive
-project App.xcodeproj
-scheme App
-configuration Release
-destination 'generic/platform=iOS'
-archivePath "$RUNNER_TEMP/TREK.xcarchive"
MARKETING_VERSION=${{ steps.version.outputs.version }}
CURRENT_PROJECT_VERSION=${{ steps.version.outputs.build }}
DEVELOPMENT_TEAM="$APPLE_TEAM_ID"
-allowProvisioningUpdates
-authenticationKeyPath "$RUNNER_TEMP/AuthKey.p8"
-authenticationKeyID "$ASC_KEY_ID"
-authenticationKeyIssuerID "$ASC_ISSUER_ID"
# ExportOptions.plist says destination=upload, so this step sends the build
# to App Store Connect, where it shows up in TestFlight after processing.
- name: Export and upload to App Store Connect
working-directory: client/ios/App
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
run: >-
xcodebuild -exportArchive
-archivePath "$RUNNER_TEMP/TREK.xcarchive"
-exportOptionsPlist ../ExportOptions.plist
-exportPath "$RUNNER_TEMP/export"
-allowProvisioningUpdates
-authenticationKeyPath "$RUNNER_TEMP/AuthKey.p8"
-authenticationKeyID "$ASC_KEY_ID"
-authenticationKeyIssuerID "$ASC_ISSUER_ID"
- name: Remove the API key
if: always()
run: rm -f "$RUNNER_TEMP/AuthKey.p8"
-132
View File
@@ -1,132 +0,0 @@
name: Native apps
permissions:
contents: read
# Builds the iOS and Android shells whenever they, their address screen or the
# Capacitor plugins change. Nobody on the team has to own a Mac for the Swift
# side to be compiled on every change, and every run leaves a debug APK to
# install on a phone.
#
# Unsigned on purpose: signing and store uploads need secrets that fork PRs do
# not get, and belong in a release workflow of their own.
on:
pull_request:
paths:
- 'client/android/**'
- 'client/ios/**'
- 'client/native-shell/**'
- 'client/src/native/trekShell.ts'
- 'client/src/native/serverAddress.ts'
- 'client/capacitor.config.ts'
- 'client/vite.native.config.js'
- 'client/scripts/check-store-metadata.mjs'
- 'client/fastlane/**'
- 'client/package.json'
- 'package-lock.json'
- 'shared/src/i18n/*/native.ts'
- '.github/workflows/native.yml'
workflow_dispatch:
concurrency:
group: native-${{ github.ref }}
cancel-in-progress: true
jobs:
# The store texts fit their limits and every listing image has its size and
# no alpha channel (client/fastlane/README.md). Plain Node, no install.
metadata:
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Check the store listings
run: node client/scripts/check-store-metadata.mjs
android:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: npm
- uses: actions/setup-java@v5
with:
distribution: temurin
java-version: 21
cache: gradle
- name: Install
run: npm ci --workspace=shared --workspace=client
- name: Build shared
run: npm run build --workspace=shared
- name: Build the address screen and sync it into the shells
working-directory: client
run: npm run cap:sync -- android
# The release build runs R8 with the keep rules in proguard-rules.pro, so a
# rule that goes missing fails here rather than in the store build. It is
# unsigned: the upload key only exists in native-release.yml.
- name: Assemble debug and release APKs
working-directory: client/android
run: ./gradlew assembleDebug assembleRelease lintDebug --no-daemon
- uses: actions/upload-artifact@v6
with:
name: trek-android-debug
path: client/android/app/build/outputs/apk/debug/app-debug.apk
retention-days: 14
ios:
runs-on: macos-15
timeout-minutes: 40
steps:
- uses: actions/checkout@v6
# App Store uploads must be built with Xcode 26; the image's default is older.
- name: Select Xcode 26
run: |
XCODE=$(ls -d /Applications/Xcode_26*.app | sort -V | tail -1)
test -n "$XCODE"
sudo xcode-select -s "$XCODE"
xcodebuild -version
- name: Check the property lists
run: plutil -lint client/ios/App/App/Info.plist client/ios/ExportOptions.plist
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: npm
- name: Install
run: npm ci --workspace=shared --workspace=client
- name: Build shared
run: npm run build --workspace=shared
- name: Build the address screen and sync it into the shells
working-directory: client
run: npm run cap:sync -- ios
- name: Build for the simulator
working-directory: client/ios/App
run: |
xcodebuild build \
-project App.xcodeproj \
-scheme App \
-configuration Debug \
-sdk iphonesimulator \
-destination 'generic/platform=iOS Simulator' \
CODE_SIGNING_ALLOWED=NO
+144
View File
@@ -0,0 +1,144 @@
name: Registration dialect
# Asks the TREK services issuer for the registration dialect of one version and
# checks it, for docker.yml (a stable release commits it with the version bump)
# and docker-dev.yml (a prerelease only builds it into its image).
#
# The issuer's answer runs here, in a job that holds no write token and keeps
# no git credentials; the token for the issuer is in the environment of the
# one step that fetches. The answer is held to what a dialect is
# (scripts/ci/check-registration-dialect.mjs), then type-checked and linted
# the way TREK's CI would see it once it is on main and dev. Only a module that
# passes all of it goes on, as the artifact `registration-dialect`; without
# one the caller decides what ships. This workflow never fails its caller.
on:
workflow_call:
inputs:
version:
description: 'The version the dialect is for, X.Y.Z or X.Y.Z-pre.N'
type: string
required: true
ref:
description: 'The commit whose checker, types and lint config the dialect is checked with'
type: string
required: true
secrets:
TREK_DIALECT_TOKEN:
required: false
permissions:
contents: read
jobs:
dialect:
runs-on: ubuntu-24.04
timeout-minutes: 20
# A failed or timed-out run leaves no artifact, which the caller already
# handles; it must not fail the release around it.
continue-on-error: true
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: package-lock.json
# The only step that sees the token. The URL is fixed; the answer is
# bounded in time and size and goes to a file nothing runs yet.
- name: Fetch the dialect from the issuer
id: fetch
timeout-minutes: 3
env:
TREK_DIALECT_TOKEN: ${{ secrets.TREK_DIALECT_TOKEN }}
VERSION: ${{ inputs.version }}
run: |
set -uo pipefail
out="$RUNNER_TEMP/registration-dialect.ts"
if [ -z "${TREK_DIALECT_TOKEN:-}" ]; then
echo "::warning::TREK_DIALECT_TOKEN is not set; the issuer was not asked for a dialect for $VERSION."
exit 0
fi
status=$(curl --silent --show-error --proto '=https' --tlsv1.2 --max-time 30 --retry 2 --retry-delay 5 \
--max-filesize 262144 -H "Authorization: Bearer $TREK_DIALECT_TOKEN" -H 'Content-Type: application/json' \
--data "{\"version\":\"$VERSION\"}" -o "$out" -w '%{http_code}' https://places.liketrek.com/v1/dialects) \
|| status=000
if [ "$status" != 200 ] && [ "$status" != 201 ]; then
echo "::warning::The issuer handed out no registration dialect for $VERSION (HTTP $status)."
rm -f "$out"
exit 0
fi
if [ "$(wc -c < "$out")" -gt 262144 ]; then
echo "::warning::The issuer's registration dialect for $VERSION is larger than a dialect."
rm -f "$out"
exit 0
fi
echo "file=$out" >> "$GITHUB_OUTPUT"
# A module that computes for long is refused by the clock as well.
- name: Check the dialect
id: check
if: steps.fetch.outputs.file != ''
timeout-minutes: 2
env:
FILE: ${{ steps.fetch.outputs.file }}
VERSION: ${{ inputs.version }}
run: |
set -uo pipefail
if ! node scripts/ci/check-registration-dialect.mjs "$FILE" "$VERSION"; then
echo "::warning::The issuer's registration dialect for $VERSION failed its check."
exit 0
fi
echo "What changes against the dialect in the tree:"
git diff --no-index -- server/src/nest/service-identity/registration-dialect.ts "$FILE" || true
echo "ok=true" >> "$GITHUB_OUTPUT"
- name: Install the toolchain
if: steps.check.outputs.ok == 'true'
timeout-minutes: 8
run: |
npm ci --ignore-scripts --no-audit --no-fund
npm run build --workspace=shared
# The bump commit skips CI, so this is where the module meets the strict
# config (lint:strict lets a new file have no strict errors) and ESLint
# with --max-warnings 0 (lint:warnings starts a new file at none) before
# it lands on main.
- name: Type-check and lint the dialect
id: types
if: steps.check.outputs.ok == 'true'
timeout-minutes: 5
working-directory: server
env:
FILE: ${{ steps.fetch.outputs.file }}
VERSION: ${{ inputs.version }}
run: |
set -uo pipefail
cp "$FILE" src/nest/service-identity/registration-dialect.ts
# TypeScript 6 includes no @types by default and the dialect imports nothing
# from node, so checked on its own it would not know Buffer: name node here.
printf '%s\n' '{"extends": "./tsconfig.strict.json", "compilerOptions": {"types": ["node"]}, "include": [],' \
'"files": ["src/nest/service-identity/registration-dialect.ts"]}' > tsconfig.dialect-check.json
if ! npx tsc -p tsconfig.dialect-check.json --noEmit --pretty false; then
echo "::warning::The issuer's registration dialect for $VERSION does not type-check under the strict config."
exit 0
fi
if ! npx eslint --max-warnings 0 src/nest/service-identity/registration-dialect.ts; then
echo "::warning::The issuer's registration dialect for $VERSION does not pass ESLint."
exit 0
fi
echo "ok=true" >> "$GITHUB_OUTPUT"
- name: Hand the dialect on
if: steps.types.outputs.ok == 'true'
uses: actions/upload-artifact@v6
with:
name: registration-dialect
path: ${{ steps.fetch.outputs.file }}
if-no-files-found: error
retention-days: 1
+2 -2
View File
@@ -74,12 +74,12 @@ jobs:
load: true
tags: trek:scan
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
- uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: docker/scout-action@221e7f4860634eeb1579e3bd7ca232e577bd1864 # v1.26.0
- uses: docker/scout-action@e7e2a72bf1485d10a58f30696b6c513b261ce4fa # v1.29.0
with:
command: cves
image: trek:scan
+42 -3
View File
@@ -1100,7 +1100,7 @@ jobs:
path: plugin-sdk/coverage/
- name: Scan
uses: SonarSource/sonarqube-scan-action@d209202bc7d53ff1cc128f7f907dac145c9d6ae9 # v8.3.0
uses: SonarSource/sonarqube-scan-action@4d1dc0c91ee400dcf8a6dddf2f705ac36542e304 # v8.4.0
env:
# SonarQube Cloud is the scan action's default host, so no
# SONAR_HOST_URL — the token is a sonarcloud.io token for the
@@ -1161,7 +1161,7 @@ jobs:
docker run -d --name trek-ci -p 127.0.0.1:3000:3000 \
--read-only --tmpfs /tmp:noexec,nosuid,size=128m \
--security-opt no-new-privileges:true \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add KILL \
-v "$RUNNER_TEMP/trek/data:/app/data" \
-v "$RUNNER_TEMP/trek/uploads:/app/uploads" \
-e ENCRYPTION_KEY="$(openssl rand -hex 32)" \
@@ -1194,6 +1194,17 @@ jobs:
curl -sf http://127.0.0.1:3000/ | grep -q '<div id="root">' \
|| { echo "::error::/ does not serve the client"; exit 1; }
# Same capabilities as the shipped compose file: dumb-init has to be able
# to pass the stop signal on to node, or docker stop ends in SIGKILL (137).
- name: Stop gracefully
run: |
set -euo pipefail
docker stop -t 30 trek-ci >/dev/null
code="$(docker inspect -f '{{.State.ExitCode}}' trek-ci)"
[ "$code" = "0" ] || { echo "::error::docker stop ended with exit code $code, not a graceful shutdown"; exit 1; }
docker logs trek-ci 2>&1 | grep 'SIGTERM received' >/dev/null \
|| { echo "::error::the server never saw SIGTERM"; exit 1; }
- name: Container log
if: always()
run: docker logs trek-ci 2>&1 || true
@@ -1252,11 +1263,39 @@ jobs:
--set 'extraEnv[0].name=SESSION_DURATION' \
--set 'extraEnv[0].value=7d' \
--set 'extraEnvFrom[0].secretRef.name=trek-smtp' \
--set 'extraVolumes[0].name=kitinerary-extractors' \
--set 'extraVolumes[0].configMap.name=trek-kitinerary-extractors' \
--set 'extraVolumeMounts[0].name=kitinerary-extractors' \
--set 'extraVolumeMounts[0].mountPath=/usr/local/share/kitinerary/extractors' \
--set 'extraVolumeMounts[0].readOnly=true' \
--set probes.liveness=null \
> "$out/variants.yaml"
helm template trek charts/trek \
--set httpRoute.enabled=true \
--set 'httpRoute.parentRefs[0].name=gateway' \
> "$out/http-route.yaml"
helm template trek charts/trek \
--set podSecurityContext.runAsUser=1000 \
--set podSecurityContext.runAsGroup=1000 \
--set podSecurityContext.runAsNonRoot=true \
--set podSecurityContext.seccompProfile.type=RuntimeDefault \
--set securityContext.allowPrivilegeEscalation=false \
--set 'securityContext.capabilities.drop[0]=ALL' \
> "$out/non-root.yaml"
helm template trek charts/trek \
--set ingress.enabled=true \
--set fullnameOverride=custom-trek \
--set service.port=8080 \
--set-json 'httpRoute={"enabled":true,"parentRefs":[{"name":"gateway","namespace":"gateway-system","sectionName":"https"}],"hostnames":["trek.example.com"],"paths":["/","/api"],"annotations":{"example.com/note":"custom route"},"timeouts":{"request":"0s","backendRequest":"0s"}}' \
> "$out/http-route-variants.yaml"
- name: Check the manifests against the Kubernetes schemas
run: kubeconform -strict -summary "$RUNNER_TEMP"/rendered/*.yaml
# The pinned Gateway API schema adds HTTPRoute validation without skipping unknown resources.
run: >-
kubeconform -strict -summary
-schema-location default
-schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/041baf4c1d3740ca21461cab765e7432ecd073ef/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json'
"$RUNNER_TEMP"/rendered/*.yaml
- name: Check every compose file
run: |
+2 -1
View File
@@ -91,7 +91,8 @@ ENV KITINERARY_EXTRACTOR_PATH=/usr/local/bin/kitinerary-extractor
COPY --chown=node:node --from=server-builder /app/server/dist ./server/dist
# Runtime data assets read from server/assets at runtime: airports.json (flight
# transport search) and atlas/*.geojson.gz (Atlas country/region map). The build
# transport search) and atlas/*.geojson.gz + atlas/admin1/*.geojson.gz (Atlas
# country/region map). The build
# only emits dist, so these must be copied explicitly or the features silently
# degrade to empty in the image.
COPY --chown=node:node --from=server-builder /app/server/assets ./server/assets
+3 -2
View File
@@ -5,8 +5,9 @@ TREK bundles and uses third-party data that requires attribution.
## geoBoundaries — country & sub-national boundaries
The Atlas map's administrative boundaries (admin-0 countries and admin-1
provinces/counties), shipped at `server/assets/atlas/admin0.geojson.gz` and
`server/assets/atlas/admin1.geojson.gz` and generated by
provinces/counties), shipped at `server/assets/atlas/admin0.geojson.gz`,
`server/assets/atlas/admin1.geojson.gz` and `server/assets/atlas/admin1/<ISO_A2>.geojson.gz`
(the same regions, one file per country) and generated by
`server/scripts/build-atlas-geo.mjs`, are derived from **geoBoundaries**.
> Runfola, D. et al. (2020) geoBoundaries: A global database of political
+51 -26
View File
@@ -6,14 +6,9 @@
<img src="docs/logo-trek-dark.svg" alt="TREK" height="96" />
</picture>
<br />
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/subtitle-light.png" />
<source media="(prefers-color-scheme: light)" srcset="docs/subtitle-dark.png" />
<img src="docs/subtitle-dark.png" alt="your trip. your plan." height="28" />
</picture>
### Every part of your trip. One app you own.
A self-hosted, real-time collaborative travel planner — with maps, budgets, packing lists, a journal, and AI built in.
TREK has grown from a travel planner into a whole ecosystem for trips: plan together in real time on a live map, build hikes, tours and road trips, keep bookings and shared costs in order, pack, write the journal, lay out the photo book and hand the busy work to the built-in AI. Self-hosted, on the web, on your phone and offline.
<br />
@@ -32,11 +27,16 @@ A self-hosted, real-time collaborative travel planner — with maps, budgets, pa
<a href="https://ko-fi.com/mauriceboe"><img alt="Ko-fi" src="https://img.shields.io/badge/Ko--fi-support-FF5E5B?style=for-the-badge" /></a>
&nbsp;
<a href="https://www.buymeacoffee.com/mauriceboe"><img alt="BMAC" src="https://img.shields.io/badge/BMAC-support-FFDD00?style=for-the-badge" /></a>
<br />
<a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-AGPL_v3-6B7280?style=flat-square" /></a>
<a href="https://github.com/liketrek/TREK/releases"><img alt="Latest Release" src="https://img.shields.io/github/v/release/liketrek/trek?include_prereleases&style=flat-square&color=6B7280" /></a>
<a href="https://hub.docker.com/r/mauriceboe/trek"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/mauriceboe/trek?style=flat-square&color=6B7280" /></a>
<a href="https://github.com/liketrek/TREK"><img alt="Stars" src="https://img.shields.io/github/stars/liketrek/trek?style=flat-square&color=6B7280" /></a>
&nbsp;
<a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/License-AGPL_v3-6B7280?style=for-the-badge" /></a>
&nbsp;
<a href="https://github.com/liketrek/TREK/releases"><img alt="Latest Release" src="https://img.shields.io/github/v/release/liketrek/trek?include_prereleases&style=for-the-badge&label=Release&color=6B7280" /></a>
&nbsp;
<a href="https://hub.docker.com/r/mauriceboe/trek"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/mauriceboe/trek?style=for-the-badge&label=Docker%20pulls&color=6B7280" /></a>
&nbsp;
<a href="https://github.com/liketrek/TREK"><img alt="Stars" src="https://img.shields.io/github/stars/liketrek/trek?style=for-the-badge&label=Stars&color=6B7280" /></a>
<sub>PUBLIC SERVICES PROUDLY SPONSORED BY</sub><br /><a href="https://cloud.dataforest.net/en?mtm_campaign=trek"><picture><source media="(prefers-color-scheme: dark)" srcset="docs/sponsors/dataforest-light.svg" /><source media="(prefers-color-scheme: light)" srcset="docs/sponsors/dataforest-dark.svg" /><img src="docs/sponsors/dataforest-dark.svg" alt="dataforest cloud" height="30" /></picture></a>
</div>
---
@@ -182,7 +182,7 @@ Most of what follows is an addon an admin switches on or off. Lists, Costs, Docu
#### 📱 Mobile and offline
- **Apps**: TREK Travel Planner for iPhone, iPad and Android connects to any TREK server, and the browser installs TREK as a standalone app with a themed status bar too
- **Installable**: iOS and Android straight from the browser, no App Store, standalone with a themed status bar
- **Phone layouts**: their own screens for every part of the app, with a bottom bar and dashboard widgets the user orders
- **Offline reads**: the app shell is precached and trips, places, tours, files and nearby places live in a per-user IndexedDB store, so a trip opens with no network
- **Offline writes**: changes queue and replay with an `X-Idempotency-Key`, and a stale edit waits for a keep-mine or keep-theirs choice
@@ -200,7 +200,7 @@ Most of what follows is an addon an admin switches on or off. Lists, Costs, Docu
- **Admin panel**: users and the permission matrix, defaults for new users, categories and templates, addons, plugins, storage, notifications, MCP tokens, backups, audit log and releases
- **Backups and storage**: scheduled backups with retention that restore their own secrets, and local or S3-compatible storage per content category with replicas
- **Appearance**: light, dark or follow the OS, seven colour schemes plus a custom accent, transparency, density, reduced motion and text size
- **27 languages**: en, de, es, fr, it, nl, hu, ru, zh, zh-TW, pl, cs, ar (right to left), br, id, tr, ja, ko, uk, gr, sv, vi, ca, az, et, sk, th
- **28 languages**: en, de, es, fr, it, nl, hu, ru, zh, zh-TW, pl, cs, ar (right to left), he (right to left), br, id, tr, ja, ko, uk, gr, sv, vi, ca, az, et, sk, th
- **Help center**: a ? in the top bar with 381 step-by-step guides with screenshots, searched together with the wiki that ships in the image
</td>
@@ -214,7 +214,39 @@ Most of what follows is an addon an admin switches on or off. Lists, Costs, Docu
<div align="center">
<a href="docs/screenshots/showcase-desktop-1.webp"><img src="docs/screenshots/showcase-desktop-1.webp" alt="Atlas · dashboard · trip planner" width="100%" /></a>
<a href="docs/screenshots/showcase-mobile.webp"><img src="docs/screenshots/showcase-mobile.webp" alt="Mobile PWA · dashboard, day plan, map, costs" width="100%" /></a>
<a href="docs/screenshots/showcase-desktop-2.webp"><img src="docs/screenshots/showcase-desktop-2.webp" alt="Collections · journey journal · costs" width="100%" /></a>
</div>
<br />
## Powered by [dataforest cloud](https://cloud.dataforest.net/en?mtm_campaign=trek)
<div align="center">
<br />
<a href="https://cloud.dataforest.net/en?mtm_campaign=trek">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/sponsors/dataforest-light.svg" />
<source media="(prefers-color-scheme: light)" srcset="docs/sponsors/dataforest-dark.svg" />
<img src="docs/sponsors/dataforest-dark.svg" alt="dataforest cloud" height="60" />
</picture>
</a>
<br />
<br />
**Every TREK installation in the world searches through servers that [dataforest cloud](https://cloud.dataforest.net/en?mtm_campaign=trek) sponsors.**
The place index with over 88 million places, the address search in every TREK language and the planet-wide routing all run on [dataforest cloud](https://cloud.dataforest.net/en?mtm_campaign=trek) in Frankfurt, Germany, free of charge for everyone who self-hosts TREK. Without this sponsorship, these services would not exist in this form.
Thank you, [dataforest](https://cloud.dataforest.net/en?mtm_campaign=trek), for backing open source this generously. 💚
<br />
<a href="https://cloud.dataforest.net/en?mtm_campaign=trek"><img alt="Visit dataforest cloud" src="https://img.shields.io/badge/Visit-dataforest_cloud-02C382?style=for-the-badge" /></a>
Looking for a server for your own TREK? [dataforest cloud](https://cloud.dataforest.net/en?mtm_campaign=trek) is where TREK's own services live.
</div>
<br />
@@ -244,6 +276,9 @@ Open `http://localhost:3000`, or `http://<server-ip>:3000` from another machine.
`COOKIE_SECURE=false` is only for trying TREK over plain HTTP: without it the browser drops the session cookie and the login does not stick. Once TREK sits behind HTTPS (see [Reverse Proxy](#reverse-proxy)), remove it.
> [!TIP]
> **No server yet?** TREK's own public services run on [**dataforest cloud**](https://cloud.dataforest.net/en?mtm_campaign=trek), our infrastructure sponsor. A small seed is plenty for a TREK instance and is ready within a minute.
<div align="center">
<a href="#docker-compose-production">Docker Compose</a>&nbsp;&nbsp;·&nbsp;&nbsp;<a href="#helm-kubernetes">Helm / Kubernetes</a>&nbsp;&nbsp;·&nbsp;&nbsp;<a href="#install-as-app-pwa">Install as PWA</a>&nbsp;&nbsp;·&nbsp;&nbsp;<a href="#reverse-proxy">Reverse Proxy</a>
@@ -300,7 +335,7 @@ See [`charts/README.md`](https://github.com/liketrek/TREK/blob/main/charts/READM
<h2 id="install-as-app-pwa">Install as App (PWA)</h2>
TREK works as a Progressive Web App, and the TREK Travel Planner app for iPhone, iPad and Android connects to the same server ([Native Apps](https://github.com/liketrek/TREK/wiki/Native-Apps)). To install the web app instead:
TREK works as a Progressive Web App — no App Store needed.
1. Open TREK in the browser (HTTPS required)
2. **iOS**: Share ▸ *Add to Home Screen*
@@ -398,16 +433,6 @@ Caddy handles TLS and WebSockets automatically.
Every variable, its default and what it does: see
[Environment Variables](https://github.com/liketrek/TREK/wiki/Environment-Variables).
## Star History
<a href="https://www.star-history.com/?repos=liketrek%2FTREK&type=date&legend=bottom-right">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=liketrek/TREK&type=date&theme=dark&legend=bottom-right&sealed_token=tW9agibKugOyPk0yPURgSdnizM7K72zbJENGD9iyk24ZcwYHtCUn-hk9jh1gdacO0O7xJ3Io7LaNrwYD3mChzcpzZIqDC8_HevYG1qnT0XNTmN6Ez4UZrQ" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=liketrek/TREK&type=date&legend=bottom-right&sealed_token=tW9agibKugOyPk0yPURgSdnizM7K72zbJENGD9iyk24ZcwYHtCUn-hk9jh1gdacO0O7xJ3Io7LaNrwYD3mChzcpzZIqDC8_HevYG1qnT0XNTmN6Ez4UZrQ" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=liketrek/TREK&type=date&legend=bottom-right&sealed_token=tW9agibKugOyPk0yPURgSdnizM7K72zbJENGD9iyk24ZcwYHtCUn-hk9jh1gdacO0O7xJ3Io7LaNrwYD3mChzcpzZIqDC8_HevYG1qnT0XNTmN6Ez4UZrQ" />
</picture>
</a>
## Data sources
The Atlas map's country and sub-national (province/county) boundaries come from
+6 -3
View File
@@ -5,9 +5,10 @@ This is a minimal Helm chart for deploying the TREK app.
## Features
- Deploys the TREK container
- Exposes port 3000 via Service
- Optional persistent storage for `/app/data` and `/app/uploads`
- Optional persistent storage for `/app/data` and `/app/uploads`, plus extra volumes and mounts
- Configurable environment variables and secrets
- Optional generic Ingress support
- Optional Gateway API HTTPRoute support
- Configurable health checks (liveness/readiness probes) on `/api/health`
## Helm Repository
@@ -27,7 +28,7 @@ helm install trek trek/trek
Or install directly from the local chart:
```sh
helm install trek ./chart \
helm install trek ./charts/trek \
--set ingress.enabled=true \
--set ingress.hosts[0].host=yourdomain.com
```
@@ -47,7 +48,7 @@ See `values.yaml` for more options.
- `JWT_SECRET` is managed entirely by the server — auto-generated into the data PVC on first start and rotatable via the admin panel (Settings → Danger Zone). No Helm configuration needed.
- `ENCRYPTION_KEY` encrypts stored secrets (API keys, MFA, SMTP, OIDC) at rest. Recommended: set via `secretEnv.ENCRYPTION_KEY` or `existingSecret`. If left empty, the server falls back automatically: existing installs use `data/.jwt_secret` (no action needed on upgrade); fresh installs auto-generate a key persisted to the data PVC.
- Liveness and readiness probes are configurable under `probes`. Overrides are merged over the defaults, so `--set probes.liveness.initialDelaySeconds=60` keeps the `httpGet` handler. When switching to `exec` or `tcpSocket`, clear `httpGet` in the same override. Set `probes.liveness: null` to remove a probe.
- If using ingress, you must manually keep `env.ALLOWED_ORIGINS` and `ingress.hosts` in sync to ensure CORS works correctly. The chart does not sync these automatically.
- If exposing TREK through an Ingress or HTTPRoute, set `env.ALLOWED_ORIGINS` and `env.APP_URL` to the public URL. The chart does not sync these automatically.
- Set `env.ALLOW_INTERNAL_NETWORK: "true"` if Immich or other integrated services are hosted on a private/RFC-1918 address (e.g. a pod on the same cluster or a NAS on your LAN). Loopback (`127.x`) and link-local/metadata addresses (`169.254.x`) remain blocked regardless.
- Set `env.ALLOW_LINK_LOCAL_IPS` to a single link-local address (e.g. `"169.254.1.2"`) only when a service TREK needs sits behind one, such as a host gateway. Cloud metadata addresses (`169.254.169.x`, `169.254.170.x`) cannot be listed.
- `FORCE_HTTPS` is optional. Set `env.FORCE_HTTPS: "true"` only when ingress (or another proxy) terminates TLS. It enables HTTPS redirects, HSTS, CSP `upgrade-insecure-requests`, and forces the session cookie `secure` flag. Requires `TRUST_PROXY` to be set.
@@ -55,3 +56,5 @@ See `values.yaml` for more options.
- `COOKIE_SECURE` is auto-derived (on when `NODE_ENV=production` or `FORCE_HTTPS=true`). Set `env.COOKIE_SECURE: "false"` only during local testing without TLS. **Not recommended for production.**
- Web Push needs no chart settings: TREK generates its key pair on first start and keeps it in the database on the data PVC. To bring your own pair, set `env.VAPID_PUBLIC_KEY` and `secretEnv.VAPID_PRIVATE_KEY` together (and optionally `env.VAPID_SUBJECT`); changing the pair later means every device has to turn push on again.
- Set `env.OIDC_DISCOVERY_URL` to override the auto-constructed OIDC discovery endpoint. Required for providers (e.g. Authentik) that expose it at a non-standard path.
- `podSecurityContext` defaults to `fsGroup: 1000` with `fsGroupChangePolicy: OnRootMismatch`, and `securityContext` (the container's) is empty. The container then starts as root, hands any file under `/app/data` and `/app/uploads` that is not owned by `node` yet to `node`, and drops to `node`. To run as `node` from the start, for example under the `restricted` Pod Security Standard, set `podSecurityContext.runAsUser: 1000` and `runAsGroup: 1000`; the image then skips that step and the privilege drop. See the example in `values.yaml`.
- Use `extraVolumes` and `extraVolumeMounts` to add your own volumes to the pod. To give the booking import more KItinerary extractors, mount a ConfigMap at `/usr/local/share/kitinerary/extractors`, with one `.json` and one `.js` key for each extractor; see the example in `values.yaml`.
+2 -2
View File
@@ -1,5 +1,5 @@
apiVersion: v2
name: trek
version: 5.0.0
version: 5.1.0
description: Minimal Helm chart for TREK app
appVersion: "5.0.0"
appVersion: "5.1.0"
+9
View File
@@ -31,3 +31,12 @@
The other values for that volume (size, storageClassName, annotations) are then ignored.
- With `persistence.enabled=false` the volumes use an emptyDir — storage is ephemeral and is lost
when the pod restarts. Use only for testing.
{{- if .Values.httpRoute.enabled }}
6. HTTPRoute:
- The chart creates an HTTPRoute v1 to the existing TREK Service. The Gateway, GatewayClass,
listeners, TLS certificates and allowedRoutes are managed by your cluster operator.
- Ingress and HTTPRoute may be enabled together.
- Timeout fields are Standard Channel in Gateway API v1.2, but controller support varies. Omit
them to use controller defaults; `0s` disables the corresponding timeout where supported.
{{- end }}
+15
View File
@@ -112,6 +112,21 @@ data:
{{- if .Values.env.TREK_PLACES_URL }}
TREK_PLACES_URL: {{ .Values.env.TREK_PLACES_URL | quote }}
{{- end }}
{{- if .Values.env.TREK_PLACES_CONTRIBUTE }}
TREK_PLACES_CONTRIBUTE: {{ .Values.env.TREK_PLACES_CONTRIBUTE | quote }}
{{- end }}
{{- if .Values.env.TREK_SERVICES }}
TREK_SERVICES: {{ .Values.env.TREK_SERVICES | quote }}
{{- end }}
{{- if .Values.env.TREK_SERVICES_HOSTS }}
TREK_SERVICES_HOSTS: {{ .Values.env.TREK_SERVICES_HOSTS | quote }}
{{- end }}
{{- if .Values.env.TREK_PHOTON_URL }}
TREK_PHOTON_URL: {{ .Values.env.TREK_PHOTON_URL | quote }}
{{- end }}
{{- if .Values.env.TREK_VALHALLA_URL }}
TREK_VALHALLA_URL: {{ .Values.env.TREK_VALHALLA_URL | quote }}
{{- end }}
{{- if .Values.env.GOOGLE_LIST_DATA_BASE }}
GOOGLE_LIST_DATA_BASE: {{ .Values.env.GOOGLE_LIST_DATA_BASE | quote }}
{{- end }}
+19 -1
View File
@@ -29,12 +29,18 @@ spec:
- name: {{ .name }}
{{- end }}
{{- end }}
{{- with .Values.podSecurityContext }}
securityContext:
fsGroup: 1000
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: trek
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- with .Values.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
@@ -85,6 +91,12 @@ spec:
name: {{ default (printf "%s-secret" (include "trek.fullname" .)) .Values.existingSecret }}
key: PLACES_API_KEY
optional: true
- name: TREK_SERVICES_KEY
valueFrom:
secretKeyRef:
name: {{ default (printf "%s-secret" (include "trek.fullname" .)) .Values.existingSecret }}
key: TREK_SERVICES_KEY
optional: true
- name: AMAP_API_KEY
valueFrom:
secretKeyRef:
@@ -135,6 +147,9 @@ spec:
mountPath: /app/data
- name: uploads
mountPath: /app/uploads
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with (default dict .Values.probes).startup }}
startupProbe:
{{- toYaml . | nindent 12 }}
@@ -162,3 +177,6 @@ spec:
{{- else }}
emptyDir: {}
{{- end }}
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
+39
View File
@@ -0,0 +1,39 @@
{{- if .Values.httpRoute.enabled -}}
{{- if not .Values.httpRoute.parentRefs }}
{{- fail "httpRoute.parentRefs must include at least one parent" }}
{{- end }}
{{- if not .Values.httpRoute.paths }}
{{- fail "httpRoute.paths must include at least one path" }}
{{- end }}
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: {{ include "trek.fullname" . }}
labels:
app: {{ include "trek.name" . }}
{{- with .Values.httpRoute.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
parentRefs:
{{- toYaml .Values.httpRoute.parentRefs | nindent 4 }}
{{- with .Values.httpRoute.hostnames }}
hostnames:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
{{- range $path := .Values.httpRoute.paths }}
- matches:
- path:
type: PathPrefix
value: {{ $path | quote }}
backendRefs:
- name: {{ include "trek.fullname" $ }}
port: {{ $.Values.service.port }}
{{- with $.Values.httpRoute.timeouts }}
timeouts:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- end }}
+6
View File
@@ -23,6 +23,9 @@ data:
{{- if .Values.secretEnv.PLACES_API_KEY }}
PLACES_API_KEY: {{ .Values.secretEnv.PLACES_API_KEY | b64enc | quote }}
{{- end }}
{{- if .Values.secretEnv.TREK_SERVICES_KEY }}
TREK_SERVICES_KEY: {{ .Values.secretEnv.TREK_SERVICES_KEY | b64enc | quote }}
{{- end }}
{{- if .Values.secretEnv.AMAP_API_KEY }}
AMAP_API_KEY: {{ .Values.secretEnv.AMAP_API_KEY | b64enc | quote }}
{{- end }}
@@ -77,6 +80,9 @@ stringData:
{{- if .Values.secretEnv.PLACES_API_KEY }}
PLACES_API_KEY: {{ .Values.secretEnv.PLACES_API_KEY | quote }}
{{- end }}
{{- if .Values.secretEnv.TREK_SERVICES_KEY }}
TREK_SERVICES_KEY: {{ .Values.secretEnv.TREK_SERVICES_KEY | quote }}
{{- end }}
{{- if .Values.secretEnv.AMAP_API_KEY }}
AMAP_API_KEY: {{ .Values.secretEnv.AMAP_API_KEY | quote }}
{{- end }}
+84 -3
View File
@@ -10,6 +10,34 @@ image:
# processes on one host, which network filesystems do not provide.
updateStrategy: Recreate
# Pod-level security context. fsGroup gives the node user (uid/gid 1000) group access
# to the data and uploads volumes; OnRootMismatch lets the kubelet skip its recursive
# permission walk once the volume root already matches, so a big PVC does not hold up
# every start.
#
# By default the container starts as root, hands any files that are not node's yet to
# node, and then drops to node itself. To start it as node straight away (needed under
# the "restricted" Pod Security Standard), add the user here and lock the container
# down below; the image then skips the ownership step and the privilege drop:
# podSecurityContext:
# fsGroup: 1000
# fsGroupChangePolicy: OnRootMismatch
# runAsUser: 1000
# runAsGroup: 1000
# runAsNonRoot: true
# seccompProfile:
# type: RuntimeDefault
# securityContext:
# allowPrivilegeEscalation: false
# capabilities:
# drop: [ALL]
podSecurityContext:
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
# Security context of the trek container itself. Empty by default.
securityContext: {}
# Optional image pull secrets for private registries
imagePullSecrets: []
# - name: my-registry-secret
@@ -36,7 +64,7 @@ env:
# Browser/OS language is auto-detected first; this is the fallback when no match is found.
# Supported: ar, az, br, ca, cs, de, en, es, et, fr, gr, hu, id, it, ja, ko, nl, pl, ru, sk, sv, th, tr, uk, vi, zh, zh-TW
# ALLOWED_ORIGINS: ""
# NOTE: If using ingress, ensure env.ALLOWED_ORIGINS matches the domains in ingress.hosts for proper CORS configuration.
# NOTE: If using ingress or HTTPRoute, ensure env.ALLOWED_ORIGINS matches the public domains for proper CORS configuration.
# APP_URL: "https://trek.example.com"
# Public base URL of this instance. Required when OIDC is enabled — must match the redirect URI registered with your IdP.
# Also used as the base URL for links in email notifications and other external links.
@@ -92,7 +120,8 @@ env:
# Custom Nominatim base URL for every geocoding call the server makes (place search, reverse lookup, Atlas region
# fill). When set it REPLACES the public OpenStreetMap service, with no fallback, so the instance behind it needs
# the coverage you expect and has to speak the Nominatim API. TREK keeps its ~1 request per second spacing either
# way. Defaults to https://nominatim.openstreetmap.org.
# way, and search and reverse lookups no longer go to the TREK services' Photon. Defaults to
# https://nominatim.openstreetmap.org.
# OVERPASS_URL: ""
# Custom Overpass endpoint(s) for the map POI "explore" search, comma-separated. When set, REPLACES the bundled
# public mirrors — point it at an internal/self-hosted Overpass instance when the public mirrors are unreachable
@@ -112,6 +141,24 @@ env:
# Base URL of a copy of the Places API you run yourself; it has to answer the same /v1 API. Read from
# configuration rather than typed by a user, so an in-cluster address works without ALLOW_INTERNAL_NETWORK.
# Defaults to https://places.liketrek.com.
# TREK_PLACES_CONTRIBUTE: "off"
# Whether an admin may let this installation send the place details its users typed in (website, phone, e-mail,
# opening hours, description, picture) to the TREK Places API. "admin" leaves the choice to the admin panel, where
# it stays off until an admin agrees; "off" takes it away. Defaults to admin.
# TREK_SERVICES: "off"
# Places search identifies this installation with a key it registers by itself on first use and keeps in the
# database. "off" sends no key; searches then use the shared pool. A key issued to you goes under
# secretEnv.TREK_SERVICES_KEY. Defaults to auto.
# TREK_SERVICES_HOSTS: ""
# Comma-separated hosts that receive the installation key over https. Defaults to
# places.liketrek.com,photon.liketrek.com,valhalla.liketrek.com.
# TREK_PHOTON_URL: ""
# The address search (Photon) of the TREK services. With a key, search and reverse lookups ask it before
# Nominatim and fall back to Nominatim when it does not answer. Never asked when NOMINATIM_URL is set.
# Defaults to https://photon.liketrek.com.
# TREK_VALHALLA_URL: ""
# The Valhalla of the TREK services, which this server relays routing to unless the admin picks another
# routing service. The browser never talks to it. Defaults to https://valhalla.liketrek.com.
# AMAP_API_BASE: ""
# Send the Amap (高德地图) calls through an egress proxy, a cache or a gateway that speaks the same API instead of
# restapi.amap.com directly. The key itself is a credential and goes under secretEnv.AMAP_API_KEY, or into
@@ -164,6 +211,9 @@ secretEnv:
# with signing enabled, because such a key rejects every unsigned request. Leave empty
# otherwise. The endpoint itself is not a secret and is set under env.AMAP_API_BASE.
AMAP_API_SECRET: ""
# Optional key issued for this installation by the TREK services. Wins over the key TREK
# registers by itself and turns that registration off. Leave empty to let TREK register.
TREK_SERVICES_KEY: ""
# Optional Web Push key pair, private half (base64url, 32 bytes). Only used together with
# env.VAPID_PUBLIC_KEY; a pair whose halves do not belong together, or only one half, turns
# push off with an error in the log until it is fixed, and every device stays registered.
@@ -179,7 +229,7 @@ generateEncryptionKey: false
# If set, the chart renders no Secret of its own and instead reads every secret env
# var from this existing Kubernetes secret. Expected keys: ENCRYPTION_KEY (rename via
# existingSecretKey), ADMIN_EMAIL, ADMIN_PASSWORD, OIDC_CLIENT_SECRET,
# UNSPLASH_ACCESS_KEY, PLACES_API_KEY, AMAP_API_KEY, AMAP_API_SECRET,
# UNSPLASH_ACCESS_KEY, PLACES_API_KEY, AMAP_API_KEY, AMAP_API_SECRET, TREK_SERVICES_KEY,
# VAPID_PRIVATE_KEY, SMTP_PASS, MAPBOX_ACCESS_TOKEN, CARTO_API_KEY, DEMO_ADMIN_PASS.
# Missing keys are simply left unset (they are optional), so only provide the ones your
# setup needs.
@@ -242,6 +292,20 @@ extraEnv: []
# name: trek-smtp
extraEnvFrom: []
# Further volumes for the pod. The chart does not create the objects they name.
# extraVolumes:
# - name: kitinerary-extractors
# configMap:
# name: trek-kitinerary-extractors
extraVolumes: []
# Where the trek container mounts them. For example, KItinerary
# loads extractors for the booking import from this path:
# extraVolumeMounts:
# - name: kitinerary-extractors
# mountPath: /usr/local/share/kitinerary/extractors
# readOnly: true
extraVolumeMounts: []
resources:
requests:
cpu: 100m
@@ -262,3 +326,20 @@ ingress:
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
httpRoute:
enabled: false
annotations: {}
parentRefs: []
# - name: gateway
# namespace: gateway-system
# sectionName: https
hostnames: []
# - trek.example.com
paths:
- /
# Timeout fields are Standard Channel in Gateway API v1.2; controller support varies.
# Leave empty for controller defaults. Use "0s" to disable the corresponding timeout.
timeouts: {}
# request: "0s"
# backendRequest: "0s"
-4
View File
@@ -7,7 +7,3 @@ playwright/.cache/
# vite-plugin-pwa dev output (devOptions.enabled)
dev-dist/
e2e/help/media.env
# The native apps' address screen (npm run build:native), copied into
# android/ and ios/ by `cap sync`
native-dist/
-108
View File
@@ -1,108 +0,0 @@
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files and store credentials. The upload key and the Play service
# account live only in the release workflow's secrets and in the owner's
# offline backup, never in the repository.
*.jks
*.keystore
keystore.properties
*service-account*.json
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# Push is not wired up and the build does not apply the google-services plugin,
# so no CI secret is needed. Kept ignored so a stray Firebase config never lands
# in the repository.
google-services.json
app/google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Android Profiling
*.hprof
# Cordova plugins for Capacitor
capacitor-cordova-android-plugins
# Copied web assets
app/src/main/assets/public
# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml
-2
View File
@@ -1,2 +0,0 @@
/build/*
!/build/.npmkeep
-66
View File
@@ -1,66 +0,0 @@
apply plugin: 'com.android.application'
android {
namespace = "com.liketrek.trek"
compileSdk = rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.liketrek.trek"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
// The release workflow (.github/workflows/native-release.yml) passes both:
// the version from client/package.json and a build number that only grows.
// Local and PR builds fall back to these placeholders.
versionCode((project.findProperty('trekVersionCode') ?: '1') as Integer)
versionName(project.findProperty('trekVersionName') ?: '0.0.0-dev')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
// The upload key only exists in the release workflow, which decodes it from a
// secret and passes its path and passwords as Gradle properties. Every other
// build (local, fork, PR) stays unsigned and needs no secret.
signingConfigs {
release {
if (project.hasProperty('trekKeystore')) {
storeFile file(project.property('trekKeystore'))
storePassword project.property('trekKeystorePassword')
keyAlias project.property('trekKeyAlias')
keyPassword project.property('trekKeyPassword')
}
}
}
buildTypes {
release {
if (project.hasProperty('trekKeystore')) {
signingConfig signingConfigs.release
}
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation "androidx.browser:browser:$androidxBrowserVersion"
implementation project(':capacitor-android')
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
implementation project(':capacitor-cordova-android-plugins')
}
apply from: 'capacitor.build.gradle'
-25
View File
@@ -1,25 +0,0 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-app')
implementation project(':capacitor-filesystem')
implementation project(':capacitor-haptics')
implementation project(':capacitor-keyboard')
implementation project(':capacitor-share')
implementation project(':capacitor-splash-screen')
implementation project(':capacitor-status-bar')
}
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}
-38
View File
@@ -1,38 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# TREK's own classes: TrekShellPlugin is reached from JavaScript by name and its
# methods through reflection, so R8 must neither rename nor drop them.
-keep class com.liketrek.trek.** { *; }
# Every Capacitor plugin: the bridge finds plugins by their annotation and calls
# the annotated methods reflectively.
-keep @com.getcapacitor.annotation.CapacitorPlugin public class * {
@com.getcapacitor.PluginMethod public <methods>;
}
# The WebView bridge: Capacitor's message handler is called from JavaScript
# through @JavascriptInterface, and every plugin class is instantiated by name.
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
-keep public class * extends com.getcapacitor.Plugin
@@ -1,80 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
android:name=".MainActivity"
android:label="@string/title_activity_main"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Deep links.
The scheme is @string/custom_url_scheme (com.liketrek.trek).
launchMode is singleTask above, so a link delivers into the
running task via onNewIntent rather than starting a second copy
of the app on top of the first.
This is the return path for SSO and passkey sign-in: TrekShell
opens the server's login page in a Custom Tab, and after the
hand-off it redirects to com.liketrek.trek://auth?code=…, which
lands here and in TrekShellPlugin.handleOnNewIntent.
Not an App Link (autoVerify + https). App Links are verified
against a specific domain via assetlinks.json, and TREK is
self-hosted — there is no single domain to verify against. A
custom scheme is the only form that works for every user's
server. -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="@string/custom_url_scheme" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
</provider>
</application>
<!-- Permissions. Only what the WebView itself needs: the file picker's
camera option and navigator.geolocation for "locate me" on the map,
both granted at runtime through Capacitor's WebChromeClient. -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- COARSE is listed first because it is the weaker grant and is enough
for "what city am I in". -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<!-- required="false" on purpose: TREK is a planner, and everything except
"locate me" works without GPS hardware. Marking it required would have
Play hide the app from tablets and other GPS-less devices. -->
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
</manifest>
@@ -1,50 +0,0 @@
package com.liketrek.trek;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
registerPlugin(TrekShellPlugin.class);
// With a server picked, the WebView loads that server instead of the
// bundled address screen, so the UI arrives in the version the server
// runs and on its own origin. BridgeActivity reads `config` while it
// builds the bridge inside super.onCreate().
boolean restarting = TrekServerStore.consumeRestart(this);
String server = TrekServerStore.getUrl(this);
boolean onServer = server != null && !TrekServerStore.consumeShellOnce(this);
config = TrekServerStore.configFor(this, onServer ? server : null, restarting);
super.onCreate(savedInstanceState);
if (bridge == null) return;
// The UI asks for no overscroll effect (overscroll-behavior: none on html)
// and Chrome honours that, but the WebView draws the stretch on its root
// scroller natively. It scales the whole view, top bar and bottom dock included.
bridge.getWebView().setOverScrollMode(View.OVER_SCROLL_NEVER);
// Set before any WebView callback can run: they are posted to this thread.
if (onServer) bridge.setWebViewClient(new TrekWebViewClient(bridge, this::showAddressScreen));
}
private boolean leavingServer;
/**
* The server did not load: back to the address screen, which offers retry
* and change. A fresh task rather than recreate(), because this usually
* happens during launch, while the system splash is still up, and a
* recreated activity leaves that splash lying over the new page.
*/
private void showAddressScreen() {
// A failed page can report more than one error; restart only once.
if (leavingServer) return;
leavingServer = true;
TrekServerStore.showShellOnce(this);
TrekServerStore.markRestart(this);
runOnUiThread(() -> {
startActivity(new Intent(this, MainActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK));
overridePendingTransition(0, 0);
});
}
}
@@ -1,123 +0,0 @@
package com.liketrek.trek;
import android.content.Context;
import android.content.SharedPreferences;
import com.getcapacitor.CapConfig;
import com.getcapacitor.Logger;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import org.json.JSONException;
import org.json.JSONObject;
/** The server the user picked, and the Capacitor configuration that loads it. */
final class TrekServerStore {
private static final String PREFS = "trek_shell";
private static final String KEY_URL = "server_url";
private static final String KEY_SHELL_ONCE = "shell_once";
private static final String KEY_RESTART = "restart";
private static final String CONFIG_DIR = "trek-server-config";
private TrekServerStore() {}
static String getUrl(Context context) {
return prefs(context).getString(KEY_URL, null);
}
static void setUrl(Context context, String url) {
SharedPreferences.Editor editor = prefs(context).edit();
if (url == null) {
editor.remove(KEY_URL);
} else {
editor.putString(KEY_URL, url);
}
editor.commit();
}
/** Start the next launch on the bundled address screen, even with a server remembered. */
static void showShellOnce(Context context) {
prefs(context).edit().putBoolean(KEY_SHELL_ONCE, true).commit();
}
static boolean consumeShellOnce(Context context) {
return consume(context, KEY_SHELL_ONCE);
}
/** The next onCreate is the app restarting itself, not a launch. */
static void markRestart(Context context) {
prefs(context).edit().putBoolean(KEY_RESTART, true).commit();
}
static boolean consumeRestart(Context context) {
return consume(context, KEY_RESTART);
}
private static boolean consume(Context context, String key) {
boolean set = prefs(context).getBoolean(key, false);
if (set) prefs(context).edit().remove(key).commit();
return set;
}
/**
* The bundled capacitor.config.json, adjusted for this start. Capacitor only
* takes these settings from a config file, so this writes a copy into the
* app's private storage and loads that; null means the bundled one as it is.
*
* With a server: server.url set, and server.errorPath dropped, because that
* page would have no bridge on the server's origin (TrekWebViewClient
* handles a failed load instead).
*
* On a restart: no launch splash. The splash plugin shows it again after
* recreate() but cannot take it down when the page hides it early, which
* left it covering a fully rendered page. A restart has no launch to cover.
*/
static CapConfig configFor(Context context, String url, boolean restarting) {
if (url == null && !restarting) return null;
try {
JSONObject json = new JSONObject(readAsset(context, "capacitor.config.json"));
if (url != null) {
JSONObject server = child(json, "server");
server.put("url", url);
server.remove("errorPath");
}
if (restarting) {
child(child(json, "plugins"), "SplashScreen").put("launchShowDuration", 0);
}
File dir = new File(context.getFilesDir(), CONFIG_DIR);
if (!dir.isDirectory() && !dir.mkdirs()) throw new IOException("Cannot create " + dir);
try (FileOutputStream out = new FileOutputStream(new File(dir, "capacitor.config.json"))) {
out.write(json.toString().getBytes(StandardCharsets.UTF_8));
}
return CapConfig.loadFromFile(context, dir.getAbsolutePath());
} catch (IOException | JSONException ex) {
Logger.error("TrekShell", "Could not adjust the app config for " + url, ex);
return null;
}
}
private static JSONObject child(JSONObject parent, String key) throws JSONException {
JSONObject existing = parent.optJSONObject(key);
if (existing != null) return existing;
JSONObject created = new JSONObject();
parent.put(key, created);
return created;
}
private static String readAsset(Context context, String name) throws IOException {
try (InputStream in = context.getAssets().open(name); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] buffer = new byte[8192];
int read;
while ((read = in.read(buffer)) != -1) out.write(buffer, 0, read);
return out.toString("UTF-8");
}
}
private static SharedPreferences prefs(Context context) {
return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
}
}
@@ -1,149 +0,0 @@
package com.liketrek.trek;
import android.content.Intent;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import androidx.browser.customtabs.CustomTabsIntent;
import com.getcapacitor.JSObject;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.CapacitorPlugin;
import org.json.JSONObject;
/** The native half of client/src/native/trekShell.ts. */
@CapacitorPlugin(name = "TrekShell")
public class TrekShellPlugin extends Plugin {
/** How long to wait after returning from the browser for the redirect to land. */
private static final long CANCEL_GRACE_MS = 600;
private String authCallId;
private String authScheme;
private boolean authBrowserOpen;
private final Handler handler = new Handler(Looper.getMainLooper());
@PluginMethod
public void getServer(PluginCall call) {
String url = TrekServerStore.getUrl(getContext());
JSObject result = new JSObject();
result.put("url", url == null ? JSONObject.NULL : url);
call.resolve(result);
}
@PluginMethod
public void setServer(PluginCall call) {
String url = call.getString("url");
Uri parsed = url == null ? null : Uri.parse(url);
if (parsed == null || parsed.getHost() == null || !("https".equals(parsed.getScheme()) || "http".equals(parsed.getScheme()))) {
call.reject("A http(s) URL is required");
return;
}
TrekServerStore.setUrl(getContext(), url);
call.resolve();
restart();
}
@PluginMethod
public void resetServer(PluginCall call) {
TrekServerStore.setUrl(getContext(), null);
call.resolve();
restart();
}
@PluginMethod
public void reload(PluginCall call) {
call.resolve();
restart();
}
/** The server URL is part of the bridge's configuration, so it takes a new activity. */
private void restart() {
TrekServerStore.markRestart(getContext());
getActivity().runOnUiThread(() -> getActivity().recreate());
}
@PluginMethod
public void authenticate(PluginCall call) {
String url = call.getString("url");
String scheme = call.getString("callbackScheme");
if (url == null || scheme == null) {
call.reject("url and callbackScheme are required");
return;
}
rejectPendingAuth("Superseded by a new sign-in");
call.setKeepAlive(true);
getBridge().saveCall(call);
authCallId = call.getCallbackId();
authScheme = scheme;
authBrowserOpen = true;
// A Custom Tab shares the browser's cookies, so a running SSO session at
// the identity provider carries over. The redirect back to the app's
// scheme arrives through the intent-filter in AndroidManifest.xml.
new CustomTabsIntent.Builder().build().launchUrl(getActivity(), Uri.parse(url));
}
@Override
protected void handleOnNewIntent(Intent intent) {
super.handleOnNewIntent(intent);
Uri data = intent.getData();
PluginCall call = pendingAuthCall();
if (call == null || data == null || !authScheme.equals(data.getScheme())) return;
JSObject result = new JSObject();
result.put("url", data.toString());
call.resolve(result);
finishAuth(call);
}
/**
* Back in the app without a redirect means the user closed the browser.
* onNewIntent runs before onResume when the redirect did arrive, so the
* grace period only covers devices that deliver the two out of order.
*/
@Override
protected void handleOnResume() {
super.handleOnResume();
if (!authBrowserOpen) return;
authBrowserOpen = false;
handler.postDelayed(() -> {
PluginCall call = pendingAuthCall();
if (call == null) return;
call.reject("Sign-in cancelled", "CANCELLED");
finishAuth(call);
}, CANCEL_GRACE_MS);
}
private PluginCall pendingAuthCall() {
return authCallId == null ? null : getBridge().getSavedCall(authCallId);
}
private void rejectPendingAuth(String message) {
PluginCall call = pendingAuthCall();
if (call == null) return;
call.reject(message, "CANCELLED");
finishAuth(call);
}
private void finishAuth(PluginCall call) {
getBridge().releaseCall(call);
authCallId = null;
authScheme = null;
authBrowserOpen = false;
}
/** iOS only: Android keeps the service worker, which already starts TREK offline. */
@PluginMethod
public void refreshOfflineCopy(PluginCall call) {
JSObject result = new JSObject();
result.put("saved", false);
call.resolve(result);
}
@PluginMethod
public void openOfflineCopy(PluginCall call) {
JSObject result = new JSObject();
result.put("opened", false);
call.resolve(result);
}
}
@@ -1,49 +0,0 @@
package com.liketrek.trek;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import com.getcapacitor.Bridge;
import com.getcapacitor.BridgeWebViewClient;
/**
* Used while the WebView shows the user's server. When that server cannot be
* loaded, Capacitor would show server.errorPath, but on Android it injects
* its bridge only into the server's origin, so that local page could neither
* retry nor change the server and stayed blank. Instead the app restarts on
* the bundled address screen, where the bridge is present and the screen
* offers exactly those two things.
*/
class TrekWebViewClient extends BridgeWebViewClient {
private final Runnable onUnreachable;
TrekWebViewClient(Bridge bridge, Runnable onUnreachable) {
super(bridge);
this.onUnreachable = onUnreachable;
}
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
if (request.isForMainFrame()) {
onUnreachable.run();
return;
}
super.onReceivedError(view, request, error);
}
/**
* Only a 5xx on the page itself means the server is down (a proxy's 502 or
* 503 while TREK restarts). A 4xx page is left alone: the server answered,
* and TREK serves its UI for every other path.
*/
@Override
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
if (!request.isForMainFrame()) {
super.onReceivedHttpError(view, request, errorResponse);
} else if (errorResponse.getStatusCode() >= 500) {
onUnreachable.run();
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background>
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
</background>
<foreground>
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
</foreground>
<monochrome>
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
</monochrome>
</adaptive-icon>
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background>
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
</background>
<foreground>
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
</foreground>
<monochrome>
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
</monochrome>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#09090B</color>
<color name="launch_background">#09090B</color>
</resources>
@@ -1,7 +0,0 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">TREK</string>
<string name="title_activity_main">TREK</string>
<string name="package_name">com.liketrek.trek</string>
<string name="custom_url_scheme">com.liketrek.trek</string>
</resources>
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
<!-- Android 12+ draws its own launch screen: the app icon on this colour.
The same near-black as the icon tile and the first painted frame. -->
<item name="windowSplashScreenBackground">@color/launch_background</item>
</style>
</resources>
@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path name="my_cache_images" path="." />
</paths>
@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TREK is self-hosted: the user points the app at their own server, so we cannot
know its scheme or its certificate authority ahead of time. Two deliberate
relaxations, both required for that to work at all:
1. cleartextTrafficPermitted="true" — Android blocks http:// by default from
targetSdk 28 on. A bare LAN server (http://192.168.1.10:3001) and a
Tailscale host (100.64.0.0/10, which is NOT RFC1918, so a private-range
allowlist would not cover it) both need this. https:// is unaffected and
remains the recommended setup.
2. <certificates src="user" /> — since Android 7 apps ignore user-installed
CAs unless they opt in. Self-hosters running their own CA (step-ca, mkcert,
an internal PKI) install its root on the device; without this the app would
reject their perfectly valid https:// server. System CAs stay first so
public certs keep validating normally.
The tradeoff is that a user-installed CA can also be a MITM proxy's. That is
the same tradeoff the browser-based PWA already makes, and it is the price of
letting people run their own infrastructure.
-->
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
-28
View File
@@ -1,28 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.13.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply from: "variables.gradle"
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
-24
View File
@@ -1,24 +0,0 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../../node_modules/@capacitor/android/capacitor')
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../../node_modules/@capacitor/app/android')
include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../../node_modules/@capacitor/filesystem/android')
include ':capacitor-haptics'
project(':capacitor-haptics').projectDir = new File('../../node_modules/@capacitor/haptics/android')
include ':capacitor-keyboard'
project(':capacitor-keyboard').projectDir = new File('../../node_modules/@capacitor/keyboard/android')
include ':capacitor-share'
project(':capacitor-share').projectDir = new File('../../node_modules/@capacitor/share/android')
include ':capacitor-splash-screen'
project(':capacitor-splash-screen').projectDir = new File('../../node_modules/@capacitor/splash-screen/android')
include ':capacitor-status-bar'
project(':capacitor-status-bar').projectDir = new File('../../node_modules/@capacitor/status-bar/android')
-22
View File
@@ -1,22 +0,0 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
Binary file not shown.
@@ -1,7 +0,0 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-251
View File
@@ -1,251 +0,0 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
-94
View File
@@ -1,94 +0,0 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

Some files were not shown because too many files have changed in this diff Show More