Files
silo-server/internal/taskmanager/tasks/notifications_tasks.go
QuickandClaude Fable 5 1e3780d4fb fix(notifications): address code review findings
- pin the four new sensitive setting keys (SMTP password, Discord
  secret/bot token, VAPID keypair) in the encryption audit test so a
  future drop from SensitiveSettingKeys fails CI
- bound account-channel digest drains strictly before the stamped
  digest time so consecutive digest windows partition rows exactly,
  instead of recapping rows created at or after the previous stamp
- keep the events websocket open when an event-frame snapshot fails,
  matching the writeSnapshotFrame degrade-gracefully contract
- rename the seed task to Seed Content Availability to match its
  episode+movie seeding behavior
- carry poster_source_path into realtime dispatch rows per the
  DeliveryRow contract

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 10:32:21 -04:00

144 lines
5.8 KiB
Go

package tasks
import (
"context"
"encoding/json"
"fmt"
"github.com/Silo-Server/silo-server/internal/notifications"
"github.com/Silo-Server/silo-server/internal/taskmanager"
)
// SeedContentAvailabilityTask inserts episode_availability and
// movie_availability rows for every currently playable episode and movie
// without creating release events, then writes the per-library, per-kind seed
// markers. Running it is what allows release events to flow for libraries
// that predate the notifications feature; rerunning is a cheap idempotent
// repair pass.
type SeedContentAvailabilityTask struct {
system *notifications.System
}
// NewSeedContentAvailabilityTask creates the seeding task.
func NewSeedContentAvailabilityTask(system *notifications.System) *SeedContentAvailabilityTask {
return &SeedContentAvailabilityTask{system: system}
}
func (t *SeedContentAvailabilityTask) Key() string { return "seed_content_availability" }
func (t *SeedContentAvailabilityTask) Name() string { return "Seed Content Availability" }
func (t *SeedContentAvailabilityTask) Description() string {
return "Records the existing episode and movie back-catalog as already-released so new-content notifications only fire for items that arrive afterwards."
}
func (t *SeedContentAvailabilityTask) Category() taskmanager.TaskCategory {
return taskmanager.TaskCategorySystem
}
func (t *SeedContentAvailabilityTask) IsHidden() bool { return true }
func (t *SeedContentAvailabilityTask) DefaultTriggers() []taskmanager.TriggerConfig {
return []taskmanager.TriggerConfig{{Type: taskmanager.TriggerTypeStartup}}
}
func (t *SeedContentAvailabilityTask) Execute(ctx context.Context, progress taskmanager.ProgressReporter) error {
if t == nil || t.system == nil {
progress.Report(100, "Notifications are not configured")
return nil
}
progress.Report(0, "Seeding content availability")
if err := t.system.SeedAvailability(ctx, func(percent int, message string) {
progress.Report(float64(percent), message)
}); err != nil {
return fmt.Errorf("seeding content availability: %w", err)
}
progress.Report(100, "Content availability seeded")
return nil
}
// RebuildReleaseInterestTask rebuilds profile_series_interest from favorites,
// watchlist, and watch progress. It is the rollout backfill and the periodic
// drift-repair pass; recomputes share the same code as live updates.
type RebuildReleaseInterestTask struct {
system *notifications.System
}
// NewRebuildReleaseInterestTask creates the interest rebuild task.
func NewRebuildReleaseInterestTask(system *notifications.System) *RebuildReleaseInterestTask {
return &RebuildReleaseInterestTask{system: system}
}
func (t *RebuildReleaseInterestTask) Key() string { return "rebuild_release_interest" }
func (t *RebuildReleaseInterestTask) Name() string { return "Rebuild Notification Interest" }
func (t *RebuildReleaseInterestTask) Description() string {
return "Recomputes which profiles care about which series (favorites, watchlist, watch progress) for new-episode notifications."
}
func (t *RebuildReleaseInterestTask) Category() taskmanager.TaskCategory {
return taskmanager.TaskCategorySystem
}
func (t *RebuildReleaseInterestTask) IsHidden() bool { return true }
func (t *RebuildReleaseInterestTask) DefaultTriggers() []taskmanager.TriggerConfig {
return []taskmanager.TriggerConfig{
{Type: taskmanager.TriggerTypeStartup},
{Type: taskmanager.TriggerTypeDaily, TimeOfDay: "04:30"},
}
}
func (t *RebuildReleaseInterestTask) Execute(ctx context.Context, progress taskmanager.ProgressReporter) error {
if t == nil || t.system == nil {
progress.Report(100, "Notifications are not configured")
return nil
}
progress.Report(0, "Rebuilding profile series interest")
if err := t.system.RebuildInterest(ctx, func(percent int, message string) {
progress.Report(float64(percent), message)
}); err != nil {
return fmt.Errorf("rebuilding notification interest: %w", err)
}
progress.Report(100, "Notification interest rebuilt")
return nil
}
// NotificationsRetentionTask applies the notification retention policy: read
// inbox rows past the read window, unread rows past the unread window,
// processed release events past the debug window, and inert interest rows.
type NotificationsRetentionTask struct {
system *notifications.System
}
// NewNotificationsRetentionTask creates the retention task.
func NewNotificationsRetentionTask(system *notifications.System) *NotificationsRetentionTask {
return &NotificationsRetentionTask{system: system}
}
func (t *NotificationsRetentionTask) Key() string { return "notifications_retention" }
func (t *NotificationsRetentionTask) Name() string { return "Clean Up Notifications" }
func (t *NotificationsRetentionTask) Description() string {
return "Prunes old notifications and processed release events according to the retention settings."
}
func (t *NotificationsRetentionTask) Category() taskmanager.TaskCategory {
return taskmanager.TaskCategorySystem
}
func (t *NotificationsRetentionTask) IsHidden() bool { return false }
func (t *NotificationsRetentionTask) DefaultTriggers() []taskmanager.TriggerConfig {
return []taskmanager.TriggerConfig{{Type: taskmanager.TriggerTypeDaily, TimeOfDay: "05:00"}}
}
func (t *NotificationsRetentionTask) Execute(ctx context.Context, progress taskmanager.ProgressReporter) error {
if t == nil || t.system == nil {
progress.Report(100, "Notifications are not configured")
return nil
}
progress.Report(0, "Applying notification retention policy")
stats, err := t.system.RunRetention(ctx)
if err != nil {
return fmt.Errorf("notification retention: %w", err)
}
if data, err := json.Marshal(stats); err == nil {
progress.SetResultData(data)
}
progress.Report(100, fmt.Sprintf(
"Removed %d notifications, %d release events, %d inert interest rows",
stats.DeliveriesDeleted, stats.EventsDeleted, stats.InterestPruned))
return nil
}