Repository navigation
Conversation
Comics jumped Queued -> Matched in the admin tagging status table and never showed "Looking up", though the right rail did. A regression from 7716fc5, which replaced the single Status column -- whose template rendered the row-level in_flight with a spinner -- with per-source columns. A per-source in_flight cell could never be published: the fold set it on SearchStarted and cleared it on FileFinished, and comicbox emits the whole per-file sequence before tag_many yields, which was the only moment codex published. Every publish sampled the one instant when no lookup was recorded. Liveness is now a marker on the scan (SessionState.live) rather than a residue in the event history: set from comicbox's SourceStarted (4.8.7, which unlike SearchStarted also covers the explicit-id, stored-id and series-cache fast paths) and from codex's own stored-id prepass, cleared path-matched on FileFinished so a late event for a prior comic can't wipe the current one. build_snapshot elects the in-flight row by identity and projects one live cell onto it. The row is no longer guessed from path_to_pk position. tag_many re-sorts the batch by series fingerprint (series_batching defaults on), so that position never meant processing order -- harmless while the row rendered as "Queued" anyway, but not under a spinner claiming the comic is being looked up right now. No marker means no in-flight row, which is honest between comics and is exactly what Resume re-runs. The live publish rides its own clock and its own dedupe. It must not stamp _last_publish: that would throttle out the unforced result-boundary publish carrying the terminal status and the resume descriptor. It needs a time floor because comicbox answers a search from its on-disk HTTP cache when it can, so a re-scan fires events at CPU speed with nothing pacing them. And it is wholly guarded -- comicbox's _emit forwards unguarded and SourceStarted fires before the lookup's own try, so an escaping exception would land as a FileError and paint the comic ERROR in every column. A status display must not be able to corrupt an outcome. Snapshot changes now announce themselves on their own tag-session.snapshot message instead of riding task.progress, which fires for every librarian job and cost two other fetches per frame. The announcement lives in _publish_snapshot so every write is followed by exactly one notification, after the write rather than before it. Also fixes two stale-state bugs this made reachable: deactivate_snapshot could never repair a snapshot frozen inactive by a raised scan (its guard required active), and the snapshot view now demotes a snapshot whose scan is no longer the active one, closing the hard-kill window. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_aggregate_settings_field groups every settings row without filtering on the user FK the row already carries, so the sessions buckets merge registered users with anonymous sessions. On any install with traffic that is overwhelmingly sessions sitting on defaults nobody chose, which makes every setting read as near-unanimously default and answers neither "is this default right" nor "does anyone use this option". The new per_user section counts people instead, two families per surface. The live/global buckets are one vote per user and carry "" for a setting nobody touched -- that untouched mass is the measure of whether a default is tolerated, and it is not the same as choosing the value the setting defaults to. The chosen buckets count every user who set a value anywhere, any scope, any saved view, so they overlap and never carry "": a value nothing reaches is one nobody uses. Three things the queries have to get right. The browser live row is filtered to client=API and name="", because the unique constraint is (user, client, name) and named saved settings and OPDS rows are additional rows for the same user -- counting them would let one person with three saved views outweigh two people. Reach deliberately drops both filters, since a value saved under any name is still someone using it. And a user with reader rows but no global row votes "" rather than being left out, so the histogram partitions over the whole reader population instead of quietly shrinking its own denominator. The section is always emitted, with zero counts when there are no registered users, never omitted. An absent section has to mean "this codex is too old to send it"; chronicle already had to retrofit a migration because a hardcoded zero was indistinguishable from a real one. Named per_user rather than user_settings: the admin stats tab already had a "User Settings" table rendering the unrelated sessions section. Both tables are now titled for what they actually count -- Settings by Session and Settings by User. That tab also had a live bug this makes acute: bucket labels resolve through the choices maps, none of which has an empty-string key, so "" rendered as the literal string "undefined" -- already visible under Fit To, and "" is the headline value of the new family. Unset labels are added to the tab's own local maps rather than the generated choices JSON, which make build-choices overwrites, and an unmapped key now falls back to itself rather than to undefined. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Formatting only, no behaviour change: line wrapping in the table declaration and two of the bucket-label helpers. Both this and the committed version satisfy `prettier --check`, so this is cosmetic rather than a fix -- committed to leave the tree clean rather than carrying a diff nobody meant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ComicInfo AlternateSeries / AlternateNumber already imported into the Reprint model, but there was no way to order by the alternate numbering and no way to read an alternate series through in order. Alternate series tags make durable reading lists — they survive a re-import, rename or re-tag — which is what issue #360 asks for. Add an "Alternate Number" sort. Reprint.issue is a string, so "#10" sorted before "#2"; split it into issue_number / issue_suffix columns mirroring Comic's, derived in presave and backfilled by migration. The sort scopes to the alternate series picked by the reprints filter via a FilteredRelation, the way story_arc_number scopes to the browsed arc. Comics carrying no alternate number fall back to their own issue number so a mixed listing stays readable rather than collapsing to NULL. Surface the existing "Alternate Series" sort in cover view too, and sort it through a Coalesce onto the real series sort_name so untagged comics interleave instead of clumping under an empty aggregate. Let the reader follow an alternate series as a reading order. An alternate series is identified by (series_name, volume_number, language) — the Reprint unique key minus the issue — and the arc handle is the whole group's pks, since each issue is its own Reprint row and a per-comic handle would change from book to book. Reprint rows join the TimestampUpdater re-stamp flow and the mtime probe accepts them, so an open reader still notices a re-import. Also fixes a latent error: an M2M primary sort outside table view (cover cards, OPDS) emitted an ORDER BY alias that was only annotated in table mode. Unreachable through the UI before, but surfacing the Alternate Series sort in cover view makes it a normal path. Claude-Session: https://claude.ai/code/session_01CrB4hZ7HtGE9YjV5EkXeQ3 Co-authored-by: Claude <noreply@anthropic.com>
* fix(browser): send settings in the save request body
updateSettings passed the settings as a `params` wrapper, which xior sends
as the request body `{"params": {...}}`. The endpoint validates the body's
top level keys, so every one of them was unknown, the update validated as
empty and the request saved nothing.
Nothing was actually lost: the browse page request that follows a settings
change carries the same settings as query params and persists them. The
save request itself has just been doing nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwhwRtnMZ2pLcnhF96AyWA
* feat(browser): store a remembered sort per top collection
Sorting is one global setting today, so sorting issues by added time
leaves the publisher list in added-time order too. Issue #415 asks each
top collection to keep the sort it was last browsed with.
Add SettingsBrowser.collection_order_memory, a map of top collection to
the sort it was left in. It follows table_columns, the other per top
collection map: a JSON column in DIRECT_KEYS, so loading, saving,
resetting and saved-view cloning all carry it without further wiring.
The map round-trips out of stored settings, so the validator cleans
rather than rejects: unknown collections and sorts that no longer exist
are dropped with a warning instead of failing the whole browse page.
search_score is never stored, since relevance ordering only means
anything while its search is running.
Empty for every existing row, which reads as "nothing customized yet"
and keeps today's carry-the-sort-over behavior.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwhwRtnMZ2pLcnhF96AyWA
* feat(browser): swap the remembered sort when the collection changes
Switching top collections now files the sort the old one is left in and
hands back the sort the new one was last browsed with. A collection with
nothing filed keeps whatever sort arrives with it, so nothing changes
until you sort something.
Searching does the same pair: entering a search files the sort it
displaces and clearing the search puts it back, instead of always
landing on sort_name.
Whether a payload is the user changing collections is decided before the
validators run, since they inject an orderBy of their own that would
otherwise look like a payload that already carries a sort. Payloads that
really do carry one -- settings loads, saved views, redirects -- keep it.
The memory rides the settings the browse page request already echoes, so
there is no new request to persist it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwhwRtnMZ2pLcnhF96AyWA
* feat(browser): carry the sort when the server moves the top collection
The browser store handles collection switches the user makes, but the
server makes some of its own: a url whose collection forces a different
top, or a redirect after folder view is turned off. Those bypass the
store, so the old collection's sort leaked into the new one and would
later be filed under the wrong collection, overwriting a real memory.
Apply the same file-and-restore in the settings GET when validation
rewrites the top collection, and in raise_redirect, which is the one
place every settings-mask redirect passes through.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwhwRtnMZ2pLcnhF96AyWA
* feat(user_data): back up and restore the collection sort memory
The sidecar adds columns it is missing when it opens, so a backup written
before this column restores an empty memory rather than failing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwhwRtnMZ2pLcnhF96AyWA
* test(browser): cover the collection sort memory
Backend: the field's wiring, the lenient validator, the file-and-restore
helper, and the HTTP round trips -- PATCH, the browse page query params
that actually persist the map, reset, saved views, and a url that forces
a different top collection.
Frontend: switching collections, coming back to one, a collection with
nothing filed, payloads that carry their own sort, and the sort a search
displaces and gives back.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwhwRtnMZ2pLcnhF96AyWA
* chore: drop an unused order-defaults helper, add news
_get_browser_order_defaults has no callers; set_order_by_default is the
copy that runs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwhwRtnMZ2pLcnhF96AyWA
---------
Co-authored-by: Claude <noreply@anthropic.com>
0053 and 0054 both ship in v2.3.0, so there is no reason for a migrated database to walk through them separately. 0054's collection_order_memory field moves into 0053 next to the other settingsbrowser operation, and 0053 is renamed to cover both changes. Fold the v2.4.0 news entries into v2.3.0 for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The collection order memory test helper had no annotation, so its params dict inferred a str | bool | list | dict value type and every index into it was an error. Annotate it dict[str, Any], which is what the helper already is where it feeds apply_collection_order_memory. Drop a pyright ignore the isinstance guard above it made unnecessary, and build the two dropped-entry warnings as parenthesized msg variables like validate_table_columns above already does. Pyright only objects to an implicit string concatenation that isn't parenthesized on its own, which is why the older sibling never tripped it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Merge the Alternate Number sort into the Alternate Series sort One sort key now does both halves: comics group by their alternate series (ComicInfo AlternateSeries / MetronInfo reprints) and order by the parsed AlternateNumber inside the group, falling back to the comic's own series and issue so untagged comics interleave. A correlated subquery elects one reprint per comic by natural tuple order and ORDER BY is a five-field Coalesce list reading from that election — numbers compare numerically, and the shared election ordering keeps the tuple atomic (the old sort aggregated number and suffix independently and could pair parts from different reprints). An active reprints filter narrows the election instead of using a FilteredRelation. Benchmarked at parity with a join+aggregate at 120k comics; the fallback series segment uses lower(name) so article-led titles interleave with their alternate namesakes. Collection rows sort by a matching fixed-width key in the intersection RawSQL, with an in-SQL fallback to the collection's own name. The key reads only reprint columns (a per-comic fallback under GROUP BY came back from an arbitrary row) and its printf formats are %%-escaped so the DEBUG query logger doesn't consume bound params. alternate_number is removed from the order-by choices (0053 edited in place, unreleased). New 0054 remaps stored settings in order_by, order_extra_keys and collection_order_memory with dedupe; sidecar restore gets a sort-key rename map covering the same three homes. Cover-pick subqueries for the merged key normalize to sort_name like every other M2M sort. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ignore tasks * ignore tasks * prettier --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The 0054 data migration remapped stored alternate_number sort keys, but it can only fire on a database that already applied 0053 (so 0053 never re-runs) AND holds the key — the dev database, which is being rebuilt. On a fresh database it finds zero rows, and the key never reached a release, so it is dead by construction. 0053 is the only migration again. The sidecar restore rename stays: a sidecar written by a development build outlives the database it came from, and that is now the only way the retired key can enter a fresh install. The concept is renamed to Reprints across the sort, table column, filter, metadata panel, tag editor and the reader's reading-order menu. "Alternate Series" is ComicInfo's word for one of five sources — MetronInfo contributes both Reprints and localized AlternativeNames, CoMet isVersionOf, and the Metron and ComicVine APIs their own — and it misdescribes the localized names. MetronInfo, comicbox and the ORM all already say reprints, so the UI now matches the data model end to end and the internal vocabulary stops needing translation. 0053's choices tuple is edited in place (unreleased); released 0049 keeps its historical label, since the final migrated state comes from 0053. The redundant metadata TAG_NAMES override is dropped now that the label is the capital-cased key. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
d88f4f1 split the "User Settings" table into "Settings by Session" and "Settings by User", but the spec kept its own copy of the section titles and still asserted the old name, so it failed. Rather than retype the new names into the test, the tab now declares its tables once as a SECTIONS list the template loops over, and exports the titles. The spec compares the rendered captions to that export, so a rename cannot leave a stale copy behind again. The assertion is the whole ordered caption list rather than a containment check per title. With the titles derived, containment would pass vacuously; comparing the list still catches a section that stops rendering, one rendered twice, or an empty title. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Unclip the settings button's librarian progress ring Vuetify 4.2.0 added `overflow: hidden` to `.v-btn` (fix(variant) #22992), which makes the button both the containing block and the clip box for the admin overlays inside it. The 32px progress ring does not fit the 28px compact icon button, so on xs it lost its left and bottom arcs; the safe-area padding clips it on notched phones in landscape too. Restore the pre-4.2 behavior for this one button. The scoped id selector is unlayered so it wins without `!important`, and nothing escapes that the Vuetify clip was meant to contain: `variant="plain"` hides the button overlay, the ripple container clips itself, and inside `v-toolbar-items` the button has no border radius. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Keep the filter menu's extra rows keyboard reachable Vuetify 4.2.0 moved select keyboard navigation into `useScrolling`, whose list keydown capture wraps from either end of the item rows and calls `stopImmediatePropagation()`. That pre-empts VList's own focus walk, which was what used to carry the user from the bookmark rows into the rows this menu contributes through the prepend/append slots, so "Clear All Filters", "Favorites Only" and the filter sub-menus became mouse-only. A capture listener on the overlay content runs before the list's, so stepping off either end of the bookmark rows now lands on the adjacent slot row instead of wrapping. Every other key and row is left alone and Vuetify still owns the navigation. The neighbour is found by walking the content's focusable rows rather than by naming the slot rows, so it follows the rows that are actually rendered: logged out, there is no "Favorites Only" and the step lands on the first filter sub-menu. Also cover the other 4.2 delta in this menu: `closeOnSelect()` now bails when `menuProps` carries `closeOnContentClick: false`, which leaves `onSubMenuSelected` as the only thing closing the menu after a pick. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Drop the book change drawer's conditional width The drawer is 33vw rather than Vuetify's 256px default. Vuetify 4.1 parked an inactive layout item at `translateX(-(width prop + 1)px)`, computed from the prop and not the rendered box, so a 33vw drawer stayed partly on screen when closed. 24f8d1b worked around that by applying the width only while the drawer was open, which also made closing shrink it from 33vw to 256px mid-slide. Vuetify 4.2.0 parks it at `calc(+/-100% +/- 1px)` of its own rendered box, so the width can be unconditional and the close is a plain slide. Tests pin the offscreen transform that makes this safe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The vertical pager hands `v-virtual-scroll` a list of page numbers and reverses that list for bottom-to-top reading, so page 0 becomes the last item. `scrollToIndex` takes an index into that list, but `scrollToPage` passed the page number straight through, so every jump landed on the mirrored page: opening a comic at page 0 scrolled to the last page. Look the page up in `items` rather than repeating the reversal, so this keeps following however that list is built. Top to Bottom is unaffected, where a page number is already its own index; a test pins that too. Page tracking was never wrong. The intersection observer reads the real page number off the element, so only jumping to a page was affected. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix(comicbox): adopt comicbox 5.0.0's moved and renamed APIs Installing comicbox 5.0.0 broke 27 test modules at import: MatchMode and its neighbours moved to comicbox.config.online, WriteMode became MergeMode, and ID_URL_KEY and NUMBER_TO_KEY are gone. Those cascade through the URL conf, so almost nothing ran. Identifiers no longer carry a url. Comicbox derives links from the key rather than storing a copy that could disagree with it, so codex derives its Identifier.url column the same way, through comicbox's own helpers. The bridge between the two type vocabularies -- codex names a type after the table it points at, comicbox after the thing itself -- is the IdentifierType enum's own member names, so neither side can drift. The online cache directory travelled as COMICBOX_ONLINE_CACHE_DIR, a name comicbox 5.0 no longer reads. It now rides a settings object handed to each session, which is not a name comicbox is free to change. The online settings deliberately carry no delete_keys: that set is the read side's parse-skip list, and online tagging writes what a database returns straight back to the archive. The fixtures become v3.0 documents. Two of their entries were malformed in a way 4.8.7 dropped silently and 5.0 now names in a warning: a null team voided the whole teams field, and a credit role's identifier was keyed by "key" instead of by its source. Both are repaired, which restores the coverage they were quietly costing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(identifiers): file an identifier under the type it states A comicbox 5 identifier names its type whenever that type isn't the one its position implies, so a comic can carry an id for its series or its volume among its own. Codex filed every comic-level identifier as an issue id and built the wrong url for the ones that weren't. Codex and comicbox name these types differently -- codex after the table the id points at, comicbox after the thing itself -- and the bridge is derived from the IdentifierType member names rather than written out a second time. A drift guard holds the two vocabularies together, and it found the first gap immediately: comicbox has a volume type codex had no member for. Migration 0054 adds it. The tag editor now speaks comicbox's names throughout. It used to offer codex's, glue the type onto the front of the key, and send an always empty url, while the add-by-URL endpoint beside it returned comicbox's names -- so a URL-added identifier landed on a type its own select could not display. A reprint keeps the name the file gave it, which is what stands in when comicbox parsed no series out of that name. MetronInfo's alternative names, the reason the old series sort_name fallback existed, no longer arrive as reprints at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(metadata): store the facts comicbox 5 separates out Comicbox 5 reports several things codex had no column for, and the importer's field whitelist is derived from comicbox's own schema, so every one of them was being skipped in silence. manga and reading direction are now two facts rather than ComicInfo's one compound YesAndRightToLeft value, and each gets a column and an editor field. MetronInfo's MangaVolume gets one too. A comic's web links used to ride along inside its identifiers. An identifier is now only a key -- codex derives its link from that key rather than storing a copy that could disagree -- so the links the file itself wrote down needed somewhere else to live. The primary credit flag moved onto each role, which is the point of it: the primary writer is not thereby also the primary inker. Codex stores it on the person-and-role pairing, and since credit rows are shared between comics, the flag has to be part of what identifies one. The series' other names are no longer reprints upstream, but they still mean what a reprint means -- another edition of this book -- so they keep sharing the table, flagged. The flag is what the editor consults to put each row back in the list it came from, and it splits the metadata panel's one row into the two things a reader is actually looking at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(onlinetag): spend API budget by effort, and price the run by it Comicbox 5 bounds Comic Vine's per-candidate fan-out by default and takes an effort setting to widen or narrow it, which codex had no way to reach: effort is not an OnlineSession keyword, it lives in the settings tree the session layers over. Codex now hands those settings across, so the same channel carries the cache directory and the scan's effort. The run estimate moves onto that axis too. It was keyed on match mode, which changes no request count at all -- match mode decides how a verdict is applied once the calls are already spent. The default projection roughly triples as a result. That is the estimate becoming honest about a cold search, not the run getting slower: a real scan batches by series, so one search answers for a whole series and beats the projection. Two identifier workarounds come out, now that comicbox files both cases correctly. A series' other names state that their ids name a series, so codex stops injecting that. A reprint's id is the reprinted issue's id, so it resolves to an issue page instead of to nothing. The sources strip learns that Comic Vine meters per endpoint pool rather than per account. It shows the pool with the least left, since that is the one that will stop the run, and says which window the number covers. A prompt is replayed by handing its fingerprint back to comicbox, and comicbox 5 changed what goes into one, so prompts stored by comicbox 4 can never match again. They are dropped at daemon start and nightly rather than by a migration: the tagging cache lives under the config directory, where a test run's migrations would reach real prompts. Comic Vine credential validation starts working. Simyan 3 returned a rejected key's error body verbatim, so the empty result read as success; simyan 4 raises, which the existing handler already caught. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(migrations): ask for one full re-read under comicbox 5 Comicbox 5 changes what existing files mean without changing the files: age ratings collapse to MetronInfo's seven names, credit roles canonicalize, ComicInfo's AlternateSeries reads as a story arc, a manga tag now also sets a reading direction, and a stated title stops being rebuilt from the story list. Codex re-reads a comic only when its file changed, so left alone a library would hold both readings at once -- the old one everywhere, the new one on whatever happened to be re-tagged -- and its browser facets would list Everyone beside Everyone 10+ as though they were different things. Clearing the stored stat is how the re-read is asked for: the poller reads a missing stat as "stat the file and call it modified", and the importer's prefilter skips only on a stat that matches or an unadvanced metadata mtime. It is resumable by construction, since a comic gets its stat back only once it has been imported. The import marker is deliberately left alone. It drives the "not imported" hint, and clearing it would blank that hint library-wide for the length of the re-read, which reads as data loss rather than as work in progress. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(news): record the comicbox 5 work under the unreleased 2.3.0 Folded into 2.3.0 rather than opening a 2.4.0: 2.3.0 has never been tagged, so there is no release anyone has seen for this to come after. The Dev section carries the two things an upgrading admin has to know before they see them happen -- every comic is read again on the first scan, and saved tagging prompts are discarded -- along with what the re-read will visibly change about their tags. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * add script * refactor(migrations): one migration, and no forced re-read Squashes the three comicbox 5 migrations into 0054, and drops the one that asked for a full re-read. Re-reading a library is an admin's call, not an upgrade's. The new columns can only be filled from the files, so filling them means opening every archive, and doing that unprompted on the first scan after an upgrade spends hours of someone's disk on a decision they were never asked about. Force Update Tags already does it, on whatever scope the admin picks, and the release notes now say so. The cost of not re-reading is a library that reads its tags two ways at once: age ratings and credit roles keep comicbox 4's spellings until a comic is next read, so a filter can offer both Everyone 10+ and Everyone. That is untidy rather than harmful -- age-restricted access derives from the Metron scale, which both spellings still map onto identically. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(importer): key a credit that names no role A file may credit someone without saying what they did, and comicbox passes that through as a person with an empty role list. Codex built a key with only the person in it, and a short key is padded with nulls on its way to the database. That was survivable while the padded column was the role, which is nullable. The primary flag is not, so the first role-less credit in a library aborted the whole import with an IntegrityError -- taking every other comic in the batch with it. Such a credit now keys as (person, no role, not primary), which is what it means: there is no role for the person to be the primary holder of. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * update deps * refactor(migrations): fold the comicbox 5 columns into 0053 0053 is unreleased too -- v2.2.11 ships 0052 -- so everything here lands in v2.3.0 together and a database has no reason to walk through it in two steps. Renamed to cover what it now does. This is the same fold as "Combine the two unreleased migrations into 0053", one release later. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(telemeter): count the comicbox 5 tags, and show them honestly Comicbox 5 gave codex several new facts about a library and nothing counted any of them. The stats report now says how many comics are manga, carry a manga volume or carry web links, how many credits mark a primary holder, and how many reprints are a series' other names. It also reports the Effort default, which decides how much API budget a scan spends per comic. Counts only, and every key is a name fixed in this source. manga is three scalar counts rather than a bucket for exactly that reason: the payload's shape then cannot be invented by a value read out of a comic file, and a new comicbox manga value fails the serializer-registration test instead of quietly becoming a new key. Anything outside the vocabulary is folded into unknown, so the three still partition the library. Never the volume string, never the links, never a person or a role. Two of the columns needed their own counter. The existing one asks whether a column is NULL, and manga_volume and urls are NOT NULL with empty defaults, so it would have reported the whole library for both. There is a regression test for that, and the privacy suite now seeds a comic carrying both columns -- without one, the sentinel walk could not tell a count of them from a report of them. None of these can be backfilled, since they can only be filled by reading a file, so they all read zero until an install re-reads. That is in the release notes rather than left to be discovered. Then the Admin Stats tab, which is the visible contract for what leaves an install, and which had been misreporting it. The whole Settings by User table was empty: it read stats.per_user while the API renders the payload camelCased and ships perUser, so the only telemeter addition since 2.2.11 was invisible on the one page that exists to disclose it. Order By read "sortName" and Fit To read "W", because the labels were looked up with a case conversion applied to keys the choices files already emit camelCased. And four rows that detail the row above them rendered flat, because the indent set held plural spellings that matched no payload key. Boolean buckets read the wire's "true" and "false"; they now say On and Off, the same words chronicle's dashboard uses for those numbers. The tab's test fixture is now camelCased like the wire it stands in for, which is what let all three of those pass, and a new assertion requires every populated section to render at least one row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * trim news * refactor(onlinetag): follow comicbox 5's OnlineSession rename comicbox 5 renames the session facade's mode= to match= and unattended= to prompts=, so the public API says what the CLI and the config keys have always said (ajslater/comicbox#205). Codex only ever passed mode=, so that is all that moves here, at both construction sites, plus the DeferredPrompt attribute the prompt serializer reads. The serialized prompt's own "mode" key stays: it is codex's cache format, versioned by PROMPT_VERSION, and renaming it would strand every prompt already persisted. Nothing else in the rename reaches codex. It never used unattended=, never called set_mode/set_unattended, and the only PromptResponse it builds is a plain "skip" — so the set_unattended -> set_prompts action rename, the one break that fails quietly rather than loudly, has no call site here. FakeDP carries .match now. The fake standing in for a comicbox object has to track the real attribute name or it hides exactly this kind of break. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * update deps * fix(covers): serve conditional GETs instead of caching response bodies The per-pk cover routes carried `cache_page`, which stored a copy of every cover response in the default cache. Because the page-cache key hashes the Vary header values, and these routes vary on Cookie (plus Authorization for OPDS), that meant one stored copy per user of bytes that already exist once on disk as a webp. Session-cookie rotation orphaned entries, a 100-cover page could populate 200+ of them, and a single pk could never be invalidated -- only a clear() of the whole cache, which import finish, tag writes, and Library/Group CRUD all do. Covers were collateral on every scan. Serve ETag and Last-Modified derived from the thumb's size and mtime instead. A revalidating client gets a 304 answered from the stat alone, without the image bytes being read, and a regenerated cover invalidates itself. Also: - /api/v4/covers/<source>/<pk>, the route the web client uses for every cover on a browse page, had neither Cache-Control nor Vary. It now has both; the ?ts=<mtime> cache-buster already made it safe to cache. - Comic covers are private rather than public. They are ACL-gated, so a shared proxy must not store them. Custom covers have no ACL and stay public. - Cover and reader-page views are exempt from throttling. `cache_page` used to short-circuit before DRF dispatched, so cover hits never reached the throttles; without it a browse page would spend 100 requests against a configured throttle.user rate for one user action. - BROWSER_TIMEOUT and READER_TIMEOUT are removed. No browser or reader route has ever been cache_page'd, so the MAX_ENTRIES comment was partly justifying a cache population that does not exist. Adds tests/test_cover_views.py, which had no predecessor -- the cover endpoints were previously untested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(onlinetag): let comicbox spare a large scan, and add a way back up Two things, found checking codex against the comicbox 5.0.0 release. Effort now defaults to Auto rather than Balanced. Comicbox reads any named effort as a decision to leave alone, and only while none is named is it free to drop a large unattended scan to minimal -- the protection it added precisely because balanced fans out enough Comic Vine calls to blow past the hourly cap and stretch a big library into hours. Codex named balanced on every scan, so nobody ever got that. Naming one is still available and still honored; it just isn't the default. The release also renamed the session's mode= to match= and unattended= to prompts=. Production code already followed that; one test fake had not. Separately: a group whose parents have gone missing yields a single breadcrumb, which is the current view and so no link at all, leaving a deep route with nothing to climb and no way out but the back button. It now offers the top, which is the one destination always there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(reader): close a book to a route that exists The close button's last-resort fallback read a key the browser defaults do not have, so the one branch meant to guarantee the reader stays closable would have thrown a TypeError and taken the render down with it. The defaults call that route lastRoute, which is what the router itself already falls back to. Unreachable today: the server always injects a last route, its own fallback being the admin default. It was a landmine rather than a live crash, and nothing exercised it, so a test now covers both the ordinary close and the fallback. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Stop leaking a DB connection per ASGI request, and pool the workers that hold them (#838) * fix(db): stop leaking a database connection per ASGI request Every DB-touching request orphaned an open SQLite connection until the garbage collector emitted "ResourceWarning: unclosed database". A page of covers leaked dozens, three file descriptors apiece under WAL, which is what codex/run.py's RLIMIT_NOFILE bump was papering over. Django's ASGIHandler wraps each request in an asgiref ThreadSensitiveContext, which is backed by a private one-thread executor that is shut down when the request ends. django.db.connections is thread-local, so CONN_MAX_AGE=600 kept a connection alive on a thread about to be retired: never reusable, never closed. Django documents this ("When using ASGI, persistent connections should be disabled") and closed #33497 as documentation only, so this is by design, not a bug. Take the documented configuration: DATABASES leaves CONN_MAX_AGE at Django's default of 0, and the processes whose threads actually persist opt back in for themselves. The librarian does so as the first statement of its run loop, before it spawns the workers that reuse a connection across tasks. That covers every path Django controls, but not one it doesn't: send_response runs inside handle()'s TaskGroup, and if it raises, the exception group is re-raised before either request_finished or response.close(). A streaming body is consumed there, outside the middleware chain, so an iterator that raises is never converted to a 500 -- and codex streams comic archives off a user's library, where the librarian can move a file mid-download. ConnectionClosingApplication stays as the backstop for that exit, now documented as such. Two more connections nothing was closing: the admin restore endpoint's sidecar sqlite handle, and the admin stats view's off-loop query, which runs on an event-loop executor thread no request lifecycle cleans up. Also corrects six comments estimating the reconnect cost at "~5-20 ms". It measures ~1 ms, of which the PRAGMAs everyone worried about are ~0.01 ms; ~90% is SQLite re-parsing the schema. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * perf(db): serve requests on pooled workers that keep a warm connection Django gives every ASGI request a private worker thread and retires it at the end of the request, so each DB-touching request opened a fresh SQLite connection. Measured on this codebase that is ~0.76 ms, of which ~90% is SQLite re-parsing the schema (84 tables + 266 indexes); with per-connection statement re-preparation a five-query request pays ~1.26 ms, 53% of its database time. It also means cache_size=64MB and the 256MB mmap window can never warm on the HTTP path, and that a cold cover grid opens ~100 connections at three file descriptors each. Django's answer to this is pooling, but its pool is PostgreSQL-only and a SQLite equivalent is unsafe: a handle returned with an unexhausted cursor pins a WAL read snapshot that rollback() does not release, so the next borrower reads stale rows and its first write fails "database is locked". Pool the threads instead. WorkerPool owns a fixed set of ThreadSensitiveContext objects that are never entered, pre-seeds each with its own one-thread executor, and lends one to each request. asgiref's context manager is re-entrant, so Django's inner "async with" is a no-op and the worker -- with its thread-local connection -- outlives the request. That restores the stable-thread model CONN_MAX_AGE was written for, so the pool opts this process into persistent connections and Django's own request_started / request_finished hygiene recycles them exactly as under WSGI. Nine requests that opened six connections now open one. The pool reaches into two of asgiref's class attributes, which have been stable since 3.3.2 but are not a documented contract. So it checks them at startup rather than trusting them: if thread-sensitive work no longer lands on the seeded worker, the pool logs and disables itself, and every request takes the per-request path from the previous commit. asgiref is now a direct dependency so a major bump is a conscious upgrade, and a test pins the behavior so it fails loudly rather than silently degrading. Requests that would hold a worker for a client-speed transfer bypass the pool: the file downloads, and the health check, whose whole job is to answer even when the pool is saturated. Size it with db_workers under [server]; 0 restores per-request connections. Each worker can grow its own page cache, so lower it on a memory-constrained host. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
typecheck (basedpyright), 3 warnings -> 0: - workers.py, librariand.py: build log messages before the call, since ruff's ISC003 rejects the explicit `+` form. - test_asgi_db_connections.py: mark SlowOrmApplication._query @OverRide. ty, 6 warnings -> 0. All were dead conditions; one hid a real bug: - search/sync.py: _update_search_index_clean was typed `-> None` and discarded the count from remove_stale_records, so `elif cleaned_count:` never fired and the search index summary never reported cleaned entries. Return the count (0 on the rebuild path, reported separately). - crond.py: `not self._task_times[0]` on a 2-tuple. - search/parse.py: `if match else` on a finditer result. - opds metadata/publications/manifest: `not OPDS_M2M_MODELS` guards against a 7-element module constant. complexity (radon cc), 3 C-rank functions -> A/B: - _build_browser_defaults C(11) -> B(6): extract _rename_table_columns, fold the repeated `row[x] or ""` / `bool(row[x])` lines into column tuples, and reuse _rename_sort_key for order_by. - test_new_sections_have_content -> A(5): assert list becomes a table. - test_facet_ua_gets_facet_links_no_fake_entries -> A(3): body extracted to _assert_facet_feed, three-way title check becomes one regex. Both restructured tests were mutation-checked to confirm they still fail when their invariant breaks. make fix/lint clean, 1094 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tatus, live status rail (#840) * Default Auto Update on outside Docker Installing a new codex over the running one is what a pip, pipx or uv install wants, so Auto Update now seeds on there. A container is an immutable deployment — an in-place upgrade only lives until the next restart brings the image's version back — so docker keeps seeding it off and updates by image pull. Seeding is still get_or_create, so an existing install (and an admin who turned the flag off) is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Report a source that ran and found nothing as No match Comicbox emits a terminal event only for the resolutions its matcher reaches. A search that comes back with zero candidates — routine for Metron, whose database is much smaller than Comic Vine's — ends that source's turn with a log line and no event at all, as does a search that raises and a comic with nothing to search on. The status table had no cell to render and fell back to an em-dash, which reads as "this source was never consulted": the one thing that had not happened. FileFinished now closes the comic. Every source that announced itself with SourceStarted (emitted after comicbox's first-wins skip, so a source that sat out never appears) and reported nothing settles as no_match, and an empty cell again means only what it looks like. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Move the status rail in step with the online lookup The admin drawer's rail renders the LibrarianStatus row, which an online tagging scan only touched when a comic *completed* — a minute of network per comic during which the rail sat still while the Tagging tab's table, fed by its own snapshot notification, plainly showed work happening. The live-lookup marker now names the source it is consulting in the status subtitle, so the rail gets the same heartbeat, paced by the live publish's existing one-second floor. Two supporting fixes in StatusController: - update() takes force, for a state change rather than a progress tick. Dropping one of those into the five-second coalescing window loses it instead of deferring it, since it *is* the whole update. The rate-limit path was already doing this by reaching in and backdating the status's own clock; it asks now. - The subtitle is written on every update, including an empty one. It was skipped as "nothing to write", so a phase's subtitle outlived it: a recovered rate limit kept describing the task as rate limited, and the importer's own subtitle clear never took. Also drops a verbatim duplicate of tagWriteErrorsNotified in the socket store. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Both of the previous PR's fixes hooked the event-emitting search session. Neither one fires for a re-tag, because comics codex already holds an issue id for never reach that session: the stored-id prepass fetches them by explicit id outside it, and on a re-tag that prepass is the whole run. Two consequences, both visible in the admin Tagging tab: - A merged multi-source refresh was credited to the primary source alone. Under merge-all the prepass pins every stored id and comicbox fetches and merges them all, so a comic refreshed from Metron *and* Comic Vine showed Matched in one column and an em-dash in the other — the cell that reads as "this source was never consulted". Each source whose requested id comes back in the merged record is now credited; the primary is proven by the fetch itself, which returns nothing unless its own id landed. A source the scan never pinned (first-wins, or no credentials) still reports nothing, which is what the em-dash is for. - Only the search pass opened a librarian status row, so a scan that resolved every comic from a stored id ran start to finish with an empty status rail, while the Tagging tab's table — published from the same loop — showed each lookup as it happened. The prepass now opens the row, totalled over the whole batch, advances it per comic, and the search pass adopts it instead of starting a second one, so the rail shows one continuous job whose elapsed time counts the prepass. The live marker's "looking up on <source>" subtitle lands there too, which is what it was missing to reach the rail at all. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Read stale pickled tagging-cache entries as misses, not 500s Codex 2.5.3 (comicbox 5.2.2) pickled imagehash's numpy.float64 cover scores into pending online-tag prompts. 2.5.4 (comicbox 5.3.1) ships without numpy, so every read of those prompts raised ModuleNotFoundError: the daemon's startup prune (logged under "Clearing stale online tag scan marker on startup"), /admin/tag-prompts and /admin/tag-sessions/snapshot. - ResilientFileBasedCache also treats ImportError and AttributeError on load as an unreadable entry: delete the file, report a miss. A pickle naming a module or class that no longer exists is as unloadable as a truncated one. The discard now logs a warning, so an admin whose review queue emptied after an upgrade can see why. - serialize_candidate casts score, metadata_score and cover_score to plain float, so a library's number type can't end up in the cache again. Prompts already stored with numpy scores are dropped; those comics need another online-tag pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Log discarded cache entries with their key and traceback The discard deletes the file, so the log line is the only record of what was dropped and why. Name the cache key (the file name is a hash) and attach the traceback, so an error class caught there by mistake can be traced from the log alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * NEWS for v2.5.5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* style: satisfy the unicorn and ruff rules the toolchain bump added Pre-existing on develop, where pushes do not run CI: eslint-plugin-unicorn prefer-ternary and no-immediate-mutation in three frontend files, and ruff ICN001 (`import datetime as dt`) in four Python files. Behavior unchanged; `make lint` could not reach the shell, Dockerfile and workflow checks until these passed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ci: install the lint tools in the codex-ci stage; update devenv to 0.2.9 devenv 0.2.9 makes a missing lint tool an error in CI instead of a skip, so the codex-ci stage now copies shellcheck, shfmt, hadolint, actionlint and dockerfmt from their upstream images and unpacks shellharden's latest release, as devenv's README prescribes. apt's shellcheck goes, so the image runs the same version as the dev machines. The devenv update brings the `need` guard in bin/_lib.sh, forwards CI into the compose service, and adds the devenv-lint-tools action and the native-mode install step. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * build: keep the two directory symlinks out of the sdist uv's sdist builder reads docs/NEWS/codex and docs/img, symlinks to directories, as files and fails with "Is a directory". Pre-existing on develop, where pushes do not run CI; it broke the Build Dist job. The site builds from the checkout, so the sdist does not need them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * update deps --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
`make ty` (ty 0.0.89) failed with 99 diagnostics on develop. False positives (84): every `class Meta(<Base>.Meta)` in codex/models and codex/serializers trips invalid-attribute-override (astral-sh/ty#4711). A [[tool.ty.overrides]] block turns that rule off for those two trees only, as comicbox 5.3.2 does for its schemas. It also hides the deliberate Volume.name and CustomCover.library overrides there; the pyproject comment says to mark them with ty ignores when the block goes. Real (15): - permission_classes (7): AuthMixin declared Sequence[type[BasePermission]], narrower than DRF's Sequence[_PermissionClass], so every subclass that set its own classes clashed with APIView. Using DRF's type fixes all seven, lets three views drop their copied annotation, and retires three pyright reportIncompatibleVariableOverride ignores (AuthAPIView, AuthGenericAPIView, OpenSearch1View). - input_serializer_class (4): BrowserParamsView typed it type[BrowserSettingsSerializerBase], yet choices, metadata and mtime set a superclass or sibling serializer. Widened to their common base, BrowserFilterChoicesInputSerializer, which is all init_params needs; subclasses now inherit the type, dropping three pyright ignores. - NULL_CODE (1): the mixin is typed int | str, since VuetifyCharField compares against a str; drops a pyright ignore. - content_negotiation_class (1): djangorestframework-stubs types it str | None, but DRF resolves the setting to a class, so the override is right; ignored for both checkers with that reason. - Volume.name, CustomCover.library (2): deliberate type-changing model overrides, covered by the scoped override above. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
comicbox 5.3.2 (pinned in c36f194) needs no code changes here, only accurate comments and the user-facing fixes it brings in NEWS: - Renames strip ":" and the other characters Windows and SMB shares can't store. Codex has no filename sanitizing of its own to retire. - MetronOnlineSource now sends mokkari the API token alone when one is set, so a leftover username & password never reaches mokkari, and mokkari 4.10 stops ~/.netrc replacing the Bearer token. Codex still hands comicbox all three fields, so comments that said "mokkari prefers the token" now say comicbox sends only the token. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A user behind a reverse proxy got CSRF 403s and worked around them by removing CsrfViewMiddleware. That is unsafe and doesn't even work for the API: DRF's SessionAuthentication enforces CSRF itself. Codex never set Django's CSRF_TRUSTED_ORIGINS, so a proxy that serves Codex under another scheme or host (usually one missing X-Forwarded-Proto) had no fix. [server] csrf_trusted_origins (TOML array) or CODEX_CSRF_TRUSTED_ORIGINS (comma-separated) now feeds CSRF_TRUSTED_ORIGINS. Entries lose a trailing slash and are deduped; a scheme-less entry, which could never match an Origin header, is dropped with a warning that points at the X-Forwarded-* headers first. The default is empty, so existing installs are unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ins (#979) The setting is a fallback for a proxy that can't send X-Forwarded-Proto and X-Forwarded-Host. Say so next to that advice in the codex.toml comments and the README's "CSRF 403 behind a proxy" section, so users fix the proxy first instead of reaching for the setting. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#910 pinned dynamic covers, custom covers and finish-on-last-page on and read-RTL-in-reverse off, and removed their checkboxes, but kept the columns, serializer fields, defaults, telemetry buckets and sidecar export so the toggles could come back with a revert. Nobody needs them back, so they go: - Migration 0057 drops SettingsBrowser.custom_covers/dynamic_covers and SettingsReader.finish_on_last_page/read_rtl_in_reverse. They shipped in 0038, so this is a real RemoveField migration, not a fold edit. - BROWSER_DEFAULTS/READER_DEFAULTS, the browser, reader and session serializers, the telemetry and admin stats buckets, and the user-data sidecar schema/dump/restore lose the keys. The sidecar SCHEMA_VERSION is unchanged: the store only adds columns and restore reads named columns, so a pre-removal sidecar still restores (the legacy restore fixture keeps the two keys as the regression guard). - Dead code the pin left behind goes too: the sort-name fuzzy cover match and parent_route, the custom-cover early return, the unused BrowserCoverInputSerializer, the coverSettings getter and its key lists, setReadRTLInReverse, and the parked browser-settings-covers component. BrowserCoverInputSerializerBase folds into BrowserSettingsSerializerBase, and get_reader_defaults now iterates READER_DEFAULTS. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Give each pytest process its own config dir Every test run shared ./config: the developer's real codex.toml, secret keys and, above all, the file-based Django caches that cachalot fills. A -p plugin now points CODEX_CONFIG_DIR at a per-process directory under the run's tmp root before pytest-django imports the settings, so serial runs, parallel worktrees and (next) xdist workers never share a cache. An explicit CODEX_CONFIG_DIR still wins in a serial run. With that isolation the SerializeMixin file lock on BaseTestImporter only serialized importer classes across processes, so it goes. --durations=25 --durations-min=1.0 makes slow tests visible in every run's output. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Mock only the abort event's wait in second-look tests The delete-backstop and custom-cover cleanup tests patched threading.Event.wait for every Event in the process. Thread.start() waits on an Event too, and the first multiprocessing.Queue put in a process starts the queue's feeder thread, so a stray wait() call landed on the mock whenever that test was the first in its process to put on LIBRARIAN_QUEUE. TestSecondLookBeforeDeleting then failed its assert_called_once_with(5), alone or on a fresh xdist worker, and the janitor test's assert_called() could pass without the second look ever waiting. Both now patch wait on the abort event the importer or janitor is built with, which is the wait the code under test calls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Run the Python suite in parallel with pytest-xdist --numprocesses=auto --dist=loadscope spreads test classes across every core and keeps each class on one worker, so setUpTestData runs once per class. Each worker gets its own in-memory test database and, from the previous commit's plugin, its own config dir and file caches. --no-loadscope-reorder keeps pytest-django's TestCase-before- TransactionTestCase order on every worker. loadscope's default sorts scopes by size, which put BookmarkDedupeMigrationTests mid-run; its flush then stripped the migration-seeded rows (ComicboxTaggingDefaults, AgeRatingMetron) and left the comic sequence at 14 for the TestCases that followed on that worker: 13 failures. Locally on 12 cores: 239 s serial -> 75 s; -n 4 (the CI runner's vCPUs) 87 s. Same 1668 passed, 1 xfailed and the same coverage TOTAL as the serial run. -n0 runs serially. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Stop vitest linting and rewriting its snapshot vite-plugin-checker ran a full eslint_d pass inside every vitest run. The Lint job already lints the frontend, so the checker now loads only outside the test environment. reader-nav-button's only assertion on its initial markup was toMatchSnapshot() against a gitignored file that --update rewrote on every run, so it could never fail. It now asserts the link's tag, href, title, disabled state and classes. --update leaves test and test:ci; test:update regenerates snapshots on purpose. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * update devevnv --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Move the raw ciphertext read out of check_credentials into _stored_credentials, taking check_credentials from C to A (5). 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.
csrf_trusted_originssetting for CSRF 403s behind a reverse proxy.review list and scan status with "No module named 'numpy'". They are
dropped, so those comics need tagging again.
SMB shares can't store.
~/.netrclogin.