chore(tooling): add lint/format gates, pin the toolchain, enforce commit checks

Adds the frontend's first linter and formatter — the Rust half has had
cargo fmt --check and clippy in CI for a while, while 274 TS/Svelte files had
only svelte-check. ESLint runs clean; 159 findings are recorded as warnings
rather than suppressed, so the backlog is visible without painting CI red.

Also: `bun run test` no longer drops into watch mode (the "Before Committing"
list told people to run a command that never returns), the traceability ratchet
moves 82% -> 88%, a pre-commit hook enforces the fast half of that list instead
of relying on memory, the dead webdriverio e2e suite and its five devDeps are
removed, and the Rust toolchain is pinned to 1.97.1 so the developer machine and
the CI builder image stop being five releases apart.

TRACES: | DR-205, DR-206, DR-207
This commit is contained in:
2026-08-20 19:53:13 +02:00
35 changed files with 680 additions and 1876 deletions
@@ -66,6 +66,9 @@
// Recompute when the reserved bottom gap changes (mini-player shows/hides).
$effect(() => {
// Bare read: registers `bottomGap` as a dependency of this effect. Svelte 5
// idiom, not a stray expression.
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
bottomGap;
measure();
});
@@ -52,6 +52,9 @@
// A new item in the same slot (scrolling a virtualised list, switching series)
// must drop the previous item's optimistic state or it shows the wrong tick.
$effect(() => {
// Bare read: registers `itemId` as a dependency of this effect. Svelte 5
// idiom, not a stray expression.
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
itemId;
optimistic = null;
});
+3 -2
View File
@@ -263,7 +263,6 @@ export class Html5PlayerAdapter implements PlayerAdapter {
timeoutMs: number
): Promise<boolean> {
return new Promise<boolean>((resolve) => {
let timer: ReturnType<typeof setTimeout>;
const done = (fired: boolean) => {
el.removeEventListener(event, listener);
clearTimeout(timer);
@@ -271,7 +270,9 @@ export class Html5PlayerAdapter implements PlayerAdapter {
};
const listener = () => done(true);
el.addEventListener(event, listener);
timer = setTimeout(() => done(false), timeoutMs);
// `done` closes over `timer`, but can only run once the listener fires or
// the timeout elapses — both strictly after this assignment.
const timer: ReturnType<typeof setTimeout> = setTimeout(() => done(false), timeoutMs);
});
}
}
+1 -1
View File
@@ -38,7 +38,7 @@ export async function getDeviceId(): Promise<string> {
return deviceId;
} catch (e) {
log.error("Failed to get device ID from backend:", e);
throw new Error("Failed to initialize device ID: " + String(e));
throw new Error("Failed to initialize device ID: " + String(e), { cause: e });
}
}
+4 -2
View File
@@ -195,7 +195,7 @@ async function handleStateChanged(state: string, _mediaId: string | null): Promi
switch (state) {
case "playing":
case "paused":
case "loading":
case "loading": {
// When local playback starts, ensure mode is set to local
const mode = get(playbackMode);
if (mode.mode !== "local") {
@@ -236,9 +236,10 @@ async function handleStateChanged(state: string, _mediaId: string | null): Promi
}
break;
}
case "idle":
case "stopped":
case "stopped": {
player.setIdle();
// When local playback stops, revert to idle mode
const currentMode = get(playbackMode);
@@ -248,6 +249,7 @@ async function handleStateChanged(state: string, _mediaId: string | null): Promi
}
break;
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ export interface InvokeCall {
}
let invokeHistory: InvokeCall[] = [];
let invokeResponses: Map<string, any> = new Map();
const invokeResponses: Map<string, any> = new Map();
/**
* Mock invoke function that captures calls
+1 -1
View File
@@ -79,7 +79,7 @@ export function validateUrlPathSegment(segment: string): void {
}
// Reject path separators and null bytes
if (/[\/\\%]/.test(segment)) {
if (/[/\\%]/.test(segment)) {
throw new Error("Invalid path segment: contains invalid characters");
}
}