diff --git a/README.md b/README.md
index 93b2502..d673acb 100644
--- a/README.md
+++ b/README.md
@@ -88,8 +88,9 @@ Open `http://localhost:3000`. The HTTP server and background worker start togeth
Hexagonal (Ports & Adapters) with Domain-Driven Design:
```
-api-types — shared REST API request/response DTOs (Serialize/Deserialize + utoipa schemas); used by presentation and tui
-domain — pure types and trait definitions, no external deps
+api-types — shared REST API request/response DTOs (Serialize/Deserialize + utoipa schemas) + HtmlPageContext; used by presentation, tui, and template adapters
+infra-wiring — shared infrastructure types (DbPool, EventBusBackend, AppConfig) used by both presentation and worker binaries
+domain — pure types and CQRS port traits (MovieCommand/MovieQuery, WatchEventCommand/WatchEventQuery, PersonCommand/PersonQuery, SearchCommand/SearchPort, ImageFetcher, RssFeedRenderer), no external deps except serde
application — use cases (commands + queries), business logic orchestration; handlers delegate here for all domain logic
presentation — Axum HTTP router, OpenAPI spec assembly, Swagger UI + Scalar serving, composition root for the HTTP process
worker — standalone worker binary (event consumer, poster sync, federation)
diff --git a/architecture.mmd b/architecture.mmd
index d36d5a5..23949b6 100644
--- a/architecture.mmd
+++ b/architecture.mmd
@@ -59,8 +59,8 @@ graph TB
M_SEARCH["SearchQuery,
SearchResults"]
end
subgraph Ports["Port Traits (Interfaces)"]
- P_REPOS["MovieRepository
ReviewRepository
DiaryRepository
UserRepository
WatchlistRepository
WatchEventRepository
WebhookTokenRepository
ImportSessionRepository
MovieProfileRepository
WrapUpRepository
GoalRepository
UserSettingsRepository
MovieDeduplicator"]
- P_SERVICES["AuthService
MetadataClient
PosterFetcherClient
ObjectStorage
EventPublisher
EventConsumer
PasswordHasher
DiaryExporter
DocumentParser"]
+ P_REPOS["MovieCommand / MovieQuery
ReviewRepository
DiaryRepository / StatsRepository
UserRepository
WatchlistRepository
WatchEventCommand / WatchEventQuery
WebhookTokenRepository
ImportSessionRepository
MovieProfileRepository
WrapUpRepository
GoalRepository
UserSettingsRepository
MovieDeduplicator"]
+ P_SERVICES["AuthService
MetadataClient
PosterFetcherClient
ImageFetcher
ObjectStorage
EventPublisher
EventConsumer
PasswordHasher
DiaryExporter
DocumentParser
RssFeedRenderer"]
P_SEARCH["SearchPort
SearchCommand
PersonQuery
PersonCommand"]
P_FEDERATION["SocialQueryPort
LocalApContentQuery
RemoteWatchlistRepository
RemoteGoalRepository"]
end
@@ -72,8 +72,12 @@ graph TB
VO["Value Objects
MovieId, UserId, Rating,
WatchMedium, Email, Username,
Password, ..."]
end
- subgraph ApiTypes["api-types (0 domain deps)"]
- DTO["DTOs
MovieDto, ReviewDto,
FeedEntryDto, UserSummaryDto,
CastMemberDto, ..."]
+ subgraph ApiTypes["api-types"]
+ DTO["DTOs
MovieDto, ReviewDto,
FeedEntryDto, UserSummaryDto,
HtmlPageContext, ..."]
+ end
+
+ subgraph InfraWiring["infra-wiring"]
+ IW["DbPool, EventBusBackend,
AppConfig
Shared infra types"]
end
subgraph Adapters["Adapters (implement Port Traits)"]
diff --git a/crates/presentation/src/errors.rs b/crates/presentation/src/errors.rs
index 968aec8..073151a 100644
--- a/crates/presentation/src/errors.rs
+++ b/crates/presentation/src/errors.rs
@@ -36,6 +36,13 @@ impl From for ApiError {
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
- domain_error_response(self.0)
+ let status = domain_error_status(&self.0);
+ match &self.0 {
+ DomainError::InfrastructureError(_) => {
+ tracing::error!("Internal error: {:?}", self.0);
+ (status, axum::Json(serde_json::json!({"error": "internal server error"}))).into_response()
+ }
+ _ => (status, axum::Json(serde_json::json!({"error": self.0.to_string()}))).into_response(),
+ }
}
}
diff --git a/movies-diary.insomnia.json b/movies-diary.insomnia.json
index e23b94f..b552def 100644
--- a/movies-diary.insomnia.json
+++ b/movies-diary.insomnia.json
@@ -355,6 +355,32 @@
"settingRebuildPath": true,
"settingFollowRedirects": "global"
},
+ {
+ "_id": "req_reviews_edit",
+ "_type": "request",
+ "parentId": "fld_reviews",
+ "modified": 1749340800000,
+ "created": 1749340800000,
+ "name": "Edit Review",
+ "method": "PATCH",
+ "url": "{{ base_url }}/api/v1/reviews/:id",
+ "description": "Partial update — all fields optional",
+ "headers": [{ "name": "Content-Type", "value": "application/json" }],
+ "parameters": [],
+ "authentication": { "type": "bearer", "token": "{{ token }}" },
+ "body": {
+ "mimeType": "application/json",
+ "text": "{\n \"rating\": 9,\n \"comment\": \"Updated thoughts\",\n \"watch_medium\": \"cinema\"\n}"
+ },
+ "metaSortKey": -3,
+ "isPrivate": false,
+ "settingStoreCookies": true,
+ "settingSendCookies": true,
+ "settingDisableRenderRequestBody": false,
+ "settingEncodeUrl": true,
+ "settingRebuildPath": true,
+ "settingFollowRedirects": "global"
+ },
{
"_id": "fld_people",