@chrysb/alphaclaw 0.7.0 → 0.7.1
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.
- package/lib/public/css/cron.css +26 -17
- package/lib/public/css/theme.css +14 -0
- package/lib/public/js/components/cron-tab/cron-calendar.js +17 -12
- package/lib/public/js/components/cron-tab/cron-job-list.js +11 -1
- package/lib/public/js/components/cron-tab/index.js +16 -2
- package/lib/public/js/components/icons.js +11 -0
- package/lib/public/js/components/routes/watchdog-route.js +1 -1
- package/lib/public/js/components/watchdog-tab/console/index.js +115 -0
- package/lib/public/js/components/watchdog-tab/console/use-console.js +137 -0
- package/lib/public/js/components/watchdog-tab/helpers.js +106 -0
- package/lib/public/js/components/watchdog-tab/incidents/index.js +56 -0
- package/lib/public/js/components/watchdog-tab/incidents/use-incidents.js +33 -0
- package/lib/public/js/components/watchdog-tab/index.js +84 -0
- package/lib/public/js/components/watchdog-tab/resource-bar.js +76 -0
- package/lib/public/js/components/watchdog-tab/resources/index.js +85 -0
- package/lib/public/js/components/watchdog-tab/resources/use-resources.js +13 -0
- package/lib/public/js/components/watchdog-tab/settings/index.js +44 -0
- package/lib/public/js/components/watchdog-tab/settings/use-settings.js +117 -0
- package/lib/public/js/components/watchdog-tab/terminal/index.js +20 -0
- package/lib/public/js/components/watchdog-tab/terminal/use-terminal.js +263 -0
- package/lib/public/js/components/watchdog-tab/use-watchdog-tab.js +55 -0
- package/lib/public/js/lib/api.js +39 -0
- package/lib/server/init/register-server-routes.js +239 -0
- package/lib/server/init/runtime-init.js +44 -0
- package/lib/server/init/server-lifecycle.js +55 -0
- package/lib/server/routes/watchdog.js +62 -0
- package/lib/server/watchdog-terminal-ws.js +114 -0
- package/lib/server/watchdog-terminal.js +262 -0
- package/lib/server.js +89 -215
- package/package.json +3 -2
- package/lib/public/js/components/watchdog-tab.js +0 -535
package/lib/public/css/cron.css
CHANGED
|
@@ -400,23 +400,6 @@
|
|
|
400
400
|
justify-content: center;
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
-
.cron-calendar-expand-btn {
|
|
404
|
-
width: 22px;
|
|
405
|
-
height: 22px;
|
|
406
|
-
border-radius: 6px;
|
|
407
|
-
border: 1px solid var(--border);
|
|
408
|
-
background: rgba(255, 255, 255, 0.03);
|
|
409
|
-
color: var(--text-dim);
|
|
410
|
-
display: inline-flex;
|
|
411
|
-
align-items: center;
|
|
412
|
-
justify-content: center;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
.cron-calendar-expand-btn:hover {
|
|
416
|
-
color: var(--text);
|
|
417
|
-
border-color: rgba(148, 163, 184, 0.5);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
403
|
.cron-calendar-grid-wrap {
|
|
421
404
|
position: relative;
|
|
422
405
|
}
|
|
@@ -464,6 +447,7 @@
|
|
|
464
447
|
border-left: 1px solid var(--border);
|
|
465
448
|
border-bottom: 1px solid var(--border);
|
|
466
449
|
padding: 5px;
|
|
450
|
+
position: relative;
|
|
467
451
|
display: flex;
|
|
468
452
|
flex-direction: column;
|
|
469
453
|
gap: 4px;
|
|
@@ -473,6 +457,29 @@
|
|
|
473
457
|
background: rgba(99, 235, 255, 0.04);
|
|
474
458
|
}
|
|
475
459
|
|
|
460
|
+
.cron-calendar-now-indicator {
|
|
461
|
+
position: absolute;
|
|
462
|
+
left: 5px;
|
|
463
|
+
right: 5px;
|
|
464
|
+
height: 2px;
|
|
465
|
+
border-radius: 999px;
|
|
466
|
+
background: rgba(248, 113, 113, 0.95);
|
|
467
|
+
box-shadow: 0 0 0 1px rgba(248, 113, 113, 0.18), 0 0 8px rgba(239, 68, 68, 0.55);
|
|
468
|
+
pointer-events: none;
|
|
469
|
+
z-index: 1;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.cron-calendar-now-indicator-dot {
|
|
473
|
+
position: absolute;
|
|
474
|
+
left: -3px;
|
|
475
|
+
top: 50%;
|
|
476
|
+
width: 6px;
|
|
477
|
+
height: 6px;
|
|
478
|
+
border-radius: 999px;
|
|
479
|
+
background: rgba(248, 113, 113, 0.98);
|
|
480
|
+
transform: translateY(-50%);
|
|
481
|
+
}
|
|
482
|
+
|
|
476
483
|
.cron-calendar-slot-chip {
|
|
477
484
|
font-size: 11px;
|
|
478
485
|
line-height: 1.2;
|
|
@@ -485,6 +492,8 @@
|
|
|
485
492
|
width: 100%;
|
|
486
493
|
max-width: 100%;
|
|
487
494
|
overflow: hidden;
|
|
495
|
+
position: relative;
|
|
496
|
+
z-index: 2;
|
|
488
497
|
}
|
|
489
498
|
|
|
490
499
|
.cron-calendar-slot-overflow {
|
package/lib/public/css/theme.css
CHANGED
|
@@ -685,3 +685,17 @@ textarea:focus {
|
|
|
685
685
|
resize: vertical;
|
|
686
686
|
}
|
|
687
687
|
|
|
688
|
+
.watchdog-terminal-host {
|
|
689
|
+
position: relative;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.watchdog-terminal-host .xterm {
|
|
693
|
+
height: 100%;
|
|
694
|
+
letter-spacing: 0;
|
|
695
|
+
font-kerning: none;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.watchdog-terminal-host .xterm-viewport {
|
|
699
|
+
overflow-y: auto !important;
|
|
700
|
+
}
|
|
701
|
+
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import htm from "https://esm.sh/htm";
|
|
9
9
|
import { Tooltip } from "../tooltip.js";
|
|
10
10
|
import { ModalShell } from "../modal-shell.js";
|
|
11
|
-
import { CloseIcon
|
|
11
|
+
import { CloseIcon } from "../icons.js";
|
|
12
12
|
import {
|
|
13
13
|
formatCost,
|
|
14
14
|
formatCronScheduleLabel,
|
|
@@ -306,6 +306,9 @@ export const CronCalendar = ({
|
|
|
306
306
|
};
|
|
307
307
|
}, []);
|
|
308
308
|
const todayDayKey = toLocalDayKey(nowMs);
|
|
309
|
+
const nowDateValue = useMemo(() => new Date(nowMs), [nowMs]);
|
|
310
|
+
const currentHourOfDay = nowDateValue.getHours();
|
|
311
|
+
const currentMinuteProgress = nowDateValue.getMinutes() / 60;
|
|
309
312
|
const { repeatingJobs, scheduledJobs } = useMemo(
|
|
310
313
|
() => classifyRepeatingJobs(jobs),
|
|
311
314
|
[jobs],
|
|
@@ -569,17 +572,7 @@ export const CronCalendar = ({
|
|
|
569
572
|
: html`
|
|
570
573
|
<div class="cron-calendar-grid-wrap">
|
|
571
574
|
<div class="cron-calendar-grid-header">
|
|
572
|
-
<div class="cron-calendar-hour-cell cron-calendar-grid-corner">
|
|
573
|
-
<button
|
|
574
|
-
type="button"
|
|
575
|
-
class="cron-calendar-expand-btn"
|
|
576
|
-
title="Expand calendar"
|
|
577
|
-
aria-label="Expand calendar"
|
|
578
|
-
onClick=${() => setCalendarLightboxOpen(true)}
|
|
579
|
-
>
|
|
580
|
-
<${FullscreenLineIcon} className="w-3.5 h-3.5" />
|
|
581
|
-
</button>
|
|
582
|
-
</div>
|
|
575
|
+
<div class="cron-calendar-hour-cell cron-calendar-grid-corner"></div>
|
|
583
576
|
${timeline.days.map(
|
|
584
577
|
(day) => html`
|
|
585
578
|
<div
|
|
@@ -611,6 +604,18 @@ export const CronCalendar = ({
|
|
|
611
604
|
key=${cellKey}
|
|
612
605
|
class=${`cron-calendar-grid-cell ${day.dayKey === todayDayKey ? "is-today" : ""}`}
|
|
613
606
|
>
|
|
607
|
+
${day.dayKey === todayDayKey &&
|
|
608
|
+
hourOfDay === currentHourOfDay
|
|
609
|
+
? html`
|
|
610
|
+
<div
|
|
611
|
+
class="cron-calendar-now-indicator"
|
|
612
|
+
style=${`top: ${Math.max(0, Math.min(100, currentMinuteProgress * 100))}%;`}
|
|
613
|
+
aria-hidden="true"
|
|
614
|
+
>
|
|
615
|
+
<span class="cron-calendar-now-indicator-dot"></span>
|
|
616
|
+
</div>
|
|
617
|
+
`
|
|
618
|
+
: null}
|
|
614
619
|
${visibleSlots.map((slot) => {
|
|
615
620
|
const status = statusBySlotKey[slot.key] || "";
|
|
616
621
|
const isPast = slot.scheduledAtMs <= nowMs;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
-
import { useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import {
|
|
5
5
|
formatCronScheduleLabel,
|
|
@@ -202,7 +202,16 @@ export const CronJobList = ({
|
|
|
202
202
|
onSelectAllJobs = () => {},
|
|
203
203
|
onSelectJob = () => {},
|
|
204
204
|
}) => {
|
|
205
|
+
const searchInputRef = useRef(null);
|
|
205
206
|
const [searchQuery, setSearchQuery] = useState("");
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
const frameId = window.requestAnimationFrame(() => {
|
|
209
|
+
searchInputRef.current?.focus();
|
|
210
|
+
});
|
|
211
|
+
return () => {
|
|
212
|
+
window.cancelAnimationFrame(frameId);
|
|
213
|
+
};
|
|
214
|
+
}, []);
|
|
206
215
|
const normalizedQuery = String(searchQuery || "").trim().toLowerCase();
|
|
207
216
|
const filteredJobs = useMemo(() => {
|
|
208
217
|
if (!normalizedQuery) return jobs;
|
|
@@ -236,6 +245,7 @@ export const CronJobList = ({
|
|
|
236
245
|
<div class="cron-list-panel-inner">
|
|
237
246
|
<div class="cron-list-sticky-search">
|
|
238
247
|
<input
|
|
248
|
+
ref=${searchInputRef}
|
|
239
249
|
type="text"
|
|
240
250
|
value=${searchQuery}
|
|
241
251
|
placeholder="Search cron jobs..."
|
|
@@ -2,6 +2,7 @@ import { h } from "https://esm.sh/preact";
|
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
3
3
|
import htm from "https://esm.sh/htm";
|
|
4
4
|
import { ActionButton } from "../action-button.js";
|
|
5
|
+
import { AlarmLineIcon } from "../icons.js";
|
|
5
6
|
import { PageHeader } from "../page-header.js";
|
|
6
7
|
import { CronJobList } from "./cron-job-list.js";
|
|
7
8
|
import { CronJobDetail } from "./cron-job-detail.js";
|
|
@@ -144,8 +145,21 @@ export const CronTab = ({ jobId = "", onSetLocation = () => {} }) => {
|
|
|
144
145
|
<main class="cron-detail-panel">
|
|
145
146
|
${noJobs
|
|
146
147
|
? html`
|
|
147
|
-
<div
|
|
148
|
-
|
|
148
|
+
<div
|
|
149
|
+
class="bg-surface border border-border rounded-xl px-6 py-10 min-h-[26rem] flex flex-col items-center justify-center text-center"
|
|
150
|
+
>
|
|
151
|
+
<div class="max-w-md w-full flex flex-col items-center gap-4">
|
|
152
|
+
<${AlarmLineIcon} className="h-12 w-12 text-cyan-400" />
|
|
153
|
+
<div class="space-y-2">
|
|
154
|
+
<h2 class="font-semibold text-lg text-gray-100">
|
|
155
|
+
No cron jobs yet
|
|
156
|
+
</h2>
|
|
157
|
+
<p class="text-xs text-gray-400 leading-5">
|
|
158
|
+
Cron jobs are managed via the OpenClaw CLI. Once jobs are
|
|
159
|
+
configured, schedules and run history will appear here.
|
|
160
|
+
</p>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
149
163
|
</div>
|
|
150
164
|
`
|
|
151
165
|
: isAllJobsSelected
|
|
@@ -429,6 +429,17 @@ export const ErrorWarningLineIcon = ({ className = "" }) => html`
|
|
|
429
429
|
</svg>
|
|
430
430
|
`;
|
|
431
431
|
|
|
432
|
+
export const AlarmLineIcon = ({ className = "" }) => html`
|
|
433
|
+
<svg
|
|
434
|
+
class=${className}
|
|
435
|
+
viewBox="0 0 24 24"
|
|
436
|
+
fill="currentColor"
|
|
437
|
+
aria-hidden="true"
|
|
438
|
+
>
|
|
439
|
+
<path d="M12.0001 22.0001C7.02956 22.0001 3.00012 17.9707 3.00012 13.0001C3.00012 8.02956 7.02956 4.00012 12.0001 4.00012C16.9707 4.00012 21.0001 8.02956 21.0001 13.0001C21.0001 17.9707 16.9707 22.0001 12.0001 22.0001ZM12.0001 20.0001C15.8661 20.0001 19.0001 16.8661 19.0001 13.0001C19.0001 9.13412 15.8661 6.00012 12.0001 6.00012C8.13412 6.00012 5.00012 9.13412 5.00012 13.0001C5.00012 16.8661 8.13412 20.0001 12.0001 20.0001ZM13.0001 13.0001H16.0001V15.0001H11.0001V8.00012H13.0001V13.0001ZM1.74707 6.2826L5.2826 2.74707L6.69682 4.16128L3.16128 7.69682L1.74707 6.2826ZM18.7176 2.74707L22.2532 6.2826L20.839 7.69682L17.3034 4.16128L18.7176 2.74707Z" />
|
|
440
|
+
</svg>
|
|
441
|
+
`;
|
|
442
|
+
|
|
432
443
|
export const FullscreenLineIcon = ({ className = "" }) => html`
|
|
433
444
|
<svg
|
|
434
445
|
class=${className}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import {
|
|
4
|
+
kWatchdogConsoleTabLogs,
|
|
5
|
+
kWatchdogConsoleTabTerminal,
|
|
6
|
+
} from "../helpers.js";
|
|
7
|
+
import { WatchdogTerminal } from "../terminal/index.js";
|
|
8
|
+
|
|
9
|
+
const html = htm.bind(h);
|
|
10
|
+
|
|
11
|
+
export const WatchdogConsoleCard = ({
|
|
12
|
+
activeConsoleTab = kWatchdogConsoleTabLogs,
|
|
13
|
+
stickToBottom = true,
|
|
14
|
+
onSetStickToBottom = () => {},
|
|
15
|
+
onSelectConsoleTab = () => {},
|
|
16
|
+
connectingTerminal = false,
|
|
17
|
+
terminalConnected = false,
|
|
18
|
+
terminalEnded = false,
|
|
19
|
+
terminalStatusText = "",
|
|
20
|
+
terminalUiSettling = false,
|
|
21
|
+
onRestartTerminalSession = () => {},
|
|
22
|
+
logsRef = null,
|
|
23
|
+
logs = "",
|
|
24
|
+
loadingLogs = true,
|
|
25
|
+
terminalPanelRef = null,
|
|
26
|
+
terminalHostRef = null,
|
|
27
|
+
terminalInstanceRef = null,
|
|
28
|
+
logsPanelHeightPx = 320,
|
|
29
|
+
}) => html`
|
|
30
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
31
|
+
<div class="flex items-center justify-between gap-2 mb-3">
|
|
32
|
+
<div
|
|
33
|
+
class="inline-flex items-center rounded-lg border border-border bg-black/20 p-0.5"
|
|
34
|
+
>
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
class=${`px-2.5 py-1 text-xs rounded-md ${activeConsoleTab === kWatchdogConsoleTabLogs ? "bg-surface text-gray-100" : "text-gray-400 hover:text-gray-200"}`}
|
|
38
|
+
onClick=${() => onSelectConsoleTab(kWatchdogConsoleTabLogs)}
|
|
39
|
+
>
|
|
40
|
+
Logs
|
|
41
|
+
</button>
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
class=${`px-2.5 py-1 text-xs rounded-md ${activeConsoleTab === kWatchdogConsoleTabTerminal ? "bg-surface text-gray-100" : "text-gray-400 hover:text-gray-200"}`}
|
|
45
|
+
onClick=${() => onSelectConsoleTab(kWatchdogConsoleTabTerminal)}
|
|
46
|
+
>
|
|
47
|
+
Terminal
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="flex items-center gap-2">
|
|
51
|
+
${activeConsoleTab === kWatchdogConsoleTabLogs
|
|
52
|
+
? html`
|
|
53
|
+
<label class="inline-flex items-center gap-2 text-xs text-gray-400">
|
|
54
|
+
<input
|
|
55
|
+
type="checkbox"
|
|
56
|
+
checked=${stickToBottom}
|
|
57
|
+
onchange=${(event) =>
|
|
58
|
+
onSetStickToBottom(!!event.currentTarget?.checked)}
|
|
59
|
+
/>
|
|
60
|
+
Stick to bottom
|
|
61
|
+
</label>
|
|
62
|
+
`
|
|
63
|
+
: html`
|
|
64
|
+
<div class="flex items-center gap-2 pr-1">
|
|
65
|
+
${terminalUiSettling
|
|
66
|
+
? null
|
|
67
|
+
: html`
|
|
68
|
+
<span class="text-xs text-gray-500">
|
|
69
|
+
${connectingTerminal
|
|
70
|
+
? "Connecting..."
|
|
71
|
+
: terminalEnded
|
|
72
|
+
? "Session ended"
|
|
73
|
+
: terminalConnected
|
|
74
|
+
? "Connected"
|
|
75
|
+
: terminalStatusText || "Disconnected"}
|
|
76
|
+
</span>
|
|
77
|
+
${connectingTerminal || terminalConnected
|
|
78
|
+
? null
|
|
79
|
+
: html`
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
class="ac-btn-secondary text-xs px-2.5 py-1 rounded-lg"
|
|
83
|
+
onClick=${onRestartTerminalSession}
|
|
84
|
+
>
|
|
85
|
+
New session
|
|
86
|
+
</button>
|
|
87
|
+
`}
|
|
88
|
+
`}
|
|
89
|
+
</div>
|
|
90
|
+
`}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<div class=${activeConsoleTab === kWatchdogConsoleTabLogs ? "" : "hidden"}>
|
|
94
|
+
<pre
|
|
95
|
+
ref=${logsRef}
|
|
96
|
+
class="watchdog-logs-panel bg-black/40 border border-border rounded-lg p-3 overflow-auto text-xs text-gray-300 whitespace-pre-wrap break-words"
|
|
97
|
+
style=${{ height: `${logsPanelHeightPx}px` }}
|
|
98
|
+
>
|
|
99
|
+
${loadingLogs ? "Loading logs..." : logs || "No logs yet."}</pre
|
|
100
|
+
>
|
|
101
|
+
</div>
|
|
102
|
+
<div
|
|
103
|
+
class=${activeConsoleTab === kWatchdogConsoleTabTerminal
|
|
104
|
+
? "space-y-2"
|
|
105
|
+
: "hidden"}
|
|
106
|
+
>
|
|
107
|
+
<${WatchdogTerminal}
|
|
108
|
+
panelRef=${terminalPanelRef}
|
|
109
|
+
hostRef=${terminalHostRef}
|
|
110
|
+
terminalInstanceRef=${terminalInstanceRef}
|
|
111
|
+
panelHeightPx=${logsPanelHeightPx}
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
`;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import { fetchWatchdogLogs } from "../../../lib/api.js";
|
|
3
|
+
import { readUiSettings, writeUiSettings } from "../../../lib/ui-settings.js";
|
|
4
|
+
import {
|
|
5
|
+
clampWatchdogLogsPanelHeight,
|
|
6
|
+
kWatchdogConsoleTabLogs,
|
|
7
|
+
kWatchdogConsoleTabTerminal,
|
|
8
|
+
kWatchdogConsoleTabUiSettingKey,
|
|
9
|
+
kWatchdogLogsPanelHeightUiSettingKey,
|
|
10
|
+
normalizeWatchdogConsoleTab,
|
|
11
|
+
readCssHeightPx,
|
|
12
|
+
} from "../helpers.js";
|
|
13
|
+
import { useWatchdogTerminal } from "../terminal/use-terminal.js";
|
|
14
|
+
|
|
15
|
+
export const useWatchdogConsole = () => {
|
|
16
|
+
const [logs, setLogs] = useState("");
|
|
17
|
+
const [loadingLogs, setLoadingLogs] = useState(true);
|
|
18
|
+
const [stickToBottom, setStickToBottom] = useState(true);
|
|
19
|
+
const [activeConsoleTab, setActiveConsoleTab] = useState(() => {
|
|
20
|
+
const settings = readUiSettings();
|
|
21
|
+
return normalizeWatchdogConsoleTab(settings?.[kWatchdogConsoleTabUiSettingKey]);
|
|
22
|
+
});
|
|
23
|
+
const [logsPanelHeightPx, setLogsPanelHeightPx] = useState(() => {
|
|
24
|
+
const settings = readUiSettings();
|
|
25
|
+
return clampWatchdogLogsPanelHeight(
|
|
26
|
+
settings?.[kWatchdogLogsPanelHeightUiSettingKey],
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
const logsRef = useRef(null);
|
|
30
|
+
const terminalPanelRef = useRef(null);
|
|
31
|
+
const terminalHostRef = useRef(null);
|
|
32
|
+
const terminal = useWatchdogTerminal({
|
|
33
|
+
active: activeConsoleTab === kWatchdogConsoleTabTerminal,
|
|
34
|
+
panelRef: terminalPanelRef,
|
|
35
|
+
hostRef: terminalHostRef,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const settings = readUiSettings();
|
|
40
|
+
settings[kWatchdogConsoleTabUiSettingKey] =
|
|
41
|
+
normalizeWatchdogConsoleTab(activeConsoleTab);
|
|
42
|
+
writeUiSettings(settings);
|
|
43
|
+
}, [activeConsoleTab]);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
let active = true;
|
|
47
|
+
let timer = null;
|
|
48
|
+
const pollLogs = async () => {
|
|
49
|
+
try {
|
|
50
|
+
const text = await fetchWatchdogLogs(65536);
|
|
51
|
+
if (!active) return;
|
|
52
|
+
setLogs(text || "");
|
|
53
|
+
setLoadingLogs(false);
|
|
54
|
+
} catch {
|
|
55
|
+
if (!active) return;
|
|
56
|
+
setLoadingLogs(false);
|
|
57
|
+
}
|
|
58
|
+
if (!active) return;
|
|
59
|
+
timer = setTimeout(pollLogs, 3000);
|
|
60
|
+
};
|
|
61
|
+
pollLogs();
|
|
62
|
+
return () => {
|
|
63
|
+
active = false;
|
|
64
|
+
if (timer) clearTimeout(timer);
|
|
65
|
+
};
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const logsElement = logsRef.current;
|
|
70
|
+
if (!logsElement || !stickToBottom) return;
|
|
71
|
+
logsElement.scrollTop = logsElement.scrollHeight;
|
|
72
|
+
}, [logs, stickToBottom]);
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const panelElement =
|
|
76
|
+
activeConsoleTab === kWatchdogConsoleTabLogs
|
|
77
|
+
? logsRef.current
|
|
78
|
+
: terminalPanelRef.current;
|
|
79
|
+
if (!panelElement || typeof ResizeObserver === "undefined") return () => {};
|
|
80
|
+
let saveTimer = null;
|
|
81
|
+
const observer = new ResizeObserver((entries) => {
|
|
82
|
+
const entry = entries?.[0];
|
|
83
|
+
const nextHeight = clampWatchdogLogsPanelHeight(
|
|
84
|
+
readCssHeightPx(entry?.target),
|
|
85
|
+
);
|
|
86
|
+
setLogsPanelHeightPx((currentValue) =>
|
|
87
|
+
Math.abs(currentValue - nextHeight) >= 1 ? nextHeight : currentValue,
|
|
88
|
+
);
|
|
89
|
+
if (saveTimer) window.clearTimeout(saveTimer);
|
|
90
|
+
saveTimer = window.setTimeout(() => {
|
|
91
|
+
const settings = readUiSettings();
|
|
92
|
+
settings[kWatchdogLogsPanelHeightUiSettingKey] = nextHeight;
|
|
93
|
+
writeUiSettings(settings);
|
|
94
|
+
}, 120);
|
|
95
|
+
if (activeConsoleTab === kWatchdogConsoleTabTerminal) {
|
|
96
|
+
window.requestAnimationFrame(() => {
|
|
97
|
+
terminal.fitNow();
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
observer.observe(panelElement);
|
|
102
|
+
return () => {
|
|
103
|
+
observer.disconnect();
|
|
104
|
+
if (saveTimer) window.clearTimeout(saveTimer);
|
|
105
|
+
};
|
|
106
|
+
}, [activeConsoleTab]);
|
|
107
|
+
|
|
108
|
+
const handleSelectConsoleTab = (nextTab = kWatchdogConsoleTabLogs) => {
|
|
109
|
+
const normalizedTab = normalizeWatchdogConsoleTab(nextTab);
|
|
110
|
+
if (normalizedTab === kWatchdogConsoleTabTerminal) {
|
|
111
|
+
terminal.prepareForActivate();
|
|
112
|
+
} else {
|
|
113
|
+
terminal.clearSettling();
|
|
114
|
+
}
|
|
115
|
+
setActiveConsoleTab(normalizedTab);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const onRestartTerminalSession = () => {
|
|
119
|
+
terminal.restartSession();
|
|
120
|
+
setActiveConsoleTab(kWatchdogConsoleTabTerminal);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
logs,
|
|
125
|
+
loadingLogs,
|
|
126
|
+
stickToBottom,
|
|
127
|
+
setStickToBottom,
|
|
128
|
+
activeConsoleTab,
|
|
129
|
+
handleSelectConsoleTab,
|
|
130
|
+
logsPanelHeightPx,
|
|
131
|
+
logsRef,
|
|
132
|
+
terminalPanelRef,
|
|
133
|
+
terminalHostRef,
|
|
134
|
+
onRestartTerminalSession,
|
|
135
|
+
...terminal,
|
|
136
|
+
};
|
|
137
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export const kWatchdogConsoleTabLogs = "logs";
|
|
2
|
+
export const kWatchdogConsoleTabTerminal = "terminal";
|
|
3
|
+
export const kWatchdogConsoleTabUiSettingKey = "watchdogConsoleTab";
|
|
4
|
+
export const kWatchdogLogsPanelHeightUiSettingKey = "watchdogLogsPanelHeightPx";
|
|
5
|
+
export const kWatchdogLogsPanelDefaultHeightPx = 320;
|
|
6
|
+
export const kWatchdogLogsPanelMinHeightPx = 160;
|
|
7
|
+
export const kXtermCssUrl =
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css";
|
|
9
|
+
export const kWatchdogTerminalWsPath = "/api/watchdog/terminal/ws";
|
|
10
|
+
|
|
11
|
+
let xtermModulesPromise = null;
|
|
12
|
+
|
|
13
|
+
export const loadXtermModules = () => {
|
|
14
|
+
if (!xtermModulesPromise) {
|
|
15
|
+
xtermModulesPromise = Promise.all([
|
|
16
|
+
import("https://esm.sh/@xterm/xterm@5.5.0"),
|
|
17
|
+
import("https://esm.sh/@xterm/addon-fit@0.10.0"),
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
20
|
+
return xtermModulesPromise;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const ensureXtermStylesheet = () => {
|
|
24
|
+
if (typeof document === "undefined") return;
|
|
25
|
+
if (document.getElementById("ac-xterm-css")) return;
|
|
26
|
+
const link = document.createElement("link");
|
|
27
|
+
link.id = "ac-xterm-css";
|
|
28
|
+
link.rel = "stylesheet";
|
|
29
|
+
link.href = kXtermCssUrl;
|
|
30
|
+
document.head.appendChild(link);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const fitTerminalWhenVisible = ({
|
|
34
|
+
panel = null,
|
|
35
|
+
fitAddon = null,
|
|
36
|
+
minWidthPx = 120,
|
|
37
|
+
minHeightPx = 80,
|
|
38
|
+
} = {}) => {
|
|
39
|
+
if (!panel || !fitAddon) return false;
|
|
40
|
+
const panelWidth = Number(panel.clientWidth || 0);
|
|
41
|
+
const panelHeight = Number(panel.clientHeight || 0);
|
|
42
|
+
if (panelWidth < minWidthPx || panelHeight < minHeightPx) return false;
|
|
43
|
+
fitAddon.fit();
|
|
44
|
+
return true;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const normalizeWatchdogConsoleTab = (value) =>
|
|
48
|
+
value === kWatchdogConsoleTabTerminal
|
|
49
|
+
? kWatchdogConsoleTabTerminal
|
|
50
|
+
: kWatchdogConsoleTabLogs;
|
|
51
|
+
|
|
52
|
+
export const clampWatchdogLogsPanelHeight = (value) => {
|
|
53
|
+
const parsed = Number(value);
|
|
54
|
+
const normalized = Number.isFinite(parsed)
|
|
55
|
+
? Math.round(parsed)
|
|
56
|
+
: kWatchdogLogsPanelDefaultHeightPx;
|
|
57
|
+
return Math.max(kWatchdogLogsPanelMinHeightPx, normalized);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const readCssHeightPx = (element) => {
|
|
61
|
+
if (!element) return 0;
|
|
62
|
+
const computedHeight = Number.parseFloat(
|
|
63
|
+
window.getComputedStyle(element).height || "0",
|
|
64
|
+
);
|
|
65
|
+
return Number.isFinite(computedHeight) ? computedHeight : 0;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const formatBytes = (bytes) => {
|
|
69
|
+
if (bytes == null) return "—";
|
|
70
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
71
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
72
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
73
|
+
return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
|
|
74
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const getIncidentStatusTone = (event) => {
|
|
78
|
+
const eventType = String(event?.eventType || "")
|
|
79
|
+
.trim()
|
|
80
|
+
.toLowerCase();
|
|
81
|
+
const status = String(event?.status || "")
|
|
82
|
+
.trim()
|
|
83
|
+
.toLowerCase();
|
|
84
|
+
if (status === "failed") {
|
|
85
|
+
return {
|
|
86
|
+
dotClass: "bg-red-500/90",
|
|
87
|
+
label: "Failed",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (status === "ok" && eventType === "health_check") {
|
|
91
|
+
return {
|
|
92
|
+
dotClass: "bg-green-500/90",
|
|
93
|
+
label: "Healthy",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (status === "warn" || status === "warning") {
|
|
97
|
+
return {
|
|
98
|
+
dotClass: "bg-yellow-400/90",
|
|
99
|
+
label: "Warning",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
dotClass: "bg-gray-500/70",
|
|
104
|
+
label: "Unknown",
|
|
105
|
+
};
|
|
106
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { getIncidentStatusTone } from "../helpers.js";
|
|
4
|
+
|
|
5
|
+
const html = htm.bind(h);
|
|
6
|
+
|
|
7
|
+
export const WatchdogIncidentsCard = ({
|
|
8
|
+
events = [],
|
|
9
|
+
onRefresh = () => {},
|
|
10
|
+
}) => html`
|
|
11
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
12
|
+
<div class="flex items-center justify-between gap-2 mb-3">
|
|
13
|
+
<h2 class="card-label">Recent incidents</h2>
|
|
14
|
+
<button class="text-xs text-gray-400 hover:text-gray-200" onclick=${onRefresh}>
|
|
15
|
+
Refresh
|
|
16
|
+
</button>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="ac-history-list">
|
|
19
|
+
${events.length === 0 &&
|
|
20
|
+
html`<p class="text-xs text-gray-500">No incidents recorded.</p>`}
|
|
21
|
+
${events.map((event) => {
|
|
22
|
+
const tone = getIncidentStatusTone(event);
|
|
23
|
+
return html`
|
|
24
|
+
<details class="ac-history-item">
|
|
25
|
+
<summary class="ac-history-summary">
|
|
26
|
+
<div class="ac-history-summary-row">
|
|
27
|
+
<span class="inline-flex items-center gap-2 min-w-0">
|
|
28
|
+
<span class="ac-history-toggle shrink-0" aria-hidden="true"
|
|
29
|
+
>▸</span
|
|
30
|
+
>
|
|
31
|
+
<span class="truncate">
|
|
32
|
+
${event.createdAt || ""} · ${event.eventType || "event"} ·
|
|
33
|
+
${event.status || "unknown"}
|
|
34
|
+
</span>
|
|
35
|
+
</span>
|
|
36
|
+
<span
|
|
37
|
+
class=${`h-2.5 w-2.5 shrink-0 rounded-full ${tone.dotClass}`}
|
|
38
|
+
title=${tone.label}
|
|
39
|
+
aria-label=${tone.label}
|
|
40
|
+
></span>
|
|
41
|
+
</div>
|
|
42
|
+
</summary>
|
|
43
|
+
<div class="ac-history-body text-xs text-gray-400">
|
|
44
|
+
<div>Source: ${event.source || "unknown"}</div>
|
|
45
|
+
<pre class="mt-2 bg-black/30 rounded p-2 whitespace-pre-wrap break-words">
|
|
46
|
+
${typeof event.details === "string"
|
|
47
|
+
? event.details
|
|
48
|
+
: JSON.stringify(event.details || {}, null, 2)}</pre
|
|
49
|
+
>
|
|
50
|
+
</div>
|
|
51
|
+
</details>
|
|
52
|
+
`;
|
|
53
|
+
})}
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
`;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import { usePolling } from "../../../hooks/usePolling.js";
|
|
3
|
+
import { fetchWatchdogEvents } from "../../../lib/api.js";
|
|
4
|
+
|
|
5
|
+
export const useWatchdogIncidents = ({
|
|
6
|
+
restartSignal = 0,
|
|
7
|
+
onRefreshStatuses = () => {},
|
|
8
|
+
} = {}) => {
|
|
9
|
+
const eventsPoll = usePolling(() => fetchWatchdogEvents(20), 15000);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!restartSignal) return;
|
|
13
|
+
onRefreshStatuses();
|
|
14
|
+
eventsPoll.refresh();
|
|
15
|
+
const t1 = setTimeout(() => {
|
|
16
|
+
onRefreshStatuses();
|
|
17
|
+
eventsPoll.refresh();
|
|
18
|
+
}, 1200);
|
|
19
|
+
const t2 = setTimeout(() => {
|
|
20
|
+
onRefreshStatuses();
|
|
21
|
+
eventsPoll.refresh();
|
|
22
|
+
}, 3500);
|
|
23
|
+
return () => {
|
|
24
|
+
clearTimeout(t1);
|
|
25
|
+
clearTimeout(t2);
|
|
26
|
+
};
|
|
27
|
+
}, [restartSignal, onRefreshStatuses, eventsPoll.refresh]);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
events: eventsPoll.data?.events || [],
|
|
31
|
+
refreshEvents: eventsPoll.refresh,
|
|
32
|
+
};
|
|
33
|
+
};
|