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
43 changes: 23 additions & 20 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
> from git history and grouped by theme rather than exhaustive per-commit
> detail.

## [Unreleased]
## [2.5.1] - unreleased

### Fixed
- **Pro-mode panel resizing on touch.** Dragging a resizable divider (the
left/right split and the InfoBox/MiniMap split in pro mode, plus the video
panel) would stop after only a few pixels on touchscreens. The handle's
invisible grab strip was only a few pixels wide while the drag-start margin
extended over the neighbouring chart/map — so a finger landing just off the
divider started the resize on an element without `touch-action: none`, and the
browser reclaimed the gesture as a scroll. The grab strip is now wider and the
start margin is aligned to it, so touch drags track all the way.

### Changed
- **Mobile garage & header polish.** The Garage/Device drawer now covers the
full screen on mobile (it stays at half width on larger screens) so it's
easier to use on a phone. In the loaded-session header, the track/course label
and edit button are consolidated into a single course control — now using a
route icon (at every screen size) with the current track : course as its label
from tablet up.

## [2.5.1] - 2026-06-13
### Added
- **Log type bubble in the file browser.** Each session row (shown by date/time)
now carries a small pill with the log's format — Dove, Dovex, XRK, XRZ,
iRacing, VBO, MoTeC, UBX, NMEA, CSV, … — derived from the file's extension, so
you can tell at a glance what kind of log each one is. Appears on local and
cloud rows and in the Profile → Cloud logs list.

### Changed
- **Landing page UX overhaul** — the home screen is simpler and friendlier. The
Expand All @@ -42,6 +29,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
clearly-labelled tile. Pricing is no longer shown on the landing page — it
lives on the registration page where you pick a plan. Colors and design tokens
are unchanged; this is a layout/usability pass only.
- **Mobile garage & header polish.** The Garage/Device drawer now covers the
full screen on mobile (it stays at half width on larger screens) so it's
easier to use on a phone. In the loaded-session header, the track/course label
and edit button are consolidated into a single course control — now using a
route icon (at every screen size) with the current track : course as its label
from tablet up.

### Fixed
- **Pro-mode panel resizing on touch.** Dragging a resizable divider (the
left/right split and the InfoBox/MiniMap split in pro mode, plus the video
panel) would stop after only a few pixels on touchscreens. The handle's
invisible grab strip was only a few pixels wide while the drag-start margin
extended over the neighbouring chart/map — so a finger landing just off the
divider started the resize on an element without `touch-action: none`, and the
browser reclaimed the gesture as a scroll. The grab strip is now wider and the
start margin is aligned to it, so touch drags track all the way.

## [2.5.0] - 2026-06-13

Expand Down
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
4. **Update credits** (in README) when adding new FOSS dependencies.
5. **Never do on the server what you can do on the client.**
6. **Add tests when possible**: New parsers, pure utilities, and protocol/format logic should ship with Vitest coverage. Bug fixes should add a regression test that fails before the fix. Don't leave testable logic untested.
7. **Keep `CHANGELOG.md` updated**: Add user-facing changes under the `[Unreleased]` heading (Keep a Changelog format) as you make them — don't wait for release time. Cut a new version section + tag when releasing.
7. **Keep `CHANGELOG.md` updated**: Add user-facing changes as you make them — don't wait for release time. **Once a beta version number has been picked (e.g. `2.5.1`), keep that version as the heading and mark it unreleased — `## [2.5.1] - unreleased` — and keep adding every change under that same section until it actually ships. Do NOT create a new `[Unreleased]` block or bump the patch number on every commit; we are not cutting a `.1` release per change.** Only on release do you set the date and tag, then start the next version. (A bare `[Unreleased]` is fine only before any beta version has been chosen.)
8. **Keep it professional**: This is a public, released OSS project (v1.5.0+). Hold the bar — see the standards below.

---
Expand Down Expand Up @@ -775,6 +775,10 @@ Profile **Cloud logs** panel reuses `SessionBrowser` with its own rows.
- **Display name = the session's date/time**, derived from `sessionStartTime` (the
first valid sample), e.g. "2/12/2026 11:15 AM" — *not* the upload time or raw
filename (filename is the row's `title`/tooltip + the stable IndexedDB key).
- **Log type bubble:** each row shows a `FileTypeBadge` (`components/FileTypeBadge.tsx`)
with the format derived from the file extension (`lib/logFileType.ts`, pure +
unit-tested) — the format isn't persisted, so the extension is the source of
truth. Used on FilesTab local + cloud rows and the Profile Cloud logs panel.
- **Smart collapse:** a folder level is only rendered when there's more than one
entry — a single track and/or single course auto-descends straight to the logs
(the breadcrumb still records the collapsed segments so date names read in
Expand Down
19 changes: 19 additions & 0 deletions src/components/FileTypeBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { logFileTypeLabel } from "@/lib/logFileType";

/**
* A small pill showing a log file's format, derived from its extension. Renders
* nothing when the name has no extension. Used in the file browser rows so each
* session (shown by date/time) still says what kind of log it is.
*/
export function FileTypeBadge({ fileName, className }: { fileName: string; className?: string }) {
const label = logFileTypeLabel(fileName);
if (!label) return null;
return (
<span
className={`shrink-0 rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium tracking-wide text-muted-foreground ${className ?? ""}`}
title={`${label} log file`}
>
{label}
</span>
);
}
3 changes: 3 additions & 0 deletions src/components/drawer/FilesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type BrowserSession, type NavState,
} from "@/lib/fileBrowserTree";
import { SessionBrowser } from "@/components/SessionBrowser";
import { FileTypeBadge } from "@/components/FileTypeBadge";
import { useFileSources, type FileSource, type RemoteFile } from "@/plugins/fileSources";
// Lazy — keeps the BLE module in its own chunk, loaded only on device use.
const DataloggerDownload = lazy(() =>
Expand Down Expand Up @@ -257,6 +258,7 @@ export function FilesTab({
>
<div className="flex items-center gap-1.5">
<span className="text-sm font-medium truncate text-muted-foreground">{s.displayName}</span>
<FileTypeBadge fileName={s.fileName} />
{busy
? <Loader2 className="w-3.5 h-3.5 text-primary shrink-0 animate-spin" />
: <Cloud className="w-3.5 h-3.5 text-muted-foreground shrink-0" />}
Expand Down Expand Up @@ -285,6 +287,7 @@ export function FilesTab({
>
<div className="flex items-center gap-1.5">
<span className="text-sm font-medium truncate text-foreground" title={s.fileName}>{s.displayName}</span>
<FileTypeBadge fileName={s.fileName} />
{videoFiles.has(s.fileName) && (
<span title={(() => {
const m = videoFiles.get(s.fileName)!;
Expand Down
48 changes: 48 additions & 0 deletions src/lib/logFileType.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, it, expect } from "vitest";
import { logFileExtension, logFileTypeLabel } from "./logFileType";

describe("logFileExtension", () => {
it("returns the lowercased extension", () => {
expect(logFileExtension("session.VBO")).toBe("vbo");
expect(logFileExtension("LOG001.dove")).toBe("dove");
});

it("uses only the final extension", () => {
expect(logFileExtension("my.session.dovex")).toBe("dovex");
});

it("ignores directories in the path", () => {
expect(logFileExtension("folder.name/log")).toBe("");
expect(logFileExtension("folder.name/log.ibt")).toBe("ibt");
});

it("returns empty for names without an extension", () => {
expect(logFileExtension("logfile")).toBe("");
expect(logFileExtension("trailingdot.")).toBe("");
expect(logFileExtension(".hidden")).toBe("");
});
});

describe("logFileTypeLabel", () => {
it("maps known extensions to friendly labels", () => {
expect(logFileTypeLabel("a.dove")).toBe("Dove");
expect(logFileTypeLabel("a.dovex")).toBe("Dovex");
expect(logFileTypeLabel("a.xrk")).toBe("XRK");
expect(logFileTypeLabel("a.xrz")).toBe("XRZ");
expect(logFileTypeLabel("a.ibt")).toBe("iRacing");
expect(logFileTypeLabel("a.vbo")).toBe("VBO");
expect(logFileTypeLabel("a.ld")).toBe("MoTeC");
expect(logFileTypeLabel("a.ubx")).toBe("UBX");
expect(logFileTypeLabel("a.nmea")).toBe("NMEA");
expect(logFileTypeLabel("a.csv")).toBe("CSV");
expect(logFileTypeLabel("a.txt")).toBe("TXT");
});

it("uppercases unknown extensions", () => {
expect(logFileTypeLabel("a.gpx")).toBe("GPX");
});

it("returns empty when there is no extension", () => {
expect(logFileTypeLabel("logfile")).toBe("");
});
});
34 changes: 34 additions & 0 deletions src/lib/logFileType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Maps a log file's extension to a short, human-friendly label for the file
* browser's "type" bubble. We don't persist the detected format, so the file
* name's extension is the source of truth here. Unknown extensions fall back to
* the uppercased extension itself; a name with no extension yields "".
*/
const EXTENSION_LABELS: Record<string, string> = {
dove: "Dove",
dovex: "Dovex",
xrk: "XRK",
xrz: "XRZ",
ibt: "iRacing",
vbo: "VBO",
ld: "MoTeC",
ubx: "UBX",
nmea: "NMEA",
csv: "CSV",
txt: "TXT",
};

/** Lowercased extension (no dot), or "" when the name has none. */
export function logFileExtension(fileName: string): string {
const base = fileName.slice(fileName.lastIndexOf("/") + 1);
const dot = base.lastIndexOf(".");
if (dot <= 0 || dot === base.length - 1) return "";
return base.slice(dot + 1).toLowerCase();
}

/** Short type label for the browser bubble, or "" when no extension is present. */
export function logFileTypeLabel(fileName: string): string {
const ext = logFileExtension(fileName);
if (!ext) return "";
return EXTENSION_LABELS[ext] ?? ext.toUpperCase();
}
6 changes: 5 additions & 1 deletion src/plugins/cloud-sync/CloudLogsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
type BrowserSession, type NavState,
} from "@/lib/fileBrowserTree";
import { SessionBrowser } from "@/components/SessionBrowser";
import { FileTypeBadge } from "@/components/FileTypeBadge";
import { cleanupOrphanBlobs, deleteCloudFile, downloadCloudFile, listCloudFiles, type CloudFile } from "./syncEngine";
import { cloudOnlyNames, markPushed, unselectFile } from "./fileSync";
import { formatBytes } from "./storageTypes";
Expand Down Expand Up @@ -146,7 +147,10 @@ export default function CloudLogsPanel(_props: PluginPanelProps) {
{/* Cloud-only rows are greyed out (not on this device until downloaded). */}
<div className={`flex items-center gap-2 px-3 py-2${onDevice ? "" : " opacity-60"}`}>
<div className="min-w-0 flex-1">
<p className={`truncate text-sm ${onDevice ? "text-foreground" : "text-muted-foreground"}`} title={s.fileName}>{s.displayName}</p>
<div className="flex items-center gap-1.5">
<p className={`truncate text-sm ${onDevice ? "text-foreground" : "text-muted-foreground"}`} title={s.fileName}>{s.displayName}</p>
<FileTypeBadge fileName={s.fileName} />
</div>
<p className="text-[11px] text-muted-foreground">
{formatDate(cf?.uploadedAt)}
{cf?.size != null && ` · ${formatBytes(cf.size)}`}
Expand Down
Loading