Skip to content

Split series total_book_count into total_volume_count and total_chapter_count#8

Merged
AshDevFr merged 19 commits into
mainfrom
metadata-count-split
May 3, 2026
Merged

Split series total_book_count into total_volume_count and total_chapter_count#8
AshDevFr merged 19 commits into
mainfrom
metadata-count-split

Conversation

@AshDevFr
Copy link
Copy Markdown
Owner

@AshDevFr AshDevFr commented May 2, 2026

Summary

  • Replaces the overloaded series_metadata.total_book_count with two semantically distinct fields, total_volume_count (Option<i32>) and total_chapter_count (Option<f32>), each with its own lock. Fixes the long-standing "109/14" incoherence on chapter-organized series and unblocks per-axis comparisons for release tracking.
  • Threads the split end-to-end: schema + migration, plugin protocol (bumped to 1.2, breaking minor), MetadataApplier writes with per-field locks/permissions, MangaBaka + AniList plugins populated from provider data, v1 API DTOs, frontend (series detail, manual edit, bulk edit, conditions editor, recommendations), and Komga compatibility (internal field renamed; totalBookCount wire shape preserved for Komic/Mihon).
  • Hard cutover, single PR: legacy total_book_count column, lock, permission, DTO field, and MetadataWriteTotalBookCount permission are all dropped in the final phase. Older plugins that still emit total_book_count round-trip silently (serde drops the unknown field).
  • Drive-by (Phase 5b): adds an optional format discriminator (manga / novel / light_novel / manhwa / etc.) to plugin SearchResultPreview, populated by MangaBaka, rendered as a colored badge in MetadataSearchModal. Fixes the recurring papercut where two visually identical search rows differ only by being the manga vs the novel adaptation.

Why now

total_book_count meant volumes for some users and chapters for others, and there was no way to model a mixed library or a chapter-organized series with a volume-count provider. The release-tracking branch already split the tracking side (series_tracking.latest_known_chapter REAL + latest_known_volume INTEGER); release-tracking Phase 5+ is blocked on metadata catching up. This PR unblocks it.

Notable changes

  • Schema: migration m20260502_000067_split_book_count adds the four new columns and backfills total_volume_count (+ lock) from total_book_count (+ lock); m20260502_000068_drop_book_count removes the legacy columns. down() re-adds them as nullable / default-false with no data restore.
  • Plugin protocol → 1.2: PluginSeriesMetadata and UserLibraryEntry lose total_book_count on the wire; MetadataWriteTotalBookCount permission removed; new metadata:write:total_volume_count and metadata:write:total_chapter_count permissions added.
  • Komga DTO: internal field renamed to total_volume_count; explicit #[serde(rename = "totalBookCount")] keeps the wire format unchanged. No chapter-count field is invented (Komga upstream has none).
  • Handlebars / preprocessing context: totalBookCount access path is gone; templates and rules using it must switch to totalVolumeCount / totalChapterCount.
  • Decision: hard removal, not long-lived deprecation. Single-user codebase, short overlap window across phases, no long-term carrying cost. Older external plugins keep working (unknown fields dropped); users on Handlebars templates that referenced totalBookCount need to update.

Breaking changes

  • v1 API: totalBookCount and totalBookCountLock are gone from every series-metadata request and response shape (PUT, PATCH, GET, bulk PATCH, recommendations, full metadata, locks).
  • Plugin protocol: 1.0/1.1 plugins should declare 1.2 if they want to populate the new fields; 1.0/1.1 plugins still load (the old field is silently dropped on the wire).
  • Permission system: metadata:write:total_book_count removed.
  • Handlebars / preprocessing rules: totalBookCount field-access path removed.
  • Komga API consumers: no change (wire format preserved).

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 2, 2026

Deploying codex with  Cloudflare Pages  Cloudflare Pages

Latest commit: 2b8f979
Status: ✅  Deploy successful!
Preview URL: https://2b111de7.codex-asm.pages.dev
Branch Preview URL: https://metadata-count-split.codex-asm.pages.dev

View logs

AshDevFr added 19 commits May 2, 2026 23:10
…er counts

Replace the overloaded series_metadata.total_book_count with two
semantically distinct fields, total_volume_count (Option<i32>) and
total_chapter_count (Option<f32>), each with its own lock. The single
column was ambiguous: it meant volumes for volume-organized libraries
and chapters for chapter-organized ones, producing incoherent counts
like "109/14" when providers returned volume totals against a local
chapter-organized series.

This change adds the new schema and supporting plumbing without yet
touching read sites, plugin protocol, or DTOs:

- Migration m20260502_000067_split_book_count adds the four new
  columns and backfills total_volume_count + lock from the legacy
  total_book_count + lock. The legacy column is kept for now and will
  be dropped in a follow-up once all read sites are migrated.
- SeriesMetadata entity gains the four new fields; legacy fields are
  marked deprecated. All explicit ActiveModel constructors are updated.
- SeriesMetadataRepository gains update_total_volume_count and
  update_total_chapter_count, and set_lock / is_field_locked recognise
  the new lock keys.
- Adds backfill and round-trip tests, including fractional f32 chapter
  counts and independent lock toggling.
…in protocol

Extend PluginSeriesMetadata and UserLibraryEntry with total_volume_count
(Option<i32>) and total_chapter_count (Option<f32>) so providers can return
the two semantically distinct counts separately. Legacy total_book_count
remains on the wire and is doc-flagged DEPRECATED for one phase of
backward-compat with older plugins; it will be removed once the apply
pipeline and DTOs migrate.

Add MetadataWriteTotalVolumeCount and MetadataWriteTotalChapterCount
permissions (metadata:write:total_volume_count and
metadata:write:total_chapter_count), wired into as_str/from_str, the
all_write_permissions and common_write_permissions lists, and the
metadata:write:* wildcard branch in has_permission.

Bump PROTOCOL_VERSION from 1.0 to 1.1 (additive minor): 1.0 plugins
continue to deserialize cleanly because total_book_count is still in
the schema.

Tests cover serde round-trip for the new fields, legacy total_book_count
deserialization, skip-when-unset behavior, permission round-trip, and
wildcard permission grants.
…MetadataApplier

Wire writes for the two new count fields through the existing MetadataApplier
pipeline so plugins can populate them end-to-end. Each new field reuses the
established allowlist + lock + permission + repo-update + applied/skipped
tracking pattern, with no logic divergence:

- totalVolumeCount: gated by MetadataWriteTotalVolumeCount and
  total_volume_count_lock; writes via update_total_volume_count.
- totalChapterCount: gated by MetadataWriteTotalChapterCount and
  total_chapter_count_lock; writes via update_total_chapter_count.

The legacy totalBookCount block is left in place and now carries a DEPRECATED
doc-comment marking it for backward-compat-only writes; it will be replaced
by a fallback that routes legacy plugin payloads to totalVolumeCount, and
removed when total_book_count is dropped from the schema.

Adds integration tests covering happy-path writes, fractional chapter
round-trips, both-fields-at-once, lock skipping with reason, permission
denial, allowlist filtering, and absent-value no-op behavior.
…writing total_book_count

MetadataApplier no longer writes the legacy total_book_count column. A
backward-compat fallback routes legacy `total_book_count` payloads from
older plugins into `total_volume_count`, with a one-time-per-plugin
deprecation warning. The legacy column is now frozen.

Switch every internal read of total_book_count to total_volume_count
(and surface total_chapter_count where applicable):

- services: completion filter, export collector (new
  ExpectedChapterCount field), recommendations protocol, plugin
  library entries, preprocessing context (new totalVolumeCount /
  totalChapterCount template field-access paths), user-plugin-sync
  push (now also surfaces total_chapters on SyncProgress).
- v1 API handlers: series, recommendations, plugin_actions (separate
  totalVolumeCount and totalChapterCount previews), bulk_metadata.
- Komga compatibility: handler maps the volume-shaped totalBookCount
  and the oneshot heuristic from total_volume_count.

DTOs gain total_volume_count and total_chapter_count (and their lock
counterparts) alongside the legacy fields. Both are populated for one
phase to give clients and plugins a window to migrate; the legacy
fields and the database column will be dropped in a later phase. PUT,
PATCH, and lock-update handlers prefer the new fields and fall back to
the legacy field when only it is provided.

Tests cover the legacy fallback, new-field precedence, and the
frozen-column invariant; pre-existing fixtures that seeded
total_book_count are updated to use total_volume_count.
…om MangaBaka and AniList

Extend the TypeScript SDK's PluginSeriesMetadata, UserLibraryEntry, and
Recommendation types with totalVolumeCount (number) and totalChapterCount
(number, fractional supported), mark totalBookCount @deprecated, and widen
the manifest protocolVersion union to "1.0" | "1.1" so updated plugins can
declare 1.1 without breaking 1.0 consumers.

Update the MangaBaka metadata plugin to map both fields from MangaBaka's
final_volume and total_chapters response fields via dedicated parser
helpers that reject null/empty/non-numeric/non-positive inputs. The legacy
totalBookCount continues to mirror the volume count for one phase of
backward-compat with older Codex versions. Bump the manifest to protocol
1.1 and add fixture tests for the basic case, fractional chapter counts,
null handling, and garbage input.

Update the AniList recommendations plugin to fetch Media.chapters from the
GraphQL recommendations query and derive totalVolumeCount and
totalChapterCount in convertRecommendations, filtering zero/negative
values and mirroring the volume count to legacy totalBookCount. Bump the
manifest to protocol 1.1. The existing volumes coverage was rewritten to
assert the new fields plus mirroring, with new cases for chapters-only,
both-fields-known, and zero/negative rejection.

MAL is skipped (no metadata-mal plugin in this repo). OpenLibrary is left
as-is since its mapper does not currently populate totalBookCount, so
there is nothing to migrate yet.
Add an optional `format` field to the plugin `SearchResultPreview`
protocol so search-result rows can carry a content-type discriminator
(manga, novel, light_novel, manhwa, manhua, comic, webtoon, one_shot,
etc.). The field is free-form at the protocol layer so plugin authors
are not locked into an enum that requires Codex core changes when new
formats appear; the recommended vocabulary is documented on the field.

The MangaBaka plugin populates it from `series.type` (already lowercase
snake_case, no conversion needed). The metadata search modal renders a
colored badge ahead of the existing status/bookCount/genres badges:
grape for the manga family, teal for novels, orange for comics, gray
fallback for unknowns with the raw value title-cased. A small helper
module owns the color/label resolution so the mapping is unit-testable.

This fixes a recurring papercut where MangaBaka returns two visually
identical rows for the same title (e.g. one tagged manga, one tagged
novel) and the only way to tell them apart was to click through to the
provider. The data was already on the wire from MangaBaka; it just was
not propagated through the plugin protocol or rendered in the modal.

Tests added at every layer (protocol round-trip + old-shape compat,
mapper, badge helper, modal rendering with distinct colors). OpenAPI
spec and TypeScript types regenerated.
Replaces the single "Total Books" surface across the frontend with separate
volume and chapter counts, matching the metadata schema split.

- Add seriesCounts helper that formats the series detail header line based
  on which axes are known: both → "<local>/<vol> vol · <chap> ch", volumes
  only → "<local>/<vol> vol", chapters only → "<local>/<chap> ch" (the
  chapter-organized fix), legacy "<local> books" fallback, and nothing
  rendered when neither side has data.
- SeriesDetail header switches to the helper, reading totalVolumeCount and
  totalChapterCount directly instead of falling back through totalBookCount.
- SeriesMetadataEditModal splits the single Total Books field into Total
  Volumes (int) and Total Chapters (float) with independent lock toggles;
  save handler parses both and writes them on the PATCH.
- BulkMetadataEditModal applies the same split to series form state, lock
  field list, and UI; bulk patch payload now carries both new fields.
- ConditionsEditor field/lock option lists swap the legacy paths for the
  new totalVolumeCount and totalChapterCount entries.
- SeriesMetadata grid component renders separate Total Volumes and Total
  Chapters tiles.

Tests added for the formatter, the manual-edit round-trip (including
fractional chapter counts), and the bulk patch round-trip.
Strip the deprecated `totalBookCount` (and its lock counterpart) from
every v1 request and response shape: SeriesMetadata, PatchSeriesMetadataRequest,
SeriesMetadataResponse, MetadataLocks, FullSeriesMetadataResponse,
SeriesFullMetadata, MetadataContextDto, UpdateMetadataLocksRequest,
BulkPatchSeriesMetadataRequest, and RecommendationDto. Update v1 handlers
across series, bulk_metadata, and recommendations to drop the matching
read/write paths. The plugin-protocol fallback that maps a legacy
`total_book_count` from older plugins onto `total_volume_count` is kept,
along with the entity-side legacy-column setters, until the column itself
is dropped.

Add `metadata:write:total_volume_count` and
`metadata:write:total_chapter_count` to the documented permission
allowlist; the legacy `metadata:write:total_book_count` permission
remains until the column drop.

OPDS 1.2 and OPDS 2.0 expose no expected-total counts and need no
changes. The Komga compatibility DTO retains `totalBookCount` on the
wire because Komga clients depend on that name; that mapping is owned
by the next phase of the migration.

Regenerate the OpenAPI spec and TypeScript types so the frontend
consumes only the new fields. Tests updated where they referenced the
removed DTO field.
…ve totalBookCount wire format

Rename the internal field on `KomgaSeriesMetadataDto` from `total_book_count`
to `total_volume_count` to match the canonical source field on
`series_metadata`. Keep Komga's wire format unchanged with explicit
`#[serde(rename = "totalBookCount" / "totalBookCountLock")]` so Komga
clients (Komic, Mihon, etc.) continue to see the field names they expect.
The struct-level `rename_all = "camelCase"` would otherwise emit
`totalVolumeCount`.

Komga upstream has no chapter-count field, so `total_chapter_count` is
intentionally not surfaced on the Komga DTO; chapter data is exposed only
via the native v1 API.

Update the handler in routes/komga/handlers/series.rs to populate the
renamed fields from `series_metadata.total_volume_count` (and lock).

Add tests covering the wire shape end-to-end: a Komic-style PUT-payload
roundtrip on the DTO, an Option-skip assertion when the count is None,
and an integration test against the live handler that asserts
`metadata.totalBookCount` is populated from `total_volume_count`, no
internal field name leaks, and no chapter-count field is invented.

Regenerate web/openapi.json, docs/api/openapi.json, and
web/src/types/api.generated.ts to match.
…l, and DTOs

Hard-remove the overloaded total_book_count field everywhere it still
exists outside the Komga compatibility layer, completing the migration
to total_volume_count + total_chapter_count.

Schema: new migration drops series_metadata.total_book_count and
total_book_count_lock; down() re-adds them as nullable column + non-null
default-false bool with no data restore.

Plugin protocol bumped to 1.2 (breaking minor): PluginSeriesMetadata and
UserLibraryEntry lose total_book_count on the wire, and the
MetadataWriteTotalBookCount permission is removed from the enum, parser,
display, and permission-set builders. Older plugins that still emit the
legacy field round-trip silently — serde drops the unknown value with
no fallback path.

MetadataApplier loses its legacy routing block and the per-plugin
deprecation-warning helper. The plugin-actions preview shim that
or'd total_book_count into the proposed volume count is gone.
SeriesMetadataRepository::update_total_book_count() and the
"total_book_count" branches in set_lock / is_field_locked are removed.

Read-site sweep: Recommendation, MetadataContext (preprocessing), the
recommendations handler fallback, the user-library push entries, and the
v1 plugin permission allowlist are now split-only. SDK TypeScript types
(protocol.ts, recommendations.ts) lose the deprecated field from
PluginSeriesMetadata, UserLibraryEntry, and Recommendation.

Plugins: metadata-echo, metadata-mangabaka, and recommendations-anilist
no longer mirror the volume value into a legacy field; matching test
assertions are pruned.

Frontend: MetadataPreview labels swap "Total Books" for separate "Total
Volumes" + "Total Chapters" entries; MetadataSearchModal reads
totalVolumeCount; RecommendationCard renders separate vol/ch badges;
templateUtils, exampleTemplates Handlebars samples, and the MSW mock
handlers carry the split fields and split locks. The web permission
union/lookup adds metadata:write:total_volume_count and
metadata:write:total_chapter_count, dropping the legacy permission.

The Komga API DTO keeps its serde-renamed totalBookCount wire field
intact — Komga clients (Komic, Mihon, etc.) still see the field name
they expect.

Migration test coverage: a new SQLite test runs the count-split + drop
pair step-by-step and asserts the legacy columns disappear while the
split-count columns survive. Three integration tests in metadata_apply
that exercised the (now-gone) backward-compat fallback are removed
along with their helper.
Add the user-facing and plugin-author documentation for the metadata
count split (total_volume_count + total_chapter_count replacing the
legacy total_book_count).

User docs:
- New series-metadata.md page covering volumes vs. chapters semantics,
  the four total-display variants, manual-edit instructions with
  per-field locks, what each first-party plugin populates, migration
  notes, and the Komga compatibility caveat. Wired into the docs
  sidebar between book-metadata and custom-metadata.
- Sweep stale totalBookCount references in custom-metadata.md (field
  table + Handlebars examples), preprocessing-rules.md (field table),
  and plugins/anilist-sync.md (Completed-status criterion).

Plugin author guide:
- Add a "Volume and Chapter Counts" subsection to writing-plugins.md
  with a field-comparison table, mapping examples for MangaBaka,
  AniList, and volume-only providers, and a protocol-1.2 deprecation
  note. Update the example provider's get() to populate both fields.
- Replace totalBookCount with totalVolumeCount and totalChapterCount
  on the PluginSeriesMetadata interface in sdk.md and on the
  metadata/series/get example response in protocol.md.

Changelog:
- Prepend an [unreleased] block calling out the breaking API DTO
  change, the plugin-protocol 1.2 bump, and the Handlebars template
  context change, plus the additive features (per-axis split + the
  search-result format discriminator).
Adds the local counterpart to the world totals (total_volume_count /
total_chapter_count) by splitting per-book classification into sibling
volume and chapter columns on book_metadata. The populated/null pairing
across the two columns derives the kind of book (volume / chapter /
chapter-of-volume / unknown) without an enum, enabling the
local_max_volume / local_max_chapter aggregations release-tracking needs.

- Migration adds chapter (REAL) + chapter_lock (BOOLEAN) to book_metadata
  with up/down coverage.
- Entity gains chapter + chapter_lock; BookMetadataRepository gains
  update_chapter, update_volume, and a set_lock(field, locked) helper
  mirroring the series_metadata pattern.
- Renames the BookNamingStrategy trait to BookMetadataStrategy (alias
  retained) and extends it with resolve_volume / resolve_chapter. Each
  strategy implements per its semantics: Filename uses a structured
  regex, MetadataFirst reads ComicInfo only, Smart prefers ComicInfo
  with a filename fallback, SeriesName passes through context, and
  Custom delegates to its existing named-group extraction.
- New structured filename parser uses a lenient prefix
  (v / vol / volume, c / ch / chapter), strict left boundary,
  first-match-wins per axis, no bare-number fallback. Fractional
  volumes are rejected (column is i32) and fractional chapters are
  preserved (e.g. 47.5 side chapters).
- Scanner wires the classification into both metadata-write paths so
  the active strategy populates volume + chapter on every analysis,
  with per-field locks gating writes the same way they gate every other
  field.

Tests added at the migration, repository, strategy, and parser layers.
…icInfo

Wire the per-book classification trait into the analyzer's metadata write
paths and add a one-time backfill over already-scanned libraries.

- ComicInfo gains a structured `chapter: Option<f32>` derived from
  `<Number>` (handles fractional chapters like 47.5; non-numeric values
  leave chapter null while preserving the raw string for legacy display).
- analyzer_queue threads `comic_info.chapter` into the strategy input
  and uses `comic_info.chapter` as the existing-row update fallback,
  matching the volume rule and restoring symmetry between the two axes.
- New `m20260503_000070_backfill_book_volume_chapter` migration
  re-parses each book's filename in 1000-row batches, populating volume
  and chapter only where currently null. Strictly additive: never
  overwrites a populated value, never touches lock fields. Idempotent
  on rerun. The filename parser is duplicated inline since the
  migration crate cannot depend on the main crate.
- BookMetadataApplier gains volume and chapter write blocks following
  the existing per-field lock + permission pattern. Plugin-protocol
  f64 volume is narrowed to i32 with fractional rejection rather than
  silent truncation. New `metadata:write:volume` and
  `metadata:write:chapter` permissions; both flow through the
  `metadata:write:*` wildcard.

Tests cover the strategy x parse-case matrix, ComicInfo `<Number>`
derivation, the backfill (full case matrix + additive-only +
idempotency), and the new applier blocks (write, fractional preserved
or rejected, lock-honored, permission-missing-skipped, allowlist).
… display to <max>/<total>

Adds per-series aggregates derived from book_metadata.volume / chapter so
the series detail header can render `14/17 vol · 137/158 ch` instead of
the file-count-based `17/17 vol`, which was incoherent for libraries
that mix complete volumes with loose chapters.

- Repo: new BookClassificationAggregates struct and
  get_book_classification_aggregates{,_for_series_ids} on
  SeriesRepository. Single SQL pass: MAX(volume), MAX(chapter), and
  SUM(CASE WHEN volume IS NOT NULL AND chapter IS NULL THEN 1 ELSE 0)
  over books LEFT JOIN book_metadata grouped by series_id. Works on
  SQLite and PostgreSQL.
- DTOs: SeriesDto and FullSeriesResponse gain optional localMaxVolume,
  localMaxChapter, and volumesOwned (skipped when None,
  schema-documented). Both the single-series helper and the batched
  variant feed the new aggregator (added to the existing tokio::join!
  block so list endpoints stay one round-trip).
- Frontend: formatSeriesCounts gains optional localMaxVolume and
  localMaxChapter inputs. When present they replace the file-count
  numerator on each axis; when absent the legacy formatting is
  preserved verbatim. Wired into SeriesDetail.
- OpenAPI regenerated; new fields land in the TypeScript client types.

Tests cover mixed/empty/unclassified series at the repo layer, the
DTO round-trip via /api/v1/series/{id}, and the new formatter
branches alongside every existing case.
…classification in the UI

Adds the chapter axis end-to-end on per-book metadata, completing the count-split
work down to the individual file. Previously chapter lived only on the entity
and series-level aggregates; the per-book DTOs and frontend had no way to read
or write it.

API:
- Add `chapter: Option<f32>` and `chapter_lock: bool` to BookMetadataDto,
  BookFullMetadata, BookMetadataResponse, ReplaceBookMetadataRequest,
  PatchBookMetadataRequest (PatchValue<f32>), BookMetadataLocks,
  UpdateBookMetadataLocksRequest, and BookMetadataContextDto.
- Wire the new field through both write paths in replace_book_metadata and
  patch_book_metadata with the same auto-lock-on-set rule as `volume`.
- update_book_metadata_locks accepts and writes `chapter_lock`.
- Preprocessing context exposes `chapter` and `chapter_lock` plus a new
  `"chapter"` accessor in get_metadata_field for templates and conditions.
- Regenerate OpenAPI spec and TypeScript types.

UI:
- New BookKindBadge component classifies a book by which of (volume, chapter)
  are populated. Four cases: volume-only -> "Vol N", chapter-only -> "Ch N",
  both -> "Vol V . Ch C" (single combined badge), neither -> muted "Vol"
  with explanatory tooltip. Wired into the book detail header next to
  BookTypeBadge.
- BookMetadataEditModal gains a Chapter LockableInput between Volume and
  Count in the Publication tab, with step="any" so fractional values like
  42.5 round-trip cleanly. Independent lock toggle from volume's.
- Strategy UI: rename the user-facing label "Book Naming Strategy" to
  "Book Metadata Strategy" with the description "How book metadata
  (title, volume, chapter) is extracted from files". Per-strategy
  descriptions rewritten to call out volume/chapter behavior. The
  server-side BookStrategy enum is unchanged to avoid migrating stored
  library configs.

Tests added for the new badge component, the chapter round-trip through
the edit modal, and the renamed Strategy UI label and description.
Add a Volume and Chapter Numbers section to book-metadata.md covering the
classification matrix, the canonical filename markers (vN, cN, Vol.N,
Chapter N, v15 - c126, fractional c042.5), the boundary and
fractional-volume rules, and the strategy-conditional ComicInfo override
behavior. Cross-link the Filename / Smart / Metadata First / Custom
sections in book-strategies.md so each strategy explains what it does
for volume and chapter, and refresh the Custom-strategy named-group
reference with a fractional-chapter regex variant.

Extend examples.md with two new worked custom-regex examples
(chapter-only libraries with range filenames; volume-of-series + issue
number for Western comics) and add explanatory text to the existing
scanlation-bracketed and SxxExx examples.

Fix a broken link in series-metadata.md (the dev plugin author guide
lives under a separate Docusaurus content plugin and needs an absolute
path) so the docs build succeeds.

Update CHANGELOG with two unreleased Features entries covering per-book
classification across all four book strategies and the series-detail
count-display switch to local_max/total.
…Postgres

The book volume/chapter backfill migration constructed its per-row UPDATE
with hand-rolled SQL containing `?` placeholders and shipped it through
`Statement::from_sql_and_values`. SeaORM does not rewrite placeholders to
match the target backend, so on PostgreSQL the literal `?` reached the
server and parsing failed near `WHERE`, aborting database init for any
fresh Postgres deployment. SQLite tolerated `?` and masked the bug locally.

Switch the UPDATE to `sea_query::Query::update()` rendered via
`backend.build(...)`, which emits `?` for SQLite and `$1..$N` for Postgres
from the same code path. Behavior is otherwise unchanged: the migration
remains additive, only fills NULL columns, and stays idempotent.

Verified by running the full migration chain against both SQLite and a
real PostgreSQL 16 instance via the existing migration test harness.
…series table

Surface volume and chapter directly on the native BookDto so listings can
classify books without fetching full metadata. The series detail table view
gains a "Kind" column rendering a BookKindBadge, and long titles now
ellipsis-truncate with a tooltip instead of wrapping across rows.

The badge collapses to two semantic colors:
- blue  = volume only (a complete volume)
- grape = chapter (with or without a parent volume — a chapter is a chapter)

The Book Info modal mirrors the same Volume/Chapter rows and Classification
badge, and reads the fields straight off Book now that they're on BookDto.

Tests added for the new badge color rules, the modal classification rows,
and the table behavior; existing books/series API integration tests still
pass.
Update the MSW mock factories, store, and handlers so the new BookDto
volume/chapter and SeriesDto localMaxVolume/localMaxChapter/volumesOwned
fields surface in the dev-mode UI:

- createBook derives volume/chapter from a series-type heuristic so manga
  series mix complete volumes and loose chapters in the same series
- createSeries seeds the local aggregates and the store recomputes them
  from the books actually created
- toFullBookResponse passes volume/chapter through on both the outer body
  and the metadata block, and adds chapterLock to the locks block
@AshDevFr AshDevFr force-pushed the metadata-count-split branch from 2d1f16f to 2b8f979 Compare May 3, 2026 06:12
@AshDevFr AshDevFr merged commit e11cd30 into main May 3, 2026
17 checks passed
@AshDevFr AshDevFr deleted the metadata-count-split branch May 3, 2026 16:43
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