1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use uuid::Uuid;
pub use uptrakit_shared_types::{ParseUpdateStatusError, UpdateStatus};
#[non_exhaustive]
#[derive(Default, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema, utoipa::IntoParams))]
pub struct UpdateHistoryQuery {
/// Filter by host UUID.
pub host_id: Option<Uuid>,
/// Filter by software item UUID.
pub software_item_id: Option<Uuid>,
/// Filter by status (queued, pending, in_progress, awaiting_restart, completed, failed, interrupted).
pub status: Option<UpdateStatus>,
/// Page number (1-indexed). Defaults to 1.
pub page: Option<u64>,
/// Items per page. Defaults to 20, max 1000.
pub per_page: Option<u64>,
}
impl UpdateHistoryQuery {
/// Creates a new `UpdateHistoryQuery` with all filter fields set explicitly.
pub fn new(
host_id: Option<Uuid>,
software_item_id: Option<Uuid>,
status: Option<UpdateStatus>,
page: Option<u64>,
per_page: Option<u64>,
) -> Self {
Self {
host_id,
software_item_id,
status,
page,
per_page,
}
}
pub fn pagination(&self) -> crate::pagination::PaginationParams {
crate::pagination::PaginationParams {
page: self.page,
per_page: self.per_page,
}
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct UpdateHistoryResponse {
pub id: Uuid,
pub host_id: Uuid,
pub host_name: String,
pub software_item_id: Uuid,
pub software_item_name: String,
pub from_version: Option<String>,
pub to_version: String,
pub status: UpdateStatus,
pub output: String,
pub actor_type: String,
pub actor_id: String,
/// Human-readable display name of the actor, if resolvable.
///
/// For `actor_type = "user"` this is `"First Last"`.
/// For `actor_type = "service"` or `"system_service"` this is `friendly_name`.
/// `None` when the actor record no longer exists or the ID is not a valid UUID.
pub actor_name: Option<String>,
#[serde(with = "time::serde::rfc3339")]
#[cfg_attr(
feature = "openapi",
schema(value_type = String, format = DateTime)
)]
pub started_at: OffsetDateTime,
#[serde(with = "time::serde::rfc3339::option")]
#[cfg_attr(
feature = "openapi",
schema(value_type = Option<String>, format = DateTime)
)]
pub completed_at: Option<OffsetDateTime>,
#[serde(with = "time::serde::rfc3339")]
#[cfg_attr(
feature = "openapi",
schema(value_type = String, format = DateTime)
)]
pub created_at: OffsetDateTime,
/// Classification of the update (security, bugfix, feature, unknown).
pub update_category: String,
/// Whether the update was dispatched in interactive mode (PTY allocation
/// intended; input unlocks in the UI once the PTY is live).
///
/// The UI uses this to show an "Input Required" badge on every in-progress
/// interactive update in the history list, even when not actively watching
/// the stream.
pub interactive: bool,
/// Whether any output was dropped because it exceeded the output size cap.
///
/// When `true`, only the first 50 MB of output is stored. The truncation
/// point is marked in the output stream with a system notice line. The
/// detail view shows an amber warning banner when this field is `true`.
pub output_truncated: bool,
/// Optional generic pre-update protection status.
pub pre_update_protection_status: Option<String>,
/// Optional generic pre-update protection summary.
pub pre_update_protection_summary: Option<String>,
/// Optional hint for recovery actions.
pub recovery_hint: Option<String>,
}
impl UpdateHistoryResponse {
/// Creates a new `UpdateHistoryResponse` with all fields explicitly set.
#[expect(
clippy::too_many_arguments,
reason = "all fields are required for a fully-typed constructor; splitting would obscure the call-site semantics"
)]
pub fn new(
id: Uuid,
host_id: Uuid,
host_name: String,
software_item_id: Uuid,
software_item_name: String,
from_version: Option<String>,
to_version: String,
status: UpdateStatus,
output: String,
actor_type: String,
actor_id: String,
actor_name: Option<String>,
started_at: OffsetDateTime,
completed_at: Option<OffsetDateTime>,
created_at: OffsetDateTime,
update_category: String,
interactive: bool,
output_truncated: bool,
pre_update_protection_status: Option<String>,
pre_update_protection_summary: Option<String>,
recovery_hint: Option<String>,
) -> Self {
Self {
id,
host_id,
host_name,
software_item_id,
software_item_name,
from_version,
to_version,
status,
output,
actor_type,
actor_id,
actor_name,
started_at,
completed_at,
created_at,
update_category,
interactive,
output_truncated,
pre_update_protection_status,
pre_update_protection_summary,
recovery_hint,
}
}
}
// ---------------------------------------------------------------------------
// SSE event types for real-time update output streaming
// ---------------------------------------------------------------------------
/// SSE `output` event payload: a single line of update output.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OutputLineSSE {
pub id: Uuid,
pub text: String,
pub stream: String,
#[serde(with = "time::serde::rfc3339")]
pub timestamp: OffsetDateTime,
pub seq: u64,
}
/// SSE `completed` event payload: the update has finished.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UpdateCompletedSSE {
pub status: String,
pub error: Option<String>,
}
/// SSE `stdin_attention` event payload: the process is waiting for input.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct StdinAttentionSSE {
pub hint: Option<String>,
}