@happyvertical/smrt-jobs 0.30.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 (92) hide show
  1. package/AGENTS.md +71 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +151 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/background-policy.d.ts +121 -0
  8. package/dist/background-policy.d.ts.map +1 -0
  9. package/dist/chunks/runner-DV8FBO0y.js +1642 -0
  10. package/dist/chunks/runner-DV8FBO0y.js.map +1 -0
  11. package/dist/chunks/worker-liveness-DOTjoIjr.js +65 -0
  12. package/dist/chunks/worker-liveness-DOTjoIjr.js.map +1 -0
  13. package/dist/error-redaction.d.ts +48 -0
  14. package/dist/error-redaction.d.ts.map +1 -0
  15. package/dist/index.d.ts +13 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +926 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/job-builder.d.ts +94 -0
  20. package/dist/job-builder.d.ts.map +1 -0
  21. package/dist/job-handle.d.ts +71 -0
  22. package/dist/job-handle.d.ts.map +1 -0
  23. package/dist/logger-extension.d.ts +58 -0
  24. package/dist/logger-extension.d.ts.map +1 -0
  25. package/dist/manifest.json +1327 -0
  26. package/dist/object-extension.d.ts +68 -0
  27. package/dist/object-extension.d.ts.map +1 -0
  28. package/dist/playground.d.ts +2 -0
  29. package/dist/playground.d.ts.map +1 -0
  30. package/dist/playground.js +179 -0
  31. package/dist/playground.js.map +1 -0
  32. package/dist/runner.d.ts +189 -0
  33. package/dist/runner.d.ts.map +1 -0
  34. package/dist/runner.js +15 -0
  35. package/dist/runner.js.map +1 -0
  36. package/dist/schedule-runner.d.ts +151 -0
  37. package/dist/schedule-runner.d.ts.map +1 -0
  38. package/dist/smrt-job-event.d.ts +54 -0
  39. package/dist/smrt-job-event.d.ts.map +1 -0
  40. package/dist/smrt-job.d.ts +215 -0
  41. package/dist/smrt-job.d.ts.map +1 -0
  42. package/dist/smrt-knowledge.json +508 -0
  43. package/dist/smrt-worker.d.ts +72 -0
  44. package/dist/smrt-worker.d.ts.map +1 -0
  45. package/dist/stale-recovery.d.ts +34 -0
  46. package/dist/stale-recovery.d.ts.map +1 -0
  47. package/dist/svelte/components/JobActions.svelte +103 -0
  48. package/dist/svelte/components/JobActions.svelte.d.ts +23 -0
  49. package/dist/svelte/components/JobActions.svelte.d.ts.map +1 -0
  50. package/dist/svelte/components/JobDashboard.svelte +199 -0
  51. package/dist/svelte/components/JobDashboard.svelte.d.ts +27 -0
  52. package/dist/svelte/components/JobDashboard.svelte.d.ts.map +1 -0
  53. package/dist/svelte/components/JobDetail.svelte +256 -0
  54. package/dist/svelte/components/JobDetail.svelte.d.ts +17 -0
  55. package/dist/svelte/components/JobDetail.svelte.d.ts.map +1 -0
  56. package/dist/svelte/components/JobList.svelte +360 -0
  57. package/dist/svelte/components/JobList.svelte.d.ts +28 -0
  58. package/dist/svelte/components/JobList.svelte.d.ts.map +1 -0
  59. package/dist/svelte/components/JobStats.svelte +242 -0
  60. package/dist/svelte/components/JobStats.svelte.d.ts +15 -0
  61. package/dist/svelte/components/JobStats.svelte.d.ts.map +1 -0
  62. package/dist/svelte/components/JobStatusBadge.svelte +23 -0
  63. package/dist/svelte/components/JobStatusBadge.svelte.d.ts +9 -0
  64. package/dist/svelte/components/JobStatusBadge.svelte.d.ts.map +1 -0
  65. package/dist/svelte/components/types.d.ts +9 -0
  66. package/dist/svelte/components/types.d.ts.map +1 -0
  67. package/dist/svelte/components/types.js +8 -0
  68. package/dist/svelte/i18n.d.ts +22 -0
  69. package/dist/svelte/i18n.d.ts.map +1 -0
  70. package/dist/svelte/i18n.js +22 -0
  71. package/dist/svelte/index.d.ts +25 -0
  72. package/dist/svelte/index.d.ts.map +1 -0
  73. package/dist/svelte/index.js +28 -0
  74. package/dist/svelte/playground.d.ts +329 -0
  75. package/dist/svelte/playground.d.ts.map +1 -0
  76. package/dist/svelte/playground.js +174 -0
  77. package/dist/svelte/types.d.ts +191 -0
  78. package/dist/svelte/types.d.ts.map +1 -0
  79. package/dist/svelte/types.js +87 -0
  80. package/dist/ui.d.ts +10 -0
  81. package/dist/ui.d.ts.map +1 -0
  82. package/dist/ui.js +69 -0
  83. package/dist/ui.js.map +1 -0
  84. package/dist/worker-liveness-thread.d.ts +2 -0
  85. package/dist/worker-liveness-thread.d.ts.map +1 -0
  86. package/dist/worker-liveness-thread.js +66 -0
  87. package/dist/worker-liveness-thread.js.map +1 -0
  88. package/dist/worker-liveness-ticker.d.ts +30 -0
  89. package/dist/worker-liveness-ticker.d.ts.map +1 -0
  90. package/dist/worker-liveness.d.ts +71 -0
  91. package/dist/worker-liveness.d.ts.map +1 -0
  92. package/package.json +93 -0
@@ -0,0 +1,360 @@
1
+ <script lang="ts">
2
+ /**
3
+ * JobList - Display a filterable, sortable list of background jobs
4
+ */
5
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
6
+ import type { Snippet } from 'svelte';
7
+ import { M } from '../i18n.js';
8
+ import JobActions from './JobActions.svelte';
9
+ import JobStatusBadge from './JobStatusBadge.svelte';
10
+ import type { JobData, JobFilter, JobSort } from './types.js';
11
+ import { formatRelativeTime, getPriorityLabel } from './types.js';
12
+
13
+ const { t } = useI18n();
14
+
15
+ export interface Props {
16
+ /** Jobs to display */
17
+ jobs: JobData[];
18
+ /** Loading state */
19
+ loading?: boolean;
20
+ /** Selectable rows */
21
+ selectable?: boolean;
22
+ /** Selected job IDs */
23
+ selected?: Set<string>;
24
+ /** Show actions column */
25
+ showActions?: boolean;
26
+ /** Callback when selection changes */
27
+ onSelectionChange?: (selected: Set<string>) => void;
28
+ /** Callback when job is clicked */
29
+ onJobClick?: (job: JobData) => void;
30
+ /** Callback when retry is clicked */
31
+ onRetry?: (job: JobData) => void;
32
+ /** Callback when cancel is clicked */
33
+ onCancel?: (job: JobData) => void;
34
+ /** Empty state snippet */
35
+ empty?: Snippet;
36
+ }
37
+
38
+ let {
39
+ jobs = [],
40
+ loading = false,
41
+ selectable = false,
42
+ selected = $bindable(new Set<string>()),
43
+ showActions = true,
44
+ onSelectionChange,
45
+ onJobClick,
46
+ onRetry,
47
+ onCancel,
48
+ empty,
49
+ }: Props = $props();
50
+
51
+ // Handle row click
52
+ function handleRowClick(job: JobData) {
53
+ onJobClick?.(job);
54
+ }
55
+
56
+ // Handle row selection
57
+ function handleRowSelect(jobId: string, event: Event) {
58
+ event.stopPropagation();
59
+ const newSelected = new Set(selected);
60
+ if (newSelected.has(jobId)) {
61
+ newSelected.delete(jobId);
62
+ } else {
63
+ newSelected.add(jobId);
64
+ }
65
+ selected = newSelected;
66
+ onSelectionChange?.(newSelected);
67
+ }
68
+
69
+ // Handle select all
70
+ function handleSelectAll() {
71
+ if (allSelected) {
72
+ selected = new Set();
73
+ } else {
74
+ selected = new Set(jobs.map((job) => job.id));
75
+ }
76
+ onSelectionChange?.(selected);
77
+ }
78
+
79
+ // Selection state
80
+ const allSelected = $derived(
81
+ jobs.length > 0 && jobs.every((job) => selected.has(job.id)),
82
+ );
83
+ const someSelected = $derived(
84
+ jobs.some((job) => selected.has(job.id)) && !allSelected,
85
+ );
86
+
87
+ // Action to set indeterminate state
88
+ function setIndeterminate(node: HTMLInputElement, value: boolean) {
89
+ node.indeterminate = value;
90
+ return {
91
+ update(newValue: boolean) {
92
+ node.indeterminate = newValue;
93
+ },
94
+ };
95
+ }
96
+ </script>
97
+
98
+ <div class="job-list-container">
99
+ <table class="job-list" class:loading>
100
+ <thead class="job-list__head">
101
+ <tr>
102
+ {#if selectable}
103
+ <th class="job-list__cell job-list__cell--checkbox">
104
+ <input
105
+ type="checkbox"
106
+ checked={allSelected}
107
+ use:setIndeterminate={someSelected}
108
+ onchange={handleSelectAll}
109
+ aria-label={t(M['jobs.job_list.select_all_jobs'])}
110
+ />
111
+ </th>
112
+ {/if}
113
+ <th class="job-list__cell">Status</th>
114
+ <th class="job-list__cell">Queue</th>
115
+ <th class="job-list__cell">Object</th>
116
+ <th class="job-list__cell">Method</th>
117
+ <th class="job-list__cell">Priority</th>
118
+ <th class="job-list__cell">Attempts</th>
119
+ <th class="job-list__cell">Created</th>
120
+ <th class="job-list__cell">{t(M['jobs.job_list.run_at'])}</th>
121
+ {#if showActions}
122
+ <th class="job-list__cell">Actions</th>
123
+ {/if}
124
+ </tr>
125
+ </thead>
126
+
127
+ <tbody class="job-list__body">
128
+ {#if loading}
129
+ <tr class="job-list__row job-list__row--loading">
130
+ <td class="job-list__cell job-list__cell--loading" colspan="10">
131
+ <div class="job-list__loading">
132
+ <span class="job-list__spinner"></span>
133
+ <span>{t(M['jobs.job_list.loading_jobs'])}</span>
134
+ </div>
135
+ </td>
136
+ </tr>
137
+ {:else if jobs.length === 0}
138
+ <tr class="job-list__row job-list__row--empty">
139
+ <td class="job-list__cell job-list__cell--empty" colspan="10">
140
+ {#if empty}
141
+ {@render empty()}
142
+ {:else}
143
+ <div class="job-list__empty">
144
+ <span>{t(M['jobs.job_list.no_jobs_found'])}</span>
145
+ </div>
146
+ {/if}
147
+ </td>
148
+ </tr>
149
+ {:else}
150
+ {#each jobs as job (job.id)}
151
+ {@const isSelected = selected.has(job.id)}
152
+ <tr
153
+ class="job-list__row"
154
+ class:job-list__row--selected={isSelected}
155
+ onclick={() => handleRowClick(job)}
156
+ role={onJobClick ? 'button' : undefined}
157
+ tabindex={onJobClick ? 0 : undefined}
158
+ >
159
+ {#if selectable}
160
+ <td class="job-list__cell job-list__cell--checkbox">
161
+ <input
162
+ type="checkbox"
163
+ checked={isSelected}
164
+ onchange={(e) => handleRowSelect(job.id, e)}
165
+ aria-label={t(M['jobs.job_list.select_job'])}
166
+ />
167
+ </td>
168
+ {/if}
169
+ <td class="job-list__cell">
170
+ <JobStatusBadge status={job.status} />
171
+ </td>
172
+ <td class="job-list__cell job-list__cell--queue">
173
+ {job.queue}
174
+ </td>
175
+ <td class="job-list__cell job-list__cell--object">
176
+ <span class="object-type">{job.objectType}</span>
177
+ {#if job.objectId}
178
+ <span class="object-id">#{job.objectId.slice(0, 8)}</span>
179
+ {/if}
180
+ </td>
181
+ <td class="job-list__cell job-list__cell--method">
182
+ <code>{job.method}</code>
183
+ </td>
184
+ <td class="job-list__cell job-list__cell--priority">
185
+ {getPriorityLabel(job.priority)}
186
+ </td>
187
+ <td class="job-list__cell job-list__cell--attempts">
188
+ {job.attempts}/{job.maxAttempts}
189
+ </td>
190
+ <td class="job-list__cell job-list__cell--date">
191
+ {formatRelativeTime(job.createdAt)}
192
+ </td>
193
+ <td class="job-list__cell job-list__cell--date">
194
+ {formatRelativeTime(job.runAt)}
195
+ </td>
196
+ {#if showActions}
197
+ <td class="job-list__cell job-list__cell--actions">
198
+ <JobActions
199
+ {job}
200
+ compact
201
+ showDelete={false}
202
+ {onRetry}
203
+ {onCancel}
204
+ />
205
+ </td>
206
+ {/if}
207
+ </tr>
208
+ {/each}
209
+ {/if}
210
+ </tbody>
211
+ </table>
212
+ </div>
213
+
214
+ <style>
215
+ .job-list-container {
216
+ width: 100%;
217
+ overflow-x: auto;
218
+ }
219
+
220
+ .job-list {
221
+ width: 100%;
222
+ border-collapse: collapse;
223
+ border-spacing: 0;
224
+ font: var(--smrt-typography-body-medium-font, 0.875rem / 1.25 sans-serif);
225
+ background: var(--smrt-color-surface, #ffffff);
226
+ }
227
+
228
+ .job-list.loading {
229
+ opacity: 0.7;
230
+ pointer-events: none;
231
+ }
232
+
233
+ .job-list__head {
234
+ background: var(--smrt-color-surface-container, #f3f4f6);
235
+ }
236
+
237
+ .job-list__head th {
238
+ padding: var(--smrt-spacing-sm, 0.5rem) var(--smrt-spacing-md, 1rem);
239
+ font-weight: var(--smrt-typography-weight-semibold, 600);
240
+ text-align: left;
241
+ white-space: nowrap;
242
+ border-bottom: 1px solid var(--smrt-color-outline-variant, #c4c6cf);
243
+ }
244
+
245
+ .job-list__body tr {
246
+ border-bottom: 1px solid var(--smrt-color-outline-variant, #c4c6cf);
247
+ transition: background-color var(--smrt-duration-short2, 150ms) var(--smrt-easing-standard, ease);
248
+ }
249
+
250
+ .job-list__body tr:hover:not(.job-list__row--loading):not(.job-list__row--empty) {
251
+ background: var(--smrt-color-surface-container-low, #f9fafb);
252
+ cursor: pointer;
253
+ }
254
+
255
+ .job-list__row--selected {
256
+ background: var(--smrt-color-primary-container, #d6e3ff) !important;
257
+ }
258
+
259
+ @media (prefers-reduced-motion: reduce) {
260
+ .job-list__body tr {
261
+ transition: none;
262
+ }
263
+
264
+ .job-list__spinner {
265
+ animation: none;
266
+ }
267
+ }
268
+
269
+ .job-list__cell {
270
+ padding: var(--smrt-spacing-sm, 0.5rem) var(--smrt-spacing-md, 1rem);
271
+ vertical-align: middle;
272
+ }
273
+
274
+ .job-list__cell--checkbox {
275
+ width: 40px;
276
+ text-align: center;
277
+ }
278
+
279
+ .job-list__cell--checkbox input {
280
+ cursor: pointer;
281
+ }
282
+
283
+ .job-list__cell--queue {
284
+ font-family: var(--smrt-font-family-mono, ui-monospace, monospace);
285
+ font: var(--smrt-typography-body-small-font, 0.75rem / 1.25 sans-serif);
286
+ }
287
+
288
+ .job-list__cell--object {
289
+ max-width: 200px;
290
+ }
291
+
292
+ .object-type {
293
+ font-weight: var(--smrt-typography-weight-medium, 500);
294
+ }
295
+
296
+ .object-id {
297
+ margin-left: var(--smrt-spacing-xs, 0.25rem);
298
+ color: var(--smrt-color-on-surface-variant, #43474e);
299
+ font-family: var(--smrt-font-family-mono, ui-monospace, monospace);
300
+ font: var(--smrt-typography-body-small-font, 0.75rem / 1.25 sans-serif);
301
+ }
302
+
303
+ .job-list__cell--method code {
304
+ padding: 0.125rem 0.375rem;
305
+ background: var(--smrt-color-surface-container, #f3f4f6);
306
+ border-radius: var(--smrt-radius-small, 0.25rem);
307
+ font: var(--smrt-typography-body-small-font, 0.75rem / 1.25 sans-serif);
308
+ }
309
+
310
+ .job-list__cell--priority {
311
+ text-align: center;
312
+ }
313
+
314
+ .job-list__cell--attempts {
315
+ text-align: center;
316
+ font-family: var(--smrt-font-family-mono, ui-monospace, monospace);
317
+ }
318
+
319
+ .job-list__cell--date {
320
+ color: var(--smrt-color-on-surface-variant, #43474e);
321
+ white-space: nowrap;
322
+ }
323
+
324
+ .job-list__cell--actions {
325
+ white-space: nowrap;
326
+ }
327
+
328
+ .job-list__cell--loading,
329
+ .job-list__cell--empty {
330
+ padding: var(--smrt-spacing-xl, 2rem);
331
+ text-align: center;
332
+ }
333
+
334
+ .job-list__loading {
335
+ display: flex;
336
+ align-items: center;
337
+ justify-content: center;
338
+ gap: var(--smrt-spacing-sm, 0.5rem);
339
+ color: var(--smrt-color-on-surface-variant, #43474e);
340
+ }
341
+
342
+ .job-list__spinner {
343
+ width: 20px;
344
+ height: 20px;
345
+ border: 2px solid var(--smrt-color-outline-variant, #c4c6cf);
346
+ border-top-color: var(--smrt-color-primary, #005ac1);
347
+ border-radius: var(--smrt-radius-full, 9999px);
348
+ animation: spin 0.8s linear infinite;
349
+ }
350
+
351
+ @keyframes spin {
352
+ to {
353
+ transform: rotate(360deg);
354
+ }
355
+ }
356
+
357
+ .job-list__empty {
358
+ color: var(--smrt-color-on-surface-variant, #43474e);
359
+ }
360
+ </style>
@@ -0,0 +1,28 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { JobData } from './types.js';
3
+ export interface Props {
4
+ /** Jobs to display */
5
+ jobs: JobData[];
6
+ /** Loading state */
7
+ loading?: boolean;
8
+ /** Selectable rows */
9
+ selectable?: boolean;
10
+ /** Selected job IDs */
11
+ selected?: Set<string>;
12
+ /** Show actions column */
13
+ showActions?: boolean;
14
+ /** Callback when selection changes */
15
+ onSelectionChange?: (selected: Set<string>) => void;
16
+ /** Callback when job is clicked */
17
+ onJobClick?: (job: JobData) => void;
18
+ /** Callback when retry is clicked */
19
+ onRetry?: (job: JobData) => void;
20
+ /** Callback when cancel is clicked */
21
+ onCancel?: (job: JobData) => void;
22
+ /** Empty state snippet */
23
+ empty?: Snippet;
24
+ }
25
+ declare const JobList: import("svelte").Component<Props, {}, "selected">;
26
+ type JobList = ReturnType<typeof JobList>;
27
+ export default JobList;
28
+ //# sourceMappingURL=JobList.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JobList.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/JobList.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAItC,OAAO,KAAK,EAAE,OAAO,EAAsB,MAAM,YAAY,CAAC;AAI9D,MAAM,WAAW,KAAK;IACpB,sBAAsB;IACtB,IAAI,EAAE,OAAO,EAAE,CAAC;IAChB,oBAAoB;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,sBAAsB;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,uBAAuB;IACvB,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,0BAA0B;IAC1B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,sCAAsC;IACtC,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC;IACpD,mCAAmC;IACnC,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACpC,qCAAqC;IACrC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACjC,sCAAsC;IACtC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IAClC,0BAA0B;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AA2KD,QAAA,MAAM,OAAO,mDAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
@@ -0,0 +1,242 @@
1
+ <script lang="ts">
2
+ /**
3
+ * JobStats - Display job statistics overview
4
+ */
5
+
6
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
7
+ import { Card } from '@happyvertical/smrt-ui/ui';
8
+ import { M } from '../i18n.js';
9
+ import type { JobStats, QueueStats } from './types.js';
10
+ import { formatDuration } from './types.js';
11
+
12
+ const { t } = useI18n();
13
+
14
+ export interface Props {
15
+ /** Statistics data */
16
+ stats: JobStats;
17
+ /** Queue breakdown */
18
+ queues?: QueueStats[];
19
+ /** Loading state */
20
+ loading?: boolean;
21
+ /** Compact mode */
22
+ compact?: boolean;
23
+ }
24
+
25
+ let { stats, queues = [], loading = false, compact = false }: Props = $props();
26
+
27
+ const successRateFormatted = $derived(
28
+ stats.successRate !== null ? `${(stats.successRate * 100).toFixed(1)}%` : '-',
29
+ );
30
+ </script>
31
+
32
+ <div class="job-stats" class:compact class:loading>
33
+ <div class="job-stats__grid">
34
+ <!-- Total Jobs -->
35
+ <Card padding="sm">
36
+ <div class="stat-card">
37
+ <div class="stat-card__value">{stats.total.toLocaleString()}</div>
38
+ <div class="stat-card__label">{t(M['jobs.job_stats.total_jobs'])}</div>
39
+ </div>
40
+ </Card>
41
+
42
+ <!-- Pending -->
43
+ <Card padding="sm">
44
+ <div class="stat-card stat-card--pending">
45
+ <div class="stat-card__value">{stats.pending.toLocaleString()}</div>
46
+ <div class="stat-card__label">Pending</div>
47
+ </div>
48
+ </Card>
49
+
50
+ <!-- Running -->
51
+ <Card padding="sm">
52
+ <div class="stat-card stat-card--running">
53
+ <div class="stat-card__value">{stats.running.toLocaleString()}</div>
54
+ <div class="stat-card__label">Running</div>
55
+ </div>
56
+ </Card>
57
+
58
+ <!-- Completed -->
59
+ <Card padding="sm">
60
+ <div class="stat-card stat-card--completed">
61
+ <div class="stat-card__value">{stats.completed.toLocaleString()}</div>
62
+ <div class="stat-card__label">Completed</div>
63
+ </div>
64
+ </Card>
65
+
66
+ <!-- Failed -->
67
+ <Card padding="sm">
68
+ <div class="stat-card stat-card--failed">
69
+ <div class="stat-card__value">{stats.failed.toLocaleString()}</div>
70
+ <div class="stat-card__label">Failed</div>
71
+ </div>
72
+ </Card>
73
+
74
+ <!-- Success Rate -->
75
+ <Card padding="sm">
76
+ <div class="stat-card">
77
+ <div class="stat-card__value">{successRateFormatted}</div>
78
+ <div class="stat-card__label">{t(M['jobs.job_stats.success_rate'])}</div>
79
+ </div>
80
+ </Card>
81
+
82
+ <!-- Avg Duration -->
83
+ <Card padding="sm">
84
+ <div class="stat-card">
85
+ <div class="stat-card__value">{formatDuration(stats.avgDuration)}</div>
86
+ <div class="stat-card__label">{t(M['jobs.job_stats.avg_duration'])}</div>
87
+ </div>
88
+ </Card>
89
+ </div>
90
+
91
+ {#if queues.length > 0 && !compact}
92
+ <div class="job-stats__queues">
93
+ <h3>Queues</h3>
94
+ <div class="queue-list">
95
+ {#each queues as queue (queue.name)}
96
+ <Card padding="sm">
97
+ <div class="queue-card">
98
+ <div class="queue-card__header">
99
+ <span class="queue-card__name">{queue.name}</span>
100
+ <span class="queue-card__running">{queue.running} running</span>
101
+ </div>
102
+ <div class="queue-card__stats">
103
+ <div class="queue-stat">
104
+ <span class="queue-stat__value">{queue.pending}</span>
105
+ <span class="queue-stat__label">pending</span>
106
+ </div>
107
+ <div class="queue-stat">
108
+ <span class="queue-stat__value">{queue.completed24h}</span>
109
+ <span class="queue-stat__label">completed/24h</span>
110
+ </div>
111
+ <div class="queue-stat">
112
+ <span class="queue-stat__value">{queue.failed24h}</span>
113
+ <span class="queue-stat__label">failed/24h</span>
114
+ </div>
115
+ <div class="queue-stat">
116
+ <span class="queue-stat__value"
117
+ >{formatDuration(queue.avgDuration)}</span
118
+ >
119
+ <span class="queue-stat__label">avg</span>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </Card>
124
+ {/each}
125
+ </div>
126
+ </div>
127
+ {/if}
128
+ </div>
129
+
130
+ <style>
131
+ .job-stats {
132
+ width: 100%;
133
+ }
134
+
135
+ .job-stats.loading {
136
+ opacity: 0.7;
137
+ pointer-events: none;
138
+ }
139
+
140
+ .job-stats__grid {
141
+ display: grid;
142
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
143
+ gap: var(--smrt-spacing-md, 1rem);
144
+ }
145
+
146
+ .compact .job-stats__grid {
147
+ grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
148
+ gap: var(--smrt-spacing-sm, 0.5rem);
149
+ }
150
+
151
+ .stat-card {
152
+ text-align: center;
153
+ padding: var(--smrt-spacing-sm, 0.5rem);
154
+ }
155
+
156
+ .stat-card__value {
157
+ font: var(--smrt-typography-headline-small-font, 700 1.5rem / 1.2 sans-serif);
158
+ }
159
+
160
+ .compact .stat-card__value {
161
+ font: var(--smrt-typography-title-large-font, 1.25rem / 1.25 sans-serif);
162
+ }
163
+
164
+ .stat-card__label {
165
+ font: var(--smrt-typography-body-small-font, 0.875rem / 1.25 sans-serif);
166
+ color: var(--smrt-color-on-surface-variant, #43474e);
167
+ margin-top: var(--smrt-spacing-xs, 0.25rem);
168
+ }
169
+
170
+ .stat-card--pending .stat-card__value {
171
+ color: var(--smrt-color-on-surface, #1a1c1e);
172
+ }
173
+
174
+ .stat-card--running .stat-card__value {
175
+ color: var(--smrt-color-primary, #005ac1);
176
+ }
177
+
178
+ .stat-card--completed .stat-card__value {
179
+ color: var(--smrt-color-tertiary, #006c4f);
180
+ }
181
+
182
+ .stat-card--failed .stat-card__value {
183
+ color: var(--smrt-color-error, #ba1a1a);
184
+ }
185
+
186
+ .job-stats__queues {
187
+ margin-top: var(--smrt-spacing-lg, 1.5rem);
188
+ }
189
+
190
+ .job-stats__queues h3 {
191
+ margin: 0 0 var(--smrt-spacing-md, 1rem) 0;
192
+ font: var(--smrt-typography-title-medium-font, 600 1rem / 1.5 sans-serif);
193
+ }
194
+
195
+ .queue-list {
196
+ display: grid;
197
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
198
+ gap: var(--smrt-spacing-md, 1rem);
199
+ }
200
+
201
+ .queue-card {
202
+ padding: var(--smrt-spacing-xs, 0.25rem);
203
+ }
204
+
205
+ .queue-card__header {
206
+ display: flex;
207
+ justify-content: space-between;
208
+ align-items: center;
209
+ margin-bottom: var(--spacing-sm, 0.5rem);
210
+ }
211
+
212
+ .queue-card__name {
213
+ font-weight: var(--smrt-typography-weight-semibold, 600);
214
+ font-family: var(--smrt-font-family-mono, ui-monospace, monospace);
215
+ }
216
+
217
+ .queue-card__running {
218
+ font: var(--smrt-typography-body-medium-font, 0.875rem / 1.25 sans-serif);
219
+ color: var(--smrt-color-primary, #005ac1);
220
+ }
221
+
222
+ .queue-card__stats {
223
+ display: flex;
224
+ gap: var(--smrt-spacing-md, 1rem);
225
+ }
226
+
227
+ .queue-stat {
228
+ display: flex;
229
+ flex-direction: column;
230
+ align-items: center;
231
+ }
232
+
233
+ .queue-stat__value {
234
+ font-weight: var(--smrt-typography-weight-semibold, 600);
235
+ font: var(--smrt-typography-title-medium-font, 1rem / 1.5 sans-serif);
236
+ }
237
+
238
+ .queue-stat__label {
239
+ font: var(--smrt-typography-body-small-font, 0.75rem / 1.25 sans-serif);
240
+ color: var(--smrt-color-on-surface-variant, #43474e);
241
+ }
242
+ </style>
@@ -0,0 +1,15 @@
1
+ import type { JobStats, QueueStats } from './types.js';
2
+ export interface Props {
3
+ /** Statistics data */
4
+ stats: JobStats;
5
+ /** Queue breakdown */
6
+ queues?: QueueStats[];
7
+ /** Loading state */
8
+ loading?: boolean;
9
+ /** Compact mode */
10
+ compact?: boolean;
11
+ }
12
+ declare const JobStats: import("svelte").Component<Props, {}, "">;
13
+ type JobStats = ReturnType<typeof JobStats>;
14
+ export default JobStats;
15
+ //# sourceMappingURL=JobStats.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JobStats.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/JobStats.svelte.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAIvD,MAAM,WAAW,KAAK;IACpB,sBAAsB;IACtB,KAAK,EAAE,QAAQ,CAAC;IAChB,sBAAsB;IACtB,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC;IACtB,oBAAoB;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mBAAmB;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAwHD,QAAA,MAAM,QAAQ,2CAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ /**
3
+ * JobStatusBadge - Display job status with appropriate styling
4
+ */
5
+
6
+ import { Badge } from '@happyvertical/smrt-ui/ui';
7
+ import type { JobStatus } from './types.js';
8
+ import { getStatusVariant } from './types.js';
9
+
10
+ export interface Props {
11
+ status: JobStatus;
12
+ size?: 'sm' | 'md';
13
+ }
14
+
15
+ const { status, size = 'sm' }: Props = $props();
16
+
17
+ const variant = $derived(getStatusVariant(status));
18
+ const label = $derived(status.charAt(0).toUpperCase() + status.slice(1));
19
+ </script>
20
+
21
+ <Badge {variant} {size}>
22
+ {label}
23
+ </Badge>
@@ -0,0 +1,9 @@
1
+ import type { JobStatus } from './types.js';
2
+ export interface Props {
3
+ status: JobStatus;
4
+ size?: 'sm' | 'md';
5
+ }
6
+ declare const JobStatusBadge: import("svelte").Component<Props, {}, "">;
7
+ type JobStatusBadge = ReturnType<typeof JobStatusBadge>;
8
+ export default JobStatusBadge;
9
+ //# sourceMappingURL=JobStatusBadge.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JobStatusBadge.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/JobStatusBadge.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAI5C,MAAM,WAAW,KAAK;IACpB,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CACpB;AAqBD,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}