Skip to content

refactor: remove the duplication and clarity findings from the review - #365

Merged
joszz merged 4 commits into
mainfrom
refactor/remove-duplication
Sep 16, 2026
Merged

joszz merged 4 commits into
mainfrom
refactor/remove-duplication

Conversation

@joszz

@joszz joszz commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Pull Request

Summary

Works through the duplication and clarity list from the review. Nothing here changes what the app does, with two exceptions called out below.

Frontend

  • Card registry. A dashboard card was described in four places: the defaults list, an icon map, a has-data switch, and DashboardCardContent's own config table, which repeated the icons and data checks again. src/cards/registry.ts now holds one entry per card (icon, default visibility per drivetrain, whether the telemetry has anything to show), and the card ids, default layout, skeleton, edit-mode placeholders and render guards are all derived from it.
  • Map layers. usePoiLayers carried the same fetch-id guard, tile bookkeeping, cache and cluster handling three times. That engine is now composables/poiTileLayer.ts, and the three layers are configurations of it. "This layer does not apply to this vehicle" folds into the layer's enabled state rather than a watch each.
  • Vehicle lookup. vehicles[0] in nine components becomes store.activeVehicle / store.activeVin.
  • StatsChartCard took bar data cast as ChartData<'line'>. It takes the chart kind as a prop and renders through vue-chartjs's own Chart, so the cast is gone.
  • Styles. MapView's style block was unscoped for the sake of the Leaflet markers in it; only those stay global. Stylelint now runs over the stylesheets (part of pnpm lint and the lint job), which is what found the duplicate :root and the duplicated flex-shrink.

Backend

  • Model-derived filters. HasData listed seventy fields by hand, beside merge loops that read the same fields off the model reflectively, so a new telemetry field was treated as empty until someone extended the list. Both filters are built from a type now, the chart one from TelemetryHistoryPoint, whose field list it was copying.
  • Shared startup. The API and Worker configured Serilog and the tyre thresholds identically; both now use Core.Configuration.HostingExtensions.
  • Repository access. Three Worker services read db.Vehicles past IVehicleRepository. The one write that made that awkward, the parked-at timestamp, has a repository method.
  • Vehicle type. The frontend parsed hw_version itself, duplicating VehicleTypeHelper. The vehicle list endpoint reports the detected drivetrain.

Deployment

  • The api and worker environment blocks were copies, and env_file: .env handed every container every secret. The shared half is a YAML anchor; each service lists what it gets. The Worker no longer receives the OIDC secret, the widget key or the MG credentials.
  • pnpm is pinned once, in packageManager, which corepack and pnpm/action-setup both read (was four pins in CI, two in Dockerfiles). The release workflow installs the MinVer version from Directory.Packages.props: it was pinned to minver-cli 7.0.0 while the package had moved to 8.0.0, so the image tag and the version stamped inside it could come from different majors.
  • Runtime defaults live in the app; the compose files and the all-in-one entrypoint pass a setting through only when the operator sets one, so the tyre thresholds are not restated in three files.

Two behaviour changes

  1. Login recovery (bug fix). The auth store cached its config request for the life of the page, failures included, so an API that was not up yet left the login page reading "No sign-in method is available" with no form and no retry. A restart of the API container was enough. Failures are no longer cached, the page retries, and the message now distinguishes "could not reach the API" from "the API has nothing configured". Found because it made the smoke suite flaky; reproduced and verified against the demo stack.
  2. Cards with no data stay hidden. remainingCharge and speed could render an empty card (a charge timer on an HEV, speed before any is reported). The registry's predicates cover those cases, so the slot is dropped instead.

Type of Change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation
  • Dependency update
  • Other (build and deployment configuration)

Related Issues

Follow-up to the review findings raised after #364.

Checklist

  • Tests added or updated
  • Linting passes (pnpm lint / dotnet build)
  • No secrets or personal data committed
  • Responsive on mobile (no layout changes)
  • i18n keys added for any new UI strings (auth.apiUnreachable, common.retry, both languages)

Verification

  • Backend: 467 tests pass (12 new), build clean with 0 warnings, dotnet format clean, no pending model changes.
  • Frontend: 324 tests pass (21 new, covering the card registry and the tile-layer engine), type-check, ESLint, oxlint, Stylelint and Prettier clean.
  • Browser: the 8 smoke tests pass against a rebuilt demo stack, including immediately after an API restart, which used to fail.

🤖 Generated with Claude Code

joszz and others added 4 commits September 16, 2026 15:07
Four places restated something the code already knew.

The repository's "does this row carry telemetry" filter listed seventy fields by
hand, next to merge loops that read the same fields off the model by reflection.
A field added to the model was silently treated as empty until someone extended
that list. Both filters are now built from a type: the general one from the model,
the chart one from TelemetryHistoryPoint, whose field list it was copying.

The API and the Worker each configured Serilog and the tyre pressure thresholds
with identical blocks. They now share GarageStack.Core.Configuration.
HostingExtensions, and the thresholds fall back per value, so a deployment that
passes an empty string gets the app's defaults rather than zeroes.

Three Worker services read db.Vehicles directly, past IVehicleRepository. They go
through the repository now; the one write that made this awkward, the parked-at
timestamp, has a repository method rather than a tracked entity save.

The frontend detected the drivetrain by parsing hw_version itself, duplicating
VehicleTypeHelper. The vehicle list endpoint now reports the detected type.

Also fixes a flaky test: the command gate's hold window was measured with
DateTime.UtcNow, whose ~15ms tick can read a full wait back as slightly short.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A dashboard card was spread over a defaults list, an icon map, a has-data switch
and the renderer's own config table, which repeated the icons and the data checks
a third time. src/cards/registry.ts now describes each card once: icon, default
visibility per drivetrain, and whether the telemetry holds anything for it. The
card ids, the default layout, the skeleton, the edit-mode placeholders and the
render guards all read from it, so a new card is one entry plus its markup.

usePoiLayers repeated the same state and loading logic three times, once per
layer, with the fetch-id guard, tile bookkeeping and cluster handling copied each
time. That engine now lives in composables/poiTileLayer.ts, and charging stations,
fuel stations and service areas are three configurations of it, each with its own
fetch, filter and marker. Layers that do not apply to the vehicle (a plug for an
HEV, a tank for a BEV) fold into the layer's "enabled" instead of a watch apiece.

Components looked the car up as vehicles[0] in nine places; the store now exposes
activeVehicle/activeVin, and the drivetrain comes from the API rather than from
parsing hw_version in the browser.

StatsChartCard took bar data cast as line data. It now takes the chart kind as a
prop and renders through vue-chartjs's own Chart component, so neither side lies.

MapView's style block was entirely unscoped for the sake of the Leaflet markers in
it; only those stay global now. Stylelint runs over the stylesheets, which is how
the duplicate rule in main.css and the duplicated flex-shrink turned up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tings

The api and worker services repeated their whole environment block, and both took
env_file: .env, which handed every container every secret in the file. The shared
half is a YAML anchor now, and each service lists what it gets: the Worker no
longer receives the OIDC client secret, the widget key or the MG credentials, and
the API no longer receives the notification language. Compose reads .env for
${...} interpolation either way, so nothing needs the blanket passthrough.

Versions that sat in several files now sit in one. pnpm lives in package.json's
packageManager field, which corepack and pnpm/action-setup both read, replacing
four pins in CI and two in Dockerfiles. The release workflow installs the same
MinVer the build uses, read from Directory.Packages.props: it was pinned to
minver-cli 7.0.0 while the package had moved to 8.0.0, so the image tag and the
version stamped inside it could come from different majors.

Runtime defaults belong to the app, so the compose files and the all-in-one
entrypoint pass a setting through only when the operator sets one. The tyre
pressure thresholds and the rate limit no longer restate the app's own numbers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The auth store cached its config request for the life of the page, including a
failed one. If the API was not up yet, the login page showed "No sign-in method
is available" and stayed that way: no form, no retry, nothing but a reload. A
container restart was enough to trigger it, which is how it surfaced.

A failed attempt is no longer cached, the page retries a few times before giving
up, and the message it does show says the API could not be reached and offers a
retry. "No sign-in method" now means what it says: the API answered and has
nothing configured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@joszz
joszz merged commit 049a657 into main Sep 16, 2026
22 checks passed
@joszz
joszz deleted the refactor/remove-duplication branch September 16, 2026 13:21
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