runmonitor 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runmonitor/__init__.py +125 -0
- runmonitor/__main__.py +54 -0
- runmonitor/server.py +163 -0
- runmonitor/static/style.css +398 -0
- runmonitor/storage.py +235 -0
- runmonitor/templates/dashboard.html +615 -0
- runmonitor-0.2.0.dist-info/METADATA +148 -0
- runmonitor-0.2.0.dist-info/RECORD +12 -0
- runmonitor-0.2.0.dist-info/WHEEL +5 -0
- runmonitor-0.2.0.dist-info/entry_points.txt +2 -0
- runmonitor-0.2.0.dist-info/licenses/LICENSE +21 -0
- runmonitor-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
2
|
+
RUNMONITOR — Empero design system, terminal edition
|
|
3
|
+
Black/purple. Box-drawn panels, JetBrains Mono, crisp hairlines.
|
|
4
|
+
Palette mirrors empero.org's midnight theme.
|
|
5
|
+
═════════════════════════════════════════════════════════════ */
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
/* Type */
|
|
9
|
+
--ff-display: "Inter", "Satoshi", "SF Pro Display", system-ui, -apple-system, sans-serif;
|
|
10
|
+
--ff-body: "Inter", -apple-system, system-ui, sans-serif;
|
|
11
|
+
--ff-mono: "JetBrains Mono", "Fira Code", ui-monospace, "SF Mono", Menlo, monospace;
|
|
12
|
+
|
|
13
|
+
/* Shape */
|
|
14
|
+
--r-1: 2px;
|
|
15
|
+
--r-2: 4px;
|
|
16
|
+
--rule-w: 0.75px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* ── Light — empero bone paper ─────────────────────────── */
|
|
20
|
+
[data-theme="light"] {
|
|
21
|
+
--paper: #F4F1EC;
|
|
22
|
+
--paper-2: #E8E3DA;
|
|
23
|
+
--panel: #ECE7DF;
|
|
24
|
+
--ink: #15121C;
|
|
25
|
+
--ink-2: #2A2632;
|
|
26
|
+
--rule: rgba(21, 18, 28, 0.18);
|
|
27
|
+
--rule-2: rgba(21, 18, 28, 0.08);
|
|
28
|
+
--muted: rgba(21, 18, 28, 0.55);
|
|
29
|
+
--accent: #6B2BD9;
|
|
30
|
+
--accent-2:#3370ff;
|
|
31
|
+
--magenta: #C8267C;
|
|
32
|
+
--alert: #D11149;
|
|
33
|
+
--green: #2BA84A;
|
|
34
|
+
--amber: #C77700;
|
|
35
|
+
--grid: rgba(21, 18, 28, 0.06);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* ── Midnight — empero black/purple (default) ──────────── */
|
|
39
|
+
:root,
|
|
40
|
+
[data-theme="midnight"] {
|
|
41
|
+
--paper: #0E0B14;
|
|
42
|
+
--paper-2: #161220;
|
|
43
|
+
--panel: #14101E;
|
|
44
|
+
--ink: #E9E4F0;
|
|
45
|
+
--ink-2: #C2BCD0;
|
|
46
|
+
--rule: rgba(233, 228, 240, 0.16);
|
|
47
|
+
--rule-2: rgba(233, 228, 240, 0.07);
|
|
48
|
+
--muted: rgba(233, 228, 240, 0.55);
|
|
49
|
+
--accent: #B66BFF;
|
|
50
|
+
--accent-2:#5b8fff;
|
|
51
|
+
--magenta: #C8267C;
|
|
52
|
+
--alert: #FF5470;
|
|
53
|
+
--green: #3DDC84;
|
|
54
|
+
--amber: #F4A236;
|
|
55
|
+
--grid: rgba(233, 228, 240, 0.05);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
59
|
+
|
|
60
|
+
html, body {
|
|
61
|
+
background: var(--paper);
|
|
62
|
+
color: var(--ink);
|
|
63
|
+
font-family: var(--ff-mono);
|
|
64
|
+
font-size: 14px;
|
|
65
|
+
line-height: 1.5;
|
|
66
|
+
min-height: 100vh;
|
|
67
|
+
-webkit-font-smoothing: antialiased;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* faint terminal grid behind everything */
|
|
71
|
+
body::before {
|
|
72
|
+
content: "";
|
|
73
|
+
position: fixed;
|
|
74
|
+
inset: 0;
|
|
75
|
+
pointer-events: none;
|
|
76
|
+
z-index: 0;
|
|
77
|
+
background-image:
|
|
78
|
+
linear-gradient(var(--grid) 1px, transparent 1px),
|
|
79
|
+
linear-gradient(90deg, var(--grid) 1px, transparent 1px);
|
|
80
|
+
background-size: 28px 28px;
|
|
81
|
+
mask-image: radial-gradient(ellipse 90% 70% at 50% 0%, #000 30%, transparent 100%);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ── Navbar ───────────────────────────────────────────── */
|
|
85
|
+
|
|
86
|
+
.navbar {
|
|
87
|
+
position: sticky;
|
|
88
|
+
top: 0;
|
|
89
|
+
z-index: 100;
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: space-between;
|
|
93
|
+
height: 46px;
|
|
94
|
+
padding: 0 18px;
|
|
95
|
+
background: var(--paper);
|
|
96
|
+
border-bottom: var(--rule-w) solid var(--rule);
|
|
97
|
+
backdrop-filter: blur(6px);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.navbar-brand { display: flex; align-items: center; gap: 9px; }
|
|
101
|
+
|
|
102
|
+
.logo { color: var(--accent); flex-shrink: 0; }
|
|
103
|
+
|
|
104
|
+
.brand-text {
|
|
105
|
+
font-family: var(--ff-display);
|
|
106
|
+
font-weight: 800;
|
|
107
|
+
font-size: 15px;
|
|
108
|
+
letter-spacing: -0.02em;
|
|
109
|
+
color: var(--ink);
|
|
110
|
+
}
|
|
111
|
+
.brand-text::after {
|
|
112
|
+
content: "_";
|
|
113
|
+
color: var(--accent);
|
|
114
|
+
animation: blink 1.1s steps(1) infinite;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@keyframes blink { 0%,50% { opacity: 1; } 50.01%,100% { opacity: 0; } }
|
|
118
|
+
|
|
119
|
+
.navbar-actions { display: flex; align-items: center; gap: 10px; }
|
|
120
|
+
|
|
121
|
+
.status-dot {
|
|
122
|
+
width: 7px; height: 7px;
|
|
123
|
+
border-radius: 50%;
|
|
124
|
+
background: var(--green);
|
|
125
|
+
box-shadow: 0 0 0 0 color-mix(in srgb, var(--green) 70%, transparent);
|
|
126
|
+
transition: background 0.15s;
|
|
127
|
+
}
|
|
128
|
+
.status-dot.pulse { animation: ping 0.9s ease-out; }
|
|
129
|
+
@keyframes ping {
|
|
130
|
+
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 60%, transparent); background: var(--accent); }
|
|
131
|
+
100% { box-shadow: 0 0 0 7px transparent; background: var(--green); }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.btn-icon {
|
|
135
|
+
background: none;
|
|
136
|
+
border: var(--rule-w) solid var(--rule);
|
|
137
|
+
color: var(--ink-2);
|
|
138
|
+
border-radius: var(--r-2);
|
|
139
|
+
padding: 3px 8px;
|
|
140
|
+
cursor: pointer;
|
|
141
|
+
font-size: 14px;
|
|
142
|
+
font-family: var(--ff-mono);
|
|
143
|
+
line-height: 1.2;
|
|
144
|
+
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
|
145
|
+
}
|
|
146
|
+
.btn-icon:hover { background: var(--paper-2); color: var(--accent); border-color: var(--accent); }
|
|
147
|
+
|
|
148
|
+
.gamification-badges { display: flex; gap: 6px; align-items: center; }
|
|
149
|
+
.badge {
|
|
150
|
+
font-size: 12px;
|
|
151
|
+
padding: 1px 7px;
|
|
152
|
+
border-radius: var(--r-1);
|
|
153
|
+
font-family: var(--ff-mono);
|
|
154
|
+
border: var(--rule-w) solid var(--rule);
|
|
155
|
+
}
|
|
156
|
+
.badge-fire { color: var(--amber); }
|
|
157
|
+
.badge-best { color: var(--accent); animation: pop 0.4s ease; }
|
|
158
|
+
@keyframes pop { 0% { transform: scale(0.5); } 60% { transform: scale(1.2); } 100% { transform: scale(1); } }
|
|
159
|
+
|
|
160
|
+
/* ── Main layout ──────────────────────────────────────── */
|
|
161
|
+
|
|
162
|
+
.main {
|
|
163
|
+
position: relative;
|
|
164
|
+
z-index: 1;
|
|
165
|
+
max-width: 1080px;
|
|
166
|
+
margin: 0 auto;
|
|
167
|
+
padding: 22px 20px 70px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* ── Selectors ────────────────────────────────────────── */
|
|
171
|
+
|
|
172
|
+
.selectors { display: flex; gap: 14px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
173
|
+
.field { flex: 1; min-width: 200px; max-width: 320px; }
|
|
174
|
+
.field label {
|
|
175
|
+
display: block;
|
|
176
|
+
font-size: 10px;
|
|
177
|
+
font-weight: 700;
|
|
178
|
+
text-transform: uppercase;
|
|
179
|
+
letter-spacing: 0.1em;
|
|
180
|
+
color: var(--accent);
|
|
181
|
+
margin-bottom: 4px;
|
|
182
|
+
}
|
|
183
|
+
.field label::before { content: "› "; opacity: 0.7; }
|
|
184
|
+
.field select {
|
|
185
|
+
width: 100%;
|
|
186
|
+
height: 34px;
|
|
187
|
+
padding: 0 10px;
|
|
188
|
+
background: var(--paper-2);
|
|
189
|
+
border: var(--rule-w) solid var(--rule);
|
|
190
|
+
border-radius: var(--r-1);
|
|
191
|
+
color: var(--ink);
|
|
192
|
+
font-family: var(--ff-mono);
|
|
193
|
+
font-size: 12.5px;
|
|
194
|
+
cursor: pointer;
|
|
195
|
+
appearance: none;
|
|
196
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 5l3 3 3-3' fill='none' stroke='%23B66BFF' stroke-width='1.3'/%3E%3C/svg%3E");
|
|
197
|
+
background-repeat: no-repeat;
|
|
198
|
+
background-position: right 8px center;
|
|
199
|
+
}
|
|
200
|
+
.field select:focus { outline: none; border-color: var(--accent); }
|
|
201
|
+
|
|
202
|
+
/* ── Panels (box-drawn) ───────────────────────────────── */
|
|
203
|
+
|
|
204
|
+
.panel {
|
|
205
|
+
position: relative;
|
|
206
|
+
background: var(--panel);
|
|
207
|
+
border: var(--rule-w) solid var(--rule);
|
|
208
|
+
border-radius: var(--r-2);
|
|
209
|
+
padding: 18px 16px 14px;
|
|
210
|
+
margin-bottom: 14px;
|
|
211
|
+
}
|
|
212
|
+
.panel > .panel-label {
|
|
213
|
+
position: absolute;
|
|
214
|
+
top: -8px;
|
|
215
|
+
left: 12px;
|
|
216
|
+
padding: 0 7px;
|
|
217
|
+
background: var(--paper);
|
|
218
|
+
color: var(--accent);
|
|
219
|
+
font-family: var(--ff-mono);
|
|
220
|
+
font-size: 11px;
|
|
221
|
+
letter-spacing: 0.06em;
|
|
222
|
+
white-space: nowrap;
|
|
223
|
+
}
|
|
224
|
+
.panel > .panel-label::before { content: "┄ "; opacity: 0.55; }
|
|
225
|
+
|
|
226
|
+
/* ── Run header ───────────────────────────────────────── */
|
|
227
|
+
|
|
228
|
+
.runhead-grid {
|
|
229
|
+
display: flex;
|
|
230
|
+
align-items: center;
|
|
231
|
+
gap: 18px;
|
|
232
|
+
flex-wrap: wrap;
|
|
233
|
+
}
|
|
234
|
+
.rh-name {
|
|
235
|
+
font-family: var(--ff-display);
|
|
236
|
+
font-weight: 800;
|
|
237
|
+
font-size: 18px;
|
|
238
|
+
letter-spacing: -0.01em;
|
|
239
|
+
color: var(--ink);
|
|
240
|
+
}
|
|
241
|
+
.rh-step { color: var(--muted); }
|
|
242
|
+
.rh-step strong { color: var(--ink); font-weight: 600; }
|
|
243
|
+
.rh-status {
|
|
244
|
+
text-transform: uppercase;
|
|
245
|
+
font-size: 11px;
|
|
246
|
+
letter-spacing: 0.08em;
|
|
247
|
+
padding: 2px 9px;
|
|
248
|
+
border-radius: 999px;
|
|
249
|
+
border: var(--rule-w) solid var(--rule);
|
|
250
|
+
}
|
|
251
|
+
.rh-status.running { color: var(--green); border-color: color-mix(in srgb, var(--green) 50%, transparent); }
|
|
252
|
+
.rh-status.finished { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 50%, transparent); }
|
|
253
|
+
.rh-status.crashed { color: var(--alert); border-color: color-mix(in srgb, var(--alert) 50%, transparent); }
|
|
254
|
+
.rh-status.running::before {
|
|
255
|
+
content: "● ";
|
|
256
|
+
animation: blink 1.4s steps(1) infinite;
|
|
257
|
+
}
|
|
258
|
+
.rh-spacer { flex: 1; min-width: 8px; }
|
|
259
|
+
.rh-stat { text-align: right; }
|
|
260
|
+
.rh-stat label {
|
|
261
|
+
display: block;
|
|
262
|
+
font-size: 9px;
|
|
263
|
+
text-transform: uppercase;
|
|
264
|
+
letter-spacing: 0.1em;
|
|
265
|
+
color: var(--muted);
|
|
266
|
+
}
|
|
267
|
+
.rh-stat span { color: var(--ink); font-size: 14px; }
|
|
268
|
+
|
|
269
|
+
.progress { display: flex; align-items: center; gap: 12px; margin-top: 14px; }
|
|
270
|
+
.progress-bar {
|
|
271
|
+
flex: 1; height: 6px;
|
|
272
|
+
background: var(--rule-2);
|
|
273
|
+
border-radius: 3px;
|
|
274
|
+
overflow: hidden;
|
|
275
|
+
}
|
|
276
|
+
.progress-fill {
|
|
277
|
+
height: 100%;
|
|
278
|
+
background: linear-gradient(90deg, var(--accent), var(--magenta));
|
|
279
|
+
border-radius: 3px;
|
|
280
|
+
transition: width 0.5s ease;
|
|
281
|
+
min-width: 2px;
|
|
282
|
+
}
|
|
283
|
+
.progress-label { color: var(--muted); font-size: 12px; white-space: nowrap; }
|
|
284
|
+
|
|
285
|
+
/* ── Metric ticker (the selector) ─────────────────────── */
|
|
286
|
+
|
|
287
|
+
.ticker {
|
|
288
|
+
display: flex;
|
|
289
|
+
flex-wrap: wrap;
|
|
290
|
+
gap: 6px 8px;
|
|
291
|
+
align-items: center;
|
|
292
|
+
}
|
|
293
|
+
.chip {
|
|
294
|
+
cursor: pointer;
|
|
295
|
+
padding: 3px 9px;
|
|
296
|
+
border-radius: var(--r-1);
|
|
297
|
+
border: var(--rule-w) solid transparent;
|
|
298
|
+
color: var(--muted);
|
|
299
|
+
white-space: nowrap;
|
|
300
|
+
font-size: 13px;
|
|
301
|
+
transition: color 0.15s, background 0.15s, border-color 0.15s;
|
|
302
|
+
user-select: none;
|
|
303
|
+
}
|
|
304
|
+
.chip .k { color: var(--ink-2); }
|
|
305
|
+
.chip .v { color: var(--ink); }
|
|
306
|
+
.chip:hover { background: var(--paper-2); color: var(--ink); }
|
|
307
|
+
.chip.sel {
|
|
308
|
+
border-color: color-mix(in srgb, var(--accent) 55%, transparent);
|
|
309
|
+
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
|
310
|
+
}
|
|
311
|
+
.chip.sel .k, .chip.sel .v { color: var(--accent); }
|
|
312
|
+
.chip.readonly { cursor: default; opacity: 0.85; }
|
|
313
|
+
.chip.readonly:hover { background: none; }
|
|
314
|
+
.chip.flash .v { animation: flashval 0.5s ease; }
|
|
315
|
+
@keyframes flashval {
|
|
316
|
+
0% { color: var(--magenta); text-shadow: 0 0 8px color-mix(in srgb, var(--magenta) 60%, transparent); }
|
|
317
|
+
100% {}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* ── Hero chart ───────────────────────────────────────── */
|
|
321
|
+
|
|
322
|
+
.hero .panel-label { color: var(--accent); }
|
|
323
|
+
.hero-wrap {
|
|
324
|
+
position: relative;
|
|
325
|
+
height: 340px;
|
|
326
|
+
margin-top: 4px;
|
|
327
|
+
}
|
|
328
|
+
.hero-cursor {
|
|
329
|
+
color: var(--accent);
|
|
330
|
+
animation: blink 1.1s steps(1) infinite;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* ── Status line ──────────────────────────────────────── */
|
|
334
|
+
|
|
335
|
+
.statusline {
|
|
336
|
+
font-size: 12.5px;
|
|
337
|
+
padding: 9px 12px;
|
|
338
|
+
margin-bottom: 14px;
|
|
339
|
+
border: var(--rule-w) solid var(--rule);
|
|
340
|
+
border-radius: var(--r-2);
|
|
341
|
+
background: var(--panel);
|
|
342
|
+
}
|
|
343
|
+
.statusline.calm { color: var(--muted); }
|
|
344
|
+
.statusline.calm::before { content: ""; }
|
|
345
|
+
.statusline.alert {
|
|
346
|
+
color: var(--alert);
|
|
347
|
+
border-color: color-mix(in srgb, var(--alert) 45%, transparent);
|
|
348
|
+
background: color-mix(in srgb, var(--alert) 8%, var(--panel));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/* ── System charts ────────────────────────────────────── */
|
|
352
|
+
|
|
353
|
+
.sys-grid {
|
|
354
|
+
display: grid;
|
|
355
|
+
grid-template-columns: repeat(2, 1fr);
|
|
356
|
+
gap: 16px;
|
|
357
|
+
}
|
|
358
|
+
.sys-chart-title {
|
|
359
|
+
font-size: 11px;
|
|
360
|
+
text-transform: uppercase;
|
|
361
|
+
letter-spacing: 0.08em;
|
|
362
|
+
color: var(--muted);
|
|
363
|
+
margin-bottom: 6px;
|
|
364
|
+
}
|
|
365
|
+
.sys-canvas-wrap { position: relative; height: 150px; }
|
|
366
|
+
|
|
367
|
+
/* ── Tables (config / artifacts) ──────────────────────── */
|
|
368
|
+
|
|
369
|
+
.config-table {
|
|
370
|
+
width: 100%;
|
|
371
|
+
border-collapse: collapse;
|
|
372
|
+
font-size: 12.5px;
|
|
373
|
+
}
|
|
374
|
+
.config-table tr { border-bottom: var(--rule-w) solid var(--rule-2); }
|
|
375
|
+
.config-table tr:last-child { border-bottom: none; }
|
|
376
|
+
.config-table td { padding: 7px 10px; }
|
|
377
|
+
.cfg-key { font-weight: 600; color: var(--accent); width: 32%; }
|
|
378
|
+
.cfg-val { color: var(--ink-2); }
|
|
379
|
+
|
|
380
|
+
.mono { font-family: var(--ff-mono); }
|
|
381
|
+
|
|
382
|
+
/* ── Empty state ──────────────────────────────────────── */
|
|
383
|
+
.empty {
|
|
384
|
+
text-align: center;
|
|
385
|
+
color: var(--muted);
|
|
386
|
+
padding: 60px 20px;
|
|
387
|
+
font-size: 13px;
|
|
388
|
+
}
|
|
389
|
+
.empty .big { font-size: 15px; color: var(--ink-2); margin-bottom: 6px; }
|
|
390
|
+
|
|
391
|
+
/* ── Responsive ───────────────────────────────────────── */
|
|
392
|
+
@media (max-width: 700px) {
|
|
393
|
+
.sys-grid { grid-template-columns: 1fr; }
|
|
394
|
+
.selectors { flex-direction: column; }
|
|
395
|
+
.field { max-width: 100%; }
|
|
396
|
+
.hero-wrap { height: 260px; }
|
|
397
|
+
.rh-spacer { display: none; }
|
|
398
|
+
}
|
runmonitor/storage.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import threading
|
|
5
|
+
|
|
6
|
+
DB_DIR = os.path.expanduser("~/.runmonitor")
|
|
7
|
+
DB_PATH = os.path.join(DB_DIR, "runs.db")
|
|
8
|
+
ARTIFACTS_DIR = os.path.join(DB_DIR, "artifacts")
|
|
9
|
+
|
|
10
|
+
_lock = threading.Lock()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_db() -> sqlite3.Connection:
|
|
14
|
+
os.makedirs(DB_DIR, exist_ok=True)
|
|
15
|
+
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
|
16
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
17
|
+
conn.row_factory = sqlite3.Row
|
|
18
|
+
return conn
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def init_db():
|
|
22
|
+
with _lock:
|
|
23
|
+
conn = get_db()
|
|
24
|
+
conn.executescript("""
|
|
25
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
26
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
27
|
+
name TEXT NOT NULL UNIQUE,
|
|
28
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
32
|
+
id TEXT PRIMARY KEY,
|
|
33
|
+
project_id INTEGER NOT NULL REFERENCES projects(id),
|
|
34
|
+
name TEXT,
|
|
35
|
+
status TEXT DEFAULT 'running',
|
|
36
|
+
config_json TEXT DEFAULT '{}',
|
|
37
|
+
total_steps INTEGER,
|
|
38
|
+
started_at TEXT DEFAULT (datetime('now')),
|
|
39
|
+
finished_at TEXT
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE IF NOT EXISTS metrics (
|
|
43
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
44
|
+
run_id TEXT NOT NULL REFERENCES runs(id),
|
|
45
|
+
step INTEGER NOT NULL,
|
|
46
|
+
key TEXT NOT NULL,
|
|
47
|
+
value REAL NOT NULL,
|
|
48
|
+
timestamp TEXT DEFAULT (datetime('now'))
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_run_step ON metrics(run_id, step);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_run_key ON metrics(run_id, key);
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
55
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
56
|
+
run_id TEXT NOT NULL REFERENCES runs(id),
|
|
57
|
+
filename TEXT NOT NULL,
|
|
58
|
+
filepath TEXT NOT NULL,
|
|
59
|
+
size_bytes INTEGER,
|
|
60
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
CREATE TABLE IF NOT EXISTS system_metrics (
|
|
64
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
65
|
+
run_id TEXT NOT NULL REFERENCES runs(id),
|
|
66
|
+
step INTEGER NOT NULL,
|
|
67
|
+
cpu_percent REAL,
|
|
68
|
+
mem_percent REAL,
|
|
69
|
+
timestamp TEXT DEFAULT (datetime('now'))
|
|
70
|
+
);
|
|
71
|
+
""")
|
|
72
|
+
conn.commit()
|
|
73
|
+
conn.close()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def create_project(name: str) -> int:
|
|
77
|
+
with _lock:
|
|
78
|
+
conn = get_db()
|
|
79
|
+
cur = conn.execute(
|
|
80
|
+
"INSERT OR IGNORE INTO projects (name) VALUES (?)", (name,)
|
|
81
|
+
)
|
|
82
|
+
if cur.rowcount == 1:
|
|
83
|
+
project_id = cur.lastrowid
|
|
84
|
+
else:
|
|
85
|
+
row = conn.execute("SELECT id FROM projects WHERE name=?", (name,)).fetchone()
|
|
86
|
+
project_id = row["id"]
|
|
87
|
+
conn.commit()
|
|
88
|
+
conn.close()
|
|
89
|
+
return project_id
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def create_run(run_id: str, project_id: int, name: str | None, config_json: str, total_steps: int | None) -> None:
|
|
93
|
+
with _lock:
|
|
94
|
+
conn = get_db()
|
|
95
|
+
conn.execute(
|
|
96
|
+
"""INSERT INTO runs (id, project_id, name, config_json, total_steps)
|
|
97
|
+
VALUES (?, ?, ?, ?, ?)""",
|
|
98
|
+
(run_id, project_id, name, config_json, total_steps),
|
|
99
|
+
)
|
|
100
|
+
conn.commit()
|
|
101
|
+
conn.close()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def log_metrics(run_id: str, metrics: dict, step: int):
|
|
105
|
+
with _lock:
|
|
106
|
+
conn = get_db()
|
|
107
|
+
rows = [(run_id, step, k, v) for k, v in metrics.items()]
|
|
108
|
+
conn.executemany(
|
|
109
|
+
"INSERT INTO metrics (run_id, step, key, value) VALUES (?, ?, ?, ?)",
|
|
110
|
+
rows,
|
|
111
|
+
)
|
|
112
|
+
conn.commit()
|
|
113
|
+
conn.close()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def finish_run(run_id: str, status: str = "finished"):
|
|
117
|
+
with _lock:
|
|
118
|
+
conn = get_db()
|
|
119
|
+
conn.execute(
|
|
120
|
+
"UPDATE runs SET status=?, finished_at=datetime('now') WHERE id=?",
|
|
121
|
+
(status, run_id),
|
|
122
|
+
)
|
|
123
|
+
conn.commit()
|
|
124
|
+
conn.close()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_projects() -> list[dict]:
|
|
128
|
+
conn = get_db()
|
|
129
|
+
rows = conn.execute("SELECT id, name, created_at FROM projects ORDER BY id DESC").fetchall()
|
|
130
|
+
conn.close()
|
|
131
|
+
return [dict(r) for r in rows]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_runs(project_name: str | None = None) -> list[dict]:
|
|
135
|
+
conn = get_db()
|
|
136
|
+
if project_name:
|
|
137
|
+
rows = conn.execute(
|
|
138
|
+
"""SELECT r.*, p.name as project_name
|
|
139
|
+
FROM runs r JOIN projects p ON r.project_id = p.id
|
|
140
|
+
WHERE p.name = ? ORDER BY r.started_at DESC""",
|
|
141
|
+
(project_name,),
|
|
142
|
+
).fetchall()
|
|
143
|
+
else:
|
|
144
|
+
rows = conn.execute(
|
|
145
|
+
"""SELECT r.*, p.name as project_name
|
|
146
|
+
FROM runs r JOIN projects p ON r.project_id = p.id
|
|
147
|
+
ORDER BY r.started_at DESC"""
|
|
148
|
+
).fetchall()
|
|
149
|
+
conn.close()
|
|
150
|
+
return [dict(r) for r in rows]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_metrics(run_id: str, key: str | None = None, limit: int | None = None,
|
|
154
|
+
since: int | None = None) -> list[dict]:
|
|
155
|
+
conn = get_db()
|
|
156
|
+
clauses = ["run_id=?"]
|
|
157
|
+
params: list = [run_id]
|
|
158
|
+
if key:
|
|
159
|
+
clauses.append("key=?")
|
|
160
|
+
params.append(key)
|
|
161
|
+
if since is not None:
|
|
162
|
+
clauses.append("step > ?")
|
|
163
|
+
params.append(since)
|
|
164
|
+
order = "ORDER BY step" if key else "ORDER BY step, key"
|
|
165
|
+
query = f"SELECT step, key, value, timestamp FROM metrics WHERE {' AND '.join(clauses)} {order}"
|
|
166
|
+
if limit:
|
|
167
|
+
query += f" LIMIT {int(limit)}"
|
|
168
|
+
rows = conn.execute(query, tuple(params)).fetchall()
|
|
169
|
+
conn.close()
|
|
170
|
+
return [dict(r) for r in rows]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def get_run_config(run_id: str) -> dict | None:
|
|
174
|
+
conn = get_db()
|
|
175
|
+
row = conn.execute(
|
|
176
|
+
"SELECT config_json, total_steps, status, started_at FROM runs WHERE id=?",
|
|
177
|
+
(run_id,),
|
|
178
|
+
).fetchone()
|
|
179
|
+
conn.close()
|
|
180
|
+
return dict(row) if row else None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# ── Artifacts ──────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
def save_artifact(run_id: str, src_path: str) -> dict:
|
|
186
|
+
"""Copy a file into the run's artifact directory and record it."""
|
|
187
|
+
filename = os.path.basename(src_path)
|
|
188
|
+
dest_dir = os.path.join(ARTIFACTS_DIR, run_id)
|
|
189
|
+
os.makedirs(dest_dir, exist_ok=True)
|
|
190
|
+
dest_path = os.path.join(dest_dir, filename)
|
|
191
|
+
shutil.copy2(src_path, dest_path)
|
|
192
|
+
size_bytes = os.path.getsize(dest_path)
|
|
193
|
+
|
|
194
|
+
with _lock:
|
|
195
|
+
conn = get_db()
|
|
196
|
+
conn.execute(
|
|
197
|
+
"INSERT INTO artifacts (run_id, filename, filepath, size_bytes) VALUES (?, ?, ?, ?)",
|
|
198
|
+
(run_id, filename, dest_path, size_bytes),
|
|
199
|
+
)
|
|
200
|
+
conn.commit()
|
|
201
|
+
conn.close()
|
|
202
|
+
return {"filename": filename, "filepath": dest_path, "size_bytes": size_bytes}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def get_artifacts(run_id: str) -> list[dict]:
|
|
206
|
+
conn = get_db()
|
|
207
|
+
rows = conn.execute(
|
|
208
|
+
"SELECT filename, filepath, size_bytes, created_at FROM artifacts WHERE run_id=? ORDER BY created_at",
|
|
209
|
+
(run_id,),
|
|
210
|
+
).fetchall()
|
|
211
|
+
conn.close()
|
|
212
|
+
return [dict(r) for r in rows]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ── System metrics ─────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
def log_system_metrics(run_id: str, step: int, cpu: float, mem: float):
|
|
218
|
+
with _lock:
|
|
219
|
+
conn = get_db()
|
|
220
|
+
conn.execute(
|
|
221
|
+
"INSERT INTO system_metrics (run_id, step, cpu_percent, mem_percent) VALUES (?, ?, ?, ?)",
|
|
222
|
+
(run_id, step, cpu, mem),
|
|
223
|
+
)
|
|
224
|
+
conn.commit()
|
|
225
|
+
conn.close()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def get_system_metrics(run_id: str) -> list[dict]:
|
|
229
|
+
conn = get_db()
|
|
230
|
+
rows = conn.execute(
|
|
231
|
+
"SELECT step, cpu_percent, mem_percent, timestamp FROM system_metrics WHERE run_id=? ORDER BY step",
|
|
232
|
+
(run_id,),
|
|
233
|
+
).fetchall()
|
|
234
|
+
conn.close()
|
|
235
|
+
return [dict(r) for r in rows]
|