feat: always-on Insights — correlation analysis over viewing habits
Some checks failed
CI / Check / Test (push) Failing after 9m16s

Conservative, live insights (ADR-0010) derived from each user's diary:
rating lift by genre (first-time watches only), genre seasonality vs a
uniform 1/12 month baseline, medium weekend-lift, and watch-time rhythm.

- domain: WatchRow model, InsightsQuery port, pure insights_analyzer with
  all honesty thresholds (sample minimums, strength cutoffs, suppressions
  for import noise), unit-tested
- adapters: thin Sqlite/Postgres fetch twins, reusing WrapUp's genre fetch
- application: get_insights use case, InsightsGroup in Deps
- composition/server: wiring through Repositories/DatabaseOutput
- presentation: GET /api/v1/insights handler, typed serde-tagged DTOs,
  openapi doc
- spa: /insights route (shadcn cards), insights feature hook, profile
  entry link, en locale strings
- server test: end-to-end seed sqlite → HTTP → rating lift + rhythm

Also records ADR-0010, defines "Insight" in the glossary, and includes
pre-existing worktree comment-stripping cleanups.
This commit is contained in:
2026-10-12 19:29:54 +02:00
parent 4d221e60de
commit 020fbb891e
126 changed files with 1922 additions and 639 deletions

View File

@@ -2,8 +2,9 @@ use application::auth::deps::{
LoginDeps, LogoutDeps, RefreshDeps, RegisterAndLoginDeps, RegisterDeps,
};
use application::deps::{
AuthGroup, Deps, DiaryGroup, GoalsGroup, ImportGroup, IntegrationsGroup, MoviesGroup,
PersonGroup, SearchGroup, SocialGroup, UsersGroup, WatchlistGroup, WorkerDeps, WrapupGroup,
AuthGroup, Deps, DiaryGroup, GoalsGroup, ImportGroup, InsightsGroup, IntegrationsGroup,
MoviesGroup, PersonGroup, SearchGroup, SocialGroup, UsersGroup, WatchlistGroup, WorkerDeps,
WrapupGroup,
};
use application::diary::deps::{
DeleteReviewDeps, EditReviewDeps, ExportDiaryDeps, GetActivityFeedDeps, GetDiaryDeps,
@@ -15,6 +16,7 @@ use application::import::deps::{
DeleteImportProfileDeps, ExecuteImportDeps, GetMappingStageDeps, GetPreviewStageDeps,
GetSessionStateDeps, ListImportProfilesDeps, SaveProfileDeps,
};
use application::insights::deps::GetInsightsDeps;
use application::integrations::deps::{
ConfirmWatchEventsDeps, DismissWatchEventsDeps, GenerateWebhookTokenDeps, GetWatchQueueDeps,
GetWebhookTokensDeps, IngestWatchEventDeps, RevokeWebhookTokenDeps,
@@ -175,6 +177,11 @@ pub fn build_deps(
import_profile: repos.import_profile.clone(),
},
},
insights: InsightsGroup {
get: GetInsightsDeps {
insights: repos.insights.clone(),
},
},
integrations: IntegrationsGroup {
ingest_watch_event: IngestWatchEventDeps {
webhook_token: repos.webhook_token.clone(),

View File

@@ -32,6 +32,7 @@ pub struct DatabaseOutput {
pub ap_content: Arc<dyn LocalApContentQuery>,
pub wrapup_stats: Arc<dyn domain::ports::WrapUpStatsQuery>,
pub wrapup_repo: Arc<dyn domain::ports::WrapUpRepository>,
pub insights: Arc<dyn domain::ports::InsightsQuery>,
pub goal_command: Arc<dyn domain::ports::GoalCommand>,
pub goal_query: Arc<dyn domain::ports::GoalQuery>,
pub user_settings: Arc<dyn domain::ports::UserSettingsRepository>,
@@ -91,6 +92,7 @@ pub async fn build_database_adapters(
ap_content: w.ap_content,
wrapup_stats: w.wrapup_stats,
wrapup_repo: w.wrapup_repo,
insights: w.insights,
goal_command: w.goal_command,
goal_query: w.goal_query,
user_settings: w.user_settings,
@@ -145,6 +147,7 @@ pub async fn build_database_adapters(
ap_content: w.ap_content,
wrapup_stats: w.wrapup_stats,
wrapup_repo: w.wrapup_repo,
insights: w.insights,
goal_command: w.goal_command,
goal_query: w.goal_query,
user_settings: w.user_settings,

View File

@@ -2,12 +2,12 @@ use std::sync::Arc;
use domain::ports::{
BlockQuery, DiaryQuery, FederatedProfileQuery, FederationAdminQuery, FollowGraphQuery,
GoalCommand, GoalQuery, ImportProfileRepository, ImportSessionRepository, MovieCommand,
MovieProfileRepository, MovieQuery, PersonCommand, PersonQuery, RefreshSessionRepository,
RemoteGoalRepository, RemoteWatchlistRepository, ReviewRepository, SearchCommand, SearchPort,
SocialCommand, StatsRepository, UserProfileFieldsRepository, UserRepository,
UserSettingsRepository, WatchEventCommand, WatchEventQuery, WatchlistRepository,
WebhookTokenRepository, WrapUpRepository, WrapUpStatsQuery,
GoalCommand, GoalQuery, ImportProfileRepository, ImportSessionRepository, InsightsQuery,
MovieCommand, MovieProfileRepository, MovieQuery, PersonCommand, PersonQuery,
RefreshSessionRepository, RemoteGoalRepository, RemoteWatchlistRepository, ReviewRepository,
SearchCommand, SearchPort, SocialCommand, StatsRepository, UserProfileFieldsRepository,
UserRepository, UserSettingsRepository, WatchEventCommand, WatchEventQuery,
WatchlistRepository, WebhookTokenRepository, WrapUpRepository, WrapUpStatsQuery,
};
#[derive(Clone)]
@@ -37,6 +37,7 @@ pub struct Repositories {
pub federation_admin: Arc<dyn FederationAdminQuery>,
pub wrapup_stats: Arc<dyn WrapUpStatsQuery>,
pub wrapup_repo: Arc<dyn WrapUpRepository>,
pub insights: Arc<dyn InsightsQuery>,
pub goal_command: Arc<dyn GoalCommand>,
pub goal_query: Arc<dyn GoalQuery>,
pub user_settings: Arc<dyn UserSettingsRepository>,

View File

@@ -5,13 +5,13 @@ use domain::testing::{
FakeAuthService, FakeDocumentParser, FakeMetadataClient, FakePasswordHasher, FakePersonQuery,
FakePosterFetcher, FakeSearchCommand, FakeSearchPort, FakeStatsRepository,
InMemoryGoalRepository, InMemoryImportProfileRepository, InMemoryImportSessionRepository,
InMemoryMovieProfileRepository, InMemoryMovieRepository, InMemoryProfileFieldsRepo,
InMemoryRefreshSessionRepository, InMemoryReviewRepository, InMemorySocialRepository,
InMemoryUserRepository, InMemoryUserSettingsRepository, InMemoryWatchEventRepository,
InMemoryWatchlistRepository, InMemoryWebhookTokenRepository, InMemoryWrapUpRepository,
InMemoryWrapUpStatsQuery, NoopEventPublisher, NoopFederationAdminQuery, NoopObjectStorage,
NoopRemoteGoalRepository, NoopRemoteWatchlistRepository, PanicDiaryExporter,
PanicPersonCommand,
InMemoryInsightsQuery, InMemoryMovieProfileRepository, InMemoryMovieRepository,
InMemoryProfileFieldsRepo, InMemoryRefreshSessionRepository, InMemoryReviewRepository,
InMemorySocialRepository, InMemoryUserRepository, InMemoryUserSettingsRepository,
InMemoryWatchEventRepository, InMemoryWatchlistRepository, InMemoryWebhookTokenRepository,
InMemoryWrapUpRepository, InMemoryWrapUpStatsQuery, NoopEventPublisher,
NoopFederationAdminQuery, NoopObjectStorage, NoopRemoteGoalRepository,
NoopRemoteWatchlistRepository, PanicDiaryExporter, PanicPersonCommand,
};
use crate::{DatabaseOutput, Repositories, build::build_deps, build::build_worker_deps};
@@ -254,6 +254,7 @@ fn test_repositories() -> Repositories {
federation_admin: Arc::new(NoopFederationAdminQuery),
wrapup_stats: InMemoryWrapUpStatsQuery::new(),
wrapup_repo: InMemoryWrapUpRepository::new(),
insights: InMemoryInsightsQuery::new(),
goal_command: Arc::clone(&goals) as _,
goal_query: goals as _,
user_settings: InMemoryUserSettingsRepository::new(),
@@ -312,6 +313,7 @@ fn test_database_output() -> DatabaseOutput {
ap_content: Arc::new(NoopLocalApContentQuery),
wrapup_stats: InMemoryWrapUpStatsQuery::new(),
wrapup_repo: InMemoryWrapUpRepository::new(),
insights: InMemoryInsightsQuery::new(),
goal_command: Arc::clone(&goals) as _,
goal_query: goals as _,
user_settings: Arc::clone(&user_settings) as _,