create-ekka-desktop-app 0.2.2

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 (96) hide show
  1. package/README.md +137 -0
  2. package/bin/cli.js +72 -0
  3. package/package.json +23 -0
  4. package/template/branding/app.json +6 -0
  5. package/template/branding/icon.icns +0 -0
  6. package/template/eslint.config.js +98 -0
  7. package/template/index.html +29 -0
  8. package/template/package.json +40 -0
  9. package/template/src/app/App.tsx +24 -0
  10. package/template/src/demo/DemoApp.tsx +260 -0
  11. package/template/src/demo/components/Banner.tsx +82 -0
  12. package/template/src/demo/components/EmptyState.tsx +61 -0
  13. package/template/src/demo/components/InfoPopover.tsx +171 -0
  14. package/template/src/demo/components/InfoTooltip.tsx +76 -0
  15. package/template/src/demo/components/LearnMore.tsx +98 -0
  16. package/template/src/demo/components/NodeCredentialsOnboarding.tsx +219 -0
  17. package/template/src/demo/components/SetupWizard.tsx +48 -0
  18. package/template/src/demo/components/StatusBadge.tsx +83 -0
  19. package/template/src/demo/components/index.ts +10 -0
  20. package/template/src/demo/hooks/index.ts +6 -0
  21. package/template/src/demo/hooks/useAuditEvents.ts +30 -0
  22. package/template/src/demo/layout/Shell.tsx +110 -0
  23. package/template/src/demo/layout/Sidebar.tsx +192 -0
  24. package/template/src/demo/pages/AuditLogPage.tsx +235 -0
  25. package/template/src/demo/pages/DocGenPage.tsx +874 -0
  26. package/template/src/demo/pages/HomeSetupPage.tsx +182 -0
  27. package/template/src/demo/pages/LoginPage.tsx +192 -0
  28. package/template/src/demo/pages/PathPermissionsPage.tsx +873 -0
  29. package/template/src/demo/pages/RunnerPage.tsx +445 -0
  30. package/template/src/demo/pages/SystemPage.tsx +557 -0
  31. package/template/src/demo/pages/VaultPage.tsx +805 -0
  32. package/template/src/ekka/__tests__/demo-backend.test.ts +187 -0
  33. package/template/src/ekka/audit/index.ts +7 -0
  34. package/template/src/ekka/audit/store.ts +68 -0
  35. package/template/src/ekka/audit/types.ts +22 -0
  36. package/template/src/ekka/auth/client.ts +212 -0
  37. package/template/src/ekka/auth/index.ts +30 -0
  38. package/template/src/ekka/auth/storage.ts +114 -0
  39. package/template/src/ekka/auth/types.ts +67 -0
  40. package/template/src/ekka/backend/demo.ts +151 -0
  41. package/template/src/ekka/backend/interface.ts +36 -0
  42. package/template/src/ekka/config.ts +48 -0
  43. package/template/src/ekka/constants.ts +143 -0
  44. package/template/src/ekka/errors.ts +54 -0
  45. package/template/src/ekka/index.ts +516 -0
  46. package/template/src/ekka/internal/backend.ts +156 -0
  47. package/template/src/ekka/internal/index.ts +7 -0
  48. package/template/src/ekka/ops/auth.ts +29 -0
  49. package/template/src/ekka/ops/debug.ts +68 -0
  50. package/template/src/ekka/ops/home.ts +101 -0
  51. package/template/src/ekka/ops/index.ts +16 -0
  52. package/template/src/ekka/ops/nodeCredentials.ts +131 -0
  53. package/template/src/ekka/ops/nodeSession.ts +145 -0
  54. package/template/src/ekka/ops/paths.ts +183 -0
  55. package/template/src/ekka/ops/runner.ts +86 -0
  56. package/template/src/ekka/ops/runtime.ts +31 -0
  57. package/template/src/ekka/ops/setup.ts +47 -0
  58. package/template/src/ekka/ops/vault.ts +459 -0
  59. package/template/src/ekka/ops/workflowRuns.ts +116 -0
  60. package/template/src/ekka/types.ts +82 -0
  61. package/template/src/ekka/utils/idempotency.ts +14 -0
  62. package/template/src/ekka/utils/index.ts +7 -0
  63. package/template/src/ekka/utils/time.ts +77 -0
  64. package/template/src/main.tsx +12 -0
  65. package/template/src/vite-env.d.ts +12 -0
  66. package/template/src-tauri/Cargo.toml +41 -0
  67. package/template/src-tauri/build.rs +3 -0
  68. package/template/src-tauri/capabilities/default.json +11 -0
  69. package/template/src-tauri/icons/icon.icns +0 -0
  70. package/template/src-tauri/icons/icon.png +0 -0
  71. package/template/src-tauri/resources/ekka-engine-bootstrap +0 -0
  72. package/template/src-tauri/src/bootstrap.rs +37 -0
  73. package/template/src-tauri/src/commands.rs +1215 -0
  74. package/template/src-tauri/src/device_secret.rs +111 -0
  75. package/template/src-tauri/src/engine_process.rs +538 -0
  76. package/template/src-tauri/src/grants.rs +129 -0
  77. package/template/src-tauri/src/handlers/home.rs +65 -0
  78. package/template/src-tauri/src/handlers/mod.rs +7 -0
  79. package/template/src-tauri/src/handlers/paths.rs +128 -0
  80. package/template/src-tauri/src/handlers/vault.rs +680 -0
  81. package/template/src-tauri/src/main.rs +243 -0
  82. package/template/src-tauri/src/node_auth.rs +858 -0
  83. package/template/src-tauri/src/node_credentials.rs +541 -0
  84. package/template/src-tauri/src/node_runner.rs +882 -0
  85. package/template/src-tauri/src/node_vault_crypto.rs +113 -0
  86. package/template/src-tauri/src/node_vault_store.rs +267 -0
  87. package/template/src-tauri/src/ops/auth.rs +50 -0
  88. package/template/src-tauri/src/ops/home.rs +251 -0
  89. package/template/src-tauri/src/ops/mod.rs +7 -0
  90. package/template/src-tauri/src/ops/runtime.rs +21 -0
  91. package/template/src-tauri/src/state.rs +639 -0
  92. package/template/src-tauri/src/types.rs +84 -0
  93. package/template/src-tauri/tauri.conf.json +41 -0
  94. package/template/tsconfig.json +26 -0
  95. package/template/tsconfig.tsbuildinfo +1 -0
  96. package/template/vite.config.ts +34 -0
@@ -0,0 +1,445 @@
1
+ /**
2
+ * Runner Page - Runner + Task Queue Observability
3
+ *
4
+ * Displays runner task stats to diagnose "workflow stuck" situations:
5
+ * - Active runners (inferred from recent claims)
6
+ * - Queue counts (pending, claimed, completed_5m, failed_5m)
7
+ * - By subtype breakdown
8
+ * - Recent tasks table
9
+ *
10
+ * Polls runner.taskStats() every 2 seconds while tab is open.
11
+ * Stats are fetched via Rust proxy (no direct TS HTTP).
12
+ */
13
+
14
+ import { useState, useEffect, useRef, type CSSProperties, type ReactElement } from 'react';
15
+ import { advanced, type RunnerTaskStats } from '../../ekka';
16
+
17
+ interface RunnerPageProps {
18
+ darkMode: boolean;
19
+ }
20
+
21
+ // Fetch stats via Rust proxy (no direct HTTP from TS)
22
+ async function fetchStats(): Promise<RunnerTaskStats> {
23
+ return advanced.runner.taskStats();
24
+ }
25
+
26
+ // =============================================================================
27
+ // HELPERS
28
+ // =============================================================================
29
+
30
+ function timeAgo(iso: string): string {
31
+ try {
32
+ const now = Date.now();
33
+ const then = new Date(iso).getTime();
34
+ const diffSec = Math.floor((now - then) / 1000);
35
+ if (diffSec < 60) return `${diffSec}s ago`;
36
+ if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago`;
37
+ if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}h ago`;
38
+ return `${Math.floor(diffSec / 86400)}d ago`;
39
+ } catch {
40
+ return iso;
41
+ }
42
+ }
43
+
44
+ function shortId(id: string | null): string {
45
+ if (!id) return '-';
46
+ return id.length > 8 ? `${id.slice(0, 8)}...` : id;
47
+ }
48
+
49
+ // =============================================================================
50
+ // COMPONENT
51
+ // =============================================================================
52
+
53
+ interface ErrorDetails {
54
+ message: string;
55
+ status?: number;
56
+ statusText?: string;
57
+ data?: unknown;
58
+ }
59
+
60
+ export function RunnerPage({ darkMode }: RunnerPageProps): ReactElement {
61
+ const [stats, setStats] = useState<RunnerTaskStats | null>(null);
62
+ const [error, setError] = useState<ErrorDetails | null>(null);
63
+ const [showErrorDetails, setShowErrorDetails] = useState(false);
64
+ const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
65
+ const pollingRef = useRef<number | null>(null);
66
+
67
+ const colors = {
68
+ text: darkMode ? '#ffffff' : '#1d1d1f',
69
+ textMuted: darkMode ? '#98989d' : '#6e6e73',
70
+ bg: darkMode ? '#2c2c2e' : '#fafafa',
71
+ bgAlt: darkMode ? '#1c1c1e' : '#ffffff',
72
+ border: darkMode ? '#3a3a3c' : '#e5e5e5',
73
+ green: darkMode ? '#30d158' : '#34c759',
74
+ yellow: darkMode ? '#ffd60a' : '#ffcc00',
75
+ blue: darkMode ? '#0a84ff' : '#007aff',
76
+ red: darkMode ? '#ff453a' : '#ff3b30',
77
+ };
78
+
79
+ const styles: Record<string, CSSProperties> = {
80
+ container: { width: '100%', maxWidth: '1000px' },
81
+ header: { marginBottom: '24px' },
82
+ title: {
83
+ fontSize: '28px',
84
+ fontWeight: 700,
85
+ color: colors.text,
86
+ marginBottom: '4px',
87
+ letterSpacing: '-0.02em',
88
+ },
89
+ subtitle: {
90
+ fontSize: '13px',
91
+ color: colors.textMuted,
92
+ display: 'flex',
93
+ alignItems: 'center',
94
+ gap: '12px',
95
+ },
96
+ refreshBadge: {
97
+ background: colors.green,
98
+ color: '#fff',
99
+ padding: '2px 8px',
100
+ borderRadius: '4px',
101
+ fontSize: '11px',
102
+ fontWeight: 600,
103
+ },
104
+ grid: {
105
+ display: 'grid',
106
+ gridTemplateColumns: '1fr 1fr',
107
+ gap: '16px',
108
+ marginBottom: '16px',
109
+ },
110
+ card: {
111
+ background: colors.bg,
112
+ border: `1px solid ${colors.border}`,
113
+ borderRadius: '12px',
114
+ padding: '16px',
115
+ },
116
+ cardTitle: {
117
+ fontSize: '12px',
118
+ fontWeight: 600,
119
+ color: colors.textMuted,
120
+ marginBottom: '12px',
121
+ textTransform: 'uppercase' as const,
122
+ letterSpacing: '0.03em',
123
+ },
124
+ countGrid: {
125
+ display: 'grid',
126
+ gridTemplateColumns: 'repeat(4, 1fr)',
127
+ gap: '8px',
128
+ },
129
+ countItem: {
130
+ textAlign: 'center' as const,
131
+ },
132
+ countValue: {
133
+ fontSize: '24px',
134
+ fontWeight: 700,
135
+ },
136
+ countLabel: {
137
+ fontSize: '11px',
138
+ color: colors.textMuted,
139
+ },
140
+ runnerList: {
141
+ listStyle: 'none',
142
+ padding: 0,
143
+ margin: 0,
144
+ },
145
+ runnerItem: {
146
+ display: 'flex',
147
+ justifyContent: 'space-between',
148
+ padding: '6px 0',
149
+ fontSize: '13px',
150
+ borderBottom: `1px solid ${colors.border}`,
151
+ },
152
+ noRunners: {
153
+ background: darkMode ? '#3c1618' : '#fef2f2',
154
+ border: `1px solid ${darkMode ? '#7f1d1d' : '#fecaca'}`,
155
+ borderRadius: '8px',
156
+ padding: '12px',
157
+ color: darkMode ? '#fca5a5' : '#991b1b',
158
+ fontSize: '13px',
159
+ },
160
+ hasRunners: {
161
+ background: darkMode ? '#14532d' : '#f0fdf4',
162
+ border: `1px solid ${darkMode ? '#166534' : '#bbf7d0'}`,
163
+ borderRadius: '8px',
164
+ padding: '12px',
165
+ },
166
+ warning: {
167
+ background: darkMode ? '#7f1d1d' : '#fef2f2',
168
+ border: `1px solid ${darkMode ? '#991b1b' : '#fecaca'}`,
169
+ borderRadius: '8px',
170
+ padding: '12px',
171
+ marginBottom: '16px',
172
+ color: darkMode ? '#fca5a5' : '#991b1b',
173
+ fontSize: '13px',
174
+ fontWeight: 500,
175
+ },
176
+ table: {
177
+ width: '100%',
178
+ borderCollapse: 'collapse' as const,
179
+ fontSize: '12px',
180
+ },
181
+ th: {
182
+ textAlign: 'left' as const,
183
+ padding: '8px 12px',
184
+ borderBottom: `1px solid ${colors.border}`,
185
+ color: colors.textMuted,
186
+ fontWeight: 600,
187
+ fontSize: '11px',
188
+ textTransform: 'uppercase' as const,
189
+ },
190
+ td: {
191
+ padding: '8px 12px',
192
+ borderBottom: `1px solid ${colors.border}`,
193
+ color: colors.text,
194
+ },
195
+ statusBadge: {
196
+ padding: '2px 8px',
197
+ borderRadius: '4px',
198
+ fontSize: '11px',
199
+ fontWeight: 600,
200
+ },
201
+ error: {
202
+ background: darkMode ? '#3c1618' : '#fef2f2',
203
+ border: `1px solid ${darkMode ? '#7f1d1d' : '#fecaca'}`,
204
+ borderRadius: '8px',
205
+ padding: '12px',
206
+ color: darkMode ? '#fca5a5' : '#991b1b',
207
+ fontSize: '13px',
208
+ marginBottom: '16px',
209
+ },
210
+ };
211
+
212
+ const loadStats = async () => {
213
+ try {
214
+ const data = await fetchStats();
215
+ setStats(data);
216
+ setLastUpdated(new Date());
217
+ setError(null);
218
+ } catch (err) {
219
+ const errorMessage = err instanceof Error ? err.message : 'Failed to load stats';
220
+ setError({
221
+ message: errorMessage,
222
+ status: undefined,
223
+ statusText: undefined,
224
+ data: undefined,
225
+ });
226
+ }
227
+ };
228
+
229
+ useEffect(() => {
230
+ loadStats();
231
+ pollingRef.current = window.setInterval(loadStats, 2000);
232
+ return () => {
233
+ if (pollingRef.current) clearInterval(pollingRef.current);
234
+ };
235
+ }, []);
236
+
237
+ const getStatusColor = (status: string) => {
238
+ switch (status) {
239
+ case 'pending': return { bg: colors.yellow, text: '#000' };
240
+ case 'claimed': return { bg: colors.blue, text: '#fff' };
241
+ case 'completed': return { bg: colors.green, text: '#fff' };
242
+ case 'failed': return { bg: colors.red, text: '#fff' };
243
+ default: return { bg: colors.border, text: colors.text };
244
+ }
245
+ };
246
+
247
+ return (
248
+ <div style={styles.container}>
249
+ <div style={styles.header}>
250
+ <h1 style={styles.title}>Runner</h1>
251
+ <div style={styles.subtitle}>
252
+ <span>Task queue observability</span>
253
+ <span style={styles.refreshBadge}>Auto-refresh: 2s</span>
254
+ {lastUpdated && <span>Updated: {lastUpdated.toLocaleTimeString()}</span>}
255
+ </div>
256
+ </div>
257
+
258
+ {error && (
259
+ <div style={styles.error}>
260
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
261
+ <div>
262
+ <strong>Error:</strong> {error.message}
263
+ {error.status && (
264
+ <span style={{ marginLeft: '8px', opacity: 0.8 }}>
265
+ (HTTP {error.status}{error.statusText ? `: ${error.statusText}` : ''})
266
+ </span>
267
+ )}
268
+ </div>
269
+ {error.data !== undefined && error.data !== null && (
270
+ <button
271
+ onClick={() => setShowErrorDetails(!showErrorDetails)}
272
+ style={{
273
+ background: 'transparent',
274
+ border: 'none',
275
+ color: 'inherit',
276
+ cursor: 'pointer',
277
+ fontSize: '12px',
278
+ textDecoration: 'underline',
279
+ }}
280
+ >
281
+ {showErrorDetails ? 'Hide details' : 'Show details'}
282
+ </button>
283
+ )}
284
+ </div>
285
+ {showErrorDetails && error.data !== undefined && error.data !== null && (
286
+ <pre style={{
287
+ marginTop: '8px',
288
+ padding: '8px',
289
+ background: darkMode ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0.05)',
290
+ borderRadius: '4px',
291
+ fontSize: '11px',
292
+ overflow: 'auto',
293
+ maxHeight: '200px',
294
+ }}>
295
+ {JSON.stringify(error.data, null, 2)}
296
+ </pre>
297
+ )}
298
+ </div>
299
+ )}
300
+
301
+ {stats && (
302
+ <>
303
+ {/* Warning if pending but no runners */}
304
+ {stats.counts.pending > 0 && stats.active_runners.length === 0 && (
305
+ <div style={styles.warning}>
306
+ {stats.counts.pending} task(s) pending but no active runners detected.
307
+ Workflows will be stuck until a runner starts polling.
308
+ </div>
309
+ )}
310
+
311
+ <div style={styles.grid}>
312
+ {/* Active Runners */}
313
+ <div style={styles.card}>
314
+ <div style={styles.cardTitle}>Active Runners</div>
315
+ {stats.active_runners.length === 0 ? (
316
+ <div style={styles.noRunners}>
317
+ No active runners detected (last 10m)
318
+ </div>
319
+ ) : (
320
+ <div style={styles.hasRunners}>
321
+ <ul style={styles.runnerList}>
322
+ {stats.active_runners.map((r) => (
323
+ <li key={r.runner_id} style={styles.runnerItem}>
324
+ <span style={{ fontFamily: 'monospace' }}>{shortId(r.runner_id)}</span>
325
+ <span style={{ color: colors.textMuted }}>
326
+ last claimed {timeAgo(r.last_claimed_at)}
327
+ </span>
328
+ </li>
329
+ ))}
330
+ </ul>
331
+ </div>
332
+ )}
333
+ </div>
334
+
335
+ {/* Queue Counts */}
336
+ <div style={styles.card}>
337
+ <div style={styles.cardTitle}>Queue Counts</div>
338
+ <div style={styles.countGrid}>
339
+ <div style={styles.countItem}>
340
+ <div style={{ ...styles.countValue, color: stats.counts.pending > 0 ? colors.yellow : colors.textMuted }}>
341
+ {stats.counts.pending}
342
+ </div>
343
+ <div style={styles.countLabel}>Pending</div>
344
+ </div>
345
+ <div style={styles.countItem}>
346
+ <div style={{ ...styles.countValue, color: stats.counts.claimed > 0 ? colors.blue : colors.textMuted }}>
347
+ {stats.counts.claimed}
348
+ </div>
349
+ <div style={styles.countLabel}>Claimed</div>
350
+ </div>
351
+ <div style={styles.countItem}>
352
+ <div style={{ ...styles.countValue, color: stats.counts.completed_5m > 0 ? colors.green : colors.textMuted }}>
353
+ {stats.counts.completed_5m}
354
+ </div>
355
+ <div style={styles.countLabel}>Completed (5m)</div>
356
+ </div>
357
+ <div style={styles.countItem}>
358
+ <div style={{ ...styles.countValue, color: stats.counts.failed_5m > 0 ? colors.red : colors.textMuted }}>
359
+ {stats.counts.failed_5m}
360
+ </div>
361
+ <div style={styles.countLabel}>Failed (5m)</div>
362
+ </div>
363
+ </div>
364
+ </div>
365
+ </div>
366
+
367
+ {/* By Subtype */}
368
+ {Object.keys(stats.by_subtype).length > 0 && (
369
+ <div style={{ ...styles.card, marginBottom: '16px' }}>
370
+ <div style={styles.cardTitle}>By Subtype</div>
371
+ <table style={styles.table}>
372
+ <thead>
373
+ <tr>
374
+ <th style={styles.th}>Subtype</th>
375
+ <th style={{ ...styles.th, textAlign: 'right' }}>Pending</th>
376
+ <th style={{ ...styles.th, textAlign: 'right' }}>Claimed</th>
377
+ </tr>
378
+ </thead>
379
+ <tbody>
380
+ {Object.entries(stats.by_subtype).map(([subtype, data]) => (
381
+ <tr key={subtype}>
382
+ <td style={{ ...styles.td, fontFamily: 'monospace' }}>{subtype}</td>
383
+ <td style={{ ...styles.td, textAlign: 'right', color: data.pending > 0 ? colors.yellow : colors.textMuted }}>
384
+ {data.pending}
385
+ </td>
386
+ <td style={{ ...styles.td, textAlign: 'right', color: data.claimed > 0 ? colors.blue : colors.textMuted }}>
387
+ {data.claimed}
388
+ </td>
389
+ </tr>
390
+ ))}
391
+ </tbody>
392
+ </table>
393
+ </div>
394
+ )}
395
+
396
+ {/* Recent Tasks */}
397
+ <div style={styles.card}>
398
+ <div style={styles.cardTitle}>Recent Tasks (25)</div>
399
+ {stats.recent.length === 0 ? (
400
+ <div style={{ color: colors.textMuted, fontSize: '13px' }}>No tasks found</div>
401
+ ) : (
402
+ <table style={styles.table}>
403
+ <thead>
404
+ <tr>
405
+ <th style={styles.th}>Task ID</th>
406
+ <th style={styles.th}>Subtype</th>
407
+ <th style={styles.th}>Status</th>
408
+ <th style={styles.th}>Runner</th>
409
+ <th style={styles.th}>Created</th>
410
+ <th style={styles.th}>Claimed</th>
411
+ </tr>
412
+ </thead>
413
+ <tbody>
414
+ {stats.recent.map((t) => {
415
+ const statusColor = getStatusColor(t.status);
416
+ return (
417
+ <tr key={t.task_id}>
418
+ <td style={{ ...styles.td, fontFamily: 'monospace' }}>{shortId(t.task_id)}</td>
419
+ <td style={{ ...styles.td, fontFamily: 'monospace' }}>{t.task_subtype || 'default'}</td>
420
+ <td style={styles.td}>
421
+ <span style={{ ...styles.statusBadge, background: statusColor.bg, color: statusColor.text }}>
422
+ {t.status}
423
+ </span>
424
+ </td>
425
+ <td style={{ ...styles.td, fontFamily: 'monospace' }}>{shortId(t.runner_id)}</td>
426
+ <td style={{ ...styles.td, color: colors.textMuted }}>{timeAgo(t.created_at)}</td>
427
+ <td style={{ ...styles.td, color: colors.textMuted }}>{t.claimed_at ? timeAgo(t.claimed_at) : '-'}</td>
428
+ </tr>
429
+ );
430
+ })}
431
+ </tbody>
432
+ </table>
433
+ )}
434
+ </div>
435
+ </>
436
+ )}
437
+
438
+ {!stats && !error && (
439
+ <div style={{ color: colors.textMuted, textAlign: 'center', padding: '40px' }}>
440
+ Loading stats...
441
+ </div>
442
+ )}
443
+ </div>
444
+ );
445
+ }