TBT — Style & Architecture Review

TBT — Style & Architecture Themes (current codebase)

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.md and ../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.


Executive summary (the 7 things that matter)

  1. 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.

  2. 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.

  3. “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.”

  4. 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).

  5. 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.

  6. 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.

  7. 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).


Cross-cutting: what the current code does well (carry the DNA forward)

These are the deliberate, good patterns — the “reusable managers, avoid boilerplate, happy medium” ethos showing through:


Style themes (consolidated)

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

Architecture / approach themes (consolidated)

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 RawFixProcessedPosition
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

Risks & security register (surface early — several feed the plan)

# 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 querygetRoutes() 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

Hardcoded constants → move to the config service (§4 of ideas.md)

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).


Recommendations into the plan

  1. Promote the position pipeline to a first-class design artifact — it unifies location-manager cleanup + smoothing + clamping + depot + gps-test. Define PositionSource + ordered stages up front.
  2. Write golden regression tests for chainage/off-route/next-stop against recorded GPS traces before porting (locks the special sauce).
  3. Treat the SW/offline story as a deliverable feature, not a carry-over — it doesn’t work today.
  4. Fix R1/R2 (data-loss risks) as part of the data-layer port — atomic, migration-safe GTFS sync.
  5. Central config service seeded from the constants inventory; keys out of source (R4–R6).
  6. Standardise comms on RxJS BehaviorSubject services (org standard) — removes the direct-ref/listener smell and the teardown leak in one move.
  7. Delete, don’t port, custom.js — reimplement only the 5–6 items above.
  8. Decide enrollment (R7) and admin-PIN (R6) intent explicitly — both are currently dead/insecure placeholders.