refactor: fix HIGH+MEDIUM architectural violations from code review

HIGH: fix watch_medium data-loss bug, standardize error handling on
ApiError, fix dep direction (rss/template-askama no longer dep on
application), extract ImageFetcher port (remove reqwest from app layer),
move event construction from save_review to use case, extract
infra-wiring crate (DbPool/EventBusBackend dedup), deduplicate
presentation helpers (encode_error, export streaming, multipart parsing)

MEDIUM: split LocalApContentQuery god-trait 10→3 methods, dedup movie
resolution orchestration, add RemoteActorDto/PersonDto mappers, move
AppConfig to infra-wiring, fix SocialQueryPort Uuid→UserId, replace
stringly-typed api-types with domain enums, move count_reviews_in_year
to StatsRepository, dedup event publisher cfg blocks, extract
should_enrich, move group_by_month to application, dedup
count_local_posts, add FederationFlags Default, TUI input helper +
ShowError rename + typed auth errors, api-types cleanup
(UserSettingsDto/UserProfileBase/PreviewRowData)

102 files changed, -681 lines net
This commit is contained in:
2026-07-10 02:08:39 +02:00
parent 26152660bb
commit 12da356a40
110 changed files with 1399 additions and 1867 deletions

View File

@@ -7,3 +7,4 @@ edition = "2024"
serde = { workspace = true }
uuid = { workspace = true }
utoipa = { version = "5.5.0", features = ["axum_extras", "uuid"] }
domain = { path = "../domain" }

View File

@@ -14,7 +14,8 @@ pub struct LoginResponse {
pub user_id: Uuid,
pub email: String,
pub expires_at: String,
pub role: String,
#[schema(value_type = String)]
pub role: domain::models::UserRole,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]

View File

@@ -18,7 +18,8 @@ pub struct LogReviewRequest {
pub comment: Option<String>,
pub watched_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub watch_medium: Option<String>,
#[schema(value_type = Option<String>)]
pub watch_medium: Option<domain::value_objects::WatchMedium>,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
@@ -89,7 +90,8 @@ pub struct EditReviewRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub watched_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub watch_medium: Option<Option<String>>,
#[schema(value_type = Option<Option<String>>)]
pub watch_medium: Option<Option<domain::value_objects::WatchMedium>>,
}
fn default_export_format() -> String {

View File

@@ -7,7 +7,8 @@ pub struct GoalDto {
pub current_count: u32,
pub percentage: f64,
pub is_complete: bool,
pub goal_type: String,
#[schema(value_type = String)]
pub goal_type: domain::models::GoalType,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
@@ -25,17 +26,3 @@ pub struct CreateGoalRequest {
pub struct UpdateGoalRequest {
pub target_count: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct UserSettingsDto {
pub federate_goals: bool,
pub federate_reviews: bool,
pub federate_watchlist: bool,
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct UpdateUserSettingsRequest {
pub federate_goals: bool,
pub federate_reviews: bool,
pub federate_watchlist: bool,
}

View File

@@ -46,29 +46,24 @@ pub struct SaveProfileRequest {
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct PreviewRowData {
pub index: usize,
pub title: Option<String>,
pub release_year: Option<String>,
pub director: Option<String>,
pub rating: Option<String>,
pub watched_at: Option<String>,
pub comment: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(tag = "status")]
pub enum PreviewRowDto {
#[serde(rename = "valid")]
Valid {
index: usize,
title: Option<String>,
release_year: Option<String>,
director: Option<String>,
rating: Option<String>,
watched_at: Option<String>,
comment: Option<String>,
},
Valid(PreviewRowData),
#[serde(rename = "duplicate")]
Duplicate {
index: usize,
title: Option<String>,
release_year: Option<String>,
director: Option<String>,
rating: Option<String>,
watched_at: Option<String>,
comment: Option<String>,
},
Duplicate(PreviewRowData),
#[serde(rename = "invalid")]
Invalid { index: usize, errors: Vec<String> },
}

View File

@@ -4,6 +4,7 @@ pub mod diary;
pub mod goals;
pub mod import;
pub mod movies;
pub mod rendering;
pub mod search;
pub mod social;
pub mod users;
@@ -17,6 +18,7 @@ pub use diary::*;
pub use goals::*;
pub use import::*;
pub use movies::*;
pub use rendering::*;
pub use social::*;
pub use users::*;
pub use watchlist::*;

View File

@@ -99,7 +99,8 @@ pub struct ReviewDto {
pub comment: Option<String>,
pub watched_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub watch_medium: Option<String>,
#[schema(value_type = Option<String>)]
pub watch_medium: Option<domain::value_objects::WatchMedium>,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
@@ -125,7 +126,8 @@ pub struct SocialReviewDto {
pub watched_at: String,
pub is_federated: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub watch_medium: Option<String>,
#[schema(value_type = Option<String>)]
pub watch_medium: Option<domain::value_objects::WatchMedium>,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]

View File

@@ -0,0 +1,19 @@
use uuid::Uuid;
pub struct HtmlPageContext {
pub user_email: Option<String>,
pub user_id: Option<Uuid>,
pub is_admin: bool,
pub register_enabled: bool,
pub rss_url: String,
pub page_title: String,
pub canonical_url: String,
pub csrf_token: String,
pub page_rss_url: Option<String>,
}
impl HtmlPageContext {
pub fn is_current_user(&self, id: Uuid) -> bool {
self.user_id == Some(id)
}
}

View File

@@ -67,13 +67,23 @@ pub struct UserTrendsDto {
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct UserProfileResponse {
pub user_id: Uuid,
pub struct UserProfileBase {
pub username: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bio: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub banner_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct UserProfileResponse {
pub user_id: Uuid,
#[serde(flatten)]
pub profile: UserProfileBase,
pub stats: UserStatsDto,
pub following_count: usize,
pub followers_count: usize,
@@ -90,23 +100,17 @@ pub struct UserProfileResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub handle: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bio: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub actor_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct ProfileResponse {
pub username: String,
pub display_name: Option<String>,
pub bio: Option<String>,
pub avatar_url: Option<String>,
pub banner_url: Option<String>,
#[serde(flatten)]
pub profile: UserProfileBase,
pub also_known_as: Option<String>,
pub fields: Vec<ProfileFieldDto>,
pub role: String,
#[schema(value_type = String)]
pub role: domain::models::UserRole,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
@@ -119,3 +123,17 @@ pub struct ProfileFieldDto {
pub struct UpdateProfileFieldsRequest {
pub fields: Vec<ProfileFieldDto>,
}
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct UserSettingsDto {
pub federate_goals: bool,
pub federate_reviews: bool,
pub federate_watchlist: bool,
}
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
pub struct UpdateUserSettingsRequest {
pub federate_goals: bool,
pub federate_reviews: bool,
pub federate_watchlist: bool,
}