Conversation
Tagging a comic with rename enabled produced a failed import for the
pre-rename path, and the tags that had just been written never imported
at all.
The tag write and the rename land in one watch batch: a modify naming
the old path, plus a delete+add that inode matching pairs into a move.
Move detection never looked at the modify, and the task builder pruned
modified paths only against move destinations, so the task carried
files_moved={old: new} alongside files_modified={old}. The importer
applies moves before reading, so the read opened a path that no longer
existed.
Remap modified paths through the move map rather than dropping
destinations. Sources become their destination (the write-then-rename
every external tagger performs, codex's own included), and destinations
survive, which the poller emits deliberately for a move whose stats
also changed.
Stop the move phase from refreshing Comic.stat. The stored stat means
"the file as of its last tag import", so refreshing it on a move erased
the only evidence the read phase had that the renamed file's contents
had changed too -- the tags were lost rather than deferred. A pure
rename leaves inode, mtime and size alone, so the preserved stat still
matches disk.
Also stop recording failed imports for files that vanished mid-import:
the row was queued before presave() stat'd the path, so the OSError
meant to drop it did not. Key the failed-import map by str so its
membership test against db paths can match.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Tag Online dialog's Search and By ID tabs become one pane. Ids are
pinned per source: a pinned source is fetched by that issue id while the
unpinned ones search, in a single comicbox lookup, so merge_all_sources
merges across both. The submit button reads Search, Tag by ID & Search,
or Tag by ID accordingly, and entering an id selects its source.
Ids now ride on tag-sessions/start as {source: token}. That retires the
parallel POST /admin/tag-by-id path entirely -- AdminTagByIdView,
OnlineTagByIdTask, TagByIdRequestSerializer -- so tagging by id gains
session status, resume, and the write pipeline the scan already had.
Also drops dry_run, which the start view accepted and never read.
run_session skips the DB stored-id prepass when ids are pinned: the
prepass pops the comic out of comic_paths, which would leave the
unpinned sources nothing to search.
Needs comicbox 4.8.0 for OnlineSession(ids=...). That release is not on
PyPI yet, so the pin here is still ~=4.7.1 -- bump it after publishing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v2.2.4 (0dd3e51) removed the custom URL fields for both online sources. Metron's was correctly retired — mokkari hardcodes METRON_URL and comicbox warns the url is a no-op — but Comic Vine's worked: comicbox passes it through OnlineCredentials.comicvine_url to OnlineSourceCredentials.url and on to simyan's base_url, which is what lets tagging run against a Comic Vine proxy or mirror. Restores the Comic Vine half only, no comicbox change needed: - Model field + migration 0052. A plain URLField rather than an EncryptedCharField, because unlike the API keys it is not a secret and must read back for the admin form's placeholder. - Admin serializer (read+write), validate request serializer, and the validate view's _CREDENTIAL_FIELDS. - All three librarian consumers: the scan session's OnlineCredentials, the explicit-id auth mapping, and the credential validator's simyan base_url, so Test checks the endpoint the scan will actually use. - Backup/restore sidecar column, serializer, and restore allowlist. _reconcile_columns retrofits existing sidecar files; a legacy metron_url column in an older backup stays silently ignored. - Telemetry reports only bool(comicvine_url) — never the value. - The admin Tagging tab's Comic Vine panel gets the field back, its save button reverts to "Save Comic Vine Credentials" now that a URL-only save is possible again, and Clear removes both. A URL alone is not a credential: it must not satisfy the session manager's configured-sources gate, enable the source checkbox, or pass validation without a key. Tests pin all three. URLs dropped by 0051 are unrecoverable; admins re-enter them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three items `make complexity` flagged, all pure refactors. AdminOnlineTagStartView.post (CC 11 -> C): three near-identical "request value or stored default" blocks collapse into a _FLAG_DEFAULT_FIELDS mapping plus a _resolve_flags helper. test_telemeter_privacy._vocabularies (CC 12 -> C): split by vocabulary source into _choice_vocabularies, _identifier_bucket_vocabulary and _tagging_vocabularies, with the literal and OIDC sets hoisted to module constants. The same string set is checked; the walk is not widened. test_onlinetag_session_manager (MI 17.37 -> B): 916 lines in one module, every function already rank A. Split along its seams into the scan pass, prompt resolution and credentials, with the doubles and the comic factory moved to tests/onlinetag_session_fakes.py. All 31 tests come across unchanged; the move dedupes the BulkTagWriteTask filter into write_tasks() and the prompt dict into a _prompt() factory. test_onlinetag_tag_pass now imports the fakes from their new home. make complexity, lint, ty clean; 815 pytest + 371 vitest pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A move whose destination path already belonged to another comic row violated the (library, path) unique constraint inside bulk_update. The IntegrityError aborted the whole import before the delete phase could clear the stale row, so the poller rebuilt the same task and crashed on every subsequent scan. Mirror the folder guard for comics and custom covers: drop moves onto paths an existing row holds, and moves that two sources in one batch claim, which files_moved can express because it is a plain dict rather than a bidict. Skipping converges. The destination row's stat refreshes, the next scan sees two rows sharing an inode and suppresses the bogus move, and the stale source falls through to the delete phase. Also contain any move phase failure in all three move steps so a bad batch degrades to a skipped phase the next scan reconciles instead of aborting the import. This covers the same class of crash in the folder step, where converting dirs_moved to a bidict raises on the duplicate destinations the watcher can emit. Fixes #807 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The self-update was not flaky, it was inert. Nothing ever asked for an automatic update. update_latest_version() had an `update` hook that queues JanitorCodexUpdateTask and no caller passed it, and the update task was not in the nightly fan-out, so the Auto Update flag promised a daily upgrade that could never happen. The nightly check now forces a fetch and chains into the update when the flag is on. The flag gate moves to that scheduling point, so the admin Jobs tab button updates whether or not the flag is set -- it used to silently no-op, since the flag defaults off and the button sends force=False. The installer ran `sys.executable -m pip install --upgrade codex` with no timeout, no captured output, and a blanket except that swallowed every failure while still logging "updated to the same version". uv and pipx environments have no pip and the docker image uninstalls it, so those installs always failed invisibly. Now: pip if importable, else `uv pip install --python <sys.executable>`, else an ERROR naming both and pointing docker at a new image. Failures log the installer's own stderr, a hung installer times out instead of wedging the scribe queue behind it, and the restart only fires when the install really happened. Restarting exec'd __file__, which relied on run.py's executable bit and its `#!/usr/bin/env python3` shebang resolving to a python that has codex installed. Under macOS, pipx, uv, systemd and launchd that is frequently a different interpreter and the server never came back. Exec `sys.executable -m codex.run` instead. _is_outdated() called Version() on unvalidated strings, raising on a fresh install's empty cache and on a source checkout's "test" version. Version comparison is now codex.version.is_outdated(), total by construction, shared by the janitor and the version view. The browser could not announce anything: semverGreaterThan(a > b) passed one boolean into a two-argument function so `outdated` was always false, and the comparison read 1.0.9 as newer than 1.1.0. The server ships `outdated` and `docker` in the version payload and the javascript comparison is gone. The footer renders a codex orange "upgrade to codex vX.Y.Z" link to the Update Codex job, or to the image repo in docker, where codex cannot install over itself. The Jobs tab grows section anchors, names the version the job would install, and explains the docker case. While the cache is empty every /api/v4/version request queued a fetch task, each on its own thread with a 5 second PyPI call. Add a process wide in-flight lock and a cooldown after a failed fetch. Tests cover the version comparison, the payload, installer selection and failure paths, the flag gate and fetch guards, the nightly wiring, and both frontend surfaces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nginx hides the upstream Server header by default (along with Date,
X-Pad and X-Accel-*) and substitutes its own, so codex/<version> never
reaches clients behind a reverse proxy. Granian passes the header set by
CodexMiddleware through untouched; nginx is the only clobberer.
Add proxy_pass_header Server to the README's example location, plus a
subsection covering the curl verification, why server_tokens off is not
a substitute, and the array-directive inheritance trap (a location that
declares any proxy_pass_header drops the one inherited from server{}).
Set the same directive in the test-proxy harness so the documented
config is the one actually exercised.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…#935) The reader debounces bookmark writes by a second. The pending write read `books.current` when its timer fired, and a new write replaced a pending one. Paging to a book's last page and moving straight on to the next book therefore either dropped that book's final page and its `finished` flag, or wrote them to the next book, depending on whether the timer fired before the next book's first write. The debounce now captures the book with the page when the write is scheduled, and a pending write for a different book lands immediately instead of being replaced. Page turns within one book still coalesce into a single write. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…936) Every reader settings action awaits the server and then writes to whatever book or series scope is open at that moment. Changing a setting and turning to the next book before the save returned wrote the old book's settings onto the new one. A slow settings load for a book the reader had already left could overwrite the settings of the book now open, including its series settings. updateComicSettings and clearComicSettings now apply only if the book they saved is still current. loadAllSettings drops a response for a book no longer open, since loadBooks starts a fresh load for every new book. updateIntermediateSettings and clearIntermediateSettings apply only if the same series, folder or arc scope is still open. Clearing the merged settings cache stays unconditional, because it is keyed by book. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
loadGlobalSettings started its request in a fire-and-forget async IIFE and resolved at once. reloadOnDefaultsChange awaits it when the admin defaults change under an open reader, so it reported the reload done before the new settings had arrived. The action now awaits its own request. Callers that don't await it, the reader view and loadBooks, are unaffected. Errors are still logged and never reject. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…uests (#938) _filterSettings built a pruned copy of `filters` and then returned the original: Array#filter only reads its callback's result as a keep/drop flag, so the pruned object was thrown away. Mark Read, Force Update and metadata loads sent every empty filter list along with the ones in use. It now builds the result directly, so `filters` carries only the filters in use, and is still dropped when none are. The server reads every filter with a default, and the filter serializer's fields are optional with no default, so a missing filter already meant the same as an empty one. Results don't change; the request gets smaller. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The dialog re-validates the form on every edit and set the submit button
from whichever async form.validate() settled last. It also validated
before the inputs re-rendered, so the check read each field's value from
before the edit: a group name entered in one input event was checked as
blank ("Name is required") and "Add Group" stayed disabled on a valid
form.
Wait a tick so the fields settle, and apply only the newest validation.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Anonymous browsing with Non-Users on logged "Authentication credentials were not provided." on every load: the SPA sends the browser timezone for kiosk sessions on purpose (app.vue's nonUsers watcher), but the v4 cutover folded the v1.7 PUT /auth/timezone/ into PATCH /auth/profile, whose IsAuthenticated policy 403s anonymous callers. The client was right; the server regressed. CodexMiddleware activates the session timezone on every request, anonymous or not, so the value is used. Restore a dedicated PUT /api/v4/auth/timezone on the default IsAuthenticatedOrEnabledNonUsers policy (what v1.7 used), point the client at it, and drop timezone from the profile PATCH so there is one write path. Relaxing ProfileView instead would have opened its GET and username/email edits to anonymous callers. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The admin store's generic table engine (loadTable, loadTables, the row writes and pending-delete revival) backs every admin tab, but the only store specs covered OIDC and the site defaults. This adds a characterization spec for it, with every TABLES request function mocked and each table's real stateField kept. It covers both response shapes loadTable accepts (a bare array and a cursor page) and what happens to anything else, the 5 s sticky cache per table, AgeRatingMetron never expiring, force bypassing the cache, and loadTables filling each table's own field and resolving only when all have landed. It also pins which tables reload after a write: create, update and delete force-reload only their own table, and a revive force-reloads PendingDelete and Library, with nothing reloaded when the request fails. The librarian-status diff keeps unchanged rows (and their watchers) untouched, and non-staff visitors get no request and no state change. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
_validateTopCollection has needed five fixes and no test reached it: the existing pipeline specs enter through _validateSearch and never take its top-collection branches. This spec drives the live store through each of them, with every case named for the route it runs on. It covers the root guard and its folders/arcs exception, the switches that keep the route, the in-place redirect for a parent top collection (whose numeric page is what keeps _validateAndSaveSettings' "already there" check from dropping it), the root redirect for a child, the moves to and from folders and arcs, and the case where no browser route exists yet. getTopCollection's walk up the hierarchy is pinned against settings.show, and lowestShownCollection against both browse and non-browse top collections. No production code changes. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The browser store kept a top-level `zeroPad` that nothing reads; the card subtitle reads the server's `page.zeroPad`. `deleteSavedSettings` has had no caller since the saved-settings UI landed without a delete button; the API function it wrapped stays for when one is added. The misc settings drawer mapped `twentyFourHourTimeTitle`, which its template never uses. No behavior change. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…om to top (#943) * refactor(reader): drop reader store bindings no component uses Eight reader components mapped store members they never read. Two of them pointed at nothing: the pager mapped an `isBookVertical` action the store doesn't have, and the books window defined `bookChangePrev` and `bookChangeNext` computeds that called an unmapped `bookChangeShow` and would have thrown if anything had rendered them. The rest were real members left behind by earlier moves: the top toolbar's four navigation actions, the download panel's `routeParams`, `isBTT` in the book-change drawer and nav button, and the horizontal pager's `nextBook` and `setBookChangeFlag`. The vertical pager only reads `reactWithScroll`, so it maps it with `mapState` instead of `mapWritableState`. No behavior change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(reader): make the "b" shortcut set bottom to top reading The reader settings drawer's "b" key wrote the reading direction "bbt", which isn't a direction the server accepts, so the shortcut never switched a book to bottom to top. It now writes "btt". A new spec presses each direction and fit key and checks the values sent against the reader choices the server generates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…944) * fix(reader): stop a redundant settings request and a stale zoom fit Opening a book asked the server for the global reader settings twice. loadBooks fetched them on its own behind a settingsLoaded flag that nothing has set since the flag's setter was removed, and then loadAllSettings fetched the global, series and comic settings for the same book. Every book open made the extra request. loadBooks now relies on loadAllSettings for the global settings, and the dead flag is gone. The reader view still loads the global settings on its own before a book arrives. loadAllSettings now also marks the global settings as loaded when they arrive. The dropped request used to do that, and without it a site default seed landing after the book's settings could overwrite the stored global settings until the reader view's own load arrived. Zooming a page with a double-click did not switch the page to its original-size fit. The fit class is cached with each book's settings and depends on the zoom level, but the zoom was the one reader setting that did not drop that cache. A zoomed page kept its screen, width or height fit and was magnified from the fitted size, off centre and larger or smaller than the scroll area sized for it. The zoom now drops the cache like every other settings change, so zooming in shows the original size and resetting the zoom restores the chosen fit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(reader): pin how book settings are masked and cached Cover getBookSettings: the book over the series over the global scope, empty values never overriding a lower scope while false and 0 do, two pages forced off in vertical reading, the direction flags for all four directions, the fit class when zoomed, the per-book cache, and every action that must drop that cache, zoom included. Also pin that opening a book makes one settings request that includes the global scope, and that the settings it loads stop a later site default seed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
) The admin store's folder picker state used the key `root`, but the server sends `rootFolder` and the picker reads `rootFolder`. Clearing the Library path field reset the picker to `{ root, folders }`, so the root folder read as empty until the follow-up folder load replaced the whole object. The state and `clearFolders` now use `rootFolder`. The libraries table also mapped a `clearErrors` action the admin store doesn't have; that lives in the common store. It was never called, and would have thrown if it had been. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ndles (#946) The socket client lazy-imports the admin store so non-admins never download it, but the metadata dialog's controls imported the online-tag launcher statically, and the launcher imports the admin store. That put the store in every browser and reader page load. The controls now load the launcher with defineAsyncComponent, the same way the metadata dialog already loads the tag edit panel. The launcher only renders for admins, so only they fetch it. Checked in the build manifest: the reader entry no longer reaches any admin chunk, and the browser entry no longer reaches the admin store. The browser still reaches the admin API client, which the card menu's custom cover buttons import directly. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…948) * test(admin): cover the table engine The admin store's generic table engine (loadTable, loadTables, the row writes and pending-delete revival) backs every admin tab, but the only store specs covered OIDC and the site defaults. This adds a characterization spec for it, with every TABLES request function mocked and each table's real stateField kept. It covers both response shapes loadTable accepts (a bare array and a cursor page) and what happens to anything else, the 5 s sticky cache per table, AgeRatingMetron never expiring, force bypassing the cache, and loadTables filling each table's own field and resolving only when all have landed. It also pins which tables reload after a write: create, update and delete force-reload only their own table, and a revive force-reloads PendingDelete and Library, with nothing reloaded when the request fails. The librarian-status diff keeps unchanged rows (and their watchers) untouched, and non-staff visitors get no request and no state change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(admin): share one sticky-cache test across the admin loads The admin store's table loader and its six settings loaders each repeated the same five-line check of a timestamp against the cache window. They now call one isFresh helper, so the window's edges (a never-loaded stamp is stale, and so is data exactly one window old) are defined once. No behavior change. A flat spec pins the helper. The table engine spec and the OIDC and site defaults specs exercise three of the seven gates through the store; the tagging defaults, tag-write errors, email and throttle gates are the same one-line call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…950) The browser card menu's Upload Cover and Remove Cover buttons imported uploadCustomCover/removeCustomCover from @/api/v4/admin statically, so the browser entry pulled the whole admin API chunk (~4.8 kB) on first load for every user. Both buttons are admin-only (isUserAdmin gate, AdminAPIView on the server), so load the client with a dynamic import inside their handlers instead. The menu stays synchronous; the chunk is fetched only when an admin actually uploads or removes a cover. Adds unit tests for both buttons' admin flow and non-admin hiding. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
loadTable didn't check the order its responses came back in, and it stamped each table when the response arrived. If a tab mounted with an empty or expired cache, its read could still be in flight when the admin saved a row. The save's forced reload landed the new rows, then the older read landed late and put the pre-save rows back. It also restamped the table, so every unforced read for the next five seconds was served those stale rows from cache. A WebSocket-forced reload overlapping a slower unforced read did the same thing. Every loadTable call now takes a request number. Rows land only if no later request for the same table has landed first. A later request that fails doesn't block an earlier one. The table is stamped with the time its request went out, because the rows are only as fresh as that moment. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
FIT_TO_CLASSES mapped "O" to "Original", so the Original Size setting at the default zoom produced fitToOriginal / fitToOriginalTwo / fitToOriginalVertical while page-img.vue and pdf-doc.vue only style fitToOrig*. The zoomed-in path already used "Orig". Map "O" to "Orig" and have the zoom path read the same entry so the two can't drift. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
None of pdf-doc.vue's four canvas rules had matched anything since the Vuetify 3 port (Dec 2022). That port moved them from an unscoped block into top-level :deep() selectors, which compile to `[data-v-x] .vue-pdf-embed.X > div > canvas` and need a scoped ancestor. The embed is the component's root, so it has none. vue-pdf-embed 2.x also nests the canvas one div deeper than `> div > canvas`. With the selectors fixed (`.pdfDoc.X :deep(canvas)`), each rule was checked in a browser with Force vector at DPR 2: - Fit to Height/Screen two-page `width: inherit` and Fit to Width two-page `height: inherit` did nothing. vue-pdf-embed already gives the canvas an aspect-correct inline size. Deleted. - Original two-page `width/height: inherit` resolved to auto, so each canvas showed at its bitmap size, 2x on HiDPI (1023x1581 instead of 512x790). Zoom uses these classes too. Deleted. - Fit to Screen `object-fit: contain` did nothing, because the box already matched the bitmap. Kept, and paired with max-width 100vw/50vw as page-img.vue does for images. Wide pages now fit the screen instead of overflowing it: a spread plus a page in two-page mode at 1024px went from a 1491px scroll width to 1024px, and a wide page on a 390px phone from 1092px to 390px. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…errors (#952) loadTagWriteErrors had the race loadTable had before #949. The Tagging tab and the settings button read the list unforced when they mount, and the WebSocket forces a reload whenever it changes. If a slow mount read landed after the forced reload, it put the older list back and stamped it when it arrived, so every unforced read for the next 5 s was served the stale list. clearTagWriteErrors wrote [] directly, so a read that went out before the clear could put the cleared errors back the same way. Both now use loadTable's request counter. #949's landing check moves into a claimLanding(key, request) helper that all three share, keyed like timestamps. The tag-write reads take a number when they go out, land only if no later request has landed, and stamp the time they went out. The clear takes a number when its DELETE goes out, so an older read can't undo it, and a read that went out after it and already landed isn't wiped by it. The singleton settings loads (tagging defaults, email, OIDC, site defaults, throttling) are unchanged. Nothing forces them, and their only writer is the same tab's own Save. That can't realistically fire before the tab's mount read comes back. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* chore: update devenv - Retire the dead .circleci ignore lines (devenv's new retirement lists). - eslint base config: presets apply again and Markdown code blocks are linted, which reformats the compose YAML example in docs/DOCKER.md. - pyproject template: T201 exemption for bin/release_info.py. - eslint-plugin-package-json bump. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * ci: compose CI from devenv's building blocks ci.yml is now a caller of devenv's CI building blocks (copied in by `make update-devenv`) plus codex's own image jobs: - ci: devenv-check.yml with ci-target codex-ci and the same four checks. Its gate runs the Release Preflight and, on a main push, reuses the python-dist of an earlier run that passed on the same git tree (keyed by tree hash, replacing the "last merged PR" lookup). One image job builds codex-ci and pushes it by digest; each check pulls it instead of building and loading its own. The required check becomes "CI / Lint, Test & Build Dist". - build, deploy, deploy-hub: unchanged in substance, but they read deploy/version/final from ci's outputs instead of testing the event or grepping pyproject.toml. deploy publishes to PyPI through devenv-pypi (uv publish --check-url) after the manifest. deploy-hub now skips at the job level for alphas. - release: devenv-release.yml (tag, GitHub Release, merge-back), after deploy and deploy-hub. Removes .github/actions/ci-container (now devenv-ci-container) and the release-engine tests, which devenv owns now; codex keeps its golden NEWS.md tests. tests/test_ci_workflow.py pins how codex wires the blocks. CLAUDE.md describes the new graph and the current Dockerfile stages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * update deps * fix(make): build the choices JSON before lint and fix Since the ESLint preset fix (#931, #932), import-x/no-unresolved resolves the frontend's `@/choices/*.json` imports, which bin/build-choices.sh generates and git ignores. A checkout that never built them, such as CI's fresh codex-ci container, fails `make lint` and `make fix` with 19 unresolved-import errors. Give lint and fix the same build-choices prerequisite test-frontend and build already have. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…955) * feat(browser): mark finished items with a check cap, not a thick bar Issue 856 feedback: the 6px grey finished bar was hard to tell from the 3px reading bar when scanning a grid, and only really read when two cards sat side by side. The dimmed caption was a second cue that made read titles harder to read. Every bar is now 3px. A finished comic or all-read collection ends its bar in a 10px grey check circle, the same grey as the fill. The bar runs under the cap to its centre so the two join with no gap. The check is stroked in the page background instead of cut out, because a cut-out would show the track through it. The fill is still the real bookmark position, so a comic marked read but never opened is an empty track and its cap. Read titles and subtitles keep their normal colour. The bar is now one component, ReadStateBar, used by both the browser card and the metadata dialog. They had duplicated its CSS, and the cap would have been a third copy to keep in sync. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(metadata): draw the read state under the cover, not behind it The read state bar in the metadata dialog has been invisible since the read state landed in v2.4.0. It sat 15px up over the cover's bottom edge, where the old v-progress-linear used to be. That component is positioned, so it painted over the cover. The plain div that replaced it is not, and the absolutely positioned cover image painted over the bar. It now sits 1px under the cover at every breakpoint, as on the browser card, which moves the dialog's buttons down about 16px. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
with, replaces Default View, and can update existing anonymous sessions.