Skip to content

terminus 2.6

Choose a tag to compare

@rcnsh rcnsh released this 09 Oct 13:47
· 284 commits to main since this release
v2.6.0
547d6de

Released 9 October 2026

terminus 2.6

A big one: everything since 2.4.2. The highlights are below; every change is listed further down.

A Buses tab. Every stop and every service, not just the ones for your classes, with services that have stopped for the day greyed out. Pull down on Now or Buses to refresh, and your bus pulls in.

Trips that change buses. Where no single bus goes there, terminus plans a change: the card shows both buses, follows you onto the second, and has you wait at the change until it leaves.

The map. Tap a bus to see where it's going and the stops ahead; tap a stop for its board. Stop and bus sheets look alike, a bus's stops open their own, and tapping Map again goes back to the whole campus. Place names show in Chinese too.

On Android, the widget is redrawn under Now's sky, with a layout for each size, and an × takes a row off Today, with Undo in its place. Favourites fold in and out everywhere.

Is this wrong? opens a sheet with quick reasons, and lets you suggest the stop you actually use for a building.

NUS's own days off, starting with Well-Being Day, count as holidays.

A new address, terminus.run. The apps keep using terminus.rcn.sh, which NUS Wi-Fi lets through, so nothing changes for you.

Under the hood: easier to use with TalkBack, VoiceOver and large text, readable colours on every sky, steadier when the network or NUS's feed is down, and a "~" on every time that's a guess rather than live.

Every change since v2.4.2
  • No layout shift while fonts and the first card load
  • Landing page arrives with its version and account link, and no font jump
  • A Buses tab, for every stop and service, not just your classes
  • The Buses tab lists services that have stopped, greyed out
  • Harden sign-in, pairing and headers found in the security audit
  • Scrape job's push token reaches only its commit step
  • One helper each for whole-second ISO times and SGT dates
  • One re-mint path for both bus proxy calls
  • Doc comments sit on the code they describe, stale ones gone
  • Fewer repeated blocks in the planner, card and trip code
  • One SGT offset and one SGT midnight for the core modules
  • Comments that match the code they sit on
  • Share small helpers instead of repeating them
  • Consistent indentation in cloudflare.config.ts
  • One operator mailer, a readable /docs CSP, mint state in one place
  • Drop style rules no page uses
  • Share repeated card and settings logic
  • Name the map's shared colours and padding
  • Bring two comments up to date
  • Drop unused imports and put stray doc comments back
  • Fold the widget's repeated layout blocks into helpers
  • One helper each for prefs, alarms and countdowns
  • One place for the account screens' error handling
  • Share small helpers across the app's screens
  • Pin the time inside the leave countdown
  • Drop unused imports and put doc comments on what they describe
  • Share the API's error, version and answer plumbing
  • One copy of what setup and settings both ask
  • One banner view, one hex parser, and the map's layer helpers
  • Drop four unused translations and tidy declarations
  • Snapshot renders write their PNGs through one function
  • Less load on NUS and LTA under failure, and fewer client retries
  • A feed that changed shape reads as down, never as "no bus"
  • /me/next keeps to operating hours, counts from now, tries both sides
  • Ready-made text for what the clients used to word themselves
  • Show the server's words, follow its refresh marks and clock
  • A guessed arrival gets its "~" in the class timing and ride line
  • Show the server's words, trip steps and ride; follow its clock
  • Show the server's words, follow its marks and clock
  • Riding a public bus reads its arrival from LTA, not NUS
  • The Buses tab no longer breaks when a stopped service has a direction
  • Glossary settles service, scheduled, expired, crowding and email words
  • Index the crowd and push-token lookups, sweep large tables daily
  • Chinese location prompt, glossary words and spacing; plainer errors
  • The Buses tab's stop sits in the sky, as Settings' title does
  • Index the crowd and push-token lookups, sweep large tables daily
  • Scripts and styles skip the Worker; vendored files kept a year
  • Glossary settles service, scheduled, expired, crowding and email words
  • Chinese location prompt, glossary words and spacing; plainer errors
  • Chinese spacing and glossary words, AM/PM, favourites wording
  • Chinese spacing, two missing error translations, plainer errors
  • Round every location to about 11 metres on the server
  • Log only the answers someone asked for
  • The Buses tab's stop sits in the sky, as Settings' title does
  • Cut the street map from zoom 12, dropping levels never drawn
  • Chinese for the pair page button, glossary words, honest privacy pages
  • Keep the campus data and map style an hour, then ask by ETag
  • Don't resend crowd sightings this isolate already counted
  • A trip home with no class name reads "Home after your last class"
  • The admin dashboard reads each table once, in one round trip
  • Deleting an account leaves nothing behind in KV or the trip object
  • Fewer Trip object requests, writes and alarms per card
  • Keep status, FCM token, device id and release files in the isolate
  • The timelapse recorder makes its tables once and counts rows in meta
  • Trip history keeps the day, reports a year; report emails say less
  • The data export has everything kept, today's trip included
  • A Trip object's wake waits only for its plan writes
  • Build static responses on first use, once, and without ICU
  • Pages and the map load their modules at once; leaner offline copy
  • The semester reminder's words no longer go through Firebase
  • Fetch the semester reminder's words rather than receive them
  • /buses remembers who it let in for 30 s
  • Signing out clears what the widgets were showing
  • A1's red is deep enough for white text
  • Errors and late times in a readable red, and spoken
  • Service codes in dark ink on light service colours
  • Lines and buses edged in ink on the light map; no camera glide with animations off
  • Fonts and icons kept by path, map pieces streamed, cache never limited
  • No stars on the Buses tab's words, and search is just the field over the hills
  • Timetable guesses say so, in the header and to VoiceOver
  • The landing page answers 304 while the browser's copy is current
  • Map stops, buses and zoom reachable without a mouse
  • Chinese spacing keeps a class's room apart from its start time
  • The card's rows, the ride and Today read well with VoiceOver
  • The same words across clients for favourites, stops, leave alerts and paths
  • Search works from the keyboard and says what it found
  • The tabs are one named group, each saying its place
  • TalkBack reads the Now card as one sentence and hears it change
  • Keyboard shortcuts in the popover, and Escape cancels
  • The map says what it is and opens as a list for TalkBack
  • Named fields, headings, spelled-out codes and calmer motion
  • Signing out or deleting the account clears what this browser kept
  • Errors said when they show, fields keep their names, headings marked
  • A timetable guess counts down in minutes, on one line
  • The device name is the Mac's model; directions open Apple Maps
  • Buses tab's sides, pages and rows say what they are and do
  • The route card and buttons make room for large text
  • Internals and analytics notes match what is kept
  • Headlines wrap at large text; buttons a fingertip wide
  • Bigger targets for a place's X and "Is this wrong?"
  • The time-to-leave alert shows its whole line when opened
  • Screen readers hear changes once, and focus is never lost
  • Colours readable on every sky, map and contrast setting
  • The privacy policy says what is actually kept, in both languages
  • The API docs say how reports, deletion and locations are handled
  • A line's closing time is in the sky's ink, readable at night
  • Nothing cut off or sideways at 320 px or large text
  • The landing page's shuttles can be paused, and motion follows the device
  • A failed cache write keeps a good answer; pending marker outlasts the slowest fetch
  • A NUS host that times out or can't be reached opens the breaker
  • DataMall answering 5xx opens the public buses' breaker
  • The NUS device id is made only when none is stored
  • Feed answers are timed from the call that answered
  • An old version refused after a switch doesn't reopen the breaker
  • An isolate that loses the race to store a device id takes the stored one
  • An old version refused after a switch doesn't quiet its stop either
  • A connection that fails on workerd counts as unreachable too
  • DataMall timing out or unreachable opens the public buses' breaker
  • The breaker lists agree on timeouts and failed connections, for LTA too
  • Pushes time out, one device can't stop the rest, bad messages keep tokens
  • /health says whether push to Android and the web app is set up
  • Calls to the Trip object time out, so a stuck one can't hold up the card
  • Trip pushes survive a failed wake or send, and a /watch or /clear meanwhile
  • A token FCM says is not valid is dropped, even without a field
  • A pushed phase is saved as soon as it is sent
  • Web subscriptions are skipped, not failed, when the VAPID key is unusable
  • Note why the trip alarm's writes can be held like a POST
  • The cron waits for every step before it reports done
  • A failed KV read no longer drops the merged years
  • Failed cron steps show with the errors on the dashboard
  • Trips the morning cron could not arm are asked again
  • Term reminders neither repeat nor count a failed batch as sent
  • The feed monitor keeps its state and outages right when KV fails
  • A feed alert is marked sent under its own KV key, not a second state write
  • The feed alert's delivery mark has its own KV key
  • One unreachable phone no longer holds up the semester reminder
  • A retried semester reminder neither repeats nor ignores a newer profile
  • A recovered outage ends at the first good check, not the second
  • The test D1 counts rows changed by a RETURNING statement
  • Pnpm run deploy applies pending D1 migrations first
  • A sign-in is spent only once its account and session are made
  • A one-off trip or import no longer loses another device's save
  • A D1 outage answers 503 with Retry-After, not 500 everywhere
  • Turnstile failing to answer is logged
  • A failed last_seen or key last_used update is logged
  • An email send that hangs fails after 10 s
  • The daily ride-time table is worked out in D1
  • A fault in a D1 query stays a 500, not a 503 outage
  • An emailed code survives a failed sign-in; email timeout is 20 s
  • D1 overload is a 503 everywhere; a query fault on a keyed route is a 500
  • Log a day row's leave-by that fails instead of dropping it silently
  • A ride's arrival counts from its own feed's fetch, not the older one
  • A stale answer under a minute old says so, not "0 min old"
  • A stale answer's clock times keep their ~ on the card
  • No guessed bus once a service has closed for the day
  • A sliding leave-by asks for a refetch no sooner than 30 s ahead
  • Bus boards count their times from now and say how old they are
  • A stale bus board time keeps its ~, as on the card
  • An R2 outage serves cached pieces and answers 503, not 416
  • The timelapse switch reads as off when KV can't be read
  • Asking about an unrecorded timelapse day no longer creates storage
  • A timelapse poll that throws no longer stalls the day's recording
  • A timelapse reading and its timing are saved together
  • The timelapse recorder enforces its 17,280 polls a day in code
  • The timelapse recorder follows the calendar kept in KV
  • A closed timelapse day always reaches R2 or is cleaned up
  • Calls to the timelapse recorder give up after 10 seconds
  • The bus recorder and timelapse renderer time out hung requests
  • The recorder's wait timer type-checks against the Workers types
  • The morning cron reaches a timelapse day until its retries end
  • During an R2 outage cached pieces skip R2 and reach new isolates
  • Calls give up after 20 s, and a captive portal's page isn't an answer
  • Now's refreshes don't pile up, and a sliding leave-by asks every 30 s
  • Buses tab keeps the age after a failed update, old times no longer live
  • Buses fade when polls fail, and a hung poll no longer stops the map
  • A push waits at most 8 s for the card before its plain notice
  • Language and setup say when they weren't saved; failed lists say so
  • Status and admin pages give up on a hung call
  • Writes wait 60 s, and an offline call reads in the page's language
  • A language that isn't saved stays as it was and says so
  • Setup shows its 'not saved' notice before going back to the app
  • Coming back to the app asks again even with a call still waiting
  • Now polls every 30 s, not every 5 s while a leave time slides
  • Background fetches wait for a cold answer; no bare HTTP codes
  • Widget dims and retries offline; push token re-sent
  • Live notification stops counting down a bus that may be gone
  • A leave reminder from an old answer says it's unconfirmed
  • A medium widget says when its time is a timetable estimate
  • Buses fade when the app can't reach the server
  • An offline widget still dims at staleAt after an earlier alarm
  • Background fetches fit a 10 s budget; fresh reminders not unconfirmed
  • MapViewModel keeps its doc comment above the class
  • An offline widget still turns to leave now and moves stop by stop
  • An unconfirmed ride says so in the live notification
  • Ride reminder's countdown stops at getting off
  • An error page without a message reads as plain words, not HTTP 502
  • Keeping this Mac's setup says so when the merge fails
  • Offline, the menu bar drops a glance gone stale
  • A failed Today fetch after a card button keeps the old Today
  • The map's live buses fade once the map hasn't heard from /buses for 15 s
  • Leave reminders say when they were last checked once refreshes stop
  • Offline, a stale glance leaves the menu bar
  • A merge whose reply was lost no longer holds sign-in on the choice
  • Rewording a leave reminder keeps its time instead of pushing it later
  • Sign-in says try again later when the human check is down
  • The timelapse recorder deletes its alarm with a finished day
  • Review follow-ups to the privacy fixes
  • Term reminders aren't retried for someone no device can reach
  • The spec lists the accounts-down 503 on every keyed route
  • Now's fallback says the update failed and when the plan is from
  • Buses keeps its page on Back, map stops open on purpose, quieter screen reader
  • TalkBack hears a plain card's phase and destination once
  • VoiceOver hears search counts once typing settles, bus counts less often
  • A release shows on the landing page and downloads at once
  • A push is never late when the leave-by moves a little sooner
  • Chinese screen readers hear "。" between the card's sentences
  • No flash of the email step after signing in with a code
  • Remove "Notice when I board" and measured ride times
  • Client comments match what the apps and server do now
  • Pin when trip suggestions are and are not offered
  • The leave reminder's choice is a pure function
  • One heads-up per class, checks two minutes ahead
  • Tell apps older than config:minClient to update
  • Pin how long each limit on the feeds holds, either side of its edge
  • Source comments describe what the code does now
  • Sign-in pages say when to retry after a 429
  • Every rate-limit binding refusing gives a 429 with retry-after
  • READMEs and CLAUDE.md match the repo as it is
  • A due push for a trip with reminders off goes at normal priority
  • An unsent app sign-in email is a 502 and holds no cooldown
  • The leave reminders' choice is a pure function
  • One heads-up per trip, a new trip clears the last
  • Golden answers fail when missing or orphaned, and set their own clock
  • Sign-in link cooldown and expiry at their edges
  • Every golden answer is read, English and Chinese
  • Pin the timelapse recorder's back-off, interval and day of requests
  • A stale bus's times show with a ~ on the card's journey
  • 429s say when to retry, CORS takes x-api-key, mints counted
  • Internals.md and analytics.md match the code
  • The card's countdowns and lines match Android on every shared answer
  • Check CI, signing keys and build numbers before publishing
  • Every golden answer is read, English and Chinese
  • Migrations after 0006 only add, and applied ones are frozen
  • "or go now" marks a guessed arrival time with a ~
  • Pin the cron's one call to each feed and the version check's failures
  • A wrong on-the-bus guess standing off the road becomes a miss
  • -PapiBase only changes debug builds
  • The header never says "Left 0 min ago" or "in 0 min 45 s"
  • The header's countdowns round as the phone's do
  • Build release variants, harden the scrape bot and pin map tools
  • The stop sheet shows the Buses tab's board, more compactly
  • Time to go wakes the phone again for trips with reminders off
  • An outdated app can still stop its pushes and delete its account
  • Signing out leaves no widget holding the old account's place
  • A tapped bus shows where it's going and the stops ahead
  • No blank notification for the new semester's reminder
  • The term reminder's words, from the push or /me/notice
  • Read partial replies, wait out Retry-After, stop on a 426
  • Sign-out, sign-in races and the refresh loop's energy use
  • The beta's cron stops calling NUS and LTA; deploys check first
  • Walking to a class you're late for says when you'll get there
  • The spec, robots.txt and llms.txt match what the API does
  • Walk taps, choice and import refusals, profile 409, trip object 404
  • What the live notification counts down to
  • Reading week and the exams are named on a day with no classes
  • Golden answers for a failing feed, the day's ends, holidays and the calendar
  • CLAUDE.md and the Android README match the new CI, release and deploy checks
  • The refresh loop's back-off and its 30 s floor
  • No mention of ride detection or measured ride times left
  • A newer server's phase and quality are neither a trip nor live
  • Street-map pieces from R2: exact ranges, 304, 404, a replaced file
  • A newer server's quality is never shown as live
  • The service worker in a stand-in scope, and SHELL pinned to its file list
  • An unknown destination gives the day's answer, not "No more classes"
  • Push failures: stale FCM token, failed mint, database, web 5xx
  • When Now fetches again, and its banner while the card is old
  • An A1 or A2 bus slides on past KRB instead of jumping back
  • The map's stop card rounds a bus's time as Nearby does
  • The map's stop card rounds a bus's time as Nearby does
  • A bus's time rounds to the nearest minute on the map and in Nearby
  • Buses slide along the real route lines as the API places them
  • Invariants on the bundled stops, hours, calendar and twins
  • Tests that a normal weekly scrape no longer breaks
  • A failed weekly scrape says how to refresh the golden answers
  • Which language a browser gets, and t()'s blanks
  • Keep files and databases out of backups and device transfer
  • Only a live time is shown without a "~"
  • The widget's Nearby times, from the server then counted down
  • The OpenAPI spec says what the Worker really answers
  • Check the OpenAPI spec against what the Worker sends
  • One sign-out path, answers read before kept, request deadlines
  • When the push token is sent again and push is trusted
  • The card's headline and the profile's helpers on the real /campus
  • Thresholds at their exact edges: stops, line, walk, loops, dates
  • Late for a class, the leave-by is the bus that gets you there first
  • The map's bus slides stop with the window, and draw lighter
  • The offline map keeps only a style and map file that check out
  • The widget's offline fallback, refresh button and tap
  • The map stops and says so when signed out or out of date
  • The Buses tab's addresses, pages, board across the road and pin limit
  • Beta noindex on HTML, the cron entry, the health probe and lookup
  • The update check is daily and only offers a newer release
  • "On it" for a bus other than the plan's rides on that bus's times
  • "Go later today" near midnight starts at 23:55, not 00:10
  • Go later's first time, on a five-minute mark and today
  • Golden answers for public buses, the road to cross, trips and their phases
  • Settings' times, the status page's outages and the theme choice
  • The glance after the last bus says the walk, not "No bus walk"
  • After the last bus, the glance is the walk
  • The Buses tab search checks the empty box too, not only typed queries
  • The map file is kept, checked weekly and never cut short
  • Drop an unused location from the golden cases
  • Pin the bus's place on /line, count llms.txt operation links, check deduped pins
  • Pin dev tools exactly; wrangler and cf pull undici 7.29.1
  • The map file is kept, checked weekly and never cut short
  • The street map uploads only what map-tiles.lock pins
  • Logged-only failures stay contained; a web write still runs at 25 s
  • Satisfy lint in the new sign-in and profile tests
  • Updates wait for the map window too, and can be turned off
  • Sparkle no older than 2.10.0
  • Vendored web libraries are checked by hash
  • Blob: workers only on the timelapse page
  • Hardened runtime, pinned packages and a hash for MapLibre
  • Scrape in three jobs, so npm code never sees the secrets or token
  • No leave countdown while waiting at the stop, as on Android
  • Say the live notification may wait for the app without exact alarms
  • Push, leave alerts and the live notification follow what can be seen
  • A Mac release checks its whole signature and certificate
  • Refuse a release the live site would undo or not serve
  • The Mac install note says how to open it the first time
  • A sign-in given up on no longer hides a sign-out, and rest's end is located
  • A map that fails to load keeps its offline map file if it checks out
  • After a 426, ask the server again in half an hour, not six
  • Deploy before releasing, and how to finish after the tag
  • Fewer wake-ups and requests, redraws for what the widget can draw itself
  • Read and parse what the widget shows once per redraw, not per size
  • Map file kept whole and by version, and a missing style asked for again
  • A sliding bus redraws only the buses, and the tabs wait out a 503
  • Fresh dependencies, a pinned sign_update and a signed appcast
  • Scrapers refuse redirects and check new data against HEAD
  • The weekly sync refuses implausible calendars
  • CLAUDE.md, internals.md and the Mac README match the supply-chain checks
  • A push in a trip fetches at once, and term reminders keep push on
  • The refresh lock covers only the answer, and fast callers wait a second at most
  • Heads-up at remindAt only with exact alarms, and bounded re-checks
  • Card buttons come back after a stale 401, and a 426 says it once
  • An outdated app can still sign out, unpair and delete its account
  • Locked, hash-checked dependencies from their own repositories
  • Arriving early at a class says "You're there" straight away
  • The leave reminder's tests word a leave time, and cover posting at remindAt
  • The map file tests keep a real map file, under its own name
  • The site's new look, under the sky, with the Buses tab
  • "Is this wrong?" opens a sheet with quick reasons
  • Banner shows every client standing on the live campus map
  • Bump the android group in /apps/android with 5 updates
  • "How it fits together" is a picture, light and dark
  • Lock and verify the android group's new versions
  • The edge cache's in-flight marker no longer shares the answer's key
  • The stable site and the beta keep their map files apart in the edge cache
  • The smoke test's in-flight marker is at the edge cache's new key
  • Releases go ahead on the scrape's commit; the beta's wait says what it checks
  • Verify junit-bom 5.14.4's Gradle module file too
  • Signed out from the Buses tab or the map, the browser forgets the account too
  • The Buses tab no longer takes a stale answer's age off its times twice
  • The landing page never has more than two shuttles out, however it's scrolled or paused
  • One service's unreadable times no longer take down a whole stop's board
  • Signing in with a code to an account with a setup no longer gets stuck
  • The card's Undo is back for a skip made elsewhere
  • A request still out at sign-out can't put the old account back
  • A device session has no expiry, as LogoutWorker's comment now says
  • The feed-down notice goes at the first good check, not 15 minutes later
  • One lock on the migrations, the one CLAUDE.md describes
  • A tapped bus's card is described as it is now, with where it's going
  • During a deploy, a bus ending its run at a stop is never the one to catch
  • /health, the status page and the dashboard agree when the feed comes back
  • Rides are a per-stop guess; measured ride times are gone
  • A 429 from the app's own polling no longer blocks signing out
  • /line's asOf counts the age of the stop row it carries, as /arrivals does
  • Drop the server strings and list nothing reads (LANGS, am, pm)
  • The map backs off when the server is in trouble; plainer words on the board
  • The map backs off in an outage; one update banner; old details hidden; report reasons
  • No frozen "Live" times on the Buses tab, and far fewer wakeups
  • An added place's × sits inside its own tab, so it reads as removing that place
  • A control that acts on one item sits inside it, drawn alike on every client
  • The interface explains itself, alike on every client
  • On the bus only when a location shows it, never just because nothing said otherwise
  • Verify the metadata files a cold Gradle cache also fetches
  • No "Leave now" once the planned bus has gone and nobody knows if you're on it
  • Pull down on Now and Buses to refresh, and your bus pulls in
  • Lockfiles written again pass the next build
  • Lock and verify Dependabot's Gradle updates on its PR
  • Any home stop counts as home, so the A1 to PGP beats walking
  • Pull to refresh stretches the sky, with no strip above the page
  • The pull's bus is the horizon's own shuttle when there's no service
  • The sky opens less while a pull refreshes, just room for the pill
  • The sky band scrolls away with the page
  • Pulling to refresh drives the shuttle already on the horizon
  • Pull to refresh only from a touch that starts at the top
  • Terminus moves to terminus.run, and the old address keeps working
  • The Worker keeps answering on terminus.rcn.sh
  • Apps call terminus.rcn.sh, with a developer menu to switch server
  • Pages opened on terminus.rcn.sh go to terminus.run
  • Sign-in email comes from login@terminus.run
  • Which address the apps call, the developer menu, and why rcn.sh stays
  • Mail to terminus.run reaches the operator through Email Routing
  • An up-to-date pull on Buses says when the next update comes
  • The old address works in full while NUS Wi-Fi blocks terminus.run
  • Overnight feed times no longer show as live buses hours away
  • No dark band above the sky when a page is pulled past the top
  • The map can no longer be panned off the street map
  • Leaving LT21 starts from S17 and LT27, and rooms between two stops use both
  • Adding a class by hand scrolls its form into view
  • The sample day's walk is to a COM4 class, which only COM3 serves
  • Settings' Your trips and Timetable are tiles like the rest
  • One bus's leg and score worked out by shared helpers
  • Trips that change buses where no single bus goes there
  • Favourites fold in and out, with Undo after Remove
  • A trip that changes buses is followed onto the second bus
  • The card draws both buses
  • The app reads a trip's second bus
  • The card shows both buses of a trip that changes
  • The landing page's features in cards of every size, with the map
  • A Turnstile pass counts only for sign-in, on the site's own address
  • The apps' links open terminus.rcn.sh, which NUS Wi-Fi lets through
  • The privacy policy links Cloudflare's Turnstile Privacy Addendum
  • The widget redrawn under Now's sky, in a layout for each size
  • The privacy policy's contents sit apart from the text and glide to a section
  • The menu bar's icon sits close to its text
  • A stop's service badges on a line are one width, so P is as wide as R2
  • Walk_routes.py drops a building's stops when it's down to one
  • Suggest the stop you use for a building, from Send feedback
  • Buses' stops and lines follow a back gesture, as Settings' pages do
  • The sky closes a second after a pull's result, the bus driving home after
  • Settings loads the account again when you come back to it
  • A pull's bus comes back to the horizon's own place, with the sky closing first
  • Opening the app signed out goes straight to sign-in, without the app flashing up
  • No empty gap under the email box now Turnstile is invisible
  • The Buses tab says updated just now, never 0 s ago
  • CLAUDE.md works out a release's version and how betas run
  • NUS's own days off count as holidays, starting with Well-Being Day
  • No sideways scrollbar after a Settings page slides
  • An × takes a row off Today, with Undo in its place
  • The pull-to-refresh bus has the horizon's dimmed window
  • Tapping Map again goes back to the whole campus
  • Building stop suggestions only from "Is this wrong?", not Settings
  • A slow pull to refresh sends the same bus round again
  • Stop and bus sheets look alike, and a bus's stops open theirs
  • The map glides to a stop from a bus, centred above its sheet
  • CLAUDE.md cuts a beta whenever there's something to test
  • Dragging a sheet's handle down closes it
  • Lock migrations 0013 and 0014, applied to both databases
  • The landing page and Play listing say up front it's for the NUS shuttle
  • A two-bus trip waits at the change until the second bus leaves
  • A zoom out by the edge of the map comes back onto the street map
  • The Play listing's name says it's a bus planner for NUS
  • Settings' messages float in the bar at the foot, not over the page
  • The small widget keeps 09:37 前出发 on one line, off the hills
  • Chinese place names drawn on Android, from Noto Sans SC
  • The dev stub's Date header follows its own clock
  • Play screenshots from today's app, feature graphics say NUS
  • The landing page leads with one Android button, the app in view
  • Chinese place names reach maps that kept the empty glyphs
  • Landing page figures line up on a phone

Install

Download Runs on
Android terminus-2.6.0.apk Android 12 or later
Android, older 32-bit phones terminus-2.6.0-armv7.apk Android 12 or later
Android, x86_64 (emulators) terminus-2.6.0-x86_64.apk Android 12 or later
Mac terminus-2.6.0.dmg macOS 14 or later, Apple silicon
  • Android: open the APK and allow your browser to install apps when asked. Then open terminus and tap Get started, or sign in if you already have an account. Installing over an older version keeps everything.
  • Mac: open the disk image and drag terminus to Applications, then sign in with your email. The first time, macOS stops it, as it isn't from an identified developer: choose Done, then Open Anyway in System Settings, under Privacy & Security. Installed Mac apps update themselves.
SHA-256 checksums
File SHA-256
terminus-2.6.0.apk fafc03af397134a68af34edab9c153fbe1f7ec3f52cca02566db0f78182cf113
terminus-2.6.0-armv7.apk 5e96529a168a2d38472a421650f5a7cf6c2c982513b8c7e96bd5fb039cc6441c
terminus-2.6.0-x86_64.apk c4c9601f5a8f526e58a9137de58ae96ffdcb41752ce82a64a1c9476999385977
terminus-2.6.0.dmg 5b6e95ef58baf9b2eb710077a4ad214fea733f2c7e6d9b242b9321c1fbfd9b61