@gravito/zenith 1.1.3 → 1.1.6

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 (70) hide show
  1. package/README.md +28 -10
  2. package/dist/bin.js +43235 -76691
  3. package/dist/client/index.html +13 -0
  4. package/dist/server/index.js +43235 -76691
  5. package/package.json +16 -7
  6. package/CHANGELOG.md +0 -62
  7. package/Dockerfile +0 -46
  8. package/Dockerfile.demo-worker +0 -29
  9. package/bin/flux-console.ts +0 -2
  10. package/doc/ECOSYSTEM_EXPANSION_RFC.md +0 -130
  11. package/docker-compose.yml +0 -40
  12. package/docs/ALERTING_GUIDE.md +0 -71
  13. package/docs/DEPLOYMENT.md +0 -157
  14. package/docs/DOCS_INTERNAL.md +0 -73
  15. package/docs/LARAVEL_ZENITH_ROADMAP.md +0 -109
  16. package/docs/QUASAR_MASTER_PLAN.md +0 -140
  17. package/docs/QUICK_TEST_GUIDE.md +0 -72
  18. package/docs/ROADMAP.md +0 -85
  19. package/docs/integrations/LARAVEL.md +0 -207
  20. package/postcss.config.js +0 -6
  21. package/scripts/debug_redis_keys.ts +0 -24
  22. package/scripts/flood-logs.ts +0 -21
  23. package/scripts/seed.ts +0 -213
  24. package/scripts/verify-throttle.ts +0 -49
  25. package/scripts/worker.ts +0 -124
  26. package/specs/PULSE_SPEC.md +0 -86
  27. package/src/bin.ts +0 -6
  28. package/src/client/App.tsx +0 -72
  29. package/src/client/Layout.tsx +0 -669
  30. package/src/client/Sidebar.tsx +0 -112
  31. package/src/client/ThroughputChart.tsx +0 -158
  32. package/src/client/WorkerStatus.tsx +0 -202
  33. package/src/client/components/BrandIcons.tsx +0 -168
  34. package/src/client/components/ConfirmDialog.tsx +0 -134
  35. package/src/client/components/JobInspector.tsx +0 -487
  36. package/src/client/components/LogArchiveModal.tsx +0 -432
  37. package/src/client/components/NotificationBell.tsx +0 -212
  38. package/src/client/components/PageHeader.tsx +0 -47
  39. package/src/client/components/Toaster.tsx +0 -90
  40. package/src/client/components/UserProfileDropdown.tsx +0 -186
  41. package/src/client/contexts/AuthContext.tsx +0 -105
  42. package/src/client/contexts/NotificationContext.tsx +0 -128
  43. package/src/client/index.css +0 -172
  44. package/src/client/main.tsx +0 -15
  45. package/src/client/pages/LoginPage.tsx +0 -164
  46. package/src/client/pages/MetricsPage.tsx +0 -445
  47. package/src/client/pages/OverviewPage.tsx +0 -519
  48. package/src/client/pages/PulsePage.tsx +0 -409
  49. package/src/client/pages/QueuesPage.tsx +0 -378
  50. package/src/client/pages/SchedulesPage.tsx +0 -535
  51. package/src/client/pages/SettingsPage.tsx +0 -1001
  52. package/src/client/pages/WorkersPage.tsx +0 -380
  53. package/src/client/pages/index.ts +0 -8
  54. package/src/client/utils.ts +0 -15
  55. package/src/server/config/ServerConfigManager.ts +0 -90
  56. package/src/server/index.ts +0 -860
  57. package/src/server/middleware/auth.ts +0 -127
  58. package/src/server/services/AlertService.ts +0 -321
  59. package/src/server/services/CommandService.ts +0 -136
  60. package/src/server/services/LogStreamProcessor.ts +0 -93
  61. package/src/server/services/MaintenanceScheduler.ts +0 -78
  62. package/src/server/services/PulseService.ts +0 -148
  63. package/src/server/services/QueueMetricsCollector.ts +0 -138
  64. package/src/server/services/QueueService.ts +0 -924
  65. package/src/shared/types.ts +0 -223
  66. package/tailwind.config.js +0 -80
  67. package/tests/placeholder.test.ts +0 -7
  68. package/tsconfig.json +0 -29
  69. package/tsconfig.node.json +0 -10
  70. package/vite.config.ts +0 -27
@@ -1,134 +0,0 @@
1
- import { AnimatePresence, motion } from 'framer-motion'
2
- import { createPortal } from 'react-dom'
3
- import { cn } from '../utils'
4
-
5
- /**
6
- * Props for the ConfirmDialog component.
7
- *
8
- * @public
9
- * @since 3.0.0
10
- */
11
- export interface ConfirmDialogProps {
12
- /** Whether the dialog is visible. */
13
- open: boolean
14
- /** Dialog title text. */
15
- title: string
16
- /** Detailed confirmation message. */
17
- message: string
18
- /** Text for the confirmation button. @default 'Confirm' */
19
- confirmText?: string
20
- /** Text for the cancel button. @default 'Cancel' */
21
- cancelText?: string
22
- /** Callback triggered when user confirms the action. */
23
- onConfirm: () => void
24
- /** Callback triggered when user cancels the action. */
25
- onCancel: () => void
26
- /** Visual style of the confirmation button. @default 'danger' */
27
- variant?: 'danger' | 'warning' | 'info'
28
- /** Whether an action is currently in progress (shows a spinner). @default false */
29
- isProcessing?: boolean
30
- }
31
-
32
- /**
33
- * A modal dialog used for user confirmation before performing sensitive actions.
34
- *
35
- * It provides a consistent UI for confirmations across the Zenith dashboard
36
- * and supports different visual variants.
37
- *
38
- * @public
39
- * @since 3.0.0
40
- */
41
- export function ConfirmDialog({
42
- open,
43
- title,
44
- message,
45
- confirmText = 'Confirm',
46
- cancelText = 'Cancel',
47
- onConfirm,
48
- onCancel,
49
- variant = 'danger',
50
- isProcessing = false,
51
- }: ConfirmDialogProps) {
52
- return createPortal(
53
- <AnimatePresence>
54
- {open && (
55
- // biome-ignore lint/a11y/noStaticElementInteractions: Backdrop needs click handler to stop propagation
56
- <div
57
- role="presentation"
58
- className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-[5000] pointer-events-auto"
59
- onClick={(e) => e.stopPropagation()}
60
- onKeyDown={(e) => e.stopPropagation()}
61
- >
62
- <motion.div
63
- initial={{ scale: 0.95, opacity: 0, y: 20 }}
64
- animate={{ scale: 1, opacity: 1, y: 0 }}
65
- exit={{ scale: 0.95, opacity: 0, y: 20 }}
66
- transition={{ type: 'spring', damping: 25, stiffness: 300 }}
67
- className="bg-zinc-900 border border-white/10 rounded-3xl p-8 max-w-md shadow-[0_0_50px_rgba(0,0,0,0.8)] scanline overflow-hidden"
68
- onClick={(e) => e.stopPropagation()}
69
- >
70
- <h3 className="text-2xl font-black mb-3 font-heading tracking-tight text-white uppercase italic italic">
71
- {title}
72
- </h3>
73
- <div className="h-px w-full bg-white/5 mb-6" />
74
- <p className="text-[13px] font-bold text-muted-foreground mb-8 leading-relaxed uppercase tracking-wide opacity-80">
75
- {message}
76
- </p>
77
- <div className="flex gap-4 justify-end">
78
- <button
79
- type="button"
80
- onClick={(e) => {
81
- e.stopPropagation()
82
- onCancel()
83
- }}
84
- disabled={isProcessing}
85
- className="px-6 py-3 bg-zinc-800 text-white/60 rounded-xl hover:bg-zinc-700 transition-all disabled:opacity-20 disabled:cursor-not-allowed text-[10px] font-black uppercase tracking-[0.2em] font-heading border border-white/5"
86
- >
87
- {cancelText}
88
- </button>
89
- <button
90
- type="button"
91
- onClick={(e) => {
92
- e.stopPropagation()
93
- onConfirm()
94
- }}
95
- disabled={isProcessing}
96
- className={cn(
97
- 'px-6 py-3 rounded-xl text-black transition-all disabled:opacity-20 disabled:cursor-not-allowed flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] font-heading shadow-lg',
98
- variant === 'danger' &&
99
- 'bg-red-500 shadow-[0_0_20px_rgba(239,68,68,0.3)] hover:bg-red-400',
100
- variant === 'warning' &&
101
- 'bg-amber-500 shadow-[0_0_20px_rgba(245,158,11,0.3)] hover:bg-amber-400',
102
- variant === 'info' &&
103
- 'bg-primary shadow-[0_0_20px_rgba(0,240,255,0.3)] hover:bg-primary/80'
104
- )}
105
- >
106
- {isProcessing && (
107
- <svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" aria-label="Loading">
108
- <title>Loading</title>
109
- <circle
110
- className="opacity-25"
111
- cx="12"
112
- cy="12"
113
- r="10"
114
- stroke="currentColor"
115
- strokeWidth="4"
116
- fill="none"
117
- />
118
- <path
119
- className="opacity-75"
120
- fill="currentColor"
121
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
122
- />
123
- </svg>
124
- )}
125
- {isProcessing ? 'Executing...' : confirmText}
126
- </button>
127
- </div>
128
- </motion.div>
129
- </div>
130
- )}
131
- </AnimatePresence>,
132
- document.body
133
- )
134
- }
@@ -1,487 +0,0 @@
1
- import { useQuery, useQueryClient } from '@tanstack/react-query'
2
- import { motion } from 'framer-motion'
3
- import { AlertCircle, CheckCircle2, Clock, RefreshCcw, Search } from 'lucide-react'
4
- import React from 'react'
5
- import { createPortal } from 'react-dom'
6
- import { cn } from '../utils'
7
- import { ConfirmDialog } from './ConfirmDialog'
8
-
9
- interface Job {
10
- id: string
11
- name?: string
12
- data?: any
13
- status?: string
14
- timestamp?: number
15
- scheduledAt?: string
16
- error?: string
17
- failedAt?: number
18
- _raw?: string
19
- _archived?: boolean
20
- _status?: 'completed' | 'failed'
21
- _archivedAt?: string
22
- }
23
-
24
- /**
25
- * Props for the JobInspector component.
26
- *
27
- * @public
28
- * @since 3.0.0
29
- */
30
- export interface JobInspectorProps {
31
- /** The name of the queue to inspect. */
32
- queueName: string
33
- /** Callback triggered when the inspector is closed. */
34
- onClose: () => void
35
- }
36
-
37
- /**
38
- * A detailed view for inspecting jobs within a specific queue.
39
- *
40
- * It provides tabs for viewing waiting, delayed, and failed jobs,
41
- * as well as an archive search. Users can perform bulk actions like
42
- * deleting or retrying jobs.
43
- *
44
- * @public
45
- * @since 3.0.0
46
- */
47
- export function JobInspector({ queueName, onClose }: JobInspectorProps) {
48
- const [view, setView] = React.useState<'waiting' | 'delayed' | 'failed' | 'archive'>('waiting')
49
- const [page, setPage] = React.useState(1)
50
- const [selectedIndices, setSelectedIndices] = React.useState<Set<number>>(new Set())
51
- const [isProcessing, setIsProcessing] = React.useState(false)
52
- const [confirmDialog, setConfirmDialog] = React.useState<{
53
- open: boolean
54
- title: string
55
- message: string
56
- action: () => void
57
- variant?: 'danger' | 'warning' | 'info'
58
- } | null>(null)
59
-
60
- const queryClient = useQueryClient()
61
-
62
- const { isPending, error, data } = useQuery<{ jobs: Job[]; total?: number }>({
63
- queryKey: ['jobs', queueName, view, page],
64
- queryFn: () => {
65
- const url =
66
- view === 'archive'
67
- ? `/api/queues/${queueName}/archive?page=${page}&limit=50`
68
- : `/api/queues/${queueName}/jobs?type=${view}`
69
- return fetch(url).then((res) => res.json())
70
- },
71
- })
72
-
73
- // Reset selection when view changes
74
- // biome-ignore lint/correctness/useExhaustiveDependencies: We want to reset when view changes
75
- React.useEffect(() => {
76
- setSelectedIndices(new Set())
77
- setPage(1)
78
- }, [view])
79
-
80
- const toggleSelection = (index: number) => {
81
- const next = new Set(selectedIndices)
82
- if (next.has(index)) {
83
- next.delete(index)
84
- } else {
85
- next.add(index)
86
- }
87
- setSelectedIndices(next)
88
- }
89
-
90
- const toggleSelectAll = React.useCallback(() => {
91
- if (!data?.jobs) {
92
- return
93
- }
94
- const availableCount = data.jobs.filter((j) => j._raw && !j._archived).length
95
- if (selectedIndices.size === availableCount && availableCount > 0) {
96
- setSelectedIndices(new Set())
97
- } else {
98
- const indices = new Set<number>()
99
- data.jobs.forEach((j, i) => {
100
- if (j._raw && !j._archived) {
101
- indices.add(i)
102
- }
103
- })
104
- setSelectedIndices(indices)
105
- }
106
- }, [data?.jobs, selectedIndices])
107
-
108
- // Keyboard shortcuts
109
- React.useEffect(() => {
110
- const handleKeyDown = (e: KeyboardEvent) => {
111
- if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
112
- e.preventDefault()
113
- toggleSelectAll()
114
- }
115
- if (e.key === 'Escape') {
116
- if (confirmDialog?.open) {
117
- setConfirmDialog(null)
118
- } else if (selectedIndices.size > 0) {
119
- setSelectedIndices(new Set())
120
- } else {
121
- onClose()
122
- }
123
- }
124
- }
125
-
126
- window.addEventListener('keydown', handleKeyDown)
127
- return () => window.removeEventListener('keydown', handleKeyDown)
128
- }, [selectedIndices, confirmDialog, toggleSelectAll, onClose])
129
-
130
- // Lock body scroll when modal opens
131
- React.useEffect(() => {
132
- const originalOverflow = document.body.style.overflow
133
- const originalPaddingRight = document.body.style.paddingRight
134
- const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth
135
-
136
- document.body.style.overflow = 'hidden'
137
- document.body.style.paddingRight = `${scrollbarWidth}px`
138
-
139
- return () => {
140
- document.body.style.overflow = originalOverflow
141
- document.body.style.paddingRight = originalPaddingRight
142
- }
143
- }, [])
144
-
145
- const handleAction = async (action: 'delete' | 'retry', job: Job) => {
146
- if (!job._raw) {
147
- return
148
- }
149
- const endpoint = action === 'delete' ? 'delete' : 'retry'
150
- const body: any = { raw: job._raw }
151
- if (action === 'delete') {
152
- body.type = view
153
- }
154
-
155
- await fetch(`/api/queues/${queueName}/jobs/${endpoint}`, {
156
- method: 'POST',
157
- headers: { 'Content-Type': 'application/json' },
158
- body: JSON.stringify(body),
159
- })
160
-
161
- queryClient.invalidateQueries({ queryKey: ['jobs', queueName] })
162
- queryClient.invalidateQueries({ queryKey: ['queues'] })
163
- }
164
-
165
- const handleBulkAction = async (action: 'delete' | 'retry') => {
166
- if (selectedIndices.size === 0 || !data?.jobs) {
167
- return
168
- }
169
-
170
- const count = selectedIndices.size
171
- setConfirmDialog({
172
- open: true,
173
- title: `${action === 'delete' ? 'Delete' : 'Retry'} ${count} Jobs?`,
174
- message: `Are you sure you want to ${action} ${count} selected ${view} jobs in "${queueName}"?\n\nThis action cannot be undone.`,
175
- variant: action === 'delete' ? 'danger' : 'warning',
176
- action: async () => {
177
- setIsProcessing(true)
178
- try {
179
- const endpoint = action === 'delete' ? 'bulk-delete' : 'bulk-retry'
180
- const raws = Array.from(selectedIndices)
181
- .map((i) => data?.jobs[i]?._raw)
182
- .filter(Boolean) as string[]
183
-
184
- await fetch(`/api/queues/${queueName}/jobs/${endpoint}`, {
185
- method: 'POST',
186
- headers: { 'Content-Type': 'application/json' },
187
- body: JSON.stringify({ type: view, raws }),
188
- })
189
-
190
- setSelectedIndices(new Set())
191
- queryClient.invalidateQueries({ queryKey: ['jobs', queueName] })
192
- queryClient.invalidateQueries({ queryKey: ['queues'] })
193
- setConfirmDialog(null)
194
- } catch (err) {
195
- console.error(`Failed to ${action} jobs:`, err)
196
- } finally {
197
- setIsProcessing(false)
198
- }
199
- },
200
- })
201
- }
202
-
203
- return createPortal(
204
- <div className="fixed inset-0 z-[1001] flex items-center justify-end p-4 sm:p-6 outline-none pointer-events-none">
205
- <motion.div
206
- initial={{ opacity: 0 }}
207
- animate={{ opacity: 1 }}
208
- exit={{ opacity: 0 }}
209
- className="absolute inset-0 bg-black/60 backdrop-blur-md cursor-default pointer-events-auto"
210
- onClick={onClose}
211
- />
212
-
213
- <motion.div
214
- initial={{ x: '100%', opacity: 0 }}
215
- animate={{ x: 0, opacity: 1 }}
216
- exit={{ x: '100%', opacity: 0 }}
217
- transition={{ type: 'spring', damping: 25, stiffness: 200 }}
218
- className="bg-zinc-950 border-l border-white/10 h-screen w-full max-w-2xl shadow-2xl flex flex-col overflow-hidden relative z-[1002] pointer-events-auto"
219
- onClick={(e) => e.stopPropagation()}
220
- >
221
- <div className="p-8 border-b border-white/5 bg-black/40 flex justify-between items-center flex-shrink-0">
222
- <div>
223
- <h2 className="text-2xl font-black flex items-center gap-3 font-heading tracking-tight italic uppercase">
224
- <Search className="text-primary" size={24} />
225
- Inspector <span className="text-primary/60">/</span> {queueName}
226
- </h2>
227
- <div className="flex items-center gap-3 mt-4">
228
- {(['waiting', 'delayed', 'failed', 'archive'] as const).map((v) => (
229
- <button
230
- type="button"
231
- key={v}
232
- onClick={() => setView(v)}
233
- className={cn(
234
- 'text-[9px] font-black px-3 py-1.5 rounded-lg transition-all border shrink-0 uppercase tracking-[0.2em] font-mono',
235
- view === v
236
- ? v === 'failed'
237
- ? 'bg-red-500 text-black border-red-500 shadow-[0_0_20px_rgba(239,68,68,0.2)]'
238
- : v === 'delayed'
239
- ? 'bg-amber-500 text-black border-amber-500 shadow-[0_0_20px_rgba(245,158,11,0.2)]'
240
- : v === 'archive'
241
- ? 'bg-indigo-500 text-black border-indigo-500 shadow-[0_0_20px_rgba(99,102,241,0.2)]'
242
- : 'bg-primary text-black border-primary shadow-[0_0_20px_rgba(0,240,255,0.2)]'
243
- : 'bg-zinc-900 text-muted-foreground border-white/5 hover:bg-zinc-800'
244
- )}
245
- >
246
- {v}
247
- </button>
248
- ))}
249
- </div>
250
- </div>
251
- <button
252
- type="button"
253
- onClick={onClose}
254
- aria-label="Close"
255
- className="w-12 h-12 rounded-2xl bg-white/5 border border-white/5 hover:bg-white/10 flex items-center justify-center transition-all text-white/40 hover:text-white"
256
- >
257
-
258
- </button>
259
- </div>
260
-
261
- <div className="flex-1 overflow-y-auto bg-black/20 min-h-0 scrollbar-thin">
262
- {isPending && (
263
- <div className="p-20 text-center flex flex-col items-center gap-4">
264
- <RefreshCcw className="animate-spin text-primary opacity-40" size={32} />
265
- <p className="text-[10px] font-black uppercase tracking-[0.3em] text-muted-foreground animate-pulse">
266
- Syncing jobs...
267
- </p>
268
- </div>
269
- )}
270
- {error && (
271
- <div className="p-20 text-center">
272
- <div className="bg-red-500/10 text-red-500 p-8 rounded-2xl border border-red-500/20 font-black uppercase text-xs tracking-widest italic">
273
- Connection Fault: {error.message}
274
- </div>
275
- </div>
276
- )}
277
-
278
- {data?.jobs && data.jobs.length > 0 && (
279
- <div className="px-8 py-4 border-b border-white/5 bg-white/[0.02] flex items-center gap-4">
280
- <input
281
- type="checkbox"
282
- aria-label="Select all jobs"
283
- className="w-4 h-4 rounded border-white/10 bg-black/40 text-primary focus:ring-primary/20"
284
- checked={
285
- selectedIndices.size === data.jobs.filter((j) => j._raw && !j._archived).length &&
286
- selectedIndices.size > 0
287
- }
288
- onChange={toggleSelectAll}
289
- />
290
- <span className="text-[10px] font-black uppercase tracking-[0.2em] text-muted-foreground/60 font-heading">
291
- Batch Operations
292
- </span>
293
- {selectedIndices.size > 0 && (
294
- <div className="ml-auto flex items-center gap-3">
295
- <span className="text-[10px] font-black uppercase text-primary font-mono bg-primary/10 px-2 py-0.5 rounded border border-primary/20">
296
- {selectedIndices.size} Selected
297
- </span>
298
- <button
299
- type="button"
300
- onClick={() => handleBulkAction('delete')}
301
- className="px-3 py-1.5 bg-red-500/10 text-red-500 rounded-lg text-[10px] font-black uppercase hover:bg-red-500 hover:text-white transition-all border border-red-500/20"
302
- >
303
- Delete
304
- </button>
305
- {(view === 'delayed' || view === 'failed') && (
306
- <button
307
- type="button"
308
- onClick={() => handleBulkAction('retry')}
309
- className="px-3 py-1.5 bg-primary/10 text-primary rounded-lg text-[10px] font-black uppercase hover:bg-primary hover:text-black transition-all border border-primary/20"
310
- >
311
- Retry
312
- </button>
313
- )}
314
- </div>
315
- )}
316
- </div>
317
- )}
318
-
319
- {data?.jobs && data.jobs.length === 0 && (
320
- <div className="p-20 text-center text-muted-foreground flex flex-col items-center gap-6 opacity-40">
321
- <div className="w-20 h-20 bg-white/5 rounded-full flex items-center justify-center text-primary/40 border border-white/5">
322
- <CheckCircle2 size={40} />
323
- </div>
324
- <div className="space-y-2">
325
- <p className="text-xl font-black font-heading uppercase italic tracking-widest">
326
- Pipeline Clear
327
- </p>
328
- <p className="text-[10px] font-black uppercase tracking-[0.2em]">
329
- Zero incidents detected in spectrum
330
- </p>
331
- </div>
332
- </div>
333
- )}
334
-
335
- {data?.jobs && (
336
- <div className="p-8 space-y-6">
337
- {data.jobs.map((job, i) => (
338
- <div
339
- key={i}
340
- className={cn(
341
- 'bg-zinc-900/40 border rounded-2xl overflow-hidden transition-all group border-white/5',
342
- selectedIndices.has(i) && 'ring-2 ring-primary border-primary bg-primary/5'
343
- )}
344
- >
345
- <div className="p-4 border-b border-white/5 bg-black/40 flex justify-between items-center text-[10px] font-mono">
346
- <div className="flex items-center gap-4">
347
- {job._raw && !job._archived && (
348
- <input
349
- type="checkbox"
350
- aria-label={`Select job ${job.id || i}`}
351
- className="w-4 h-4 rounded border-white/10 bg-black/40 text-primary focus:ring-primary/20"
352
- checked={selectedIndices.has(i)}
353
- onChange={() => toggleSelection(i)}
354
- />
355
- )}
356
- <span className="bg-primary/10 text-primary px-2 py-1 rounded-md font-black uppercase tracking-widest flex items-center gap-2 border border-primary/20 shadow-[0_0_15px_rgba(0,240,255,0.05)]">
357
- ID:{job.id || 'N/A'}
358
- {job._archived && (
359
- <span
360
- className={cn(
361
- 'px-1.5 py-0.5 rounded text-[8px] border ml-1',
362
- job._status === 'completed'
363
- ? 'bg-green-500/20 text-green-500 border-green-500/20'
364
- : 'bg-red-500/20 text-red-500 border-red-500/20'
365
- )}
366
- >
367
- {job._status}
368
- </span>
369
- )}
370
- </span>
371
- </div>
372
- <span className="text-white/20 font-black flex items-center gap-4 uppercase tracking-tighter">
373
- {view === 'delayed' && job.scheduledAt && (
374
- <span className="text-amber-500 flex items-center gap-1.5">
375
- <Clock size={12} /> {new Date(job.scheduledAt).toLocaleString()}
376
- </span>
377
- )}
378
- {view === 'failed' && job.failedAt && (
379
- <span className="text-red-500 flex items-center gap-1.5">
380
- <AlertCircle size={12} /> {new Date(job.failedAt).toLocaleString()}
381
- </span>
382
- )}
383
- {job.timestamp &&
384
- !job._archivedAt &&
385
- new Date(job.timestamp).toLocaleString()}
386
- </span>
387
- </div>
388
- <button
389
- type="button"
390
- onClick={() => job._raw && !job._archived && toggleSelection(i)}
391
- className="w-full text-left cursor-pointer focus:outline-none focus:ring-inset"
392
- >
393
- {job.error && (
394
- <div className="p-5 bg-red-500/10 text-red-500 text-xs font-black border-b border-red-500/10 flex items-start gap-3 uppercase tracking-tight">
395
- <AlertCircle size={16} className="mt-0.5 shrink-0" />
396
- <p>{job.error}</p>
397
- </div>
398
- )}
399
- <pre className="text-[11px] font-mono p-6 overflow-x-auto text-white/60 leading-relaxed bg-black/40">
400
- {JSON.stringify(job, null, 2)}
401
- </pre>
402
- </button>
403
- <div className="p-4 bg-black/20 border-t border-white/5 flex justify-end gap-3">
404
- {!job._archived && (
405
- <button
406
- type="button"
407
- onClick={() => handleAction('delete', job)}
408
- className="text-[10px] font-black uppercase tracking-[0.2em] px-5 py-2.5 rounded-xl hover:bg-red-500/10 text-red-500/60 hover:text-red-500 transition-all font-heading border border-transparent hover:border-red-500/20"
409
- >
410
- Terminate
411
- </button>
412
- )}
413
- {!job._archived && (view === 'delayed' || view === 'failed') && (
414
- <button
415
- type="button"
416
- onClick={() => handleAction('retry', job)}
417
- className={cn(
418
- 'text-[10px] font-black uppercase tracking-[0.2em] px-5 py-2.5 rounded-xl text-black shadow-lg transition-all font-heading',
419
- view === 'delayed'
420
- ? 'bg-amber-500 shadow-amber-500/20 hover:bg-amber-400'
421
- : 'bg-primary shadow-primary/20 hover:bg-primary/80'
422
- )}
423
- >
424
- {view === 'delayed' ? 'Execute Now' : 'Re-Run Cycle'}
425
- </button>
426
- )}
427
- </div>
428
- </div>
429
- ))}
430
-
431
- {view === 'archive' && data?.total && data.total > 50 && (
432
- <div className="flex items-center justify-between py-6 border-t border-border/30">
433
- <p className="text-[10px] font-black text-muted-foreground uppercase tracking-widest">
434
- Total {data.total} archived jobs
435
- </p>
436
- <div className="flex items-center gap-2">
437
- <button
438
- type="button"
439
- onClick={() => setPage((p) => Math.max(1, p - 1))}
440
- disabled={page === 1}
441
- aria-label="Previous page"
442
- className="p-2 rounded-lg bg-muted text-muted-foreground disabled:opacity-30 hover:bg-primary hover:text-white transition-all"
443
- >
444
-
445
- </button>
446
- <span className="text-xs font-bold px-4">{page}</span>
447
- <button
448
- type="button"
449
- onClick={() => setPage((p) => p + 1)}
450
- disabled={page * 50 >= (data.total || 0)}
451
- aria-label="Next page"
452
- className="p-2 rounded-lg bg-muted text-muted-foreground disabled:opacity-30 hover:bg-primary hover:text-white transition-all"
453
- >
454
-
455
- </button>
456
- </div>
457
- </div>
458
- )}
459
- </div>
460
- )}
461
- </div>
462
- <div className="p-4 border-t bg-card text-right flex-shrink-0">
463
- <button
464
- type="button"
465
- onClick={onClose}
466
- className="px-8 py-3 bg-muted text-foreground rounded-xl hover:bg-muted/80 text-sm font-bold transition-all active:scale-95 uppercase tracking-widest"
467
- >
468
- Dismiss
469
- </button>
470
- </div>
471
- </motion.div>
472
-
473
- {confirmDialog && (
474
- <ConfirmDialog
475
- open={confirmDialog.open}
476
- title={confirmDialog.title}
477
- message={confirmDialog.message}
478
- variant={confirmDialog.variant}
479
- isProcessing={isProcessing}
480
- onConfirm={confirmDialog.action}
481
- onCancel={() => setConfirmDialog(null)}
482
- />
483
- )}
484
- </div>,
485
- document.body
486
- )
487
- }