Skip to content

v2.0.0 alpha#779

Open
ajslater wants to merge 248 commits into
developfrom
pre-release
Open

v2.0.0 alpha#779
ajslater wants to merge 248 commits into
developfrom
pre-release

Conversation

@ajslater
Copy link
Copy Markdown
Owner

@ajslater ajslater commented Jun 6, 2026

No description provided.

ajslater and others added 30 commits May 22, 2026 21:42
Bump comicbox to 4.0.0a0 and update import paths to match the
reorganized layout:

- comicbox.schemas.comicbox.*KEY*     -> comicbox.formats.comicbox.schema
- comicbox.schemas.comicbox.yaml      -> comicbox.formats.comicbox.schema.yaml
- comicbox.schemas.comicinfo          -> comicbox.formats.comic_info.schema
- comicbox.fields.fields.IssueField   -> comicbox.formats.base.fields.fields
- comicbox.fields.number_fields.PAGE_COUNT_KEY -> comicbox.formats.comicbox.schema
- ID_KEY_KEY / ID_URL_KEY moved to comicbox.identifiers

Adds a local editable source override under [tool.uv.sources] so the
branch resolves against ../comicbox while 4.0.0a0 is unpublished.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tag Edit Dialog:
- Full metadata editing: groups, credits, technical fields, tags,
  universes, identifiers, and read-only info (path, dates, size)
- Format-aware field disabling based on selected write formats
- Per-field clear controls for explicit field clearing
- Use comicbox "update" mode (removed write mode selector)
- Dismiss dialog on save since metadata is stale until reimport

Admin Tagging Tab:
- Compound h/m/s timeout input, collapsible credential panels
- Removed Comicbox/CoMet/ComicBookInfo format options
- Credential status shown green, clear credentials with confirm
- Disabled online sources without credentials
- Fixed match mode values to match comicbox (strict/normal/fast)
- Moved tab after Flags, before Jobs

Online Tagging:
- Prompt button in browser toolbar with pulse animation
- Prompt popup mounted in browser view (was admin-only)
- Session state persisted to DB for cross-process/refresh survival
- Prompts appear as they're deferred (not just at end of lookup)
- Rate limit status shown in librarian status panel
- Comic count, rate limit warnings, and time estimates in launcher
- Credential-aware source disabling with admin link

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ations

Edit Panel:
- Change tracking: only modified fields are written, Save button disabled
  until actual changes are made, changed fields highlighted in orange
- Per-field clear controls with tooltips (ClearFieldIcon component)
  for explicit field deletion vs leaving untouched
- Removed write mode selector, always uses comicbox "update" mode
- Confirmation dialog before write with conversion count and
  delete_original checkbox for CBR/CB7→CBZ conversion
- Format-filtered select values: age ratings, original formats,
  reading directions limited to selected format capabilities
- Age ratings show ComicInfo equivalents in parentheses
- Stories label adapts: "Stories" / "Title" / "Stories / Title"
- Added volume_count field, identifier URL parser
- Per-role credit change highlighting aware of MetronInfo's
  per-person credit structure
- Moved Tag Online and Edit Tags buttons to metadata controls row
- At least one metadata format must be selected

Admin Tagging Tab:
- delete_original option with help text
- Collapsible credential panels
- Removed write mode selector

Backend:
- Preflight endpoint for conversion stats before tag write
- Identifier URL parser endpoint using comicbox IDENTIFIER_PARTS_MAP
- delete_original wired through task → tag writer → comicbox config
- Format field support updated for MetronInfo (imprint, volume, etc.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Critical fixes:
- Fix CamelCase parser mangling dict keys in tag write patches
  (person names, role names, arc names converted to snake_case).
  Patch now sent as JSON string, decoded server-side.
- Fix online tagging deadlock: session blocked thread waiting for
  prompt resolution tasks that queued behind it. Now polls the
  thread queue during prompt wait.
- Fix double import after tag write: only reimport comics in
  libraries without filesystem event watching.

Online tagging improvements:
- Flush auto-matched comic writes during rate limit pauses
  instead of waiting for entire session to complete.
- Conversion warning with delete_original checkbox in launcher
- Remove prompt status from librarian status bar (redundant
  with Match Review button)
- Dismiss metadata dialog when online tagging starts
- Rename prompt UI to "Match Review"
- delete_original passed through online tag session

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… info

- Add section headers (Publishing, Description, Credits, Details,
  Tags, Universes, Identifiers, Notes) for visual hierarchy
- Move format selector inline with Save/Cancel toolbar
- Replace mixed thirdRow/inlineRow with 2-column CSS grid for Details
- Collapse read-only file info (dates, size, type, path) into
  expansion panel
- Reduce section margins from 25px to 4px (headers provide separation)
- Unify table footers: credits, universes, identifiers all use
  consistent flex row with add buttons left, Clear All right
- Move universe/identifier footer buttons outside table elements

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a new online tagging task arrives while a session is already
running, merge its comics into the active session instead of queueing
a separate hidden job. The status total rises dynamically and the
OnlineSession's series cache carries over.

- _drain_thread_queue handles BulkOnlineTagTask by merging paths
- _collect_results loops on pending_paths after each tag_many pass
- Cumulative total_comics/completed_comics for accurate progress
- RateLimited event detection: restored event-based handling (for
  comicbox fix) plus time-based fallback (>10s = rate limited)
- Removed unused RateLimited import workaround

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Design doc covering an admin upload flow for group/folder custom covers,
removal of the Library.covers_only fake-library mechanism, a one-time
migration of legacy filesystem-discovered covers into a pk-keyed uploads
dir, Volume custom-cover support, and a new admin "Custom Covers" tab.
Implementation is deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mp v2.0.0

Match-mode alignment
- ComicboxTaggingDefaults.default_match_mode now stores the comicbox
  MatchMode enum values (careful/auto/eager) instead of the legacy
  strict/normal/fast strings. Migration 0043 is updated in place since
  it has not shipped to a stable release yet.
- session_manager passes MatchMode(task.mode) directly to OnlineSession
  per the new comicbox API.
- TAGGING_CHOICES + TAGGING_DEFAULTS land in codex.choices.admin and
  flow through choices_to_json to tagging-choices.json. The Vue
  components consume the generated JSON instead of hardcoding the
  arrays in two places.

Credential scrubbing
- httpx logs every request URL at INFO, leaking ComicVine api_key.
- New scrub_secrets() in codex.settings.logging masks
  api_key/access_token/token/secret/password/auth query params and
  Authorization: Basic|Bearer headers.
- Applied at LoguruHandler.emit (the stdlib bridge) and as a sink
  filter on main + failed-login sinks. Sink filters live at module
  level so loguru's enqueue=True pickle path doesn't break.

Release prep
- Version bump to v2.0.0 in pyproject.toml + frontend/package.json
  reflects the breaking comicbox 4.x dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Split SessionState + helpers into session_state.py and the tag-pass
  driver into tag_pass_runner.py to lift session_manager MI from B to A
- Reduce TagWriter.write_tags complexity from C to B via small helpers
- Replace pyright ignores with proper match-exhaustive `case _`, cast()
  forms, and a protocol-compliant prompt-handler signature
- Whitelist string-cast imports (Literal, Mode) in vulture_ignorelist

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renames user-visible "metadata" to "tags" in browser sort/column labels,
the comic dialog close button, the reader keyboard shortcut help, and six
librarian/scribe + browser-metadata view log lines. Internal identifiers
(modules, classes, the metadata_mtime field, URL paths) are left intact;
the OPDS PSE debug log keeps "metadata" since it's spec terminology.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plan for adding a "Clear Stale Online Tagging State" janitor task and
admin Jobs button, plus an OnlineTagThread startup hook that clears
persisted session state on process restart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
commit 71889c06142546726f5f88a978cd3efa17d91844
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 13:59:10 2026 -0700

    feat: add "Clear Stale Online Tagging State" janitor task

    The online tagging pipeline persists session state to
    ComicboxTaggingDefaults (active_session_id, active_prompts) so prompt
    state survives process restarts. Until now there was no path — manual
    or scheduled — to clear those rows when a session dies uncleanly.

    - OnlineTagThread.run_start clears persisted state on every thread
      start, since in-memory session state cannot survive a process
      restart and any persisted row at startup is by definition orphan.
    - New janitor task cleanup_tagging_state (status code JTG) runs
      nightly and is exposed in the admin Jobs page as "Clear Stale
      Online Tagging State". It asks the OnlineTagThread whether the
      persisted session id is still in-memory and clears stale or
      orphan-prompt-only rows under db_write_lock.
    - LibrarianDaemon._create_threads gives ScribeThread (host of the
      janitor) a back-reference to OnlineTagThread so the liveness check
      can cross the thread boundary.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
commit 2994e3ca8f9579d465d767f415d1e5b49eca2e63
Merge: 48837f60 7ddf056
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 14:05:16 2026 -0700

    Merge branch 'codex-v2' into skip-all-prompts

commit 48837f606a84bede2f78fcd7ae68c6c223c17e30
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 13:59:55 2026 -0700

    feat: add Skip All button to online tag prompt dialog

    Skip-resolves every queued prompt in one shot so the session can
    unblock and commit auto-matched tags via the existing deferred-pass
    write. The session keeps running; new prompts streaming in from the
    active job will continue to fill the queue.

    Distinct from the existing controls:
    - per-prompt Skip resolves a single prompt
    - Abort Session tears down the session and writes nothing
    - Skip All resolves every current prompt with action="skip" and
      leaves the session running

    Backend: OnlineTagSkipAllPromptsTask routes through the daemon and
    session-manager's _drain_thread_queue to a new skip_all_prompts()
    method that snapshots state.deferred_prompts under the lock,
    preloads skip resolutions, persists once, and signals the wait
    event when the queue empties.

    Frontend: new Skip All button in the prompt dialog header, wired
    to a skipAllPrompts() store action that POSTs to
    /admin/online-tag/<session_id>/skip-all-prompts and clears the
    local prompt list.

commit 8715b66d69ce7b3fd10aad9e9ea15a25ea5fb3b1
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 13:52:04 2026 -0700

    fix: serialize OnlineTagSessionManager state mutations under one RLock

    resolve_prompt, _sync_deferred_prompts, and _wait_for_prompts all
    read-modify-write state.deferred_prompts but previously released the
    manager lock before mutating. If two of these ran on different
    threads (e.g. comicbox emits PromptDeferred on a worker thread while
    the librarian thread is processing a resolution), one update could
    clobber the other.

    Switch _lock to RLock so the lock can be held across the full
    critical section without self-deadlocking on _persist_prompts ->
    get_pending_prompts. Side effects that don't touch shared state
    (librarian_queue.put, state.event.set) are released first.
commit 6949f56a76c77371f019304660c33cb4e901b5e0
Merge: c0d91726 c348ebe
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 14:09:37 2026 -0700

    Merge branch 'codex-v2' into worktree-validate-tagging-creds

commit c0d917268cedbab78168aa67e69e88550a083175
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 14:09:26 2026 -0700

    feat: admin button to validate online tagging credentials

    Adds a "Test" button next to each source's Save under the admin
    tagging settings. Click sends current form values (falling back
    to stored credentials field-by-field) to a new
    /admin/tagging-defaults/validate endpoint, which builds short-
    lived mokkari / simyan clients with cache disabled and makes one
    tiny authenticated call per source. Per-source result lands as a
    green "Connected" chip or a red error chip — so operators learn
    about bad credentials immediately instead of mid-tagging-run.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
commit be7a607f3855715507b40118d8c822ba4aecf094
Merge: df031486 0a5e0cb
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 14:21:11 2026 -0700

    Merge branch 'codex-v2' into feature/password-reset

commit df031486ec30b159d00f4fd0070264f5a8e37ac5
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 13:42:33 2026 -0700

    test(auth): unit-test new auth-store actions

    Eight cases for updateProfile / sendResetPasswordLink / resetPassword:
    empty-diff short-circuit; success path that refreshes user state and
    calls the API with only changed fields; error path that surfaces to
    commonStore. Pattern matches the existing favorites-store tests
    (mocked API module, fresh Pinia per test).

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

commit 049af3da31c25375bc51e98ce7c7d55f3bc66ee2
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 13:41:01 2026 -0700

    style: make fix + ty fixes for password-reset feature

    - ruff/prettier formatting auto-fixes
    - @OverRide on RegisterView.post (was implicit override warning)
    - ty: setattr on Field.allow_blank to avoid the static-checker walking
      the parent Field type's narrower interface
    - minor doc tweaks introduced by the formatter

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

commit 0c891d91cb96ff9638e6d56c08d113f163289ddf
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 13:33:21 2026 -0700

    test(auth): password reset + register verification + profile coverage

    Covers 16 cases across 4 classes:

    * PasswordResetDisabledTests - default install: flags report
      emailEnabled=false; send-reset-link and reset-password return 404.

    * PasswordResetEnabledTests (override_settings + locmem) - flags
      flip; reset-link by username and by email each produce one mail
      with a signed link; unknown logins produce success + zero mail
      (enumeration-safe); happy-path reset actually changes the
      password; tampered signature 400s without touching it.

    * ProfileUpdateTests - self-service PATCH for email and username
      persists; under AUTH_REMOTE_USER the username field becomes
      read-only and the value sticks; duplicate username collisions
      surface a 400 via DB uniqueness.

    * RegisterVerificationTests - RV off creates active users with no
      outbox; RV on + email creates inactive users with one
      verification email; REGISTRATION admin flag off returns 404 from
      the register endpoint.

    CodexProfileSerializer: rest-registration hard-codes email as
    read-only on the assumption callers use the
    register-email/verify-email handshake. Codex's profile dialog edits
    email directly, so the serializer flips email back to writable
    alongside the username remote-user guard.

    USER_EDITABLE_FIELDS narrows the writable set so future fields
    default to read-only.

    Tests rely on a small _ensure_admin_flags helper to seed AdminFlag
    rows that startup normally creates (codex_init isn't invoked by the
    TestCase harness). cache.clear() in setUp keeps the
    reset_password ScopedRateThrottle scope from accumulating across
    sibling tests.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

commit c63fd0df12dbfedf9000a88d163ee72692a09828
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 13:25:19 2026 -0700

    docs(auth): document [email] config + password-reset workflow

    Adds a "Users & Access" feature bullet and a new "Email & Password
    Reset" subsection under Administration. Calls out the Gmail App
    Password requirement, the Amazon SES verified-identity requirement,
    and the from_address fallback for generic SMTP relays. Notes that
    existing users need email backfill (admin Users tab or self-service
    Profile) before they can request a reset.

    Embeds the throttle.reset_password and [email] block in the README's
    Full codex.toml Reference so the inline docs stay in sync with
    codex.toml.default.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

commit 9a51d191590afcea9ce9329b7018ec202d538090
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 13:24:13 2026 -0700

    feat(admin): expose email in admin user CRUD

    Adds email to the admin UserSerializer fields (optional, allow_blank).
    The admin user-tab gains an Email column and the create/update dialog
    gains an Email input with format validation. Admins can backfill
    emails for existing accounts so their users can use the
    password-reset flow.

    Removes the unconditional ``validated_data['email'] = ''`` from the
    create path so submitted emails are honoured; defaults to empty when
    not provided. Drops the corresponding queryset defer so list/detail
    GET returns the field.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

commit e916ac2f868b543d1375cf10903656d8a8c5e6c9
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 13:22:14 2026 -0700

    feat(auth): forgot-password dialog + reset-password confirm route

    Adds two components:

    * ResetPasswordRequestDialog: link form rendered inside login-dialog,
      gated by adminFlags.emailEnabled. Submits to
      /auth/send-reset-password-link/ and always reports the same generic
      "if that account exists, a link was sent" message so callers can't
      enumerate users.

    * ResetPasswordConfirm: standalone page at /auth/reset-password that
      parses signed user_id / timestamp / signature from the URL the
      email link landed on, takes a new password + confirm, posts to
      /auth/reset-password/. Camel-cases the user_id key for the JSON
      body (the parser snake-cases at the API boundary).

    Wires the route into vue-router with lazy import.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

commit 6dd25358a39692fdc427eba8810526196adfe50f
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 13:20:11 2026 -0700

    feat(auth): profile dialog with username + email self-service

    Adds CodexProfileSerializer that marks username read-only when
    AUTH_REMOTE_USER is on (backend belt-and-braces behind the disabled
    UI). Registered via REST_REGISTRATION["PROFILE_SERIALIZER_CLASS"].

    Adds API helpers updateProfile, sendResetPasswordLink, resetPassword
    and matching Pinia store actions. New showProfileDialog +
    showResetPasswordRequestDialog state slots; isAuthDialogOpen widens to
    include them.

    New ProfileDialog renames the auth menu item to "Profile" and groups
    username + email + collapsible password-change in a single dialog. A
    single Save button computes the diff against the current user and
    issues PATCH /auth/profile/ for changed fields + POST
    /auth/change-password/ only when the password panel is filled. The
    username field is read-only with "Managed by upstream authentication"
    helper text under remote-user mode; otherwise it carries a warning
    about OPDS / API client re-configuration.

    The original change-password-dialog stays in place for the admin
    user-tab flow (admin changing another user's password).

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

commit ad24f61af354001d4ac40bcc2c4a625a8171a91d
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 13:16:33 2026 -0700

    feat(auth): expose email_enabled + remote_user_enabled + register_verification on /auth/flags/

    AdminFlagsView merges the new REGISTER_VERIFICATION AdminFlag plus two
    settings-derived booleans into its response so the frontend has the
    full auth-related context in one request. emailEnabled gates the
    "Forgot password?" link; remoteUserEnabled gates the username field in
    the (upcoming) profile dialog.

    Admin flags tab gains a description + group entry for the new RV flag.
    Frontend auth store declares the new fields as undefined placeholders
    so reactive bindings don't read missing keys before /auth/flags/
    returns.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

commit 77e8913f49145c357c3a9aa5082d0ba88b272776
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 13:14:27 2026 -0700

    feat(auth): REGISTER_VERIFICATION admin flag + custom register view

    Adds REGISTER_VERIFICATION = "RV" to AdminFlagChoices (seeded off via
    FALSE_DEFAULTS) and migration 0044. When the flag is on AND email is
    configured, new accounts are created inactive and a verification email
    is sent; otherwise they activate immediately.

    The custom RegisterView checks both REGISTRATION (existing) and
    REGISTER_VERIFICATION at request time rather than relying on
    rest-registration's cached settings, so admin toggles via the UI take
    effect on the next request. Mounted at /api/v3/auth/register/ before
    the rest-registration include so it wins URL resolution.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

commit 400887c81f30afc0583b0a1220e0cbe2b7c34375
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 13:09:15 2026 -0700

    feat(auth): SMTP config + rest-registration reset-password wiring

    Adds [email] section to codex.toml with documented Gmail / generic SMTP
    / SES examples and from_address fallback to user. EMAIL_ENABLED is the
    boolean gate: when host or sender is missing, EMAIL_BACKEND falls back
    to dummy and rest-registration's RESET_PASSWORD_VERIFICATION_ENABLED is
    False, so reset endpoints respond 404.

    Wraps rest-registration's send-reset-password-link and reset-password
    views with ScopedRateThrottle (reset_password scope, default 5/hour);
    overrides are registered before the rest_registration include so they
    match first.

    Ships plain-text email templates for the reset and register
    verification flows. RESET_PASSWORD_VERIFICATION_URL includes
    GRANIAN_URL_PATH_PREFIX so reverse-proxy installs get correct links.

    Email is intentionally removed from USER_HIDDEN_FIELDS so users can
    self-service it through the profile dialog (Phase 5). USER_LOGIN_FIELDS
    includes both username and email so the reset link can be requested
    with either.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

commit d756b97e6f9e4c00d4bc2ce7f5f2626ca840a7db
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 13:00:41 2026 -0700

    docs: update password-reset plan with username editing + remote-user guards

    Folds in the username-editing recommendations: Profile dialog includes
    username with helper text about OPDS clients, hidden when AUTH_REMOTE_USER
    is on. Adds remote_user_enabled to the capability flags exposed via
    /auth/flags/. Tests cover username PATCH, remote-user reject, and
    uniqueness collision.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…server

When the [email] config is unset, password reset can't work, so the
email field's primary purpose is gone. Tuck it into an expansion panel
(same pattern as Change Password) so it's still settable for when an
admin configures SMTP later, without competing for visual weight with
the username and password sections.

The visible-field variant keeps its current hint. The collapsed
variant gets a hint that explains the field is dormant pending mail
server setup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
commit 883b522c0cad38b9bd7c9cd58bd47a4557617fa5
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 16:51:28 2026 -0700

    fix(migrations): renumber custom-cover migrations after merge

    codex-v2's 0044_register_verification_admin_flag collided with the
    custom-cover 0044. Shift the three custom-cover migrations to
    0045/0046/0047 and update their dependency chain.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

commit 61d9ff9f7d8b3a077c22b14505ab9c0dc2e8a109
Merge: 6334248a 4f89764
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 16:48:12 2026 -0700

    Merge branch 'codex-v2' into worktree-custom-covers

commit 6334248a88e2491eb1740e659e4f5e3b7be44757
Author: AJ Slater <aj@slater.net>
Date:   Mon May 25 16:48:06 2026 -0700

    feat: custom cover upload UI; remove covers_only library hack

    Admins can now upload, replace, and remove custom group covers from
    each card's action menu and manage them in a new "Custom Covers"
    admin tab — replacing the filesystem-watched ``config/custom-covers/``
    and in-library ``.codex-cover.{ext}`` mechanisms with a direct
    ``POST /api/v3/admin/custom-cover`` flow.

    Volumes now support custom covers (previously skipped because their
    numeric name couldn't be matched by the legacy sort-name lookup).

    The ``Library.covers_only`` flag and its many downstream branches —
    filesystem watcher, poller, snapshot, importer, janitor, telemeter,
    browser view, library admin view, library serializer, and frontend
    admin store — are removed. ``CustomCover.library`` becomes nullable;
    the synthetic covers-only ``Library`` row is deleted by a one-time
    data migration that also moves legacy on-disk covers into the new
    ``config/custom-covers/uploads/{pk}-{group_char}-{slug}.{ext}``
    layout.

    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ajslater and others added 20 commits June 5, 2026 15:28
User-facing changes since v1.12.7: tag editing & online tagging, custom
cover uploads, user-data backup/restore sidecar, email password reset,
and the settings move from codex.toml into the Admin UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- demote the Docker-move announcement to a quiet note
- drop the obsolete pymupdf AARCH64 workaround; a prebuilt wheel now ships
- fix `docker run` image (ghcr.io), libssl3, and Emergency DB Repair paths
  (codex.sqlite3 + .before-rebuild.bak)
- /api/v3/auth/login -> /api/v4/auth/login
- favorites group -> collection name-chain
- advertise Edit & Tag / Online Tagging in Features
- flag [browser]/[throttle]/[email] config as Admin-UI-managed (deprecated)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Help text under the Comic Vine API Key and Metron password fields
in the Online Source Credentials section now links to where users
obtain credentials: comicvine.gamespot.com/api and the metron.cloud
account signup page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comic.main_team was copy-pasted from main_character and incorrectly
targeted the Character model; a "main team" must reference a Team. The
importer (PROTAGONIST_FIELD_MODEL_MAP), the comic serializer
(TeamSerializer), and the browser ordering/intersection queries already
treated main_team as a Team, so only the model field was wrong.

Squash the corrective AlterField into migration 0043 (the unreleased
2.0.0 head) instead of shipping a standalone migration; main_team was
originally added in released migration 0034, which is left untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 5 JSON files imported by the metadata edit panel
(format-field-support, format-field-values, identifier-sources,
identifier-types, languages) were gitignored build artifacts that
choices_to_json.py never produced, so a clean checkout failed the
Vite build with UNLOADABLE_DEPENDENCY.

Add codex/choices/tagging.py to derive all five from their upstream
sources -- comicbox transforms/enums, pycountry, and codex's own
IdentifierType -- and wire them into choices_to_json.py so
`make build-choices` emits them.

Deriving also corrects stale data the old fixtures carried: ComicInfo
now supports country; MetronInfo gains language/original_format and
drops country, matching current comicbox.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
USER_LOGIN_FIELDS includes "email" so the password-reset form can
resolve a user by username or email, but codex runs on the stock
auth.User whose email is non-unique and blank=True. rest-registration's
E010 system check requires every login field be DB-unique, which is
impossible here: empty emails are normal, so a unique constraint would
forbid more than one account without an email.

Disable the check via the package's sanctioned
USER_LOGIN_FIELDS_UNIQUE_CHECK_ENABLED flag. Email lookup stays
best-effort by design -- the login form is username-only and the
reset-link endpoint returns generic success (no enumeration), so a
non-unique email degrades gracefully rather than being an integrity
hazard. Fixes the `make django-check` SystemCheckError.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…BasedCache

cachalot's W001 check string-matches CACHES['default']['BACKEND'] against a
hardcoded set instead of using issubclass(), so it flags our
ResilientFileBasedCache subclass even though its FileBasedCache base is
supported. cachalot was never actually disabled; this just quiets the
spurious warning. Filebased is also the correct backend for our
multi-process layout where LocMemCache would not propagate invalidation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LIBRARIAN_QUEUE and BROADCAST_QUEUE are module-level multiprocessing.Queue
singletons. Tests put tasks on them but no librarian process drains the pipe
under pytest, so the queue's daemon feeder thread blocks once the pipe buffer
fills. At interpreter shutdown multiprocessing's atexit finalizer join()s that
feeder thread (Queue._finalize_join) and waits forever: pytest reports
"426 passed" then hangs, and CI never advances.

Add a pytest_sessionfinish hook that calls cancel_join_thread() on both queues
so the unconsumed buffered items are dropped and the process exits cleanly.
Production is unaffected: the real librarian daemon drains these queues.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This reverts commit 7ec7729.
commit 1f5015d8ccc10a6b8bf0ec06ded6e5cf17aaf66e
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 16:31:22 2026 -0700

    Admin Tagging tab: move Metadata Formats docs into the field hint

    The "These metadata formats are written..." line with the ComicInfo /
    MetronInfo links was a section-level hint but really describes the
    Metadata Formats select. Render it through the select's #message slot so
    the links live in the field's own hint (VSelect forwards #message down to
    VInput's VMessages), styled identically to every other field hint. The
    section hint keeps only the write-permission warning.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit e831f2361af916e5dd3cfd71258051717b904cee
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 16:22:37 2026 -0700

    Admin Tagging tab: auto-save defaults, reorder, inline help

    Tagging Defaults and Online Tagging Defaults now save on change and the
    Save Defaults / Revert buttons are gone. A deep watch on the draft
    auto-saves (serialized, with a one-shot re-run if the draft changes
    mid-save); the defaults watcher seeds the draft once on load so a save's
    echoed response can't clobber an in-flight edit.

    Reordered sections to: Tagging Defaults, Online Tagging Defaults, Online
    Tagging Sources.

    Replaced the Online Tagging Sources hint paragraph with a per-checkbox
    title tooltip shown only while the checkbox is disabled (title on a
    wrapper element, since Vuetify drops pointer events on disabled
    controls).

    Added Match Mode and Prompts help text (static strings in data, not
    computed).

    Updates tagging-tab.test.js (12 tests).

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit d25bf69c6b52e1689ebdb64cb89689c65383f77a
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:43:42 2026 -0700

    Squash 0044 field alterations back into 0043

    Fold the auto-generated 0044 migration into 0043 in place so the
    migration history stays a single comicbox-tagging-defaults step:

    - adminflag.key: BG label "Browser Default Group" -> "Browser Default
      Collection" (group->collection unification drift)
    - librarianstatus.status_type: add JDU (Snapshot User Data Sidecar)
      and JTG (Cleanup Stale Online Tagging State)
    - settingsbrowser.order_by: add the missing AlterField

    makemigrations --check --dry-run reports no changes detected.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit e1a29cfd5f542a89add7dc05bbefac78dffac40e
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:36:03 2026 -0700

    bump alpha version

commit 35859ea7396079298bb8212743f24d77e3bc14ab
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:35:17 2026 -0700

    Tag editor: fix Identifiers round-trip (blank Source/Type/Key)

    shape_identifiers() now emits the raw `source` (e.g. "comicvine") alongside `displayName` in the {pk, source, type, code, displayName, url} shape, and the edit panel's initFromMetadata reads that structured shape instead of parsing the legacy "source::id_type:key" name string. The legacy parse populated the right number of rows but left every Source/Type/Key field blank.

    Tests: shape_identifiers raw-source/null/empty coverage (backend); edit-panel identifiers round-trip regression — N populated rows, none blank (frontend).

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit bcbfa9a83ffc034cf9c190cf1a455c2335571153
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:27:49 2026 -0700

    Tag editor: tooltips on format-disabled add buttons and inputs

    Disabled-by-format controls now carry the "Not supported by selected
    metadata formats" tooltip. The title sits on a wrapper element because
    Vuetify sets pointer-events:none on disabled controls, so a title on the
    control itself never fires. Covers the Add Universe / Add Identifier /
    Add URL buttons, the Add-role select, the credits / story-arc / universe
    / identifier table cells, and the Main Character / Main Team selects.

    Adds edit-panel-disabled-tooltips.test.js (5 tests).

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 9ea8eed6df3ae2db7c2f2c906c9238c4b9deb4cd
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:21:20 2026 -0700

    reorg admin tagging tab

commit e5ef1a8a11187b24457ef59f94836479a9fb6cf4
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:15:49 2026 -0700

    Online tagging: honor prompts_mode "never" (skip, don't queue)

    When a scan's prompts_mode is "never", build the session with
    defer_prompts=False so comicbox skips ambiguous matches inline (the
    CodexPromptHandler skips by default) and don't persist any prompts —
    only confidently-matched comics are written this run. "ask" is unchanged.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit c0530362ec3e1e832ef6b4a1e3085046aa55d519
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:11:00 2026 -0700

    Migrate BROWSER_DEFAULT_COLLECTION admin flag char -> collection

    Migration 0039 seeds the BG (Default View) admin flag with the legacy
    top-group char "p". The group->collection unification in 0043 rewrote
    every char-coded value for SettingsBrowser/Favorite/CustomCover but
    missed this AdminFlag, leaving DBs upgraded through 0043 with an invalid
    BG value: the read path silently fell back to "publishers" and the admin
    "Default View" dropdown rendered blank.

    Fold the char->collection flip into 0043 alongside the
    SettingsBrowser.top_collection move it mirrors, and add regression tests
    covering all seven chars, the reverse move, and idempotency.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit c1d7525d38646aef13feae1efe7a7dc3e4628f40
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 14:46:44 2026 -0700

    Online tagging: non-blocking prompts, drop Prompt Timeout

    Tagging scans no longer block waiting for an admin to answer ambiguous-
    match prompts. run_session does Pass 1 (auto-match + write the confident
    comics), persists any deferred prompts to the cache, and returns — the
    librarian thread is never held.

    Prompts now persist independently of any scan: they linger in the cache
    (no TTL, surviving daemon restarts) until answered or skipped. Answering
    a prompt is decoupled — resolve_prompt builds a fresh OnlineSession and
    applies that single comic's chosen match (re-mapping a chosen index to
    its issue id so a re-search can't mis-pick), then enqueues a one-comic
    write. The deferred-prompt fingerprint is deterministic across processes,
    so no live session is required.

    This makes the Prompt Timeout setting obsolete: removed the
    prompt_timeout_seconds field (model, migration 0043, serializer,
    user-data dump/restore) and the blocking _wait_for_prompts wait.

    Supporting changes:
    - session_cache: prompts are a global {fingerprint: prompt} map plus an
      active-scan marker; daemon run_start clears only the scan marker.
    - prompt endpoints are session-agnostic (/admin/tag-prompts[...]).
    - janitor prunes only prompts whose comic is gone, not "orphan" prompts.
    - frontend store/socket use the new endpoints and resync on (re)connect;
      removed the Prompt Timeout input + timeout-input.vue.
    - tests for the non-blocking flow, resolution, skip, endpoints, janitor.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 708f7ee822c6b2f64e8da4259a4f3e2ae57eedec
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 14:37:34 2026 -0700

    Tag write errors: admin feedback panel, badge, and docs

    Collect comic tag-write failures (read-only mount, permission error)
    in the filesystem cache and surface them to admins:

    - tagwrite_errors fs-cache accessors + recording in TagWriter
    - TAG_WRITE_ERRORS_CHANGED admin-only websocket notification
    - read/clear endpoint (AdminTagWriteErrorsView)
    - Tagging-tab error panel (#tagging-errors anchor), a red dot on the
      hamburger menu, and a sidebar drawer link to the error section
    - write-permission help on the Tagging tab; README/DOCKER/NEWS notes
      that the comics dir must be mounted writable for tag editing to work

    Note: the URL route (urls/api/v4/admin.py) and the socket dispatch
    case (stores/socket.js) are intentionally NOT in this commit because
    those files are mid-refactor in a concurrent session; both of my hunks
    are present in the working tree and should be committed alongside that
    work.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit d8605b9002ef61f8e7877b3993c1c2321e816afb
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 14:31:44 2026 -0700

    use key as icon for bearer token

commit f670b9099e26235bd5ead9247f586ac2fc5e644a
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 14:28:50 2026 -0700

    more explicit verbose description of bearer token

commit f53b58ed68dc21a7732c776428e5dca3e96d5507
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 14:12:04 2026 -0700

    Rename Metron online source to "Metron Cloud" in UI

    Disambiguates the Metron online tagging source from the MetronInfo
    metadata format. Presentation strings only — internal source values,
    variables, and method names are unchanged.

    - Admin Tagging tab, Online Sources: card title, enable aria-label,
      signup link, save/clear credential controls
    - Online Tagging launcher dialog: source picker title + rate-limit label
    - Match Review dialog: map the source chip to a friendly label
      (also Comic Vine), falling back to the raw id for unknown sources

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 15488b07b77bb16f4859eec64262aa8c4a2aa391
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 13:53:17 2026 -0700

    change write tags headline

commit 7139e18d4fb16a36747908d190c3ddbd92b81445
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 13:50:37 2026 -0700

    Tagging tab: per-source enable checkboxes + Write Defaults help

    Replace the "Online Sources" multi-select on the admin Tagging tab. The
    Online Source Credentials panels now live inside the Online Tagging
    Defaults block, each with an enable checkbox to its left that drives
    defaultSources. The checkbox stays disabled until that source's
    credentials are saved, and a credential-less source always reads as off.

    Add Write Defaults help text noting the formats are written every time
    tags are edited, with links to the ComicInfo and MetronInfo docs.

    Add tagging-tab unit tests covering the enable-checkbox wiring, the
    credential-gated disable, and the help links.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ajslater ajslater changed the title v2.0.0a0 v2.0.0 alpha Jun 6, 2026
ajslater and others added 9 commits June 6, 2026 16:53
- session_manager: compare prompts_mode against PromptsModeChoices.NEVER.value
  so the check is str != str, not str != the member's tuple value type
  (reportUnnecessaryComparison). Runtime-equivalent.
- test_onlinetag_session_manager: funnel intentional test doubles through an
  Any-returning _double() seam so they satisfy the strictly-typed constructor
  and _pass_runner attribute without weakening production annotations or using
  casts that basedpyright rejects (reportInvalidCast).
- Drop two unnecessary "# pyright: ignore" comments: Django's SimpleTestCase
  already declares client, and reportPrivateUsage is disabled project-wide.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Give failed imports and online-tagging match prompts the same polished
treatment as tag-write errors, across the hamburger button and sidebar
admin menu.

- Hamburger: broaden the red error dot to cover tag-write errors OR
  failed imports; add a separate amber dot for pending tagging matches.
- Sidebar admin menu: add a "Failed Imports" item (deep-links to the
  Libraries tab) and an "N Matches to Review" item (opens the Match
  Review dialog). Drop the dead clearFailedImports mapping and the
  redundant append-icon.
- Restyle the Failed Imports panel to the AdminSection look (count +
  alert icon, hint, striped table) and add a Reason column.

Expose the failure reason: FailedImportSerializer now surfaces the
model's `name` as `reason`, and set_reason falls back to the exception
class name when the message is empty so a reason is never blank.

Add a per-session "Clear Warning" control on the Failed Imports panel
that dismisses the hamburger dot + sidebar item (the table persists); a
failed-imports change event re-activates the warning.

Remove the redundant Match-to-Review button from the browser toolbar
(the sidebar item replaces it) and mount the prompt dialog in the reader
so the item works everywhere the drawer appears.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the in-memory dismissal (which reset on every reload) with a
durable "last seen" marker so a cleared failed-import warning stays
cleared across refreshes, sessions, and browsers.

- Add Timestamp key FI ("Failed Imports Seen"), folded into 0043 — the
  row auto-seeds at startup, so no separate migration is needed.
- New /admin/failed-imports/seen endpoint: GET reads the marker, PUT
  stamps it to now and notifies other admin sessions.
- admin store gains a hasUnseenFailedImports getter: the hamburger dot,
  sidebar item, and the panel's Clear Warning cluster show when any
  failed import is newer than the marker (empty marker = never cleared =
  show all). Clear Warning PUTs the marker; the socket reloads it so a
  clear in one session propagates to others.

Also refine the Failed Imports panel header: cluster the Clear Warning
button + count to the right and hide the whole cluster once cleared.

Tidy two tests touched by this sub-project (unused-arg lambda naming,
extract an OPDS stream-walk helper).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
commit 21218ec8013eef7553ed5337973170449e952c28
Author: AJ Slater <aj@slater.net>
Date:   Sun Jun 7 19:00:13 2026 -0700

    update deps

commit 013517ea0b72e34e76280d4f601718af994fda77
Author: AJ Slater <aj@slater.net>
Date:   Sun Jun 7 17:19:07 2026 -0700

    feat(browser): refresh filtered views on membership change via head probe

    The library.changed gate compared only the page mtime, which catches in-view
    edits but can miss a comic *leaving* the active filter — its collection drops
    out of the filter-matching set and, if no other matching collection moved, the
    mtime is unchanged so the view never refreshes.

    Add a membership signal. A lightweight BrowserHeadView returns {mtime, count}
    for a route, reusing BrowserView's filtered querysets (so both match the page
    exactly) while skipping pagination, card/cover annotation, and serialization.
    The browse page now also returns the full filtered membership count. The
    frontend gate calls /head on the current route and reloads when *either* mtime
    (in-view edit) or count (entered/left the filter) moved.

    - BrowserHeadView + BrowserHeadSerializer; /browse/{collection}[/{pks}]/head.
    - BrowserPageSerializer exposes the full filtered count; get_object provides it.
    - _get_collection_and_books returns the count; OPDS unpack sites widened.
    - Frontend getBrowserHead; loadMtimes compares (mtime, count). The MtimeView
      probe stays for the reader gate.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit b9ec23d3911ce654464aede66b9baa6375e68b95
Author: AJ Slater <aj@slater.net>
Date:   Sun Jun 7 16:27:57 2026 -0700

    fix(importer): re-stamp the source story arc/folder a comic leaves (m2m)

    The committed move re-stamp covered FK collections (publisher/imprint/series/
    volume); extend it to the m2m case. When a metadata edit removes a comic from a
    story arc, the prune phase now records the unlinked StoryArcNumber's story_arc
    (and folder) into the same moved_source_collections accumulator, so the delete
    phase's force-update map re-stamps the source arc. Otherwise a viewer of that
    arc never refreshes — TimestampUpdater only re-stamps arcs a current comic still
    links into. Tag-style m2ms (genres, characters, …) are not collections and are
    ignored.

    Adds a story-arc-removal regression test.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 47f3fc0ec9c09387e087ffd86b14778ce47ffb80
Author: AJ Slater <aj@slater.net>
Date:   Sun Jun 7 15:49:35 2026 -0700

    fix(browser): make library.changed refresh gate detect all collection changes

    The browser reloads the current view on library.changed only when a scoped
    /api/v4/mtime probe differs from the page's stored mtime. Four defects left the
    probe blind to real edits (tag changes, publisher renames), so the view never
    auto-refreshed:

    - TimestampUpdater: `comic__updated_at > start_time` dropped same-second
      re-imports — SQLite stamps updated_at at ms precision (3 fractional digits)
      while the Python start_time serializes 6, so a same-second row sorts before
      it as TEXT. Floor start_time to the second. Force-mapped collections now also
      re-stamp when emptied (bypass the child-count filter).
    - browser._get_page_mtime read self.model (the child collection) filtered by
      pk__in=route_pks (the parent pks) → empty aggregate → EPOCH (0). Read the
      route/container model, matching MtimeView.
    - A comic that changes publisher/imprint/series/volume moves collections, but
      only the destination was re-stamped. Capture the source collections in the
      importer and feed them to TimestampUpdater's force-update map.
    - The probe annotated per-row then took .first() (implicit ORDER BY pk),
      returning the lowest-pk collection's mtime instead of the global max — at
      root, edits to any other collection were invisible. Use order_by("-max").
      first(). Read the probe uncached (a change-detector must be fresh).

    Adds regression tests for the gate scope, precision, move, and global-max paths.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 6b347661736642cf3820a574bfbad80a534dd445
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 20:23:46 2026 -0700

    fix: remove duplicate showTagWriteErrors computed key in admin-menu

    The computed block defined showTagWriteErrors() twice with identical bodies, tripping ESLint vue/no-dupe-keys and failing make fix / make lint. Removed the second definition; behavior is unchanged.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 3503ecc2ae6e5c2cb00b3b126da86bb6d69f9bd7
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 20:19:35 2026 -0700

    fix: stop tag-write progress from oscillating during bulk writes

    comicbox.bulk_write runs writes on a thread pool and emits per-file
    events in completion order, each carrying the file's submission index.
    TagWriter set `status.complete = event.index`, so the count jumped up
    and down as out-of-order indices streamed in. Creating a fresh
    TagWriteStatus per event also reset `since_updated`, defeating the
    StatusController 5s update throttle, so every jumbled value was written
    and broadcast.

    Keep one TagWriteStatus for the whole batch and increment a monotonic
    completion counter on each terminal per-file event (parsed, error, or
    short-circuit) so the count climbs steadily to total and the throttle
    works again.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 8f334c0913ae6f363054d61334e1ffd74701a56c
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 20:15:41 2026 -0700

    update version 2.0.0a2

commit 0ac6145cefb5618951b599e10f35763807d8cb4e
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 20:14:35 2026 -0700

    feat: combo-box collection inputs to normalize duplicate-case tags

    When a browser group spans collections whose names differ only by case (e.g. a 'Marvel' and a 'marvel' publisher), the tag editor pre-filled the field with the first value and gated Save on a diff against that pre-fill -- so the duplicate could not be written across the selection.

    Publisher, Imprint, Series, and Volume are now v-comboboxes seeded from the group's own distinct values. An ambiguous field (>1 distinct value) starts blank so picking any option -- including the canonical one -- registers as a change and enables Save. Single-value editing is unchanged and untouched fields are never written. Frontend-only; the existing tag-write path collapses the duplicate on re-import.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 09dc4b535ebde866603f7e5e1db596af1ed0e211
Merge: 1f5015d8c 08f06d3
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 19:43:30 2026 -0700

    Merge branch 'pre-release' into codex-v2

commit 1f5015d8ccc10a6b8bf0ec06ded6e5cf17aaf66e
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 16:31:22 2026 -0700

    Admin Tagging tab: move Metadata Formats docs into the field hint

    The "These metadata formats are written..." line with the ComicInfo /
    MetronInfo links was a section-level hint but really describes the
    Metadata Formats select. Render it through the select's #message slot so
    the links live in the field's own hint (VSelect forwards #message down to
    VInput's VMessages), styled identically to every other field hint. The
    section hint keeps only the write-permission warning.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit e831f2361af916e5dd3cfd71258051717b904cee
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 16:22:37 2026 -0700

    Admin Tagging tab: auto-save defaults, reorder, inline help

    Tagging Defaults and Online Tagging Defaults now save on change and the
    Save Defaults / Revert buttons are gone. A deep watch on the draft
    auto-saves (serialized, with a one-shot re-run if the draft changes
    mid-save); the defaults watcher seeds the draft once on load so a save's
    echoed response can't clobber an in-flight edit.

    Reordered sections to: Tagging Defaults, Online Tagging Defaults, Online
    Tagging Sources.

    Replaced the Online Tagging Sources hint paragraph with a per-checkbox
    title tooltip shown only while the checkbox is disabled (title on a
    wrapper element, since Vuetify drops pointer events on disabled
    controls).

    Added Match Mode and Prompts help text (static strings in data, not
    computed).

    Updates tagging-tab.test.js (12 tests).

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit d25bf69c6b52e1689ebdb64cb89689c65383f77a
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:43:42 2026 -0700

    Squash 0044 field alterations back into 0043

    Fold the auto-generated 0044 migration into 0043 in place so the
    migration history stays a single comicbox-tagging-defaults step:

    - adminflag.key: BG label "Browser Default Group" -> "Browser Default
      Collection" (group->collection unification drift)
    - librarianstatus.status_type: add JDU (Snapshot User Data Sidecar)
      and JTG (Cleanup Stale Online Tagging State)
    - settingsbrowser.order_by: add the missing AlterField

    makemigrations --check --dry-run reports no changes detected.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit e1a29cfd5f542a89add7dc05bbefac78dffac40e
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:36:03 2026 -0700

    bump alpha version

commit 35859ea7396079298bb8212743f24d77e3bc14ab
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:35:17 2026 -0700

    Tag editor: fix Identifiers round-trip (blank Source/Type/Key)

    shape_identifiers() now emits the raw `source` (e.g. "comicvine") alongside `displayName` in the {pk, source, type, code, displayName, url} shape, and the edit panel's initFromMetadata reads that structured shape instead of parsing the legacy "source::id_type:key" name string. The legacy parse populated the right number of rows but left every Source/Type/Key field blank.

    Tests: shape_identifiers raw-source/null/empty coverage (backend); edit-panel identifiers round-trip regression — N populated rows, none blank (frontend).

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit bcbfa9a83ffc034cf9c190cf1a455c2335571153
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:27:49 2026 -0700

    Tag editor: tooltips on format-disabled add buttons and inputs

    Disabled-by-format controls now carry the "Not supported by selected
    metadata formats" tooltip. The title sits on a wrapper element because
    Vuetify sets pointer-events:none on disabled controls, so a title on the
    control itself never fires. Covers the Add Universe / Add Identifier /
    Add URL buttons, the Add-role select, the credits / story-arc / universe
    / identifier table cells, and the Main Character / Main Team selects.

    Adds edit-panel-disabled-tooltips.test.js (5 tests).

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 9ea8eed6df3ae2db7c2f2c906c9238c4b9deb4cd
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:21:20 2026 -0700

    reorg admin tagging tab

commit e5ef1a8a11187b24457ef59f94836479a9fb6cf4
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:15:49 2026 -0700

    Online tagging: honor prompts_mode "never" (skip, don't queue)

    When a scan's prompts_mode is "never", build the session with
    defer_prompts=False so comicbox skips ambiguous matches inline (the
    CodexPromptHandler skips by default) and don't persist any prompts —
    only confidently-matched comics are written this run. "ask" is unchanged.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit c0530362ec3e1e832ef6b4a1e3085046aa55d519
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:11:00 2026 -0700

    Migrate BROWSER_DEFAULT_COLLECTION admin flag char -> collection

    Migration 0039 seeds the BG (Default View) admin flag with the legacy
    top-group char "p". The group->collection unification in 0043 rewrote
    every char-coded value for SettingsBrowser/Favorite/CustomCover but
    missed this AdminFlag, leaving DBs upgraded through 0043 with an invalid
    BG value: the read path silently fell back to "publishers" and the admin
    "Default View" dropdown rendered blank.

    Fold the char->collection flip into 0043 alongside the
    SettingsBrowser.top_collection move it mirrors, and add regression tests
    covering all seven chars, the reverse move, and idempotency.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit c1d7525d38646aef13feae1efe7a7dc3e4628f40
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 14:46:44 2026 -0700

    Online tagging: non-blocking prompts, drop Prompt Timeout

    Tagging scans no longer block waiting for an admin to answer ambiguous-
    match prompts. run_session does Pass 1 (auto-match + write the confident
    comics), persists any deferred prompts to the cache, and returns — the
    librarian thread is never held.

    Prompts now persist independently of any scan: they linger in the cache
    (no TTL, surviving daemon restarts) until answered or skipped. Answering
    a prompt is decoupled — resolve_prompt builds a fresh OnlineSession and
    applies that single comic's chosen match (re-mapping a chosen index to
    its issue id so a re-search can't mis-pick), then enqueues a one-comic
    write. The deferred-prompt fingerprint is deterministic across processes,
    so no live session is required.

    This makes the Prompt Timeout setting obsolete: removed the
    prompt_timeout_seconds field (model, migration 0043, serializer,
    user-data dump/restore) and the blocking _wait_for_prompts wait.

    Supporting changes:
    - session_cache: prompts are a global {fingerprint: prompt} map plus an
      active-scan marker; daemon run_start clears only the scan marker.
    - prompt endpoints are session-agnostic (/admin/tag-prompts[...]).
    - janitor prunes only prompts whose comic is gone, not "orphan" prompts.
    - frontend store/socket use the new endpoints and resync on (re)connect;
      removed the Prompt Timeout input + timeout-input.vue.
    - tests for the non-blocking flow, resolution, skip, endpoints, janitor.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 708f7ee822c6b2f64e8da4259a4f3e2ae57eedec
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 14:37:34 2026 -0700

    Tag write errors: admin feedback panel, badge, and docs

    Collect comic tag-write failures (read-only mount, permission error)
    in the filesystem cache and surface them to admins:

    - tagwrite_errors fs-cache accessors + recording in TagWriter
    - TAG_WRITE_ERRORS_CHANGED admin-only websocket notification
    - read/clear endpoint (AdminTagWriteErrorsView)
    - Tagging-tab error panel (#tagging-errors anchor), a red dot on the
      hamburger menu, and a sidebar drawer link to the error section
    - write-permission help on the Tagging tab; README/DOCKER/NEWS notes
      that the comics dir must be mounted writable for tag editing to work

    Note: the URL route (urls/api/v4/admin.py) and the socket dispatch
    case (stores/socket.js) are intentionally NOT in this commit because
    those files are mid-refactor in a concurrent session; both of my hunks
    are present in the working tree and should be committed alongside that
    work.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit d8605b9002ef61f8e7877b3993c1c2321e816afb
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 14:31:44 2026 -0700

    use key as icon for bearer token

commit f670b9099e26235bd5ead9247f586ac2fc5e644a
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 14:28:50 2026 -0700

    more explicit verbose description of bearer token

commit f53b58ed68dc21a7732c776428e5dca3e96d5507
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 14:12:04 2026 -0700

    Rename Metron online source to "Metron Cloud" in UI

    Disambiguates the Metron online tagging source from the MetronInfo
    metadata format. Presentation strings only — internal source values,
    variables, and method names are unchanged.

    - Admin Tagging tab, Online Sources: card title, enable aria-label,
      signup link, save/clear credential controls
    - Online Tagging launcher dialog: source picker title + rate-limit label
    - Match Review dialog: map the source chip to a friendly label
      (also Comic Vine), falling back to the raw id for unknown sources

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 15488b07b77bb16f4859eec64262aa8c4a2aa391
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 13:53:17 2026 -0700

    change write tags headline

commit 7139e18d4fb16a36747908d190c3ddbd92b81445
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 13:50:37 2026 -0700

    Tagging tab: per-source enable checkboxes + Write Defaults help

    Replace the "Online Sources" multi-select on the admin Tagging tab. The
    Online Source Credentials panels now live inside the Online Tagging
    Defaults block, each with an enable checkbox to its left that drives
    defaultSources. The checkbox stays disabled until that source's
    credentials are saved, and a credential-less source always reads as off.

    Add Write Defaults help text noting the formats are written every time
    tags are edited, with links to the ComicInfo and MetronInfo docs.

    Add tagging-tab unit tests covering the enable-checkbox wiring, the
    credential-gated disable, and the help links.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
commit c2b57fbe0387a103d4ac0554fa7cfdc626429453
Author: AJ Slater <aj@slater.net>
Date:   Sun Jun 7 21:42:31 2026 -0700

    update depds and bump version to alpha 3

commit ff20d7e261298e1aed9e2068be362ee96ec98beb
Author: AJ Slater <aj@slater.net>
Date:   Sun Jun 7 21:16:28 2026 -0700

    fix(browser): show metadata action toolbar in multi-select

    The metadata dialog's action toolbar was gated by
    `v-if="!multiSelect && !editing"`, so selecting several comics or
    collections and opening Tags showed the intersection tag screen but no
    Download / Mark Read / Edit Tags / Tag Online / favorite controls. The
    underlying actions already accept multi-id payloads, so the gate was the
    only thing missing.

    - metadata-header: drop `!multiSelect` from the controls gate.
    - favorites store: add `setManyFavorites` (optimistic, skips pks already
      in the target state, per-pk rollback over the idempotent endpoints).
    - favorite-toggle: accept a `pks` array (cards keep single `pk`); on =
      all favorited; click routes single->toggle, bulk->setManyFavorites.
    - metadata-controls: favorite the selection target (controlBook) so the
      star bulk-favorites the whole selection; hide Read unless the
      selection resolves to a single comic.

    Adds regression coverage for the toolbar gate, the bulk-favorite store
    action, and the one-or-many toggle. vitest 150 passed; lint clean.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 62b80922e05b502af6dd030dba0661d0a1cc05da
Merge: 21218ec80 69a0280
Author: AJ Slater <aj@slater.net>
Date:   Sun Jun 7 19:52:18 2026 -0700

    Merge branch 'pre-release' into codex-v2

commit 21218ec8013eef7553ed5337973170449e952c28
Author: AJ Slater <aj@slater.net>
Date:   Sun Jun 7 19:00:13 2026 -0700

    update deps

commit 013517ea0b72e34e76280d4f601718af994fda77
Author: AJ Slater <aj@slater.net>
Date:   Sun Jun 7 17:19:07 2026 -0700

    feat(browser): refresh filtered views on membership change via head probe

    The library.changed gate compared only the page mtime, which catches in-view
    edits but can miss a comic *leaving* the active filter — its collection drops
    out of the filter-matching set and, if no other matching collection moved, the
    mtime is unchanged so the view never refreshes.

    Add a membership signal. A lightweight BrowserHeadView returns {mtime, count}
    for a route, reusing BrowserView's filtered querysets (so both match the page
    exactly) while skipping pagination, card/cover annotation, and serialization.
    The browse page now also returns the full filtered membership count. The
    frontend gate calls /head on the current route and reloads when *either* mtime
    (in-view edit) or count (entered/left the filter) moved.

    - BrowserHeadView + BrowserHeadSerializer; /browse/{collection}[/{pks}]/head.
    - BrowserPageSerializer exposes the full filtered count; get_object provides it.
    - _get_collection_and_books returns the count; OPDS unpack sites widened.
    - Frontend getBrowserHead; loadMtimes compares (mtime, count). The MtimeView
      probe stays for the reader gate.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit b9ec23d3911ce654464aede66b9baa6375e68b95
Author: AJ Slater <aj@slater.net>
Date:   Sun Jun 7 16:27:57 2026 -0700

    fix(importer): re-stamp the source story arc/folder a comic leaves (m2m)

    The committed move re-stamp covered FK collections (publisher/imprint/series/
    volume); extend it to the m2m case. When a metadata edit removes a comic from a
    story arc, the prune phase now records the unlinked StoryArcNumber's story_arc
    (and folder) into the same moved_source_collections accumulator, so the delete
    phase's force-update map re-stamps the source arc. Otherwise a viewer of that
    arc never refreshes — TimestampUpdater only re-stamps arcs a current comic still
    links into. Tag-style m2ms (genres, characters, …) are not collections and are
    ignored.

    Adds a story-arc-removal regression test.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 47f3fc0ec9c09387e087ffd86b14778ce47ffb80
Author: AJ Slater <aj@slater.net>
Date:   Sun Jun 7 15:49:35 2026 -0700

    fix(browser): make library.changed refresh gate detect all collection changes

    The browser reloads the current view on library.changed only when a scoped
    /api/v4/mtime probe differs from the page's stored mtime. Four defects left the
    probe blind to real edits (tag changes, publisher renames), so the view never
    auto-refreshed:

    - TimestampUpdater: `comic__updated_at > start_time` dropped same-second
      re-imports — SQLite stamps updated_at at ms precision (3 fractional digits)
      while the Python start_time serializes 6, so a same-second row sorts before
      it as TEXT. Floor start_time to the second. Force-mapped collections now also
      re-stamp when emptied (bypass the child-count filter).
    - browser._get_page_mtime read self.model (the child collection) filtered by
      pk__in=route_pks (the parent pks) → empty aggregate → EPOCH (0). Read the
      route/container model, matching MtimeView.
    - A comic that changes publisher/imprint/series/volume moves collections, but
      only the destination was re-stamped. Capture the source collections in the
      importer and feed them to TimestampUpdater's force-update map.
    - The probe annotated per-row then took .first() (implicit ORDER BY pk),
      returning the lowest-pk collection's mtime instead of the global max — at
      root, edits to any other collection were invisible. Use order_by("-max").
      first(). Read the probe uncached (a change-detector must be fresh).

    Adds regression tests for the gate scope, precision, move, and global-max paths.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 6b347661736642cf3820a574bfbad80a534dd445
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 20:23:46 2026 -0700

    fix: remove duplicate showTagWriteErrors computed key in admin-menu

    The computed block defined showTagWriteErrors() twice with identical bodies, tripping ESLint vue/no-dupe-keys and failing make fix / make lint. Removed the second definition; behavior is unchanged.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 3503ecc2ae6e5c2cb00b3b126da86bb6d69f9bd7
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 20:19:35 2026 -0700

    fix: stop tag-write progress from oscillating during bulk writes

    comicbox.bulk_write runs writes on a thread pool and emits per-file
    events in completion order, each carrying the file's submission index.
    TagWriter set `status.complete = event.index`, so the count jumped up
    and down as out-of-order indices streamed in. Creating a fresh
    TagWriteStatus per event also reset `since_updated`, defeating the
    StatusController 5s update throttle, so every jumbled value was written
    and broadcast.

    Keep one TagWriteStatus for the whole batch and increment a monotonic
    completion counter on each terminal per-file event (parsed, error, or
    short-circuit) so the count climbs steadily to total and the throttle
    works again.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 8f334c0913ae6f363054d61334e1ffd74701a56c
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 20:15:41 2026 -0700

    update version 2.0.0a2

commit 0ac6145cefb5618951b599e10f35763807d8cb4e
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 20:14:35 2026 -0700

    feat: combo-box collection inputs to normalize duplicate-case tags

    When a browser group spans collections whose names differ only by case (e.g. a 'Marvel' and a 'marvel' publisher), the tag editor pre-filled the field with the first value and gated Save on a diff against that pre-fill -- so the duplicate could not be written across the selection.

    Publisher, Imprint, Series, and Volume are now v-comboboxes seeded from the group's own distinct values. An ambiguous field (>1 distinct value) starts blank so picking any option -- including the canonical one -- registers as a change and enables Save. Single-value editing is unchanged and untouched fields are never written. Frontend-only; the existing tag-write path collapses the duplicate on re-import.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 09dc4b535ebde866603f7e5e1db596af1ed0e211
Merge: 1f5015d8c 08f06d3
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 19:43:30 2026 -0700

    Merge branch 'pre-release' into codex-v2

commit 1f5015d8ccc10a6b8bf0ec06ded6e5cf17aaf66e
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 16:31:22 2026 -0700

    Admin Tagging tab: move Metadata Formats docs into the field hint

    The "These metadata formats are written..." line with the ComicInfo /
    MetronInfo links was a section-level hint but really describes the
    Metadata Formats select. Render it through the select's #message slot so
    the links live in the field's own hint (VSelect forwards #message down to
    VInput's VMessages), styled identically to every other field hint. The
    section hint keeps only the write-permission warning.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit e831f2361af916e5dd3cfd71258051717b904cee
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 16:22:37 2026 -0700

    Admin Tagging tab: auto-save defaults, reorder, inline help

    Tagging Defaults and Online Tagging Defaults now save on change and the
    Save Defaults / Revert buttons are gone. A deep watch on the draft
    auto-saves (serialized, with a one-shot re-run if the draft changes
    mid-save); the defaults watcher seeds the draft once on load so a save's
    echoed response can't clobber an in-flight edit.

    Reordered sections to: Tagging Defaults, Online Tagging Defaults, Online
    Tagging Sources.

    Replaced the Online Tagging Sources hint paragraph with a per-checkbox
    title tooltip shown only while the checkbox is disabled (title on a
    wrapper element, since Vuetify drops pointer events on disabled
    controls).

    Added Match Mode and Prompts help text (static strings in data, not
    computed).

    Updates tagging-tab.test.js (12 tests).

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit d25bf69c6b52e1689ebdb64cb89689c65383f77a
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:43:42 2026 -0700

    Squash 0044 field alterations back into 0043

    Fold the auto-generated 0044 migration into 0043 in place so the
    migration history stays a single comicbox-tagging-defaults step:

    - adminflag.key: BG label "Browser Default Group" -> "Browser Default
      Collection" (group->collection unification drift)
    - librarianstatus.status_type: add JDU (Snapshot User Data Sidecar)
      and JTG (Cleanup Stale Online Tagging State)
    - settingsbrowser.order_by: add the missing AlterField

    makemigrations --check --dry-run reports no changes detected.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit e1a29cfd5f542a89add7dc05bbefac78dffac40e
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:36:03 2026 -0700

    bump alpha version

commit 35859ea7396079298bb8212743f24d77e3bc14ab
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:35:17 2026 -0700

    Tag editor: fix Identifiers round-trip (blank Source/Type/Key)

    shape_identifiers() now emits the raw `source` (e.g. "comicvine") alongside `displayName` in the {pk, source, type, code, displayName, url} shape, and the edit panel's initFromMetadata reads that structured shape instead of parsing the legacy "source::id_type:key" name string. The legacy parse populated the right number of rows but left every Source/Type/Key field blank.

    Tests: shape_identifiers raw-source/null/empty coverage (backend); edit-panel identifiers round-trip regression — N populated rows, none blank (frontend).

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit bcbfa9a83ffc034cf9c190cf1a455c2335571153
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:27:49 2026 -0700

    Tag editor: tooltips on format-disabled add buttons and inputs

    Disabled-by-format controls now carry the "Not supported by selected
    metadata formats" tooltip. The title sits on a wrapper element because
    Vuetify sets pointer-events:none on disabled controls, so a title on the
    control itself never fires. Covers the Add Universe / Add Identifier /
    Add URL buttons, the Add-role select, the credits / story-arc / universe
    / identifier table cells, and the Main Character / Main Team selects.

    Adds edit-panel-disabled-tooltips.test.js (5 tests).

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 9ea8eed6df3ae2db7c2f2c906c9238c4b9deb4cd
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:21:20 2026 -0700

    reorg admin tagging tab

commit e5ef1a8a11187b24457ef59f94836479a9fb6cf4
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:15:49 2026 -0700

    Online tagging: honor prompts_mode "never" (skip, don't queue)

    When a scan's prompts_mode is "never", build the session with
    defer_prompts=False so comicbox skips ambiguous matches inline (the
    CodexPromptHandler skips by default) and don't persist any prompts —
    only confidently-matched comics are written this run. "ask" is unchanged.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit c0530362ec3e1e832ef6b4a1e3085046aa55d519
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 15:11:00 2026 -0700

    Migrate BROWSER_DEFAULT_COLLECTION admin flag char -> collection

    Migration 0039 seeds the BG (Default View) admin flag with the legacy
    top-group char "p". The group->collection unification in 0043 rewrote
    every char-coded value for SettingsBrowser/Favorite/CustomCover but
    missed this AdminFlag, leaving DBs upgraded through 0043 with an invalid
    BG value: the read path silently fell back to "publishers" and the admin
    "Default View" dropdown rendered blank.

    Fold the char->collection flip into 0043 alongside the
    SettingsBrowser.top_collection move it mirrors, and add regression tests
    covering all seven chars, the reverse move, and idempotency.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit c1d7525d38646aef13feae1efe7a7dc3e4628f40
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 14:46:44 2026 -0700

    Online tagging: non-blocking prompts, drop Prompt Timeout

    Tagging scans no longer block waiting for an admin to answer ambiguous-
    match prompts. run_session does Pass 1 (auto-match + write the confident
    comics), persists any deferred prompts to the cache, and returns — the
    librarian thread is never held.

    Prompts now persist independently of any scan: they linger in the cache
    (no TTL, surviving daemon restarts) until answered or skipped. Answering
    a prompt is decoupled — resolve_prompt builds a fresh OnlineSession and
    applies that single comic's chosen match (re-mapping a chosen index to
    its issue id so a re-search can't mis-pick), then enqueues a one-comic
    write. The deferred-prompt fingerprint is deterministic across processes,
    so no live session is required.

    This makes the Prompt Timeout setting obsolete: removed the
    prompt_timeout_seconds field (model, migration 0043, serializer,
    user-data dump/restore) and the blocking _wait_for_prompts wait.

    Supporting changes:
    - session_cache: prompts are a global {fingerprint: prompt} map plus an
      active-scan marker; daemon run_start clears only the scan marker.
    - prompt endpoints are session-agnostic (/admin/tag-prompts[...]).
    - janitor prunes only prompts whose comic is gone, not "orphan" prompts.
    - frontend store/socket use the new endpoints and resync on (re)connect;
      removed the Prompt Timeout input + timeout-input.vue.
    - tests for the non-blocking flow, resolution, skip, endpoints, janitor.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 708f7ee822c6b2f64e8da4259a4f3e2ae57eedec
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 14:37:34 2026 -0700

    Tag write errors: admin feedback panel, badge, and docs

    Collect comic tag-write failures (read-only mount, permission error)
    in the filesystem cache and surface them to admins:

    - tagwrite_errors fs-cache accessors + recording in TagWriter
    - TAG_WRITE_ERRORS_CHANGED admin-only websocket notification
    - read/clear endpoint (AdminTagWriteErrorsView)
    - Tagging-tab error panel (#tagging-errors anchor), a red dot on the
      hamburger menu, and a sidebar drawer link to the error section
    - write-permission help on the Tagging tab; README/DOCKER/NEWS notes
      that the comics dir must be mounted writable for tag editing to work

    Note: the URL route (urls/api/v4/admin.py) and the socket dispatch
    case (stores/socket.js) are intentionally NOT in this commit because
    those files are mid-refactor in a concurrent session; both of my hunks
    are present in the working tree and should be committed alongside that
    work.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit d8605b9002ef61f8e7877b3993c1c2321e816afb
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 14:31:44 2026 -0700

    use key as icon for bearer token

commit f670b9099e26235bd5ead9247f586ac2fc5e644a
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 14:28:50 2026 -0700

    more explicit verbose description of bearer token

commit f53b58ed68dc21a7732c776428e5dca3e96d5507
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 14:12:04 2026 -0700

    Rename Metron online source to "Metron Cloud" in UI

    Disambiguates the Metron online tagging source from the MetronInfo
    metadata format. Presentation strings only — internal source values,
    variables, and method names are unchanged.

    - Admin Tagging tab, Online Sources: card title, enable aria-label,
      signup link, save/clear credential controls
    - Online Tagging launcher dialog: source picker title + rate-limit label
    - Match Review dialog: map the source chip to a friendly label
      (also Comic Vine), falling back to the raw id for unknown sources

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

commit 15488b07b77bb16f4859eec64262aa8c4a2aa391
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 13:53:17 2026 -0700

    change write tags headline

commit 7139e18d4fb16a36747908d190c3ddbd92b81445
Author: AJ Slater <aj@slater.net>
Date:   Sat Jun 6 13:50:37 2026 -0700

    Tagging tab: per-source enable checkboxes + Write Defaults help

    Replace the "Online Sources" multi-select on the admin Tagging tab. The
    Online Source Credentials panels now live inside the Online Tagging
    Defaults block, each with an enable checkbox to its left that drives
    defaultSources. The checkbox stays disabled until that source's
    credentials are saved, and a credential-less source always reads as off.

    Add Write Defaults help text noting the formats are written every time
    tags are edited, with links to the ComicInfo and MetronInfo docs.

    Add tagging-tab unit tests covering the enable-checkbox wiring, the
    credential-gated disable, and the help links.

    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant