Skip to content

Train timetable, keyless map, NBS rates, and weekly content sync - #5

Merged
ialakey merged 3 commits into
masterfrom
feat/trains-places-content-sync
Sep 6, 2026
Merged

Train timetable, keyless map, NBS rates, and weekly content sync#5
ialakey merged 3 commits into
masterfrom
feat/trains-places-content-sync

Conversation

@ialakey

@ialakey ialakey commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Works through the remaining items in the backlog: two new screens, a fifth rate
source, a broader weekly content sync, and content updates that no longer wait
for a Play release. Every external source was verified live before anything was
built on it.

Recolour

The seed colour was the Serbian flag red, and Material 3 tints every surface
with the seed — which is why backgrounds and cards came out pink rather than
just the accents. The palette is now a blue accent over a neutral surface
ramp
: colour marks actions, the active tab and an urgent deadline, and
nothing else. Cards gained a hairline border since they are no longer separated
from the background by tint.

Train timetable

Serbian Railways' site is a jQuery front end over a JSON station lookup and two
server-rendered result pages. The app uses the same endpoints:

  • Route — direct services between two stations on a date.
  • Board — departures or arrivals for one station.

Stations you have used are remembered, so the usual trip takes two taps. Each
result is rendered twice by the operator (wide and narrow layouts), so the row
layouts are pinned down in one service rather than spread through the UI.

Verified live: 36 connections and 98 board entries parsed for Beograd Centar.

Map without an API key

362 relocant-run businesses from the stats.srb.guide catalogue on an
OpenStreetMap map. OSM needs no API key and no billing account, which is
what made this practical — the Google Maps SDK needs both.

The catalogue is bundled, so the list, the search and the category filters work
offline; only the tiles need a connection. Categories get their own icon and
colour, and each place links out to Google Maps for directions.

Its API requires auth, so the catalogue is read from the map page's hydration
payload, which robots.txt allows. Records are located by id and each field is
then read from that record's own window — field order varies between records,
and a single fixed-order pattern silently dropped 28 of them.

Exchange rates: a fifth source

Adds the National Bank of Serbia official rate. It is the only source with a
real API, and it replaces the previous reference rate, which was scraped out of
one office's ticker as a side effect.

Reference sources are now flagged and excluded from the "best rate"
comparison
— the NBS is not a counter you can walk up to. RUB and GBP have no
NBS cash rate, so those fall back to the non-cash figure.

All five verified live in test/exchange_parsers_live_test.dart.

Content sync covers three datasets

sync-guide.yml becomes sync-content.yml and now refreshes weekly:

Dataset Source Records
guide.json srb.guide 74 articles
places.json stats.srb.guide/map 362 places (new)
tg_chats.json stats.srb.guide 432 chats (was 340)

The chat directory had drifted — it was a hand-maintained list of 340 entries.
It now comes from the catalogue with topic, size and whether each chat is still
active, while keeping the original name/url/group keys so the existing
screen keeps working.

Only datasets whose payload actually changed are staged, so a new sync
timestamp alone never produces a commit.

Guide updates between releases

Because that workflow commits the guide to master, the app now checks GitHub
for a newer copy once a day, conditional on an ETag so an unchanged guide
costs a single 304 and no body transfer. A correction on srb.guide reaches
users in days instead of waiting for a Play release.

The download is validated before it is accepted — the same shape checks CI runs
— and the bundled asset stays the fallback, so a bad fetch can never leave the
app without content. Settings gains a manual check.

Search

Article text was re-lowercased on every keystroke: 1.5 MB per search, with
the search debounced to every 200 ms. The lowercased forms are now computed once
per article and reused.

One item I did not implement

The backlog asked for SQLite + FTS5. I measured before building it, and the
numbers argue against it at this size:

  • Search over all 74 articles currently takes ~12 ms.
  • The APK cost of guide.json is already ~0.5 MB, not 2.4 MB — Android
    compresses assets, and the file gzips to 0.53 MB.
  • SQLite would not shrink that. A prebuilt .db is about the same size, and
    building the index on device means shipping the JSON and storing the DB, so
    on-device usage goes up.
  • package:sqlite3 3.x compiles SQLite from source through Dart build hooks,
    which is a new native dependency in the Android and CI build I would rather
    not add for a 12 ms operation.

So the cost is a native dependency and build risk, and the benefit is nil until
the content is several times larger. I did the part that was actually costing
something — the per-keystroke lowercasing above. Happy to add FTS5 if you want
it regardless; the search API is already isolated behind GuideRepository.

Housekeeping

Removed assets/data/media/ — 112 images, 5.6 MB, that nothing referenced.
They were never declared under assets: in pubspec.yaml (a bare
assets/data/ entry does not recurse), so they were not in the APK either; they
only sat in the repository. They predate the guide re-sync: the old guide's
image links were site-relative paths like /guides/business/open/media/…webp,
addresses on srb.guide rather than local files, whose names do not match these.
Checked before deleting — no Dart loads assets/data/media, no bundled dataset
points at assets/, and the app builds and renders articles without it.

Updated the actions to their current majors. GitHub was force-running the
Node 20 ones on Node 24 and warning on every run, and setup-java v4 is
explicitly no longer maintained:

actions/checkout                 v4 -> v7
actions/setup-java               v4 -> v6
actions/upload-artifact          v4 -> v7
actions/github-script            v7 -> v9
peter-evans/create-pull-request  v6 -> v8
softprops/action-gh-release      v2 -> v3

subosito/flutter-action stays on v2, still its current major. Every tag was
checked to exist before pinning, and the inputs used here are unchanged across
these majors.

Verification

  • dart format, flutter analyze (0 issues), 29 unit tests, guide validation
  • All five rate sources and all three timetable endpoints verified against the
    live sites
  • Release AAB builds; merged manifest keeps targetSdk 36 / minSdk 24 and
    gains no new permissions
  • Exercised on an Android 16 emulator: new theme in light and dark, the map with
    tiles and 362 markers, a train search returning real departures, and the rates
    screen showing NBS alongside the four offices

Recolour

The seed was the Serbian flag red, and Material 3 tints every surface with the
seed — so backgrounds and cards came out pink. The palette is now a blue accent
over a neutral surface ramp: colour marks actions, the active tab and an urgent
deadline, and nothing else.

Train timetable

New screen over the Serbian Railways timetable at w3.srbvoz.rs, which exposes a
JSON station lookup and two server-rendered result pages. Route search and a
per-station departure/arrival board, with recently used stations remembered.
Row layouts are pinned down in one service rather than spread through the UI,
because the operator renders each result twice for wide and narrow layouts.

Map without an API key

Replaces nothing, adds a screen: 362 relocant-run businesses from the
stats.srb.guide catalogue on an OpenStreetMap map. OSM needs no key and no
billing account, unlike the Google Maps SDK. The catalogue is bundled, so the
list and the category filters work offline and only the tiles need a
connection. The app identifies itself as the OSM tile usage policy requires.

Exchange rates

Adds the National Bank of Serbia as a fifth source. It is the only one with a
real API, and it replaces the previous reference rate, which was scraped out of
one office's ticker. Reference sources are flagged and excluded from the "best
rate" comparison — the NBS is not a counter you can walk up to.

Content sync

sync-guide.yml becomes sync-content.yml and now refreshes three datasets
weekly: the guide, the places catalogue (new) and the chat directory, which had
drifted to 340 hand-maintained entries and is now 432 from the catalogue with
topic, size and activity. Only datasets whose payload actually changed are
staged, so a new sync timestamp alone never produces a commit.

Guide updates between releases

Because that workflow commits the guide to master, the app now checks GitHub
for a newer copy once a day, conditional on an ETag so an unchanged guide costs
a single 304. Downloads are validated before being accepted and the bundled
asset stays the fallback, so a bad fetch cannot leave the app without content.
Settings gains a manual check.

Search

Article text was re-lowercased on every keystroke — 1.5 MB per search. The
lowercased forms are now computed once per article and reused.
112 images that nothing referenced. They were never declared under `assets:` in
pubspec.yaml — a bare `assets/data/` entry does not recurse into subdirectories
— so they were not in the APK either; they only sat in the repository.

They predate the guide re-sync. The previous guide's image links were
site-relative paths like `/guides/business/open/media/…webp`, which are
addresses on srb.guide rather than local assets, and whose names do not match
these files. The current guide references 283 images, all remote.

Verified before deleting: no Dart code loads `assets/data/media`, no image
reference in any bundled dataset points at `assets/`, and the app builds and
renders articles with the directory gone.
GitHub is force-running the Node 20 actions on Node 24 and warns on every run;
setup-java v4 is explicitly no longer receiving updates.

  actions/checkout                 v4 -> v7
  actions/setup-java               v4 -> v6
  actions/upload-artifact          v4 -> v7
  actions/github-script            v7 -> v9
  peter-evans/create-pull-request  v6 -> v8
  softprops/action-gh-release      v2 -> v3

subosito/flutter-action stays on v2, which is still its current major.

Every tag was checked to exist before pinning, and the inputs used here —
`flutter-version`, `distribution`/`java-version`, `path`/`retention-days`,
`branch`/`add-paths`, `draft`/`files` — are unchanged across these majors.
@ialakey
ialakey merged commit 80a12cc into master Sep 6, 2026
2 checks passed
@ialakey
ialakey deleted the feat/trains-places-content-sync branch September 6, 2026 00:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant