Skip to content

Migrate the frontend to Svelte 5 and SvelteKit 3 - #24

Merged
arg274 merged 36 commits into
mainfrom
svelte5-migration
Oct 10, 2026
Merged

arg274 merged 36 commits into
mainfrom
svelte5-migration

Conversation

@arg274

@arg274 arg274 commented Oct 10, 2026

Copy link
Copy Markdown
Collaborator

Moves the web UI to Svelte 5 runes and SvelteKit 3, replaces Skeleton with Bits UI and an owned styling layer, and brings in the parser work that landed on this branch along the way.

Frontend

  • Svelte 4 → 5 with runes throughout, SvelteKit 2.5 → 3, both via the sv migrate codemods. Config now lives in the sveltekit() plugin in vite.config.ts, $lib imports are #lib subpath imports, and $app/environment is $app/env.
  • Skeleton 2 is gone: a short stop at Skeleton v5, then Bits UI with the palette re-anchored to master's, so the visuals match the pre-migration UI.
  • Tailwind 3 → 4, Vite 5 → 8, Vitest 1 → 4, TypeScript 5 → 6, typescript-eslint 6 → 8. Standing eslint errors cleared.
  • Geist as a variable font, which drops 5.2 MB of static Inter faces from the binary.
  • Routing follows the act of choosing files rather than the file list, the selection clears on returning home, and the single log view no longer races the route a new selection asks for.
  • Help page brought up to date.

Parsers

  • EZ CD Audio Converter 6.0.0 logs parse (three error columns, firmware suffix on the drive name). 10.3.0 output is unchanged.
  • The six experimental parsers now use the same idioms as the EAC, XLD and whipper parsers, with shared parser::split_tracks and util::Time helpers. Output is byte-identical on the 11 test logs.
  • Rip AccurateRip checksums keep their leading zeros (0199F19E, not 199F19E).

CI

Gates run on every push, releases are cut from tags, and the workflows are off the deprecated action runtime.

Verification

  • svelte-check: 0 errors. vite build succeeds.
  • A headless navigation run (single upload, batch upload, error page, direct loads of /log and /logs) behaves identically before and after the SvelteKit 3 upgrade.

Known, not regressions

Both reproduce on the build from before the SvelteKit 3 upgrade:

  • Going back from /logs to home and uploading again ends on "Connection to the API failed."
  • Loading /log or /logs directly does not redirect home, though both pages try to.

The SvelteKit 3 migration also ran prettier over the ts-rs output in src/lib/types; regenerating those files will flip their formatting back unless the directory goes in .prettierignore.

🤖 Generated with Claude Code

arg274 and others added 30 commits August 31, 2026 12:11
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
- Props via $props(), prop-derived values via $derived/$derived.by
- Event directives to event attributes; createEventDispatcher to a callback prop
- Slots to snippets; $app/stores to $app/state
- Replace svelte/legacy run()/preventDefault() shims with $effect and inline handlers
- Bridge Skeleton v2 / svelte-headless-table ComponentType typings to Svelte 5
- Repair the eslint config, which still referenced eslint-plugin-svelte3

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Svelte 5 hydration-checks {@html} content, and stringifying Skeleton's
setInitialClassState into svelte:head trips hydration_html_changed on
every page. An inline script in app.html does the same job and runs
before hydration.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Skeleton v5 requires Tailwind 4 and replaces v2's monolithic components
with Zag-based compound anatomies:

- Tailwind 3 -> 4: @tailwindcss/vite, CSS-first config, purgecss dropped
- cambia-theme.ts -> src/cambia-theme.css against the v5 theme contract
  (hex, shades 50-950, contrast ramps); data-theme moved to <html>
- variant-* -> preset-*, *-token pairings -> symmetric v5 pairings
- AppShell (removed upstream) -> hand-rolled scroll/header/footer layout
- popup action -> Tooltip; clipboard action -> navigator.clipboard
- toast store -> createToaster + Toast.Group; modal store -> a plain flag
- Paginator -> shared frag over Pagination; Accordion, Progress,
  RecursiveTreeView -> collection-driven TreeView
- lightswitch now comes from the in-repo copy, since v5 dropped it
- svelte-headless-table -> @humanspeak/svelte-headless-table (Svelte 5)

getCssColor now parses hex, as v5 themes no longer emit RGB triplets.
ComboBox.svelte is removed: it was unreferenced and its ListBox+popup
API has no v5 equivalent worth porting blind.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Clears 10 of 14 npm audit findings, all in dev/build tooling that never
reaches the bundle: the Vitest UI arbitrary-read (critical), the Vite
fs.deny bypass and esbuild dev-server issues, and the minimatch ReDoS
reached through typescript-eslint 6.

The remaining four are not fixable here and are not reachable:
- cookie <0.7.0 via @sveltejs/kit, which still pins cookie ^0.6.0 even
  on latest; adapter-static ships no server anyway
- uuid via musicbrainz-api, whose advisory covers v3/v5/v6 when a buffer
  argument is passed, while musicbrainz-api only calls v4 in its
  digest-auth path

typescript-eslint 8 turns on no-unused-expressions by default, which
flags four pre-existing ternary-as-statement lines; left as-is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Both components built class names by interpolation, e.g. bg-{bgColor}-400.
Tailwind has never been able to see those, and under Tailwind 3 they only
resolved because the content globs scanned Skeleton v2's Svelte source,
which happened to contain the same literals. Skeleton v5 ships CSS only,
so the classes stopped being emitted and the fields lost their tint.

ChecksumSegment was also using bg-opacity-25, removed in Tailwind 4, so
even the colours that did survive rendered fully opaque.

Both now select complete class strings, which the scanner can find, with
the opacity expressed as /25. That also retires the hidden divs in Grade
that existed solely to force the classes into the bundle.

The Unknown state uses surface-950 in dark mode rather than surface-900,
because the card behind it is now itself surface-900.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Measured the migrated UI against a master build rendering the same
fixture, and put back the metrics the framework upgrades had changed.

app.css compatibility shims:
- .btn-icon was 43px square in v2 and resolves to 30px in v5. This also
  set the height of the rows holding the mono hash fields, which is why
  the gap between a field and its label disappeared.
- .chip was 0.75rem text with 0.375rem/0.75rem padding in v2.
- preset-tonal-* now matches v2's variant-soft-*: the palette's 400 shade
  at 20% alpha with 700/200 text, instead of v5's solid 50/950 pairing,
  which made every chip read far brighter.
- Default border-color follows a muted surface tone again. Tailwind 4
  defaults it to currentColor, which rendered the dashed rules at full
  text contrast.

Paginator now mirrors v2: arrows either side of an "x-y of n" readout in
a flush pill, rather than one button per page. v5's btn-group adds gap
and padding that v2 had not, so the group is styled directly.

EvaluationInfo regains the branch caret, matches v2's 16px lead spacing,
and no longer expands the evaluator node on first render.

copyToClipboard prefers clipboard.write with a ClipboardItem, as the v2
action did. A bare writeText matches the ClickFix scam pattern and gets
blocked by uBlock Origin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
unplugin-icons renders bare icons at 1.2em, so the jump-to-page arrow
came out at 19px beside a 14px input, and its button sat 3px shorter
than the box. The arrows now match the text they sit next to and the
control stretches to a single height.

The readout drops v2's range form: with one item per page "1-1 of 10"
only ever repeated itself, so it now reads "1 of 10".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Sizing the input and its arrow button independently gave 34px and 31px
against the paginator pill's 32: the input carries a 1px border from
@tailwindcss/forms, and the previous items-stretch fix pulled the button
up to the taller of the two rather than to the row. All three are 32 now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Declaring the v2 metric shims as raw CSS at the end of @layer utilities
made them beat the app's own utilities: a chip carrying py-1 rendered at
28px instead of 24 because the shim's padding-block won. Declaring them
with @Utility puts them where v2's component classes sat, so an explicit
py-1, h-6 or w-10 wins again while un-overridden chips keep v2 sizing.

Also clears the dead and misspelled classes the check surfaced:
- bg-success-900 alongside bg-surface-50-950 in TocSource and
  AccurateRipChecksumSegment; the surface pairing already won, so this
  only removes the dead declaration
- text-md is not a Tailwind class and resolved to the inherited 16px,
  which is what text-base gives
- text-balanced -> text-balance, the real utility; this one does change
  wrapping in the two rip-info tooltips

Includes the editor's rewrites to v4 shorthand, [&>*]: -> *: and
rounded-[var(--x)] -> rounded-(--x); both compile to identical CSS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Tailwind 4 drops Preflight's `cursor: pointer` on buttons to match native
browser behaviour, which left every button in the app feeling inert. A
base rule puts it back for buttons, role=button and summary; disabled
pagination arrows keep their not-allowed cursor.

Copying now reports on the button rather than through a toast: a new
CopyButton frag cross-fades the copy glyph to a checkmark for 1.5s.
Both icons sit in one grid cell so the swap cannot shift layout, and the
checkmark inherits currentColor so it matches the button. copyToClipboard
returns a boolean for this and only toasts on failure, where there is
nothing on the button left to show.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
It is a label for the hidden file input, and labels get no pointer by
default; the underline already advertises it as the click target.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Skeleton v5 pins a btn-icon's child svg to --btn-size, which overrode
both the icon-* classes and the 1.2em the icons ship with: the help,
theme and github glyphs rendered at 16px instead of 24. Sizing them from
their own font-size again restores v2 behaviour for every icon button.

The navbar's bottom-right rounding existed to clear the old scrollbar,
which no longer overlaps it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Skeleton v2's .card carried an opaque surface background; v5's sets only
border-radius. That left the grade-bar tooltips as a bare 20%-alpha panel
sitting straight over the log table, so the text was unreadable. Blurring
the backdrop matches what the Card tooltips already do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Skeleton's Svelte package is now one of several framework bindings over a
shared Zag core; Bits UI is Svelte-only and idiomatic with runes. Both
Skeleton packages are gone.

Components:
- Tooltip, Pagination, Accordion, Progress map onto Bits equivalents
- AppBar contributed only data attributes, so header/footer are landmarks
- Bits has no tree view; the evaluations tree is three fixed levels of
  expandable groups, which Collapsible models directly
- Bits has no toast either, and the toaster backed a single message once
  showError turned out to be dead, so copy failure now reads on the
  button beside its tick and the toaster is gone

Styling, which Bits does not supply:
- cambia-theme.css moves from Skeleton's [data-theme] contract to a plain
  @theme block. Colour values are unchanged; the light/dark pairings are
  declared with light-dark() in @theme inline
- app.css absorbs the base layer Skeleton's globals.css provided, the one
  that matters being color-scheme, which every pairing resolves against
- preset-filled, card and placeholder were the only Skeleton utilities
  still referenced (7 usages); they are reproduced verbatim
- The chip and btn-icon shims were overrides layered on Skeleton's
  definitions and carried only the metrics that differed. Standing alone
  they need the layout properties too, without which the disc-id chips
  wrapped their trailing icon onto a second line

Verified against screenshots and computed metrics captured before the
change: header, card, chip, table row, rule, grade border and both
themes' backgrounds are unchanged, and the paginator is identical at
134.3x32. Stylesheet drops from 69KB to 49KB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Comparing against the v5 branch only proved the Bits swap changed
nothing; it could not catch drift introduced earlier in the port. Built
master and diffed against that instead, across home, help and the log
views in both themes. Four real differences came back:

- v2's tokens paired asymmetrically (100 light / 800 dark) where the v5
  port only had symmetric pairs, shifting the entire dark palette a step
  darker. The pairs v2 used are declared directly now, so cards, mono
  fields, striped rows and the page background match again.
- placeholder was surface-300 in v2, not the 200 v5 uses.
- variant-filled sat on surface-900, not 950.
- variant-soft used the 400 shade in light but 500 in dark; the shim had
  400 for both, so every tonal chip was slightly light in dark mode.

Three differences remain and are all deliberate: the pointer cursor on
"click here", the copy button's tick, and the jump-to-page control at
32px rather than 34 to line up with the paginator. The page background
also moves from an AppShell div onto <html>, which computes differently
but paints the same colour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
The error screen centres with 'grid h-full place-items-center', but
nothing in the chain had a definite height once AppShell was gone: the
content wrapper sized itself with min-height, and a percentage height
cannot resolve against that, so the grid collapsed to its content and
sat under the header instead of centred.

The page is now explicit rows -- header auto, content 1fr, footer auto.
The 1fr row fills a short page, so h-full resolves and the error screen
centres, and its min-content floor lets a long page expand past the
viewport with the footer still last.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
The redirect to /logs was a standing condition: processedCount.subscribe
replays its current value on subscribe and fires on every change, then
decided routing from $fileListStore.length. That list outlives a
client-side navigation, so any later notification while on / sent you
straight back to /logs. The same shape set loading = true whenever a
stale single file was around, with nothing running to clear it.

Navigation for multiple logs now happens once, in inputChanged, next to
the upload it belongs to. A `processing` store marks an upload as being
in flight; the subscription ignores everything else, and the overlay is
derived from it rather than from the file list. The subscription is also
unsubscribed on teardown, which it never was.

A failed connection never reaches the completion path, so it releases
the flag itself; otherwise the overlay would cover the error page.

Verified: one log goes to /log, three go to /logs, the logo returns to /
and stays there with the file list still populated, and a reload stays
put with no overlay.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Scoped to the root route rather than every navigation: /logs reads the
file list as its "is there a batch" signal, so clearing it there blanks
the page, and clearing it on /log breaks going back to /logs from a row.

An empty FileList rather than undefined, so bind:files clears the inputs
themselves. The in-flight upload holds its own reference, so nothing
depends on the selection surviving. processing is also scoped to the
single-log case it actually gates, which otherwise stayed true for the
lifetime of a batch.

Verified with the sample logs: four logs give four rows on /logs, a row
opens and browser-back returns to a populated /logs, and the logo clears
the inputs and stays on /.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Tailwind only derives variants for utilities registered with @Utility. I
had written the tonal presets as plain rules inside @layer utilities, so
hover:preset-tonal generated nothing and every icon button lost its
hover state once Skeleton -- which did register them -- was removed.

Confirmed by swapping the previous app.css back in: the help, theme and
github buttons stay transparent on hover with it, and tint correctly
without it, in both themes.

Audited the other 32 variant classes in the markup against the compiled
stylesheet; hover:preset-tonal was the only one producing no CSS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Both halves of the control were already 32px tall and on the same
baseline, but @tailwindcss/forms puts a 1px grey border on a bare
number input. That inset the dark fill by a pixel top and bottom, so
the field read as shorter than the arrow button beside it.

The button has no border, so the field should not either: the two are
meant to look like one pill split down the middle.

Verified at 900px: input and button both 32px high at the same y, no
border on either, identical background, in light and dark.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
DropScreen hides the page content on dragenter so only the drop target
shows. It did that with `display: none`, which takes the content out of
the grid entirely: the footer then auto-placed itself into the freed 1fr
row and slid up to just under the header.

That was visible because the overlay does not appear instantly. It sets
duration-200 without a transition-property, so the initial value `all`
applies and its background fades in over 200ms -- long enough to watch
the footer move underneath it.

AppShell used to wrap the content in a <main class="flex-auto">, which
held the row open no matter what happened inside it. Removing AppShell
dropped that element, and the landmark with it. It is back, and the
content is hidden with `visibility` instead, so nothing reflows at all.

Verified at 1200x800: on / and /help the footer stays at the same y
through the whole drag, the overlay hit-tests on top at header, middle
and footer, and h-full still resolves through <main>, which the error
screen's centring needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
None of these came from Tailwind; they are TypeScript hygiene errors
that predate the migration and only started being reported when the
config moved from eslint-plugin-svelte3 to svelte-eslint-parser, which
actually runs the TS rules over .svelte files.

Dead code removed: getText in AccurateRipSummary, cubeGenerator's unused
`cols` parameter, the CtdbSummary import whose only use is a commented
out panel (the comment now says to bring the import back with it), the
IconIncompleteCancel import, FileSystemIconLoader in the Vite config,
and the view-model and page-plugin fields MultiLogView never reads.

The two `any` casts become `object`, which is what castToType and
castToTypeArray already take. app.d.ts moves its namespace into
`declare global`, the shape SvelteKit templates use now. An unused catch
binding becomes a bare `catch`.

Three ternaries were being used as statements. TrackInfo's two become a
`bucket` helper, which also drops a pair of non-null assertions;
lightswitch's two become classList.toggle with an explicit force. That
file is otherwise still byte-identical to the vendored Skeleton v2 copy.

Verified: the disc geometry and AccurateRip grid render byte-identically
before and after (same innerHTML hash from the same fixture, with the
dev server proven to be picking up edits), and the theme toggle still
flips .dark both ways and survives a reload.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Choosing files from /log sent you home instead of to the new logs.

/log resolved its id from an effect over responseStore, and treated a
miss as "go home". inputChanged calls initialiseResponseStore before
anything else, which clears hashIndexLookup and replaces the store, so
the id of the log you were looking at stopped resolving the moment you
dropped new files. That effect then fired and its goto('/') landed after
the goto('/logs') issued synchronously in inputChanged, so home won.

Master got away with the same effect because routing for a batch lived
in a processedCount subscription that fired again on every completion,
re-issuing goto('/logs') until it stuck. Removing that standing
condition -- which is what sent you back to /logs after a reload --
left nothing to paper over the race.

The redirect now runs in afterNavigate, so it answers "was this page
opened with an id nothing can resolve", which is the question it was
always meant to ask. Store churn only updates what is rendered.

Verified against the backend on 3031, seventeen checks: one log and
several logs from home, from /log and from /logs; a row click and
browser back; the logo home from both views with a reload after it;
cold /log?id=bogus and cold /logs; and a rejected file reaching a
centred /error. The suite fails on the parent commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
There was one workflow and it only ran when somebody pressed a button,
so nothing was ever checked before it landed.

ci.yml runs on every branch push and every pull request: npm ci, the
svelte-check script, eslint, the static build, then clippy with warnings
denied. rust-embed compiles web/build into the binary, so the backend
job reuses the frontend's artifact rather than building the site twice.
Binaries are built on pushes only -- which includes the push to the
default branch that merging a pull request creates -- so a commit is
never built twice on its way through review.

release.yml is the only thing that touches Releases, and only a version
tag starts it. It refuses to publish when the tag and the Cargo.toml
version disagree, since a release that is not a version bump is a
mistake. Its workflow_dispatch input exists because Actions runs a
workflow as it exists at the ref being built, so a tag older than this
file can never trigger the push event; dispatching it from a branch that
has the file covers that.

Left out of the gates: prettier differs from 105 files here and cargo
fmt from most of the Rust, so both would land red. Reformatting either
is a commit of its own.

Verified locally: npm ci is in sync with the lockfile, npm run check and
eslint are clean, and clippy passes with -D warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
The first run warned that checkout, setup-node and upload-artifact all
target Node 20 and were being forced onto Node 24. That is a removal
notice, not a style note, so the actions go to the majors that declare
node24 themselves: checkout v7, setup-node v7, upload-artifact v7,
download-artifact v8. Every input in use still exists in those, checked
against each action's own manifest. rust-cache v2 already runs on
node24 and stays where it is.

ubuntu-latest becomes ubuntu-24.04. The runner label migrates to
Ubuntu 26 in October, and for the release binaries that is a real
change rather than a cosmetic one: glibc comes from the build image, so
letting the label drift silently raises the floor on what the published
Linux binary will run on. Windows and macOS stay on their latest labels.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
The frontend at 0.1.0 does not build on Node 22 or 24: Tailwind loads
tailwind.config.ts through jiti, whose sucrase interop hands back an
undefined binding for the Skeleton plugin, and postcss dies with
"Cannot read properties of undefined (reading 'call')". It builds
cleanly on Node 20. Nothing is wrong with that tree -- the workflow it
was written against said Node 21, and the pin here says 22.

Tag pushes are unaffected and still get 22. The dispatch path already
exists for tags older than this file; the Node they were written
against is part of what is older.

Verified on the same worktree at bef0975 with its own lockfile: builds
on 20.20.2, fails identically on 22.21.0 and 24.15.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Brings over 0.1.1: the drive-offset work from #9 and its follow-up
fixes in #17, the release workflow, the codegen crate, the clippy
sweep and the regenerated tables.

One conflict, in release.yml, where both sides had added the
node-version input. Resolved to a default of 22, which is what master
pinned to 20 until this branch landed -- the frontend here is the one
that builds on it. Older tags still need 20 passed explicitly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Brings in the six experimental ripper parsers, cambia-core 0.1.1, the
Shuttle config removal and the codegen lint fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
arg274 and others added 6 commits September 26, 2026 19:30
Inter shipped 18 static faces across nine weights and both styles. Only
six weights were ever referenced and no italic was, so twelve of the
faces were unreachable, and rust-embed baked all 5.2 MB into the binary.

Geist replaces them with five subsetted variable faces spanning 100-900,
so the three weights that previously had no file are now available too.
Greek is the one subset Inter had that Geist does not; Cyrillic, which
actually turns up in rip logs, is covered.

Also ships OFL.txt. Inter was OFL-1.1 as well and the licence had never
been distributed alongside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
Lists the six experimental rippers, and names the two TOC ID specs that
were already being generated but never mentioned, AccurateRip and freedb.

Platforms become a list because several of the new entries run on more
than one, so a row can carry an icon per platform.

The footer link follows the repository to its new home.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WDLpm8ZfJRx9rqNfSbNYxe
The parser was written against a single 10.3.0 log, and a 6.0.0 one
came out with no tracks at all.

6.0.0 splits the summary table's error count into read, data and sync
columns, so the row pattern now takes one or more counts.

Its firmware revision is "F611" rather than "2.00", so any four-character
trailing token is dropped from the drive name, not only a dotted one.

Normalization was read from "Calculate ReplayGain", which only tags.
"Apply ReplayGain" is what alters the audio. 10.3.0 does not log it, but
it does log Calculate, and gain that was never calculated cannot have
been applied, so a disabled Calculate still reads as no normalization.

Checked against both logs: the 10.3.0 output is unchanged, and 6.0.0
now gives 10 tracks with matching CRCs and the drive at offset 6.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
cyanrip, dBpoweramp, EZ CD, fre:ac, morituri and Rip now read like the
EAC, XLD and whipper parsers: the boolean_matcher, string_match and
optional_match helpers over named groups, nested crate imports, private
Single and Track structs with a Translator impl, new_no_skipzero for a
lone copy CRC, and no matching on bools or doc comments.

Code they each carried a copy of is shared. split_tracks slices a log
into per-track sections, and Time gains from_mm_ss_ff and from_frames
for the MM:SS.FF and raw frame counts several of these logs use.

Two dBpoweramp regexes nothing referenced are gone, as is a Rip branch
that returned the same status as the one after it.

Output is byte-identical on all 11 test logs these parsers handle.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
low32 counted digits after stripping leading zeros, so 0199f19e came out
as 199F19E and a zero checksum as 0. Parse the value and print the low
32 bits as eight hex digits instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Migrated with sv migrate: the config moves from svelte.config.js into
the sveltekit() plugin in vite.config.ts, $lib imports become #lib
subpath imports, $app/environment becomes $app/env, and TypeScript goes
to 6. The migration also ran prettier over src.

Two follow-ups the migration left broken:
- env.publicPrefix no longer exists in SvelteKit 3 and nothing read a
  CAMBIA_ variable, so it is gone.
- TypeScript 6 rejects ws.send() on a value annotated as plain
  Uint8Array; the inferred Uint8Array<ArrayBuffer> is accepted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@arg274
arg274 merged commit 1e7bb98 into main Oct 10, 2026
8 checks passed
@arg274
arg274 deleted the svelte5-migration branch October 10, 2026 05:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant