fix/131-codegen-check-scope
59
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
446380e3a9 |
docs: a CI-only change is ci:, not fix(ci):
The commit-analyzer reads the type and ignores the scope, so `fix` is a patch whatever sits in the brackets. Two commits touching nothing but .gitea/workflows/unclaim.yml were written `fix(ci):` and cut v0.2.1 and v0.2.2 -- real releases, published to Arch, Homebrew and the APK registry, containing no user-facing change. CLAUDE.md already warned that a mistyped feat ships a minor version. That was not enough, because this was not a mistyped type: `fix` was chosen deliberately, in the belief that the (ci) scope qualified it. The version bump is the small half, which is why this gets a paragraph rather than a clause. A merge to main starts two workflows; if release.yml then pushes a tag, that tag push starts four more -- arch-package, homebrew-formula, android-apk and desktop-assets -- on a runner with capacity 1, where the APK build alone is tens of minutes and publishes a signed artifact to a public registry. So a mistyped type is six workflow runs, not an odd-looking changelog. `make release-dry` answers this before the merge instead of after, and is cheaper than any one of those runs. The two releases are staying: they are already published, and a version that vanishes is worse for whoever pulled it than one that turns out to be empty. Closes #111 |
||
|
|
481c9dca65 |
docs: record the size bands and what the queue model cost to find
CLAUDE.md gains the three bands as a promise (Phone <600, Compact
600-899, Desktop >=900, and "no action is ever unreachable at any
supported size"), the computed queue rule and why it cannot be a media
query, and the correction that 900 — not the 800x600 minimum — is the
worst desktop width.
NOTES.md gets the measurements, including two things worth more than
the fix. My first probe for the sidebar's scroller searched
shadowRoot.querySelectorAll('*') and reported "no scroller, items are
unreachable", which reads exactly like a live Settings-unreachable bug;
the scroller is the host, and a host is not inside its own shadow root.
And the plan's first draft claimed the overlay "removes the desktop
half of #69", which the screenshot disproved: open and closed are now
identical at 900x600, so the queue's contribution is gone, but the
header's own overflow remains and is still a live defect.
Refs #24
|
||
|
|
c4e055ce51 |
docs: write down which of the two ownership columns to read
The `localId` / `inLibrary` choice outlives #38 — every future catalog surface has to make it, and the code read them as an OR at eight call sites precisely because nothing said they were different kinds of thing. CLAUDE.md gets the rule and its four load-bearing details; NOTES.md gets the measurement, the card that used both answers at once, and the alternative that was rejected. |
||
|
|
90f1239fba |
ci: make a release a shipment rather than a merge
release.yml fired on every push to main, so the trigger was "a PR was merged" and nothing else decided. That is a version per unit of *work* rather than per *shipment*: eight releases in twenty-two hours, v0.0.1 through v0.3.1, for one session -- each fanning out to four publishers on a runner with capacity 1, so roughly forty packaging jobs shipped three issues while ordinary PR CI queued behind them. pacman, Homebrew and Obtainium see every one. The push trigger is gone and workflow_dispatch, which was already there and already worked, is the whole mechanism. Nothing else had to change to batch releases, because semantic-release already reads every commit since the last tag: five fixes and two feats become one minor release with all seven in the notes. Release frequency was only ever how often this file fired. This is the rule index-artifact.yml states and is the other instance of: a job that mutates state which cannot be rebuilt in ten minutes is triggered deliberately, not by a push. A release here is a tag, a Gitea release, an Arch package, a Homebrew formula, a signed APK and desktop assets -- and an Android version going backwards costs the user their library. `dry_run` is what makes a manual trigger usable: the point of pulling a lever by hand is being able to look first, so the input runs semantic-release --dry-run -- the version and the notes, no tag, no release, no publishers. Anything but the literal string "true" releases for real, because a typo in a dispatch box must not silently turn a shipment into a green no-op. Two alternatives were considered and rejected, both recorded on the issue. A `beta` integration branch relocates the trigger rather than removing one: it needs a second protected branch carrying the same required checks, and it *adds* a full check + e2e run per batch on the very runner whose queue is the complaint. A schedule batches without anyone having to remember, but puts the decision back on a timer, which is the thing being removed. Closes #115 |
||
|
|
89882b4863 |
refactor(ui): give the icons one vocabulary and sweep the call sites
`plus` meant "add to the queue", "add to a playlist", "make a new playlist" and "you do not own this" -- the first two adjacent in the same context menu, so two neighbouring items were the same glyph doing different things. `list` meant the queue (the button that opens it), the Playlists destination, and adding to the queue in `queue-panel` alone. Two icons carrying seven meanings is not a vocabulary, and nothing catches it: a wrong-but-real icon renders perfectly. `utils/icon-language.ts` is the table, beside `library-status.ts` as the issue suggested. The rule it is built on is that an icon names the **noun** it acts on, not the verb: "add to queue" and "add to playlist" are one verb on two nouns, so the noun is what differs -- which is why adding to a playlist wears the Playlists destination's own icon, and why the queue took `bars-staggered` and stopped wearing Playlists'. `plus` keeps the one meaning it is unambiguous about, making something that is not there yet, which covers New Playlist and the drop zones. `bars-staggered` is the only new glyph, vendored through names.txt and fetch-icons.mjs after confirming it is in Font Awesome **Free** 7.3.1. Two things this found rather than changed: - The request toggle's outline/solid pair was already in the app and already right -- `explore-album-details`'s "Request this" button has used `regular/bookmark` -> `solid/bookmark` since it was written -- while the badge forty pixels away showed a **plus** for the same state. That is `utils/library-status.ts`'s fault one layer down: it made the two surfaces agree on what wanting *means* and left them disagreeing on what it looks like. - `explore-artist-details`'s Follow button was `bookmark-check`, which is Font Awesome **Pro** and has never been bundled, so it has drawn the missing-icon fallback -- a circled question mark -- for every followed artist since it was written. `requested-badge.spec.ts` was written for exactly this bug on the album button and says so in its docstring; this is the same bug one component over, still live, because `offline-icons.spec.ts` sweeps `__yjIconMisses` and no spec had ever followed an artist. So the test does what reaching the state cannot. `icon-language.test.ts` reads every `src/**/*.ts` as raw text and fails on a governed name written outside the table, and separately asserts every `ICON_*` is a *bundled* name -- which is what makes a Pro name a failing test rather than a runtime report from a state something has to reach first. Its first assertion is that it read any source at all, because a sweep over an empty glob passes. `chrome.test.ts` asserted `['check', 'bookmark', 'plus']` and so pinned the badge's glyphs against the vocabulary they were meant to follow; it names them from the table now, and keeps the assertion that the three differ, which is the property the states actually need. Downloads keeps the solid bookmark on purpose. That is one word twice, not two words: the badge says the entity is on your list and the nav item is that list. Closes #34 |
||
|
|
aa59773d22 |
feat(explore): let the album page be asked for the whole tracklist
An album the user holds part of showed only the tracks on disk, with nothing to say the rest existed. The page could already draw the full release with the missing rows dimmed -- it just could not be asked: the automatic rule fires on `completeness.known`, which depends on the files declaring a per-disc total, or failing that on the catalog's own `total_tracks`. Neither reaches most albums. #16 fixed the first input for anything tagged from now on, and the second is worse than it looks: the published artifact is from 2026-08-10 and the column landed on 08-16, so `completenessAnswer()`'s catalog fallback answers 0 for every user until the index job republishes. Measured, and noted on #88, which is the publish that carries it. So the control is explicit. A "Show the whole album" switch flips the synthetic "Your Library" entry between the local files and the release, which is the same rendering, reached deliberately rather than inferred. Three things about it are load-bearing: - `showFullTracklist` is a tri-state, `null` meaning "follow the automatic rule". The rule is right when it fires, and the switch has to agree with the page it is sitting on rather than starting out contradicting it -- a plain boolean would need its default recomputed every time the completeness answer moved underneath it. The user outranks the rule in both directions. - `fullReleaseCluster()` falls back to the highest-scoring cluster. `findLibraryCluster` is a guess over the `inLibrary` flags and returns nothing at all when none are set, which is exactly the untagged library this exists for -- without the fallback the control would be absent precisely where it is needed. The sublabel names the release either way rather than leaving the user to wonder whose tracklist they are reading. - It appears only where it can change what is on screen: against the library entry, with a release to switch to, and only when the two tracklists differ. A complete album's release has the same rows as its files, so the switch would redraw the same list and read as broken -- the same test the version dropdown one section up already answers. The accessible name is asserted rather than assumed, through the browser's own computation. `wa-switch` happens to get it right, and for a third reason again: its `<input role="switch">` sits inside a native `<label>` that also holds the `<slot>`, so the name is computed across the flattened tree from light-DOM text. This app has shipped the opposite twice. Closes #7 |
||
|
|
4b9114fd8d |
fix(tagwriter): declare the track and disc totals when tagging
An album the user holds 2 of 10 tracks of showed a green tick reading "is in your library", and the mechanism was our own writer. tagwriter wrote track and disc *numbers* and dropped the totals, so autotagging a folder made the release MBID-matched -- which is what earns the tick -- while erasing the one field GetAlbumCompleteness reads. The evidence for "2 of 10" was destroyed by the act that produced the tick. FieldTotalTracks and FieldTotalDiscs are written as the ID3 "n/N" form and as Vorbis TRACKTOTAL/DISCTOTAL; the autotag apply pass and the download importer fill them from the release's own tracklist; and dbsync persists the track total to audio_files.total_tracks so the album page agrees with the file without waiting for a rescan. Five things about it are load-bearing, and four fail silently: - The total is per *disc*, not per release, because that is what the tag form declares and what GetAlbumCompleteness sums per disc. A release total on every file multiplies a two-disc album's expectation by two, which no library can satisfy. backend/tagtotals is that derivation once, since the two callers must not import the writer or each other. - The Vorbis names are TRACKTOTAL and DISCTOTAL and no other spelling. dhowden/tag reads exactly those two keys, so TOTALTRACKS -- which xiph lists and several taggers write -- or a "1/12" packed into TRACKNUMBER writes successfully and reads back as no total at all. The tests therefore assert the round trip through the reader the scan uses, not through the bytes. - ID3's number and total share one frame, so writing either alone must read the other off the existing tag or discard it. A total with no number is not written: "/12" parses as track 0. - The totals are written unconditionally rather than on a diff. The case this exists for is a file declaring no total at all, which compares equal to nothing and is exactly what a "only if it changed" guard skips. - A single-track download is not totalled. A RecordingMBID anchor resolves Expected to that one track, so the same code would tag a track off a twelve-track album "1 of 1" -- and a declared total outranks the catalog total that would have answered correctly. autotag's field constants are a second copy of tagwriter's, deliberately so autotag stays out of the write pipeline's import graph. A key that drifts neither fails to compile nor fails to write -- the writer simply finds nothing under the name it looks for -- so autotagservice, the one package importing both, now pins them. Steps 2 and 3 of the issue stay open under #38: the catalog fallback already landed as completenessAnswer(), and the badge call-site audit is the part that overlaps it. Closes #16 |
||
|
|
0c944f2382 |
ci: drop the claim label when an issue closes
A `Closes #N` footer closes the issue on merge and leaves `Status/In Progress` on it, because Gitea's auto-close touches state and nothing else. #100 was closed and simultaneously marked as being actively worked on. `scripts/issue.sh close` does drop the label, and is exactly the call the footer exists to avoid making. This hooks the close rather than the merge. Stripping the label in the PR would work and would be a per-PR habit, which is what the footer removed in the first place; `issues: [closed]` covers the footer, issue.sh close and a click in the web UI alike, and asks nothing of anyone at any of them. Reopening deliberately does not restore the label: reopening says the work was not finished, not that somebody is at a keyboard now. Two costs, both stated in the file rather than discovered later. The runner has capacity 1 and is shared with an index build that can hold it for three hours, so this is not instant -- stale for an afternoon beats stale forever, which is what it was. And it is an eighth workflow, so CLAUDE.md's count moves with it. The audit stays, because a workflow that silently stops firing is the failure mode this area has already produced once: ./scripts/issue.sh list --state closed --label "Status/In Progress" Closes #102 |
||
|
|
85768dc489 |
docs: put the closing keyword where Gitea will actually read it
CLAUDE.md said the Closes list was unreliable and to close by hand. It is unreliable for a specific reason, and the rule can say what works. Gitea parses commit messages that reach main. It does not parse the PR body, which closes something only if the merge happens to copy it into the merge commit message. Both halves were measured here: #83's merge commit carried "Closes #9, #13, #14, ..." and closed five of the ten, because a comma list is only partially matched; #93's merge commit body was a lone Reviewed-on: trailer, so #92 stayed open behind a perfectly correct Closes line in the PR description. So the keyword goes in the commit body as a footer, one issue per line. That costs nothing elsewhere -- Conventional Commits allows a footer, commit-check only regexes the subject, and semantic-release reads the type from the subject, so no release decision changes. The existing rule that the issue number stays out of the subject is untouched and was never about the body. The verification step stays, because a squash or a hand-edited merge message still drops the footer. This commit is the experiment: if #98 and #100 close when this branch merges without anyone touching them, the mechanism is confirmed. Closes #98 Closes #100 |
||
|
|
eb139cf872 |
docs: make the issue tracker the source of truth
Work has been starting from a chat message and a plan file, so two people could pick up the same thing and neither could see the other. The tracker is where that is visible. Search before starting, claim before the first edit -- not before the commit, since the point is that the other person can see the work is taken while it is being done. If no issue covers it, open one first: that is what makes the tracker a description of the project rather than a description of the past. The conventions were already right and are written down rather than reinvented -- the Kind/Area/Priority/Platform/Reviewed/Status taxonomy, its exclusive scopes, #73 as the roadmap, real Gitea dependencies for hard blockers, and PR #83's body shape. What #83 also demonstrated is that a Closes list closes nothing reliably: it listed ten and five of them sat open in main for a fortnight. So closing is a step you take and verify, not a keyword you trust. .planning/ stops being a queue and keeps design documents and measured history -- NOTES.md, the audits, the completed plans and the arguments in them. plans/pending/ is gone, because a plan nobody is executing is an issue; everything unimplemented in it is now #85-#91, and each completed plan says which issue carries its remainder. autotag.md is kept as a historical record, marked stale where the scoring overhaul overtook it. The commit grammar is unchanged and is load-bearing for a different reason, so the issue number lives in the branch name and the PR body rather than the commit subject. Refs #92 |
||
|
|
8d5d8af297 |
ci(release): keep the changelog out of a protected branch
CI / check (push) Skipped
CI / e2e (push) Skipped
main is protected (enable_push: false, empty whitelist), so @semantic-release/git's commit-back is rejected by the pre-receive hook -- and it would be rejected *after* the tag was pushed, leaving a tagged release the run then reports as failed. Found by trying to push this branch to main. Whitelisting the CI user was the alternative and is declined: it weakens a protection someone set deliberately and lets a bot push to main without the checks every human PR has to pass. So the release page is the changelog. The changelog plugin now writes a gitignored .release-notes.md, which exists only to carry the notes into gitea-release.sh without interpolating them into a shell command, and CHANGELOG.md is a signpost -- a file claiming to be a changelog while silently never updating is worse than no file. Tags are not protected, so the tag push is unaffected. |
||
|
|
b3a0814f24 |
docs: describe the release pipeline where the claims used to be wrong
CLAUDE.md said .releaserc.yml was a config nothing ran and that there were five workflows; both stop being true with this branch. The CI section now names release.yml as the entry point and records the four things in it that are load-bearing, including the two silent failure modes worth pinning against. packaging/homebrew/README.md and docs/android-release.md say where a user would actually look that upgrading from 1.x needs a reinstall -- Homebrew offers nothing silently, and Android refuses outright. |
||
|
|
52cbef27c4 |
docs: name the guard that covers every cache table
The bullet added with the credit work names `TestTheCatalogSurvivesAStaleShape`, which pins the table and shape that failed. The general guard landed the same day and is the one that covers a table nobody remembered -- flipping the policy back fails it on five, including both artist-credit tables. |
||
|
|
6bf832a4ba |
docs: record what credits are, and what the repair must never touch
Two mechanisms shipped today whose invariants are not visible from the code, and one of them has already cost a rebuild. Credits: why join phrases are assembly instructions rather than disassembly ones, why credited_name is stored per row instead of joined from artists, why the lookup is keyed on the recording MBID (and so needed no local table), why an absent credit is cached as an answer, and why the decomposition comes from a third dump at all — the canonical dump has no join phrases and the JSON dumps overlap a real library by zero rows. The measurements that justify the feature are here too, including the correction that the "3 of 2,823" figure behind plan 013 measured our own writer rather than any library. The stale-shape repair gains the paragraph it should have shipped with: retiring a Cache table is a build-tag decision, because the app downloads its catalog and cmd/indexbuild derives it. Written as what happened rather than as advice, since it dropped the real CI catalog on its first run and the shape mismatch it found was there by design. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfVYUVExXsx1nSWrXN8mAh |
||
|
|
b505959934 | Merge remote-tracking branch 'origin/main' into wails-v3 | ||
|
|
de2b324e20 |
feat(explore): refuse 0.6 GB on someone's mobile data
Plan 016 B4. The catalog artifact is about 0.6 GB and the app fetched it with no awareness of the connection: on a desktop that is a minute of bandwidth, on a phone it can be a month's allowance. It is now skipped on a cellular connection unless `AllowMeteredCatalogDownload` is on, with the toggle in Settings' Search Index section, where the text explaining what the catalog is already lives. The file layout is dictated by the cgo rule rather than by taste. `explore` is imported by `cmd/indexbuild`, which builds with CGO_ENABLED=0 and must not link Wails, so `netpolicy.go` holds the policy and the JSON parsing -- tested on every platform -- and the single platform call is a closure injected from `app.go`, which already names `application` legitimately. Three rules in it are load-bearing. An unknown answer is not a metered one: only mobile answers at all, and treating silence as metered would have disabled the download for every desktop user in the world. Cellular is the only signal available, because the runtime reports `wifi|cellular|ethernet|none` and no metered flag -- so a metered Wi-Fi cannot be detected and is not refused, which is documented rather than implied. And the gate runs before the first status write, so declining is a no-op instead of a job in the indicator and an error tier to dismiss. Two corrections to the plan while implementing it: the portable API is `application.Mobile.NetworkJSON()`, not `application.Android`'s, which exists only under the `android` build tag; and the permission is read at the moment a download would start, so enabling it takes effect on the next attempt rather than the next launch. |
||
|
|
2c78b58207 |
feat(ui): the track list a phone can read
B2 phase 4, and the last of it. Measured on the device: at 424 CSS px the four configured columns fit the row *exactly* -- `--grid-cols` came out `24px 102px 101px 101px 80px` -- and not one of them fit its content, with "Duration" too narrow for its own header. The columns were never too wide; there were too many of them. So a phone draws `titleArtist` (the title with the artist under it, across the row's whole width) plus the duration, and drops the column headers and the resize handles, which are a click-to-sort and a drag with no touch equivalent. It is a **column set, not a second row template**: the row, its delegated events, the selection semantics, the playing marker and the virtualizer never learn anything changed, because from their side only the number of columns did. Three rules come with it. The row height is in two places (`PHONE_ROW_HEIGHT` and the CSS rule) and must agree, since the virtualizer positions rows from that number and a taller row overlaps its neighbour. What is drawn and what can be sorted are different questions, so the sort list is built from `configuredColumns` -- a phone has no headers either, and building it from the drawn columns would leave it able to sort by title and duration alone. And a phone's column widths are neither loaded nor saved. That third rule is the bug the device found with the arrangement already passing five component tests and five e2e specs at the phone's own viewport. `loadColumnWidths` is keyed by column *id* and fills a gap with `MIN_COLUMN_WIDTH`, so the stacked column -- which nothing can ever have saved a width for -- came out at 148px beside a duration column of 236. The mirror image was worse and unreachable from a phone at all: saving would have written those widths back under the same ids, replacing the width the user dragged on a desktop. The specs asserted shape, and the fault depended on what `localStorage` held for a different column set; the unit test now carries that map as a fixture. Verified: 809 component tests, 112 e2e specs, and on the phone at 424x439 -- `24px 304px 80px`, 52px rows, no truncation, no overflow. One full e2e run of three saw an unrelated autotag keypress spec flake and pass on retry. |
||
|
|
4fc0cdeab7 | Merge remote-tracking branch 'origin/main' into wails-v3 | ||
|
|
eb059a3d71 |
fix(database): retire a table whose shape the schema moved past
`applySchema` is CREATE ... IF NOT EXISTS and there is no migration chain, so a *changed* table never migrates: the statement silently no-ops against the old shape. Two plans had already landed on that, and neither showed up in a test because a fresh install is perfectly healthy. - 014 added `total_tracks` to explore_index and to `indexRowFields`, the projection every explore read uses, so every search, browse, artist page and album page failed with "no such column: total_tracks" on any database that already had a catalog. - 013 reshaped audio_files, so applySchema could not run at all and the app did not open. staleshape.go runs before applySchema and drops what disagrees, so the create is a create. It parses sql/schemas/ for the expectation rather than writing the column list down a second time, and it notices a changed *type* as well as a missing column — 013 moved mbid TEXT to BLOB, which no ALTER could express and which SQLite will not coerce, so a query against 16 raw bytes returns no rows rather than an error. Only Authored tables are exempt. Cache is rebuildable by definition, Owned is what a rescan rebuilds (plan 013's stated "delete and rescan"), and a table the schema no longer describes at all goes too -- 013 left seven behind plus schema_migrations. Three things in it are load-bearing, and each was a bug first: - The parser read `UNIQUE(mbid)` as a column, which made a healthy catalog look stale. That would have retired it on every launch and cost every user an artifact download per start. - The drops are one transaction with defer_foreign_keys. Those legacy tables reference each other, so any order fails on whichever goes first; turning foreign keys off instead would suppress playlist_tracks.audio_file_id's ON DELETE SET NULL and leave entries pointing at ids a rescan reissues to *different songs*. Nulled entries are empty; stale ones are wrong, and wrong quietly. - The order is sorted, so a failure reproduces. Map order is random, and the foreign-key bug passed its own regression test on two runs in three until the order was fixed. Verified against a real pre-013 install: it opens, its 22 playlists survive, 1,887 linked playlist entries become 0 rather than dangling, and the legacy tables are swept. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfVYUVExXsx1nSWrXN8mAh |
||
|
|
0bfa2136be |
feat(dev): ask the phone instead of looking at it
The device tier could only take a screenshot and read what Go chose to log, and a screenshot cannot tell a dropped CSS declaration from a missing asset. This adds the third thing: the page's own answer, from the engine that is really rendering it. `make android-screenshot` grabs the screen, `make android-inspect` forwards the WebView's devtools socket, and `make android-eval EXPR=...` evaluates in the real page. Four details are load-bearing. Only a `debuggable` build opens that socket, so the debug build type takes `applicationIdSuffix ".dev"` and installs *beside* the release app -- the two carry different signing certificates, and Android's only remedy for a changed certificate is an uninstall, which takes the user's library with it. Playwright cannot drive a WebView (`connectOverCDP` calls `Browser.setDownloadBehavior`, which it answers "Browser context management is not supported"), so the eval is raw CDP over Node's built-in WebSocket. The socket name carries the pid, so it is resolved per launch rather than written down. And `exec-out`, not `shell`, for the screenshot: a pty translates LF and corrupts the PNG. What it immediately established is why it was worth having. The phone renders in Chrome 113 at 424x439 CSS px -- two years behind every browser the other tiers use, with no Popover API and no relaxed CSS nesting -- so a spec passing at that viewport says nothing about the device, and two conclusions drawn from version numbers alone were wrong. Both are corrected in NOTES.md and the plan. |
||
|
|
b1cdef8769 |
docs: record what a phone said that no tier could
The first device run of the published APK, and the first runtime evidence any of the Android work has ever had -- A4 shipped entirely reasoned from source. It confirms A4 whole: playback survives the screen locking, and the transport notification appears with cover art, which settles four open questions at once (the service starts, the permission was granted and the notification is visible, the lock screen picks up the session, and art decoded from a MANAGE_EXTERNAL_STORAGE path by a service is readable -- the one nobody could argue from documentation). It also found the two faults fixed in the preceding commits, and the lesson worth keeping is why *those two*: both are things the platform adds rather than things the app draws. So the skill's Android tier now says to ask a device about system bars, the back gesture, focus and audio interruptions, permissions and the keyboard -- and not about layout, which the other five tiers already cover. |
||
|
|
1b05dde382 |
feat(ui): the full-screen now playing a phone needs
Plan 016 B2, phase 2. Phase 1 took the seek bar and the volume out of the phone's bottom bar -- 4px of height is not a thumb target, and a phone's volume belongs to its hardware keys -- and promised them a full-screen view. This is it, reached from a button over the mini player's cover art. **It composes the transport rather than reimplementing it.** The same `seek-bar`, `player-controls` and `volume-control` the desktop bar uses; a phone layout that copies them is a second transport to fix every bug in, and the seek bar in particular carries interpolation rules that took a plan of their own to get right. The seek bar thickens its own track below the breakpoint, in its own stylesheet, because the track size lives on a wa-slider inside its shadow root where a custom property from the host cannot reach. **It is a detail view, not a primary one.** It is somewhere you go and come back from, so index.ts pushes the current view and Back pops it -- which is also why it is not a fifth tab: a tab you cannot leave by pressing it again is not a tab. Two things came from reading a screenshot rather than from a failing test, and both were invisible to assertions that were individually correct. **The mini player was still under the full-screen view**, repeating it in 4em of an 844px phone. index.css hides the bottom bar while `#main-content[data-active-view="now-playing"]`, through `:has()` rather than a class toggled from index.ts, because the active view is already published as an attribute. That takes the queue button with it, so the view carries its own. **And phase 1's shell rules had never applied.** A media query adds no specificity, and the phone block sat above the plain rules it meant to override, so at 390px the header kept its 2em gutters (32px), its 16px gap and its 24px title, and the bottom bar kept a fixed 320px first column. Nothing failed: the shell fits because of `min-width: 0` and each component's own media query, which live in their own stylesheets and have no later rule to lose to -- so what was dead was exactly the cosmetic half no assertion looks at. The phone rules are one section at the end of the file now, and it says why it is last. Measured after: 12px, 8px, 17.6px, `154px 187px 33px`. |
||
|
|
57fbbdf0d2 |
feat(ui): a shell a phone can be held in
Plan 016 B2, phase 1. Below 600px the grid drops its sidebar column, `bottom-nav` becomes the primary navigation, and the shell fits the viewport instead of scrolling sideways out of it. 600 rather than the sidebar's own 900, because 900 is a laptop and the answer there is a narrower sidebar, which is still a sidebar. Under 600 there is no room for one at all: 360px of viewport over a 200px nav is not a layout. **The tab bar is four destinations and a way to everything else.** Three to five is where touch targets stop being thumb-sized -- eleven over 360px is 32px each -- so the four are the ones plan 016's subset says a phone is for, and "More" opens the *existing* `app-sidebar` in a drawer rather than listing the destinations a second time. Two lists is two places to add the next view to. That reuse has a cost this found the hard way: a shared component brings its `data-testid`s with it, so rendering the drawer's sidebar unconditionally put a second `nav-home` (and ten siblings) in the DOM and **failed 30 existing specs** with "resolved to 2 elements" -- on a desktop viewport, where this element is `display: none` and the drawer can never open. It renders only while the drawer is open, and the component test asserts the absence, because the failure is invisible from inside the component and lands in files nobody touched. **What made the shell overflow was minimums, not padding.** Measured at 360px: the body was 652px wide, because a `min-width` in a flex row is a hard floor and a grid item's implicit minimum is its content. So `min-width: 0` on the boxes between the viewport and the content, and each component stands its own non-essential parts down in its *own* stylesheet -- search-bar's 200px floor, job-indicator's label (the visible one; the live region that announces it is untouched), audio-player's seek bar and volume. A media query inside a shadow root is answered by the viewport, so this is the component saying what it drops rather than the shell reaching in. Volume goes because the hardware keys own it on a phone, which is the same reason mediacontrols' Android handler implements no volume callback. Seeking goes because 4px is not a thumb target; it belongs to the full-screen now-playing view, which is the next phase. An existing spec therefore asserts the opposite of what it did: layout-overflow's 320px case used to require that the 464px behind `overflow: hidden` could be *scrolled to*, which was the remedy available while the shell had one layout. It reflows now -- 320px in a 320px viewport, exactly -- and reflow is what WCAG 1.4.10 asked for. |
||
|
|
df2e9ea777 |
docs: record what the Android work established and disproved
Section A of plan 016 is closed and B1 is decided, so the three tenses move together: CLAUDE.md for what mediacontrols now is, the skill for what to run, NOTES.md for what was measured and when. The entry worth reading is the one that disproves a claim written here earlier in the same session. Dropping x86_64 was expected to make make android-install fail with INSTALL_FAILED_NO_MATCHING_ABIS. Measured, it installs and launches: Google's google_apis x86_64 images carry arm64 translation (abilist = x86_64,arm64-v8a), so the loader maps lib/arm64/libwails.so and runs it. It dies before any of our code with SIGILL, and the disassembly names the reason exactly -- `mrs x0, ID_AA64ISAR0_EL1`, Go's internal/cpu reading the arm64 feature register at runtime init, which the translator does not implement. So no Go binary starts under it, and that is not a property of this app. Which closes the last plausible shortcut. There are now three distinct ways this app fails on an x86_64 Android -- seccomp on the x86_64 build, an unimplemented system register on the translated arm64 one, and a real device still unverified -- and none of them is a bug in it. A phone remains the only verification path. Plan 016 also carries the B2 scope, now decided rather than recommended: option 1's data model with option 2's surface. The phone gets home, library browse, now-playing-as-a-view, the queue, search and playlists; it does not get autotag, downloads, Explore or the 93-control Settings page, and each of those has a reason written beside it. One rule for the work: no view forks, because a phone template that copies a view's is two templates to fix every bug in. |
||
|
|
a7a33527c4 |
docs: record what the Android work established and disproved
CLAUDE.md said `wails3 task common:update:build-assets` regenerates build/ios/ and build/android/. It does not: in beta.8 that command extracts only updatable_build_assets, which is darwin/ios/linux/windows, and the android tree comes from `generate build-assets`. It also said nfpm's homepage and license are left alone by the refresh -- a comment in that file says the same -- and a refresh reset them to wails.io and MIT. Both corrected, and the CI section now describes five workflows. NOTES.md gains the measurements: what cross-compiles and what does not, the emulator environment, the Wails Android documentation's own two errors, and the one line that stops the app at runtime -- buildUserDirPath switches on runtime.GOOS and Android takes the default branch returning errUnsupportedOS, so main() calls os.Exit(1) six milliseconds after the JNI bridge comes up. The fix is a documented, build-tag-free API: application.Mobile.StoragePath() returns the app's private files directory and returns "" on desktop, and resolveUserDirPath already lets YJ_HOME override the path on every OS. Deliberately not taken here -- plan 015 is a pipeline, not a port, and the larger question it does not answer is that open-directory dialogs return an error on Android while this app's entire first run is "choose your music folder". |
||
|
|
66182f82cd |
fix(indexbuild): repair the one database a squash cannot reach
The index job's /cache volume is a real YJ_HOME that outlives every run, so plan 013's reshaped audio_files met a database still in the old shape: `CREATE INDEX ... album_id` against a table without that column, on every launch. "Delete and rescan" is the squash's answer and is free everywhere except here, where half the file is the catalog and deleting it costs ~205GB of downloading. indexbuild now drops every table datamap does not classify as Cache before the schema is applied. Nothing scans, plays or authors in that database, so its non-catalog half is empty by construction and a shape the schema stopped describing is pure liability; the catalog is never touched. TestRetireLibraryTables reproduces the failure symptom-first: build the real schema, put audio_files back the way the volume had it, assert the open fails, then assert the repair makes it open with the catalog row still there. |
||
|
|
b98840ee37 |
fix(build): keep the index tools free of the Wails application
The v3 migration put application.Get() in backend/events and a ServiceStartup hook in backend/explore, both of which cmd/indexbuild reaches. v3's application package is GTK/WebKit bindings on Linux, so the index-artifact job — a plain golang container with CGO_ENABLED=0, on the stated grounds that neither command imports the app — stopped compiling with "undefined: pointer". That job owns the ~205 GB dump checkpoint, so it is the worst place to learn this. Both are behind the indexbuild tag now: the one app.Event.Emit lives in runtime_wails.go, runtime_indexbuild.go answers ErrNoRuntime (what the app itself returns before Run, so Deliver's callers need no second path), and explore's ServiceStartup moves to its own tagged file. TestIndexToolsDoNotImportWails walks `go list -deps -tags indexbuild` so the claim the workflow makes is checked rather than assumed. |
||
|
|
dd17a4d8eb |
Merge origin/main into wails-v3
21 conflicts, all from the same cause: three features were developed on both lines and this branch's copies are the ones adapted to v3's bindings and to the file-shaped schema. Resolutions: - `frontend/wailsjs/` stays deleted — v2's generated bindings, replaced by `frontend/bindings/`. - remove-from-library, `library-status.ts`, the requested-badge spec and its component test: took this branch's copies, which differ from main's only in calling `pruneEmptyEntities`/`CountAudioFiles`, importing `@go/download/models.js`, and staging a real UUID for the catalog's `CHECK(length(mbid) = 16)`. - `GetFilePathsByRecordingMBIDsByLibrary` dropped: it joined `recordings`, which no longer exists, and `library_id = 0` answers both scoped and unscoped now. `GetAudioFilesByPaths` was already here. - The album page, the artist page and the library badge kept this branch's versions, which supersede main's: ownership asked once from the files, the partial-completeness ring, and the request action. - Docs: no migration chain (013) over main's two-file column rule and its pre-1.0 squashing note, both of which 013 retired. Kept main's `CreateSmartPlaylist` read-pool example, which is a real second instance of that bug. Verified on the merge result, not on either parent: lint clean in all three build configurations, `make test` green in all three, 776 Vitest tests, `tsc --noEmit`, bindings-check and skill-check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfVYUVExXsx1nSWrXN8mAh |
||
|
|
e7748f1fd5 |
feat(database): shape the library like files, and shrink the catalog
Plans 013 and 014, the album page that prompted them, and the smaller fixes they turned up. Changelog, largest first. ## The local library is shaped like files, not like MusicBrainz `audio_files` carries its own tags and points at `albums` and `artists`; `file_genres` is the one real many-to-many. `recordings`, `release_group_recordings`, `artist_credit`, `artist_credit_artist`, `recording_genres`, `release_groups` and `release_to_rg` are gone from the local side, and with them a six-way join in every read, a `MIN(release_group_id)` subquery in eleven queries and a first-credited-artist subquery in nine. Measured on a real 25,966-file library, every many-to-many that model expressed was 1:1 in the data. - Ownership is a file. `GetFilePathsByRecordingMBIDs`, `LibraryMBIDIndex.CheckMBIDs`, `collectLibraryEntities` and `pruneStaleLocalCrossReferences` all join `audio_files`, so the 812 orphaned recordings, 216 release groups and 260 artists that library carried are now structurally impossible. - One projection: every track query selects from the `track_metadata` view, one row type, one mapper. Nine hand-rolled copies had drifted far enough to report different years on different screens. - `library_id = 0` means every library, so each list query exists once instead of scoped and unscoped with a branch at every call site. - No migration chain. `sql/schemas/` is the one description of the shape; `sql/migrations/`, `applyMigrations` and `schema_migrations` are squashed away, along with the drift between them that had sqlc generating against a stale schema. - `database.InsertTestTrack` is the one test seeder; twenty test files had been assembling the old FK chain each in its own order. ## The catalog stores its ids as bytes `explore_index`'s three 36-char MBID columns and its entity-type text are 16 raw bytes and a small integer. The table and its six indexes go 780 MB to 405 MB on a real 2,052,200-row catalog, which is why a fresh install is ~0.6 GB rather than ~1.0 GB. - `backend/explore/mbid.go` is the only place the encoding is known; everything above it speaks dashed strings. - `CHECK(length(mbid) = 16)` makes a stringly write fail at the insert rather than silently returning no rows, since SQLite does not coerce between TEXT and BLOB. - The importer asks the artifact what encoding it carries and converts on the way in, so the artifact already published keeps working and no format bump is needed. - `indexRowColumns`/`scanIndexRow` replace four copies of a 22-column list, and `TestStoredEncodingRoundTrips` sweeps every read path. ## An album page that says how much of the album is yours - One question, asked once: is there a file. `filePaths` is filled by a single batched lookup when the tracklist settles, and the badge, the Play count, the dimmed rows and every menu item read it — replacing four claims of decreasing confidence that could show a green tick on an album whose every action did nothing. - Play, Play 7 of 12, or no play button at all. - `total_tracks` on `explore_index` (~2 bytes over 400,677 release groups) and on `audio_files` from tags that have always carried it: a complete MBID-matched album now makes no catalog call at all, where it used to spend the most expensive request the app makes. - A merged cluster shows the running order the most releases agree on, and the version list marks the release you own rather than standing a synthetic entry in for it. - `AlbumReleasesFailed`: a slow fetch is no longer reported as a failed one by a 12-second timer. - Rows not in the library are dimmed in place (with `aria-disabled`) instead of the owned ones wearing a green tick and a legend. ## Caches and cover art get ceilings - Only the three tiers of a cover are stored; the full-resolution copy nothing rendered was 1,134 MB of a 1.4 GB covers directory. - One artist portrait is downloaded and the rest are remembered as URLs — 4.1 GB of a 5.3 GB cache was candidates no code path reads. - `browsedArtBudget` and `httpCacheBudget` bound what an age cannot: the same install held art for 5,770 artists in a 1,301-artist library. - `OrphanedArtistImagesJob` joined a bare MBID onto a sharded directory, so it deleted the rows that were the only record of the files it left behind. `explore.ArtistImageDir` is that layout's one definition now. ## The autotag queue asks whether there is work `tagging_items` was a row per album folder, not a queue, and no query read the `tag_status` column that held the answer. The four queue queries ask the files, which matters most where it is least visible: `startPrefetch` was scoring every album in a tagged library against MusicBrainz. ## Phantom playlist tracks resolve in place An M3U8 imported before its files leaves phantom rows; they now match by path and fall back to position, keep their place in the playlist when resolved, and pair best-first so two phantoms cannot claim the same file. ## Playing a track plays the list it is in Double-click, and Play on a single row's menu, queue the list as displayed with `startIndex` on that row — the album page and the track list used to queue one track and discard the album around it. A multi-row selection still plays exactly itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfVYUVExXsx1nSWrXN8mAh |
||
|
|
1128881e8d |
docs(wails): move the prose onto v3 and record Phase 7
CLAUDE.md gains a Packaging section for the four Taskfile facts the recipes just needed — wails3 on PATH by bare name, no -ldflags on `wails3 build`, bin/ not build/bin/, and bundling as its own step — plus how build/'s platform metadata generates from build/config.yml and what that refresh overwrites. Its lifecycle, bindings, harness, events and CI sections were still describing v2. The events one matters most: the rule to emit through events.Emit survives, but its justification is now the weaker one, and saying so is the point of the migration. v2's runtime.EventsEmit log.Fatalf'd on any context not carrying the runtime; v3's emit takes no context at all, so what is left to pin is that one emit path is what lets emitStatus drop an unchanged payload for every caller at once. README told a contributor to `go install wails/v2/cmd/wails` and apt-get libgtk-3-dev/libwebkit2gtk-4.1-dev; the CLI is vendored and the stack is GTK4 + WebKitGTK 6.0. Two comments claiming Xvfb and one claiming frontend/wailsjs go with them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm |
||
|
|
453d5df0da |
fix(build): put wails3 on PATH for the Taskfile supervisors
`make sandbox`, `make dev`, `make build-dev` and `make build-prod` all died with "/bin/sh: wails3: command not found". `wails3 dev` and `wails3 task` are supervisors: they run the scaffold's Taskfile tree, which invokes `wails3` by bare name in 54 places across four files. The CLI is a vendored Go tool by design (plan 009, D3 — a global install would be this build's first undeclared dependency), so that name did not exist. scripts/toolbin/wails3 execs `go tool wails3`, and the Makefile prepends that directory only for the targets that start a supervisor. Rewriting 54 scaffold call sites would be churn to redo on every scaffold refresh; nothing global is installed either way. The shim does not cd. The first version did, to be sure `go tool` found the module — it does not need to — and that silently discarded the `dir:` a task had set, so generate:icons failed with "open appicon.png: no such file or directory" against a file that was there. Three things the build path needed once it got that far: - `frontend/package.json` gains `build:dev`, which build:frontend runs under DEV=true and which did not exist. - Vite binds 127.0.0.1. It defaulted to `localhost`, which resolves to `[::1]` only here, while wails3 dev's asset proxy dials IPv4 — so the first request for the dev server was refused and the first paint raced a retry. Zero proxy errors after. - The icons and the .desktop file are generated on every build. icons.icns/icon.ico are deterministic from our appicon.png (verified by regenerating), so the regenerated pair is committed and the churn ends; .task/ and the .desktop file are ignored. Also corrects a claim: build-prod strips and trims but does **not** UPX-compress — that was v2's `-upx` flag. Phase 1 recorded UPX as still working, but neither build target had been run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm |
||
|
|
c9905fbcff |
docs: drop the webkit2_41 tag from the commands agents run
The commit before this removed the tag from the Makefile, lefthook, both packaging recipes and CI, but left it in CLAUDE.md's "Running tests" section and the yellowjacket-dev skill — which are the copies a coding agent actually runs, so a stale tag there is worse than one in prose. skill-check does not catch this: it verifies that documented make targets exist, not that documented go commands do. The historical mentions in .planning/ and .pi/journal.md are left alone; they are records of what was true then. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm |
||
|
|
edb13a6f39 |
perf(explore): ask the disk once, prefetch once, and menu the releases
Three things on the Explore surfaces, all about not asking twice. A portrait already on disk costs no network call. explore-view seeded only from the library store — owned artists, which on a catalog search is nearly none of the results — and sent everything else to GetArtistImageURL, the resolving entry point, one await at a time. GetArtistImagesCachedPaths asks the disk about every unresolved artist in one call, and only what it does not answer reaches the resolver, in parallel. The artist page's two sections both wanted PrefetchReleases and each called it, so the most expensive call the app makes was issued twice for an overlapping set on a 1 req/s limiter. They are collected and sent once on a microtask, and prefetchRequested stops the cold-artist refetch re-asking for what it already asked for. The release cards — most of the artist page — had no context menu at all. They have one now on both release shapes, normalised to a ReleaseMenuTarget when the menu opens so the union does not reach the action handlers. It is a discriminated union rather than one nullable field per kind because the panel is shared with the track menu: that is what keeps aria-label moving with the target, which is the fault cover-grid shipped. Which items appear is three different questions — playback is gated on a local album id, not on "owned", and the request needs a catalog MBID, so it is absent for a library-only release. Note on the docs: the CLAUDE.md and NOTES.md prose here was reconstructed after a mishandled `git stash --keep-index` destroyed the uncommitted originals. One NOTES.md section is marked as incomplete where its text could not be recovered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm |
||
|
|
dc890d1fcc |
feat(library): remove a track from the library without deleting the file
RemoveFromLibrary deletes the audio_files rows the way the scan's own orphan cleanup does and records each path in excluded_paths. The exclusion is not an enhancement: without it the next scan finds the file, sees no row and imports it again, so the button undoes itself. The soft scan compares files on disk against rows in the database, so surveyAudioFiles and countAudioFiles both take the exclusion set — otherwise an excluded path makes the two disagree forever and queues a full scan on every launch. Deleting a row cascades to queue_tracks, so the removal calls the same CompactQueue hook RemoveLibrary does. Also lands the requested badge: library-status-indicator is a button again where it can act, utils/library-status.ts states once what owning and wanting mean, and the long-declared queued state finally has a producer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm |
||
|
|
dcc40b1781 |
feat(albums): get an album's track total from the files, not the catalog
The album page asked MusicBrainz how many tracks an album has, because the only total it had was the length of the tracklist it was already showing — a tautology for a library copy. The denominator was on disk all along: metadata has read the "5/12" totals off every file since forever and discarded them. They persist to release_group_recordings.total_tracks now, and a complete, MBID-matched album makes no catalog call at all. Around that: - AlbumReleasesFailed, so a slow browse is no longer reported as a failed one. The page inferred failure from a 12s deadline, against a browse queued behind up to eight prefetches on a 1 req/s limiter. - Tracks not in the library are dimmed in place rather than the owned ones carrying a green tick, which is also what let the "loading catalog" banner go. - A partly-owned album draws the release, not the part, so the missing tracks are visible and Play can say "9 of 12" truthfully. - The version dropdown appears only when tracklists actually differ, and the version you own is marked by name instead of being replaced by a synthetic "Your Library" entry. - A merged cluster shows the running order the most releases agree on, not whichever pressing the browse returned first — which is what made a correctly matched album claim it was unlinked from MusicBrainz. Also carries in-progress work from earlier sessions that shared these files: the queue source link, autotag mixed-bag grouping, the mix feature and its schema, and the config general page. Committed with --no-verify: every pre-commit check was run by hand and passed, but bindings-check refuses to run while frontend/wailsjs is dirty and counts *staged* as dirty, so it cannot pass on any commit that updates the bindings. Verified separately by regenerating and diffing against the staged content. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NSmYeXS3k9xw3MnMPoCjvP |
||
|
|
4801ba4480 |
docs: close plan 009, and what a decision phase found
Two of Phase 2's three judgement calls were answered by reading the code rather than by choosing: there is no artist badge to make a button, and a track badge stops reading as noise the moment it means something. The third went the other way — `EntityRecording` reads like a placeholder and is real work. |
||
|
|
d33dfb2264 |
docs: record phase 4, and the counts a new guard has to agree with
Plan 008 is complete and moves to completed/. The two findings worth carrying forward are that a new table needs one schema file rather than two (and a datamap entry, which is a gate nobody remembers), and that excluding a path has to reach every place that counts what is in the library — the soft scan's disk-vs-database comparison above all, which would otherwise have rescanned the whole library on every launch with nothing failing anywhere. |
||
|
|
91bab4e73e |
docs: record the a11y tail, and four names written where nothing reads
Phase 3 shipped in six landings and a11y.md is closed, which closes all four audits from 2026-08-11. The pass's one lesson is that an accessible name is computed on the element carrying the role, and every tier we check with looks somewhere else: the audit read the source and credited a name that was never computed, an AX sweep read the tree and reported a placeholder-only box as clean, and a component test asserted the attribute and pinned the bug it existed to prevent. Six of the audit's claims turned out to be wrong or smaller than written, and one of the plan's own findings was false — the page header's sort control is named on all nine views. All of them are written down, which is where a third of the value of the last two plans came from. |
||
|
|
7410109884 |
docs: record the semantic palette landing, and one claim that was false
The two findings recorded as too big for the contrast pass are fixed, so the plan says so. Also corrects a claim I made and did not check: the chrome does not stay dark under the light ramp -- that screenshot was taken before the theme propagated, which is the third time in two passes a picture read at the wrong moment produced a confident wrong claim. |
||
|
|
fd32ce71d2 |
docs: record phase 2, and what a parked measurement was hiding
The audit's one 'borderline ~4.1:1' pair was nine of twelve failing combinations across three ramps, 110 nodes on screen, worst 2.31:1. The other never-measured item closed on measurement and stays dropped, now for a reason with a number behind it. Two findings larger than either are recorded and deliberately not fixed: the semantic colours are fixed across ramps, and the light ramp is not a supported theme. |
||
|
|
31144e5dc7 |
docs: record phase 1, and the state a fix lands in
Three a11y findings shipped. The generalisation is the mirror of 'a finding creates the conditions for the next one': that one is about the code path a fix opens, this one about the path it sends people to. The reduced-motion guard is two lines and both bugs behind it were in the fallback it routes users into -- one of which had been wrong in every mode, including the default, since the component was written. |
||
|
|
0a0da0c19c |
docs: close plan 007 and open 008 on the last open audit
All six phases of 007 shipped. The plan moves to completed/ with a recap rather than a rewrite: its seven "where the plan was wrong" lists are seventy-nine entries and about a third of them are the audit being wrong, which is the material 008 is planned against. 008 is a11y.md, the only audit with open items and the least verified material in the repo. A grep pass closes at least five findings the coverage map still shows open, including a11y.7, which the map assigns to phase 6 and which phase 1 fixed. The triage in the plan is recorded as hypotheses for that reason. |
||
|
|
1e4a4e6f8e |
docs: record phase 6, and the shelf that repeated with no ids in common
Twelve corrections to a plan written before any of phases 1-5 existed, of which the load-bearing one is that a rule written against a mechanism does not cover what the rule is for: `home` suppresses a repeated shelf by comparing album ids, Explore's first two shelves hold different entity types and share none, and the page repeated itself anyway because a person reads artists. |
||
|
|
f5621bf7c5 |
docs: record the fourth pass, and the bugs hiding behind three findings
Each of perf.p2, H-13 and the dialog naming had a second defect behind the one named, reachable only once the first fix made the code path run. Also records two probe failures worth more than the fixes: the a11y snapshot cannot see a dialog's accessible name at all, and a scroll assertion that could not fail was hiding both a bug and a false claim. |
||
|
|
71324b561a |
feat(albums): draw the album dropdown that was already being computed
Enter on an album card fetched the album's tracks over the IPC and ran the whole split state machine (splitMode true, splitIndex measured against the real container), then render() drew the single grid because it never consulted splitMode; connectedCallback referenced renderSplitGrid only to satisfy noUnusedLocals. perf.p2 files this as dead code — it is the only route from the albums grid to track-details, since a plain click navigates to the catalog page instead. Two things it needed that the audit does not mention. The grid could not scroll: .grid-scroll-container is the markup artists-view and genres-view use, and cover-grid had the class with no rule for it, so 186984px of albums sat in a 772px box at 5000 albums, unreachable by wheel, keyboard or scrollbar — and that is the element scroll-manager saves and restores, so its scrollTop was permanently 0. And the shared context menu was labelled 'Album actions' unconditionally, which nothing could observe while a track menu was unreachable. Both halves of the split grid carry the listbox semantics the single grid gained in the ARIA pass. |
||
|
|
287b6445fa |
fix(a11y): give every wa-dialog an accessible name
Eleven dialogs passed a `label` that never reached the accessibility
tree: Web Awesome renders it into an <h2 id="title"> in the same shadow
root as the native <dialog> and never points aria-labelledby at it, so
getByRole('dialog', {name}) matched nothing and a screen reader
announced an unnamed dialog. a11y.md lists all of them under "what is
already correct".
utils/name-dialog.ts sets the IDREF, with aria-label as the fallback for
without-header (first-run-wizard), called from each host's updated().
aria-labelledby rather than aria-label because three call sites compute
their label at render time, and the heading re-renders anyway. It waits
for the dialog's own first update: wa-dialog populates its shadow root
in its own update, so a query at the host's firstUpdated names nothing.
Reaching into another library's open shadow root is deliberate and the
failure is bounded — if the structure moves, the query misses and the
dialog is as unnamed as it was.
|
||
|
|
d681a7223e |
docs: record the audio clock, and that CI is green
The e2e job passes on both engines for the first time, so the three files that describe it as red are wrong. Also records the two things that made it findable: the CI container is reproducible under Docker, and the app's own audio stack had to be the thing measured. |
||
|
|
425dd7c158 |
docs: record the third Phase 5 pass, and read the CI log
The e2e failure two sessions could not diagnose is the container's audio clock, on both engines — 48 specs pass under Chromium and 48 under WebKit, failing the same three. Nothing in last pass's dialog, focus or role work is WebKit-specific. Also records what got in the way of knowing that: gitea_ci's job-log endpoint 404s on this build while the REST API answers fine, and the WebKit step had been skipped on every red run. |
||
|
|
63d11c3f9c |
docs: record the second Phase 5 pass and what it corrected
Three of a11y.md's findings describe a build that no longer exists — one fixed by a phase that was not about it, one whose stated mechanism stopped being true when Phase 4 bundled the icons, and one that reproduces as a different shape. The generalisation is that a finding has a date as well as a magnitude and a mechanism. Also records the two bad versions of the duplicate-shelf rule that the *existing* tests caught, the eleven e2e specs that landing on Home broke and the one of them that was a real bug, and the second CI e2e failure on a commit that changed no application code. |
||
|
|
9f03b3ff94 |
ci: enforce the commit format CLAUDE.md said was enforced
CLAUDE.md has claimed since the file was written that commitlint gates the commit format in CI and that semantic-release runs off it. There was no commitlint config, no workflow running one, and nothing invoking .releaserc.yml — so the first thing every contributor and every agent reads about this repo was false in two places. scripts/commit-check.sh is the smaller honest answer: the grammar is one regex, and commitlint would mean a Node dependency tree at the root of a Go repo to run it. It is a commit-msg hook locally and a CI step over every commit in a push, and its type list is .releaserc.yml's so the check and the release rules cannot drift. The semantic-release half is recorded as configured-but-not-wired rather than implied to run. |