From 3215dc8d2dda8b680804ab0bda9fd96ccabaa0b5 Mon Sep 17 00:00:00 2001 From: Javed Hussain Date: Thu, 19 Mar 2026 17:19:27 +0530 Subject: [PATCH 01/15] Create 1.3.0.md --- changelogs/1.3.0.md | 115 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 changelogs/1.3.0.md diff --git a/changelogs/1.3.0.md b/changelogs/1.3.0.md new file mode 100644 index 00000000..1bdb9aa6 --- /dev/null +++ b/changelogs/1.3.0.md @@ -0,0 +1,115 @@ +# Release Notes – v1.3.0 (2026-03-19) + +## Overview + +v1.3.0 focuses on the settings experience, notification delivery controls, clearer overview charts, and a broad cleanup of shared helpers and notification flows. It also improves demo seed data so mileage and cost trends look more realistic over time. + +## Major Features + +### Settings UX Improvements + +- Expanded all settings accordions by default for quicker access +- Made settings forms more responsive with two- and three-column layouts where space allows +- Added a compact switch-based style for feature flags +- Updated notification provider forms to default new providers to enabled +- Simplified channel subscription controls in the provider dialog +- Extracted reusable settings sections, field helpers, and display blocks +- Reused shared tab shell and form composition patterns in settings + +### Notification Delivery Controls + +- Added a toggle to enable or disable scheduled notification delivery +- Kept the delivery schedule tied to the notification settings state +- Disabled schedule inputs automatically when scheduling is turned off +- Added webhook and Gotify providers alongside email +- Improved provider toggling, cron scheduling, and notification send templates +- Allowed editing providers without exposing keys/tokens + +### Overview Charts + +- Added an average reference line to mileage and cost graphs +- Displayed the average as a top-right label with unit-aware formatting +- Formatted tooltip values with the correct units for mileage and currency +- Added a unit-aware average formatter for chart tooltips and labels +- Improved the chart presentation with clearer dashed average lines + +### Demo Seed Data + +- Updated seeded mileage values to progress more naturally over time +- Added small mileage deviations and realistic fuel cost variation +- Made the overall seed data better reflect real-world usage patterns +- Refined seeded notifications and maintenance history for more natural trends + +## Configuration Changes + +- Added `notificationProcessingEnabled` to control scheduled notification delivery +- Kept `notificationProcessingSchedule` as the cron expression for delivery timing +- Added default config values for LPG and CNG fuel units +- Preserved mileage unit format settings for distance-per-fuel and fuel-per-distance +- Expanded settings schema and defaults for feature flags and notification delivery +- Updated chart formatting helpers to support unit-aware mileage and currency display +- Added shared config handling and merge helpers for notification provider settings +- Seeded the new config entries automatically for demo setups + +## Environment/Runtime Changes + +- Demo seeding now generates more realistic mileage and cost trends +- Notification scheduler respects the new enabled/disabled config state +- Overview charts now format tooltip and average values using app units +- The app now keeps chart and settings formatting aligned with the active locale/config + +## UI/UX Improvements + +- Compact provider channel subscriptions in the add/edit provider dialog +- Better chart labeling and readability in the overview section +- More consistent settings layout across personalization, units, and feature flags +- Better mobile behavior and tighter spacing across settings and dialogs +- Improved loading skeletons and shared record card layouts across the UI + +## Architecture & Shared Helpers + +- Consolidated route error helpers and standardized backend service responses +- Added typed payload helpers for domain and service layers +- Reduced shared store and form `any` usage +- Extracted reusable table, skeleton, and formatter helpers +- Reused shared resource state and feature card layouts across the UI +- Improved notification provider config merge and service date helpers +- Added helper reuse across vehicle, fuel, maintenance, insurance, and reminders + +## Localization & Messaging + +- Continued moving hardcoded UI text into i18n message functions +- Added or refined translated messages for settings, notifications, and charts +- Improved localized labels across dashboard, forms, and notifications + +## Developer Experience + +- Added MCP support for the repo's Svelte workflow +- Upgraded dependencies and fixed follow-up lint/check issues +- Cleaned up formatting, typing, and shared abstractions across the codebase + +## Bug Fixes & Improvements + +- Fixed reactive binding issues in settings forms so inputs update correctly +- Fixed notification delivery scheduling state so it no longer re-enables unexpectedly after save +- Improved unit display for mileage and cost values in chart tooltips and labels +- Fixed settings accordions to stay expanded by default +- Fixed provider add/edit flow to keep new providers enabled by default +- Fixed fuel and maintenance sorting when records share dates +- Fixed NaN-prone calculations and lint issues carried over from refactors + +## Migration Notes + +- No breaking changes were introduced +- Existing settings and data remain compatible + +## Environment Variables + +- No new environment variables were required for this release +- Existing runtime behavior continues to use the current app configuration and demo flags + +## Known Issues + +- None reported at release time + +For detailed commit history, see the [compare view](https://github.com/javedh-dev/tracktor/compare/v1.2.0...v1.3.0). From e210f4a4352ef82c0a9d103e7b50ede49eb4bd47 Mon Sep 17 00:00:00 2001 From: pHama <18518012+pHamala@users.noreply.github.com> Date: Sun, 22 Mar 2026 15:05:17 +0200 Subject: [PATCH 02/15] Update Finnish translations for 1.3.x --- messages/fi.json | 109 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 4 deletions(-) diff --git a/messages/fi.json b/messages/fi.json index 375c6b46..4ce17f09 100644 --- a/messages/fi.json +++ b/messages/fi.json @@ -3,6 +3,7 @@ "hello_world": "Hei, {name} fi:stä!", "app_name": "Tracktor", "app_title": "Autotallisi", + "app_new_update_available": "Uusi päivitys saatavilla. Ladataan uudelleen..!", "app_add_vehicle": "Lisää ajoneuvo", "app_empty_select_message": "Valitse ajoneuvo nähdäksesi sen yksityiskohdat", "app_empty_select_hint": "Valitse ajoneuvo nähdäksesi sen kojelaudan", @@ -50,7 +51,7 @@ "settings_updated_success": "Asetukset päivitetty", "common_example_prefix": "Esimerkiksi - ", "common_invalid_format": "Virheellinen muoto", - "common_kilometer": "Kilometri", + "common_kilometer": "Kilomcommon_exaetri", "common_mile": "Maili", "common_litre": "Litra", "common_gallon": "Gallona", @@ -79,7 +80,6 @@ "nav_insurance": "Vakuutus", "nav_pollution": "Päästöt", "nav_reminders": "Muistutukset", - "nav_settings": "Asetukset", "tools_export_data": "Vie data", "tools_import_data": "Tuo data", "vehicle_form_make_label": "Valmistaja", @@ -163,7 +163,7 @@ "fuel_add_title": "Lisää tankkaus", "col_date": "Päivämäärä", "col_odometer": "Matkamittari", - "col_filled": "Täytetty", + "col_filled": "Täysi tankki", "col_missed_last": "Edellinen ohitettu", "col_fuel_amount": "Polttoainemäärä", "col_cost": "Kustannus", @@ -204,6 +204,8 @@ "notifications_section_alerts": "Hälytykset", "notifications_mark_done_title": "Merkitse muistutus valmiiksi", "notifications_mark_done_aria": "Merkitse {type} muistutus valmiiksi", + "notifications_mark_all_read_title": "Merkitse kaikki luetuiksi", + "notifications_mark_all_read_aria": "Merkitse kaikki ilmoitukset luetuiksi", "notifications_overdue_days": "{days} päivää myöhässä", "notifications_due_today": "Määräaika tänään", "notifications_due_tomorrow": "Määräaika huomenna", @@ -449,5 +451,104 @@ "pollution_recurrence_type_fixed": "Kiinteä päättymispäivä", "pollution_recurrence_type_yearly": "Uusiutuu vuosittain", "pollution_recurrence_type_monthly": "Uusiutuu kuukausittain", - "pollution_recurrence_type_no_end": "Ei päättymispäivämäärää" + "pollution_recurrence_type_no_end": "Ei päättymispäivämäärää", + "file_drop_existing_note": "Liitetiedosto (Napauta nähdäksesi)", + "fuel_import_step_1_title": "Vaihe 1 : Lataa CSV tiedosto", + "fuel_import_step_1_desc": "Valitse erotinmerkkiä käyttävä tekstitiedosto, joka sisältää polttoaineen kulutustietosi, aloittaaksesi tuontiprosessin", + "fuel_import_drop_placeholder": "Liitä erotinmerkillinen teksti tai napauta selataksesi", + "fuel_import_headers_checkbox": "Ensimmäinen rivi sisältää otsikot", + "fuel_import_delimiter_title": "Erotin", + "fuel_import_delimiter_desc": "Valitse merkki, joka erottaa kentät", + "fuel_import_date_format_title": "Päivämäärän muoto", + "fuel_import_date_format_desc": "Määritä CSV-tiedostosi päivämäärille käytettävä muoto.", + "fuel_import_error_no_headers": "Otsikoita ei havaittu. Päivitä csv.helper.ts palauttaaksesi otsikot.", + "fuel_import_step_2_title": "Vaihe 2 : CSV-sarakkeiden kartoitus", + "fuel_import_step_2_desc": "Määritä CSV-tiedostosi sarakkeet vastaaviin polttoainelokin kenttiin. Pakolliset kentät on merkitty merkinnällä *.", + "fuel_import_step_3_title": "Vaihe 3 : Esikatsele ja tuo", + "fuel_import_step_3_desc": "Tarkista tuotavat tiedot esikatselusta.", + "fuel_import_no_preview": "Ei esikatseltavaa tietoa vielä. Toteuta jäsentely csv.helper.ts-tiedostossa rivien täyttämiseksi.", + "fuel_import_success": "Tuotu onnistuneesti {count} polttoainelokia.", + "fuel_import_failed_count": "Tuotu {imported}, epäonnistui {failed}", + "fuel_import_error_generic": "Polttoainelokin tuonti epäonnistui.", + "fuel_import_vehicle_label": "Ajoneuvo:", + "fuel_import_delimiter_comma": "Pilkku ( , )", + "fuel_import_delimiter_semicolon": "Puolipiste ( ; )", + "fuel_import_delimiter_tab": "Sarkain ( \\t )", + "fuel_import_delimiter_pipe": "Pystyviiva ( | )", + "fuel_import_delimiter_custom": "Muokattu", + "fuel_import_date_error": "Jotkin rivit sisältävät virheellisesti muotoiltuja päivämääriä \"{format}\"", + "fuel_import_date_format_placeholder": "esim., MM/DD/YYYY", + "fuel_import_date_invalid": "Virheellinen päivämäärä", + "fuel_import_no_vehicle": "Ajoneuvoa ei valittu", + "fuel_import_col_date_hint": "Tankkauspäivämäärä", + "fuel_import_col_odometer_hint": "Mittarilukema tankkaushetkellä", + "fuel_import_col_fuel_hint": "Tankattu määrä tai energia", + "fuel_import_col_cost_hint": "Kokonaiskustannus", + "fuel_import_col_filled_hint": "Onko tämä täysi tankki/lataus?", + "fuel_import_col_missed_hint": "Jäikö edellinen merkintä väliin?", + "fuel_import_col_notes_hint": "Lisätietoja", + "autocomplete_placeholder": "Kirjoita tai valitse...", + "autocomplete_loading": "Ladataan ehdotuksia...", + "autocomplete_no_results": "Ehdotuksia ei löydy. Voit kirjoittaa uuden arvon.", + "input_date_placeholder": "Valitse päivämäärä", + "loading_default_message": "Ladataan...", + "dropzone_placeholder_image": "Napauta tai vedä kuva ladataksesi", + "dropzone_placeholder_attachment": "Pudota kuva tänne tai napauta valitaksesi", + "dropzone_placeholder_default": "Napauta tai vedä tiedostoja ladataksesi", + "dropzone_error_single_file": "Lataa vain yksi tiedosto.", + "dropzone_error_file_size": "Tiedoston koko ylittää sallitun enimmäiskoon {size}.", + "dropzone_unknown_file": "Tuntematon tiedosto", + "dropzone_uploading": "Ladataan...", + "dropzone_supports": "Tuettu: {types}", + "dropzone_max_size": "Enimmäiskoko: {size}", + "dropzone_error_file_type": "Tiedoston tyyppi ei ole tuettu", + "dropzone_hint_accept_limit": "{types} enintään {size}", + "vehicle_details_color_aria": "Väri", + "vehicle_details_close_aria": "Sulje", + "attachment_link_view_title": "Näytä liite", + "file_preview_not_available": "Esikatselu ei ole saatavilla", + "file_preview_download_hint": "Tätä tiedostotyyppiä ei voi esikatsella suoraan. Lataa tiedosto tarkastellaksesi sitä.", + "file_preview_download_button": "Lataa tiedosto", + "file_preview_aria_download": "Lataa", + "file_preview_aria_close": "Sulje", + "theme_toggle_label": "Vaihda teemaa", + "fuel_log_edit": "Muokkaa", + "fuel_log_delete": "Poista", + "fuel_log_menu_open": "Avaa valikko", + "fuel_log_delete_success": "Poistetut polttoainelokimerkinnät", + "fuel_log_menu_sheet_title": "Päivitä polttoainelokimerkintä", + "fuel_log_delete_error": "Polttoainelokimerkinnän poistamisessa tapahtui virhe", + "color_picker_label": "Valitse väri", + "reminder_type_maintenance": "Huolto", + "reminder_type_insurance": "Vakuutuksen uusiminen", + "reminder_type_pollution": "Päästömittaus / katsastus", + "reminder_type_registration": "Rekisteröinti / ajoneuvovero", + "reminder_type_inspection": "Katsastus", + "reminder_type_custom": "Custom", + "alert_type_insurance": "Vakuutus", + "alert_type_pucc": "Päästötodistus", + "alert_status_expired_ago": "{label} vanheni {days} päivää sitten", + "alert_status_expires_in": "{label} vanhenee {days} päivässä", + "alert_status_valid_for": "{label} voimassa {days} päivää", + "alert_insurance_active_no_end": "Vakuutus on voimassa toistaiseksi", + "alert_pucc_active_no_end": "Päästötodistus on voimassa toistaiseksi", + "alert_record_not_found": "{label}-tietoja ei löydy. Lisää tiedot pysyäksesi vaatimusten mukaisena.", + "settings_error_format_not_valid": "Virheellinen muoto", + "common_kilogram_unit": "Kilogrammaa (kg)", + "common_pound_unit": "Paunaa (lb)", + "settings_section_fuel_types": "Polttoaineen tyyppi", + "settings_section_fuel_types_desc": "Valitse mittayksikkö kullekin polttoaineelle.", + "fuel_type_petrol_diesel": "Bensiini/Diesel", + "theme_slate": "Liuskekivi", + "theme_stone": "Kivi", + "theme_red": "Punainen", + "theme_rose": "Ruusu", + "theme_blue": "Sininen", + "theme_green": "Vihreä", + "theme_purple": "Violetti", + "theme_orange": "Oranssi", + "theme_yellow": "Keltainen", + "theme_teal": "Sinivihreä", + "theme_indigo": "Indigo", + "theme_pink": "Vaaleanpunainen" } From 71445effbc876c7f830939c27ad09524f4b748c4 Mon Sep 17 00:00:00 2001 From: pHama <18518012+pHamala@users.noreply.github.com> Date: Sun, 22 Mar 2026 15:10:04 +0200 Subject: [PATCH 03/15] fix(i18n): fix typos in Finnish translations --- messages/fi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/fi.json b/messages/fi.json index 4ce17f09..5333a127 100644 --- a/messages/fi.json +++ b/messages/fi.json @@ -51,7 +51,7 @@ "settings_updated_success": "Asetukset päivitetty", "common_example_prefix": "Esimerkiksi - ", "common_invalid_format": "Virheellinen muoto", - "common_kilometer": "Kilomcommon_exaetri", + "common_kilometer": "Kilometri", "common_mile": "Maili", "common_litre": "Litra", "common_gallon": "Gallona", From abdfdf0dbff06ba817a03cd629800434709cc37f Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Tue, 24 Mar 2026 13:49:31 +0000 Subject: [PATCH 04/15] Add UK MPG fuel consumption unit --- messages/en.json | 1 + .../components/feature/settings/SettingsModal.svelte | 6 +++++- src/lib/helper/format.helper.ts | 10 ++++++++++ src/lib/helper/settings-form.helper.ts | 11 +++++++++-- src/lib/types/settings.ts | 2 +- src/server/services/fuelLogService.ts | 9 +++++++++ 6 files changed, 35 insertions(+), 4 deletions(-) diff --git a/messages/en.json b/messages/en.json index 6c4e825c..c7536046 100644 --- a/messages/en.json +++ b/messages/en.json @@ -45,6 +45,7 @@ "settings_desc_mileage_format": "Choose how fuel efficiency is displayed", "settings_mileage_format_distance_per_fuel": "Distance per Fuel (e.g., km/L, mpg)", "settings_mileage_format_fuel_per_distance": "Fuel per Distance (e.g., L/100km)", + "settings_mileage_format_uk_mpg": "UK MPG (miles per imperial gallon)", "settings_desc_theme": "Choose your preferred theme", "settings_desc_custom_css": "CSS Styles for customizing the interface", "settings_select_language": "Select language", diff --git a/src/lib/components/feature/settings/SettingsModal.svelte b/src/lib/components/feature/settings/SettingsModal.svelte index 9cbd4ec1..7ca53d7f 100644 --- a/src/lib/components/feature/settings/SettingsModal.svelte +++ b/src/lib/components/feature/settings/SettingsModal.svelte @@ -50,7 +50,7 @@ unitOfLpg: z.enum(['liter', 'gallon', 'kilogram', 'pound']).default('liter'), unitOfCng: z.enum(['liter', 'gallon', 'kilogram', 'pound']).default('kilogram'), mileageUnitFormat: z - .enum(['distance-per-fuel', 'fuel-per-distance']) + .enum(['distance-per-fuel', 'fuel-per-distance', 'uk-mpg']) .default('distance-per-fuel'), theme: z.string().default('light'), customCss: z.string().optional(), @@ -177,6 +177,10 @@ { value: 'fuel-per-distance', label: m.settings_mileage_format_fuel_per_distance() + }, + { + value: 'uk-mpg', + label: m.settings_mileage_format_uk_mpg() } ]; diff --git a/src/lib/helper/format.helper.ts b/src/lib/helper/format.helper.ts index 9128565d..a66f92ee 100644 --- a/src/lib/helper/format.helper.ts +++ b/src/lib/helper/format.helper.ts @@ -210,6 +210,11 @@ const getMileageUnit = (vehicleType: string): string => { return `${fuelLabel}/100${distanceUnit}`; } + // only show uk mpg if miles and liters are used + if (configs.mileageUnitFormat === 'uk-mpg' && configs.unitOfDistance === 'mile' && fuelUnit === 'liter') { + return 'mpg'; + } + // Default: distance-per-fuel (e.g., km/L, mpg) const mileageUnit = `${configs.unitOfDistance}-per-${fuelUnit}`; const label = safeUnitLabel(mileageUnit); @@ -230,6 +235,11 @@ const formatMileage = (mileage: number, vehicleType: string): string => { return `${mileage.toFixed(2)} ${fuelLabel}/100${distanceUnit}`; } + // only show uk mpg if miles and liters are used + if (configs.mileageUnitFormat === 'uk-mpg' && configs.unitOfDistance === 'mile' && fuelUnit === 'liter') { + return `${mileage.toFixed(2)} mpg`; + } + // Default: distance-per-fuel (e.g., km/L, mpg) const mileageUnit = `${configs.unitOfDistance}-per-${fuelUnit}`; return ( diff --git a/src/lib/helper/settings-form.helper.ts b/src/lib/helper/settings-form.helper.ts index 01aa9f3d..f9cece9d 100644 --- a/src/lib/helper/settings-form.helper.ts +++ b/src/lib/helper/settings-form.helper.ts @@ -35,7 +35,7 @@ export function createSettingsConfigSchema( unitOfLpg: z.enum(['liter', 'gallon', 'kilogram', 'pound']).default('liter'), unitOfCng: z.enum(['liter', 'gallon', 'kilogram', 'pound']).default('kilogram'), mileageUnitFormat: z - .enum(['distance-per-fuel', 'fuel-per-distance']) + .enum(['distance-per-fuel', 'fuel-per-distance', 'uk-mpg']) .default('distance-per-fuel'), theme: z.string().default('light'), customCss: z.string().optional(), @@ -46,7 +46,10 @@ export function createSettingsConfigSchema( featureInsurance: z.boolean().default(true), featureOverview: z.boolean().default(true), notificationProcessingEnabled: z.boolean().default(true) - }); + }).refine((obj) => { + if (obj.mileageUnitFormat !== 'uk-mpg') return true; + return obj.unitOfDistance === 'mile' && obj.unitOfVolume === 'liter' + }, 'UK MPG calculation requires unit of distance to be miles and unit of volume to be litres.'); if (!options.includeNotificationProcessingSchedule) { return baseSchema; @@ -108,6 +111,10 @@ export function createSettingsOptions( { value: 'fuel-per-distance', label: m.settings_mileage_format_fuel_per_distance() + }, + { + value: 'uk-mpg', + label: m.settings_mileage_format_uk_mpg() } ], localeOptions: locales.map((code) => ({ diff --git a/src/lib/types/settings.ts b/src/lib/types/settings.ts index c853ea08..862db3b7 100644 --- a/src/lib/types/settings.ts +++ b/src/lib/types/settings.ts @@ -7,7 +7,7 @@ export interface SettingsFormShape extends Record { unitOfVolume: 'liter' | 'gallon'; unitOfLpg: 'liter' | 'gallon' | 'kilogram' | 'pound'; unitOfCng: 'liter' | 'gallon' | 'kilogram' | 'pound'; - mileageUnitFormat: 'distance-per-fuel' | 'fuel-per-distance'; + mileageUnitFormat: 'distance-per-fuel' | 'fuel-per-distance' | 'uk-mpg'; theme: string; customCss?: string; featureFuelLog: boolean; diff --git a/src/server/services/fuelLogService.ts b/src/server/services/fuelLogService.ts index eeb27a9d..c6653edb 100644 --- a/src/server/services/fuelLogService.ts +++ b/src/server/services/fuelLogService.ts @@ -40,6 +40,12 @@ export const getFuelLogs = async (vehicleId: string): Promise => { const mileageFormatConfig = await db.query.configTable.findFirst({ where: (config, { eq }) => eq(config.key, 'mileageUnitFormat') }); + const distanceUnit = (await db.query.configTable.findFirst({ + where: (config, { eq }) => eq(config.key, 'unitOfDistance') + }))?.value; + const volumeUnit = (await db.query.configTable.findFirst({ + where: (config, { eq }) => eq(config.key, 'unitOfVolume') + }))?.value; const mileageFormat = mileageFormatConfig?.value || 'distance-per-fuel'; const fuelLogs = await db.query.fuelLogTable.findMany({ @@ -102,6 +108,9 @@ export const getFuelLogs = async (vehicleId: string): Promise => { if (mileageFormat === 'fuel-per-distance') { // Fuel per 100 distance units (e.g., L/100km, gal/100mi) mileage = (totalFuel / distance) * 100; + } else if (mileageFormat === 'uk-mpg' && distanceUnit === 'mile' && volumeUnit === 'liter') { + // Miles per imperial gallon (mpg) + mileage = (distance / totalFuel) * 4.546; } else { // Distance per fuel unit (e.g., km/L, mpg) - default mileage = distance / totalFuel; From 72f24b69a950f09debbfe65ede4b7a8562e0950e Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Tue, 24 Mar 2026 13:51:28 +0000 Subject: [PATCH 05/15] Fix code formatting --- src/lib/helper/format.helper.ts | 12 +++++- src/lib/helper/settings-form.helper.ts | 52 +++++++++++++------------- src/server/services/fuelLogService.ts | 16 +++++--- 3 files changed, 47 insertions(+), 33 deletions(-) diff --git a/src/lib/helper/format.helper.ts b/src/lib/helper/format.helper.ts index a66f92ee..33467f9f 100644 --- a/src/lib/helper/format.helper.ts +++ b/src/lib/helper/format.helper.ts @@ -211,7 +211,11 @@ const getMileageUnit = (vehicleType: string): string => { } // only show uk mpg if miles and liters are used - if (configs.mileageUnitFormat === 'uk-mpg' && configs.unitOfDistance === 'mile' && fuelUnit === 'liter') { + if ( + configs.mileageUnitFormat === 'uk-mpg' && + configs.unitOfDistance === 'mile' && + fuelUnit === 'liter' + ) { return 'mpg'; } @@ -236,7 +240,11 @@ const formatMileage = (mileage: number, vehicleType: string): string => { } // only show uk mpg if miles and liters are used - if (configs.mileageUnitFormat === 'uk-mpg' && configs.unitOfDistance === 'mile' && fuelUnit === 'liter') { + if ( + configs.mileageUnitFormat === 'uk-mpg' && + configs.unitOfDistance === 'mile' && + fuelUnit === 'liter' + ) { return `${mileage.toFixed(2)} mpg`; } diff --git a/src/lib/helper/settings-form.helper.ts b/src/lib/helper/settings-form.helper.ts index f9cece9d..a2e4c4f8 100644 --- a/src/lib/helper/settings-form.helper.ts +++ b/src/lib/helper/settings-form.helper.ts @@ -25,31 +25,33 @@ export function createSettingsConfigSchema( isValidTimezone: (value: string) => boolean, options: SettingsSchemaOptions = {} ) { - const baseSchema = z.object({ - dateFormat: z.string().refine((fmt) => isValidFormat(fmt).valid, 'Format not valid'), - locale: z.string().min(2), - timezone: z.string().min(3).refine(isValidTimezone, 'Invalid timzone value.'), - currency: z.string().min(1, 'Currency is required'), - unitOfDistance: z.enum(['kilometer', 'mile']), - unitOfVolume: z.enum(['liter', 'gallon']), - unitOfLpg: z.enum(['liter', 'gallon', 'kilogram', 'pound']).default('liter'), - unitOfCng: z.enum(['liter', 'gallon', 'kilogram', 'pound']).default('kilogram'), - mileageUnitFormat: z - .enum(['distance-per-fuel', 'fuel-per-distance', 'uk-mpg']) - .default('distance-per-fuel'), - theme: z.string().default('light'), - customCss: z.string().optional(), - featureFuelLog: z.boolean().default(true), - featureMaintenance: z.boolean().default(true), - featurePucc: z.boolean().default(true), - featureReminders: z.boolean().default(true), - featureInsurance: z.boolean().default(true), - featureOverview: z.boolean().default(true), - notificationProcessingEnabled: z.boolean().default(true) - }).refine((obj) => { - if (obj.mileageUnitFormat !== 'uk-mpg') return true; - return obj.unitOfDistance === 'mile' && obj.unitOfVolume === 'liter' - }, 'UK MPG calculation requires unit of distance to be miles and unit of volume to be litres.'); + const baseSchema = z + .object({ + dateFormat: z.string().refine((fmt) => isValidFormat(fmt).valid, 'Format not valid'), + locale: z.string().min(2), + timezone: z.string().min(3).refine(isValidTimezone, 'Invalid timzone value.'), + currency: z.string().min(1, 'Currency is required'), + unitOfDistance: z.enum(['kilometer', 'mile']), + unitOfVolume: z.enum(['liter', 'gallon']), + unitOfLpg: z.enum(['liter', 'gallon', 'kilogram', 'pound']).default('liter'), + unitOfCng: z.enum(['liter', 'gallon', 'kilogram', 'pound']).default('kilogram'), + mileageUnitFormat: z + .enum(['distance-per-fuel', 'fuel-per-distance', 'uk-mpg']) + .default('distance-per-fuel'), + theme: z.string().default('light'), + customCss: z.string().optional(), + featureFuelLog: z.boolean().default(true), + featureMaintenance: z.boolean().default(true), + featurePucc: z.boolean().default(true), + featureReminders: z.boolean().default(true), + featureInsurance: z.boolean().default(true), + featureOverview: z.boolean().default(true), + notificationProcessingEnabled: z.boolean().default(true) + }) + .refine((obj) => { + if (obj.mileageUnitFormat !== 'uk-mpg') return true; + return obj.unitOfDistance === 'mile' && obj.unitOfVolume === 'liter'; + }, 'UK MPG calculation requires unit of distance to be miles and unit of volume to be litres.'); if (!options.includeNotificationProcessingSchedule) { return baseSchema; diff --git a/src/server/services/fuelLogService.ts b/src/server/services/fuelLogService.ts index c6653edb..ef71c210 100644 --- a/src/server/services/fuelLogService.ts +++ b/src/server/services/fuelLogService.ts @@ -40,12 +40,16 @@ export const getFuelLogs = async (vehicleId: string): Promise => { const mileageFormatConfig = await db.query.configTable.findFirst({ where: (config, { eq }) => eq(config.key, 'mileageUnitFormat') }); - const distanceUnit = (await db.query.configTable.findFirst({ - where: (config, { eq }) => eq(config.key, 'unitOfDistance') - }))?.value; - const volumeUnit = (await db.query.configTable.findFirst({ - where: (config, { eq }) => eq(config.key, 'unitOfVolume') - }))?.value; + const distanceUnit = ( + await db.query.configTable.findFirst({ + where: (config, { eq }) => eq(config.key, 'unitOfDistance') + }) + )?.value; + const volumeUnit = ( + await db.query.configTable.findFirst({ + where: (config, { eq }) => eq(config.key, 'unitOfVolume') + }) + )?.value; const mileageFormat = mileageFormatConfig?.value || 'distance-per-fuel'; const fuelLogs = await db.query.fuelLogTable.findMany({ From 3ed91216d6f0fb4ef88ac60bb0e3f5c6fd88df3b Mon Sep 17 00:00:00 2001 From: Ross Date: Wed, 25 Mar 2026 17:15:05 -0700 Subject: [PATCH 06/15] Allow zero cost in maintenance logs Fixes #179 --- src/lib/domain/maintenance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/domain/maintenance.ts b/src/lib/domain/maintenance.ts index 662106e1..9f0b28d1 100644 --- a/src/lib/domain/maintenance.ts +++ b/src/lib/domain/maintenance.ts @@ -28,7 +28,7 @@ export const maintenanceSchema = z.object({ .string() .min(2, 'It must be more than 1 character.') .max(50, 'It must be less than 50 characters.'), - cost: z.float32().positive(), + cost: z.float32().nonnegative(), notes: z.string().nullable(), attachment: z.string().nullable() }); From 57c19301ad3695b3fbd70f98fff9e762f5e87b1a Mon Sep 17 00:00:00 2001 From: Ross Date: Wed, 25 Mar 2026 17:30:35 -0700 Subject: [PATCH 07/15] Fix vehicle list not refreshing after delete Removed stale PIN check that was gating the store refresh call. The PIN feature no longer exists in the codebase, so the refresh was never triggered after deletion. Now calls refreshVehicles() directly, consistent with the create/edit flow. --- src/lib/components/feature/vehicle/VehicleCard.svelte | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/lib/components/feature/vehicle/VehicleCard.svelte b/src/lib/components/feature/vehicle/VehicleCard.svelte index b638b72c..842cac7b 100644 --- a/src/lib/components/feature/vehicle/VehicleCard.svelte +++ b/src/lib/components/feature/vehicle/VehicleCard.svelte @@ -9,7 +9,6 @@ import BellRing from '@lucide/svelte/icons/bell-ring'; import Info from '@lucide/svelte/icons/info'; import { vehicleStore } from '$stores/vehicle.svelte'; - import { browser } from '$app/environment'; import IconButton from '$appui/IconButton.svelte'; import DeleteConfirmation from '$appui/DeleteConfirmation.svelte'; import * as Card from '$ui/card'; @@ -36,7 +35,7 @@ const performDelete = async (vehicleId: string) => { deleteVehicle(vehicleId).then((res) => { if (res.status == 'OK') { - fetchVehicles(); + vehicleStore.refreshVehicles(); toast.success(m.vehicle_delete_success()); } else { toast.error(res.error || m.vehicle_delete_error()); @@ -44,13 +43,6 @@ }); }; - const fetchVehicles = () => { - if (browser) { - const pin = localStorage.getItem('userPin') || undefined; - if (pin) vehicleStore.refreshVehicles(); - } - }; - // Dynamic image URL - fallback to default if vehicle doesn't have image const imageUrl = $derived( vehicle.image ? withBase(`/api/files/${vehicle.image}`) : '/default-vehicle.png' From e89db0497747071340383009e92e37f48ba2e796 Mon Sep 17 00:00:00 2001 From: Ross Date: Wed, 25 Mar 2026 17:44:59 -0700 Subject: [PATCH 08/15] Skip pollution notifications when feature is disabled --- src/server/services/notificationService.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/server/services/notificationService.ts b/src/server/services/notificationService.ts index cff14869..ebc5fcaa 100644 --- a/src/server/services/notificationService.ts +++ b/src/server/services/notificationService.ts @@ -17,6 +17,7 @@ import { type GeneratedNotification } from './notification-service.helper'; import { createFailureResponse, createSuccessResponse } from './service-response.helper'; +import { getAppConfigByKey } from './configService'; type NotificationType = keyof typeof NOTIFICATION_TYPES; type NotificationSource = keyof typeof NOTIFICATION_SOURCES; @@ -114,10 +115,18 @@ async function buildPuccNotifications(vehicleId: string): Promise { + let puccEnabled = true; + try { + const puccConfig = await getAppConfigByKey('featurePucc'); + if (puccConfig.success) puccEnabled = puccConfig.data?.value !== 'false'; + } catch { + // key not in DB yet — default to enabled + } + const [reminders, insurances, puccCertificates] = await Promise.all([ buildReminderNotifications(vehicleId), buildInsuranceNotifications(vehicleId), - buildPuccNotifications(vehicleId) + puccEnabled ? buildPuccNotifications(vehicleId) : Promise.resolve([]) ]); return sortNotificationsByDueDate([...reminders, ...insurances, ...puccCertificates]); From 58f5dc34713f099150cc08cdf492e4abfa74527d Mon Sep 17 00:00:00 2001 From: Agoston Dauner Date: Fri, 27 Mar 2026 10:03:24 +0100 Subject: [PATCH 09/15] Add .gitattributes to force LF ending --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6313b56c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf From c18387644df7cc6ef6afd3a829b18f9b55093944 Mon Sep 17 00:00:00 2001 From: Agoston Dauner Date: Fri, 27 Mar 2026 10:05:50 +0100 Subject: [PATCH 10/15] fix: Fix to filter out id key when creating fuel log, insurance, pollution cert to not to save object with null id --- src/server/services/fuelLogService.ts | 3 ++- src/server/services/insuranceService.ts | 3 ++- src/server/services/pollutionCertificateService.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/server/services/fuelLogService.ts b/src/server/services/fuelLogService.ts index eeb27a9d..f905f986 100644 --- a/src/server/services/fuelLogService.ts +++ b/src/server/services/fuelLogService.ts @@ -29,7 +29,8 @@ export const addFuelLog = async ( .insert(schema.fuelLogTable) .values({ ...fuelLogData, - vehicleId: vehicleId + vehicleId: vehicleId, + id: undefined }) .returning(); return createSuccessResponse(fuelLog[0], 'Fuel log added successfully.'); diff --git a/src/server/services/insuranceService.ts b/src/server/services/insuranceService.ts index 15e2f461..d62383f0 100644 --- a/src/server/services/insuranceService.ts +++ b/src/server/services/insuranceService.ts @@ -29,7 +29,8 @@ export const addInsurance = async ( .insert(schema.insuranceTable) .values({ ...sanitizedInsuranceData, - vehicleId: vehicleId + vehicleId: vehicleId, + id: undefined }) .returning(); return createSuccessResponse(insurance[0], 'Insurance details added successfully.'); diff --git a/src/server/services/pollutionCertificateService.ts b/src/server/services/pollutionCertificateService.ts index 47f7a323..2be03dc9 100644 --- a/src/server/services/pollutionCertificateService.ts +++ b/src/server/services/pollutionCertificateService.ts @@ -28,7 +28,8 @@ export const addPollutionCertificate = async ( .insert(schema.pollutionCertificateTable) .values({ ...sanitizedPayload, - vehicleId: vehicleId + vehicleId: vehicleId, + id: undefined }) .returning(); return createSuccessResponse( From f631294b8baf3e501b971d67bde908eb3b738227 Mon Sep 17 00:00:00 2001 From: Agoston Dauner Date: Fri, 27 Mar 2026 10:06:16 +0100 Subject: [PATCH 11/15] feat: Add missing hungarian translations --- messages/hu.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/messages/hu.json b/messages/hu.json index 35bfd08e..ed2722cc 100644 --- a/messages/hu.json +++ b/messages/hu.json @@ -205,6 +205,8 @@ "notifications_section_alerts": "Riasztások", "notifications_mark_done_title": "Emlékeztető megjelölése készként", "notifications_mark_done_aria": "{type} emlékeztető megjelölése készként", + "notifications_mark_all_read_title": "Az összes megjelölése olvasottként", + "notifications_mark_all_read_aria": "Összes értesítés megjelölése olvasottként", "notifications_overdue_days": "{days} napja lejárt", "notifications_due_today": "Ma esedékes", "notifications_due_tomorrow": "Holnap esedékes", From d5cbcf0cd6b33dfc5d30f3ba173702db77d3cebc Mon Sep 17 00:00:00 2001 From: Agoston Dauner Date: Fri, 27 Mar 2026 10:17:09 +0100 Subject: [PATCH 12/15] fix: Add locale to calendar to handle it based on it (e.g. week starting date) --- src/lib/components/ui/calendar/calendar.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/components/ui/calendar/calendar.svelte b/src/lib/components/ui/calendar/calendar.svelte index 519ac642..dd04ece0 100644 --- a/src/lib/components/ui/calendar/calendar.svelte +++ b/src/lib/components/ui/calendar/calendar.svelte @@ -5,6 +5,7 @@ import type { ButtonVariant } from '../button/button.svelte'; import { isEqualMonth, type DateValue } from '@internationalized/date'; import type { Snippet } from 'svelte'; + import { getLocale } from '$lib/paraglide/runtime.js'; let { ref = $bindable(null), @@ -14,7 +15,7 @@ weekdayFormat = 'short', buttonVariant = 'ghost', captionLayout = 'label', - locale = 'en-US', + locale = getLocale(), months: monthsProp, years, monthFormat: monthFormatProp, From 1fb0584fd99facf5d675dcd002ea450aaccd9613 Mon Sep 17 00:00:00 2001 From: Agoston Dauner Date: Fri, 27 Mar 2026 10:32:07 +0100 Subject: [PATCH 13/15] fix: Fix settins date format input validation check --- .../feature/settings/SettingsPersonalizationTab.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/feature/settings/SettingsPersonalizationTab.svelte b/src/lib/components/feature/settings/SettingsPersonalizationTab.svelte index da4127e9..e7fe2ed5 100644 --- a/src/lib/components/feature/settings/SettingsPersonalizationTab.svelte +++ b/src/lib/components/feature/settings/SettingsPersonalizationTab.svelte @@ -105,7 +105,7 @@ /> {m.common_example_prefix()} - {isValidFormat(formData.dateFormat).ex || m.common_invalid_format()} + {isValidFormat($formData.dateFormat).ex || m.common_invalid_format()} {/snippet} From 3294a80a433cc5f133afd3a26bf5e648cf8b4eeb Mon Sep 17 00:00:00 2001 From: Ross Date: Sat, 28 Mar 2026 08:52:08 -0700 Subject: [PATCH 14/15] Fix vehicle creation failing with null id --- src/server/services/vehicleService.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/server/services/vehicleService.ts b/src/server/services/vehicleService.ts index 0a99fefd..24366eb2 100644 --- a/src/server/services/vehicleService.ts +++ b/src/server/services/vehicleService.ts @@ -109,7 +109,10 @@ const calculateOverallMileage = async (vehicleId: string) => { export const addVehicle = async (vehicleData: VehicleMutationPayload): Promise => { const processedData = serializeVehiclePayload(vehicleData); - const [vehicle] = await db.insert(schema.vehicleTable).values(processedData).returning(); + const [vehicle] = await db + .insert(schema.vehicleTable) + .values({ ...processedData, id: crypto.randomUUID() }) + .returning(); return createSuccessResponse(parseVehicleRecord(vehicle), 'Vehicle added successfully.'); }; From 83d2304cf27dd4faf1af1d9b4d771907069ce375 Mon Sep 17 00:00:00 2001 From: Ross Date: Sun, 29 Mar 2026 20:13:11 -0700 Subject: [PATCH 15/15] Let Drizzle generate vehicle UUID via schema defaultFn Strip id from payload in serializeVehiclePayload so the runtime null from the UI request body never reaches Drizzle, allowing $defaultFn to generate the UUID as intended. --- src/server/services/vehicleService.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/server/services/vehicleService.ts b/src/server/services/vehicleService.ts index 24366eb2..9c431571 100644 --- a/src/server/services/vehicleService.ts +++ b/src/server/services/vehicleService.ts @@ -10,8 +10,9 @@ type VehiclePayload = Omit; type VehicleMutationPayload = Omit; function serializeVehiclePayload(vehicleData: VehicleMutationPayload) { + const { id: _, ...data } = vehicleData as VehicleMutationPayload & { id?: unknown }; return { - ...vehicleData, + ...data, customFields: vehicleData.customFields ? JSON.stringify(vehicleData.customFields) : null }; } @@ -109,10 +110,7 @@ const calculateOverallMileage = async (vehicleId: string) => { export const addVehicle = async (vehicleData: VehicleMutationPayload): Promise => { const processedData = serializeVehiclePayload(vehicleData); - const [vehicle] = await db - .insert(schema.vehicleTable) - .values({ ...processedData, id: crypto.randomUUID() }) - .returning(); + const [vehicle] = await db.insert(schema.vehicleTable).values(processedData).returning(); return createSuccessResponse(parseVehicleRecord(vehicle), 'Vehicle added successfully.'); };