1use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use std::collections::HashMap;
5
6fn is_zero<T: Default + PartialEq>(v: &T) -> bool {
8 *v == T::default()
9}
10
11fn is_default_priority(p: &Priority) -> bool {
12 *p == PRIORITY_DEFAULT
13}
14
15mod metrics_serde {
17 use super::*;
18
19 pub fn serialize<S: Serializer>(metrics: &[i64; 8], s: S) -> Result<S::Ok, S::Error> {
20 let len = metrics
22 .iter()
23 .rposition(|&x| x != 0)
24 .map(|i| i + 1)
25 .unwrap_or(0);
26 s.collect_seq(&metrics[..len])
27 }
28
29 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[i64; 8], D::Error> {
30 let v: Vec<i64> = Vec::deserialize(d)?;
31 let mut arr = [0i64; 8];
32 for (i, val) in v.into_iter().take(8).enumerate() {
33 arr[i] = val;
34 }
35 Ok(arr)
36 }
37
38 pub fn is_empty(metrics: &[i64; 8]) -> bool {
39 metrics.iter().all(|&x| x == 0)
40 }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct Worker {
46 pub id: String,
47 #[serde(skip_serializing_if = "Vec::is_empty")]
48 pub tags: Vec<String>,
49 pub max_claims: i32,
50 pub registered_at: i64,
51 pub last_heartbeat: i64,
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub last_status: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub last_phase: Option<String>,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub workflow: Option<String>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct WorkerInfo {
66 pub id: String,
67 #[serde(skip_serializing_if = "Vec::is_empty")]
68 pub tags: Vec<String>,
69 pub max_claims: i32,
70 #[serde(skip_serializing_if = "is_zero")]
71 pub claim_count: i32,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub current_thought: Option<String>,
74 pub registered_at: i64,
75 pub last_heartbeat: i64,
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub last_status: Option<String>,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub last_phase: Option<String>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub workflow: Option<String>,
85}
86
87pub type Priority = i32;
90
91pub const PRIORITY_DEFAULT: Priority = 5;
93
94pub fn parse_priority(s: &str) -> Priority {
96 s.parse().unwrap_or(PRIORITY_DEFAULT).clamp(0, 10)
97}
98
99pub fn clamp_priority(p: Priority) -> Priority {
101 p.clamp(0, 10)
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct Task {
107 pub id: String,
108 pub title: String,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub description: Option<String>,
111 pub status: String,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub phase: Option<String>,
114 #[serde(skip_serializing_if = "is_default_priority")]
115 pub priority: Priority,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub worker_id: Option<String>,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub claimed_at: Option<i64>,
120
121 #[serde(skip_serializing_if = "Vec::is_empty")]
123 pub needed_tags: Vec<String>,
124 #[serde(skip_serializing_if = "Vec::is_empty")]
125 pub wanted_tags: Vec<String>,
126
127 #[serde(skip_serializing_if = "Vec::is_empty")]
129 pub tags: Vec<String>,
130
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub points: Option<i32>,
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub time_estimate_ms: Option<i64>,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub time_actual_ms: Option<i64>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub started_at: Option<i64>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub completed_at: Option<i64>,
142
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub current_thought: Option<String>,
146
147 #[serde(skip_serializing_if = "is_zero")]
149 pub cost_usd: f64,
150 #[serde(
152 with = "metrics_serde",
153 skip_serializing_if = "metrics_serde::is_empty",
154 default
155 )]
156 pub metrics: [i64; 8],
157
158 pub created_at: i64,
159 pub updated_at: i64,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct TaskTree {
165 #[serde(flatten)]
166 pub task: Task,
167 pub children: Vec<TaskTree>,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct TaskTreeInput {
174 #[serde(rename = "ref")]
178 pub ref_id: Option<String>,
179
180 pub id: Option<String>,
183
184 #[serde(default)]
186 pub title: String,
187
188 pub description: Option<String>,
190
191 pub phase: Option<String>,
193
194 pub priority: Option<Priority>,
196
197 pub points: Option<i32>,
199
200 pub time_estimate_ms: Option<i64>,
202
203 pub needed_tags: Option<Vec<String>>,
205
206 pub wanted_tags: Option<Vec<String>>,
208
209 pub tags: Option<Vec<String>>,
211
212 #[serde(default)]
214 pub children: Vec<TaskTreeInput>,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct Dependency {
221 pub from_task_id: String,
222 pub to_task_id: String,
223 pub dep_type: String,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct FileLock {
230 pub file_path: String,
231 pub worker_id: String,
232 #[serde(skip_serializing_if = "Option::is_none")]
233 pub reason: Option<String>,
234 pub locked_at: i64,
235 #[serde(skip_serializing_if = "Option::is_none")]
236 pub task_id: Option<String>,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct ClaimEvent {
242 pub id: i64,
243 pub file_path: String,
244 pub worker_id: String,
245 pub event: ClaimEventType,
246 #[serde(skip_serializing_if = "Option::is_none")]
247 pub reason: Option<String>,
248 pub timestamp: i64,
249 #[serde(skip_serializing_if = "Option::is_none")]
250 pub end_timestamp: Option<i64>,
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub claim_id: Option<i64>,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct TaskSequenceEvent {
259 pub id: i64,
260 pub task_id: String,
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub worker_id: Option<String>,
263 #[serde(skip_serializing_if = "Option::is_none")]
265 pub status: Option<String>,
266 #[serde(skip_serializing_if = "Option::is_none")]
268 pub phase: Option<String>,
269 #[serde(skip_serializing_if = "Option::is_none")]
270 pub reason: Option<String>,
271 pub timestamp: i64,
272 #[serde(skip_serializing_if = "Option::is_none")]
273 pub end_timestamp: Option<i64>,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct TaskStateEvent {
280 pub id: i64,
281 pub task_id: String,
282 pub worker_id: Option<String>,
283 pub event: String,
284 pub reason: Option<String>,
285 pub timestamp: i64,
286 pub end_timestamp: Option<i64>,
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
291#[serde(rename_all = "snake_case")]
292pub enum ClaimEventType {
293 Claimed,
294 Released,
295}
296
297impl ClaimEventType {
298 pub fn as_str(&self) -> &'static str {
299 match self {
300 ClaimEventType::Claimed => "claimed",
301 ClaimEventType::Released => "released",
302 }
303 }
304
305 pub fn parse(s: &str) -> Option<Self> {
306 match s {
307 "claimed" => Some(ClaimEventType::Claimed),
308 "released" => Some(ClaimEventType::Released),
309 _ => None,
310 }
311 }
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct ClaimUpdates {
317 pub new_claims: Vec<ClaimEvent>,
318 pub dropped_claims: Vec<ClaimEvent>,
319 pub sequence: i64,
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct Attachment {
327 pub task_id: String,
328 pub attachment_type: String,
329 pub sequence: i32,
330 pub name: String,
331 pub mime_type: String,
332 pub content: String,
333 #[serde(skip_serializing_if = "Option::is_none")]
336 pub file_path: Option<String>,
337 pub created_at: i64,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct AttachmentMeta {
344 pub task_id: String,
345 pub attachment_type: String,
346 pub sequence: i32,
347 pub name: String,
348 pub mime_type: String,
349 #[serde(skip_serializing_if = "Option::is_none")]
351 pub file_path: Option<String>,
352 pub created_at: i64,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct Stats {
358 pub total_tasks: i64,
359 pub tasks_by_status: HashMap<String, i64>,
361 #[serde(skip_serializing_if = "is_zero")]
362 pub total_points: i64,
363 #[serde(skip_serializing_if = "is_zero")]
364 pub completed_points: i64,
365 #[serde(skip_serializing_if = "is_zero")]
366 pub total_time_estimate_ms: i64,
367 #[serde(skip_serializing_if = "is_zero")]
368 pub total_time_actual_ms: i64,
369 #[serde(skip_serializing_if = "is_zero")]
370 pub total_cost_usd: f64,
371 #[serde(
373 with = "metrics_serde",
374 skip_serializing_if = "metrics_serde::is_empty",
375 default
376 )]
377 pub total_metrics: [i64; 8],
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct TaskSummary {
383 pub id: String,
384 pub title: String,
385 pub status: String,
386 #[serde(skip_serializing_if = "is_default_priority")]
387 pub priority: Priority,
388 #[serde(skip_serializing_if = "Option::is_none")]
389 pub worker_id: Option<String>,
390 #[serde(skip_serializing_if = "Option::is_none")]
391 pub points: Option<i32>,
392 #[serde(skip_serializing_if = "Option::is_none")]
393 pub current_thought: Option<String>,
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct ScanResult {
400 pub root: Task,
402 #[serde(skip_serializing_if = "Vec::is_empty")]
404 pub before: Vec<Task>,
405 #[serde(skip_serializing_if = "Vec::is_empty")]
407 pub after: Vec<Task>,
408 #[serde(skip_serializing_if = "Vec::is_empty")]
410 pub above: Vec<Task>,
411 #[serde(skip_serializing_if = "Vec::is_empty")]
413 pub below: Vec<Task>,
414}
415
416#[derive(Debug, Clone, Serialize, Deserialize)]
418pub struct DisconnectSummary {
419 pub tasks_released: i32,
421 pub files_released: i32,
423 pub final_status: String,
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct CleanupSummary {
430 pub workers_evicted: i32,
432 pub tasks_released: i32,
434 pub files_released: i32,
436 pub final_status: String,
438 pub evicted_worker_ids: Vec<String>,
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct TaskTagRow {
445 pub task_id: String,
446 pub tag: String,
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize)]
451pub struct TaskNeededTagRow {
452 pub task_id: String,
453 pub tag: String,
454}
455
456#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct TaskWantedTagRow {
459 pub task_id: String,
460 pub tag: String,
461}
462
463#[derive(Debug, Clone, Default, Serialize, Deserialize)]
465pub struct ExportTables {
466 #[serde(skip_serializing_if = "Option::is_none")]
467 pub tasks: Option<Vec<Task>>,
468 #[serde(skip_serializing_if = "Option::is_none")]
469 pub dependencies: Option<Vec<Dependency>>,
470 #[serde(skip_serializing_if = "Option::is_none")]
471 pub attachments: Option<Vec<Attachment>>,
472 #[serde(skip_serializing_if = "Option::is_none")]
473 pub task_tags: Option<Vec<TaskTagRow>>,
474 #[serde(skip_serializing_if = "Option::is_none")]
475 pub task_needed_tags: Option<Vec<TaskNeededTagRow>>,
476 #[serde(skip_serializing_if = "Option::is_none")]
477 pub task_wanted_tags: Option<Vec<TaskWantedTagRow>>,
478 #[serde(skip_serializing_if = "Option::is_none")]
479 pub task_sequence: Option<Vec<TaskSequenceEvent>>,
480}
481
482#[cfg(test)]
483mod tests {
484 }