Skip to main content

tuitbot_core/storage/approval_queue/
mod.rs

1//! Storage operations for the approval queue.
2//!
3//! Provides CRUD operations for queuing posts for human review
4//! when `approval_mode` is enabled.
5
6mod edit_history;
7mod queries;
8#[cfg(test)]
9mod tests;
10
11pub use edit_history::{get_edit_history, record_edit, EditHistoryEntry};
12pub use queries::*;
13
14/// Row type for approval queue queries (expanded with review and QA metadata).
15#[derive(Debug, Clone, sqlx::FromRow)]
16struct ApprovalRow {
17    id: i64,
18    action_type: String,
19    target_tweet_id: String,
20    target_author: String,
21    generated_content: String,
22    topic: String,
23    archetype: String,
24    score: f64,
25    status: String,
26    created_at: String,
27    media_paths: String,
28    reviewed_by: Option<String>,
29    review_notes: Option<String>,
30    reason: Option<String>,
31    detected_risks: String,
32    qa_report: String,
33    qa_hard_flags: String,
34    qa_soft_flags: String,
35    qa_recommendations: String,
36    qa_score: f64,
37    qa_requires_override: i64,
38    qa_override_by: Option<String>,
39    qa_override_note: Option<String>,
40    qa_override_at: Option<String>,
41    source_node_id: Option<i64>,
42    source_seed_id: Option<i64>,
43    source_chunks_json: String,
44    scheduled_for: Option<String>,
45}
46
47/// A pending item in the approval queue.
48#[derive(Debug, Clone, serde::Serialize)]
49pub struct ApprovalItem {
50    pub id: i64,
51    pub action_type: String,
52    pub target_tweet_id: String,
53    pub target_author: String,
54    pub generated_content: String,
55    pub topic: String,
56    pub archetype: String,
57    pub score: f64,
58    pub status: String,
59    pub created_at: String,
60    /// JSON-encoded list of local media file paths.
61    #[serde(serialize_with = "serialize_json_string")]
62    pub media_paths: String,
63    pub reviewed_by: Option<String>,
64    pub review_notes: Option<String>,
65    pub reason: Option<String>,
66    /// JSON-encoded list of detected risks.
67    #[serde(serialize_with = "serialize_json_string")]
68    pub detected_risks: String,
69    /// Full QA report payload as JSON.
70    #[serde(serialize_with = "serialize_json_string")]
71    pub qa_report: String,
72    /// JSON-encoded hard QA flags.
73    #[serde(serialize_with = "serialize_json_string")]
74    pub qa_hard_flags: String,
75    /// JSON-encoded soft QA flags.
76    #[serde(serialize_with = "serialize_json_string")]
77    pub qa_soft_flags: String,
78    /// JSON-encoded QA recommendations.
79    #[serde(serialize_with = "serialize_json_string")]
80    pub qa_recommendations: String,
81    /// QA score summary (0-100).
82    pub qa_score: f64,
83    /// Whether approval requires explicit hard-flag override.
84    pub qa_requires_override: bool,
85    /// Actor who performed override.
86    pub qa_override_by: Option<String>,
87    /// Required override note.
88    pub qa_override_note: Option<String>,
89    /// Timestamp of override action.
90    pub qa_override_at: Option<String>,
91    /// Source content node that influenced this draft.
92    pub source_node_id: Option<i64>,
93    /// Source seed used to generate this draft.
94    pub source_seed_id: Option<i64>,
95    /// JSON array of selected chunk references.
96    #[serde(serialize_with = "serialize_json_string")]
97    pub source_chunks_json: String,
98    /// Optional UTC timestamp preserving the user's scheduling intent.
99    pub scheduled_for: Option<String>,
100}
101
102/// Serialize a JSON-encoded string as a raw JSON value.
103///
104/// The database stores `media_paths` and `detected_risks` as JSON strings.
105/// This serializer emits them as actual JSON arrays in the API response.
106fn serialize_json_string<S: serde::Serializer>(
107    value: &str,
108    serializer: S,
109) -> Result<S::Ok, S::Error> {
110    use serde::Serialize;
111    let parsed: serde_json::Value =
112        serde_json::from_str(value).unwrap_or(serde_json::Value::Array(vec![]));
113    parsed.serialize(serializer)
114}
115
116impl From<ApprovalRow> for ApprovalItem {
117    fn from(r: ApprovalRow) -> Self {
118        Self {
119            id: r.id,
120            action_type: r.action_type,
121            target_tweet_id: r.target_tweet_id,
122            target_author: r.target_author,
123            generated_content: r.generated_content,
124            topic: r.topic,
125            archetype: r.archetype,
126            score: r.score,
127            status: r.status,
128            created_at: r.created_at,
129            media_paths: r.media_paths,
130            reviewed_by: r.reviewed_by,
131            review_notes: r.review_notes,
132            reason: r.reason,
133            detected_risks: r.detected_risks,
134            qa_report: r.qa_report,
135            qa_hard_flags: r.qa_hard_flags,
136            qa_soft_flags: r.qa_soft_flags,
137            qa_recommendations: r.qa_recommendations,
138            qa_score: r.qa_score,
139            qa_requires_override: r.qa_requires_override != 0,
140            qa_override_by: r.qa_override_by,
141            qa_override_note: r.qa_override_note,
142            qa_override_at: r.qa_override_at,
143            source_node_id: r.source_node_id,
144            source_seed_id: r.source_seed_id,
145            source_chunks_json: r.source_chunks_json,
146            scheduled_for: r.scheduled_for,
147        }
148    }
149}
150
151/// Counts of approval items grouped by status.
152#[derive(Debug, Clone, serde::Serialize)]
153pub struct ApprovalStats {
154    pub pending: i64,
155    pub approved: i64,
156    pub rejected: i64,
157    pub failed: i64,
158    pub scheduled: i64,
159}
160
161/// Optional review metadata for approve/reject actions.
162#[derive(Debug, Clone, Default, serde::Deserialize)]
163pub struct ReviewAction {
164    pub actor: Option<String>,
165    pub notes: Option<String>,
166}