beercan 0.1.0 → 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 (97) hide show
  1. package/README.md +96 -2
  2. package/dist/api/handlers/bloops.d.ts +4 -0
  3. package/dist/api/handlers/bloops.d.ts.map +1 -0
  4. package/dist/api/handlers/bloops.js +96 -0
  5. package/dist/api/handlers/bloops.js.map +1 -0
  6. package/dist/api/handlers/jobs.d.ts +4 -0
  7. package/dist/api/handlers/jobs.d.ts.map +1 -0
  8. package/dist/api/handlers/jobs.js +34 -0
  9. package/dist/api/handlers/jobs.js.map +1 -0
  10. package/dist/api/handlers/projects.d.ts +4 -0
  11. package/dist/api/handlers/projects.d.ts.map +1 -0
  12. package/dist/api/handlers/projects.js +75 -0
  13. package/dist/api/handlers/projects.js.map +1 -0
  14. package/dist/api/handlers/schedules.d.ts +4 -0
  15. package/dist/api/handlers/schedules.d.ts.map +1 -0
  16. package/dist/api/handlers/schedules.js +19 -0
  17. package/dist/api/handlers/schedules.js.map +1 -0
  18. package/dist/api/handlers/status.d.ts +4 -0
  19. package/dist/api/handlers/status.d.ts.map +1 -0
  20. package/dist/api/handlers/status.js +21 -0
  21. package/dist/api/handlers/status.js.map +1 -0
  22. package/dist/api/index.d.ts +8 -0
  23. package/dist/api/index.d.ts.map +1 -0
  24. package/dist/api/index.js +17 -0
  25. package/dist/api/index.js.map +1 -0
  26. package/dist/api/utils.d.ts +5 -0
  27. package/dist/api/utils.d.ts.map +1 -0
  28. package/dist/api/utils.js +24 -0
  29. package/dist/api/utils.js.map +1 -0
  30. package/dist/chat/formatter.d.ts +43 -0
  31. package/dist/chat/formatter.d.ts.map +1 -0
  32. package/dist/chat/formatter.js +144 -0
  33. package/dist/chat/formatter.js.map +1 -0
  34. package/dist/chat/index.d.ts +33 -0
  35. package/dist/chat/index.d.ts.map +1 -0
  36. package/dist/chat/index.js +253 -0
  37. package/dist/chat/index.js.map +1 -0
  38. package/dist/chat/intent.d.ts +12 -0
  39. package/dist/chat/intent.d.ts.map +1 -0
  40. package/dist/chat/intent.js +256 -0
  41. package/dist/chat/intent.js.map +1 -0
  42. package/dist/chat/providers/slack.d.ts +17 -0
  43. package/dist/chat/providers/slack.d.ts.map +1 -0
  44. package/dist/chat/providers/slack.js +90 -0
  45. package/dist/chat/providers/slack.js.map +1 -0
  46. package/dist/chat/providers/telegram.d.ts +15 -0
  47. package/dist/chat/providers/telegram.d.ts.map +1 -0
  48. package/dist/chat/providers/telegram.js +76 -0
  49. package/dist/chat/providers/telegram.js.map +1 -0
  50. package/dist/chat/providers/terminal.d.ts +14 -0
  51. package/dist/chat/providers/terminal.d.ts.map +1 -0
  52. package/dist/chat/providers/terminal.js +77 -0
  53. package/dist/chat/providers/terminal.js.map +1 -0
  54. package/dist/chat/providers/websocket.d.ts +16 -0
  55. package/dist/chat/providers/websocket.d.ts.map +1 -0
  56. package/dist/chat/providers/websocket.js +125 -0
  57. package/dist/chat/providers/websocket.js.map +1 -0
  58. package/dist/chat/skippy.d.ts +3 -0
  59. package/dist/chat/skippy.d.ts.map +1 -0
  60. package/dist/chat/skippy.js +47 -0
  61. package/dist/chat/skippy.js.map +1 -0
  62. package/dist/chat/types.d.ts +50 -0
  63. package/dist/chat/types.d.ts.map +1 -0
  64. package/dist/chat/types.js +2 -0
  65. package/dist/chat/types.js.map +1 -0
  66. package/dist/cli.js +112 -7
  67. package/dist/cli.js.map +1 -1
  68. package/dist/config.d.ts +9 -0
  69. package/dist/config.d.ts.map +1 -1
  70. package/dist/config.js +8 -0
  71. package/dist/config.js.map +1 -1
  72. package/dist/core/job-queue.d.ts +9 -1
  73. package/dist/core/job-queue.d.ts.map +1 -1
  74. package/dist/core/job-queue.js +33 -0
  75. package/dist/core/job-queue.js.map +1 -1
  76. package/dist/core/runner.d.ts +2 -0
  77. package/dist/core/runner.d.ts.map +1 -1
  78. package/dist/core/runner.js +37 -7
  79. package/dist/core/runner.js.map +1 -1
  80. package/dist/events/daemon.d.ts +1 -1
  81. package/dist/events/daemon.d.ts.map +1 -1
  82. package/dist/events/daemon.js +35 -1
  83. package/dist/events/daemon.js.map +1 -1
  84. package/dist/events/sources/webhook-source.d.ts +2 -1
  85. package/dist/events/sources/webhook-source.d.ts.map +1 -1
  86. package/dist/events/sources/webhook-source.js +77 -5
  87. package/dist/events/sources/webhook-source.js.map +1 -1
  88. package/dist/index.d.ts +54 -0
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +64 -2
  91. package/dist/index.js.map +1 -1
  92. package/dist/storage/database.d.ts +20 -0
  93. package/dist/storage/database.d.ts.map +1 -1
  94. package/dist/storage/database.js +46 -0
  95. package/dist/storage/database.js.map +1 -1
  96. package/package.json +14 -4
  97. package/src/dashboard/index.html +1092 -0
@@ -0,0 +1,1092 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>BeerCan — System Status</title>
7
+ <meta name="description" content="Live system status dashboard for your BeerCan autonomous agent instance.">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
11
+ <style>
12
+ :root {
13
+ --gold: #F59E0B;
14
+ --gold-light: #FBBF24;
15
+ --gold-dark: #D97706;
16
+ --amber: #F97316;
17
+ --bg-dark: #0A0A0F;
18
+ --bg-card: #12121A;
19
+ --bg-card-hover: #1A1A25;
20
+ --text-primary: #F1F1F6;
21
+ --text-secondary: #9494A8;
22
+ --text-dim: #5A5A72;
23
+ --accent-blue: #3B82F6;
24
+ --accent-green: #10B981;
25
+ --accent-red: #EF4444;
26
+ --border: #2A2A3A;
27
+ }
28
+
29
+ * { margin: 0; padding: 0; box-sizing: border-box; }
30
+
31
+ html { scroll-behavior: smooth; }
32
+
33
+ body {
34
+ font-family: 'Space Grotesk', system-ui, sans-serif;
35
+ background: var(--bg-dark);
36
+ color: var(--text-primary);
37
+ line-height: 1.6;
38
+ overflow-x: hidden;
39
+ }
40
+
41
+ code, .mono { font-family: 'JetBrains Mono', monospace; }
42
+
43
+ /* ── Animated background ─────────────────────────── */
44
+ .bg-grid {
45
+ position: fixed;
46
+ inset: 0;
47
+ z-index: 0;
48
+ background-image:
49
+ linear-gradient(rgba(245, 158, 11, 0.03) 1px, transparent 1px),
50
+ linear-gradient(90deg, rgba(245, 158, 11, 0.03) 1px, transparent 1px);
51
+ background-size: 60px 60px;
52
+ pointer-events: none;
53
+ }
54
+
55
+ .bg-glow {
56
+ position: fixed;
57
+ top: -200px;
58
+ left: 50%;
59
+ transform: translateX(-50%);
60
+ width: 800px;
61
+ height: 600px;
62
+ background: radial-gradient(ellipse, rgba(245, 158, 11, 0.08) 0%, transparent 70%);
63
+ pointer-events: none;
64
+ z-index: 0;
65
+ }
66
+
67
+ /* ── Nav ──────────────────────────────────────────── */
68
+ nav {
69
+ position: fixed;
70
+ top: 0;
71
+ left: 0;
72
+ right: 0;
73
+ z-index: 100;
74
+ padding: 1rem 2rem;
75
+ display: flex;
76
+ justify-content: space-between;
77
+ align-items: center;
78
+ backdrop-filter: blur(20px);
79
+ background: rgba(10, 10, 15, 0.8);
80
+ border-bottom: 1px solid var(--border);
81
+ }
82
+
83
+ .nav-logo {
84
+ font-size: 1.4rem;
85
+ font-weight: 700;
86
+ color: var(--gold);
87
+ text-decoration: none;
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 0.5rem;
91
+ }
92
+
93
+ .nav-logo .can-icon { font-size: 1.6rem; }
94
+
95
+ .nav-links { display: flex; gap: 2rem; align-items: center; }
96
+ .nav-links a {
97
+ color: var(--text-secondary);
98
+ text-decoration: none;
99
+ font-size: 0.9rem;
100
+ font-weight: 500;
101
+ transition: color 0.2s;
102
+ }
103
+ .nav-links a:hover { color: var(--gold-light); }
104
+ .nav-links a.active { color: var(--gold); }
105
+
106
+ .btn-github {
107
+ display: inline-flex;
108
+ align-items: center;
109
+ gap: 0.5rem;
110
+ padding: 0.5rem 1rem;
111
+ background: var(--bg-card);
112
+ border: 1px solid var(--border);
113
+ border-radius: 8px;
114
+ color: var(--text-primary);
115
+ text-decoration: none;
116
+ font-size: 0.85rem;
117
+ font-weight: 500;
118
+ transition: all 0.2s;
119
+ }
120
+ .btn-github:hover { border-color: var(--gold); background: var(--bg-card-hover); }
121
+
122
+ /* ── Sections ─────────────────────────────────────── */
123
+ section { position: relative; z-index: 1; }
124
+
125
+ .container {
126
+ max-width: 1100px;
127
+ margin: 0 auto;
128
+ padding: 0 2rem;
129
+ }
130
+
131
+ .section-label {
132
+ font-size: 0.8rem;
133
+ font-weight: 600;
134
+ letter-spacing: 0.1em;
135
+ text-transform: uppercase;
136
+ color: var(--gold);
137
+ margin-bottom: 0.75rem;
138
+ }
139
+
140
+ .section-title {
141
+ font-size: clamp(2rem, 4vw, 2.8rem);
142
+ font-weight: 700;
143
+ margin-bottom: 1rem;
144
+ letter-spacing: -0.02em;
145
+ }
146
+
147
+ .section-desc {
148
+ color: var(--text-secondary);
149
+ max-width: 580px;
150
+ font-size: 1.05rem;
151
+ }
152
+
153
+ /* ── Config Bar ──────────────────────────────────── */
154
+ .config-bar {
155
+ position: relative;
156
+ z-index: 1;
157
+ margin-top: calc(60px + 1rem);
158
+ padding: 1rem 0;
159
+ }
160
+
161
+ .config-bar-inner {
162
+ display: flex;
163
+ align-items: center;
164
+ gap: 1rem;
165
+ padding: 0.75rem 1.25rem;
166
+ background: var(--bg-card);
167
+ border: 1px solid var(--border);
168
+ border-radius: 10px;
169
+ flex-wrap: wrap;
170
+ }
171
+
172
+ .config-bar-inner label {
173
+ font-size: 0.8rem;
174
+ font-weight: 600;
175
+ color: var(--text-secondary);
176
+ white-space: nowrap;
177
+ }
178
+
179
+ .config-bar-inner input {
180
+ flex: 1;
181
+ min-width: 200px;
182
+ padding: 0.4rem 0.75rem;
183
+ background: var(--bg-dark);
184
+ border: 1px solid var(--border);
185
+ border-radius: 6px;
186
+ color: var(--gold-light);
187
+ font-family: 'JetBrains Mono', monospace;
188
+ font-size: 0.82rem;
189
+ outline: none;
190
+ transition: border-color 0.2s;
191
+ }
192
+
193
+ .config-bar-inner input:focus {
194
+ border-color: var(--gold);
195
+ }
196
+
197
+ .connection-status {
198
+ display: flex;
199
+ align-items: center;
200
+ gap: 0.4rem;
201
+ font-size: 0.8rem;
202
+ font-weight: 500;
203
+ white-space: nowrap;
204
+ }
205
+
206
+ .connection-dot {
207
+ width: 8px;
208
+ height: 8px;
209
+ border-radius: 50%;
210
+ background: var(--text-dim);
211
+ transition: background 0.3s;
212
+ }
213
+
214
+ .connection-dot.connected { background: var(--accent-green); box-shadow: 0 0 6px rgba(16, 185, 129, 0.5); }
215
+ .connection-dot.disconnected { background: var(--accent-red); box-shadow: 0 0 6px rgba(239, 68, 68, 0.5); }
216
+
217
+ .connection-label { color: var(--text-secondary); }
218
+ .connection-label.connected { color: var(--accent-green); }
219
+ .connection-label.disconnected { color: var(--accent-red); }
220
+
221
+ .auto-refresh-badge {
222
+ font-size: 0.7rem;
223
+ color: var(--text-dim);
224
+ padding: 0.25rem 0.6rem;
225
+ border: 1px solid var(--border);
226
+ border-radius: 100px;
227
+ white-space: nowrap;
228
+ }
229
+
230
+ /* ── System Overview ─────────────────────────────── */
231
+ .system-overview {
232
+ padding: 3rem 0 2rem;
233
+ }
234
+
235
+ .stat-grid {
236
+ display: grid;
237
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
238
+ gap: 1rem;
239
+ margin-top: 2rem;
240
+ }
241
+
242
+ .stat-card {
243
+ background: var(--bg-card);
244
+ border: 1px solid var(--border);
245
+ border-radius: 14px;
246
+ padding: 1.5rem;
247
+ transition: all 0.3s;
248
+ }
249
+
250
+ .stat-card:hover {
251
+ border-color: rgba(245, 158, 11, 0.3);
252
+ transform: translateY(-4px);
253
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
254
+ }
255
+
256
+ .stat-card-label {
257
+ font-size: 0.8rem;
258
+ font-weight: 600;
259
+ color: var(--text-secondary);
260
+ text-transform: uppercase;
261
+ letter-spacing: 0.05em;
262
+ margin-bottom: 0.5rem;
263
+ }
264
+
265
+ .stat-card-value {
266
+ font-size: 2.2rem;
267
+ font-weight: 700;
268
+ line-height: 1.2;
269
+ }
270
+
271
+ .stat-card-sub {
272
+ font-size: 0.8rem;
273
+ color: var(--text-dim);
274
+ margin-top: 0.25rem;
275
+ }
276
+
277
+ .stat-card-value.pulse {
278
+ animation: statPulse 2s ease-in-out infinite;
279
+ }
280
+
281
+ @keyframes statPulse {
282
+ 0%, 100% { opacity: 1; }
283
+ 50% { opacity: 0.6; }
284
+ }
285
+
286
+ /* ── Projects Section ────────────────────────────── */
287
+ .projects-section {
288
+ padding: 3rem 0;
289
+ }
290
+
291
+ .project-grid {
292
+ display: grid;
293
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
294
+ gap: 1.5rem;
295
+ margin-top: 2rem;
296
+ }
297
+
298
+ .project-card {
299
+ background: var(--bg-card);
300
+ border: 1px solid var(--border);
301
+ border-radius: 14px;
302
+ padding: 1.5rem;
303
+ transition: all 0.3s;
304
+ }
305
+
306
+ .project-card:hover {
307
+ border-color: rgba(245, 158, 11, 0.3);
308
+ transform: translateY(-4px);
309
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
310
+ }
311
+
312
+ .project-name {
313
+ font-size: 1.15rem;
314
+ font-weight: 600;
315
+ margin-bottom: 0.2rem;
316
+ }
317
+
318
+ .project-slug {
319
+ font-family: 'JetBrains Mono', monospace;
320
+ font-size: 0.78rem;
321
+ color: var(--text-dim);
322
+ margin-bottom: 0.75rem;
323
+ }
324
+
325
+ .project-workdir {
326
+ font-family: 'JetBrains Mono', monospace;
327
+ font-size: 0.75rem;
328
+ color: var(--text-secondary);
329
+ background: var(--bg-dark);
330
+ padding: 0.3rem 0.6rem;
331
+ border-radius: 4px;
332
+ margin-bottom: 0.75rem;
333
+ overflow: hidden;
334
+ text-overflow: ellipsis;
335
+ white-space: nowrap;
336
+ }
337
+
338
+ .project-stats {
339
+ display: flex;
340
+ gap: 0.75rem;
341
+ flex-wrap: wrap;
342
+ margin-bottom: 0.5rem;
343
+ }
344
+
345
+ .project-stat {
346
+ font-size: 0.8rem;
347
+ font-weight: 500;
348
+ display: flex;
349
+ align-items: center;
350
+ gap: 0.3rem;
351
+ }
352
+
353
+ .project-stat .dot {
354
+ width: 6px;
355
+ height: 6px;
356
+ border-radius: 50%;
357
+ }
358
+
359
+ .project-stat .dot.green { background: var(--accent-green); }
360
+ .project-stat .dot.red { background: var(--accent-red); }
361
+ .project-stat .dot.blue { background: var(--accent-blue); }
362
+
363
+ .project-stat.green { color: var(--accent-green); }
364
+ .project-stat.red { color: var(--accent-red); }
365
+ .project-stat.blue { color: var(--accent-blue); }
366
+
367
+ .project-tokens {
368
+ font-size: 0.78rem;
369
+ color: var(--text-dim);
370
+ font-family: 'JetBrains Mono', monospace;
371
+ }
372
+
373
+ /* ── Job Queue Section ───────────────────────────── */
374
+ .jobs-section {
375
+ padding: 3rem 0;
376
+ }
377
+
378
+ .jobs-table-wrap {
379
+ margin-top: 2rem;
380
+ background: var(--bg-card);
381
+ border: 1px solid var(--border);
382
+ border-radius: 14px;
383
+ overflow: hidden;
384
+ }
385
+
386
+ .jobs-table {
387
+ width: 100%;
388
+ border-collapse: collapse;
389
+ }
390
+
391
+ .jobs-table th {
392
+ text-align: left;
393
+ padding: 0.85rem 1.25rem;
394
+ font-size: 0.75rem;
395
+ font-weight: 600;
396
+ color: var(--text-dim);
397
+ text-transform: uppercase;
398
+ letter-spacing: 0.05em;
399
+ border-bottom: 1px solid var(--border);
400
+ background: rgba(18, 18, 26, 0.8);
401
+ }
402
+
403
+ .jobs-table td {
404
+ padding: 0.75rem 1.25rem;
405
+ font-size: 0.85rem;
406
+ color: var(--text-secondary);
407
+ border-bottom: 1px solid rgba(42, 42, 58, 0.4);
408
+ vertical-align: middle;
409
+ }
410
+
411
+ .jobs-table tr:last-child td { border-bottom: none; }
412
+
413
+ .jobs-table tr:hover td { background: rgba(26, 26, 37, 0.5); }
414
+
415
+ .status-badge {
416
+ display: inline-block;
417
+ padding: 0.2rem 0.6rem;
418
+ border-radius: 100px;
419
+ font-size: 0.72rem;
420
+ font-weight: 600;
421
+ text-transform: uppercase;
422
+ letter-spacing: 0.03em;
423
+ }
424
+
425
+ .status-badge.pending {
426
+ background: rgba(245, 158, 11, 0.15);
427
+ color: var(--gold-light);
428
+ border: 1px solid rgba(245, 158, 11, 0.3);
429
+ }
430
+
431
+ .status-badge.running {
432
+ background: rgba(59, 130, 246, 0.15);
433
+ color: #60A5FA;
434
+ border: 1px solid rgba(59, 130, 246, 0.3);
435
+ }
436
+
437
+ .status-badge.completed {
438
+ background: rgba(16, 185, 129, 0.15);
439
+ color: #34D399;
440
+ border: 1px solid rgba(16, 185, 129, 0.3);
441
+ }
442
+
443
+ .status-badge.failed {
444
+ background: rgba(239, 68, 68, 0.15);
445
+ color: #F87171;
446
+ border: 1px solid rgba(239, 68, 68, 0.3);
447
+ }
448
+
449
+ .goal-text {
450
+ max-width: 260px;
451
+ overflow: hidden;
452
+ text-overflow: ellipsis;
453
+ white-space: nowrap;
454
+ color: var(--text-primary);
455
+ }
456
+
457
+ .source-badge {
458
+ font-family: 'JetBrains Mono', monospace;
459
+ font-size: 0.72rem;
460
+ color: var(--text-dim);
461
+ }
462
+
463
+ .timestamp {
464
+ font-family: 'JetBrains Mono', monospace;
465
+ font-size: 0.72rem;
466
+ color: var(--text-dim);
467
+ white-space: nowrap;
468
+ }
469
+
470
+ /* ── Active Bloops Section ───────────────────────── */
471
+ .active-bloops-section {
472
+ padding: 3rem 0;
473
+ }
474
+
475
+ .active-bloops-section.hidden { display: none; }
476
+
477
+ .active-bloop-grid {
478
+ display: grid;
479
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
480
+ gap: 1.5rem;
481
+ margin-top: 2rem;
482
+ }
483
+
484
+ .active-bloop-card {
485
+ background: var(--bg-card);
486
+ border: 1px solid var(--gold-dark);
487
+ border-radius: 14px;
488
+ padding: 1.5rem;
489
+ animation: bloopPulse 3s ease-in-out infinite;
490
+ transition: transform 0.3s, box-shadow 0.3s;
491
+ }
492
+
493
+ .active-bloop-card:hover {
494
+ transform: translateY(-4px);
495
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3);
496
+ }
497
+
498
+ @keyframes bloopPulse {
499
+ 0%, 100% { border-color: var(--gold-dark); box-shadow: 0 0 0 rgba(245, 158, 11, 0); }
500
+ 50% { border-color: var(--gold); box-shadow: 0 0 20px rgba(245, 158, 11, 0.1); }
501
+ }
502
+
503
+ .active-bloop-goal {
504
+ font-size: 1rem;
505
+ font-weight: 600;
506
+ margin-bottom: 0.5rem;
507
+ line-height: 1.4;
508
+ }
509
+
510
+ .active-bloop-project {
511
+ font-family: 'JetBrains Mono', monospace;
512
+ font-size: 0.78rem;
513
+ color: var(--accent-blue);
514
+ margin-bottom: 0.75rem;
515
+ }
516
+
517
+ .active-bloop-meta {
518
+ display: flex;
519
+ gap: 1rem;
520
+ flex-wrap: wrap;
521
+ }
522
+
523
+ .active-bloop-meta span {
524
+ font-size: 0.8rem;
525
+ color: var(--text-secondary);
526
+ }
527
+
528
+ .active-bloop-meta span strong {
529
+ color: var(--text-primary);
530
+ font-weight: 600;
531
+ }
532
+
533
+ /* ── Recent History Section ──────────────────────── */
534
+ .recent-section {
535
+ padding: 3rem 0 4rem;
536
+ }
537
+
538
+ .history-list {
539
+ margin-top: 2rem;
540
+ display: flex;
541
+ flex-direction: column;
542
+ gap: 0.5rem;
543
+ }
544
+
545
+ .history-item {
546
+ display: flex;
547
+ align-items: center;
548
+ gap: 1rem;
549
+ padding: 0.75rem 1.25rem;
550
+ background: var(--bg-card);
551
+ border: 1px solid var(--border);
552
+ border-radius: 10px;
553
+ transition: all 0.2s;
554
+ }
555
+
556
+ .history-item:hover {
557
+ border-color: rgba(245, 158, 11, 0.3);
558
+ background: var(--bg-card-hover);
559
+ }
560
+
561
+ .history-goal {
562
+ flex: 1;
563
+ font-size: 0.88rem;
564
+ overflow: hidden;
565
+ text-overflow: ellipsis;
566
+ white-space: nowrap;
567
+ color: var(--text-primary);
568
+ min-width: 0;
569
+ }
570
+
571
+ .history-project {
572
+ font-family: 'JetBrains Mono', monospace;
573
+ font-size: 0.72rem;
574
+ color: var(--text-dim);
575
+ white-space: nowrap;
576
+ }
577
+
578
+ .history-tokens {
579
+ font-family: 'JetBrains Mono', monospace;
580
+ font-size: 0.72rem;
581
+ color: var(--text-secondary);
582
+ white-space: nowrap;
583
+ }
584
+
585
+ .history-duration {
586
+ font-family: 'JetBrains Mono', monospace;
587
+ font-size: 0.72rem;
588
+ color: var(--text-dim);
589
+ white-space: nowrap;
590
+ }
591
+
592
+ /* ── Empty States ────────────────────────────────── */
593
+ .empty-state {
594
+ text-align: center;
595
+ padding: 3rem 2rem;
596
+ color: var(--text-dim);
597
+ font-size: 0.92rem;
598
+ font-style: italic;
599
+ }
600
+
601
+ /* ── Footer ──────────────────────────────────────── */
602
+ footer {
603
+ padding: 2rem;
604
+ text-align: center;
605
+ border-top: 1px solid var(--border);
606
+ color: var(--text-dim);
607
+ font-size: 0.8rem;
608
+ }
609
+
610
+ footer a { color: var(--text-secondary); text-decoration: none; }
611
+ footer a:hover { color: var(--gold); }
612
+
613
+ /* ── Responsive ──────────────────────────────────── */
614
+ @media (max-width: 768px) {
615
+ nav { padding: 0.75rem 1rem; }
616
+ .nav-links { gap: 1rem; }
617
+ .nav-links a:not(.btn-github):not(.active) { display: none; }
618
+ .config-bar-inner { flex-direction: column; align-items: stretch; }
619
+ .stat-grid { grid-template-columns: repeat(2, 1fr); }
620
+ .project-grid { grid-template-columns: 1fr; }
621
+ .active-bloop-grid { grid-template-columns: 1fr; }
622
+
623
+ /* Stack table on mobile */
624
+ .jobs-table thead { display: none; }
625
+ .jobs-table, .jobs-table tbody, .jobs-table tr, .jobs-table td {
626
+ display: block;
627
+ width: 100%;
628
+ }
629
+ .jobs-table tr {
630
+ padding: 1rem 1.25rem;
631
+ border-bottom: 1px solid rgba(42, 42, 58, 0.4);
632
+ }
633
+ .jobs-table td {
634
+ padding: 0.2rem 0;
635
+ border-bottom: none;
636
+ }
637
+ .jobs-table td::before {
638
+ content: attr(data-label);
639
+ font-size: 0.7rem;
640
+ font-weight: 600;
641
+ color: var(--text-dim);
642
+ text-transform: uppercase;
643
+ letter-spacing: 0.05em;
644
+ display: block;
645
+ margin-bottom: 0.15rem;
646
+ }
647
+ .goal-text { max-width: 100%; white-space: normal; }
648
+
649
+ .history-item {
650
+ flex-wrap: wrap;
651
+ gap: 0.5rem;
652
+ }
653
+ .history-goal { flex-basis: 100%; }
654
+ }
655
+
656
+ @media (max-width: 480px) {
657
+ .stat-grid { grid-template-columns: 1fr; }
658
+ }
659
+
660
+ /* ── Animations ──────────────────────────────────── */
661
+ .fade-in {
662
+ opacity: 0;
663
+ transform: translateY(20px);
664
+ transition: opacity 0.6s ease, transform 0.6s ease;
665
+ }
666
+ .fade-in.visible {
667
+ opacity: 1;
668
+ transform: translateY(0);
669
+ }
670
+ </style>
671
+ </head>
672
+ <body>
673
+
674
+ <div class="bg-grid"></div>
675
+ <div class="bg-glow"></div>
676
+
677
+ <!-- ── Nav ──────────────────────────────────────────────── -->
678
+ <nav>
679
+ <a href="index.html" class="nav-logo">
680
+ <span class="can-icon">🍺</span>
681
+ <span>BeerCan</span>
682
+ </a>
683
+ <div class="nav-links">
684
+ <a href="index.html">Home</a>
685
+ <a href="status.html" class="active">Status</a>
686
+ <a href="https://github.com/ArchieGoodwin/beercan" class="btn-github">
687
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
688
+ GitHub
689
+ </a>
690
+ </div>
691
+ </nav>
692
+
693
+ <!-- ── Config Bar ──────────────────────────────────────── -->
694
+ <section class="config-bar">
695
+ <div class="container">
696
+ <div class="config-bar-inner">
697
+ <label for="api-url">API URL</label>
698
+ <input type="text" id="api-url" placeholder="http://localhost:3939">
699
+ <div class="connection-status">
700
+ <div class="connection-dot" id="connection-dot"></div>
701
+ <span class="connection-label" id="connection-label">Connecting...</span>
702
+ </div>
703
+ <span class="auto-refresh-badge">Auto-refresh: 10s</span>
704
+ </div>
705
+ </div>
706
+ </section>
707
+
708
+ <!-- ── System Overview ─────────────────────────────────── -->
709
+ <section class="system-overview">
710
+ <div class="container">
711
+ <div class="section-label">System Overview</div>
712
+ <h2 class="section-title">Dashboard</h2>
713
+
714
+ <div class="stat-grid">
715
+ <div class="stat-card fade-in">
716
+ <div class="stat-card-label">Projects</div>
717
+ <div class="stat-card-value" id="stat-projects">--</div>
718
+ <div class="stat-card-sub" id="stat-projects-sub">total projects</div>
719
+ </div>
720
+ <div class="stat-card fade-in">
721
+ <div class="stat-card-label">Active Bloops</div>
722
+ <div class="stat-card-value" id="stat-active">--</div>
723
+ <div class="stat-card-sub" id="stat-active-sub">currently running</div>
724
+ </div>
725
+ <div class="stat-card fade-in">
726
+ <div class="stat-card-label">Queue</div>
727
+ <div class="stat-card-value" id="stat-queue">--</div>
728
+ <div class="stat-card-sub" id="stat-queue-sub">pending / running</div>
729
+ </div>
730
+ <div class="stat-card fade-in">
731
+ <div class="stat-card-label">Uptime</div>
732
+ <div class="stat-card-value" id="stat-uptime">--</div>
733
+ <div class="stat-card-sub" id="stat-uptime-sub">since last restart</div>
734
+ </div>
735
+ </div>
736
+ </div>
737
+ </section>
738
+
739
+ <!-- ── Projects ────────────────────────────────────────── -->
740
+ <section class="projects-section">
741
+ <div class="container">
742
+ <div class="section-label">Projects</div>
743
+ <h2 class="section-title">Sandboxed workspaces</h2>
744
+ <p class="section-desc">Each project is an isolated context with its own bloops, memory, and configuration.</p>
745
+
746
+ <div id="projects-container">
747
+ <div class="empty-state">No projects yet. Create one with <code>beercan init &lt;name&gt;</code></div>
748
+ </div>
749
+ </div>
750
+ </section>
751
+
752
+ <!-- ── Job Queue ───────────────────────────────────────── -->
753
+ <section class="jobs-section">
754
+ <div class="container">
755
+ <div class="section-label">Job Queue</div>
756
+ <h2 class="section-title">Queued work</h2>
757
+ <p class="section-desc">Jobs waiting to be processed, currently running, and recently completed.</p>
758
+
759
+ <div id="jobs-container">
760
+ <div class="empty-state">No jobs in queue. Trigger a bloop to see it here.</div>
761
+ </div>
762
+ </div>
763
+ </section>
764
+
765
+ <!-- ── Active Bloops ───────────────────────────────────── -->
766
+ <section class="active-bloops-section hidden" id="active-bloops-section">
767
+ <div class="container">
768
+ <div class="section-label">Active Bloops</div>
769
+ <h2 class="section-title">Currently executing</h2>
770
+
771
+ <div class="active-bloop-grid" id="active-bloops-container">
772
+ </div>
773
+ </div>
774
+ </section>
775
+
776
+ <!-- ── Recent History ──────────────────────────────────── -->
777
+ <section class="recent-section">
778
+ <div class="container">
779
+ <div class="section-label">Recent History</div>
780
+ <h2 class="section-title">Latest bloops</h2>
781
+ <p class="section-desc">The last 20 bloop executions across all projects.</p>
782
+
783
+ <div id="history-container">
784
+ <div class="empty-state">No bloops yet. Run one with <code>beercan run &lt;project&gt; &lt;goal&gt;</code></div>
785
+ </div>
786
+ </div>
787
+ </section>
788
+
789
+ <!-- ── Footer ──────────────────────────────────────────── -->
790
+ <footer>
791
+ <p>
792
+ Built with questionable judgment and excellent taste &bull;
793
+ <a href="https://beercan.ai">beercan.ai</a> &bull;
794
+ <a href="https://beercan.run">beercan.run</a> &bull;
795
+ <a href="https://github.com/ArchieGoodwin/beercan">GitHub</a>
796
+ </p>
797
+ <p style="margin-top: 0.5rem; font-size: 0.75rem;">
798
+ &copy; 2026 BeerCan Contributors &bull; MIT License &bull; No actual beer was harmed in the making of this framework
799
+ </p>
800
+ </footer>
801
+
802
+ <!-- ── JavaScript ──────────────────────────────────────── -->
803
+ <script>
804
+ // ── Scroll animations ──────────────────────────────
805
+ const observer = new IntersectionObserver((entries) => {
806
+ entries.forEach((entry) => {
807
+ if (entry.isIntersecting) {
808
+ entry.target.classList.add('visible');
809
+ }
810
+ });
811
+ }, { threshold: 0.1 });
812
+
813
+ document.querySelectorAll('.fade-in').forEach((el) => observer.observe(el));
814
+
815
+ // ── State ──────────────────────────────────────────
816
+ const STORAGE_KEY = 'beercan-api-url';
817
+ const DEFAULT_URL = window.location.origin;
818
+ const REFRESH_INTERVAL = 10000;
819
+
820
+ let connected = false;
821
+ let refreshTimer = null;
822
+
823
+ // ── DOM refs ───────────────────────────────────────
824
+ const apiUrlInput = document.getElementById('api-url');
825
+ const connectionDot = document.getElementById('connection-dot');
826
+ const connectionLabel = document.getElementById('connection-label');
827
+
828
+ // ── Init ───────────────────────────────────────────
829
+ function init() {
830
+ const savedUrl = localStorage.getItem(STORAGE_KEY);
831
+ apiUrlInput.value = savedUrl || DEFAULT_URL;
832
+
833
+ apiUrlInput.addEventListener('change', () => {
834
+ const url = apiUrlInput.value.trim() || DEFAULT_URL;
835
+ apiUrlInput.value = url;
836
+ localStorage.setItem(STORAGE_KEY, url);
837
+ fetchAll();
838
+ });
839
+
840
+ apiUrlInput.addEventListener('keydown', (e) => {
841
+ if (e.key === 'Enter') {
842
+ apiUrlInput.blur();
843
+ const url = apiUrlInput.value.trim() || DEFAULT_URL;
844
+ apiUrlInput.value = url;
845
+ localStorage.setItem(STORAGE_KEY, url);
846
+ fetchAll();
847
+ }
848
+ });
849
+
850
+ fetchAll();
851
+ refreshTimer = setInterval(fetchAll, REFRESH_INTERVAL);
852
+ }
853
+
854
+ // ── Helpers ────────────────────────────────────────
855
+ function getApiUrl() {
856
+ return (apiUrlInput.value.trim() || DEFAULT_URL).replace(/\/+$/, '');
857
+ }
858
+
859
+ function formatNumber(n) {
860
+ if (n == null) return '--';
861
+ return Number(n).toLocaleString();
862
+ }
863
+
864
+ function formatUptime(seconds) {
865
+ if (seconds == null) return '--';
866
+ const s = Math.floor(seconds);
867
+ const h = Math.floor(s / 3600);
868
+ const m = Math.floor((s % 3600) / 60);
869
+ if (h > 0) return h + 'h ' + m + 'm';
870
+ return m + 'm ' + (s % 60) + 's';
871
+ }
872
+
873
+ function formatDuration(startIso, endIso) {
874
+ if (!startIso) return '--';
875
+ const start = new Date(startIso).getTime();
876
+ const end = endIso ? new Date(endIso).getTime() : Date.now();
877
+ const diffS = Math.max(0, Math.floor((end - start) / 1000));
878
+ const h = Math.floor(diffS / 3600);
879
+ const m = Math.floor((diffS % 3600) / 60);
880
+ const s = diffS % 60;
881
+ if (h > 0) return h + 'h ' + m + 'm ' + s + 's';
882
+ if (m > 0) return m + 'm ' + s + 's';
883
+ return s + 's';
884
+ }
885
+
886
+ function formatTimestamp(iso) {
887
+ if (!iso) return '--';
888
+ const d = new Date(iso);
889
+ return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) +
890
+ ' ' + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
891
+ }
892
+
893
+ function truncateGoal(goal, max) {
894
+ if (!goal) return '';
895
+ if (goal.length <= max) return goal;
896
+ return goal.substring(0, max) + '...';
897
+ }
898
+
899
+ function escapeHtml(text) {
900
+ const div = document.createElement('div');
901
+ div.textContent = text || '';
902
+ return div.innerHTML;
903
+ }
904
+
905
+ function setConnected(state) {
906
+ connected = state;
907
+ connectionDot.className = 'connection-dot ' + (state ? 'connected' : 'disconnected');
908
+ connectionLabel.className = 'connection-label ' + (state ? 'connected' : 'disconnected');
909
+ connectionLabel.textContent = state ? 'Connected' : 'Disconnected';
910
+ }
911
+
912
+ // ── Fetch all data ─────────────────────────────────
913
+ async function fetchAll() {
914
+ const apiUrl = getApiUrl();
915
+
916
+ try {
917
+ const [statusRes, projectsRes, jobsRes, recentRes] = await Promise.all([
918
+ fetch(apiUrl + '/api/status').then(r => r.json()),
919
+ fetch(apiUrl + '/api/projects').then(r => r.json()),
920
+ fetch(apiUrl + '/api/jobs').then(r => r.json()),
921
+ fetch(apiUrl + '/api/bloops/recent').then(r => r.json()),
922
+ ]);
923
+
924
+ setConnected(true);
925
+ renderOverview(statusRes, jobsRes);
926
+ renderProjects(projectsRes);
927
+ renderJobs(jobsRes);
928
+ renderActiveBloops(recentRes);
929
+ renderHistory(recentRes);
930
+ } catch (err) {
931
+ setConnected(false);
932
+ }
933
+ }
934
+
935
+ // ── Render: System Overview ────────────────────────
936
+ function renderOverview(status, jobs) {
937
+ const projectCount = status.projects ? status.projects.total : 0;
938
+ const runningBloops = status.bloops ? status.bloops.running : 0;
939
+ const pendingJobs = status.jobs ? status.jobs.pending : 0;
940
+ const runningJobs = status.jobs ? status.jobs.running : 0;
941
+ const uptime = status.uptime;
942
+
943
+ document.getElementById('stat-projects').textContent = formatNumber(projectCount);
944
+ document.getElementById('stat-projects-sub').textContent = projectCount === 1 ? '1 project' : projectCount + ' total projects';
945
+
946
+ const activeEl = document.getElementById('stat-active');
947
+ activeEl.textContent = formatNumber(runningBloops);
948
+ if (runningBloops > 0) {
949
+ activeEl.classList.add('pulse');
950
+ } else {
951
+ activeEl.classList.remove('pulse');
952
+ }
953
+ document.getElementById('stat-active-sub').textContent = 'currently running';
954
+
955
+ document.getElementById('stat-queue').textContent = pendingJobs + ' / ' + runningJobs;
956
+ document.getElementById('stat-queue-sub').textContent = 'pending / running';
957
+
958
+ document.getElementById('stat-uptime').textContent = formatUptime(uptime);
959
+ document.getElementById('stat-uptime-sub').textContent = 'since last restart';
960
+ }
961
+
962
+ // ── Render: Projects ──────────────────────────────
963
+ function renderProjects(data) {
964
+ const container = document.getElementById('projects-container');
965
+ const projects = data.projects || [];
966
+
967
+ if (projects.length === 0) {
968
+ container.innerHTML = '<div class="empty-state">No projects yet. Create one with <code>beercan init &lt;name&gt;</code></div>';
969
+ return;
970
+ }
971
+
972
+ let html = '<div class="project-grid">';
973
+ for (const p of projects) {
974
+ const bloops = p.bloops || {};
975
+ html += '<div class="project-card fade-in visible">';
976
+ html += '<div class="project-name">' + escapeHtml(p.name || p.slug) + '</div>';
977
+ html += '<div class="project-slug">' + escapeHtml(p.slug) + '</div>';
978
+
979
+ if (p.workDir) {
980
+ html += '<div class="project-workdir">' + escapeHtml(p.workDir) + '</div>';
981
+ }
982
+
983
+ html += '<div class="project-stats">';
984
+ html += '<span class="project-stat green"><span class="dot green"></span>' + (bloops.completed || 0) + ' completed</span>';
985
+ html += '<span class="project-stat red"><span class="dot red"></span>' + (bloops.failed || 0) + ' failed</span>';
986
+ html += '<span class="project-stat blue"><span class="dot blue"></span>' + (bloops.running || 0) + ' running</span>';
987
+ html += '</div>';
988
+
989
+ html += '<div class="project-tokens">' + formatNumber(p.totalTokens || 0) + ' tokens</div>';
990
+ html += '</div>';
991
+ }
992
+ html += '</div>';
993
+ container.innerHTML = html;
994
+ }
995
+
996
+ // ── Render: Job Queue ─────────────────────────────
997
+ function renderJobs(data) {
998
+ const container = document.getElementById('jobs-container');
999
+ const jobs = data.jobs || [];
1000
+
1001
+ if (jobs.length === 0) {
1002
+ container.innerHTML = '<div class="empty-state">No jobs in queue. Trigger a bloop to see it here.</div>';
1003
+ return;
1004
+ }
1005
+
1006
+ let html = '<div class="jobs-table-wrap"><table class="jobs-table">';
1007
+ html += '<thead><tr>';
1008
+ html += '<th>Status</th>';
1009
+ html += '<th>Goal</th>';
1010
+ html += '<th>Source</th>';
1011
+ html += '<th>Project</th>';
1012
+ html += '<th>Created</th>';
1013
+ html += '</tr></thead>';
1014
+ html += '<tbody>';
1015
+
1016
+ for (const job of jobs) {
1017
+ const statusClass = (job.status || 'pending').toLowerCase();
1018
+ html += '<tr>';
1019
+ html += '<td data-label="Status"><span class="status-badge ' + statusClass + '">' + escapeHtml(job.status || 'unknown') + '</span></td>';
1020
+ html += '<td data-label="Goal"><div class="goal-text">' + escapeHtml(job.goal || '') + '</div></td>';
1021
+ html += '<td data-label="Source"><span class="source-badge">' + escapeHtml(job.source || 'manual') + '</span></td>';
1022
+ html += '<td data-label="Project"><span class="source-badge">' + escapeHtml(job.projectSlug || '') + '</span></td>';
1023
+ html += '<td data-label="Created"><span class="timestamp">' + formatTimestamp(job.createdAt) + '</span></td>';
1024
+ html += '</tr>';
1025
+ }
1026
+
1027
+ html += '</tbody></table></div>';
1028
+ container.innerHTML = html;
1029
+ }
1030
+
1031
+ // ── Render: Active Bloops ─────────────────────────
1032
+ function renderActiveBloops(data) {
1033
+ const bloops = (data.bloops || []).filter(b => b.status === 'running');
1034
+ const section = document.getElementById('active-bloops-section');
1035
+ const container = document.getElementById('active-bloops-container');
1036
+
1037
+ if (bloops.length === 0) {
1038
+ section.classList.add('hidden');
1039
+ return;
1040
+ }
1041
+
1042
+ section.classList.remove('hidden');
1043
+ let html = '';
1044
+
1045
+ for (const b of bloops) {
1046
+ html += '<div class="active-bloop-card">';
1047
+ html += '<div class="active-bloop-goal">' + escapeHtml(b.goal) + '</div>';
1048
+ html += '<div class="active-bloop-project">' + escapeHtml(b.projectId || '') + '</div>';
1049
+ html += '<div class="active-bloop-meta">';
1050
+ html += '<span>Iterations: <strong>' + (b.iterations || 0) + '</strong></span>';
1051
+ html += '<span>Tokens: <strong>' + formatNumber(b.tokensUsed || 0) + '</strong></span>';
1052
+ html += '<span>Elapsed: <strong>' + formatDuration(b.createdAt, null) + '</strong></span>';
1053
+ html += '</div>';
1054
+ html += '</div>';
1055
+ }
1056
+
1057
+ container.innerHTML = html;
1058
+ }
1059
+
1060
+ // ── Render: Recent History ────────────────────────
1061
+ function renderHistory(data) {
1062
+ const container = document.getElementById('history-container');
1063
+ const bloops = data.bloops || [];
1064
+
1065
+ if (bloops.length === 0) {
1066
+ container.innerHTML = '<div class="empty-state">No bloops yet. Run one with <code>beercan run &lt;project&gt; &lt;goal&gt;</code></div>';
1067
+ return;
1068
+ }
1069
+
1070
+ let html = '<div class="history-list">';
1071
+
1072
+ for (const b of bloops) {
1073
+ const statusClass = (b.status || 'pending').toLowerCase();
1074
+ html += '<div class="history-item">';
1075
+ html += '<span class="status-badge ' + statusClass + '">' + escapeHtml(b.status || 'unknown') + '</span>';
1076
+ html += '<span class="history-goal">' + escapeHtml(b.goal || '') + '</span>';
1077
+ html += '<span class="history-project">' + escapeHtml(b.projectId || '') + '</span>';
1078
+ html += '<span class="history-tokens">' + formatNumber(b.tokensUsed || 0) + ' tok</span>';
1079
+ html += '<span class="history-duration">' + formatDuration(b.createdAt, b.completedAt) + '</span>';
1080
+ html += '</div>';
1081
+ }
1082
+
1083
+ html += '</div>';
1084
+ container.innerHTML = html;
1085
+ }
1086
+
1087
+ // ── Boot ───────────────────────────────────────────
1088
+ init();
1089
+ </script>
1090
+
1091
+ </body>
1092
+ </html>