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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
63 pub overlays: Vec<String>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct WorkerInfo {
69 pub id: String,
70 #[serde(skip_serializing_if = "Vec::is_empty")]
71 pub tags: Vec<String>,
72 pub max_claims: i32,
73 #[serde(skip_serializing_if = "is_zero")]
74 pub claim_count: i32,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub current_thought: Option<String>,
77 pub registered_at: i64,
78 pub last_heartbeat: i64,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub last_status: Option<String>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub last_phase: Option<String>,
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub workflow: Option<String>,
88 #[serde(default, skip_serializing_if = "Vec::is_empty")]
90 pub overlays: Vec<String>,
91}
92
93pub type Priority = i32;
96
97pub const PRIORITY_DEFAULT: Priority = 5;
99
100pub fn parse_priority(s: &str) -> Priority {
102 s.parse().unwrap_or(PRIORITY_DEFAULT).clamp(0, 10)
103}
104
105pub fn clamp_priority(p: Priority) -> Priority {
107 p.clamp(0, 10)
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct Task {
113 pub id: String,
114 pub title: String,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub description: Option<String>,
117 pub status: String,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub phase: Option<String>,
120 #[serde(skip_serializing_if = "is_default_priority")]
121 pub priority: Priority,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub worker_id: Option<String>,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub claimed_at: Option<i64>,
126
127 #[serde(skip_serializing_if = "Vec::is_empty")]
129 pub needed_tags: Vec<String>,
130 #[serde(skip_serializing_if = "Vec::is_empty")]
131 pub wanted_tags: Vec<String>,
132
133 #[serde(skip_serializing_if = "Vec::is_empty")]
135 pub tags: Vec<String>,
136
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub points: Option<i32>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub time_estimate_ms: Option<i64>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub time_actual_ms: Option<i64>,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub started_at: Option<i64>,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub completed_at: Option<i64>,
148
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub current_thought: Option<String>,
152
153 #[serde(skip_serializing_if = "is_zero")]
155 pub cost_usd: f64,
156 #[serde(
158 with = "metrics_serde",
159 skip_serializing_if = "metrics_serde::is_empty",
160 default
161 )]
162 pub metrics: [i64; 8],
163
164 pub created_at: i64,
165 pub updated_at: i64,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct TaskTree {
171 #[serde(flatten)]
172 pub task: Task,
173 pub children: Vec<TaskTree>,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct TaskTreeInput {
180 #[serde(rename = "ref")]
184 pub ref_id: Option<String>,
185
186 pub id: Option<String>,
189
190 pub title: Option<String>,
192
193 pub description: Option<String>,
195
196 pub phase: Option<String>,
198
199 pub priority: Option<Priority>,
201
202 pub points: Option<i32>,
204
205 pub time_estimate_ms: Option<i64>,
207
208 pub needed_tags: Option<Vec<String>>,
210
211 pub wanted_tags: Option<Vec<String>>,
213
214 pub tags: Option<Vec<String>>,
216
217 #[serde(default)]
219 pub children: Vec<TaskTreeInput>,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct Dependency {
226 pub from_task_id: String,
227 pub to_task_id: String,
228 pub dep_type: String,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct FileLock {
235 pub file_path: String,
236 pub worker_id: String,
237 #[serde(skip_serializing_if = "Option::is_none")]
238 pub reason: Option<String>,
239 pub locked_at: i64,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub task_id: Option<String>,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct ClaimEvent {
247 pub id: i64,
248 pub file_path: String,
249 pub worker_id: String,
250 pub event: ClaimEventType,
251 #[serde(skip_serializing_if = "Option::is_none")]
252 pub reason: Option<String>,
253 pub timestamp: i64,
254 #[serde(skip_serializing_if = "Option::is_none")]
255 pub end_timestamp: Option<i64>,
256 #[serde(skip_serializing_if = "Option::is_none")]
258 pub claim_id: Option<i64>,
259}
260
261#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct TaskSequenceEvent {
264 pub id: i64,
265 pub task_id: String,
266 #[serde(skip_serializing_if = "Option::is_none")]
267 pub worker_id: Option<String>,
268 #[serde(skip_serializing_if = "Option::is_none")]
270 pub status: Option<String>,
271 #[serde(skip_serializing_if = "Option::is_none")]
273 pub phase: Option<String>,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub reason: Option<String>,
276 pub timestamp: i64,
277 #[serde(skip_serializing_if = "Option::is_none")]
278 pub end_timestamp: Option<i64>,
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct TaskStateEvent {
285 pub id: i64,
286 pub task_id: String,
287 pub worker_id: Option<String>,
288 pub event: String,
289 pub reason: Option<String>,
290 pub timestamp: i64,
291 pub end_timestamp: Option<i64>,
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
296#[serde(rename_all = "snake_case")]
297pub enum ClaimEventType {
298 Claimed,
299 Released,
300}
301
302impl ClaimEventType {
303 pub fn as_str(&self) -> &'static str {
304 match self {
305 ClaimEventType::Claimed => "claimed",
306 ClaimEventType::Released => "released",
307 }
308 }
309
310 pub fn parse(s: &str) -> Option<Self> {
311 match s {
312 "claimed" => Some(ClaimEventType::Claimed),
313 "released" => Some(ClaimEventType::Released),
314 _ => None,
315 }
316 }
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct ClaimUpdates {
322 pub new_claims: Vec<ClaimEvent>,
323 pub dropped_claims: Vec<ClaimEvent>,
324 pub sequence: i64,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct Attachment {
332 pub task_id: String,
333 pub attachment_type: String,
334 pub sequence: i32,
335 pub name: String,
336 pub mime_type: String,
337 pub content: String,
338 #[serde(skip_serializing_if = "Option::is_none")]
341 pub file_path: Option<String>,
342 pub created_at: i64,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct AttachmentMeta {
349 pub task_id: String,
350 pub attachment_type: String,
351 pub sequence: i32,
352 pub name: String,
353 pub mime_type: String,
354 #[serde(skip_serializing_if = "Option::is_none")]
356 pub file_path: Option<String>,
357 pub created_at: i64,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct Stats {
363 pub total_tasks: i64,
364 pub tasks_by_status: HashMap<String, i64>,
366 #[serde(skip_serializing_if = "is_zero")]
367 pub total_points: i64,
368 #[serde(skip_serializing_if = "is_zero")]
369 pub completed_points: i64,
370 #[serde(skip_serializing_if = "is_zero")]
371 pub total_time_estimate_ms: i64,
372 #[serde(skip_serializing_if = "is_zero")]
373 pub total_time_actual_ms: i64,
374 #[serde(skip_serializing_if = "is_zero")]
375 pub total_cost_usd: f64,
376 #[serde(
378 with = "metrics_serde",
379 skip_serializing_if = "metrics_serde::is_empty",
380 default
381 )]
382 pub total_metrics: [i64; 8],
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize)]
387pub struct TaskSummary {
388 pub id: String,
389 pub title: String,
390 pub status: String,
391 #[serde(skip_serializing_if = "is_default_priority")]
392 pub priority: Priority,
393 #[serde(skip_serializing_if = "Option::is_none")]
394 pub worker_id: Option<String>,
395 #[serde(skip_serializing_if = "Option::is_none")]
396 pub points: Option<i32>,
397 #[serde(skip_serializing_if = "Option::is_none")]
398 pub current_thought: Option<String>,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct ScanResult {
405 pub root: Task,
407 #[serde(skip_serializing_if = "Vec::is_empty")]
409 pub before: Vec<Task>,
410 #[serde(skip_serializing_if = "Vec::is_empty")]
412 pub after: Vec<Task>,
413 #[serde(skip_serializing_if = "Vec::is_empty")]
415 pub above: Vec<Task>,
416 #[serde(skip_serializing_if = "Vec::is_empty")]
418 pub below: Vec<Task>,
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct DisconnectSummary {
424 pub tasks_released: i32,
426 pub files_released: i32,
428 pub final_status: String,
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct CleanupSummary {
435 pub workers_evicted: i32,
437 pub tasks_released: i32,
439 pub files_released: i32,
441 pub final_status: String,
443 pub evicted_worker_ids: Vec<String>,
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize)]
449pub struct TaskTagRow {
450 pub task_id: String,
451 pub tag: String,
452}
453
454#[derive(Debug, Clone, Serialize, Deserialize)]
456pub struct TaskNeededTagRow {
457 pub task_id: String,
458 pub tag: String,
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize)]
463pub struct TaskWantedTagRow {
464 pub task_id: String,
465 pub tag: String,
466}
467
468#[derive(Debug, Clone, Default, Serialize, Deserialize)]
470pub struct ExportTables {
471 #[serde(skip_serializing_if = "Option::is_none")]
472 pub tasks: Option<Vec<Task>>,
473 #[serde(skip_serializing_if = "Option::is_none")]
474 pub dependencies: Option<Vec<Dependency>>,
475 #[serde(skip_serializing_if = "Option::is_none")]
476 pub attachments: Option<Vec<Attachment>>,
477 #[serde(skip_serializing_if = "Option::is_none")]
478 pub task_tags: Option<Vec<TaskTagRow>>,
479 #[serde(skip_serializing_if = "Option::is_none")]
480 pub task_needed_tags: Option<Vec<TaskNeededTagRow>>,
481 #[serde(skip_serializing_if = "Option::is_none")]
482 pub task_wanted_tags: Option<Vec<TaskWantedTagRow>>,
483 #[serde(skip_serializing_if = "Option::is_none")]
484 pub task_sequence: Option<Vec<TaskSequenceEvent>>,
485}
486
487#[cfg(test)]
488mod tests {
489 }