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 pub title: Option<String>,
186
187 pub description: Option<String>,
189
190 pub phase: Option<String>,
192
193 pub priority: Option<Priority>,
195
196 pub points: Option<i32>,
198
199 pub time_estimate_ms: Option<i64>,
201
202 pub needed_tags: Option<Vec<String>>,
204
205 pub wanted_tags: Option<Vec<String>>,
207
208 pub tags: Option<Vec<String>>,
210
211 #[serde(default)]
213 pub children: Vec<TaskTreeInput>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct Dependency {
220 pub from_task_id: String,
221 pub to_task_id: String,
222 pub dep_type: String,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct FileLock {
229 pub file_path: String,
230 pub worker_id: String,
231 #[serde(skip_serializing_if = "Option::is_none")]
232 pub reason: Option<String>,
233 pub locked_at: i64,
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub task_id: Option<String>,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct ClaimEvent {
241 pub id: i64,
242 pub file_path: String,
243 pub worker_id: String,
244 pub event: ClaimEventType,
245 #[serde(skip_serializing_if = "Option::is_none")]
246 pub reason: Option<String>,
247 pub timestamp: i64,
248 #[serde(skip_serializing_if = "Option::is_none")]
249 pub end_timestamp: Option<i64>,
250 #[serde(skip_serializing_if = "Option::is_none")]
252 pub claim_id: Option<i64>,
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct TaskSequenceEvent {
258 pub id: i64,
259 pub task_id: String,
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub worker_id: Option<String>,
262 #[serde(skip_serializing_if = "Option::is_none")]
264 pub status: Option<String>,
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub phase: Option<String>,
268 #[serde(skip_serializing_if = "Option::is_none")]
269 pub reason: Option<String>,
270 pub timestamp: i64,
271 #[serde(skip_serializing_if = "Option::is_none")]
272 pub end_timestamp: Option<i64>,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct TaskStateEvent {
279 pub id: i64,
280 pub task_id: String,
281 pub worker_id: Option<String>,
282 pub event: String,
283 pub reason: Option<String>,
284 pub timestamp: i64,
285 pub end_timestamp: Option<i64>,
286}
287
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
290#[serde(rename_all = "snake_case")]
291pub enum ClaimEventType {
292 Claimed,
293 Released,
294}
295
296impl ClaimEventType {
297 pub fn as_str(&self) -> &'static str {
298 match self {
299 ClaimEventType::Claimed => "claimed",
300 ClaimEventType::Released => "released",
301 }
302 }
303
304 pub fn parse(s: &str) -> Option<Self> {
305 match s {
306 "claimed" => Some(ClaimEventType::Claimed),
307 "released" => Some(ClaimEventType::Released),
308 _ => None,
309 }
310 }
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct ClaimUpdates {
316 pub new_claims: Vec<ClaimEvent>,
317 pub dropped_claims: Vec<ClaimEvent>,
318 pub sequence: i64,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct Attachment {
326 pub task_id: String,
327 pub attachment_type: String,
328 pub sequence: i32,
329 pub name: String,
330 pub mime_type: String,
331 pub content: String,
332 #[serde(skip_serializing_if = "Option::is_none")]
335 pub file_path: Option<String>,
336 pub created_at: i64,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct AttachmentMeta {
343 pub task_id: String,
344 pub attachment_type: String,
345 pub sequence: i32,
346 pub name: String,
347 pub mime_type: String,
348 #[serde(skip_serializing_if = "Option::is_none")]
350 pub file_path: Option<String>,
351 pub created_at: i64,
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct Stats {
357 pub total_tasks: i64,
358 pub tasks_by_status: HashMap<String, i64>,
360 #[serde(skip_serializing_if = "is_zero")]
361 pub total_points: i64,
362 #[serde(skip_serializing_if = "is_zero")]
363 pub completed_points: i64,
364 #[serde(skip_serializing_if = "is_zero")]
365 pub total_time_estimate_ms: i64,
366 #[serde(skip_serializing_if = "is_zero")]
367 pub total_time_actual_ms: i64,
368 #[serde(skip_serializing_if = "is_zero")]
369 pub total_cost_usd: f64,
370 #[serde(
372 with = "metrics_serde",
373 skip_serializing_if = "metrics_serde::is_empty",
374 default
375 )]
376 pub total_metrics: [i64; 8],
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
381pub struct TaskSummary {
382 pub id: String,
383 pub title: String,
384 pub status: String,
385 #[serde(skip_serializing_if = "is_default_priority")]
386 pub priority: Priority,
387 #[serde(skip_serializing_if = "Option::is_none")]
388 pub worker_id: Option<String>,
389 #[serde(skip_serializing_if = "Option::is_none")]
390 pub points: Option<i32>,
391 #[serde(skip_serializing_if = "Option::is_none")]
392 pub current_thought: Option<String>,
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize)]
398pub struct ScanResult {
399 pub root: Task,
401 #[serde(skip_serializing_if = "Vec::is_empty")]
403 pub before: Vec<Task>,
404 #[serde(skip_serializing_if = "Vec::is_empty")]
406 pub after: Vec<Task>,
407 #[serde(skip_serializing_if = "Vec::is_empty")]
409 pub above: Vec<Task>,
410 #[serde(skip_serializing_if = "Vec::is_empty")]
412 pub below: Vec<Task>,
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct DisconnectSummary {
418 pub tasks_released: i32,
420 pub files_released: i32,
422 pub final_status: String,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct CleanupSummary {
429 pub workers_evicted: i32,
431 pub tasks_released: i32,
433 pub files_released: i32,
435 pub final_status: String,
437 pub evicted_worker_ids: Vec<String>,
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize)]
443pub struct TaskTagRow {
444 pub task_id: String,
445 pub tag: String,
446}
447
448#[derive(Debug, Clone, Serialize, Deserialize)]
450pub struct TaskNeededTagRow {
451 pub task_id: String,
452 pub tag: String,
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize)]
457pub struct TaskWantedTagRow {
458 pub task_id: String,
459 pub tag: String,
460}
461
462#[derive(Debug, Clone, Default, Serialize, Deserialize)]
464pub struct ExportTables {
465 #[serde(skip_serializing_if = "Option::is_none")]
466 pub tasks: Option<Vec<Task>>,
467 #[serde(skip_serializing_if = "Option::is_none")]
468 pub dependencies: Option<Vec<Dependency>>,
469 #[serde(skip_serializing_if = "Option::is_none")]
470 pub attachments: Option<Vec<Attachment>>,
471 #[serde(skip_serializing_if = "Option::is_none")]
472 pub task_tags: Option<Vec<TaskTagRow>>,
473 #[serde(skip_serializing_if = "Option::is_none")]
474 pub task_needed_tags: Option<Vec<TaskNeededTagRow>>,
475 #[serde(skip_serializing_if = "Option::is_none")]
476 pub task_wanted_tags: Option<Vec<TaskWantedTagRow>>,
477 #[serde(skip_serializing_if = "Option::is_none")]
478 pub task_sequence: Option<Vec<TaskSequenceEvent>>,
479}
480
481#[cfg(test)]
482mod tests {
483 }