1use chrono::SecondsFormat;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::collections::HashMap;
6
7pub fn ms_to_iso(ms: i64) -> String {
9 chrono::DateTime::from_timestamp_millis(ms)
10 .unwrap_or_else(|| chrono::DateTime::from_timestamp(0, 0).unwrap())
11 .to_rfc3339_opts(SecondsFormat::Secs, true)
12}
13
14pub mod timestamp_serde {
16 use super::*;
17
18 pub fn serialize<S: Serializer>(ms: &i64, s: S) -> Result<S::Ok, S::Error> {
19 s.serialize_str(&super::ms_to_iso(*ms))
20 }
21
22 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<i64, D::Error> {
23 let v: serde_json::Value = Deserialize::deserialize(d)?;
24 match v {
25 serde_json::Value::Number(n) => n
26 .as_i64()
27 .ok_or_else(|| serde::de::Error::custom("invalid timestamp number")),
28 serde_json::Value::String(s) => {
29 if let Ok(ms) = s.parse::<i64>() {
31 return Ok(ms);
32 }
33 chrono::DateTime::parse_from_rfc3339(&s)
35 .map(|dt| dt.timestamp_millis())
36 .map_err(|e| {
37 serde::de::Error::custom(format!("invalid timestamp string: {}", e))
38 })
39 }
40 _ => Err(serde::de::Error::custom(
41 "expected number or string for timestamp",
42 )),
43 }
44 }
45}
46
47pub mod timestamp_opt_serde {
49 use super::*;
50
51 pub fn serialize<S: Serializer>(ms: &Option<i64>, s: S) -> Result<S::Ok, S::Error> {
52 match ms {
53 Some(v) => s.serialize_str(&super::ms_to_iso(*v)),
54 None => s.serialize_none(),
55 }
56 }
57
58 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Option<i64>, D::Error> {
59 let v: Option<serde_json::Value> = Option::deserialize(d)?;
60 match v {
61 None | Some(serde_json::Value::Null) => Ok(None),
62 Some(serde_json::Value::Number(n)) => n
63 .as_i64()
64 .map(Some)
65 .ok_or_else(|| serde::de::Error::custom("invalid timestamp number")),
66 Some(serde_json::Value::String(s)) => {
67 if let Ok(ms) = s.parse::<i64>() {
68 return Ok(Some(ms));
69 }
70 chrono::DateTime::parse_from_rfc3339(&s)
71 .map(|dt| Some(dt.timestamp_millis()))
72 .map_err(|e| {
73 serde::de::Error::custom(format!("invalid timestamp string: {}", e))
74 })
75 }
76 _ => Err(serde::de::Error::custom(
77 "expected number or string for timestamp",
78 )),
79 }
80 }
81}
82
83fn is_zero<T: Default + PartialEq>(v: &T) -> bool {
85 *v == T::default()
86}
87
88fn is_default_priority(p: &Priority) -> bool {
89 *p == PRIORITY_DEFAULT
90}
91
92mod metrics_serde {
94 use super::*;
95
96 pub fn serialize<S: Serializer>(metrics: &[i64; 8], s: S) -> Result<S::Ok, S::Error> {
97 let len = metrics
99 .iter()
100 .rposition(|&x| x != 0)
101 .map(|i| i + 1)
102 .unwrap_or(0);
103 s.collect_seq(&metrics[..len])
104 }
105
106 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[i64; 8], D::Error> {
107 let v: Vec<i64> = Vec::deserialize(d)?;
108 let mut arr = [0i64; 8];
109 for (i, val) in v.into_iter().take(8).enumerate() {
110 arr[i] = val;
111 }
112 Ok(arr)
113 }
114
115 pub fn is_empty(metrics: &[i64; 8]) -> bool {
116 metrics.iter().all(|&x| x == 0)
117 }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct Worker {
123 pub id: String,
124 #[serde(skip_serializing_if = "Vec::is_empty")]
125 pub tags: Vec<String>,
126 pub max_claims: i32,
127 #[serde(with = "timestamp_serde")]
128 pub registered_at: i64,
129 #[serde(with = "timestamp_serde")]
130 pub last_heartbeat: i64,
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub last_status: Option<String>,
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub last_phase: Option<String>,
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub last_task_id: Option<String>,
140 #[serde(skip_serializing_if = "Option::is_none")]
142 pub workflow: Option<String>,
143 #[serde(default, skip_serializing_if = "Vec::is_empty")]
145 pub overlays: Vec<String>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct WorkerInfo {
151 pub id: String,
152 #[serde(skip_serializing_if = "Vec::is_empty")]
153 pub tags: Vec<String>,
154 pub max_claims: i32,
155 #[serde(skip_serializing_if = "is_zero")]
156 pub claim_count: i32,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub current_thought: Option<String>,
159 #[serde(with = "timestamp_serde")]
160 pub registered_at: i64,
161 #[serde(with = "timestamp_serde")]
162 pub last_heartbeat: i64,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub last_status: Option<String>,
166 #[serde(skip_serializing_if = "Option::is_none")]
168 pub last_phase: Option<String>,
169 #[serde(skip_serializing_if = "Option::is_none")]
171 pub last_task_id: Option<String>,
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub workflow: Option<String>,
175 #[serde(default, skip_serializing_if = "Vec::is_empty")]
177 pub overlays: Vec<String>,
178}
179
180pub type Priority = i32;
183
184pub const PRIORITY_DEFAULT: Priority = 5;
186
187pub fn parse_priority(s: &str) -> Priority {
189 s.parse().unwrap_or(PRIORITY_DEFAULT).clamp(0, 10)
190}
191
192pub fn clamp_priority(p: Priority) -> Priority {
194 p.clamp(0, 10)
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct Task {
200 pub id: String,
201 pub title: String,
202 #[serde(skip_serializing_if = "Option::is_none")]
203 pub description: Option<String>,
204 pub status: String,
205 #[serde(skip_serializing_if = "Option::is_none")]
206 pub phase: Option<String>,
207 #[serde(skip_serializing_if = "is_default_priority")]
208 pub priority: Priority,
209 #[serde(skip_serializing_if = "Option::is_none")]
210 pub worker_id: Option<String>,
211 #[serde(
212 skip_serializing_if = "Option::is_none",
213 default,
214 with = "timestamp_opt_serde"
215 )]
216 pub claimed_at: Option<i64>,
217
218 #[serde(skip_serializing_if = "Vec::is_empty")]
220 pub needed_tags: Vec<String>,
221 #[serde(skip_serializing_if = "Vec::is_empty")]
222 pub wanted_tags: Vec<String>,
223
224 #[serde(skip_serializing_if = "Vec::is_empty")]
226 pub tags: Vec<String>,
227
228 #[serde(skip_serializing_if = "Option::is_none")]
230 pub points: Option<i32>,
231 #[serde(skip_serializing_if = "Option::is_none")]
232 pub time_estimate_ms: Option<i64>,
233 #[serde(skip_serializing_if = "Option::is_none")]
234 pub time_actual_ms: Option<i64>,
235 #[serde(
236 skip_serializing_if = "Option::is_none",
237 default,
238 with = "timestamp_opt_serde"
239 )]
240 pub started_at: Option<i64>,
241 #[serde(
242 skip_serializing_if = "Option::is_none",
243 default,
244 with = "timestamp_opt_serde"
245 )]
246 pub completed_at: Option<i64>,
247
248 #[serde(skip_serializing_if = "Option::is_none")]
250 pub current_thought: Option<String>,
251
252 #[serde(skip_serializing_if = "is_zero")]
254 pub cost_usd: f64,
255 #[serde(
257 with = "metrics_serde",
258 skip_serializing_if = "metrics_serde::is_empty",
259 default
260 )]
261 pub metrics: [i64; 8],
262
263 #[serde(with = "timestamp_serde")]
264 pub created_at: i64,
265 #[serde(with = "timestamp_serde")]
266 pub updated_at: i64,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct TaskTree {
272 #[serde(flatten)]
273 pub task: Task,
274 pub children: Vec<TaskTree>,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct TaskTreeInput {
281 #[serde(rename = "ref")]
285 pub ref_id: Option<String>,
286
287 pub id: Option<String>,
290
291 pub title: Option<String>,
293
294 pub description: Option<String>,
296
297 pub phase: Option<String>,
299
300 pub priority: Option<Priority>,
302
303 pub points: Option<i32>,
305
306 pub time_estimate_ms: Option<i64>,
308
309 pub needed_tags: Option<Vec<String>>,
311
312 pub wanted_tags: Option<Vec<String>>,
314
315 pub tags: Option<Vec<String>>,
317
318 #[serde(default)]
322 pub blocked_by: Vec<String>,
323
324 #[serde(default)]
326 pub children: Vec<TaskTreeInput>,
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct Dependency {
333 pub from_task_id: String,
334 pub to_task_id: String,
335 pub dep_type: String,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct FileLock {
342 pub file_path: String,
343 pub worker_id: String,
344 #[serde(skip_serializing_if = "Option::is_none")]
345 pub reason: Option<String>,
346 #[serde(with = "timestamp_serde")]
347 pub locked_at: i64,
348 #[serde(skip_serializing_if = "Option::is_none")]
349 pub task_id: Option<String>,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize)]
354pub struct ClaimEvent {
355 pub id: i64,
356 pub file_path: String,
357 pub worker_id: String,
358 pub event: ClaimEventType,
359 #[serde(skip_serializing_if = "Option::is_none")]
360 pub reason: Option<String>,
361 #[serde(with = "timestamp_serde")]
362 pub timestamp: i64,
363 #[serde(
364 skip_serializing_if = "Option::is_none",
365 default,
366 with = "timestamp_opt_serde"
367 )]
368 pub end_timestamp: Option<i64>,
369 #[serde(skip_serializing_if = "Option::is_none")]
371 pub claim_id: Option<i64>,
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct TaskSequenceEvent {
377 pub id: i64,
378 pub task_id: String,
379 #[serde(skip_serializing_if = "Option::is_none")]
380 pub worker_id: Option<String>,
381 #[serde(skip_serializing_if = "Option::is_none")]
383 pub status: Option<String>,
384 #[serde(skip_serializing_if = "Option::is_none")]
386 pub phase: Option<String>,
387 #[serde(skip_serializing_if = "Option::is_none")]
388 pub reason: Option<String>,
389 #[serde(with = "timestamp_serde")]
390 pub timestamp: i64,
391 #[serde(
392 skip_serializing_if = "Option::is_none",
393 default,
394 with = "timestamp_opt_serde"
395 )]
396 pub end_timestamp: Option<i64>,
397 #[serde(skip_serializing_if = "Option::is_none")]
400 pub concurrency: Option<i32>,
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize)]
406pub struct TaskStateEvent {
407 pub id: i64,
408 pub task_id: String,
409 pub worker_id: Option<String>,
410 pub event: String,
411 pub reason: Option<String>,
412 #[serde(with = "timestamp_serde")]
413 pub timestamp: i64,
414 #[serde(default, with = "timestamp_opt_serde")]
415 pub end_timestamp: Option<i64>,
416}
417
418#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
420#[serde(rename_all = "snake_case")]
421pub enum ClaimEventType {
422 Claimed,
423 Released,
424}
425
426impl ClaimEventType {
427 pub fn as_str(&self) -> &'static str {
428 match self {
429 ClaimEventType::Claimed => "claimed",
430 ClaimEventType::Released => "released",
431 }
432 }
433
434 pub fn parse(s: &str) -> Option<Self> {
435 match s {
436 "claimed" => Some(ClaimEventType::Claimed),
437 "released" => Some(ClaimEventType::Released),
438 _ => None,
439 }
440 }
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct ClaimUpdates {
446 pub new_claims: Vec<ClaimEvent>,
447 pub dropped_claims: Vec<ClaimEvent>,
448 pub sequence: i64,
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct Attachment {
456 pub task_id: String,
457 pub attachment_type: String,
458 pub sequence: i32,
459 pub name: String,
460 pub mime_type: String,
461 pub content: String,
462 #[serde(skip_serializing_if = "Option::is_none")]
465 pub file_path: Option<String>,
466 #[serde(with = "timestamp_serde")]
467 pub created_at: i64,
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize)]
473pub struct AttachmentMeta {
474 pub task_id: String,
475 pub attachment_type: String,
476 pub sequence: i32,
477 pub name: String,
478 pub mime_type: String,
479 #[serde(skip_serializing_if = "Option::is_none")]
481 pub file_path: Option<String>,
482 #[serde(with = "timestamp_serde")]
483 pub created_at: i64,
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct Stats {
489 pub total_tasks: i64,
490 pub tasks_by_status: HashMap<String, i64>,
492 #[serde(skip_serializing_if = "is_zero")]
493 pub total_points: i64,
494 #[serde(skip_serializing_if = "is_zero")]
495 pub completed_points: i64,
496 #[serde(skip_serializing_if = "is_zero")]
497 pub total_time_estimate_ms: i64,
498 #[serde(skip_serializing_if = "is_zero")]
499 pub total_time_actual_ms: i64,
500 #[serde(skip_serializing_if = "is_zero")]
501 pub total_cost_usd: f64,
502 #[serde(
504 with = "metrics_serde",
505 skip_serializing_if = "metrics_serde::is_empty",
506 default
507 )]
508 pub total_metrics: [i64; 8],
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct TaskSummary {
514 pub id: String,
515 pub title: String,
516 pub status: String,
517 #[serde(skip_serializing_if = "is_default_priority")]
518 pub priority: Priority,
519 #[serde(skip_serializing_if = "Option::is_none")]
520 pub worker_id: Option<String>,
521 #[serde(skip_serializing_if = "Option::is_none")]
522 pub points: Option<i32>,
523 #[serde(skip_serializing_if = "Option::is_none")]
524 pub current_thought: Option<String>,
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize)]
530pub struct ScanResult {
531 pub root: Task,
533 #[serde(skip_serializing_if = "Vec::is_empty")]
535 pub before: Vec<Task>,
536 #[serde(skip_serializing_if = "Vec::is_empty")]
538 pub after: Vec<Task>,
539 #[serde(skip_serializing_if = "Vec::is_empty")]
541 pub above: Vec<Task>,
542 #[serde(skip_serializing_if = "Vec::is_empty")]
544 pub below: Vec<Task>,
545}
546
547#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct DisconnectSummary {
550 pub tasks_released: i32,
552 pub files_released: i32,
554 pub final_status: String,
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize)]
560pub struct CleanupSummary {
561 pub workers_evicted: i32,
563 pub tasks_released: i32,
565 pub files_released: i32,
567 pub final_status: String,
569 pub evicted_worker_ids: Vec<String>,
571}
572
573#[derive(Debug, Clone, Serialize, Deserialize)]
575pub struct TaskTagRow {
576 pub task_id: String,
577 pub tag: String,
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct TaskNeededTagRow {
583 pub task_id: String,
584 pub tag: String,
585}
586
587#[derive(Debug, Clone, Serialize, Deserialize)]
589pub struct TaskWantedTagRow {
590 pub task_id: String,
591 pub tag: String,
592}
593
594#[derive(Debug, Clone, Default, Serialize, Deserialize)]
596pub struct ExportTables {
597 #[serde(skip_serializing_if = "Option::is_none")]
598 pub tasks: Option<Vec<Task>>,
599 #[serde(skip_serializing_if = "Option::is_none")]
600 pub dependencies: Option<Vec<Dependency>>,
601 #[serde(skip_serializing_if = "Option::is_none")]
602 pub attachments: Option<Vec<Attachment>>,
603 #[serde(skip_serializing_if = "Option::is_none")]
604 pub task_tags: Option<Vec<TaskTagRow>>,
605 #[serde(skip_serializing_if = "Option::is_none")]
606 pub task_needed_tags: Option<Vec<TaskNeededTagRow>>,
607 #[serde(skip_serializing_if = "Option::is_none")]
608 pub task_wanted_tags: Option<Vec<TaskWantedTagRow>>,
609 #[serde(skip_serializing_if = "Option::is_none")]
610 pub task_sequence: Option<Vec<TaskSequenceEvent>>,
611}
612
613#[cfg(test)]
614mod tests {
615 }