diff --git a/README.md b/README.md
index 311fecc..b3e979a 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,7 @@ A self-hosted, server-side rendered movie logging system with a full REST API. B
- User profiles — display name, bio, avatar, banner, custom profile fields; editable via HTML settings page or REST API
- Jellyfin/Plex auto-import — media server sends a webhook on playback stop, movies land in a watch queue; review and confirm with a rating to create diary entries; per-user webhook tokens with SHA-256 auth; setup UI at `/settings/integrations`
- Annual Wrap-Up — Spotify Wrapped for movies: per-user and instance-wide year-in-review with stats (top directors, actors, genres, rating distribution, watch time, rewatches, budget analysis), shareable HTML page at `/wrapups/{user_id}/{year}`, downloadable MP4 video with branded slides; admin-triggered or auto-generated in January
+- Goals — set a "watch N movies in YEAR" target with a progress bar; progress computed from existing reviews (backwards compatible); per-user federation toggle in settings; displayed on profile (SPA: interactive with create/edit/delete, classic HTML: read-only glassmorphic card)
- CSV and JSON diary export
- File importer: upload CSV, TSV, JSON, or XLSX from any source (Letterboxd, IMDb, etc.), map columns to domain fields via a step-by-step wizard or REST API, save mapping profiles for repeat imports
- REST API v1 (`/api/v1/`) with full feature parity with the HTML interface
diff --git a/architecture.mmd b/architecture.mmd
index 149c14b..e17a919 100644
--- a/architecture.mmd
+++ b/architecture.mmd
@@ -19,6 +19,7 @@ graph TB
UC_USERS["users
get_users, get_profile,
update_profile"]
UC_WATCHLIST["watchlist
add, remove, get"]
UC_WRAPUP["wrapup
generate, compute,
list, delete"]
+ UC_GOALS["goals
create, update, delete,
get, list"]
UC_INTEGRATIONS["integrations
webhooks, watch_queue,
confirm, dismiss"]
UC_SEARCH["search
execute"]
UC_PERSON["person
get, get_credits"]
@@ -47,16 +48,17 @@ graph TB
M_USER["User, UserSummary"]
M_PERSON["Person, PersonId,
PersonCredits"]
M_WATCHLIST["WatchlistEntry,
WatchEvent"]
+ M_GOAL["Goal, GoalWithProgress,
UserSettings, RemoteGoalEntry"]
M_WRAPUP["WrapUpReport,
MovieRef, PersonStat"]
M_SEARCH["SearchQuery,
SearchResults"]
end
subgraph Ports["Port Traits (Interfaces)"]
- P_REPOS["MovieRepository
ReviewRepository
DiaryRepository
UserRepository
WatchlistRepository
WatchEventRepository
WebhookTokenRepository
ImportSessionRepository
MovieProfileRepository
WrapUpRepository"]
+ P_REPOS["MovieRepository
ReviewRepository
DiaryRepository
UserRepository
WatchlistRepository
WatchEventRepository
WebhookTokenRepository
ImportSessionRepository
MovieProfileRepository
WrapUpRepository
GoalRepository
UserSettingsRepository"]
P_SERVICES["AuthService
MetadataClient
PosterFetcherClient
ObjectStorage
EventPublisher
EventConsumer
PasswordHasher
DiaryExporter
DocumentParser"]
P_SEARCH["SearchPort
SearchCommand
PersonQuery
PersonCommand"]
- P_FEDERATION["SocialQueryPort
LocalApContentQuery
RemoteWatchlistRepository"]
+ P_FEDERATION["SocialQueryPort
LocalApContentQuery
RemoteWatchlistRepository
RemoteGoalRepository"]
end
- EVENTS["DomainEvent enum
ReviewLogged, MovieDiscovered,
SearchReindexRequested, ..."]
+ EVENTS["DomainEvent enum
ReviewLogged, MovieDiscovered,
GoalCreated, GoalUpdated,
SearchReindexRequested, ..."]
VO["Value Objects
MovieId, UserId, Rating,
Email, Username, ..."]
end
diff --git a/crates/domain/src/testing.rs b/crates/domain/src/testing.rs
deleted file mode 100644
index bc82deb..0000000
--- a/crates/domain/src/testing.rs
+++ /dev/null
@@ -1,1309 +0,0 @@
-#![cfg(any(test, feature = "test-helpers"))]
-
-use std::collections::HashMap;
-use std::sync::{Arc, Mutex};
-
-use async_trait::async_trait;
-use chrono::Utc;
-use uuid::Uuid;
-
-use crate::{
- errors::DomainError,
- events::DomainEvent,
- models::{
- AnnotatedRow, DiaryEntry, DiaryFilter, EntityType, ExportFormat, ExternalPersonId,
- FeedEntry, FieldMapping, FileFormat, ImportError, ImportProfile, ImportSession,
- IndexableDocument, Movie, MovieFilter, MovieProfile, MovieStats, MovieSummary, ParsedFile,
- Person, PersonCredits, PersonId, Review, ReviewHistory, SearchQuery, SearchResults, User,
- UserStats, UserSummary, UserTrends, WatchlistEntry, WatchlistWithMovie,
- collections::{PageParams, Paginated},
- },
- ports::{
- AuthService, DiaryExporter, DiaryRepository, DocumentParser, EventPublisher, FeedSortBy,
- FollowingFilter, GeneratedToken, ImportProfileRepository, ImportSessionRepository,
- MetadataClient, MetadataSearchCriteria, MovieProfileRepository, MovieRepository,
- ObjectStorage, PasswordHasher, PersonCommand, PersonQuery, PosterFetcherClient,
- ReviewRepository, SearchCommand, SearchPort, StatsRepository, UserProfileFieldsRepository,
- UserRepository, WatchlistRepository, WrapUpRepository,
- },
- value_objects::{
- Email, ExternalMetadataId, ImportProfileId, ImportSessionId, MovieId, MovieTitle,
- PasswordHash, PosterUrl, ReleaseYear, ReviewId, UserId, Username, WrapUpId,
- },
-};
-
-// ── InMemoryMovieRepository ───────────────────────────────────────────────────
-
-pub struct InMemoryMovieRepository {
- pub store: Mutex>,
-}
-
-impl InMemoryMovieRepository {
- pub fn new() -> Arc {
- Arc::new(Self {
- store: Mutex::new(HashMap::new()),
- })
- }
-
- pub fn count(&self) -> usize {
- self.store.lock().unwrap().len()
- }
-}
-
-#[async_trait]
-impl MovieRepository for InMemoryMovieRepository {
- async fn get_movie_by_external_id(
- &self,
- external_metadata_id: &ExternalMetadataId,
- ) -> Result