Closes #127. Grows user_ai_prefs with five nullable owner-default columns and makes them the server-side fallback layer: request param -> stored pref -> hard default, at insight generation (agentic + standard), all three insight-chat turn variants, and reel creation. - PUT /prefs accepts the fields with the existing three-state partial update semantics (TriState generalized to TriState<T> for the numeric reel_max_segments); validation (canonical BackendKind::parse, MemoriesSpan::from_str) runs before any write. - GET /prefs returns the new fields. - capture_prefs reads the existing row and carries the AI owner-defaults through unchanged (the upsert overwrites all columns), mirroring only the effective reel span/segments. - MemoriesSpan gains Display + FromStr so stored spans round-trip losslessly. 1463/0 tests (+27), clippy clean.
611 lines
21 KiB
Rust
611 lines
21 KiB
Rust
use crate::database::schema::{
|
||
entities, entity_facts, entity_photo_links, favorites, image_exif, insight_generation_jobs,
|
||
libraries, personas, photo_insights, precomputed_reels, user_ai_prefs, users,
|
||
video_preview_clips,
|
||
};
|
||
use serde::Serialize;
|
||
|
||
/// Possible statuses for an insight generation job.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, FromSqlRow)]
|
||
#[serde(rename_all = "snake_case")]
|
||
pub enum InsightJobStatus {
|
||
Running,
|
||
Completed,
|
||
Failed,
|
||
Cancelled,
|
||
}
|
||
|
||
impl InsightJobStatus {
|
||
pub fn as_str(&self) -> &'static str {
|
||
match self {
|
||
Self::Running => "running",
|
||
Self::Completed => "completed",
|
||
Self::Failed => "failed",
|
||
Self::Cancelled => "cancelled",
|
||
}
|
||
}
|
||
|
||
pub fn parse(s: &str) -> Self {
|
||
match s {
|
||
"running" => Self::Running,
|
||
"completed" => Self::Completed,
|
||
"failed" => Self::Failed,
|
||
"cancelled" => Self::Cancelled,
|
||
other => {
|
||
log::warn!(
|
||
"Unknown InsightJobStatus value: {:?}, treating as failed",
|
||
other
|
||
);
|
||
Self::Failed
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
impl std::fmt::Display for InsightJobStatus {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
f.write_str(self.as_str())
|
||
}
|
||
}
|
||
|
||
/// Type of insight generation (standard vs agentic).
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||
#[serde(rename_all = "snake_case")]
|
||
pub enum InsightGenerationType {
|
||
Standard,
|
||
Agentic,
|
||
}
|
||
|
||
impl InsightGenerationType {
|
||
pub fn as_str(&self) -> &'static str {
|
||
match self {
|
||
Self::Standard => "standard",
|
||
Self::Agentic => "agentic",
|
||
}
|
||
}
|
||
}
|
||
|
||
impl std::fmt::Display for InsightGenerationType {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
f.write_str(self.as_str())
|
||
}
|
||
}
|
||
|
||
#[derive(Insertable)]
|
||
#[diesel(table_name = users)]
|
||
pub struct InsertUser<'a> {
|
||
pub username: &'a str,
|
||
pub password: &'a str,
|
||
}
|
||
|
||
#[derive(Serialize, Queryable, Clone, Debug)]
|
||
pub struct User {
|
||
pub id: i32,
|
||
pub username: String,
|
||
#[serde(skip_serializing)]
|
||
pub password: String,
|
||
}
|
||
|
||
#[derive(Insertable)]
|
||
#[diesel(table_name = favorites)]
|
||
pub struct InsertFavorite<'a> {
|
||
pub userid: &'a i32,
|
||
#[diesel(column_name = rel_path)]
|
||
pub path: &'a str,
|
||
}
|
||
|
||
#[derive(Serialize, Queryable, Clone, Debug)]
|
||
pub struct Favorite {
|
||
pub id: i32,
|
||
pub userid: i32,
|
||
#[diesel(column_name = rel_path)]
|
||
pub path: String,
|
||
}
|
||
|
||
#[derive(Insertable)]
|
||
#[diesel(table_name = image_exif)]
|
||
pub struct InsertImageExif {
|
||
pub library_id: i32,
|
||
#[diesel(column_name = rel_path)]
|
||
pub file_path: String,
|
||
pub camera_make: Option<String>,
|
||
pub camera_model: Option<String>,
|
||
pub lens_model: Option<String>,
|
||
pub width: Option<i32>,
|
||
pub height: Option<i32>,
|
||
pub orientation: Option<i32>,
|
||
pub gps_latitude: Option<f32>,
|
||
pub gps_longitude: Option<f32>,
|
||
pub gps_altitude: Option<f32>,
|
||
pub focal_length: Option<f32>,
|
||
pub aperture: Option<f32>,
|
||
pub shutter_speed: Option<String>,
|
||
pub iso: Option<i32>,
|
||
pub date_taken: Option<i64>,
|
||
pub created_time: i64,
|
||
pub last_modified: i64,
|
||
pub content_hash: Option<String>,
|
||
pub size_bytes: Option<i64>,
|
||
/// 64-bit pHash (DCT) packed as i64. NULL for videos and decode failures.
|
||
pub phash_64: Option<i64>,
|
||
/// 64-bit dHash (gradient). NULL for videos and decode failures.
|
||
pub dhash_64: Option<i64>,
|
||
/// Which step of the canonical-date waterfall populated `date_taken`:
|
||
/// `"exif"` | `"exiftool"` | `"filename"` | `"fs_time"`. NULL when
|
||
/// `date_taken` is NULL (no source resolved it). The per-tick backfill
|
||
/// drain re-resolves rows whose source is `"fs_time"` once exiftool
|
||
/// has had a chance to run.
|
||
pub date_taken_source: Option<String>,
|
||
/// True UTC instant (unix seconds) under the new date contract
|
||
/// (ADR-0001). NULL while the row is still on the legacy convention —
|
||
/// either unclassified (filename branch, ticket #116) or pending the
|
||
/// backfill drain (#118).
|
||
pub date_utc: Option<i64>,
|
||
/// Camera offset (local − UTC, minutes) that produced `date_utc`.
|
||
/// NULL for direct-true-UTC branches and unclassified rows.
|
||
pub camera_offset_minutes: Option<i32>,
|
||
/// Why the offset is what it is: `offset_tag` | `gps` |
|
||
/// `assumed_server` | `utc_direct` | `manual`. See `camera_tz::TzSource`.
|
||
pub date_taken_tz_source: Option<String>,
|
||
}
|
||
|
||
// Field order matches the post-migration column order in `image_exif`.
|
||
#[derive(Serialize, Queryable, Clone, Debug)]
|
||
pub struct ImageExif {
|
||
pub id: i32,
|
||
pub library_id: i32,
|
||
#[diesel(column_name = rel_path)]
|
||
pub file_path: String,
|
||
pub camera_make: Option<String>,
|
||
pub camera_model: Option<String>,
|
||
pub lens_model: Option<String>,
|
||
pub width: Option<i32>,
|
||
pub height: Option<i32>,
|
||
pub orientation: Option<i32>,
|
||
pub gps_latitude: Option<f32>,
|
||
pub gps_longitude: Option<f32>,
|
||
pub gps_altitude: Option<f32>,
|
||
pub focal_length: Option<f32>,
|
||
pub aperture: Option<f32>,
|
||
pub shutter_speed: Option<String>,
|
||
pub iso: Option<i32>,
|
||
pub date_taken: Option<i64>,
|
||
pub created_time: i64,
|
||
pub last_modified: i64,
|
||
pub content_hash: Option<String>,
|
||
pub size_bytes: Option<i64>,
|
||
pub phash_64: Option<i64>,
|
||
pub dhash_64: Option<i64>,
|
||
/// When non-null, this row is a soft-marked duplicate of the file
|
||
/// whose `content_hash` matches this value. The default `/photos`
|
||
/// listing filters such rows out.
|
||
pub duplicate_of_hash: Option<String>,
|
||
/// Unix seconds at which the resolve was committed.
|
||
pub duplicate_decided_at: Option<i64>,
|
||
/// Which step of the canonical-date waterfall populated `date_taken`.
|
||
/// Plus `"manual"` when the operator has set it via POST /image/exif/date.
|
||
pub date_taken_source: Option<String>,
|
||
/// Snapshot of the prior `date_taken` taken on first manual override.
|
||
/// NULL when no override is active. POST /image/exif/date/clear restores
|
||
/// `date_taken` from this column and nulls it back out.
|
||
pub original_date_taken: Option<i64>,
|
||
/// Snapshot of the prior `date_taken_source` taken on first manual
|
||
/// override. NULL when no override is active.
|
||
pub original_date_taken_source: Option<String>,
|
||
/// L2-normalized CLIP image embedding (raw little-endian float32 bytes;
|
||
/// length depends on the model — 768×4 for ViT-L/14, 512×4 for ViT-B/32).
|
||
/// NULL until Apollo's CLIP service has encoded this photo via the
|
||
/// backfill drain. Used by `/photos/search` for semantic queries.
|
||
pub clip_embedding: Option<Vec<u8>>,
|
||
/// Which CLIP model produced `clip_embedding` (e.g. `"ViT-L/14"`). A
|
||
/// swap of `APOLLO_CLIP_MODEL` re-eligibilizes rows whose stored
|
||
/// version differs so the drain rebuilds them.
|
||
pub clip_model_version: Option<String>,
|
||
/// True UTC instant (unix seconds) under the new date contract
|
||
/// (ADR-0001). NULL while the row is still on the legacy convention.
|
||
pub date_utc: Option<i64>,
|
||
/// Camera offset (local − UTC, minutes) that produced `date_utc`.
|
||
pub camera_offset_minutes: Option<i32>,
|
||
/// How the offset was determined. See `camera_tz::TzSource`.
|
||
pub date_taken_tz_source: Option<String>,
|
||
/// Snapshot of the prior `date_utc` taken on first manual override.
|
||
/// NULL when no override is active. POST /image/exif/date/clear
|
||
/// restores the new-contract triple from these columns.
|
||
pub original_date_utc: Option<i64>,
|
||
/// Snapshot of the prior `camera_offset_minutes` (first override).
|
||
pub original_camera_offset_minutes: Option<i32>,
|
||
/// Snapshot of the prior `date_taken_tz_source` (first override).
|
||
pub original_date_taken_tz_source: Option<String>,
|
||
}
|
||
|
||
#[derive(Insertable, Clone)]
|
||
#[diesel(table_name = photo_insights)]
|
||
pub struct InsertPhotoInsight {
|
||
pub library_id: i32,
|
||
#[diesel(column_name = rel_path)]
|
||
pub file_path: String,
|
||
pub title: String,
|
||
pub summary: String,
|
||
pub generated_at: i64,
|
||
pub model_version: String,
|
||
pub is_current: bool,
|
||
pub training_messages: Option<String>,
|
||
/// `"local"` (Ollama with images) | `"hybrid"` (local vision + OpenRouter chat).
|
||
pub backend: String,
|
||
/// JSON array of insight ids whose `training_messages` were compressed
|
||
/// and injected into the system prompt as few-shot exemplars when this
|
||
/// row was generated. `None` means no few-shot was used (pristine
|
||
/// generation). Used downstream to filter out contaminated rows when
|
||
/// assembling an unbiased training / evaluation set.
|
||
pub fewshot_source_ids: Option<String>,
|
||
/// Bytes-keyed identity. When present, this insight is considered
|
||
/// to belong to the content rather than the path — see CLAUDE.md
|
||
/// "Multi-library data model". The DAO populates this from
|
||
/// `image_exif.content_hash` at insert time when known; rows
|
||
/// inserted before the hash is available stay null and the
|
||
/// reconciliation pass backfills them.
|
||
pub content_hash: Option<String>,
|
||
pub num_ctx: Option<i32>,
|
||
pub temperature: Option<f32>,
|
||
pub top_p: Option<f32>,
|
||
pub top_k: Option<i32>,
|
||
pub min_p: Option<f32>,
|
||
pub system_prompt: Option<String>,
|
||
pub persona_id: Option<String>,
|
||
pub prompt_eval_count: Option<i32>,
|
||
pub eval_count: Option<i32>,
|
||
}
|
||
|
||
#[derive(Serialize, Queryable, Clone, Debug)]
|
||
pub struct PhotoInsight {
|
||
pub id: i32,
|
||
pub library_id: i32,
|
||
#[diesel(column_name = rel_path)]
|
||
pub file_path: String,
|
||
pub title: String,
|
||
pub summary: String,
|
||
pub generated_at: i64,
|
||
pub model_version: String,
|
||
pub is_current: bool,
|
||
pub training_messages: Option<String>,
|
||
pub approved: Option<bool>,
|
||
/// `"local"` (Ollama with images) | `"hybrid"` (local vision + OpenRouter chat).
|
||
pub backend: String,
|
||
pub fewshot_source_ids: Option<String>,
|
||
pub content_hash: Option<String>,
|
||
pub num_ctx: Option<i32>,
|
||
pub temperature: Option<f32>,
|
||
pub top_p: Option<f32>,
|
||
pub top_k: Option<i32>,
|
||
pub min_p: Option<f32>,
|
||
pub system_prompt: Option<String>,
|
||
pub persona_id: Option<String>,
|
||
pub prompt_eval_count: Option<i32>,
|
||
pub eval_count: Option<i32>,
|
||
}
|
||
|
||
// --- Libraries ---
|
||
|
||
#[derive(Serialize, Queryable, Clone, Debug)]
|
||
pub struct LibraryRow {
|
||
pub id: i32,
|
||
pub name: String,
|
||
pub root_path: String,
|
||
pub created_at: i64,
|
||
/// Operator kill switch. `false` = the watcher skips this library
|
||
/// entirely (no probe, no ingest, no maintenance) and orphan-GC
|
||
/// treats it as out-of-scope for the all-online consensus rule.
|
||
/// Toggle via SQL today — there is intentionally no HTTP endpoint
|
||
/// for library mutation (see CLAUDE.md "Multi-library data model").
|
||
pub enabled: bool,
|
||
/// Per-library excluded paths/patterns, stored comma-separated
|
||
/// (same shape as the global `EXCLUDED_DIRS` env var). NULL = no
|
||
/// extra excludes for this library; the global env var still
|
||
/// applies. The runtime `Library` struct parses this into a
|
||
/// `Vec<String>` and the walker applies the union of (global,
|
||
/// library) excludes when scanning. Use case: mount a parent
|
||
/// directory while another library covers a child subtree.
|
||
pub excluded_dirs: Option<String>,
|
||
}
|
||
|
||
#[derive(Insertable)]
|
||
#[diesel(table_name = libraries)]
|
||
pub struct InsertLibrary<'a> {
|
||
pub name: &'a str,
|
||
pub root_path: &'a str,
|
||
pub created_at: i64,
|
||
pub enabled: bool,
|
||
pub excluded_dirs: Option<&'a str>,
|
||
}
|
||
|
||
// --- Knowledge memory models ---
|
||
|
||
#[derive(Insertable, Clone)]
|
||
#[diesel(table_name = entities)]
|
||
pub struct InsertEntity {
|
||
pub name: String,
|
||
pub entity_type: String,
|
||
pub description: String,
|
||
pub embedding: Option<Vec<u8>>,
|
||
pub confidence: f32,
|
||
pub status: String,
|
||
pub created_at: i64,
|
||
pub updated_at: i64,
|
||
}
|
||
|
||
#[derive(Serialize, Queryable, Clone, Debug)]
|
||
pub struct Entity {
|
||
pub id: i32,
|
||
pub name: String,
|
||
pub entity_type: String,
|
||
pub description: String,
|
||
pub embedding: Option<Vec<u8>>,
|
||
pub confidence: f32,
|
||
pub status: String,
|
||
pub created_at: i64,
|
||
pub updated_at: i64,
|
||
}
|
||
|
||
#[derive(Insertable, Clone)]
|
||
#[diesel(table_name = entity_facts)]
|
||
pub struct InsertEntityFact {
|
||
pub subject_entity_id: i32,
|
||
pub predicate: String,
|
||
pub object_entity_id: Option<i32>,
|
||
pub object_value: Option<String>,
|
||
pub source_photo: Option<String>,
|
||
pub source_insight_id: Option<i32>,
|
||
pub confidence: f32,
|
||
pub status: String,
|
||
pub created_at: i64,
|
||
/// Which persona authored this fact. Shared entities, persona-tagged
|
||
/// facts: each persona accumulates its own voice over the same
|
||
/// real-world referents. Defaults to `'default'` for legacy rows
|
||
/// (see migration 2026-05-09-000000).
|
||
pub persona_id: String,
|
||
/// Author's user_id. Required for the composite FK to
|
||
/// `personas(user_id, persona_id)` (migration 2026-05-10-000000) and
|
||
/// for cross-user fact isolation: two users with the same 'default'
|
||
/// persona must not see each other's facts. Always paired with
|
||
/// `persona_id` — they're a unit.
|
||
pub user_id: i32,
|
||
/// Real-world period the fact is/was true (unix seconds). NULL on
|
||
/// either side = unbounded — `valid_from IS NULL` reads as
|
||
/// "always-true-back-to-the-beginning", `valid_until IS NULL` as
|
||
/// "still-true-now-or-unknown". Distinguishes valid time from
|
||
/// transaction time (`created_at` is when we recorded the fact,
|
||
/// not when it was true in the world). See migration
|
||
/// 2026-05-10-000100.
|
||
pub valid_from: Option<i64>,
|
||
pub valid_until: Option<i64>,
|
||
/// Points at the entity_facts.id that replaced this one. Set by
|
||
/// the supersede endpoint; status flips to 'superseded' in the
|
||
/// same transaction. See migration 2026-05-10-000200.
|
||
pub superseded_by: Option<i32>,
|
||
/// Provenance for model audit — see migration 2026-05-10-000300.
|
||
/// `created_by_model` is the LLM identifier (e.g. "qwen2.5:7b",
|
||
/// "anthropic/claude-sonnet-4") or NULL for legacy / manual rows.
|
||
/// `created_by_backend` is "local" / "hybrid" / "manual" / NULL.
|
||
pub created_by_model: Option<String>,
|
||
pub created_by_backend: Option<String>,
|
||
/// Audit trail for mutations after creation — see migration
|
||
/// 2026-05-10-000500. `last_modified_*` stamp on any update
|
||
/// (status flip, valid-time edit, supersede, manual PATCH);
|
||
/// `last_modified_at` is unix seconds. NULL on rows that have
|
||
/// never been touched since creation.
|
||
pub last_modified_by_model: Option<String>,
|
||
pub last_modified_by_backend: Option<String>,
|
||
pub last_modified_at: Option<i64>,
|
||
}
|
||
|
||
#[derive(Serialize, Queryable, Clone, Debug)]
|
||
pub struct EntityFact {
|
||
pub id: i32,
|
||
pub subject_entity_id: i32,
|
||
pub predicate: String,
|
||
pub object_entity_id: Option<i32>,
|
||
pub object_value: Option<String>,
|
||
pub source_photo: Option<String>,
|
||
pub source_insight_id: Option<i32>,
|
||
pub confidence: f32,
|
||
pub status: String,
|
||
pub created_at: i64,
|
||
pub persona_id: String,
|
||
pub user_id: i32,
|
||
pub valid_from: Option<i64>,
|
||
pub valid_until: Option<i64>,
|
||
pub superseded_by: Option<i32>,
|
||
pub created_by_model: Option<String>,
|
||
pub created_by_backend: Option<String>,
|
||
pub last_modified_by_model: Option<String>,
|
||
pub last_modified_by_backend: Option<String>,
|
||
pub last_modified_at: Option<i64>,
|
||
}
|
||
|
||
#[derive(Insertable, Clone)]
|
||
#[diesel(table_name = entity_photo_links)]
|
||
pub struct InsertEntityPhotoLink {
|
||
pub entity_id: i32,
|
||
pub library_id: i32,
|
||
#[diesel(column_name = rel_path)]
|
||
pub file_path: String,
|
||
pub role: String,
|
||
}
|
||
|
||
#[derive(Serialize, Queryable, Clone, Debug)]
|
||
pub struct EntityPhotoLink {
|
||
pub id: i32,
|
||
pub entity_id: i32,
|
||
pub library_id: i32,
|
||
#[diesel(column_name = rel_path)]
|
||
pub file_path: String,
|
||
pub role: String,
|
||
}
|
||
|
||
// --- Personas ---
|
||
|
||
#[derive(Insertable)]
|
||
#[diesel(table_name = personas)]
|
||
pub struct InsertPersona<'a> {
|
||
pub user_id: i32,
|
||
pub persona_id: &'a str,
|
||
pub name: &'a str,
|
||
pub system_prompt: &'a str,
|
||
pub is_built_in: bool,
|
||
pub include_all_memories: bool,
|
||
pub created_at: i64,
|
||
pub updated_at: i64,
|
||
/// "Strict mode" — agent reads only see facts with status =
|
||
/// 'reviewed' (human-verified). Default false. See migration
|
||
/// 2026-05-10-000400.
|
||
pub reviewed_only_facts: bool,
|
||
/// Gate for the agent's update_fact / supersede_fact tools.
|
||
/// Default false — fresh personas let the agent create but not
|
||
/// alter or replace. Operator opts in once a model has earned
|
||
/// trust. See migration 2026-05-10-000500.
|
||
pub allow_agent_corrections: bool,
|
||
}
|
||
|
||
#[derive(Serialize, Queryable, Clone, Debug)]
|
||
pub struct Persona {
|
||
pub id: i32,
|
||
pub user_id: i32,
|
||
pub persona_id: String,
|
||
pub name: String,
|
||
pub system_prompt: String,
|
||
pub is_built_in: bool,
|
||
pub include_all_memories: bool,
|
||
pub created_at: i64,
|
||
pub updated_at: i64,
|
||
pub reviewed_only_facts: bool,
|
||
pub allow_agent_corrections: bool,
|
||
}
|
||
|
||
#[derive(Insertable)]
|
||
#[diesel(table_name = video_preview_clips)]
|
||
pub struct InsertVideoPreviewClip {
|
||
pub library_id: i32,
|
||
#[diesel(column_name = rel_path)]
|
||
pub file_path: String,
|
||
pub status: String,
|
||
pub created_at: String,
|
||
pub updated_at: String,
|
||
}
|
||
|
||
#[derive(Serialize, Queryable, Clone, Debug)]
|
||
pub struct VideoPreviewClip {
|
||
pub id: i32,
|
||
pub library_id: i32,
|
||
#[diesel(column_name = rel_path)]
|
||
pub file_path: String,
|
||
pub status: String,
|
||
pub duration_seconds: Option<f32>,
|
||
pub file_size_bytes: Option<i32>,
|
||
pub error_message: Option<String>,
|
||
pub created_at: String,
|
||
pub updated_at: String,
|
||
}
|
||
|
||
#[derive(Insertable)]
|
||
#[diesel(table_name = insight_generation_jobs)]
|
||
pub struct InsertInsightGenerationJob {
|
||
pub library_id: i32,
|
||
#[diesel(column_name = file_path)]
|
||
pub path: String,
|
||
#[diesel(column_name = generation_type)]
|
||
pub gen_type: String,
|
||
pub status: String,
|
||
pub started_at: i64,
|
||
}
|
||
|
||
#[derive(Queryable, Serialize, Clone, Debug)]
|
||
pub struct InsightGenerationJob {
|
||
pub id: i32,
|
||
pub library_id: i32,
|
||
#[diesel(column_name = file_path)]
|
||
pub path: String,
|
||
#[diesel(column_name = generation_type)]
|
||
pub gen_type: String,
|
||
pub status: String,
|
||
pub started_at: i64,
|
||
pub completed_at: Option<i64>,
|
||
pub result_insight_id: Option<i32>,
|
||
pub error_message: Option<String>,
|
||
}
|
||
|
||
// --- Precomputed reels -------------------------------------------------------
|
||
|
||
#[derive(Insertable)]
|
||
#[diesel(table_name = precomputed_reels)]
|
||
pub struct InsertablePrecomputedReel {
|
||
pub span: String,
|
||
pub library_key: String,
|
||
pub cache_key: String,
|
||
pub output_path: String,
|
||
pub title: String,
|
||
pub media_count: i32,
|
||
pub render_version: i32,
|
||
pub tz_offset_minutes: i32,
|
||
pub voice: Option<String>,
|
||
pub generated_at: i64,
|
||
}
|
||
|
||
#[derive(Serialize, Queryable, Clone, Debug)]
|
||
pub struct PrecomputedReel {
|
||
pub id: i32,
|
||
pub span: String,
|
||
pub library_key: String,
|
||
pub cache_key: String,
|
||
pub output_path: String,
|
||
pub title: String,
|
||
pub media_count: i32,
|
||
pub render_version: i32,
|
||
pub tz_offset_minutes: i32,
|
||
pub voice: Option<String>,
|
||
pub generated_at: i64,
|
||
}
|
||
|
||
// --- User AI preferences (Section E) ----------------------------------------
|
||
|
||
#[derive(Queryable, Insertable, Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||
#[diesel(table_name = user_ai_prefs)]
|
||
pub struct UserAiPrefs {
|
||
pub id: i32,
|
||
pub voice: Option<String>,
|
||
pub tz_offset_minutes: Option<i32>,
|
||
pub library: Option<String>,
|
||
pub updated_at: i64,
|
||
/// User's display time zone as an IANA name (DST-proof). NULL = app
|
||
/// default (`camera_tz::DEFAULT_DISPLAY_TZ`). The legacy
|
||
/// `tz_offset_minutes` column stays frozen for rollback.
|
||
pub display_tz: Option<String>,
|
||
/// Last/explicit insight + chat model choice (local id or OpenRouter id).
|
||
/// NULL = current per-request / env default behavior.
|
||
pub default_model: Option<String>,
|
||
/// `"local"` | `"hybrid"` — insight/chat backend. NULL = per-request /
|
||
/// hard default ("local").
|
||
pub default_backend: Option<String>,
|
||
/// Active persona for persona chat. NULL = hard default ("default").
|
||
pub default_persona_id: Option<String>,
|
||
/// Reel span: `"day"` | `"week"` | `"month"`. NULL = per-request /
|
||
/// hard default (day).
|
||
pub reel_span: Option<String>,
|
||
/// Reel segment cap. NULL = per-request / hard default.
|
||
pub reel_max_segments: Option<i32>,
|
||
}
|
||
|
||
#[derive(Insertable, Debug, Clone, serde::Deserialize, serde::Serialize)]
|
||
#[diesel(table_name = user_ai_prefs)]
|
||
pub struct UpsertUserAiPrefs {
|
||
pub voice: Option<String>,
|
||
pub tz_offset_minutes: Option<i32>,
|
||
pub library: Option<String>,
|
||
pub updated_at: i64,
|
||
pub display_tz: Option<String>,
|
||
pub default_model: Option<String>,
|
||
pub default_backend: Option<String>,
|
||
pub default_persona_id: Option<String>,
|
||
pub reel_span: Option<String>,
|
||
pub reel_max_segments: Option<i32>,
|
||
}
|