maintenance_on_steroids 0.2.0

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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +267 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +948 -0
  5. data/app/controllers/maintenance_on_steroids/application_controller.rb +66 -0
  6. data/app/controllers/maintenance_on_steroids/dashboard_controller.rb +18 -0
  7. data/app/controllers/maintenance_on_steroids/jobs_controller.rb +59 -0
  8. data/app/controllers/maintenance_on_steroids/runs_controller.rb +223 -0
  9. data/app/jobs/maintenance_on_steroids/run_job.rb +292 -0
  10. data/app/models/maintenance_on_steroids/application_record.rb +6 -0
  11. data/app/models/maintenance_on_steroids/artifact.rb +255 -0
  12. data/app/models/maintenance_on_steroids/run.rb +310 -0
  13. data/app/views/layouts/maintenance_on_steroids/application.html.erb +48 -0
  14. data/app/views/maintenance_on_steroids/dashboard/index.html.erb +163 -0
  15. data/app/views/maintenance_on_steroids/jobs/index.html.erb +35 -0
  16. data/app/views/maintenance_on_steroids/jobs/show.html.erb +107 -0
  17. data/app/views/maintenance_on_steroids/jobs/source.html.erb +79 -0
  18. data/app/views/maintenance_on_steroids/runs/new.html.erb +98 -0
  19. data/app/views/maintenance_on_steroids/runs/show.html.erb +410 -0
  20. data/app/views/maintenance_on_steroids/shared/_auto_refresh.html.erb +85 -0
  21. data/app/views/maintenance_on_steroids/shared/_javascript.html.erb +16 -0
  22. data/app/views/maintenance_on_steroids/shared/_pager.html.erb +20 -0
  23. data/app/views/maintenance_on_steroids/shared/_styles.html.erb +649 -0
  24. data/app/views/maintenance_on_steroids/shared/_task_list_item.html.erb +31 -0
  25. data/config/routes.rb +22 -0
  26. data/lib/generators/maintenance_on_steroids/install/install_generator.rb +56 -0
  27. data/lib/generators/maintenance_on_steroids/install/templates/create_maintenance_on_steroids_tables.rb.erb +56 -0
  28. data/lib/generators/maintenance_on_steroids/install/templates/initializer.rb +42 -0
  29. data/lib/generators/maintenance_on_steroids/job/job_generator.rb +17 -0
  30. data/lib/generators/maintenance_on_steroids/job/templates/job.rb.erb +30 -0
  31. data/lib/maintenance_on_steroids/about_dsl.rb +51 -0
  32. data/lib/maintenance_on_steroids/artifact_dsl.rb +82 -0
  33. data/lib/maintenance_on_steroids/artifacts_proxy.rb +205 -0
  34. data/lib/maintenance_on_steroids/callbacks_dsl.rb +61 -0
  35. data/lib/maintenance_on_steroids/csv_artifact.rb +88 -0
  36. data/lib/maintenance_on_steroids/engine.rb +26 -0
  37. data/lib/maintenance_on_steroids/form_dsl.rb +63 -0
  38. data/lib/maintenance_on_steroids/instrumentation.rb +33 -0
  39. data/lib/maintenance_on_steroids/job_dsl.rb +61 -0
  40. data/lib/maintenance_on_steroids/job_registry.rb +83 -0
  41. data/lib/maintenance_on_steroids/jsonb_artifact.rb +39 -0
  42. data/lib/maintenance_on_steroids/params_proxy.rb +93 -0
  43. data/lib/maintenance_on_steroids/task.rb +109 -0
  44. data/lib/maintenance_on_steroids/text_artifact.rb +74 -0
  45. data/lib/maintenance_on_steroids/version.rb +3 -0
  46. data/lib/maintenance_on_steroids.rb +187 -0
  47. metadata +125 -0
@@ -0,0 +1,649 @@
1
+ <style>
2
+ /* ===== Theme Variables ===== */
3
+ :root {
4
+ --radius: 6px;
5
+ --radius-lg: 8px;
6
+ --transition: 0.2s ease;
7
+ --accent: #58a6ff;
8
+ --accent-hover: #79b8ff;
9
+ --accent-subtle: rgba(88,166,255,0.1);
10
+ --accent-border: rgba(88,166,255,0.25);
11
+ --green: #3fb950;
12
+ --red: #f85149;
13
+ --orange: #d29922;
14
+ --purple: #bc8cff;
15
+ }
16
+
17
+ [data-theme="dark"] {
18
+ --bg-body: #0a0e16;
19
+ --bg-glow: radial-gradient(1100px 420px at 50% -180px, rgba(56, 139, 253, 0.09), transparent 70%);
20
+ --bg-surface: #111722;
21
+ --bg-surface-hover: #171e2b;
22
+ --bg-card: #111722;
23
+ --bg-card-alt: #0a0e16;
24
+ --bg-input: #0a0e16;
25
+ --border-color: #2a3242;
26
+ --border-color-light: #1d2433;
27
+ --text-primary: #e8edf4;
28
+ --text-secondary: #c9d1d9;
29
+ --text-muted: #8b96a5;
30
+ --text-link: #58a6ff;
31
+ --text-link-hover: #79b8ff;
32
+ --topbar-bg: #060a12;
33
+ --topbar-border: #1d2433;
34
+ --shadow-card: 0 1px 3px rgba(0,0,0,0.35), 0 1px 2px rgba(0,0,0,0.25);
35
+ --table-row-hover: rgba(177,186,196,0.05);
36
+ --flash-notice-bg: rgba(63,185,80,0.1);
37
+ --flash-notice-border: rgba(63,185,80,0.3);
38
+ --flash-notice-text: #3fb950;
39
+ --flash-alert-bg: rgba(248,81,73,0.1);
40
+ --flash-alert-border: rgba(248,81,73,0.3);
41
+ --flash-alert-text: #f85149;
42
+ --progress-bg: #1d2433;
43
+ --btn-outline-bg: transparent;
44
+ --btn-outline-border: #2a3242;
45
+ --btn-outline-text: #c9d1d9;
46
+ --btn-outline-hover-bg: #1d2433;
47
+ --code-bg: #0a0e16;
48
+ --code-border: #2a3242;
49
+ }
50
+
51
+ [data-theme="light"] {
52
+ --bg-body: #f5f7fb;
53
+ --bg-glow: radial-gradient(1100px 420px at 50% -180px, rgba(9, 105, 218, 0.05), transparent 70%);
54
+ --bg-surface: #ffffff;
55
+ --bg-surface-hover: #f3f5f9;
56
+ --bg-card: #ffffff;
57
+ --bg-card-alt: #f5f7fb;
58
+ --bg-input: #ffffff;
59
+ --border-color: #d6dce6;
60
+ --border-color-light: #e2e7ef;
61
+ --text-primary: #1c2128;
62
+ --text-secondary: #404853;
63
+ --text-muted: #636e7d;
64
+ --text-link: #0969da;
65
+ --text-link-hover: #0550ae;
66
+ --topbar-bg: #1b2230;
67
+ --topbar-border: #2a3242;
68
+ --shadow-card: 0 1px 2px rgba(16,24,40,0.05), 0 1px 3px rgba(16,24,40,0.07);
69
+ --table-row-hover: rgba(234,238,242,0.55);
70
+ --flash-notice-bg: #dafbe1;
71
+ --flash-notice-border: #aceebb;
72
+ --flash-notice-text: #116329;
73
+ --flash-alert-bg: #ffebe9;
74
+ --flash-alert-border: #ffcecb;
75
+ --flash-alert-text: #cf222e;
76
+ --progress-bg: #d0d7de;
77
+ --btn-outline-bg: #f6f8fa;
78
+ --btn-outline-border: #d0d7de;
79
+ --btn-outline-text: #424a53;
80
+ --btn-outline-hover-bg: #eaeef2;
81
+ --code-bg: #f6f8fa;
82
+ --code-border: #d0d7de;
83
+ }
84
+
85
+ /* ===== Base ===== */
86
+ * { margin: 0; padding: 0; box-sizing: border-box; }
87
+
88
+ body {
89
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
90
+ background: var(--bg-glow) no-repeat, var(--bg-body);
91
+ color: var(--text-primary);
92
+ line-height: 1.6;
93
+ min-height: 100vh;
94
+ transition: background var(--transition), color var(--transition);
95
+ }
96
+
97
+ a { color: var(--text-link); text-decoration: none; transition: color 0.15s; }
98
+ a:hover { color: var(--text-link-hover); }
99
+
100
+ /* ===== Topbar ===== */
101
+ .topbar {
102
+ background: var(--topbar-bg);
103
+ background: color-mix(in srgb, var(--topbar-bg) 86%, transparent);
104
+ -webkit-backdrop-filter: blur(10px) saturate(1.4);
105
+ backdrop-filter: blur(10px) saturate(1.4);
106
+ border-bottom: 1px solid var(--topbar-border);
107
+ padding: 0 24px;
108
+ position: sticky;
109
+ top: 0;
110
+ z-index: 100;
111
+ }
112
+
113
+ .topbar-inner {
114
+ max-width: 1100px;
115
+ margin: 0 auto;
116
+ height: 56px;
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: space-between;
120
+ }
121
+
122
+ .topbar::after {
123
+ content: "";
124
+ position: absolute;
125
+ left: 0;
126
+ right: 0;
127
+ bottom: -1px;
128
+ height: 1px;
129
+ background: linear-gradient(90deg, transparent, rgba(88, 166, 255, 0.5), transparent);
130
+ pointer-events: none;
131
+ }
132
+
133
+ .topbar a { color: #e6edf3; text-decoration: none; }
134
+ .topbar-brand {
135
+ display: flex;
136
+ align-items: baseline;
137
+ gap: 6px;
138
+ text-decoration: none;
139
+ font-weight: 650;
140
+ font-size: 16px;
141
+ letter-spacing: -0.4px;
142
+ color: #ffffff;
143
+ transition: opacity 0.15s;
144
+ }
145
+ .topbar-brand:hover { opacity: 0.85; }
146
+ .topbar-brand .brand-main { color: #f0f6fc; }
147
+ .topbar-brand .brand-highlight {
148
+ background: linear-gradient(90deg, #58a6ff, #bc8cff);
149
+ -webkit-background-clip: text;
150
+ background-clip: text;
151
+ color: transparent;
152
+ }
153
+
154
+ .topbar-nav { display: flex; gap: 4px; align-items: center; }
155
+ .topbar-nav a {
156
+ font-size: 13px;
157
+ font-weight: 500;
158
+ padding: 7px 14px;
159
+ border-radius: 999px;
160
+ color: #8b949e;
161
+ transition: color 0.15s, background 0.15s;
162
+ }
163
+ .topbar-nav a:hover { color: #f0f6fc; background: rgba(255,255,255,0.06); }
164
+ .topbar-nav a.active {
165
+ color: #f0f6fc;
166
+ font-weight: 600;
167
+ background: rgba(88, 166, 255, 0.15);
168
+ }
169
+
170
+ .topbar-sep {
171
+ width: 1px;
172
+ height: 20px;
173
+ background: #2a3242;
174
+ margin: 0 6px;
175
+ }
176
+
177
+ .theme-toggle {
178
+ background: none;
179
+ border: 1px solid #2a3242;
180
+ color: #8b949e;
181
+ width: 32px;
182
+ height: 32px;
183
+ border-radius: 6px;
184
+ cursor: pointer;
185
+ display: flex;
186
+ align-items: center;
187
+ justify-content: center;
188
+ font-size: 16px;
189
+ transition: all 0.15s;
190
+ }
191
+ .theme-toggle:hover { color: #f0f6fc; background: rgba(255,255,255,0.06); }
192
+
193
+ /* ===== Layout ===== */
194
+ .container { max-width: 1100px; margin: 0 auto; padding: 24px 24px; }
195
+
196
+ /* ===== Flash ===== */
197
+ .flash {
198
+ padding: 10px 14px;
199
+ border-radius: var(--radius);
200
+ margin-bottom: 16px;
201
+ font-size: 13px;
202
+ border: 1px solid;
203
+ }
204
+ .flash-notice {
205
+ background: var(--flash-notice-bg);
206
+ border-color: var(--flash-notice-border);
207
+ color: var(--flash-notice-text);
208
+ }
209
+ .flash-alert {
210
+ background: var(--flash-alert-bg);
211
+ border-color: var(--flash-alert-border);
212
+ color: var(--flash-alert-text);
213
+ }
214
+
215
+ /* ===== Typography ===== */
216
+ h1 { font-size: 22px; font-weight: 600; margin-bottom: 4px; color: var(--text-primary); }
217
+ h2 { font-size: 16px; font-weight: 600; margin-bottom: 8px; color: var(--text-primary); }
218
+ h3 { font-size: 14px; font-weight: 600; margin-bottom: 4px; color: var(--text-primary); }
219
+ .subtitle { color: var(--text-muted); font-size: 13px; margin-bottom: 20px; }
220
+
221
+ /* ===== Page Header ===== */
222
+ .page-header {
223
+ display: flex;
224
+ justify-content: space-between;
225
+ align-items: center;
226
+ margin-bottom: 20px;
227
+ padding-bottom: 20px;
228
+ border-bottom: 1px solid var(--border-color-light);
229
+ }
230
+ .page-header h1 { margin-bottom: 2px; }
231
+ .page-header .subtitle { margin-bottom: 0; }
232
+
233
+ .sort-toggle {
234
+ display: flex;
235
+ align-items: center;
236
+ gap: 4px;
237
+ font-size: 13px;
238
+ }
239
+ .sort-toggle .sort-label { color: var(--text-muted); margin-right: 4px; }
240
+ .sort-toggle a {
241
+ padding: 5px 12px;
242
+ border-radius: 999px;
243
+ color: var(--text-muted);
244
+ transition: color 0.15s, background 0.15s;
245
+ }
246
+ .sort-toggle a:hover { color: var(--text-primary); background: var(--bg-surface-hover); }
247
+ .sort-toggle a.active {
248
+ color: var(--accent);
249
+ background: var(--accent-subtle);
250
+ font-weight: 600;
251
+ }
252
+
253
+ /* ===== Cards ===== */
254
+ .card {
255
+ background: var(--bg-card);
256
+ border: 1px solid var(--border-color);
257
+ border-radius: var(--radius-lg);
258
+ padding: 20px;
259
+ margin-bottom: 16px;
260
+ box-shadow: var(--shadow-card);
261
+ transition: background var(--transition), border-color var(--transition);
262
+ }
263
+
264
+ /* ===== Tables ===== */
265
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
266
+ th {
267
+ text-align: left;
268
+ padding: 10px 14px;
269
+ font-weight: 600;
270
+ color: var(--text-muted);
271
+ font-size: 11px;
272
+ text-transform: uppercase;
273
+ letter-spacing: 0.06em;
274
+ border-bottom: 1px solid var(--border-color);
275
+ }
276
+ td {
277
+ padding: 10px 14px;
278
+ border-bottom: 1px solid var(--border-color-light);
279
+ vertical-align: middle;
280
+ color: var(--text-secondary);
281
+ }
282
+ td a { font-weight: 500; }
283
+ tr { transition: background 0.1s; }
284
+ tr:hover td { background: var(--table-row-hover); }
285
+
286
+ /* ===== Badges ===== */
287
+ .badge {
288
+ display: inline-block;
289
+ padding: 2px 10px;
290
+ border-radius: 9999px;
291
+ font-size: 11px;
292
+ font-weight: 600;
293
+ text-transform: uppercase;
294
+ letter-spacing: 0.03em;
295
+ }
296
+ .badge-enqueued { background: rgba(88,166,255,0.15); color: #58a6ff; }
297
+ .badge-new { background: var(--accent-subtle); color: var(--accent); border: 1px solid var(--accent-border); }
298
+
299
+ .auto-refresh {
300
+ margin-left: 8px;
301
+ font-size: 12px;
302
+ color: var(--text-muted);
303
+ white-space: nowrap;
304
+ }
305
+ .auto-refresh::before {
306
+ content: "\21BB";
307
+ display: inline-block;
308
+ margin-right: 4px;
309
+ animation: auto-refresh-spin 2s linear infinite;
310
+ }
311
+ @keyframes auto-refresh-spin {
312
+ from { transform: rotate(0deg); }
313
+ to { transform: rotate(360deg); }
314
+ }
315
+ .badge-running { background: rgba(63,185,80,0.15); color: #3fb950; }
316
+ .badge-pausing { background: rgba(210,153,34,0.15); color: #d29922; }
317
+ .badge-paused { background: rgba(188,140,255,0.15); color: #bc8cff; }
318
+ .badge-cancelling { background: rgba(248,81,73,0.15); color: #f85149; }
319
+ .badge-cancelled { background: rgba(139,148,158,0.15); color: #8b949e; }
320
+ .badge-completed { background: rgba(63,185,80,0.15); color: #3fb950; }
321
+ .badge-errored { background: rgba(248,81,73,0.15); color: #f85149; }
322
+
323
+ /* ===== Buttons ===== */
324
+ .btn {
325
+ display: inline-flex;
326
+ align-items: center;
327
+ gap: 6px;
328
+ padding: 7px 16px;
329
+ border: none;
330
+ border-radius: var(--radius);
331
+ font-size: 13px;
332
+ font-weight: 500;
333
+ cursor: pointer;
334
+ text-decoration: none;
335
+ line-height: 1.4;
336
+ transition: all 0.15s;
337
+ font-family: inherit;
338
+ }
339
+ .btn-sm { padding: 4px 12px; font-size: 12px; }
340
+ .btn-primary { background: #238636; color: #fff; border: 1px solid rgba(240,246,252,0.1); }
341
+ .btn-primary:hover { background: #2ea043; color: #fff; }
342
+ .btn-warning { background: rgba(210,153,34,0.15); color: #d29922; border: 1px solid rgba(210,153,34,0.4); }
343
+ .btn-warning:hover { background: rgba(210,153,34,0.25); color: #d29922; }
344
+ .btn-danger { background: rgba(248,81,73,0.1); color: #f85149; border: 1px solid rgba(248,81,73,0.4); }
345
+ .btn-danger:hover { background: rgba(248,81,73,0.2); color: #f85149; }
346
+ .btn-success { background: #238636; color: #fff; border: 1px solid rgba(240,246,252,0.1); }
347
+ .btn-success:hover { background: #2ea043; color: #fff; }
348
+ .btn-outline {
349
+ background: var(--btn-outline-bg);
350
+ color: var(--btn-outline-text);
351
+ border: 1px solid var(--btn-outline-border);
352
+ }
353
+ .btn-outline:hover { background: var(--btn-outline-hover-bg); color: var(--text-primary); }
354
+ .btn-group { display: flex; gap: 8px; flex-wrap: wrap; }
355
+
356
+ /* ===== Progress ===== */
357
+ .progress-container {
358
+ width: 100%;
359
+ background: var(--progress-bg);
360
+ border-radius: 9999px;
361
+ height: 6px;
362
+ overflow: hidden;
363
+ }
364
+ .progress-bar {
365
+ height: 100%;
366
+ background: #58a6ff;
367
+ border-radius: 9999px;
368
+ transition: width 0.5s ease;
369
+ min-width: 0;
370
+ }
371
+ .progress-bar.completed { background: #3fb950; }
372
+ .progress-bar.errored { background: #f85149; }
373
+ .progress-text { font-size: 12px; color: var(--text-muted); margin-top: 6px; }
374
+
375
+ /* ===== Forms ===== */
376
+ .form-group { margin-bottom: 18px; }
377
+ .form-group label {
378
+ display: block;
379
+ font-weight: 500;
380
+ font-size: 13px;
381
+ margin-bottom: 5px;
382
+ color: var(--text-secondary);
383
+ }
384
+ .form-group .help-text { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
385
+ .form-group input[type="text"],
386
+ .form-group input[type="number"],
387
+ .form-group input[type="date"],
388
+ .form-group input[type="datetime-local"],
389
+ .form-group input[type="file"],
390
+ .form-group textarea,
391
+ .form-group select {
392
+ width: 100%;
393
+ padding: 8px 12px;
394
+ border: 1px solid var(--border-color);
395
+ border-radius: var(--radius);
396
+ font-size: 13px;
397
+ font-family: inherit;
398
+ line-height: 1.5;
399
+ transition: border-color 0.15s;
400
+ background: var(--bg-input);
401
+ color: var(--text-primary);
402
+ }
403
+ .form-group input:focus,
404
+ .form-group textarea:focus,
405
+ .form-group select:focus {
406
+ outline: none;
407
+ border-color: var(--accent);
408
+ }
409
+ .form-group textarea { resize: vertical; min-height: 80px; }
410
+ .required-mark { color: #f85149; }
411
+
412
+ /* ===== Info Grid ===== */
413
+ .info-grid {
414
+ display: grid;
415
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
416
+ gap: 16px;
417
+ }
418
+ .info-item label {
419
+ font-size: 11px;
420
+ color: var(--text-muted);
421
+ text-transform: uppercase;
422
+ letter-spacing: 0.06em;
423
+ font-weight: 600;
424
+ }
425
+ .info-item .value { font-size: 14px; color: var(--text-primary); font-weight: 500; margin-top: 2px; }
426
+
427
+ /* ===== Error ===== */
428
+ .error-box {
429
+ background: rgba(248,81,73,0.08);
430
+ border: 1px solid rgba(248,81,73,0.2);
431
+ border-radius: var(--radius-lg);
432
+ padding: 16px;
433
+ margin-top: 16px;
434
+ }
435
+ .error-box h3 { color: #f85149; }
436
+ .error-box p { color: var(--text-secondary); font-size: 13px; }
437
+ .error-box pre {
438
+ font-size: 12px;
439
+ color: var(--text-muted);
440
+ white-space: pre-wrap;
441
+ word-break: break-word;
442
+ margin-top: 8px;
443
+ max-height: 300px;
444
+ overflow-y: auto;
445
+ background: var(--bg-card-alt);
446
+ border-radius: 6px;
447
+ padding: 12px;
448
+ }
449
+
450
+ /* ===== Artifacts ===== */
451
+ .artifact-card {
452
+ background: var(--bg-card-alt);
453
+ border: 1px solid var(--border-color-light);
454
+ border-radius: var(--radius);
455
+ padding: 12px 14px;
456
+ margin-bottom: 8px;
457
+ }
458
+ .artifact-card .artifact-name { font-weight: 600; font-size: 13px; color: var(--text-primary); }
459
+ .artifact-card .artifact-type { font-size: 12px; color: var(--text-muted); }
460
+ .artifact-card .artifact-desc { font-size: 12px; color: var(--text-muted); margin-top: 6px; }
461
+ .artifact-card table.csv-preview {
462
+ margin-top: 8px;
463
+ border-collapse: collapse;
464
+ font-size: 12px;
465
+ width: 100%;
466
+ }
467
+ .artifact-card table.csv-preview th,
468
+ .artifact-card table.csv-preview td {
469
+ border: 1px solid var(--border-color-light);
470
+ padding: 4px 8px;
471
+ text-align: left;
472
+ white-space: nowrap;
473
+ color: var(--text-secondary);
474
+ }
475
+ .artifact-card table.csv-preview th {
476
+ background: var(--bg-surface-hover);
477
+ font-weight: 600;
478
+ color: var(--text-primary);
479
+ }
480
+ .artifact-card pre {
481
+ background: var(--code-bg);
482
+ border: 1px solid var(--code-border);
483
+ border-radius: 6px;
484
+ padding: 10px;
485
+ font-size: 12px;
486
+ max-height: 200px;
487
+ overflow: auto;
488
+ margin-top: 8px;
489
+ color: var(--text-secondary);
490
+ }
491
+ .artifact-pre-wrap { position: relative; }
492
+ .artifact-pre-wrap pre { margin-top: 8px; }
493
+ .artifact-copy-overlay {
494
+ position: absolute;
495
+ top: 16px;
496
+ right: 8px;
497
+ z-index: 1;
498
+ opacity: 0;
499
+ transition: opacity 0.12s;
500
+ }
501
+ .artifact-pre-wrap:hover .artifact-copy-overlay,
502
+ .artifact-copy-overlay:focus { opacity: 1; }
503
+
504
+ /* ===== Pager ===== */
505
+ .pager {
506
+ display: flex;
507
+ align-items: center;
508
+ justify-content: center;
509
+ gap: 12px;
510
+ margin-top: 16px;
511
+ }
512
+ .pager-info { font-size: 13px; color: var(--text-muted); }
513
+ .pager-disabled { opacity: 0.4; pointer-events: none; }
514
+
515
+ /* ===== Empty ===== */
516
+ .empty-state { text-align: center; padding: 40px 24px; color: var(--text-muted); }
517
+ .empty-state h3 { color: var(--text-secondary); margin-bottom: 6px; }
518
+ .empty-state code { background: var(--bg-card-alt); padding: 2px 8px; border-radius: 4px; font-size: 12px; }
519
+
520
+ /* ===== Breadcrumb ===== */
521
+ .breadcrumb { font-size: 13px; color: var(--text-muted); margin-bottom: 14px; }
522
+ .breadcrumb a { color: var(--text-link); }
523
+ .breadcrumb a:hover { text-decoration: underline; }
524
+
525
+ /* ===== Truncation utilities ===== */
526
+ .cell-truncate,
527
+ .cell-truncate-sm {
528
+ display: inline-block;
529
+ max-width: 240px;
530
+ overflow: hidden;
531
+ text-overflow: ellipsis;
532
+ white-space: nowrap;
533
+ vertical-align: bottom;
534
+ }
535
+ .cell-truncate-sm { max-width: 160px; }
536
+
537
+ /* ===== Task List ===== */
538
+ .task-list-item {
539
+ display: flex;
540
+ justify-content: space-between;
541
+ align-items: center;
542
+ padding: 14px 18px;
543
+ border-bottom: 1px solid var(--border-color-light);
544
+ transition: background 0.1s;
545
+ cursor: pointer;
546
+ text-decoration: none;
547
+ color: inherit;
548
+ }
549
+ .task-list-item:hover { background: var(--bg-surface-hover); }
550
+ .task-list-item:last-child { border-bottom: none; }
551
+ .task-list-item > div:first-child { min-width: 0; flex: 1; padding-right: 12px; }
552
+ .task-list-item > div:last-child { flex-shrink: 0; }
553
+ .task-list-item .task-name { font-weight: 600; font-size: 14px; color: var(--text-primary); }
554
+ .task-list-item .task-desc { font-size: 12px; color: var(--text-muted); margin-top: 2px; }
555
+ .task-list-item .task-meta { font-size: 11px; color: var(--text-muted); margin-top: 3px; font-family: monospace; }
556
+ .task-list-item .task-name,
557
+ .task-list-item .task-desc,
558
+ .task-list-item .task-meta {
559
+ overflow: hidden;
560
+ text-overflow: ellipsis;
561
+ white-space: nowrap;
562
+ }
563
+
564
+ /* ===== Stats ===== */
565
+ .stats-grid {
566
+ display: grid;
567
+ grid-template-columns: repeat(4, 1fr);
568
+ gap: 12px;
569
+ margin-bottom: 24px;
570
+ }
571
+ .stat-card {
572
+ background: var(--bg-card);
573
+ border: 1px solid var(--border-color);
574
+ border-radius: var(--radius-lg);
575
+ padding: 20px 16px;
576
+ text-align: center;
577
+ box-shadow: var(--shadow-card);
578
+ transition: all var(--transition);
579
+ }
580
+ .stat-number {
581
+ font-size: 32px;
582
+ font-weight: 700;
583
+ color: var(--text-primary);
584
+ line-height: 1.1;
585
+ }
586
+ .stat-card-active .stat-number { color: #58a6ff; }
587
+ .stat-card-completed .stat-number { color: #3fb950; }
588
+ .stat-card-errored .stat-number { color: #f85149; }
589
+ .stat-label {
590
+ font-size: 11px;
591
+ color: var(--text-muted);
592
+ text-transform: uppercase;
593
+ letter-spacing: 0.06em;
594
+ font-weight: 600;
595
+ margin-top: 4px;
596
+ }
597
+
598
+ /* ===== Dashboard ===== */
599
+ .dashboard-section { margin-bottom: 28px; }
600
+ .dashboard-section h2 { margin-bottom: 10px; }
601
+
602
+ /* ===== Source Code ===== */
603
+ .source-code-container {
604
+ background: var(--code-bg);
605
+ border: 1px solid var(--code-border);
606
+ border-radius: var(--radius-lg);
607
+ overflow: hidden;
608
+ }
609
+ .source-code-header {
610
+ padding: 10px 16px;
611
+ border-bottom: 1px solid var(--code-border);
612
+ font-size: 12px;
613
+ color: var(--text-muted);
614
+ font-family: monospace;
615
+ display: flex;
616
+ justify-content: space-between;
617
+ align-items: center;
618
+ }
619
+ .source-code-body {
620
+ padding: 16px;
621
+ overflow-x: auto;
622
+ font-family: "SF Mono", "Fira Code", "Fira Mono", Menlo, Consolas, monospace;
623
+ font-size: 13px;
624
+ line-height: 1.6;
625
+ color: var(--text-secondary);
626
+ }
627
+ .source-code-body pre { margin: 0; white-space: pre; }
628
+ .source-line { display: flex; }
629
+ .source-line-number {
630
+ color: var(--text-muted);
631
+ text-align: right;
632
+ padding-right: 16px;
633
+ user-select: none;
634
+ min-width: 36px;
635
+ opacity: 0.5;
636
+ }
637
+ .source-line-content { flex: 1; }
638
+
639
+ /* ===== Responsive ===== */
640
+ @media (max-width: 768px) {
641
+ .container { padding: 16px; }
642
+ .info-grid { grid-template-columns: 1fr 1fr; }
643
+ .stats-grid { grid-template-columns: 1fr 1fr; }
644
+ .topbar { padding: 0 16px; }
645
+ }
646
+ @media (max-width: 480px) {
647
+ .stats-grid { grid-template-columns: 1fr; }
648
+ }
649
+ </style>
@@ -0,0 +1,31 @@
1
+ <a href="<%= job_path(id: task_class.name) %>" class="task-list-item">
2
+ <div>
3
+ <div class="task-name"><%= task_class.task_title %></div>
4
+ <% if task_class.task_description %>
5
+ <div class="task-desc"><%= task_class.task_description %></div>
6
+ <% end %>
7
+ <div class="task-meta">
8
+ <%= task_class.name %>
9
+ <% if task_class.task_owner %>
10
+ &middot; Owner: <%= task_class.task_owner %>
11
+ <% end %>
12
+ </div>
13
+ </div>
14
+ <div style="text-align: right;">
15
+ <%# active_count / last_run are required locals, preloaded by the caller.
16
+ Falling back to a per-row query here would silently reintroduce the
17
+ N+1 that JobsController#index preloads against. %>
18
+ <% if active_count > 0 %>
19
+ <span class="badge badge-running"><%= active_count %> active</span>
20
+ <% end %>
21
+ <% if last_run %>
22
+ <div class="task-meta" style="margin-top: 4px; white-space: nowrap;">
23
+ Last run:
24
+ <span class="badge badge-<%= last_run.status %>"><%= last_run.status %></span>
25
+ <%= time_ago_in_words(last_run.created_at) %> ago
26
+ </div>
27
+ <% else %>
28
+ <span class="badge badge-new" title="Never executed">New</span>
29
+ <% end %>
30
+ </div>
31
+ </a>
data/config/routes.rb ADDED
@@ -0,0 +1,22 @@
1
+ MaintenanceOnSteroids::Engine.routes.draw do
2
+ get "dashboard", to: "dashboard#index", as: :dashboard
3
+
4
+ resources :jobs, only: %i[index show] do
5
+ member do
6
+ get :source
7
+ end
8
+ resources :runs, only: %i[new create]
9
+ end
10
+
11
+ resources :runs, only: %i[show] do
12
+ member do
13
+ post :pause
14
+ post :resume
15
+ post :cancel
16
+ get :status
17
+ get "artifacts/:artifact_id/download", action: :artifact_download, as: :artifact_download
18
+ end
19
+ end
20
+
21
+ root to: "dashboard#index"
22
+ end