Refactor insights handling and error encoding in presentation layer
All checks were successful
CI / Check / Test (push) Successful in 1h12m41s
CI / Check / Test (pull_request) Successful in 1h6m20s

- Updated insights analyzer tests to use Utc for date handling.
- Added timezone support for insights retrieval, allowing insights to be bucketed by the user's local time.
- Introduced `encode_domain_error` for better error handling in API responses.
- Modified various handlers to utilize the new error encoding function.
- Enhanced insights API to reject unknown time zones and return appropriate errors.
- Updated frontend components to handle insights data with timezone awareness.
- Improved localization for insights sample counts.
This commit is contained in:
2026-10-12 20:44:54 +02:00
parent ee06f4794c
commit 5cb6fd5fbb
28 changed files with 510 additions and 176 deletions

View File

@@ -30,7 +30,7 @@ impl InsightsQuery for PostgresInsightsQuery {
to_char(watched_at AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS') AS watched_at, \
watch_medium \
FROM reviews \
WHERE user_id = $1 \
WHERE user_id = $1 AND remote_actor_url IS NULL \
ORDER BY watched_at ASC",
)
.bind(user_id.value().to_string())

View File

@@ -8,8 +8,6 @@ use sqlx::{Row, SqlitePool};
use adapter_common::{parse_datetime, parse_uuid};
use crate::wrapup::fetch_genres_sqlite;
/// SQLite read side of Insights (ADR-0010). Deliberately thin: one query for
/// review basics, one for genres — every threshold and comparison lives in
/// the pure analyzer, not here.
@@ -29,7 +27,7 @@ impl InsightsQuery for SqliteInsightsQuery {
let rows = sqlx::query(
"SELECT movie_id, rating, watched_at, watch_medium \
FROM reviews \
WHERE user_id = ? \
WHERE user_id = ? AND remote_actor_url IS NULL \
ORDER BY watched_at ASC",
)
.bind(user_id.value().to_string())
@@ -37,20 +35,32 @@ impl InsightsQuery for SqliteInsightsQuery {
.await
.map_err(adapter_common::map_sqlx_error)?;
let mut movie_ids: Vec<String> = Vec::new();
let mut seen = std::collections::HashSet::new();
for row in &rows {
// Subquery rather than an `IN (?, …)` bind list: a whole diary can
// exceed SQLite's bound-parameter limit.
let genre_rows = sqlx::query(
"SELECT movie_id, name FROM movie_genres \
WHERE movie_id IN ( \
SELECT movie_id FROM reviews \
WHERE user_id = ? AND remote_actor_url IS NULL \
) \
ORDER BY name",
)
.bind(user_id.value().to_string())
.fetch_all(&self.pool)
.await
.map_err(adapter_common::map_sqlx_error)?;
let mut genres_map: HashMap<String, Vec<String>> = HashMap::new();
for row in genre_rows {
let mid: String = row
.try_get("movie_id")
.map_err(adapter_common::map_sqlx_error)?;
if seen.insert(mid.clone()) {
movie_ids.push(mid);
}
let name: String = row
.try_get("name")
.map_err(adapter_common::map_sqlx_error)?;
genres_map.entry(mid).or_default().push(name);
}
let genres_map: HashMap<String, Vec<String>> =
fetch_genres_sqlite(&self.pool, &movie_ids).await?;
let mut result = Vec::with_capacity(rows.len());
for row in &rows {
let movie_id: String = row