feat(search): tag-based page search surface + per-page tags & collections
Add the /search surface (Pages / Chapters / Mangas tabs) backed by per-user page tags and per-page collections: schema (migration 0023), backend endpoints for page tags/collections and tagged-page aggregations (with the OCR text-search param reserved at 501), plus the frontend API clients, library Page-tags tab, collection page sections, page context menu / AddTagsSheet, and reader long-press wiring. Includes the continuous-reader navigation fixes (?page=N handling, chapter-reset timing, back-button pops history) and tag-normalization hardening accumulated on the branch. Bump version 0.60.2 -> 0.62.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,16 @@ pub enum AppError {
|
||||
message: String,
|
||||
details: serde_json::Value,
|
||||
},
|
||||
/// 501 — the wire shape is accepted but the feature isn't built yet.
|
||||
/// Carries a `&'static str` snake_case code so clients can detect
|
||||
/// the specific pending feature (`text_search_not_yet_supported`,
|
||||
/// etc.) without parsing the message. Used today by the `?text=`
|
||||
/// reservation on the page-tag aggregation endpoints.
|
||||
#[error("not implemented: {code}")]
|
||||
NotImplemented {
|
||||
code: &'static str,
|
||||
message: &'static str,
|
||||
},
|
||||
#[error(transparent)]
|
||||
Database(#[from] sqlx::Error),
|
||||
#[error(transparent)]
|
||||
@@ -64,6 +74,7 @@ impl AppError {
|
||||
AppError::ServiceUnavailable(_) => "service_unavailable",
|
||||
AppError::TooManyRequests { .. } => "too_many_requests",
|
||||
AppError::ValidationFailed { .. } => "validation_failed",
|
||||
AppError::NotImplemented { code, .. } => code,
|
||||
AppError::Database(sqlx::Error::RowNotFound) => "not_found",
|
||||
AppError::Database(_) => "internal_error",
|
||||
AppError::Storage(StorageError::NotFound) => "not_found",
|
||||
@@ -124,6 +135,11 @@ impl IntoResponse for AppError {
|
||||
message.clone(),
|
||||
Some(details.clone()),
|
||||
),
|
||||
AppError::NotImplemented { message, .. } => (
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
(*message).to_string(),
|
||||
None,
|
||||
),
|
||||
AppError::Database(sqlx::Error::RowNotFound) => {
|
||||
(StatusCode::NOT_FOUND, "not found".to_string(), None)
|
||||
}
|
||||
@@ -180,5 +196,15 @@ mod tests {
|
||||
assert_eq!(AppError::Storage(StorageError::NotFound).code(), "not_found");
|
||||
assert_eq!(AppError::Database(sqlx::Error::RowNotFound).code(), "not_found");
|
||||
assert_eq!(AppError::Other(anyhow::anyhow!("oops")).code(), "internal_error");
|
||||
// NotImplemented carries the code through so each pending
|
||||
// feature gets its own stable identifier on the wire.
|
||||
assert_eq!(
|
||||
AppError::NotImplemented {
|
||||
code: "text_search_not_yet_supported",
|
||||
message: "x"
|
||||
}
|
||||
.code(),
|
||||
"text_search_not_yet_supported"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user