Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,8 @@ These are ran in the .github\workflows\ci.yml pipeline on every PR

BikeTracking packages as native desktop installers for Windows (.exe) and Linux (.deb) via Tauri 2.

Flow: commit to main → release-please bumps version & tags → release.yml builds installers & publishes.

### Local Build (DevContainer)

Prerequisites: DevContainer includes Rust stable + WebKit GTK libs. If local, install:
Expand Down
132 changes: 132 additions & 0 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Bike Tracking – User Guide

## Installation

### Requirements

| Platform | Minimum |
|----------|---------|
| Windows | Windows 10 or later |
| macOS | macOS 12 Monterey or later |
| Linux | Any modern distribution with .NET 10 runtime |

### Steps

1. Download the latest release package for your OS from the [Releases page](https://github.com/your-org/neCodeBikeTracking/releases).
2. Extract the archive to a folder of your choice (e.g. `C:\BikeTracking` or `~/bike-tracking`).
3. Run the application:
- **Windows**: double-click `BikeTracking.exe`, or run it from a terminal.
- **macOS / Linux**: open a terminal and run `./BikeTracking`.
4. The app starts a local web server. Open your browser and navigate to `http://localhost:5436` (or the port shown in the terminal).
5. Optionally install as a Progressive Web App (PWA) using the **Install App** button in Settings for a native app-like experience on Windows + Edge/Chrome.

> **Tip:** The app never connects to any cloud service. All data stays on your machine.

---

## Database File

The app stores all ride history, settings, and cached data in a single **SQLite** database file:

```
biketracking.local.db
```

This file is created automatically on first run in the same directory as the application binary.

### Locating the file

Open a terminal in the folder where you installed the app and look for `biketracking.local.db`.

---

## Backing Up Your Data

Backing up is a simple file copy.

**Warning:** Always back up before upgrading the app. Schema migrations run automatically on startup and cannot be rolled back without a backup.

### Manual backup

```bash
# Windows (PowerShell)
Copy-Item .\biketracking.local.db .\biketracking.local.db.backup

# macOS / Linux
cp biketracking.local.db biketracking.local.db.backup
```

### Scheduled backup (Windows Task Scheduler example)

Create a `.ps1` script:

```powershell
$src = "C:\BikeTracking\biketracking.local.db"
$dest = "C:\Backups\BikeTracking\biketracking_$(Get-Date -Format 'yyyyMMdd_HHmm').db"
New-Item -ItemType Directory -Force -Path (Split-Path $dest) | Out-Null
Copy-Item $src $dest
```

Schedule it daily via Task Scheduler → **Create Basic Task** → set trigger to Daily → set action to `powershell.exe -File C:\path\to\backup.ps1`.

### Restoring from backup

1. Stop the app.
2. Replace `biketracking.local.db` with your backup copy.
3. Restart the app.

---

## API Keys

Two optional external services enrich ride data automatically. Both keys are stored **per rider** in your local database — they are never sent to any third party other than the respective API services.

### EIA Gas Price API Key

**What it does:** Automatically looks up the national average weekly gas price for the date of each ride. Used to calculate fuel cost savings.

**Required:** Yes — gas price lookup is disabled without this key.

**How to get a free key:**

1. Go to [https://www.eia.gov/opendata/register.php](https://www.eia.gov/opendata/register.php).
2. Fill in your name and email address and submit the form.
3. Check your email — your API key arrives within a few minutes.

**Where to enter it:**

1. Log in to the app.
2. Open **Settings**.
3. Scroll to the **API Keys** section.
4. Paste your key into the **EIA Gas Price API Key** field.
5. Click **Save Settings**.

---

### Open-Meteo Weather API Key (optional)

**What it does:** Fetches weather conditions (temperature, wind, precipitation) for each ride. Used to track riding conditions over time.

**Required:** No — weather lookup works without a key using the free public tier of [Open-Meteo](https://open-meteo.com/). A paid key is only needed for higher request volumes or commercial use.

**How to get a paid key (if needed):**

1. Go to [https://open-meteo.com/en/pricing](https://open-meteo.com/en/pricing).
2. Subscribe to a plan.
3. Copy the API key from your account dashboard.

**Where to enter it:**

1. Log in to the app.
2. Open **Settings**.
3. Scroll to the **API Keys** section.
4. Paste your key into the **Open-Meteo API Key** field.
5. Click **Save Settings**.

> **Note:** Leave the Open-Meteo field blank to continue using the free tier.

---

## Multiple Riders

Each rider signs up with a name and PIN. API keys are stored per rider — each rider on the same machine can use their own keys or share keys by entering them separately under each account.
1 change: 0 additions & 1 deletion release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"src/BikeTracking.Frontend": {
"release-type": "node",
"changelog-path": "src/BikeTracking.Frontend/CHANGELOG.md",
"versioning": "semver",
"extra-files": [
{
"type": "toml",
Expand Down
6 changes: 1 addition & 5 deletions specs/010-gas-price-lookup/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,7 @@ When a user creates or edits a ride, the form now shows a **Gas Price ($/gal)**
## EIA API Key Setup (Development)

The EIA API key is required for the lookup to succeed. In development (on your local machine), set it via .NET User Secrets:

```bash
cd src/BikeTracking.Api
dotnet user-secrets set "GasPriceLookup:EiaApiKey" "YOUR_EIA_KEY_HERE"
```
put it in the settings app

If you run the API in a dev container, bind-mount your host User Secrets directory so secrets persist across container rebuilds:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@

private sealed class EditDifficultyApiHost(WebApplication app) : IAsyncDisposable
{
public WebApplication App { get; } = app;

Check warning on line 273 in src/BikeTracking.Api.Tests/Application/Rides/EditRideWithDifficultyTests.cs

View workflow job for this annotation

GitHub Actions / Verify

Parameter 'WebApplication app' is captured into the state of the enclosing type and its value is also used to initialize a field, property, or event.
public HttpClient Client { get; } = app.GetTestClient();

public static async Task<EditDifficultyApiHost> StartAsync()
Expand Down Expand Up @@ -369,12 +369,14 @@
{
public Task<decimal?> GetOrFetchAsync(
DateOnly date,
string? apiKey = null,
CancellationToken cancellationToken = default
) => Task.FromResult<decimal?>(null);

public Task<decimal?> GetOrFetchAsync(
DateOnly priceDate,
DateOnly weekStartDate,
string? apiKey = null,
CancellationToken cancellationToken = default
) => Task.FromResult<decimal?>(null);
}
Expand All @@ -385,6 +387,7 @@
decimal latitude,
decimal longitude,
DateTime dateTimeUtc,
string? apiKey = null,
CancellationToken cancellationToken = default
) => Task.FromResult<WeatherData?>(null);
}
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,7 @@ internal sealed class StubWeatherLookupService : IWeatherLookupService
decimal latitude,
decimal longitude,
DateTime dateTimeUtc,
string? apiKey = null,
CancellationToken cancellationToken = default
) => Task.FromResult<WeatherData?>(null);
}
Expand All @@ -855,6 +856,7 @@ internal sealed class TrackingWeatherLookupService(WeatherData? response) : IWea
decimal latitude,
decimal longitude,
DateTime dateTimeUtc,
string? apiKey = null,
CancellationToken cancellationToken = default
)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ internal sealed class StubWeatherLookupService : IWeatherLookupService
decimal latitude,
decimal longitude,
DateTime dateTimeUtc,
string? apiKey = null,
CancellationToken cancellationToken = default
) => Task.FromResult<WeatherData?>(null);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@

private sealed class DifficultyRecordApiHost(WebApplication app) : IAsyncDisposable
{
public WebApplication App { get; } = app;

Check warning on line 197 in src/BikeTracking.Api.Tests/Endpoints/Rides/RecordRideWithDifficultyTests.cs

View workflow job for this annotation

GitHub Actions / Verify

Parameter 'WebApplication app' is captured into the state of the enclosing type and its value is also used to initialize a field, property, or event.
public HttpClient Client { get; } = app.GetTestClient();

public static async Task<DifficultyRecordApiHost> StartAsync()
Expand Down Expand Up @@ -260,12 +260,14 @@
{
public Task<decimal?> GetOrFetchAsync(
DateOnly date,
string? apiKey = null,
CancellationToken cancellationToken = default
) => Task.FromResult<decimal?>(null);

public Task<decimal?> GetOrFetchAsync(
DateOnly priceDate,
DateOnly weekStartDate,
string? apiKey = null,
CancellationToken cancellationToken = default
) => Task.FromResult<decimal?>(null);
}
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,7 @@

private sealed class RecordRideApiHost(WebApplication app) : IAsyncDisposable
{
public WebApplication App { get; } = app;

Check warning on line 756 in src/BikeTracking.Api.Tests/Endpoints/RidesEndpointsTests.cs

View workflow job for this annotation

GitHub Actions / Verify

Parameter 'WebApplication app' is captured into the state of the enclosing type and its value is also used to initialize a field, property, or event.
public HttpClient Client { get; } = app.GetTestClient();

public static async Task<RecordRideApiHost> StartAsync()
Expand Down Expand Up @@ -944,6 +944,7 @@
{
public Task<decimal?> GetOrFetchAsync(
DateOnly date,
string? apiKey = null,
CancellationToken cancellationToken = default
)
{
Expand All @@ -958,11 +959,12 @@
public Task<decimal?> GetOrFetchAsync(
DateOnly priceDate,
DateOnly weekStartDate,
string? apiKey = null,
CancellationToken cancellationToken = default
)
{
// Delegate to the single-date overload for stub behavior
return GetOrFetchAsync(priceDate, cancellationToken);
return GetOrFetchAsync(priceDate, apiKey, cancellationToken);
}
}

Expand All @@ -972,6 +974,7 @@
decimal latitude,
decimal longitude,
DateTime dateTimeUtc,
string? apiKey = null,
CancellationToken cancellationToken = default
)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ public sealed class MigrationTestCoveragePolicyTests
"Added test: RidesEndpointsSqliteIntegrationTests validates RidePresets table including unique rider-scoped names, exact start time parsing, rider isolation, MRU ordering, and preset ownership enforcement.",
["20260520150127_AddRidePresetMiles"] =
"Updated test: RidePreset CRUD endpoint coverage validates Miles round-trip and persistence on create and update after schema migration.",
["20260612151600_AddApiKeysToUserSettings"] =
"Updated test: user settings endpoint integration tests validate WeatherApiKey and EiaGasApiKey persistence and round-trip after schema migration.",
};

[Fact]
Expand Down
10 changes: 8 additions & 2 deletions src/BikeTracking.Api/Application/Imports/ImportJobProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,10 @@ CancellationToken cancellationToken

try
{
var userSettings = await dbContext
.UserSettings.AsNoTracking()
.SingleOrDefaultAsync(x => x.UserId == riderId, cancellationToken);

var validRows = rowsToProcess
.Where(static row => row.RideDateLocal is not null && row.Miles is not null)
.ToArray();
Expand All @@ -261,6 +265,7 @@ CancellationToken cancellationToken
distinctWeeks,
dbContext,
gasLookupService,
userSettings?.EiaGasApiKey,
cancellationToken
);

Expand Down Expand Up @@ -290,6 +295,7 @@ CancellationToken cancellationToken
IReadOnlyList<DateOnly> distinctWeeks,
BikeTrackingDbContext dbContext,
IGasPriceLookupService gasLookupService,
string? eiaGasApiKey,
CancellationToken cancellationToken
)
{
Expand Down Expand Up @@ -324,7 +330,7 @@ CancellationToken cancellationToken
var value = await RetryWithThrottleAsync(
throttle,
async ct =>
await gasLookupService.GetOrFetchAsync(representativeDate, weekStart, ct),
await gasLookupService.GetOrFetchAsync(representativeDate, weekStart, eiaGasApiKey, ct),
cancellationToken
);
gasByWeek[weekStart] = value;
Expand Down Expand Up @@ -401,7 +407,7 @@ CancellationToken cancellationToken
var weather = await RetryWithThrottleAsync(
throttle,
async ct =>
await weatherLookupService.GetOrFetchAsync(latitude, longitude, noonUtc, ct),
await weatherLookupService.GetOrFetchAsync(latitude, longitude, noonUtc, userSettings?.WeatherApiKey, ct),
cancellationToken
);
weatherByDate[date] = weather;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ public async Task<EditRideResult> ExecuteAsync(
latitude,
longitude,
request.RideDateTimeLocal.ToUniversalTime(),
userSettings?.WeatherApiKey,
cancellationToken
);
}
Expand Down
15 changes: 10 additions & 5 deletions src/BikeTracking.Api/Application/Rides/GasPriceLookupService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace BikeTracking.Api.Application.Rides;

public interface IGasPriceLookupService
{
Task<decimal?> GetOrFetchAsync(DateOnly date, CancellationToken cancellationToken = default);
Task<decimal?> GetOrFetchAsync(DateOnly date, string? apiKey = null, CancellationToken cancellationToken = default);

/// <summary>
/// Get or fetch gas price using the ISO week start date as the cache key.
Expand All @@ -18,6 +18,7 @@ public interface IGasPriceLookupService
Task<decimal?> GetOrFetchAsync(
DateOnly priceDate,
DateOnly weekStartDate,
string? apiKey = null,
CancellationToken cancellationToken = default
);
}
Expand All @@ -33,16 +34,18 @@ ILogger<EiaGasPriceLookupService> logger

public async Task<decimal?> GetOrFetchAsync(
DateOnly date,
string? apiKey = null,
CancellationToken cancellationToken = default
)
{
var weekStartDate = GasPriceWeekKeyHelper.GetWeekStartDate(date);
return await GetOrFetchAsync(date, weekStartDate, cancellationToken);
return await GetOrFetchAsync(date, weekStartDate, apiKey, cancellationToken);
}

public async Task<decimal?> GetOrFetchAsync(
DateOnly priceDate,
DateOnly weekStartDate,
string? apiKey = null,
CancellationToken cancellationToken = default
)
{
Expand All @@ -56,8 +59,10 @@ ILogger<EiaGasPriceLookupService> logger
return cached.PricePerGallon;
}

var apiKey = configuration["GasPriceLookup:EiaApiKey"];
if (string.IsNullOrWhiteSpace(apiKey))
var resolvedApiKey = string.IsNullOrWhiteSpace(apiKey)
? configuration["GasPriceLookup:EiaApiKey"]
: apiKey;
if (string.IsNullOrWhiteSpace(resolvedApiKey))
{
logger.LogWarning(
"EIA API key missing; skipping gas price lookup for {Date}",
Expand All @@ -68,7 +73,7 @@ ILogger<EiaGasPriceLookupService> logger

var client = httpClientFactory.CreateClient("EiaGasPrice");
var requestUri =
$"/v2/petroleum/pri/gnd/data?api_key={Uri.EscapeDataString(apiKey)}&data[]=value"
$"/v2/petroleum/pri/gnd/data?api_key={Uri.EscapeDataString(resolvedApiKey)}&data[]=value"
+ "&facets[duoarea][]=NUS"
+ "&facets[product][]=EPM0"
+ "&frequency=weekly"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ ILogger<RecordRideService> logger
latitude,
longitude,
request.RideDateTimeLocal.ToUniversalTime(),
userSettings?.WeatherApiKey,
cancellationToken
);
}
Expand Down
Loading
Loading