diff --git a/.cargo/config.toml b/.cargo/config.toml index cfe2acd11..0ac6b72a1 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -10,20 +10,17 @@ # Linker Configuration # ============================================================================= # Using faster linkers significantly reduces link time during development. -# - macOS: lld (install with: brew install lld) # - Linux: mold (install with: apt install mold) # # If the linker is not installed, Cargo will fall back to the default linker. +# +# macOS uses the system linker. lld is not an option here: against the macOS 27 +# SDK it fails to resolve libSystem primitives (bzero, memset, lchown, fchown), +# so every binary and test link dies with hundreds of undefined symbols. This is +# not a version lag that an upgrade clears, and Apple's ld is fast enough that +# the lost link time does not justify a broken build. # ============================================================================= -# macOS (Apple Silicon) -[target.aarch64-apple-darwin] -rustflags = ["-C", "link-arg=-fuse-ld=lld"] - -# macOS (Intel) -[target.x86_64-apple-darwin] -rustflags = ["-C", "link-arg=-fuse-ld=lld"] - # Linux (x86_64) - used in CI [target.x86_64-unknown-linux-gnu] linker = "clang" diff --git a/crates/codex-api/src/routes/v1/dto/reading_progress_transfer.rs b/crates/codex-api/src/routes/v1/dto/reading_progress_transfer.rs index b7506386c..7ca26bb47 100644 --- a/crates/codex-api/src/routes/v1/dto/reading_progress_transfer.rs +++ b/crates/codex-api/src/routes/v1/dto/reading_progress_transfer.rs @@ -17,6 +17,7 @@ fn default_true() -> bool { /// Query parameters for `GET /api/v1/reading-progress/export`. #[derive(Debug, Clone, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] pub struct ExportReadingProgressQuery { /// Sessions are opt-out: they are the only source of every reading /// statistic, so leaving them out is easy to do by accident and hard to diff --git a/crates/codex-api/src/routes/v1/handlers/reading_progress_transfer.rs b/crates/codex-api/src/routes/v1/handlers/reading_progress_transfer.rs index 79e6c711e..4da111cf6 100644 --- a/crates/codex-api/src/routes/v1/handlers/reading_progress_transfer.rs +++ b/crates/codex-api/src/routes/v1/handlers/reading_progress_transfer.rs @@ -40,7 +40,7 @@ use codex_services::reading_transfer::import::{ImportError, ImportOptions}; get, path = "/api/v1/reading-progress/export", params( - ("include_sessions" = Option, Query, description = "Include the reading-session log (default: true). Sessions are the only source of every reading statistic, so this is opt-out rather than opt-in.") + ("includeSessions" = Option, Query, description = "Include the reading-session log (default: true). Sessions are the only source of every reading statistic, so this is opt-out rather than opt-in.") ), responses( (status = 200, description = "The export document", body = codex_services::reading_transfer::model::ReadingProgressExportDocument), @@ -89,8 +89,8 @@ pub async fn export_reading_progress( /// ever written against it. /// /// Matching never guesses: any step (external id, path, file name, or, under -/// `hash_mode = "match"`, hash) that finds more than one candidate reports -/// `ambiguous` and writes nothing for that series or book. `dry_run: true` +/// `hashMode = "match"`, hash) that finds more than one candidate reports +/// `ambiguous` and writes nothing for that series or book. `dryRun: true` /// returns the identical response shape without writing anything, which is /// what makes it safe to preview before committing. /// diff --git a/crates/codex-services/src/reading_transfer/model.rs b/crates/codex-services/src/reading_transfer/model.rs index 6fcc4ca52..b6b1f045b 100644 --- a/crates/codex-services/src/reading_transfer/model.rs +++ b/crates/codex-services/src/reading_transfer/model.rs @@ -9,12 +9,13 @@ //! API layer only serializes what the service already built. `codex-api` //! re-exports this module under its `dto` namespace for OpenAPI registration. //! -//! The document itself is a portable file meant to survive a library -//! reorganisation or a move to a different Codex instance, so its field names -//! are deliberately **not** camelCased like the rest of the API's DTOs: it is -//! a versioned interchange format rather than a shape a frontend deserializes, -//! and plain `snake_case` matches what anyone writing a compatible tool would -//! expect from the documented shape. +//! The document is a portable file meant to survive a library reorganisation +//! or a move to a different Codex instance, but it is an ordinary API payload +//! too: the export returns it as a response body and the import nests it in +//! its request. So the structs here are `camelCase` on the wire like every +//! other DTO, and a single import body does not carry two naming conventions +//! with an invisible boundary between them. The enums stay `snake_case`, +//! matching how enum variants are spelled everywhere else in the API. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -29,6 +30,7 @@ pub const READING_PROGRESS_VERSION: i32 = 1; /// One external identifier attached to a series (a plugin match, a ComicInfo /// value, or a manual entry). #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] pub struct ExportExternalIdDto { /// `plugin:`, `comicinfo`, `epub`, or `manual`. #[schema(example = "plugin:mangabaka")] @@ -40,6 +42,7 @@ pub struct ExportExternalIdDto { /// The live resume position for one book. Retains `r2_progression`: it is the /// only place the EPUB locator survives, since sessions strip it. #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] pub struct ExportProgressDto { pub current_page: i32, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -56,6 +59,7 @@ pub struct ExportProgressDto { /// One finished read-through. Keeps its original id so re-importing the same /// file is a no-op rather than a duplicate. #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] pub struct ExportCompletionDto { pub id: Uuid, pub started_at: DateTime, @@ -66,6 +70,7 @@ pub struct ExportCompletionDto { /// absent: nothing reads a session's historical locator, and it is the only /// non-scalar column on the row. #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] pub struct ExportSessionDto { pub id: Uuid, pub device_id: String, @@ -95,6 +100,7 @@ pub struct ExportSessionDto { /// rather than by id: the whole point of the file is that ids on the far side /// are expected to be different. #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] pub struct ExportBookDto { /// Relative to the series folder, so a series move does not invalidate it. #[schema(example = "Vol 01/v01.cbz")] @@ -112,13 +118,14 @@ pub struct ExportBookDto { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub completions: Vec, /// Omitted entirely (not an empty array) when the export was taken with - /// `include_sessions=false`. + /// `includeSessions=false`. #[serde(default, skip_serializing_if = "Option::is_none")] pub sessions: Option>, } /// One series and everything the exporting user recorded against its books. #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] pub struct ExportSeriesDto { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub external_ids: Vec, @@ -143,6 +150,7 @@ pub struct ExportSeriesDto { /// The whole export: one user's reading state, self-describing enough to be /// matched back against a differently organised library. #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] pub struct ReadingProgressExportDocument { #[schema(example = "codex-reading-progress")] pub format: String, @@ -190,6 +198,7 @@ fn default_reattach_sessions() -> bool { /// `POST /api/v1/reading-progress/import` request body. #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] pub struct ImportReadingProgressRequest { /// Compute and report the outcome without writing anything. #[serde(default)] @@ -236,12 +245,12 @@ pub enum SeriesDisposition { pub enum BookDisposition { Matched, /// Matched only by filename stem (e.g. a `.cbr` repacked to `.cbz`). - /// Applied only when `accept_stem_matches` is set. + /// Applied only when `acceptStemMatches` is set. StemMatch, Ambiguous, Unmatched, /// A single candidate was found by path or name, but its `file_hash` - /// disagreed with the export under `hash_mode = "verify"` or `"match"`. + /// disagreed with the export under `hashMode = "verify"` or `"match"`. HashMismatch, } @@ -259,6 +268,7 @@ pub enum FieldOutcome { /// Insert/reattach/skip counts for an append-only table (completions or /// sessions) within one book. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] pub struct WriteCounts { pub inserted: u32, /// Adopted an orphaned row (`book_id IS NULL`) rather than inserting a @@ -270,6 +280,7 @@ pub struct WriteCounts { /// The outcome for one book in the import file. #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] pub struct ImportBookReport { pub path: String, pub file_name: String, @@ -278,7 +289,7 @@ pub struct ImportBookReport { pub matched_book_id: Option, /// Whether any writes were attempted for this book. False for every /// disposition except `matched`, and except `stem_match` when - /// `accept_stem_matches` is off. + /// `acceptStemMatches` is off. pub applied: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub progress: Option, @@ -288,6 +299,7 @@ pub struct ImportBookReport { /// The outcome for one series in the import file. #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] pub struct ImportSeriesReport { pub library_relative_path: String, pub name: String, @@ -309,6 +321,7 @@ pub struct ImportSeriesReport { /// Totals across every series in the file, for a one-line summary. #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] pub struct ImportSummary { pub series_total: u32, pub series_matched: u32, @@ -331,12 +344,13 @@ pub struct ImportSummary { /// The response for both a real import and a dry run: the shape is identical /// either way, so a client cannot tell from the response alone whether -/// anything was written. Only `dry_run` (and the DB) says that. +/// anything was written. Only `dryRun` (and the DB) says that. #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] pub struct ImportReadingProgressResponse { pub dry_run: bool, pub sessions_in_file: bool, - /// Informational notes about the request, e.g. `reattach_sessions` having + /// Informational notes about the request, e.g. `reattachSessions` having /// no effect because the file carries no sessions. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub notices: Vec, diff --git a/docs/api/openapi.json b/docs/api/openapi.json index d23780206..c8ab3d9d8 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -10247,7 +10247,7 @@ "operationId": "export_reading_progress", "parameters": [ { - "name": "include_sessions", + "name": "includeSessions", "in": "query", "description": "Include the reading-session log (default: true). Sessions are the only source of every reading statistic, so this is opt-out rather than opt-in.", "required": false, @@ -10290,7 +10290,7 @@ "Reading Progress Transfer" ], "summary": "Import reading progress from an export document", - "description": "Resolves the file's series and books against the current library through\nthe same access-group / sharing-tag visibility that an ordinary read\nrespects: a book the importing user cannot see resolves as `unmatched`,\nnever as a permission error that would confirm it exists, and nothing is\never written against it.\n\nMatching never guesses: any step (external id, path, file name, or, under\n`hash_mode = \"match\"`, hash) that finds more than one candidate reports\n`ambiguous` and writes nothing for that series or book. `dry_run: true`\nreturns the identical response shape without writing anything, which is\nwhat makes it safe to preview before committing.\n\nEach series is applied in its own transaction; the response's per-series\n`committed` field says which ones actually landed. `read_completions` and\n`reading_sessions` reuse their exported ids on insert, so importing the\nsame file twice leaves row counts unchanged rather than duplicating\nhistory.", + "description": "Resolves the file's series and books against the current library through\nthe same access-group / sharing-tag visibility that an ordinary read\nrespects: a book the importing user cannot see resolves as `unmatched`,\nnever as a permission error that would confirm it exists, and nothing is\never written against it.\n\nMatching never guesses: any step (external id, path, file name, or, under\n`hashMode = \"match\"`, hash) that finds more than one candidate reports\n`ambiguous` and writes nothing for that series or book. `dryRun: true`\nreturns the identical response shape without writing anything, which is\nwhat makes it safe to preview before committing.\n\nEach series is applied in its own transaction; the response's per-series\n`committed` field says which ones actually landed. `read_completions` and\n`reading_sessions` reuse their exported ids on insert, so importing the\nsame file twice leaves row counts unchanged rather than duplicating\nhistory.", "operationId": "import_reading_progress", "requestBody": { "content": { @@ -30663,7 +30663,7 @@ "description": "One book inside a series, keyed for matching by path, name, and hash\nrather than by id: the whole point of the file is that ids on the far side\nare expected to be different.", "required": [ "path", - "file_name" + "fileName" ], "properties": { "completions": { @@ -30672,15 +30672,15 @@ "$ref": "#/components/schemas/ExportCompletionDto" } }, - "file_hash": { + "fileHash": { "type": "string", "description": "Empty when the book was never analyzed; never treated as a value to\nmatch on in that case." }, - "file_name": { + "fileName": { "type": "string", "example": "v01.cbz" }, - "partial_hash": { + "partialHash": { "type": "string" }, "path": { @@ -30699,7 +30699,7 @@ "items": { "$ref": "#/components/schemas/ExportSessionDto" }, - "description": "Omitted entirely (not an empty array) when the export was taken with\n`include_sessions=false`." + "description": "Omitted entirely (not an empty array) when the export was taken with\n`includeSessions=false`." } } }, @@ -30708,11 +30708,11 @@ "description": "One finished read-through. Keeps its original id so re-importing the same\nfile is a no-op rather than a duplicate.", "required": [ "id", - "started_at", - "completed_at" + "startedAt", + "completedAt" ], "properties": { - "completed_at": { + "completedAt": { "type": "string", "format": "date-time" }, @@ -30720,7 +30720,7 @@ "type": "string", "format": "uuid" }, - "started_at": { + "startedAt": { "type": "string", "format": "date-time" } @@ -30830,44 +30830,44 @@ "type": "object", "description": "The live resume position for one book. Retains `r2_progression`: it is the\nonly place the EPUB locator survives, since sessions strip it.", "required": [ - "current_page", + "currentPage", "completed", - "started_at", - "updated_at" + "startedAt", + "updatedAt" ], "properties": { "completed": { "type": "boolean" }, - "completed_at": { + "completedAt": { "type": [ "string", "null" ], "format": "date-time" }, - "current_page": { + "currentPage": { "type": "integer", "format": "int32" }, - "progress_percentage": { + "progressPercentage": { "type": [ "number", "null" ], "format": "double" }, - "r2_progression": { + "r2Progression": { "type": [ "string", "null" ] }, - "started_at": { + "startedAt": { "type": "string", "format": "date-time" }, - "updated_at": { + "updatedAt": { "type": "string", "format": "date-time" } @@ -30877,7 +30877,7 @@ "type": "object", "description": "Query parameters for `GET /api/v1/reading-progress/export`.", "properties": { - "include_sessions": { + "includeSessions": { "type": "boolean", "description": "Sessions are opt-out: they are the only source of every reading\nstatistic, so leaving them out is easy to do by accident and hard to\nnotice until the numbers are gone." } @@ -30887,7 +30887,7 @@ "type": "object", "description": "One series and everything the exporting user recorded against its books.", "required": [ - "library_relative_path", + "libraryRelativePath", "name" ], "properties": { @@ -30897,13 +30897,13 @@ "$ref": "#/components/schemas/ExportBookDto" } }, - "external_ids": { + "externalIds": { "type": "array", "items": { "$ref": "#/components/schemas/ExportExternalIdDto" } }, - "library_relative_path": { + "libraryRelativePath": { "type": "string", "description": "The series path as stored, relative to the library root.", "example": "shonen/Naruto" @@ -30925,7 +30925,7 @@ ], "format": "int32" }, - "rating_updated_at": { + "ratingUpdatedAt": { "type": [ "string", "null" @@ -30940,40 +30940,40 @@ "description": "One row from the reading-session log. `r2_progression` is deliberately\nabsent: nothing reads a session's historical locator, and it is the only\nnon-scalar column on the row.", "required": [ "id", - "device_id", + "deviceId", "pass", "kind", - "duration_source", - "client_started_at", - "client_ended_at", - "server_recorded_at" + "durationSource", + "clientStartedAt", + "clientEndedAt", + "serverRecordedAt" ], "properties": { - "active_duration_ms": { + "activeDurationMs": { "type": [ "integer", "null" ], "format": "int64" }, - "client_ended_at": { + "clientEndedAt": { "type": "string", "format": "date-time" }, - "client_started_at": { + "clientStartedAt": { "type": "string", "format": "date-time" }, - "device_id": { + "deviceId": { "type": "string" }, - "device_name": { + "deviceName": { "type": [ "string", "null" ] }, - "duration_source": { + "durationSource": { "type": "string", "description": "`\"measured\"`, `\"inferred\"`, or `\"unknown\"`.", "example": "measured" @@ -30987,7 +30987,7 @@ "description": "`\"progress\"`, `\"completed\"`, or `\"reset\"`.", "example": "progress" }, - "pages_read": { + "pagesRead": { "type": [ "integer", "null" @@ -30998,18 +30998,18 @@ "type": "integer", "format": "int32" }, - "server_recorded_at": { + "serverRecordedAt": { "type": "string", "format": "date-time" }, - "to_page": { + "toPage": { "type": [ "integer", "null" ], "format": "int32" }, - "to_percentage": { + "toPercentage": { "type": [ "number", "null" @@ -32501,7 +32501,7 @@ "description": "The outcome for one book in the import file.", "required": [ "path", - "file_name", + "fileName", "disposition", "applied", "completions", @@ -32510,7 +32510,7 @@ "properties": { "applied": { "type": "boolean", - "description": "Whether any writes were attempted for this book. False for every\ndisposition except `matched`, and except `stem_match` when\n`accept_stem_matches` is off." + "description": "Whether any writes were attempted for this book. False for every\ndisposition except `matched`, and except `stem_match` when\n`acceptStemMatches` is off." }, "completions": { "$ref": "#/components/schemas/WriteCounts" @@ -32518,10 +32518,10 @@ "disposition": { "$ref": "#/components/schemas/BookDisposition" }, - "file_name": { + "fileName": { "type": "string" }, - "matched_book_id": { + "matchedBookId": { "type": [ "string", "null" @@ -32546,28 +32546,28 @@ "file" ], "properties": { - "accept_stem_matches": { + "acceptStemMatches": { "type": "boolean", "description": "A stem match (`v01.cbr` renamed to `v01.cbz`) is reported either way,\nbut only written when this is set: two files can share a stem, and\napplying it silently risks writing progress onto the wrong one." }, - "conflict_policy": { + "conflictPolicy": { "$ref": "#/components/schemas/ConflictPolicy" }, - "dry_run": { + "dryRun": { "type": "boolean", "description": "Compute and report the outcome without writing anything." }, "file": { "$ref": "#/components/schemas/ReadingProgressExportDocument" }, - "hash_mode": { + "hashMode": { "$ref": "#/components/schemas/HashMode" }, - "reattach_sessions": { + "reattachSessions": { "type": "boolean", "description": "When a session or completion in the file already exists as the\nimporter's own row but is not on a live book (its book was hard-deleted,\nleaving `book_id` null, or the scanner marked it deleted after the file\nmoved), move it onto the matched book instead of skipping it." }, - "source_preference": { + "sourcePreference": { "type": "array", "items": { "type": "string" @@ -32578,15 +32578,15 @@ }, "ImportReadingProgressResponse": { "type": "object", - "description": "The response for both a real import and a dry run: the shape is identical\neither way, so a client cannot tell from the response alone whether\nanything was written. Only `dry_run` (and the DB) says that.", + "description": "The response for both a real import and a dry run: the shape is identical\neither way, so a client cannot tell from the response alone whether\nanything was written. Only `dryRun` (and the DB) says that.", "required": [ - "dry_run", - "sessions_in_file", + "dryRun", + "sessionsInFile", "summary", "series" ], "properties": { - "dry_run": { + "dryRun": { "type": "boolean" }, "notices": { @@ -32594,7 +32594,7 @@ "items": { "type": "string" }, - "description": "Informational notes about the request, e.g. `reattach_sessions` having\nno effect because the file carries no sessions." + "description": "Informational notes about the request, e.g. `reattachSessions` having\nno effect because the file carries no sessions." }, "series": { "type": "array", @@ -32602,7 +32602,7 @@ "$ref": "#/components/schemas/ImportSeriesReport" } }, - "sessions_in_file": { + "sessionsInFile": { "type": "boolean" }, "summary": { @@ -32614,7 +32614,7 @@ "type": "object", "description": "The outcome for one series in the import file.", "required": [ - "library_relative_path", + "libraryRelativePath", "name", "disposition", "attempted", @@ -32645,10 +32645,10 @@ "null" ] }, - "library_relative_path": { + "libraryRelativePath": { "type": "string" }, - "matched_series_id": { + "matchedSeriesId": { "type": [ "string", "null" @@ -32667,106 +32667,106 @@ "type": "object", "description": "Totals across every series in the file, for a one-line summary.", "required": [ - "series_total", - "series_matched", - "series_ambiguous", - "series_unmatched", - "series_committed", - "books_total", - "books_matched", - "books_stem_matched", - "books_ambiguous", - "books_unmatched", - "books_hash_mismatch", - "progress_written", - "ratings_written", - "completions_inserted", - "completions_reattached", - "sessions_inserted", - "sessions_reattached" - ], - "properties": { - "books_ambiguous": { + "seriesTotal", + "seriesMatched", + "seriesAmbiguous", + "seriesUnmatched", + "seriesCommitted", + "booksTotal", + "booksMatched", + "booksStemMatched", + "booksAmbiguous", + "booksUnmatched", + "booksHashMismatch", + "progressWritten", + "ratingsWritten", + "completionsInserted", + "completionsReattached", + "sessionsInserted", + "sessionsReattached" + ], + "properties": { + "booksAmbiguous": { "type": "integer", "format": "int32", "minimum": 0 }, - "books_hash_mismatch": { + "booksHashMismatch": { "type": "integer", "format": "int32", "minimum": 0 }, - "books_matched": { + "booksMatched": { "type": "integer", "format": "int32", "minimum": 0 }, - "books_stem_matched": { + "booksStemMatched": { "type": "integer", "format": "int32", "minimum": 0 }, - "books_total": { + "booksTotal": { "type": "integer", "format": "int32", "minimum": 0 }, - "books_unmatched": { + "booksUnmatched": { "type": "integer", "format": "int32", "minimum": 0 }, - "completions_inserted": { + "completionsInserted": { "type": "integer", "format": "int32", "minimum": 0 }, - "completions_reattached": { + "completionsReattached": { "type": "integer", "format": "int32", "minimum": 0 }, - "progress_written": { + "progressWritten": { "type": "integer", "format": "int32", "minimum": 0 }, - "ratings_written": { + "ratingsWritten": { "type": "integer", "format": "int32", "minimum": 0 }, - "series_ambiguous": { + "seriesAmbiguous": { "type": "integer", "format": "int32", "minimum": 0 }, - "series_committed": { + "seriesCommitted": { "type": "integer", "format": "int32", "minimum": 0 }, - "series_matched": { + "seriesMatched": { "type": "integer", "format": "int32", "minimum": 0 }, - "series_total": { + "seriesTotal": { "type": "integer", "format": "int32", "minimum": 0 }, - "series_unmatched": { + "seriesUnmatched": { "type": "integer", "format": "int32", "minimum": 0 }, - "sessions_inserted": { + "sessionsInserted": { "type": "integer", "format": "int32", "minimum": 0 }, - "sessions_reattached": { + "sessionsReattached": { "type": "integer", "format": "int32", "minimum": 0 @@ -40020,11 +40020,11 @@ "required": [ "format", "version", - "exported_at", - "includes_sessions" + "exportedAt", + "includesSessions" ], "properties": { - "exported_at": { + "exportedAt": { "type": "string", "format": "date-time" }, @@ -40032,7 +40032,7 @@ "type": "string", "example": "codex-reading-progress" }, - "includes_sessions": { + "includesSessions": { "type": "boolean" }, "series": { diff --git a/docs/docs/backup-migration/reading-progress-transfer.md b/docs/docs/backup-migration/reading-progress-transfer.md index 361d6a655..682c312f3 100644 --- a/docs/docs/backup-migration/reading-progress-transfer.md +++ b/docs/docs/backup-migration/reading-progress-transfer.md @@ -48,7 +48,7 @@ every reader to re-mark their progress by hand. `POST /api/v1/reading-progress/import`). Run it once as a **dry run** first: it returns the identical report shape without writing anything, so you can check the match quality before committing. -4. **Apply.** Re-run with `dry_run: false` (the Settings page gates this +4. **Apply.** Re-run with `dryRun: false` (the Settings page gates this behind a successful preview). Progress, completions, sessions, and ratings land on the new books and series. @@ -63,7 +63,7 @@ Each series in the file is resolved against the current library, in order, stopping at the first step that finds anything: 1. An external id (a plugin match, ComicInfo, or a manual entry), tried in the - order given by `source_preference`. + order given by `sourcePreference`. 2. The series' path, relative to its library root. 3. The series' normalized name. @@ -73,7 +73,7 @@ its files moved away) is never a candidate at any step. Books are then resolved within that series, in order: their path relative to the series folder, their file name, and finally their filename stem (which survives a `.cbr` repacked to `.cbz`). A repack always changes the file's -hash, so the stem step never checks hashes. Under `hash_mode: verify`, a path +hash, so the stem step never checks hashes. Under `hashMode: verify`, a path or name match whose hash differs is reported as `hash_mismatch`; that includes a file a tagging tool has rewritten since the export, so use `off` if you re-tagged your collection between export and import. @@ -86,7 +86,7 @@ the first were already in the database. **Nothing is ever guessed.** If a step finds more than one candidate, that series or book is reported as `ambiguous` and nothing is written for it. A stem match is reported as `stem_match` and is only applied if you turn on -`accept_stem_matches`: two files can share a stem, so applying it silently +`acceptStemMatches`: two files can share a stem, so applying it silently risks writing progress onto the wrong one. A series or book you cannot see (denied by a sharing tag, or outside your @@ -98,22 +98,22 @@ that the content exists. | Flag | Default | Effect | |---|---|---| -| `dry_run` | `false` | Report the outcome without writing anything | -| `hash_mode` | `verify` | `off` ignores hashes; `verify` rejects a path/name match whose `file_hash` disagrees; `match` additionally uses `file_hash`/`partial_hash` to find a book when path and name both fail (rescues a bulk rename) | -| `source_preference` | `[]` | External-id sources to try, in order, before falling back to path and name | -| `conflict_policy` | `newest` | How to resolve a book/rating that already has a value on this side: `newest` (later `updated_at` wins), `furthest` (further into the book wins; a finished read always beats a partial one), `skip_existing`, or `overwrite`. A rating has no position, so `furthest` behaves like `newest` for ratings, and a file without a rating timestamp never replaces an existing rating except under `overwrite` | -| `reattach_sessions` | `true` | When a session or completion in the file already exists as your own row but is not on a live book (its book was deleted, or the scanner marked it deleted after the file moved), move it onto the matched book instead of skipping it. A no-op, reported as such, when the file carries no sessions | -| `accept_stem_matches` | `false` | Apply a book match found only by filename stem | +| `dryRun` | `false` | Report the outcome without writing anything | +| `hashMode` | `verify` | `off` ignores hashes; `verify` rejects a path/name match whose `fileHash` disagrees; `match` additionally uses `fileHash`/`partialHash` to find a book when path and name both fail (rescues a bulk rename) | +| `sourcePreference` | `[]` | External-id sources to try, in order, before falling back to path and name | +| `conflictPolicy` | `newest` | How to resolve a book/rating that already has a value on this side: `newest` (later `updatedAt` wins), `furthest` (further into the book wins; a finished read always beats a partial one), `skip_existing`, or `overwrite`. A rating has no position, so `furthest` behaves like `newest` for ratings, and a file without a rating timestamp never replaces an existing rating except under `overwrite` | +| `reattachSessions` | `true` | When a session or completion in the file already exists as your own row but is not on a live book (its book was deleted, or the scanner marked it deleted after the file moved), move it onto the matched book instead of skipping it. A no-op, reported as such, when the file carries no sessions | +| `acceptStemMatches` | `false` | Apply a book match found only by filename stem | `GET /api/v1/reading-progress/export` takes one query parameter, -`include_sessions` (default `true`). Turn it off only if you specifically want +`includeSessions` (default `true`). Turn it off only if you specifically want a smaller file: sessions are the only source of every reading statistic, so leaving them out is easy to do by accident and easy not to notice until the numbers are gone. ## The response -The response is the same shape whether or not `dry_run` is set: counts, plus a +The response is the same shape whether or not `dryRun` is set: counts, plus a per-series and per-book breakdown of what matched, what did not, and what was (or would be) written. Each series is applied in its own transaction, so a bad series does not cost every other series in the file its progress; the diff --git a/tests/api/reading_progress_transfer.rs b/tests/api/reading_progress_transfer.rs index e6639526a..c30c66690 100644 --- a/tests/api/reading_progress_transfer.rs +++ b/tests/api/reading_progress_transfer.rs @@ -301,7 +301,7 @@ async fn exercise_library_split_round_trip(db: &DatabaseConnection) { // --- Export before the split --- let app = create_test_router(state.clone()).await; let request = get_request_with_auth( - "/api/v1/reading-progress/export?include_sessions=true", + "/api/v1/reading-progress/export?includeSessions=true", &token, ); let (status, exported): (StatusCode, Option) = @@ -767,7 +767,7 @@ async fn hard_deleting_a_book_then_reimporting_reattaches_its_orphaned_history() // match back against. let app = create_test_router(state.clone()).await; let request = get_request_with_auth( - "/api/v1/reading-progress/export?include_sessions=true", + "/api/v1/reading-progress/export?includeSessions=true", &token, ); let (status, exported): (StatusCode, Option) = diff --git a/web/openapi.json b/web/openapi.json index d23780206..c8ab3d9d8 100644 --- a/web/openapi.json +++ b/web/openapi.json @@ -10247,7 +10247,7 @@ "operationId": "export_reading_progress", "parameters": [ { - "name": "include_sessions", + "name": "includeSessions", "in": "query", "description": "Include the reading-session log (default: true). Sessions are the only source of every reading statistic, so this is opt-out rather than opt-in.", "required": false, @@ -10290,7 +10290,7 @@ "Reading Progress Transfer" ], "summary": "Import reading progress from an export document", - "description": "Resolves the file's series and books against the current library through\nthe same access-group / sharing-tag visibility that an ordinary read\nrespects: a book the importing user cannot see resolves as `unmatched`,\nnever as a permission error that would confirm it exists, and nothing is\never written against it.\n\nMatching never guesses: any step (external id, path, file name, or, under\n`hash_mode = \"match\"`, hash) that finds more than one candidate reports\n`ambiguous` and writes nothing for that series or book. `dry_run: true`\nreturns the identical response shape without writing anything, which is\nwhat makes it safe to preview before committing.\n\nEach series is applied in its own transaction; the response's per-series\n`committed` field says which ones actually landed. `read_completions` and\n`reading_sessions` reuse their exported ids on insert, so importing the\nsame file twice leaves row counts unchanged rather than duplicating\nhistory.", + "description": "Resolves the file's series and books against the current library through\nthe same access-group / sharing-tag visibility that an ordinary read\nrespects: a book the importing user cannot see resolves as `unmatched`,\nnever as a permission error that would confirm it exists, and nothing is\never written against it.\n\nMatching never guesses: any step (external id, path, file name, or, under\n`hashMode = \"match\"`, hash) that finds more than one candidate reports\n`ambiguous` and writes nothing for that series or book. `dryRun: true`\nreturns the identical response shape without writing anything, which is\nwhat makes it safe to preview before committing.\n\nEach series is applied in its own transaction; the response's per-series\n`committed` field says which ones actually landed. `read_completions` and\n`reading_sessions` reuse their exported ids on insert, so importing the\nsame file twice leaves row counts unchanged rather than duplicating\nhistory.", "operationId": "import_reading_progress", "requestBody": { "content": { @@ -30663,7 +30663,7 @@ "description": "One book inside a series, keyed for matching by path, name, and hash\nrather than by id: the whole point of the file is that ids on the far side\nare expected to be different.", "required": [ "path", - "file_name" + "fileName" ], "properties": { "completions": { @@ -30672,15 +30672,15 @@ "$ref": "#/components/schemas/ExportCompletionDto" } }, - "file_hash": { + "fileHash": { "type": "string", "description": "Empty when the book was never analyzed; never treated as a value to\nmatch on in that case." }, - "file_name": { + "fileName": { "type": "string", "example": "v01.cbz" }, - "partial_hash": { + "partialHash": { "type": "string" }, "path": { @@ -30699,7 +30699,7 @@ "items": { "$ref": "#/components/schemas/ExportSessionDto" }, - "description": "Omitted entirely (not an empty array) when the export was taken with\n`include_sessions=false`." + "description": "Omitted entirely (not an empty array) when the export was taken with\n`includeSessions=false`." } } }, @@ -30708,11 +30708,11 @@ "description": "One finished read-through. Keeps its original id so re-importing the same\nfile is a no-op rather than a duplicate.", "required": [ "id", - "started_at", - "completed_at" + "startedAt", + "completedAt" ], "properties": { - "completed_at": { + "completedAt": { "type": "string", "format": "date-time" }, @@ -30720,7 +30720,7 @@ "type": "string", "format": "uuid" }, - "started_at": { + "startedAt": { "type": "string", "format": "date-time" } @@ -30830,44 +30830,44 @@ "type": "object", "description": "The live resume position for one book. Retains `r2_progression`: it is the\nonly place the EPUB locator survives, since sessions strip it.", "required": [ - "current_page", + "currentPage", "completed", - "started_at", - "updated_at" + "startedAt", + "updatedAt" ], "properties": { "completed": { "type": "boolean" }, - "completed_at": { + "completedAt": { "type": [ "string", "null" ], "format": "date-time" }, - "current_page": { + "currentPage": { "type": "integer", "format": "int32" }, - "progress_percentage": { + "progressPercentage": { "type": [ "number", "null" ], "format": "double" }, - "r2_progression": { + "r2Progression": { "type": [ "string", "null" ] }, - "started_at": { + "startedAt": { "type": "string", "format": "date-time" }, - "updated_at": { + "updatedAt": { "type": "string", "format": "date-time" } @@ -30877,7 +30877,7 @@ "type": "object", "description": "Query parameters for `GET /api/v1/reading-progress/export`.", "properties": { - "include_sessions": { + "includeSessions": { "type": "boolean", "description": "Sessions are opt-out: they are the only source of every reading\nstatistic, so leaving them out is easy to do by accident and hard to\nnotice until the numbers are gone." } @@ -30887,7 +30887,7 @@ "type": "object", "description": "One series and everything the exporting user recorded against its books.", "required": [ - "library_relative_path", + "libraryRelativePath", "name" ], "properties": { @@ -30897,13 +30897,13 @@ "$ref": "#/components/schemas/ExportBookDto" } }, - "external_ids": { + "externalIds": { "type": "array", "items": { "$ref": "#/components/schemas/ExportExternalIdDto" } }, - "library_relative_path": { + "libraryRelativePath": { "type": "string", "description": "The series path as stored, relative to the library root.", "example": "shonen/Naruto" @@ -30925,7 +30925,7 @@ ], "format": "int32" }, - "rating_updated_at": { + "ratingUpdatedAt": { "type": [ "string", "null" @@ -30940,40 +30940,40 @@ "description": "One row from the reading-session log. `r2_progression` is deliberately\nabsent: nothing reads a session's historical locator, and it is the only\nnon-scalar column on the row.", "required": [ "id", - "device_id", + "deviceId", "pass", "kind", - "duration_source", - "client_started_at", - "client_ended_at", - "server_recorded_at" + "durationSource", + "clientStartedAt", + "clientEndedAt", + "serverRecordedAt" ], "properties": { - "active_duration_ms": { + "activeDurationMs": { "type": [ "integer", "null" ], "format": "int64" }, - "client_ended_at": { + "clientEndedAt": { "type": "string", "format": "date-time" }, - "client_started_at": { + "clientStartedAt": { "type": "string", "format": "date-time" }, - "device_id": { + "deviceId": { "type": "string" }, - "device_name": { + "deviceName": { "type": [ "string", "null" ] }, - "duration_source": { + "durationSource": { "type": "string", "description": "`\"measured\"`, `\"inferred\"`, or `\"unknown\"`.", "example": "measured" @@ -30987,7 +30987,7 @@ "description": "`\"progress\"`, `\"completed\"`, or `\"reset\"`.", "example": "progress" }, - "pages_read": { + "pagesRead": { "type": [ "integer", "null" @@ -30998,18 +30998,18 @@ "type": "integer", "format": "int32" }, - "server_recorded_at": { + "serverRecordedAt": { "type": "string", "format": "date-time" }, - "to_page": { + "toPage": { "type": [ "integer", "null" ], "format": "int32" }, - "to_percentage": { + "toPercentage": { "type": [ "number", "null" @@ -32501,7 +32501,7 @@ "description": "The outcome for one book in the import file.", "required": [ "path", - "file_name", + "fileName", "disposition", "applied", "completions", @@ -32510,7 +32510,7 @@ "properties": { "applied": { "type": "boolean", - "description": "Whether any writes were attempted for this book. False for every\ndisposition except `matched`, and except `stem_match` when\n`accept_stem_matches` is off." + "description": "Whether any writes were attempted for this book. False for every\ndisposition except `matched`, and except `stem_match` when\n`acceptStemMatches` is off." }, "completions": { "$ref": "#/components/schemas/WriteCounts" @@ -32518,10 +32518,10 @@ "disposition": { "$ref": "#/components/schemas/BookDisposition" }, - "file_name": { + "fileName": { "type": "string" }, - "matched_book_id": { + "matchedBookId": { "type": [ "string", "null" @@ -32546,28 +32546,28 @@ "file" ], "properties": { - "accept_stem_matches": { + "acceptStemMatches": { "type": "boolean", "description": "A stem match (`v01.cbr` renamed to `v01.cbz`) is reported either way,\nbut only written when this is set: two files can share a stem, and\napplying it silently risks writing progress onto the wrong one." }, - "conflict_policy": { + "conflictPolicy": { "$ref": "#/components/schemas/ConflictPolicy" }, - "dry_run": { + "dryRun": { "type": "boolean", "description": "Compute and report the outcome without writing anything." }, "file": { "$ref": "#/components/schemas/ReadingProgressExportDocument" }, - "hash_mode": { + "hashMode": { "$ref": "#/components/schemas/HashMode" }, - "reattach_sessions": { + "reattachSessions": { "type": "boolean", "description": "When a session or completion in the file already exists as the\nimporter's own row but is not on a live book (its book was hard-deleted,\nleaving `book_id` null, or the scanner marked it deleted after the file\nmoved), move it onto the matched book instead of skipping it." }, - "source_preference": { + "sourcePreference": { "type": "array", "items": { "type": "string" @@ -32578,15 +32578,15 @@ }, "ImportReadingProgressResponse": { "type": "object", - "description": "The response for both a real import and a dry run: the shape is identical\neither way, so a client cannot tell from the response alone whether\nanything was written. Only `dry_run` (and the DB) says that.", + "description": "The response for both a real import and a dry run: the shape is identical\neither way, so a client cannot tell from the response alone whether\nanything was written. Only `dryRun` (and the DB) says that.", "required": [ - "dry_run", - "sessions_in_file", + "dryRun", + "sessionsInFile", "summary", "series" ], "properties": { - "dry_run": { + "dryRun": { "type": "boolean" }, "notices": { @@ -32594,7 +32594,7 @@ "items": { "type": "string" }, - "description": "Informational notes about the request, e.g. `reattach_sessions` having\nno effect because the file carries no sessions." + "description": "Informational notes about the request, e.g. `reattachSessions` having\nno effect because the file carries no sessions." }, "series": { "type": "array", @@ -32602,7 +32602,7 @@ "$ref": "#/components/schemas/ImportSeriesReport" } }, - "sessions_in_file": { + "sessionsInFile": { "type": "boolean" }, "summary": { @@ -32614,7 +32614,7 @@ "type": "object", "description": "The outcome for one series in the import file.", "required": [ - "library_relative_path", + "libraryRelativePath", "name", "disposition", "attempted", @@ -32645,10 +32645,10 @@ "null" ] }, - "library_relative_path": { + "libraryRelativePath": { "type": "string" }, - "matched_series_id": { + "matchedSeriesId": { "type": [ "string", "null" @@ -32667,106 +32667,106 @@ "type": "object", "description": "Totals across every series in the file, for a one-line summary.", "required": [ - "series_total", - "series_matched", - "series_ambiguous", - "series_unmatched", - "series_committed", - "books_total", - "books_matched", - "books_stem_matched", - "books_ambiguous", - "books_unmatched", - "books_hash_mismatch", - "progress_written", - "ratings_written", - "completions_inserted", - "completions_reattached", - "sessions_inserted", - "sessions_reattached" - ], - "properties": { - "books_ambiguous": { + "seriesTotal", + "seriesMatched", + "seriesAmbiguous", + "seriesUnmatched", + "seriesCommitted", + "booksTotal", + "booksMatched", + "booksStemMatched", + "booksAmbiguous", + "booksUnmatched", + "booksHashMismatch", + "progressWritten", + "ratingsWritten", + "completionsInserted", + "completionsReattached", + "sessionsInserted", + "sessionsReattached" + ], + "properties": { + "booksAmbiguous": { "type": "integer", "format": "int32", "minimum": 0 }, - "books_hash_mismatch": { + "booksHashMismatch": { "type": "integer", "format": "int32", "minimum": 0 }, - "books_matched": { + "booksMatched": { "type": "integer", "format": "int32", "minimum": 0 }, - "books_stem_matched": { + "booksStemMatched": { "type": "integer", "format": "int32", "minimum": 0 }, - "books_total": { + "booksTotal": { "type": "integer", "format": "int32", "minimum": 0 }, - "books_unmatched": { + "booksUnmatched": { "type": "integer", "format": "int32", "minimum": 0 }, - "completions_inserted": { + "completionsInserted": { "type": "integer", "format": "int32", "minimum": 0 }, - "completions_reattached": { + "completionsReattached": { "type": "integer", "format": "int32", "minimum": 0 }, - "progress_written": { + "progressWritten": { "type": "integer", "format": "int32", "minimum": 0 }, - "ratings_written": { + "ratingsWritten": { "type": "integer", "format": "int32", "minimum": 0 }, - "series_ambiguous": { + "seriesAmbiguous": { "type": "integer", "format": "int32", "minimum": 0 }, - "series_committed": { + "seriesCommitted": { "type": "integer", "format": "int32", "minimum": 0 }, - "series_matched": { + "seriesMatched": { "type": "integer", "format": "int32", "minimum": 0 }, - "series_total": { + "seriesTotal": { "type": "integer", "format": "int32", "minimum": 0 }, - "series_unmatched": { + "seriesUnmatched": { "type": "integer", "format": "int32", "minimum": 0 }, - "sessions_inserted": { + "sessionsInserted": { "type": "integer", "format": "int32", "minimum": 0 }, - "sessions_reattached": { + "sessionsReattached": { "type": "integer", "format": "int32", "minimum": 0 @@ -40020,11 +40020,11 @@ "required": [ "format", "version", - "exported_at", - "includes_sessions" + "exportedAt", + "includesSessions" ], "properties": { - "exported_at": { + "exportedAt": { "type": "string", "format": "date-time" }, @@ -40032,7 +40032,7 @@ "type": "string", "example": "codex-reading-progress" }, - "includes_sessions": { + "includesSessions": { "type": "boolean" }, "series": { diff --git a/web/src/api/readingProgressTransfer.ts b/web/src/api/readingProgressTransfer.ts index f475dba58..6c4ffddc4 100644 --- a/web/src/api/readingProgressTransfer.ts +++ b/web/src/api/readingProgressTransfer.ts @@ -1,10 +1,10 @@ import type { components } from "@/types/api.generated"; import { api } from "./client"; -// Re-export generated types for convenience. The document, request, and -// report shapes intentionally use snake_case (see the backend's DTO module -// doc comment): this is a portable file format, not an ordinary camelCase -// API response, so the field names here are passed straight through. +// Re-export generated types for convenience. The document is both a portable +// file and an ordinary API payload: the export returns it as a response body +// and the import nests it inside its request, so it uses the same camelCase +// field names as every other endpoint rather than a second convention. export type ReadingProgressExportDocument = components["schemas"]["ReadingProgressExportDocument"]; export type ImportReadingProgressRequest = @@ -28,7 +28,7 @@ export const readingProgressTransferApi = { ): Promise => { const response = await api.get( "/reading-progress/export", - { params: { include_sessions: includeSessions } }, + { params: { includeSessions } }, ); return response.data; }, diff --git a/web/src/pages/settings/ReadingProgressTransferSettings.test.tsx b/web/src/pages/settings/ReadingProgressTransferSettings.test.tsx index 8dd89ff26..35f6a7773 100644 --- a/web/src/pages/settings/ReadingProgressTransferSettings.test.tsx +++ b/web/src/pages/settings/ReadingProgressTransferSettings.test.tsx @@ -25,18 +25,18 @@ vi.mock("@/api/readingProgressTransfer", async () => { const exportDocument: ReadingProgressExportDocument = { format: "codex-reading-progress", version: 1, - exported_at: "2026-09-27T00:00:00Z", - includes_sessions: true, + exportedAt: "2026-09-27T00:00:00Z", + includesSessions: true, series: [ { - external_ids: [], - library_relative_path: "Naruto", + externalIds: [], + libraryRelativePath: "Naruto", name: "Naruto", books: [ { path: "v01.cbz", - file_name: "v01.cbz", - file_hash: "", + fileName: "v01.cbz", + fileHash: "", partial_hash: "", completions: [], }, @@ -47,7 +47,7 @@ const exportDocument: ReadingProgressExportDocument = { function dryRunResponse(): ImportReadingProgressResponse { return { - dry_run: true, + dryRun: true, sessions_in_file: true, notices: [], summary: { @@ -71,7 +71,7 @@ function dryRunResponse(): ImportReadingProgressResponse { }, series: [ { - library_relative_path: "Naruto", + libraryRelativePath: "Naruto", name: "Naruto", disposition: "matched", matched_series_id: "11111111-1111-1111-1111-111111111111", @@ -80,7 +80,7 @@ function dryRunResponse(): ImportReadingProgressResponse { books: [ { path: "v01.cbz", - file_name: "v01.cbz", + fileName: "v01.cbz", disposition: "matched", matched_book_id: "22222222-2222-2222-2222-222222222222", applied: true, @@ -155,7 +155,7 @@ describe("ReadingProgressTransferSettings", () => { expect(applyButton).toBeEnabled(); }); expect(importProgress).toHaveBeenCalledWith( - expect.objectContaining({ dry_run: true }), + expect.objectContaining({ dryRun: true }), ); }); @@ -196,7 +196,7 @@ describe("ReadingProgressTransferSettings", () => { expect(screen.getAllByText("matched").length).toBeGreaterThan(0); }); - it("calls import with dry_run: false when Apply is pressed", async () => { + it("calls import with dryRun: false when Apply is pressed", async () => { importProgress.mockResolvedValue(dryRunResponse()); const user = userEvent.setup(); renderWithProviders(); @@ -213,7 +213,7 @@ describe("ReadingProgressTransferSettings", () => { importProgress.mockResolvedValue({ ...dryRunResponse(), - dry_run: false, + dryRun: false, series: [ { ...dryRunResponse().series[0], @@ -225,7 +225,7 @@ describe("ReadingProgressTransferSettings", () => { await waitFor(() => { expect(importProgress).toHaveBeenLastCalledWith( - expect.objectContaining({ dry_run: false }), + expect.objectContaining({ dryRun: false }), ); }); }); diff --git a/web/src/pages/settings/ReadingProgressTransferSettings.tsx b/web/src/pages/settings/ReadingProgressTransferSettings.tsx index e04d836a4..e3c94f769 100644 --- a/web/src/pages/settings/ReadingProgressTransferSettings.tsx +++ b/web/src/pages/settings/ReadingProgressTransferSettings.tsx @@ -99,29 +99,29 @@ function SummaryLine({ report }: { report: ImportReadingProgressResponse }) { return ( - Series: {summary.series_matched} matched,{" "} - {summary.series_ambiguous} ambiguous,{" "} - {summary.series_unmatched} unmatched - {!report.dry_run && ( + Series: {summary.seriesMatched} matched,{" "} + {summary.seriesAmbiguous} ambiguous,{" "} + {summary.seriesUnmatched} unmatched + {!report.dryRun && ( <> - , {summary.series_committed} committed + , {summary.seriesCommitted} committed )} - Books: {summary.books_matched} matched,{" "} - {summary.books_stem_matched} stem match,{" "} - {summary.books_ambiguous} ambiguous,{" "} - {summary.books_unmatched} unmatched,{" "} - {summary.books_hash_mismatch} hash mismatch + Books: {summary.booksMatched} matched,{" "} + {summary.booksStemMatched} stem match,{" "} + {summary.booksAmbiguous} ambiguous,{" "} + {summary.booksUnmatched} unmatched,{" "} + {summary.booksHashMismatch} hash mismatch - Progress written: {summary.progress_written} · - Completions: {summary.completions_inserted} inserted /{" "} - {summary.completions_reattached} reattached · Sessions:{" "} - {summary.sessions_inserted} inserted /{" "} - {summary.sessions_reattached} reattached · Ratings:{" "} - {summary.ratings_written} + Progress written: {summary.progressWritten} · Completions:{" "} + {summary.completionsInserted} inserted /{" "} + {summary.completionsReattached} reattached · Sessions:{" "} + {summary.sessionsInserted} inserted /{" "} + {summary.sessionsReattached} reattached · Ratings:{" "} + {summary.ratingsWritten} ); @@ -144,14 +144,14 @@ function ReportTable({ report }: { report: ImportReadingProgressResponse }) { // A split exports same-named series from several libraries, so // path and name alone are not unique. {series.name} - {series.library_relative_path} + {series.libraryRelativePath} {series.error && ( @@ -186,13 +186,13 @@ function ReportTable({ report }: { report: ImportReadingProgressResponse }) { {series.books.map((book) => ( - {book.file_name} + {book.fileName} ))} @@ -262,12 +262,12 @@ export function ReadingProgressTransferSettings() { setImportError(null); importMutation.mutate( { - dry_run: dryRun, - hash_mode: hashMode, - source_preference: [], - conflict_policy: conflictPolicy, - reattach_sessions: reattachSessions, - accept_stem_matches: acceptStemMatches, + dryRun, + hashMode, + sourcePreference: [], + conflictPolicy, + reattachSessions, + acceptStemMatches, file: parsedDocument, }, { @@ -455,13 +455,13 @@ export function ReadingProgressTransferSettings() { - {!report.sessions_in_file && ( + {!report.sessionsInFile && ( This file does not include sessions. diff --git a/web/src/types/api.generated.ts b/web/src/types/api.generated.ts index 6f6d83db9..2b3a68a2a 100644 --- a/web/src/types/api.generated.ts +++ b/web/src/types/api.generated.ts @@ -3322,8 +3322,8 @@ export interface paths { * ever written against it. * * Matching never guesses: any step (external id, path, file name, or, under - * `hash_mode = "match"`, hash) that finds more than one candidate reports - * `ambiguous` and writes nothing for that series or book. `dry_run: true` + * `hashMode = "match"`, hash) that finds more than one candidate reports + * `ambiguous` and writes nothing for that series or book. `dryRun: true` * returns the identical response shape without writing anything, which is * what makes it safe to preview before committing. * @@ -12295,10 +12295,10 @@ export interface components { * @description Empty when the book was never analyzed; never treated as a value to * match on in that case. */ - file_hash?: string; + fileHash?: string; /** @example v01.cbz */ - file_name: string; - partial_hash?: string; + fileName: string; + partialHash?: string; /** * @description Relative to the series folder, so a series move does not invalidate it. * @example Vol 01/v01.cbz @@ -12307,7 +12307,7 @@ export interface components { progress?: components["schemas"]["ExportProgressDto"]; /** * @description Omitted entirely (not an empty array) when the export was taken with - * `include_sessions=false`. + * `includeSessions=false`. */ sessions?: components["schemas"]["ExportSessionDto"][] | null; }; @@ -12317,11 +12317,11 @@ export interface components { */ ExportCompletionDto: { /** Format: date-time */ - completed_at: string; + completedAt: string; /** Format: uuid */ id: string; /** Format: date-time */ - started_at: string; + startedAt: string; }; /** * @description One external identifier attached to a series (a plugin match, a ComicInfo @@ -12367,16 +12367,16 @@ export interface components { ExportProgressDto: { completed: boolean; /** Format: date-time */ - completed_at?: string | null; + completedAt?: string | null; /** Format: int32 */ - current_page: number; + currentPage: number; /** Format: double */ - progress_percentage?: number | null; - r2_progression?: string | null; + progressPercentage?: number | null; + r2Progression?: string | null; /** Format: date-time */ - started_at: string; + startedAt: string; /** Format: date-time */ - updated_at: string; + updatedAt: string; }; /** @description Query parameters for `GET /api/v1/reading-progress/export`. */ ExportReadingProgressQuery: { @@ -12385,17 +12385,17 @@ export interface components { * statistic, so leaving them out is easy to do by accident and hard to * notice until the numbers are gone. */ - include_sessions?: boolean; + includeSessions?: boolean; }; /** @description One series and everything the exporting user recorded against its books. */ ExportSeriesDto: { books?: components["schemas"]["ExportBookDto"][]; - external_ids?: components["schemas"]["ExportExternalIdDto"][]; + externalIds?: components["schemas"]["ExportExternalIdDto"][]; /** * @description The series path as stored, relative to the library root. * @example shonen/Naruto */ - library_relative_path: string; + libraryRelativePath: string; /** @example Naruto */ name: string; notes?: string | null; @@ -12407,7 +12407,7 @@ export interface components { * to tell a stale rating from a fresh one; a file without it never * overwrites an existing rating except under `overwrite`. */ - rating_updated_at?: string | null; + ratingUpdatedAt?: string | null; }; /** * @description One row from the reading-session log. `r2_progression` is deliberately @@ -12416,18 +12416,18 @@ export interface components { */ ExportSessionDto: { /** Format: int64 */ - active_duration_ms?: number | null; + activeDurationMs?: number | null; /** Format: date-time */ - client_ended_at: string; + clientEndedAt: string; /** Format: date-time */ - client_started_at: string; - device_id: string; - device_name?: string | null; + clientStartedAt: string; + deviceId: string; + deviceName?: string | null; /** * @description `"measured"`, `"inferred"`, or `"unknown"`. * @example measured */ - duration_source: string; + durationSource: string; /** Format: uuid */ id: string; /** @@ -12436,15 +12436,15 @@ export interface components { */ kind: string; /** Format: int32 */ - pages_read?: number | null; + pagesRead?: number | null; /** Format: int32 */ pass: number; /** Format: date-time */ - server_recorded_at: string; + serverRecordedAt: string; /** Format: int32 */ - to_page?: number | null; + toPage?: number | null; /** Format: double */ - to_percentage?: number | null; + toPercentage?: number | null; }; /** * @description External ID context for template evaluation. @@ -13275,14 +13275,14 @@ export interface components { /** * @description Whether any writes were attempted for this book. False for every * disposition except `matched`, and except `stem_match` when - * `accept_stem_matches` is off. + * `acceptStemMatches` is off. */ applied: boolean; completions: components["schemas"]["WriteCounts"]; disposition: components["schemas"]["BookDisposition"]; - file_name: string; + fileName: string; /** Format: uuid */ - matched_book_id?: string | null; + matchedBookId?: string | null; path: string; progress?: components["schemas"]["FieldOutcome"]; sessions: components["schemas"]["WriteCounts"]; @@ -13294,39 +13294,39 @@ export interface components { * but only written when this is set: two files can share a stem, and * applying it silently risks writing progress onto the wrong one. */ - accept_stem_matches?: boolean; - conflict_policy?: components["schemas"]["ConflictPolicy"]; + acceptStemMatches?: boolean; + conflictPolicy?: components["schemas"]["ConflictPolicy"]; /** @description Compute and report the outcome without writing anything. */ - dry_run?: boolean; + dryRun?: boolean; file: components["schemas"]["ReadingProgressExportDocument"]; - hash_mode?: components["schemas"]["HashMode"]; + hashMode?: components["schemas"]["HashMode"]; /** * @description When a session or completion in the file already exists as the * importer's own row but is not on a live book (its book was hard-deleted, * leaving `book_id` null, or the scanner marked it deleted after the file * moved), move it onto the matched book instead of skipping it. */ - reattach_sessions?: boolean; + reattachSessions?: boolean; /** * @description External-id sources to try, in order, before falling back to path and * then normalized name. An empty list skips straight to path matching. */ - source_preference?: string[]; + sourcePreference?: string[]; }; /** * @description The response for both a real import and a dry run: the shape is identical * either way, so a client cannot tell from the response alone whether - * anything was written. Only `dry_run` (and the DB) says that. + * anything was written. Only `dryRun` (and the DB) says that. */ ImportReadingProgressResponse: { - dry_run: boolean; + dryRun: boolean; /** - * @description Informational notes about the request, e.g. `reattach_sessions` having + * @description Informational notes about the request, e.g. `reattachSessions` having * no effect because the file carries no sessions. */ notices?: string[]; series: components["schemas"]["ImportSeriesReport"][]; - sessions_in_file: boolean; + sessionsInFile: boolean; summary: components["schemas"]["ImportSummary"]; }; /** @description The outcome for one series in the import file. */ @@ -13344,48 +13344,48 @@ export interface components { committed: boolean; disposition: components["schemas"]["SeriesDisposition"]; error?: string | null; - library_relative_path: string; + libraryRelativePath: string; /** Format: uuid */ - matched_series_id?: string | null; + matchedSeriesId?: string | null; name: string; rating?: components["schemas"]["FieldOutcome"]; }; /** @description Totals across every series in the file, for a one-line summary. */ ImportSummary: { /** Format: int32 */ - books_ambiguous: number; + booksAmbiguous: number; /** Format: int32 */ - books_hash_mismatch: number; + booksHashMismatch: number; /** Format: int32 */ - books_matched: number; + booksMatched: number; /** Format: int32 */ - books_stem_matched: number; + booksStemMatched: number; /** Format: int32 */ - books_total: number; + booksTotal: number; /** Format: int32 */ - books_unmatched: number; + booksUnmatched: number; /** Format: int32 */ - completions_inserted: number; + completionsInserted: number; /** Format: int32 */ - completions_reattached: number; + completionsReattached: number; /** Format: int32 */ - progress_written: number; + progressWritten: number; /** Format: int32 */ - ratings_written: number; + ratingsWritten: number; /** Format: int32 */ - series_ambiguous: number; + seriesAmbiguous: number; /** Format: int32 */ - series_committed: number; + seriesCommitted: number; /** Format: int32 */ - series_matched: number; + seriesMatched: number; /** Format: int32 */ - series_total: number; + seriesTotal: number; /** Format: int32 */ - series_unmatched: number; + seriesUnmatched: number; /** Format: int32 */ - sessions_inserted: number; + sessionsInserted: number; /** Format: int32 */ - sessions_reattached: number; + sessionsReattached: number; }; /** * @description Which layer supplied a value the user is inheriting. @@ -17440,10 +17440,10 @@ export interface components { */ ReadingProgressExportDocument: { /** Format: date-time */ - exported_at: string; + exportedAt: string; /** @example codex-reading-progress */ format: string; - includes_sessions: boolean; + includesSessions: boolean; series?: components["schemas"]["ExportSeriesDto"][]; /** * Format: int32 @@ -29776,7 +29776,7 @@ export interface operations { parameters: { query?: { /** @description Include the reading-session log (default: true). Sessions are the only source of every reading statistic, so this is opt-out rather than opt-in. */ - include_sessions?: boolean; + includeSessions?: boolean; }; header?: never; path?: never;