summaryrefslogblamecommitdiff
path: root/makima/src/db/models.rs
blob: e16c43f9257c2692bc9aefc011c4913160b20b5d (plain) (tree)
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435



















                                                                      

















                                                         









                                         













                                          

                                                                               

 





                                                     



                                                                               




                                         




                                                              

                                             





                                                                            







                                            

                                                                                  



                                                         

                                             


                                                                






                                                                      











                                                  



                                          

                                                                           

                                                          














                                                                





                                                   




                                                      

                                             



                                         












                                     


                                                                                



                                                                         
                                  

                                                    




                                        































































































                                                                                

























































































                                                                                

                                                              
                                     
                                                       






                                    




                                                                                 
























                                                             























                                                                 

































                                                                              





                                                    








                                                                            


                                          



                                  






















                                                                     











                                           

                                                







                                             


                                                                                 



















                                                                             

                                                















                                                

                                                                                   


























































































































































































































































                                                                                                








































                                                                                




































































































                                                                                






























































































































































































































































































































































































































































                                                                                
//! Database models for the files table.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::FromRow;
use utoipa::ToSchema;
use uuid::Uuid;

/// TranscriptEntry stored in JSONB - matches frontend TranscriptEntry
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct TranscriptEntry {
    pub id: String,
    pub speaker: String,
    pub start: f32,
    pub end: f32,
    pub text: String,
    pub is_final: bool,
}

/// Chart type for visualization elements
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum ChartType {
    Line,
    Bar,
    Pie,
    Area,
}

/// Body element types for structured file content
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum BodyElement {
    /// Heading element (h1-h6)
    Heading { level: u8, text: String },
    /// Paragraph text
    Paragraph { text: String },
    /// Code block with optional language
    Code {
        language: Option<String>,
        content: String,
    },
    /// List (ordered or unordered)
    List {
        ordered: bool,
        items: Vec<String>,
    },
    /// Chart visualization
    Chart {
        #[serde(rename = "chartType")]
        chart_type: ChartType,
        title: Option<String>,
        data: serde_json::Value,
        config: Option<serde_json::Value>,
    },
    /// Image element (deferred for MVP)
    Image {
        src: String,
        alt: Option<String>,
        caption: Option<String>,
    },
    /// Raw markdown content - renders as formatted markdown, edits as raw text
    Markdown { content: String },
}

/// File record from the database.
#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct File {
    pub id: Uuid,
    pub owner_id: Uuid,
    /// Contract this file belongs to (optional)
    pub contract_id: Option<Uuid>,
    /// Phase of the contract when file was added (e.g., "research", "specify")
    pub contract_phase: Option<String>,
    pub name: String,
    pub description: Option<String>,
    #[sqlx(json)]
    pub transcript: Vec<TranscriptEntry>,
    pub location: Option<String>,
    /// AI-generated summary of the transcript
    pub summary: Option<String>,
    /// Structured body content (headings, paragraphs, charts)
    #[sqlx(json)]
    pub body: Vec<BodyElement>,
    /// Version number for optimistic locking
    pub version: i32,
    /// Path to linked repository file (e.g., "README.md", "docs/design.md")
    pub repo_file_path: Option<String>,
    /// When the file was last synced from the repository
    pub repo_synced_at: Option<DateTime<Utc>>,
    /// Sync status: 'none', 'synced', 'modified', 'conflict'
    pub repo_sync_status: Option<String>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

/// Request payload for creating a new file.
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreateFileRequest {
    /// Contract this file belongs to (required - files must belong to a contract)
    pub contract_id: Uuid,
    /// Name of the file (auto-generated if not provided)
    pub name: Option<String>,
    /// Optional description
    pub description: Option<String>,
    /// Transcript entries (default to empty)
    #[serde(default)]
    pub transcript: Vec<TranscriptEntry>,
    /// Storage location (e.g., s3://bucket/path) - not used yet
    pub location: Option<String>,
    /// Initial body elements (e.g., from a template)
    #[serde(default)]
    pub body: Vec<BodyElement>,
    /// Path to linked repository file (e.g., "README.md")
    pub repo_file_path: Option<String>,
    /// Contract phase this file belongs to (for deliverable tracking)
    pub contract_phase: Option<String>,
}

/// Request payload for updating an existing file.
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct UpdateFileRequest {
    /// New name (optional)
    pub name: Option<String>,
    /// New description (optional)
    pub description: Option<String>,
    /// New transcript (optional)
    pub transcript: Option<Vec<TranscriptEntry>>,
    /// AI-generated summary (optional)
    pub summary: Option<String>,
    /// Structured body content (optional)
    pub body: Option<Vec<BodyElement>>,
    /// Version for optimistic locking (required for updates from frontend)
    pub version: Option<i32>,
    /// Path to linked repository file (e.g., "README.md")
    pub repo_file_path: Option<String>,
}

/// Response for file list endpoint.
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct FileListResponse {
    pub files: Vec<FileSummary>,
    pub total: i64,
}

/// Summary of a file for list views (excludes full transcript).
#[derive(Debug, Clone, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct FileSummary {
    pub id: Uuid,
    /// Contract this file belongs to
    pub contract_id: Option<Uuid>,
    /// Contract name (joined from contracts table)
    pub contract_name: Option<String>,
    /// Phase when file was added to contract
    pub contract_phase: Option<String>,
    pub name: String,
    pub description: Option<String>,
    pub transcript_count: usize,
    /// Duration derived from last transcript end time
    pub duration: Option<f32>,
    /// Version number for optimistic locking
    pub version: i32,
    /// Path to linked repository file
    pub repo_file_path: Option<String>,
    /// Sync status with repository
    pub repo_sync_status: Option<String>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

impl From<File> for FileSummary {
    fn from(file: File) -> Self {
        let duration = file
            .transcript
            .iter()
            .map(|t| t.end)
            .fold(0.0_f32, f32::max);
        Self {
            id: file.id,
            contract_id: file.contract_id,
            contract_name: None, // Not available from File alone, requires JOIN
            contract_phase: file.contract_phase,
            name: file.name,
            description: file.description,
            transcript_count: file.transcript.len(),
            duration: if duration > 0.0 { Some(duration) } else { None },
            version: file.version,
            repo_file_path: file.repo_file_path,
            repo_sync_status: file.repo_sync_status,
            created_at: file.created_at,
            updated_at: file.updated_at,
        }
    }
}

// =============================================================================
// Version History Types
// =============================================================================

/// Source of a version change
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, sqlx::Type)]
#[sqlx(type_name = "varchar")]
#[serde(rename_all = "lowercase")]
pub enum VersionSource {
    #[sqlx(rename = "user")]
    User,
    #[sqlx(rename = "llm")]
    Llm,
    #[sqlx(rename = "system")]
    System,
}

impl std::fmt::Display for VersionSource {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            VersionSource::User => write!(f, "user"),
            VersionSource::Llm => write!(f, "llm"),
            VersionSource::System => write!(f, "system"),
        }
    }
}

impl std::str::FromStr for VersionSource {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "user" => Ok(VersionSource::User),
            "llm" => Ok(VersionSource::Llm),
            "system" => Ok(VersionSource::System),
            _ => Err(format!("Unknown version source: {}", s)),
        }
    }
}

/// Full version record from the database
#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct FileVersion {
    pub id: Uuid,
    pub file_id: Uuid,
    pub version: i32,
    pub name: String,
    pub description: Option<String>,
    pub summary: Option<String>,
    #[sqlx(json)]
    pub body: Vec<BodyElement>,
    pub source: String,
    pub change_description: Option<String>,
    pub created_at: DateTime<Utc>,
}

/// Summary of a version for list views
#[derive(Debug, Clone, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct FileVersionSummary {
    pub version: i32,
    pub source: String,
    pub created_at: DateTime<Utc>,
    pub change_description: Option<String>,
}

impl From<FileVersion> for FileVersionSummary {
    fn from(v: FileVersion) -> Self {
        Self {
            version: v.version,
            source: v.source,
            created_at: v.created_at,
            change_description: v.change_description,
        }
    }
}

/// Response for version list endpoint
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct FileVersionListResponse {
    pub versions: Vec<FileVersionSummary>,
    pub total: i64,
}

/// Request to restore a file to a previous version
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct RestoreVersionRequest {
    /// The version to restore to
    pub target_version: i32,
    /// The current version (for optimistic locking)
    pub current_version: i32,
}

// =============================================================================
// Mesh/Task Types
// =============================================================================

/// Task status for orchestrating Claude Code instances
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum TaskStatus {
    Pending,
    Running,
    Paused,
    Blocked,
    Done,
    Failed,
    Merged,
}

impl std::fmt::Display for TaskStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TaskStatus::Pending => write!(f, "pending"),
            TaskStatus::Running => write!(f, "running"),
            TaskStatus::Paused => write!(f, "paused"),
            TaskStatus::Blocked => write!(f, "blocked"),
            TaskStatus::Done => write!(f, "done"),
            TaskStatus::Failed => write!(f, "failed"),
            TaskStatus::Merged => write!(f, "merged"),
        }
    }
}

impl std::str::FromStr for TaskStatus {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "pending" => Ok(TaskStatus::Pending),
            "running" => Ok(TaskStatus::Running),
            "paused" => Ok(TaskStatus::Paused),
            "blocked" => Ok(TaskStatus::Blocked),
            "done" => Ok(TaskStatus::Done),
            "failed" => Ok(TaskStatus::Failed),
            "merged" => Ok(TaskStatus::Merged),
            _ => Err(format!("Unknown task status: {}", s)),
        }
    }
}

/// Merge mode for task completion
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum MergeMode {
    /// Create a PR for review
    Pr,
    /// Auto-merge to target branch
    Auto,
    /// Manual merge by user
    Manual,
}

impl std::fmt::Display for MergeMode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            MergeMode::Pr => write!(f, "pr"),
            MergeMode::Auto => write!(f, "auto"),
            MergeMode::Manual => write!(f, "manual"),
        }
    }
}

impl std::str::FromStr for MergeMode {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "pr" => Ok(MergeMode::Pr),
            "auto" => Ok(MergeMode::Auto),
            "manual" => Ok(MergeMode::Manual),
            _ => Err(format!("Unknown merge mode: {}", s)),
        }
    }
}

/// Task record from the database
#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct Task {
    pub id: Uuid,
    pub owner_id: Uuid,
    /// Contract this task belongs to (required for new tasks)
    pub contract_id: Option<Uuid>,
    pub parent_task_id: Option<Uuid>,
    /// Depth in task hierarchy (no longer constrained)
    pub depth: i32,
    pub name: String,
    pub description: Option<String>,
    pub status: String,
    pub priority: i32,
    pub plan: String,

    // Supervisor flag
    /// True for contract supervisor tasks. Only supervisors can spawn new tasks.
    #[serde(default)]
    pub is_supervisor: bool,

    // Daemon/container info
    pub daemon_id: Option<Uuid>,
    pub container_id: Option<String>,
    pub overlay_path: Option<String>,

    // Repository info
    pub repository_url: Option<String>,
    pub base_branch: Option<String>,
    pub target_branch: Option<String>,

    // Merge settings
    pub merge_mode: Option<String>,
    pub pr_url: Option<String>,

    // Completion action settings
    /// Path to user's local repository (outside ~/.makima)
    pub target_repo_path: Option<String>,
    /// Action on completion: "none", "branch", "merge", "pr"
    pub completion_action: Option<String>,

    // Progress tracking
    pub progress_summary: Option<String>,
    pub last_output: Option<String>,
    pub error_message: Option<String>,

    // Git checkpoint tracking
    /// Git commit SHA of the most recent checkpoint
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_checkpoint_sha: Option<String>,
    /// Number of checkpoints created by this task
    #[serde(default)]
    pub checkpoint_count: i32,
    /// Message from the most recent checkpoint
    #[serde(skip_serializing_if = "Option::is_none")]
    pub checkpoint_message: Option<String>,

    // Conversation state for resumption
    /// Saved conversation context for task/supervisor resumption
    #[serde(skip_serializing_if = "Option::is_none")]
    pub conversation_state: Option<serde_json::Value>,

    // Daemon migration tracking
    /// Previous daemon if task was migrated
    #[serde(skip_serializing_if = "Option::is_none")]
    pub migrated_from_daemon_id: Option<Uuid>,
    /// Most recent daemon that worked on this task
    #[serde(skip_serializing_if = "Option::is_none")]
    pub last_active_daemon_id: Option<Uuid>,

    // Timestamps
    pub started_at: Option<DateTime<Utc>>,
    pub completed_at: Option<DateTime<Utc>>,
    pub version: i32,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,

    // Task continuation
    /// Task ID to continue from (copy worktree from this task when starting).
    /// Used for sequential subtask dependencies.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub continue_from_task_id: Option<Uuid>,
    /// Files to copy from parent task's worktree when starting.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub copy_files: Option<serde_json::Value>,
}

impl Task {
    /// Parse status string to TaskStatus enum
    pub fn status_enum(&self) -> Result<TaskStatus, String> {
        self.status.parse()
    }

    /// Parse merge_mode string to MergeMode enum
    pub fn merge_mode_enum(&self) -> Option<Result<MergeMode, String>> {
        self.merge_mode.as_ref().map(|s| s.parse())
    }
}

/// Summary of a task for list views
#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct TaskSummary {
    pub id: Uuid,
    /// Contract this task belongs to
    pub contract_id: Option<Uuid>,
    /// Contract name (joined from contracts table)
    pub contract_name: Option<String>,
    /// Contract phase (joined from contracts table)
    pub contract_phase: Option<String>,
    pub parent_task_id: Option<Uuid>,
    /// Depth in task hierarchy: 0=orchestrator (top-level), 1=subtask (max)
    pub depth: i32,
    pub name: String,
    pub status: String,
    pub priority: i32,
    pub progress_summary: Option<String>,
    pub subtask_count: i64,
    pub version: i32,
    /// True for contract supervisor tasks
    #[serde(default)]
    pub is_supervisor: bool,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

/// Convert a full Task to a TaskSummary
impl From<Task> for TaskSummary {
    fn from(task: Task) -> Self {
        Self {
            id: task.id,
            contract_id: task.contract_id,
            contract_name: None, // Not available from Task directly
            contract_phase: None, // Not available from Task directly
            parent_task_id: task.parent_task_id,
            depth: task.depth,
            name: task.name,
            status: task.status,
            priority: task.priority,
            progress_summary: task.progress_summary,
            subtask_count: 0, // Would need separate query
            version: task.version,
            is_supervisor: task.is_supervisor,
            created_at: task.created_at,
            updated_at: task.updated_at,
        }
    }
}

/// Response for task list endpoint
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct TaskListResponse {
    pub tasks: Vec<TaskSummary>,
    pub total: i64,
}

/// Request payload for creating a new task
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreateTaskRequest {
    /// Contract this task belongs to (required)
    pub contract_id: Uuid,
    /// Name of the task
    pub name: String,
    /// Optional description
    pub description: Option<String>,
    /// The plan/instructions for Claude Code
    pub plan: String,
    /// Parent task ID (for subtasks)
    pub parent_task_id: Option<Uuid>,
    /// True for contract supervisor tasks. Only supervisors can spawn new tasks.
    #[serde(default)]
    pub is_supervisor: bool,
    /// Priority (higher = more urgent)
    #[serde(default)]
    pub priority: i32,
    /// Repository URL
    pub repository_url: Option<String>,
    /// Base branch for overlay
    pub base_branch: Option<String>,
    /// Target branch to merge into
    pub target_branch: Option<String>,
    /// Merge mode (pr, auto, manual)
    pub merge_mode: Option<String>,
    /// Path to user's local repository (outside ~/.makima)
    pub target_repo_path: Option<String>,
    /// Action on completion: "none", "branch", "merge", "pr"
    pub completion_action: Option<String>,
    /// Task ID to continue from (copy worktree from this task when starting)
    pub continue_from_task_id: Option<Uuid>,
    /// Files to copy from parent task's worktree when starting
    #[serde(skip_serializing_if = "Option::is_none")]
    pub copy_files: Option<Vec<String>>,
    /// Checkpoint SHA to branch from (optional)
    pub checkpoint_sha: Option<String>,
}

/// Request payload for updating a task
#[derive(Debug, Default, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct UpdateTaskRequest {
    pub name: Option<String>,
    pub description: Option<String>,
    pub plan: Option<String>,
    pub status: Option<String>,
    pub priority: Option<i32>,
    pub progress_summary: Option<String>,
    pub last_output: Option<String>,
    pub error_message: Option<String>,
    pub merge_mode: Option<String>,
    pub pr_url: Option<String>,
    /// Repository URL for the task (e.g., when updating supervisor with repo info)
    pub repository_url: Option<String>,
    /// Path to user's local repository (outside ~/.makima)
    pub target_repo_path: Option<String>,
    /// Action on completion: "none", "branch", "merge", "pr"
    pub completion_action: Option<String>,
    /// The daemon currently running this task
    pub daemon_id: Option<Uuid>,
    /// Explicitly clear daemon_id (set to NULL)
    #[serde(default)]
    pub clear_daemon_id: bool,
    /// Version for optimistic locking
    pub version: Option<i32>,
}

/// Task with its subtasks for detail view
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct TaskWithSubtasks {
    #[serde(flatten)]
    pub task: Task,
    pub subtasks: Vec<TaskSummary>,
}

/// Request to send a message to a running task's stdin.
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct SendMessageRequest {
    /// The message to send to the task's stdin.
    pub message: String,
}

// =============================================================================
// Daemon Types
// =============================================================================

/// Daemon status
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum DaemonStatus {
    Connected,
    Disconnected,
    Unhealthy,
}

impl std::fmt::Display for DaemonStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            DaemonStatus::Connected => write!(f, "connected"),
            DaemonStatus::Disconnected => write!(f, "disconnected"),
            DaemonStatus::Unhealthy => write!(f, "unhealthy"),
        }
    }
}

impl std::str::FromStr for DaemonStatus {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "connected" => Ok(DaemonStatus::Connected),
            "disconnected" => Ok(DaemonStatus::Disconnected),
            "unhealthy" => Ok(DaemonStatus::Unhealthy),
            _ => Err(format!("Unknown daemon status: {}", s)),
        }
    }
}

/// Connected daemon record from the database
#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct Daemon {
    pub id: Uuid,
    pub owner_id: Uuid,
    pub connection_id: String,
    pub hostname: Option<String>,
    pub machine_id: Option<String>,
    pub max_concurrent_tasks: i32,
    pub current_task_count: i32,
    pub status: String,
    pub last_heartbeat_at: DateTime<Utc>,
    pub connected_at: DateTime<Utc>,
    pub disconnected_at: Option<DateTime<Utc>>,
}

impl Daemon {
    /// Parse status string to DaemonStatus enum
    pub fn status_enum(&self) -> Result<DaemonStatus, String> {
        self.status.parse()
    }
}

/// Response for daemon list endpoint
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct DaemonListResponse {
    pub daemons: Vec<Daemon>,
    pub total: i64,
}

/// Response for daemon directories endpoint
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct DaemonDirectoriesResponse {
    /// List of suggested directories from connected daemons
    pub directories: Vec<DaemonDirectory>,
}

/// A suggested directory from a daemon
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct DaemonDirectory {
    /// Path to the directory
    pub path: String,
    /// Display label for the directory
    pub label: String,
    /// Type of directory: "working", "makima", "worktrees"
    pub directory_type: String,
    /// Daemon hostname this directory is from
    pub hostname: Option<String>,
    /// Whether the directory already exists (for validation)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub exists: Option<bool>,
}

// =============================================================================
// Task Event Types
// =============================================================================

/// Task event record from the database
#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct TaskEvent {
    pub id: Uuid,
    pub task_id: Uuid,
    pub event_type: String,
    pub previous_status: Option<String>,
    pub new_status: Option<String>,
    #[sqlx(json)]
    pub event_data: Option<serde_json::Value>,
    pub created_at: DateTime<Utc>,
}

/// Response for task events list endpoint
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct TaskEventListResponse {
    pub events: Vec<TaskEvent>,
    pub total: i64,
}

/// A single output entry from a Claude Code task
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct TaskOutputEntry {
    pub id: Uuid,
    pub task_id: Uuid,
    /// Message type: "assistant", "tool_use", "tool_result", "result", "system", "error", "raw"
    pub message_type: String,
    /// Main text content
    pub content: String,
    /// Tool name if tool_use message
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tool_name: Option<String>,
    /// Tool input JSON if tool_use message
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tool_input: Option<serde_json::Value>,
    /// Whether tool result was an error
    #[serde(skip_serializing_if = "Option::is_none")]
    pub is_error: Option<bool>,
    /// Cost in USD if result message
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cost_usd: Option<f64>,
    /// Duration in ms if result message
    #[serde(skip_serializing_if = "Option::is_none")]
    pub duration_ms: Option<u64>,
    /// Timestamp when this output was recorded
    pub created_at: DateTime<Utc>,
}

impl TaskOutputEntry {
    /// Convert a TaskEvent with event_type='output' to a TaskOutputEntry
    pub fn from_task_event(event: TaskEvent) -> Option<Self> {
        if event.event_type != "output" {
            return None;
        }
        let data = event.event_data?;
        Some(Self {
            id: event.id,
            task_id: event.task_id,
            message_type: data.get("messageType")?.as_str()?.to_string(),
            content: data.get("content")?.as_str().unwrap_or("").to_string(),
            tool_name: data.get("toolName").and_then(|v| v.as_str()).map(|s| s.to_string()),
            tool_input: data.get("toolInput").cloned(),
            is_error: data.get("isError").and_then(|v| v.as_bool()),
            cost_usd: data.get("costUsd").and_then(|v| v.as_f64()),
            duration_ms: data.get("durationMs").and_then(|v| v.as_u64()),
            created_at: event.created_at,
        })
    }
}

/// Response for task output history endpoint
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct TaskOutputResponse {
    pub entries: Vec<TaskOutputEntry>,
    pub total: usize,
    pub task_id: Uuid,
}

// =============================================================================
// Mesh Chat History Types
// =============================================================================

/// Mesh chat conversation for persisting history
#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MeshChatConversation {
    pub id: Uuid,
    pub owner_id: Uuid,
    pub name: Option<String>,
    pub is_active: bool,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

/// Individual message in a mesh chat conversation
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MeshChatMessageRecord {
    pub id: Uuid,
    pub conversation_id: Uuid,
    pub role: String,
    pub content: String,
    pub context_type: String,
    pub context_task_id: Option<Uuid>,
    /// Tool calls made during this message (JSON, nullable)
    pub tool_calls: Option<serde_json::Value>,
    /// Pending questions requiring user response (JSON, nullable)
    pub pending_questions: Option<serde_json::Value>,
    pub created_at: DateTime<Utc>,
}

/// Response for chat history endpoint
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MeshChatHistoryResponse {
    pub conversation_id: Uuid,
    pub messages: Vec<MeshChatMessageRecord>,
}

// =============================================================================
// Contract Chat History Types
// =============================================================================

/// Conversation thread for contract chat (scoped to a specific contract)
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ContractChatConversation {
    pub id: Uuid,
    pub contract_id: Uuid,
    pub owner_id: Uuid,
    pub name: Option<String>,
    pub is_active: bool,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

/// Individual message in a contract chat conversation
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ContractChatMessageRecord {
    pub id: Uuid,
    pub conversation_id: Uuid,
    pub role: String,
    pub content: String,
    /// Tool calls made during this message (JSON, nullable)
    pub tool_calls: Option<serde_json::Value>,
    /// Pending questions requiring user response (JSON, nullable)
    pub pending_questions: Option<serde_json::Value>,
    pub created_at: DateTime<Utc>,
}

/// Response for contract chat history endpoint
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ContractChatHistoryResponse {
    pub contract_id: Uuid,
    pub conversation_id: Uuid,
    pub messages: Vec<ContractChatMessageRecord>,
}

// =============================================================================
// Merge API Types
// =============================================================================

/// Information about a task branch
#[derive(Debug, Clone, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct BranchInfo {
    /// Full branch name
    pub name: String,
    /// Task ID extracted from branch name (if parseable)
    pub task_id: Option<Uuid>,
    /// Whether this branch has been merged
    pub is_merged: bool,
    /// Short SHA of the last commit
    pub last_commit: String,
    /// Subject line of the last commit
    pub last_commit_message: String,
}

/// Response for branch list endpoint
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct BranchListResponse {
    pub branches: Vec<BranchInfo>,
}

/// Request to start a merge
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MergeStartRequest {
    /// Branch name to merge
    pub source_branch: String,
}

/// Current merge state
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MergeStatusResponse {
    /// Whether a merge is in progress
    pub in_progress: bool,
    /// Branch being merged (if in progress)
    pub source_branch: Option<String>,
    /// Files with unresolved conflicts
    pub conflicted_files: Vec<String>,
}

/// Request to resolve a conflict
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MergeResolveRequest {
    /// File path to resolve
    pub file: String,
    /// Resolution strategy: "ours" or "theirs"
    pub strategy: String,
}

/// Request to commit a merge
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MergeCommitRequest {
    /// Commit message
    pub message: String,
}

/// Request to skip a subtask branch
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MergeSkipRequest {
    /// Subtask ID to skip
    pub subtask_id: Uuid,
    /// Reason for skipping
    pub reason: String,
}

/// Result of a merge operation
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MergeResultResponse {
    /// Whether the operation succeeded
    pub success: bool,
    /// Human-readable message
    pub message: String,
    /// Commit SHA (if a commit was created)
    pub commit_sha: Option<String>,
    /// Conflicted files (if conflicts occurred)
    pub conflicts: Option<Vec<String>>,
}

/// Response to check if all branches are merged
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct MergeCompleteCheckResponse {
    /// Whether the orchestrator can mark itself as complete
    pub can_complete: bool,
    /// Branches not yet merged or skipped
    pub unmerged_branches: Vec<String>,
    /// Count of merged branches
    pub merged_count: u32,
    /// Count of skipped branches
    pub skipped_count: u32,
}

// =============================================================================
// Contract Types
// =============================================================================

/// Contract phase for workflow progression
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum ContractPhase {
    Research,
    Specify,
    Plan,
    Execute,
    Review,
}

impl std::fmt::Display for ContractPhase {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ContractPhase::Research => write!(f, "research"),
            ContractPhase::Specify => write!(f, "specify"),
            ContractPhase::Plan => write!(f, "plan"),
            ContractPhase::Execute => write!(f, "execute"),
            ContractPhase::Review => write!(f, "review"),
        }
    }
}

impl std::str::FromStr for ContractPhase {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "research" => Ok(ContractPhase::Research),
            "specify" => Ok(ContractPhase::Specify),
            "plan" => Ok(ContractPhase::Plan),
            "execute" => Ok(ContractPhase::Execute),
            "review" => Ok(ContractPhase::Review),
            _ => Err(format!("Unknown contract phase: {}", s)),
        }
    }
}

/// Contract status
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum ContractStatus {
    Active,
    Completed,
    Archived,
}

impl std::fmt::Display for ContractStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ContractStatus::Active => write!(f, "active"),
            ContractStatus::Completed => write!(f, "completed"),
            ContractStatus::Archived => write!(f, "archived"),
        }
    }
}

impl std::str::FromStr for ContractStatus {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "active" => Ok(ContractStatus::Active),
            "completed" => Ok(ContractStatus::Completed),
            "archived" => Ok(ContractStatus::Archived),
            _ => Err(format!("Unknown contract status: {}", s)),
        }
    }
}

/// Repository source type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum RepositorySourceType {
    /// Existing remote repo (GitHub, GitLab, etc)
    Remote,
    /// Existing local repo
    Local,
    /// New repo created/managed by Makima daemon
    Managed,
}

impl std::fmt::Display for RepositorySourceType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            RepositorySourceType::Remote => write!(f, "remote"),
            RepositorySourceType::Local => write!(f, "local"),
            RepositorySourceType::Managed => write!(f, "managed"),
        }
    }
}

impl std::str::FromStr for RepositorySourceType {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "remote" => Ok(RepositorySourceType::Remote),
            "local" => Ok(RepositorySourceType::Local),
            "managed" => Ok(RepositorySourceType::Managed),
            _ => Err(format!("Unknown repository source type: {}", s)),
        }
    }
}

/// Repository status (for managed repos)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum RepositoryStatus {
    /// Repo is usable
    Ready,
    /// Waiting for daemon to create
    Pending,
    /// Daemon is creating the repo
    Creating,
    /// Creation failed
    Failed,
}

impl std::fmt::Display for RepositoryStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            RepositoryStatus::Ready => write!(f, "ready"),
            RepositoryStatus::Pending => write!(f, "pending"),
            RepositoryStatus::Creating => write!(f, "creating"),
            RepositoryStatus::Failed => write!(f, "failed"),
        }
    }
}

impl std::str::FromStr for RepositoryStatus {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "ready" => Ok(RepositoryStatus::Ready),
            "pending" => Ok(RepositoryStatus::Pending),
            "creating" => Ok(RepositoryStatus::Creating),
            "failed" => Ok(RepositoryStatus::Failed),
            _ => Err(format!("Unknown repository status: {}", s)),
        }
    }
}

/// Contract record from the database
#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct Contract {
    pub id: Uuid,
    pub owner_id: Uuid,
    pub name: String,
    pub description: Option<String>,
    pub phase: String,
    pub status: String,
    /// The long-running supervisor task that orchestrates this contract
    #[serde(skip_serializing_if = "Option::is_none")]
    pub supervisor_task_id: Option<Uuid>,
    pub version: i32,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

impl Contract {
    /// Parse phase string to ContractPhase enum
    pub fn phase_enum(&self) -> Result<ContractPhase, String> {
        self.phase.parse()
    }

    /// Parse status string to ContractStatus enum
    pub fn status_enum(&self) -> Result<ContractStatus, String> {
        self.status.parse()
    }
}

/// Contract repository record from the database
#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ContractRepository {
    pub id: Uuid,
    pub contract_id: Uuid,
    pub name: String,
    pub repository_url: Option<String>,
    pub local_path: Option<String>,
    pub source_type: String,
    pub status: String,
    pub is_primary: bool,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

impl ContractRepository {
    /// Parse source_type string to RepositorySourceType enum
    pub fn source_type_enum(&self) -> Result<RepositorySourceType, String> {
        self.source_type.parse()
    }

    /// Parse status string to RepositoryStatus enum
    pub fn status_enum(&self) -> Result<RepositoryStatus, String> {
        self.status.parse()
    }
}

/// Summary of a contract for list views
#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ContractSummary {
    pub id: Uuid,
    pub name: String,
    pub description: Option<String>,
    pub phase: String,
    pub status: String,
    pub file_count: i64,
    pub task_count: i64,
    pub repository_count: i64,
    pub version: i32,
    pub created_at: DateTime<Utc>,
}

/// Contract with all relations for detail view
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ContractWithRelations {
    #[serde(flatten)]
    pub contract: Contract,
    pub repositories: Vec<ContractRepository>,
    pub files: Vec<FileSummary>,
    pub tasks: Vec<TaskSummary>,
}

/// Response for contract list endpoint
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ContractListResponse {
    pub contracts: Vec<ContractSummary>,
    pub total: i64,
}

/// Request payload for creating a new contract
#[derive(Debug, Clone, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreateContractRequest {
    /// Name of the contract
    pub name: String,
    /// Optional description
    pub description: Option<String>,
    /// Initial phase to start in (defaults to "research")
    /// Valid values: "research", "specify", "plan", "execute", "review"
    #[serde(default)]
    pub initial_phase: Option<String>,
}

/// Request payload for updating a contract
#[derive(Debug, Default, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct UpdateContractRequest {
    pub name: Option<String>,
    pub description: Option<String>,
    pub phase: Option<String>,
    pub status: Option<String>,
    /// Supervisor task ID for contract orchestration
    #[serde(skip_serializing_if = "Option::is_none")]
    pub supervisor_task_id: Option<Uuid>,
    /// Version for optimistic locking
    pub version: Option<i32>,
}

/// Request to add a remote repository to a contract
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct AddRemoteRepositoryRequest {
    pub name: String,
    pub repository_url: String,
    #[serde(default)]
    pub is_primary: bool,
}

/// Request to add a local repository to a contract
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct AddLocalRepositoryRequest {
    pub name: String,
    pub local_path: String,
    #[serde(default)]
    pub is_primary: bool,
}

/// Request to create a managed repository (daemon will create it)
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreateManagedRepositoryRequest {
    pub name: String,
    #[serde(default)]
    pub is_primary: bool,
}

/// Request to change contract phase
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ChangePhaseRequest {
    pub phase: String,
}

/// Contract event record from the database
#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ContractEvent {
    pub id: Uuid,
    pub contract_id: Uuid,
    pub event_type: String,
    pub previous_phase: Option<String>,
    pub new_phase: Option<String>,
    #[sqlx(json)]
    pub event_data: Option<serde_json::Value>,
    pub created_at: DateTime<Utc>,
}

/// Response for contract events list endpoint
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ContractEventListResponse {
    pub events: Vec<ContractEvent>,
    pub total: i64,
}

// ============================================================================
// Task Checkpoints (for git checkpoint tracking)
// ============================================================================

/// Task checkpoint record - represents a git commit checkpoint
#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct TaskCheckpoint {
    pub id: Uuid,
    pub task_id: Uuid,
    /// Sequential checkpoint number within this task
    pub checkpoint_number: i32,
    /// Git commit SHA
    pub commit_sha: String,
    /// Git branch name
    pub branch_name: String,
    /// Commit message
    pub message: String,
    /// Files changed in this commit: [{path, action: 'A'|'M'|'D'}]
    #[sqlx(json)]
    pub files_changed: Option<serde_json::Value>,
    /// Lines added in this commit
    pub lines_added: Option<i32>,
    /// Lines removed in this commit
    pub lines_removed: Option<i32>,
    pub created_at: DateTime<Utc>,
}

/// Request to create a checkpoint
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CreateCheckpointRequest {
    /// Commit message
    pub message: String,
}

/// Response for checkpoint list endpoint
#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct CheckpointListResponse {
    pub checkpoints: Vec<TaskCheckpoint>,
    pub total: i64,
}

// ============================================================================
// Supervisor State (for supervisor resumability)
// ============================================================================

/// Supervisor state for contract supervisor tasks
/// Enables resumption after interruption
#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct SupervisorState {
    pub id: Uuid,
    pub contract_id: Uuid,
    pub task_id: Uuid,
    /// Full Claude conversation history for resumption
    #[sqlx(json)]
    pub conversation_history: serde_json::Value,
    /// Last checkpoint this supervisor created
    pub last_checkpoint_id: Option<Uuid>,
    /// Tasks the supervisor is waiting on
    #[sqlx(try_from = "Vec<Uuid>")]
    pub pending_task_ids: Vec<Uuid>,
    /// Current contract phase when supervisor was last active
    pub phase: String,
    /// When supervisor was last active
    pub last_activity: DateTime<Utc>,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

/// Request to update supervisor state
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct UpdateSupervisorStateRequest {
    /// Updated conversation history
    pub conversation_history: Option<serde_json::Value>,
    /// Tasks the supervisor is waiting on
    pub pending_task_ids: Option<Vec<Uuid>>,
    /// Current contract phase
    pub phase: Option<String>,
}

// ============================================================================
// Daemon Task Assignments (for multi-daemon support)
// ============================================================================

/// Daemon task assignment record
#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct DaemonTaskAssignment {
    pub id: Uuid,
    pub daemon_id: Uuid,
    pub task_id: Uuid,
    pub assigned_at: DateTime<Utc>,
    /// Status: 'active', 'migrating', 'completed'
    pub status: String,
}

/// Extended daemon info for selection
#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct DaemonWithCapacity {
    pub id: Uuid,
    pub owner_id: Uuid,
    pub connection_id: String,
    pub hostname: Option<String>,
    pub machine_id: Option<String>,
    pub max_concurrent_tasks: i32,
    pub current_task_count: i32,
    pub capacity_score: Option<i32>,
    pub task_queue_length: Option<i32>,
    pub supports_migration: Option<bool>,
    pub status: String,
    pub last_heartbeat_at: DateTime<Utc>,
    pub connected_at: DateTime<Utc>,
}