Skip to content

🚲 Feature 02: Virtual Garage & Component Wear Telemetry ​

NOTE

🧭 Navigation: 🧩 Features Index | 🚲 Wiki Home

The Virtual Garage module functions as a specialized enterprise inventory and dynamic wear tracking platform for bicycle equipment. It enables riders to catalog component lifetimes (chains, cassettes, tires, brake pads) and monitor dynamic degradation over cumulative distance (kilometers) and operational hours.


πŸ” How it Works (Product perspective) ​

  1. Digital Bike Inventory:
    • Cyclists can configure multiple bicycles in their profile, specifying brand, model, type (Road, MTB, Gravel), and a starting frame mileage.
  2. Component Separation & Swapping:
    • Components are modeled as independent assets. They are dynamically mounted and unmounted from bicycle frames rather than being hardcoded to a single bike. This mirrors real-world workshop operations.
  3. Dual-Wear Metrics:
    • Component wear is measured across two metrics simultaneously:
      • Distance (km): Lifespan based on odometer mileage.
      • Time (Hours): Lifespan based on cumulative duration.
    • High-density visual progress indicators in the UI change colors dynamically to communicate remaining health:
      • 🟒 Health > 50%: Safe state, standard green styling.
      • 🟑 Health 20% - 50%: Caution state, warning yellow styling.
      • πŸ”΄ Health < 20%: Replacement required, alert red styling.
  4. Environmental Wear Multipliers:
    • The wear engine supports dynamic multiplier coefficients (e.g., x1.0 for dry road conditions, x2.5 for heavy mud/rain) that scale cumulative wear figures on currently mounted components after every ride.
  5. Strava Webhook Synchronization:
    • Integrates with the Strava API. Ride events matching a registered stravaGearId automatically trigger background webhook wear updates, adjusting lifespans on mounted parts without manual user input.

πŸ—οΈ Technical Architecture & Key Files ​

πŸ–₯️ Frontend State & UI Components ​

  • UI Widgets:
    • apps/web/src/features/garage/ (handles component cards, inventories, and mounting forms).
    • apps/web/src/widgets/garage-panel/ (dashboard grid layout container).
  • Zustand State Store: useGarageStore in apps/web/src/features/garage/model/garageStore.ts.
    • Slice Fields:
      • bikes: Bike[] (list of registered frames).
      • components: Component[] (all components owned by the user).
      • activeBikeId: string | null (currently selected frame for detailing).
      • isLoading: boolean (network loading state).
      • isSyncing: boolean (Strava sync operation indicator).

πŸ”Œ API Integration & Wear Engine ​

  • Hono API Routers:
    • GET /api/garage/bikes & POST /api/garage/bikes & DELETE /api/garage/bikes/:id
    • GET /api/garage/components & POST /api/garage/components & DELETE /api/garage/components/:id
    • POST /api/garage/components/mount (binds a component to a bike, unmounting it from prior frames).
    • POST /api/garage/components/unmount (dismounts component from frame).
    • POST /api/garage/components/service (resets service meters, logs maintenance log entry).
    • POST /api/garage/wear/apply (manually distributes distance and multiplier wear transactional calculations).
    • POST /api/garage/webhooks/strava-simulate (simulates callback webhooks).
  • Wear Processor: apps/api/src/services/wear-calculator.ts handles the backend calculations of dynamic wear under condition multipliers.

πŸ’Ύ Database Schema (Drizzle & PostgreSQL) ​

Persisted across five distinct tables in apps/api/src/db/schema.ts:

  1. bikes:
    • id: uuid (PK)
    • userId: text (references users.id cascade)
    • name, brand, model, type, stravaGearId: text
    • totalKm: doublePrecision
    • isIgnored: boolean
  2. components:
    • id: uuid (PK), userId: text, bikeId: uuid (FK references bikes.id set null)
    • name, brand, model, price: doublePrecision, category: text, status: text
    • Lifespan parameters: maxLifespanKm, currentLifespanKm, maxLifespanHours, currentLifespanHours, serviceIntervalKm, currentServiceKm, serviceIntervalHours, currentServiceHours.
  3. component_mounts:
    • id: uuid, componentId: uuid (cascade), bikeId: uuid (cascade), mountedAt: timestamp, unmountedAt: timestamp.
  4. maintenance_logs:
    • id: uuid, componentId: uuid (cascade), bikeId: uuid (set null), actionType: text, notes: text, kmAtService: doublePrecision, performedAt: timestamp.
  5. ride_wear_logs:
    • id: uuid, bikeId: uuid (cascade), trackId: text, distance: doublePrecision, multiplier: doublePrecision, appliedAt: timestamp. Includes unique constraint on (bikeId, trackId) to prevent duplicate webhook/activity application.

πŸ§ͺ Test Suite Coverage & Regressions ​

πŸ”¬ Vitest Unit Tests ​

  • apps/web/src/features/garage/model/__tests__/garageStore.test.ts
  • apps/api/src/routes/__tests__/garage.router.test.ts
    • Validates transactional dual-wear calculations, Strava webhook simulation payloads, and component-to-bike validation checks.

🎭 Playwright E2E Tests ​

  • apps/web/tests/garage.spec.ts (Validates bike lifecycle: Add, equip component, inspect color indicators, unmount).
  • apps/web/tests/garage-history.spec.ts (Asserts maintenance log history lists).
  • apps/web/tests/garage-inventory-flow.spec.ts (Asserts inventory limits).
  • apps/web/tests/garage-strava-webhook.spec.ts (Validates webhook simulated wear).
  • apps/web/tests/visual-garage-audit.spec.ts (Asserts UI element collisions).

🐞 Known Issues & Resolved Bugs ​

  • React Hook Form Numeric NaN crash (Resolved): RHF passing empty strings to valueAsNumber: true fields resulted in NaN schemas, which crashed Postgres database operations.
    • The Fix: Configured a customized preprocessNumber inside packages/shared/src/schemas/garage.ts that safely coerces empty, null, or string NaN inputs back to safe fallback integers (0 or undefined) on submission.
  • Modals Key Accessibility (Resolved): Users could not dismiss modals easily.
    • The Fix: Embedded a global keydown event listener capturing Escape events inside both the AddBikeModal.tsx and EquipComponentModal.tsx panels.
  • Double Submission Vulnerability (Resolved): Rapid double clicks on the "Save" or "Equip" buttons spawned duplicate records.
    • The Fix: Configured active submitting trackers to lock input fields, disable buttons, and change the CTA text to "Equipping..." or "Saving...".

πŸ‘£ Step-by-Step Manual Verification ​

  1. Click the Garage (Π“Π°Ρ€Π°ΠΆ) icon in the top toolbar to slide open the Telemetry dashboard.
  2. Click Add Bike (Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ вСлосипСд).
  3. Fill in the name, set the starting mileage, and hit Save. Confirm the bike appears in your digital rack.
  4. In the form, clear the mileage completely and click submit. Verify the form coerces it to 0 cleanly without crashing or throwing a Zod schema validation error.
  5. Open your newly created bike panel and select Equip Component (Π£ΡΡ‚Π°Π½ΠΎΠ²ΠΈΡ‚ΡŒ Π΄Π΅Ρ‚Π°Π»ΡŒ).
  6. Create a component (e.g. Chain) and click save. Confirm it mounts immediately and displays a healthy green indicator.
  7. Hit Escape key inside the modal overlay and verify the modal closes instantly.

πŸ“Έ Interface Artifacts ​