Purpose: extract the DNA of the existing Knockout app — the style and architecture themes worth consciously keeping / adapting / dropping in the Angular rewrite. Input to the Fable 5 plan, alongside
../docs/ideas.mdand../docs/org-angular-standards.md. Built from a 4-way parallel review of../current/.Legend: Keep = preserve the idea · Adapt = same intent, Angular-idiomatic form · Drop = Knockout-era or dead.
The “position pipeline” seam already
half-exists. LocationManager decouples raw GPS
from a fixed 500 ms broadcast, buffers a 10-fix history,
applies a noise filter, derives bearing, and exposes an
override() injection point used by the
GPS-test tool and demo mode. This is exactly the foundation the
smoothing/clamping/depot ideas need — but today it’s tangled (one array
conflates raw+filtered, homegrown un-removable listeners, magic-number
threshold). Re-model it explicitly (raw → filter →
interpolate → clamp → broadcast) and four backlog items snap onto
it.
custom.js is ~81% droppable theme
boilerplate. It’s the stock “Enjoy PWA / Sticky
UI” template (Envato/WebAppKit). Only ~5 things are genuinely
wired in (below). The rewrite deletes ~1,200 lines rather than
porting them — the single biggest simplicity win.
“Offline-first” is currently aspirational, not
real. The service worker caches nothing
(REQUIRED_FILES = [], _service-worker.js:10) —
only GTFS data in IndexedDB survives a connectivity loss; the
app shell would not load offline. The Angular rewrite
(@angular/service-worker) would actually deliver
offline for the first time. Treat as an upgrade
opportunity, not just “don’t regress.”
The domain core (chainage routing) is clean and framework-agnostic and maps 1:1 to Angular signals. It’s pure Turf math with no Knockout entanglement. Protect it with golden regression tests before porting (already a §4 foundation in ideas.md).
The data layer is already headless and
well-structured (DataManager = pure Dexie access,
batch-then-join, compound indices). Ports cleanly to an
@Injectable. But it carries a latent data-loss
bug (Dexie schema jump with no migration) and a broken/dead
method.
Communication is direct-reference + a homegrown listener
array with no teardown. This is the main architectural smell.
Angular’s DI + RxJS Subject/BehaviorSubject
(the org standard) replaces it and fixes a real memory/stale-callback
leak.
UI is Bootstrap 5 utility classes; org standard is
Angular Material. A genuine (mechanical-ish) theming migration
— the sticky-ui look need not be preserved (per your
brief).
These are the deliberate, good patterns — the “reusable managers, avoid boilerplate, happy medium” ethos showing through:
managers/
are headless (data, location), view-models/ hold UI state,
templates bind. Maps directly to Angular services + components.getEnrichedShapes: 4 batched round-trips, not N+1) and
explicit compound Dexie indices on
stop_times.app.js
$(document).ready) wiring the graph explicitly — becomes
app.config.ts + root providers.#region feature-grouping in large
files — IDE-agnostic, worth keeping in big Angular services.| Theme | Verdict | Angular direction |
|---|---|---|
ES6 class view-models |
Keep | → @Component / @Injectable classes |
var self = this aliasing (KO era) |
Drop | Lexical this + arrow fns; decorators remove the
need |
snake_case for props/observables |
Adapt | Keep snake_case for GTFS field names (bridges DB schema); use camelCase for app code |
Positional listener callback args
(ts, lat, lon, bearing, listener, index) |
Drop | Emit a typed Position object via RxJS; never leak
listener/index to callers |
ko.observable / computed /
observableArray |
Adapt | signal / computed / arrays-in-signals;
combineLatest+toSignal for cross-stream |
valueHasMutated() poke on mutated plain arrays |
Drop | Immutable updates into signals/BehaviorSubjects |
// #region grouping |
Keep | Useful in large service files |
Debug via window.model global + console.*
gated on ad-hoc flags |
Adapt | Angular DevTools + a LoggingService; dev-only debug
accessor |
jQuery $() for DOM measurement inside view-models |
Drop | ElementRef / ResizeObserver / CSS; never
DOM in services |
| Comment density (moderate, intent-focused, no JSDoc) | Keep/Adapt | Fine; add typed signatures via TypeScript instead of JSDoc |
Misspellings carried through API
(accordian_clicked) |
Drop | Fix on the way through |
| Theme | Verdict | Angular direction |
|---|---|---|
Headless DataManager (Dexie access layer) |
Keep | @Injectable DataService; add a
service-level cache (signal/memo) |
| Compound-index Dexie schema | Keep | Preserve indices; add explicit .version()
migrations (see risks) |
getEnrichedShapes batch-then-join |
Keep | Same; cache result, don’t re-run per search |
Generic updateTable/purgeTable write
surface |
Keep | Adequate for admin/sync |
| Constructor DI, single composition root | Keep | Native Angular DI + app.config.ts |
Homegrown listener array (addListener,
no removeListener) |
Adapt | BehaviorSubject/Subject in a
LocationService; toSignal() +
takeUntilDestroyed() — fixes the leak |
| 500 ms fixed-interval re-broadcast (fires even when unchanged) | Adapt | Emit on new fix (+ interpolation ticks); reactive downstream |
Raw vs filtered position conflated in one #previous
array |
Adapt | Explicit position pipeline stages; typed
RawFix → ProcessedPosition |
override() synthetic GPS |
Keep | Mock source substituted into the pipeline via a
BehaviorSubject toggle |
Inter-VM comms via direct refs + set_map() setter
injection |
Adapt | Shared services/observables; lifecycle hooks
(ngAfterViewInit, @ViewChild) replace setter
injection |
Back-action stack on appVM |
Adapt | NavigationService stack or Angular Router history —
concept valid, appVM is the smell |
| Activity/Trip/Transfer inheritance + template-name discriminator | Adapt | @switch / NgComponentOutlet on activity
type; Transfer may be just a type/interface |
Lazy getTripViewModel() proxy + manual memoization |
Adapt | Service method memoizing by key
(Map<id, Observable>); @defer for
UI |
| Callback-based “loaded” signalling from constructors | Drop | Async service methods returning Observables/Promises;
await in ngOnInit |
Runtime import_templates()
<script type="text/html"> injection |
Drop | Angular compiles templates at build — no equivalent needed |
Polymorphic data-bind="template: $data.template" |
Adapt | NgComponentOutlet / @switch |
Imperative Bootstrap Modal/Collapse inside
view-models |
Drop | Angular Material
MatDialog/MatBottomSheet/expansion, called
from components |
| Bootstrap 5 utility-class UI | Adapt | Migrate to Angular Material (org standard);
sticky-ui look need not be preserved |
| SweetAlert2 for confirms/progress | Adapt | Material dialogs; keep the confirm-before-destructive pattern |
GTFS version check (version.txt + localStorage) →
prompt → sync → location.reload() |
Keep / Adapt | Keep the polling trigger + trigger points; replace hard
reload() with reactive state refresh |
PapaParse CSV → chunked Dexie upsert + typeExceptions
coercion |
Keep | Wrap in a GtfsSyncService; port the coercion map (only
validation layer today) |
Global error capture
(window.onerror/unhandledrejection) |
Keep | Idiomatic Angular ErrorHandler;
persist (ties to telemetry outbox) |
| Service worker (cache-first but empty cache) | Adapt/Upgrade | @angular/service-worker: app-shell prefetch + network
strategy for CSV/API — delivers real offline |
| Webpack (dev-only, custom cache-busting plugin) | Drop | Angular CLI: prod builds + [contenthash] |
Vendor libs as window.* globals
(vendor.js) |
Drop | ES module imports |
| # | Item | Location | Severity | Action in rewrite |
|---|---|---|---|---|
| R1 | Dexie schema at version(2) only, no
upgrade() — v1 users get auto delete-and-recreate
→ silent GTFS data loss |
data-manager.js:6 |
High | Define explicit migrations; test upgrade path |
| R2 | Partial GTFS sync leaves DB half-purged (no transaction/rollback; loading dialog closes on failure) | app.js:~319 |
High | Wrap sync in a Dexie transaction; atomic swap |
| R3 | Offline shell impossible — SW caches nothing | _service-worker.js:10 |
High (vs offline-first goal) | @angular/service-worker app-shell caching |
| R4 | MapTiler API key hardcoded (live, load-bearing) | map-view-model.js:153 |
Med | Move to env/config service; rotate; referrer-restrict |
| R5 | Google Maps key hardcoded (in dead theme code) | custom.js:504 |
Low | Drops with the theme code; rotate anyway |
| R6 | Admin PIN [REDACTED-PIN] hardcoded in
source |
app.js:82 |
Med | Remove from source; server/config-driven if still needed |
| R7 | Enrollment gate disabled
(&& false) |
app.js:759 |
Med | Decide: real enrollment or remove; don’t ship a dead gate |
| R8 | Un-removable listeners + uncancellable 500 ms interval → leak on teardown | location-manager.js:67, no
removeListener |
Med | RxJS + takeUntilDestroyed() |
| R9 | N+1 query — getRoutes() per shape |
single-route-selection-view-model.js:24-75 |
Low | Fetch routes once, join in memory |
| R10 | Dead/broken method getShapesByPrefix
(refs bare db) |
data-manager.js:346 |
Low | Don’t port |
Not UI-editable, but centralised/overridable. Inventory to seed the config service:
| Constant | Value | Location |
|---|---|---|
| Off-route proximity threshold | 50 m | routing-view-model.js:42 |
| Chainage forward look-ahead window | 50 m | routing-view-model.js:167 |
| GPS noise filter (Δdist / accuracy) | 0.2 | location-manager.js:108 |
| Broadcast interval | 500 ms | location-manager.js:67 |
| Position history buffer size | 10 | location-manager.js:~122 |
| Turn-highlight buffer | 0.05 km / 0.003 km | map-view-model.js:253 |
| Distance display rounding tiers | 5 m / 50 m / 0.1 km | routing-view-model.js:227-233 |
| GPS “not started” timeout | 2000 ms | diagnostic-view-model.js:81 |
| Error-log max entries | 100 | error-log-view-model.js:6 |
| Map default centre | -27.528, 153.197 (Brisbane) | map-view-model.js:8-9 |
| GTFS import chunk size | 1000 rows | app.js |
| MapTiler theme UUIDs | (2) | map-view-model.js:2-3 |
| NEW (ideas.md): on-time trigger 2 km/h · interaction-lock 10 km/h | — | to add |
custom.js
triage — what to actually port (~10% of 1,481 lines)Origin: “Enjoy PWA / Sticky UI” template
(WebAppKit). ~81% dead theme features (image sliders, social share, QR,
contact form, search page, age gate, OTP, vibrate, ads, stepper,
working-hours, file-upload, geolocation demo, Swup page transitions —
all confirmed absent from index.html/templates).
Port these 5–6 only (as Angular idioms): 1.
Service-worker registration → @angular/pwa
schematic. 2. Dark/light theme toggle (body-class + the
MutationObserver bridge to
MapViewModel.set_theme()) → theme service + effect. 3.
menu-settings open/close (the only menu
actually used) → Material MatBottomSheet/sidenav or a small
toggle service. 4. Online/offline connectivity banner →
legitimately useful on a bus PWA; Angular has no equivalent elsewhere —
reimplement. 5. Enrollment-card full-height sizing
(card_extender) → CSS 100dvh. 6.
(cheap) OS-detection body classes
(device-is-ios/android) if the CSS uses them for safe-area
insets — verify.
Verify (uncertain) before deleting:
data-gradient/data-highlight CSS-link
injection (custom.js:356-408) — load-bearing only if those
highlight_*.css files aren’t already bundled; Bootstrap
dropdown/toast wiring; data-back-button (superseded by the
KO back-action stack).
PositionSource +
ordered stages up front.BehaviorSubject
services (org standard) — removes the direct-ref/listener smell
and the teardown leak in one move.custom.js —
reimplement only the 5–6 items above.