feat: search reindex, worker improvements, person IDs, user display names

- add admin POST /api/v1/admin/reindex-search endpoint + event-driven handler
- backfill persons from movie_cast/movie_crew into persons table
- paginate person list_page/backfill_from_credits_batch to cap memory
- concurrent worker event dispatch with semaphore (max 8)
- graceful worker shutdown (drain in-flight tasks on SIGINT)
- always ack events, log handler errors as warnings (no infinite retry)
- NATS ack_wait 600s, AtomicBool guard against concurrent reindex
- add username/display_name to UserSummaryDto and users list
- add person_id to CastMemberDto/CrewMemberDto via get_movie_profile use case
- add movie_id to wrapup MovieRef, person_id to wrapup PersonStat
- thread tmdb_person_id through wrapup cast pipeline
- add is_federated to FeedEntryDto
- cap orphaned persons query with LIMIT 500
- add SPA link to classic site footer
This commit is contained in:
2026-06-04 14:43:28 +02:00
parent af8e58aeb8
commit bd7dc648c4
36 changed files with 693 additions and 118 deletions

View File

@@ -182,17 +182,15 @@ impl UserRepository for SqliteUserRepository {
}
async fn list_with_stats(&self) -> Result<Vec<domain::models::UserSummary>, DomainError> {
sqlx::query_as!(
UserSummaryRow,
r#"SELECT u.id AS "id!: String",
u.email AS "email!: String",
COUNT(DISTINCT r.movie_id) AS "total_movies!: i64",
sqlx::query_as::<_, UserSummaryRow>(
r#"SELECT u.id, u.email, u.username, u.display_name,
COUNT(DISTINCT r.movie_id) AS total_movies,
AVG(CAST(r.rating AS REAL)) AS avg_rating,
u.avatar_path
FROM users u
LEFT JOIN reviews r ON r.user_id = u.id AND r.remote_actor_url IS NULL
GROUP BY u.id, u.email, u.avatar_path
ORDER BY u.email ASC"#
GROUP BY u.id, u.email, u.username, u.display_name, u.avatar_path
ORDER BY u.email ASC"#,
)
.fetch_all(&self.pool)
.await