@steadwing/openalerts 0.2.2 → 0.2.3
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/README.md +41 -59
- package/dist/core/llm-enrichment.d.ts +8 -1
- package/dist/core/llm-enrichment.js +13 -7
- package/dist/index.js +2 -2
- package/dist/plugin/adapter.js +13 -2
- package/dist/plugin/dashboard-html.js +97 -90
- package/dist/plugin/dashboard-routes.js +16 -3
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<p align="center">
|
|
15
15
|
<a href="#quickstart">Quickstart</a> ·
|
|
16
16
|
<a href="#alert-rules">Alert Rules</a> ·
|
|
17
|
-
<a href="#
|
|
17
|
+
<a href="#llm-enriched-alerts">LLM Enrichment</a> ·
|
|
18
18
|
<a href="#dashboard">Dashboard</a> ·
|
|
19
19
|
<a href="#commands">Commands</a>
|
|
20
20
|
</p>
|
|
@@ -84,38 +84,24 @@ http://127.0.0.1:18789/openalerts
|
|
|
84
84
|
|
|
85
85
|
## Alert Rules
|
|
86
86
|
|
|
87
|
-
Eight rules run against every event in real-time
|
|
87
|
+
Eight rules run against every event in real-time. All thresholds and cooldowns are configurable.
|
|
88
88
|
|
|
89
|
-
| Rule | Watches for | Severity |
|
|
90
|
-
|
|
91
|
-
|
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
|
|
|
96
|
-
|
|
|
97
|
-
|
|
|
98
|
-
|
|
|
89
|
+
| Rule | Watches for | Severity | Threshold (default) |
|
|
90
|
+
|---|---|---|---|
|
|
91
|
+
| `llm-errors` | LLM/agent failures in 1 min window | ERROR | `1` error |
|
|
92
|
+
| `infra-errors` | Infrastructure errors in 1 min window | ERROR | `1` error |
|
|
93
|
+
| `gateway-down` | No heartbeat received | CRITICAL | `30000` ms (30s) |
|
|
94
|
+
| `session-stuck` | Session idle too long | WARN | `120000` ms (2 min) |
|
|
95
|
+
| `high-error-rate` | Message failure rate over last 20 | ERROR | `50`% |
|
|
96
|
+
| `queue-depth` | Queued items piling up | WARN | `10` items |
|
|
97
|
+
| `tool-errors` | Tool failures in 1 min window | WARN | `1` error |
|
|
98
|
+
| `heartbeat-fail` | Consecutive heartbeat failures | ERROR | `3` failures |
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
By default, OpenAlerts uses your configured LLM model to enrich alerts with a human-friendly summary and an actionable suggestion. The enrichment is appended below the original alert detail:
|
|
105
|
-
|
|
106
|
-
```
|
|
107
|
-
1 agent error(s) on unknown in the last minute. Last: 401 Incorrect API key...
|
|
108
|
-
|
|
109
|
-
Summary: Your OpenAI API key is invalid or expired — the agent cannot make LLM calls.
|
|
110
|
-
Action: Update your API key in ~/.openclaw/.env with a valid key from platform.openai.com/api-keys
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
- **Model**: reads from `agents.defaults.model.primary` in your `openclaw.json` (e.g. `"openai/gpt-4o-mini"`)
|
|
114
|
-
- **API key**: reads from the corresponding environment variable (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GROQ_API_KEY`, etc.)
|
|
115
|
-
- **Supported providers**: OpenAI, Anthropic, Groq, Together, DeepSeek (and any OpenAI-compatible API)
|
|
116
|
-
- **Graceful fallback**: if the LLM call fails or times out (10s), the original alert is sent unchanged
|
|
100
|
+
Every rule also accepts:
|
|
101
|
+
- **`enabled`** — `false` to disable the rule (default: `true`)
|
|
102
|
+
- **`cooldownMinutes`** — minutes before the same rule can fire again (default: `15`)
|
|
117
103
|
|
|
118
|
-
To
|
|
104
|
+
To tune rules, add a `rules` object in your plugin config:
|
|
119
105
|
|
|
120
106
|
```jsonc
|
|
121
107
|
{
|
|
@@ -123,7 +109,13 @@ To disable LLM enrichment, set `"llmEnriched": false` in your plugin config:
|
|
|
123
109
|
"entries": {
|
|
124
110
|
"openalerts": {
|
|
125
111
|
"config": {
|
|
126
|
-
"
|
|
112
|
+
"cooldownMinutes": 10,
|
|
113
|
+
"rules": {
|
|
114
|
+
"llm-errors": { "threshold": 5 },
|
|
115
|
+
"infra-errors": { "cooldownMinutes": 30 },
|
|
116
|
+
"high-error-rate": { "enabled": false },
|
|
117
|
+
"gateway-down": { "threshold": 60000 }
|
|
118
|
+
}
|
|
127
119
|
}
|
|
128
120
|
}
|
|
129
121
|
}
|
|
@@ -131,25 +123,19 @@ To disable LLM enrichment, set `"llmEnriched": false` in your plugin config:
|
|
|
131
123
|
}
|
|
132
124
|
```
|
|
133
125
|
|
|
134
|
-
|
|
126
|
+
Set `"quiet": true` at the config level for log-only mode (no messages sent).
|
|
135
127
|
|
|
136
|
-
|
|
128
|
+
## LLM-Enriched Alerts
|
|
137
129
|
|
|
138
|
-
|
|
130
|
+
OpenAlerts can optionally use your configured LLM to enrich alerts with a human-friendly summary and an actionable suggestion. **This feature is disabled by default** — opt in by setting `"llmEnriched": true` in your plugin config:
|
|
139
131
|
|
|
140
132
|
```jsonc
|
|
141
133
|
{
|
|
142
134
|
"plugins": {
|
|
143
135
|
"entries": {
|
|
144
136
|
"openalerts": {
|
|
145
|
-
"enabled": true,
|
|
146
137
|
"config": {
|
|
147
|
-
"
|
|
148
|
-
"llm-errors": { "threshold": 5 },
|
|
149
|
-
"infra-errors": { "cooldownMinutes": 30 },
|
|
150
|
-
"high-error-rate": { "enabled": false },
|
|
151
|
-
"gateway-down": { "threshold": 60000 }
|
|
152
|
-
}
|
|
138
|
+
"llmEnriched": true
|
|
153
139
|
}
|
|
154
140
|
}
|
|
155
141
|
}
|
|
@@ -157,28 +143,19 @@ Each rule can be individually tuned or disabled. You can also set global options
|
|
|
157
143
|
}
|
|
158
144
|
```
|
|
159
145
|
|
|
160
|
-
|
|
146
|
+
When enabled, alerts include an LLM-generated summary and action:
|
|
161
147
|
|
|
162
|
-
```bash
|
|
163
|
-
openclaw gateway stop && openclaw gateway run
|
|
164
148
|
```
|
|
149
|
+
1 agent error(s) on unknown in the last minute. Last: 401 Incorrect API key...
|
|
165
150
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|---|---|---|
|
|
170
|
-
| `llm-errors` | Error count in 1 min window | `1` |
|
|
171
|
-
| `infra-errors` | Error count in 1 min window | `1` |
|
|
172
|
-
| `gateway-down` | Milliseconds without heartbeat | `30000` (30s) |
|
|
173
|
-
| `session-stuck` | Milliseconds idle | `120000` (2 min) |
|
|
174
|
-
| `high-error-rate` | Error percentage (0-100) | `50` |
|
|
175
|
-
| `queue-depth` | Number of queued items | `10` |
|
|
176
|
-
| `tool-errors` | Error count in 1 min window | `1` |
|
|
177
|
-
| `heartbeat-fail` | Consecutive failures | `3` |
|
|
151
|
+
Summary: Your OpenAI API key is invalid or expired — the agent cannot make LLM calls.
|
|
152
|
+
Action: Update your API key in ~/.openclaw/.env with a valid key from platform.openai.com/api-keys
|
|
153
|
+
```
|
|
178
154
|
|
|
179
|
-
|
|
180
|
-
-
|
|
181
|
-
-
|
|
155
|
+
- **Model**: reads from `agents.defaults.model.primary` in your `openclaw.json` (e.g. `"openai/gpt-4o-mini"`)
|
|
156
|
+
- **API key**: reads from the corresponding environment variable (`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GROQ_API_KEY`, etc.)
|
|
157
|
+
- **Supported providers**: OpenAI, Anthropic, Groq, Together, DeepSeek (and any OpenAI-compatible API)
|
|
158
|
+
- **Graceful fallback**: if the LLM call fails or times out (10s), the original alert is sent unchanged
|
|
182
159
|
|
|
183
160
|
## Commands
|
|
184
161
|
|
|
@@ -190,6 +167,11 @@ Zero-token chat commands available in any connected channel:
|
|
|
190
167
|
| `/alerts` | Recent alert history with severity and timestamps |
|
|
191
168
|
| `/dashboard` | Returns the dashboard URL |
|
|
192
169
|
|
|
170
|
+
## Roadmap
|
|
171
|
+
|
|
172
|
+
- [ ] [nanobot](https://github.com/HKUDS/nanobot) adapter
|
|
173
|
+
- [ ] [OpenManus](https://github.com/FoundationAgents/OpenManus) adapter
|
|
174
|
+
|
|
193
175
|
## Development
|
|
194
176
|
|
|
195
177
|
```bash
|
|
@@ -2,13 +2,20 @@ import type { AlertEnricher, OpenAlertsLogger } from "./types.js";
|
|
|
2
2
|
export type LlmEnricherOptions = {
|
|
3
3
|
/** Model string from config, e.g. "openai/gpt-5-nano" */
|
|
4
4
|
modelString: string;
|
|
5
|
+
/** Pre-resolved API key (caller reads from env to avoid env+fetch in same file) */
|
|
6
|
+
apiKey: string;
|
|
5
7
|
/** Logger for debug/warn messages */
|
|
6
8
|
logger?: OpenAlertsLogger;
|
|
7
9
|
/** Timeout in ms (default: 10000) */
|
|
8
10
|
timeoutMs?: number;
|
|
9
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the environment variable name for a given model string's provider.
|
|
14
|
+
* Returns null if the model string is invalid or the provider is unknown.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveApiKeyEnvVar(modelString: string): string | null;
|
|
10
17
|
/**
|
|
11
18
|
* Create an AlertEnricher that calls an LLM to add a summary + action to alerts.
|
|
12
|
-
* Returns null if provider
|
|
19
|
+
* Returns null if provider can't be resolved.
|
|
13
20
|
*/
|
|
14
21
|
export declare function createLlmEnricher(opts: LlmEnricherOptions): AlertEnricher | null;
|
|
@@ -122,12 +122,23 @@ async function callAnthropic(baseUrl, apiKey, model, prompt, timeoutMs) {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
// ─── Factory ────────────────────────────────────────────────────────────────
|
|
125
|
+
/**
|
|
126
|
+
* Resolve the environment variable name for a given model string's provider.
|
|
127
|
+
* Returns null if the model string is invalid or the provider is unknown.
|
|
128
|
+
*/
|
|
129
|
+
export function resolveApiKeyEnvVar(modelString) {
|
|
130
|
+
const slashIdx = modelString.indexOf("/");
|
|
131
|
+
if (slashIdx < 1)
|
|
132
|
+
return null;
|
|
133
|
+
const providerKey = modelString.slice(0, slashIdx).toLowerCase();
|
|
134
|
+
return PROVIDER_MAP[providerKey]?.apiKeyEnvVar ?? null;
|
|
135
|
+
}
|
|
125
136
|
/**
|
|
126
137
|
* Create an AlertEnricher that calls an LLM to add a summary + action to alerts.
|
|
127
|
-
* Returns null if provider
|
|
138
|
+
* Returns null if provider can't be resolved.
|
|
128
139
|
*/
|
|
129
140
|
export function createLlmEnricher(opts) {
|
|
130
|
-
const { modelString, logger, timeoutMs = 10_000 } = opts;
|
|
141
|
+
const { modelString, apiKey, logger, timeoutMs = 10_000 } = opts;
|
|
131
142
|
// Parse "provider/model-name" format
|
|
132
143
|
const slashIdx = modelString.indexOf("/");
|
|
133
144
|
if (slashIdx < 1) {
|
|
@@ -141,11 +152,6 @@ export function createLlmEnricher(opts) {
|
|
|
141
152
|
logger?.warn(`openalerts: llm-enrichment skipped — unknown provider "${providerKey}"`);
|
|
142
153
|
return null;
|
|
143
154
|
}
|
|
144
|
-
const apiKey = process.env[providerConfig.apiKeyEnvVar];
|
|
145
|
-
if (!apiKey) {
|
|
146
|
-
logger?.warn(`openalerts: llm-enrichment skipped — ${providerConfig.apiKeyEnvVar} not set in environment`);
|
|
147
|
-
return null;
|
|
148
|
-
}
|
|
149
155
|
logger?.info(`openalerts: llm-enrichment enabled (${providerKey}/${model})`);
|
|
150
156
|
return async (alert) => {
|
|
151
157
|
const prompt = buildPrompt(alert);
|
package/dist/index.js
CHANGED
|
@@ -19,8 +19,8 @@ function createMonitorService(api) {
|
|
|
19
19
|
// Resolve alert target + create OpenClaw alert channel
|
|
20
20
|
const target = await resolveAlertTarget(api, config);
|
|
21
21
|
const channels = target ? [new OpenClawAlertChannel(api, target)] : [];
|
|
22
|
-
// Create LLM enricher if enabled (default:
|
|
23
|
-
const enricher = config.llmEnriched
|
|
22
|
+
// Create LLM enricher if enabled (default: false)
|
|
23
|
+
const enricher = config.llmEnriched === true
|
|
24
24
|
? createOpenClawEnricher(api, logger)
|
|
25
25
|
: null;
|
|
26
26
|
// Create and start the universal engine
|
package/dist/plugin/adapter.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createLlmEnricher } from "../core/llm-enrichment.js";
|
|
1
|
+
import { createLlmEnricher, resolveApiKeyEnvVar } from "../core/llm-enrichment.js";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
@@ -510,7 +510,18 @@ export function createOpenClawEnricher(api, logger) {
|
|
|
510
510
|
logger?.warn("openalerts: llm-enrichment skipped — no model configured at agents.defaults.model.primary");
|
|
511
511
|
return null;
|
|
512
512
|
}
|
|
513
|
-
|
|
513
|
+
// Resolve API key here (in adapter) to keep env access separate from network calls
|
|
514
|
+
const envVar = resolveApiKeyEnvVar(primary);
|
|
515
|
+
if (!envVar) {
|
|
516
|
+
logger?.warn("openalerts: llm-enrichment skipped — unknown provider");
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
const apiKey = process.env[envVar];
|
|
520
|
+
if (!apiKey) {
|
|
521
|
+
logger?.warn(`openalerts: llm-enrichment skipped — ${envVar} not set in environment`);
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
return createLlmEnricher({ modelString: primary, apiKey, logger });
|
|
514
525
|
}
|
|
515
526
|
catch (err) {
|
|
516
527
|
logger?.warn(`openalerts: llm-enrichment setup failed: ${String(err)}`);
|
|
@@ -13,22 +13,22 @@ export function getDashboardHtml() {
|
|
|
13
13
|
<title>OpenAlerts Monitor</title>
|
|
14
14
|
<style>
|
|
15
15
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
16
|
-
body{font-family:'SF Mono','Cascadia Code','Consolas',monospace;background:#0d1117;color:#c9d1d9;font-size:
|
|
16
|
+
body{font-family:'SF Mono','Cascadia Code','Consolas',monospace;background:#0d1117;color:#c9d1d9;font-size:14px;overflow:hidden;height:100vh}
|
|
17
17
|
.grid{display:grid;grid-template-rows:auto auto 1fr;height:100vh}
|
|
18
18
|
|
|
19
19
|
/* ── Top bar ──────────────────── */
|
|
20
20
|
.topbar{background:#161b22;border-bottom:1px solid #30363d;padding:8px 16px;display:flex;align-items:center;gap:16px;flex-wrap:wrap}
|
|
21
|
-
.topbar h1{font-size:
|
|
21
|
+
.topbar h1{font-size:16px;font-weight:600;color:#f0f6fc;letter-spacing:0.5px}
|
|
22
22
|
.dot{width:7px;height:7px;border-radius:50%;display:inline-block;margin-right:4px}
|
|
23
23
|
.dot.live{background:#3fb950;animation:pulse 2s infinite}
|
|
24
24
|
.dot.dead{background:#f85149}
|
|
25
25
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}
|
|
26
|
-
.stat{color:#8b949e;font-size:
|
|
26
|
+
.stat{color:#8b949e;font-size:12px}
|
|
27
27
|
.stat b{color:#c9d1d9;font-weight:500}
|
|
28
28
|
|
|
29
29
|
/* ── Tabs ──────────────────── */
|
|
30
30
|
.tabbar{background:#161b22;border-bottom:1px solid #30363d;display:flex}
|
|
31
|
-
.tab{padding:7px 18px;font-size:
|
|
31
|
+
.tab{padding:7px 18px;font-size:13px;font-weight:600;color:#8b949e;cursor:pointer;border-bottom:2px solid transparent;transition:all 0.15s}
|
|
32
32
|
.tab:hover{color:#c9d1d9;background:#1c2129}
|
|
33
33
|
.tab.active{color:#58a6ff;border-bottom-color:#58a6ff}
|
|
34
34
|
.tab-content{display:none;overflow:hidden;flex:1}
|
|
@@ -39,7 +39,7 @@ export function getDashboardHtml() {
|
|
|
39
39
|
.activity-panels{display:grid;grid-template-columns:1fr 280px;gap:0;overflow:hidden;flex:1}
|
|
40
40
|
@media(max-width:900px){.activity-panels{grid-template-columns:1fr}}
|
|
41
41
|
.panel{display:flex;flex-direction:column;overflow:hidden}
|
|
42
|
-
.panel-header{background:#161b22;padding:6px 12px;font-size:
|
|
42
|
+
.panel-header{background:#161b22;padding:6px 12px;font-size:12px;font-weight:600;color:#8b949e;text-transform:uppercase;letter-spacing:0.8px;border-bottom:1px solid #30363d;flex-shrink:0;display:flex;align-items:center;justify-content:space-between}
|
|
43
43
|
.panel:first-child{border-right:1px solid #30363d}
|
|
44
44
|
.scroll{flex:1;overflow-y:auto}
|
|
45
45
|
.scroll::-webkit-scrollbar{width:5px}
|
|
@@ -65,16 +65,16 @@ export function getDashboardHtml() {
|
|
|
65
65
|
.flow-body.shut{max-height:0!important;overflow:hidden}
|
|
66
66
|
|
|
67
67
|
/* ── Event/Log row ──────────────────── */
|
|
68
|
-
.row{padding:3px 10px 3px 24px;border-top:1px solid #0d1117;font-size:
|
|
68
|
+
.row{padding:3px 10px 3px 24px;border-top:1px solid #0d1117;font-size:12px;line-height:1.5;animation:fi 0.15s ease}
|
|
69
69
|
.row:hover{background:#0d1117}
|
|
70
70
|
.row.standalone{padding-left:10px;border-bottom:1px solid #21262d;border-top:none}
|
|
71
71
|
.row.deep{padding-left:38px}
|
|
72
72
|
|
|
73
73
|
/* OpenAlerts event row */
|
|
74
74
|
.row .r-main{display:flex;align-items:center;gap:5px}
|
|
75
|
-
.r-time{color:#484f58;font-size:
|
|
76
|
-
.r-icon{width:14px;text-align:center;flex-shrink:0;font-size:
|
|
77
|
-
.r-type{font-weight:600;min-width:
|
|
75
|
+
.r-time{color:#484f58;font-size:11px;min-width:55px;flex-shrink:0}
|
|
76
|
+
.r-icon{width:14px;text-align:center;flex-shrink:0;font-size:12px}
|
|
77
|
+
.r-type{font-weight:600;min-width:110px;font-size:11px;flex-shrink:0}
|
|
78
78
|
.r-type.llm{color:#58a6ff} .r-type.tool{color:#bc8cff} .r-type.agent{color:#3fb950}
|
|
79
79
|
.r-type.session{color:#d29922} .r-type.infra{color:#f85149} .r-type.custom{color:#8b949e}
|
|
80
80
|
.r-type.watchdog{color:#6e7681}
|
|
@@ -83,10 +83,10 @@ export function getDashboardHtml() {
|
|
|
83
83
|
.r-oc.error{background:#3d1a1a;color:#f85149}
|
|
84
84
|
.r-oc.timeout{background:#3d2e1a;color:#d29922}
|
|
85
85
|
.r-pills{display:flex;gap:4px;flex-wrap:wrap;margin-left:auto;align-items:center}
|
|
86
|
-
.p{font-size:
|
|
86
|
+
.p{font-size:10px;background:#21262d;padding:0 5px;border-radius:3px;white-space:nowrap}
|
|
87
87
|
.p.t{color:#bc8cff;background:#2a1f3d} .p.d{color:#d29922} .p.tk{color:#58a6ff}
|
|
88
88
|
.p.q{color:#f0883e} .p.m{color:#8b949e} .p.ch{color:#d2a8ff} .p.s{color:#6e7681;font-size:8px}
|
|
89
|
-
.r-det{padding:1px 0 1px 70px;color:#6e7681;font-size:
|
|
89
|
+
.r-det{padding:1px 0 1px 70px;color:#6e7681;font-size:11px}
|
|
90
90
|
.r-det .err{color:#f85149} .r-det .dim{color:#484f58} .r-det .sc{color:#d29922}
|
|
91
91
|
|
|
92
92
|
/* OpenClaw log row */
|
|
@@ -101,25 +101,25 @@ export function getDashboardHtml() {
|
|
|
101
101
|
@keyframes fi{from{opacity:0;transform:translateY(-2px)}to{opacity:1;transform:none}}
|
|
102
102
|
|
|
103
103
|
/* ── Alerts panel ──────────────────── */
|
|
104
|
-
.al{padding:6px 10px;border-bottom:1px solid #21262d;font-size:
|
|
105
|
-
.al-sev{font-weight:700;text-transform:uppercase;font-size:
|
|
104
|
+
.al{padding:6px 10px;border-bottom:1px solid #21262d;font-size:12px;animation:fi 0.2s}
|
|
105
|
+
.al-sev{font-weight:700;text-transform:uppercase;font-size:10px;letter-spacing:0.4px}
|
|
106
106
|
.al-sev.error{color:#f85149} .al-sev.warn{color:#d29922} .al-sev.critical{color:#ff7b72} .al-sev.info{color:#58a6ff}
|
|
107
|
-
.al-title{color:#c9d1d9;margin-top:1px;font-size:
|
|
108
|
-
.al-detail{color:#8b949e;margin-top:1px;font-size:
|
|
109
|
-
.al-time{color:#484f58;font-size:
|
|
107
|
+
.al-title{color:#c9d1d9;margin-top:1px;font-size:12px}
|
|
108
|
+
.al-detail{color:#8b949e;margin-top:1px;font-size:11px}
|
|
109
|
+
.al-time{color:#484f58;font-size:11px;margin-top:1px}
|
|
110
110
|
|
|
111
111
|
/* ── Rules ──────────────────── */
|
|
112
112
|
.rules{border-top:1px solid #30363d;padding:6px 10px;flex-shrink:0}
|
|
113
|
-
.rules h3{font-size:
|
|
114
|
-
.rl{display:flex;align-items:center;gap:5px;font-size:
|
|
113
|
+
.rules h3{font-size:12px;color:#8b949e;text-transform:uppercase;letter-spacing:0.7px;margin-bottom:4px}
|
|
114
|
+
.rl{display:flex;align-items:center;gap:5px;font-size:12px;padding:1px 0}
|
|
115
115
|
.rl-d{width:5px;height:5px;border-radius:50%;flex-shrink:0}
|
|
116
116
|
.rl-d.ok{background:#3fb950} .rl-d.fired{background:#f85149;animation:pulse 1s infinite}
|
|
117
|
-
.rl-s{color:#8b949e;margin-left:auto;font-size:
|
|
117
|
+
.rl-s{color:#8b949e;margin-left:auto;font-size:11px}
|
|
118
118
|
|
|
119
119
|
/* ── Logs tab ──────────────────── */
|
|
120
120
|
.logs-t{flex:1;display:flex;flex-direction:column;overflow:hidden}
|
|
121
|
-
.log-bar{background:#161b22;padding:8px 12px;border-bottom:1px solid #30363d;display:flex;align-items:center;gap:10px;flex-wrap:wrap;flex-shrink:0;font-size:
|
|
122
|
-
.log-bar select,.log-bar input{background:#0d1117;border:1px solid #30363d;color:#c9d1d9;font-family:inherit;font-size:
|
|
121
|
+
.log-bar{background:#161b22;padding:8px 12px;border-bottom:1px solid #30363d;display:flex;align-items:center;gap:10px;flex-wrap:wrap;flex-shrink:0;font-size:12px}
|
|
122
|
+
.log-bar select,.log-bar input{background:#0d1117;border:1px solid #30363d;color:#c9d1d9;font-family:inherit;font-size:11px;padding:3px 6px;border-radius:3px}
|
|
123
123
|
.log-bar input[type="text"]{min-width:180px}
|
|
124
124
|
.log-bar button{background:#21262d;border:1px solid #30363d;color:#c9d1d9;font-family:inherit;font-size:10px;padding:3px 10px;border-radius:3px;cursor:pointer;transition:all 0.12s}
|
|
125
125
|
.log-bar button:hover{background:#30363d;border-color:#484f58}
|
|
@@ -131,31 +131,31 @@ export function getDashboardHtml() {
|
|
|
131
131
|
.log-filters label{font-size:10px}
|
|
132
132
|
.log-truncate{background:#3d2e1a;border:1px solid #d29922;color:#d29922;padding:4px 10px;font-size:10px;border-radius:3px;margin:8px 12px;display:none}
|
|
133
133
|
.log-truncate.show{display:block}
|
|
134
|
-
.log-list{flex:1;overflow-y:auto;font-size:
|
|
134
|
+
.log-list{flex:1;overflow-y:auto;font-size:12px}
|
|
135
135
|
.log-e{padding:2px 12px;border-bottom:1px solid #161b22;display:flex;gap:6px;align-items:baseline;position:relative}
|
|
136
136
|
.log-e:hover{background:#161b22}
|
|
137
137
|
.log-e:hover .log-copy{opacity:1}
|
|
138
|
-
.log-ts{color:#484f58;font-size:
|
|
139
|
-
.log-lv{font-size:
|
|
138
|
+
.log-ts{color:#484f58;font-size:11px;min-width:70px;flex-shrink:0}
|
|
139
|
+
.log-lv{font-size:10px;font-weight:700;min-width:42px;flex-shrink:0}
|
|
140
140
|
.log-lv.TRACE{color:#6e7681} .log-lv.DEBUG{color:#8b949e} .log-lv.INFO{color:#58a6ff} .log-lv.WARN{color:#d29922} .log-lv.ERROR{color:#f85149} .log-lv.FATAL{color:#ff7b72;background:#3d1a1a;padding:1px 3px;border-radius:2px}
|
|
141
|
-
.log-su{color:#bc8cff;font-size:
|
|
142
|
-
.log-mg{color:#c9d1d9;word-break:break-all;flex:1}
|
|
141
|
+
.log-su{color:#bc8cff;font-size:11px;min-width:110px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
142
|
+
.log-mg{color:#c9d1d9;font-size:12px;word-break:break-all;flex:1}
|
|
143
143
|
.log-copy{position:absolute;right:8px;top:4px;font-size:9px;color:#484f58;cursor:pointer;border:1px solid #30363d;background:#161b22;padding:1px 4px;border-radius:2px;font-family:inherit;opacity:0;transition:opacity 0.12s}
|
|
144
144
|
.log-copy:hover{color:#c9d1d9;border-color:#484f58}
|
|
145
145
|
|
|
146
146
|
/* ── Health tab ──────────────────── */
|
|
147
147
|
.health-t{flex:1;overflow-y:auto;padding:14px}
|
|
148
148
|
.h-sec{margin-bottom:16px}
|
|
149
|
-
.h-sec h3{font-size:
|
|
149
|
+
.h-sec h3{font-size:13px;color:#8b949e;text-transform:uppercase;letter-spacing:0.7px;margin-bottom:6px;padding-bottom:3px;border-bottom:1px solid #21262d}
|
|
150
150
|
.h-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:6px}
|
|
151
151
|
.h-card{background:#161b22;border:1px solid #21262d;border-radius:5px;padding:8px 12px}
|
|
152
|
-
.h-card .lb{color:#8b949e;font-size:
|
|
153
|
-
.h-card .vl{color:#c9d1d9;font-size:
|
|
152
|
+
.h-card .lb{color:#8b949e;font-size:11px;margin-bottom:2px}
|
|
153
|
+
.h-card .vl{color:#c9d1d9;font-size:15px;font-weight:600}
|
|
154
154
|
.h-card .vl.ok{color:#3fb950} .h-card .vl.bad{color:#f85149}
|
|
155
155
|
.h-tbl{width:100%;border-collapse:collapse}
|
|
156
|
-
.h-tbl td{padding:
|
|
156
|
+
.h-tbl td{padding:4px 8px;font-size:13px;border-bottom:1px solid #161b22}
|
|
157
157
|
.h-tbl td:first-child{color:#8b949e;width:140px}
|
|
158
|
-
.h-tbl th{padding:
|
|
158
|
+
.h-tbl th{padding:4px 8px;font-size:12px;color:#8b949e;text-align:left;border-bottom:1px solid #30363d;font-weight:600}
|
|
159
159
|
|
|
160
160
|
/* ── Expandable event detail ──────────────────── */
|
|
161
161
|
.row.expandable{cursor:pointer}
|
|
@@ -245,9 +245,10 @@ export function getDashboardHtml() {
|
|
|
245
245
|
</div>
|
|
246
246
|
<button id="lR" title="Refresh logs (Ctrl+R)">↻ Refresh</button>
|
|
247
247
|
<button id="lE" title="Export filtered logs">⬇ Export</button>
|
|
248
|
+
<label title="Fetch all logs (not just recent) when searching or filtering"><input type="checkbox" id="lAll"> All logs</label>
|
|
248
249
|
<label style="margin-left:auto" title="Auto-scroll to new logs"><input type="checkbox" id="lA" checked> Auto-follow</label>
|
|
249
250
|
</div>
|
|
250
|
-
<div class="log-truncate" id="lT">⚠ Logs truncated
|
|
251
|
+
<div class="log-truncate" id="lT">⚠ Logs truncated — showing most recent entries only. Check "All logs" to search across the full log file.</div>
|
|
251
252
|
<div class="log-list" id="logList"><div class="empty-msg">Loading...</div></div>
|
|
252
253
|
</div>
|
|
253
254
|
</div>
|
|
@@ -271,14 +272,6 @@ export function getDashboardHtml() {
|
|
|
271
272
|
<h3>Alert Rules Status</h3>
|
|
272
273
|
<div id="dbRules"></div>
|
|
273
274
|
</div>
|
|
274
|
-
<div class="h-sec">
|
|
275
|
-
<h3>Circuit Breakers</h3>
|
|
276
|
-
<div id="dbCircuit" style="font-size:11px"></div>
|
|
277
|
-
</div>
|
|
278
|
-
<div class="h-sec">
|
|
279
|
-
<h3>Task Timeouts</h3>
|
|
280
|
-
<div id="dbTasks" style="font-size:11px"></div>
|
|
281
|
-
</div>
|
|
282
275
|
</div>
|
|
283
276
|
</div>
|
|
284
277
|
</div>
|
|
@@ -293,6 +286,30 @@ export function getDashboardHtml() {
|
|
|
293
286
|
function esc(s){var d=document.createElement('div');d.textContent=s;return d.innerHTML}
|
|
294
287
|
function cat(t){if(!t)return'custom';var p=t.split('.')[0];return['llm','tool','agent','session','infra','watchdog'].indexOf(p)>=0?p:'custom'}
|
|
295
288
|
function fT(ts){if(!ts)return'';var d=typeof ts==='number'?new Date(ts):new Date(ts);return d.toLocaleTimeString('en-US',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'})}
|
|
289
|
+
|
|
290
|
+
// ─── Human-readable event labels ──────────────────────
|
|
291
|
+
var EVT_LABELS={
|
|
292
|
+
'llm.call':['\\u{1F916}','LLM Called'],
|
|
293
|
+
'llm.error':['\\u26A0','LLM Failed'],
|
|
294
|
+
'llm.token_usage':['\\u{1F4CA}','Token Usage'],
|
|
295
|
+
'tool.call':['\\u{1F527}','Tool Executed'],
|
|
296
|
+
'tool.error':['\\u{1F527}','Tool Failed'],
|
|
297
|
+
'agent.start':['\\u25B6','Agent Started'],
|
|
298
|
+
'agent.end':['\\u23F9','Agent Finished'],
|
|
299
|
+
'agent.error':['\\u{1F6A8}','Agent Error'],
|
|
300
|
+
'agent.stuck':['\\u23F3','Agent Stuck'],
|
|
301
|
+
'session.start':['\\u{1F4AC}','Session Started'],
|
|
302
|
+
'session.end':['\\u{1F3C1}','Session Ended'],
|
|
303
|
+
'session.stuck':['\\u23F3','Session Stuck'],
|
|
304
|
+
'infra.error':['\\u{1F6A8}','Infra Error'],
|
|
305
|
+
'infra.heartbeat':['\\u2764','Heartbeat'],
|
|
306
|
+
'infra.queue_depth':['\\u{1F4E6}','Queue Depth'],
|
|
307
|
+
'watchdog.tick':['\\u{1F440}','Health Check'],
|
|
308
|
+
'msg.delivered':['\\u2709','Message Sent'],
|
|
309
|
+
'msg.in':['\\u{1F4E5}','Message Received'],
|
|
310
|
+
'msg.out':['\\u{1F4E4}','Message Sending']
|
|
311
|
+
};
|
|
312
|
+
function friendlyLabel(ft){return EVT_LABELS[ft]||['\\u2022',ft]}
|
|
296
313
|
function fISO(ts){if(!ts)return'';return new Date(typeof ts==='number'?ts:Date.parse(ts)).toISOString()}
|
|
297
314
|
function fD(ms){if(ms==null)return'';if(ms<1000)return ms+'ms';if(ms<60000)return(ms/1000).toFixed(1)+'s';return Math.floor(ms/60000)+'m '+Math.round((ms%60000)/1000)+'s'}
|
|
298
315
|
function fU(ms){var s=Math.floor(ms/1000),m=Math.floor(s/60),h=Math.floor(m/60);return h>0?h+'h '+m%60+'m':m+'m '+s%60+'s'}
|
|
@@ -317,7 +334,7 @@ export function getDashboardHtml() {
|
|
|
317
334
|
var c=document.createElement('div');c.className='flow active';
|
|
318
335
|
var hdr=document.createElement('div');hdr.className='flow-hdr';
|
|
319
336
|
var short=sid.length>20?sid.slice(0,8)+'..'+sid.slice(-4):sid;
|
|
320
|
-
hdr.innerHTML='<span class="flow-arr">\\u25BC</span><span class="flow-lbl" title="'+esc(sid)+'">Session '+esc(short)+'</span><span class="flow-badge active" data-r="st">
|
|
337
|
+
hdr.innerHTML='<span class="flow-arr">\\u25BC</span><span class="flow-lbl" title="'+esc(sid)+'">Session '+esc(short)+'</span><span class="flow-badge active" data-r="st">Running</span><span class="flow-info" data-r="info">'+fT(ev.ts||ev.tsMs)+'</span>';
|
|
321
338
|
var summary=document.createElement('div');summary.className='flow-summary';summary.setAttribute('data-r','summary');
|
|
322
339
|
var body=document.createElement('div');body.className='flow-body';
|
|
323
340
|
hdr.addEventListener('click',function(){
|
|
@@ -337,26 +354,27 @@ export function getDashboardHtml() {
|
|
|
337
354
|
function updFlow(f,st){
|
|
338
355
|
f.st=st;f.el.className='flow '+st;
|
|
339
356
|
var sEl=f.hdr.querySelector('[data-r="st"]');
|
|
340
|
-
|
|
357
|
+
var stLabels={active:'Running',done:'Completed',error:'Failed'};
|
|
358
|
+
if(sEl){sEl.className='flow-badge '+st;sEl.textContent=stLabels[st]||st}
|
|
341
359
|
var iEl=f.hdr.querySelector('[data-r="info"]');
|
|
342
360
|
if(iEl){
|
|
343
361
|
var ps=[f.n+' events'];
|
|
344
362
|
if(f.dur>0)ps.push(fD(f.dur));
|
|
345
|
-
if(f.tok>0)ps.push(f.tok+'
|
|
363
|
+
if(f.tok>0)ps.push(f.tok+' tokens');
|
|
346
364
|
if(f.cost>0)ps.push('$'+f.cost.toFixed(4));
|
|
347
|
-
if(f.tools>0)ps.push(f.tools+' tools');
|
|
348
|
-
if(f.llms>0)ps.push(f.llms+'
|
|
365
|
+
if(f.tools>0)ps.push(f.tools+(f.tools===1?' tool':' tools'));
|
|
366
|
+
if(f.llms>0)ps.push(f.llms+(f.llms===1?' LLM call':' LLM calls'));
|
|
349
367
|
iEl.textContent=ps.join(' \\u00B7 ');
|
|
350
368
|
}
|
|
351
369
|
// Update summary bar
|
|
352
370
|
var sh='';
|
|
353
371
|
sh+='<span>Events: <span class="fs-v">'+f.n+'</span></span>';
|
|
354
372
|
if(f.dur>0)sh+='<span>Duration: <span class="fs-v">'+fD(f.dur)+'</span></span>';
|
|
355
|
-
if(f.tok>0)sh+='<span>Tokens: <span class="fs-v">'+f.tok+'</span></span>';
|
|
373
|
+
if(f.tok>0)sh+='<span>Tokens used: <span class="fs-v">'+f.tok+'</span></span>';
|
|
356
374
|
if(f.cost>0)sh+='<span>Cost: <span class="fs-v">$'+f.cost.toFixed(4)+'</span></span>';
|
|
357
375
|
var tn=Object.keys(f.toolNames);
|
|
358
|
-
if(tn.length)sh+='<span>Tools: <span class="fs-tools">'+tn.map(esc).join(', ')+'</span></span>';
|
|
359
|
-
if(f.errCount>0)sh+='<span
|
|
376
|
+
if(tn.length)sh+='<span>Tools used: <span class="fs-tools">'+tn.map(esc).join(', ')+'</span></span>';
|
|
377
|
+
if(f.errCount>0)sh+='<span>\\u26A0 Errors: <span class="fs-err">'+f.errCount+'</span></span>';
|
|
360
378
|
if(f.agentId)sh+='<span>Agent: <span class="fs-agent">'+esc(f.agentId)+'</span></span>';
|
|
361
379
|
f.summary.innerHTML=sh;
|
|
362
380
|
}
|
|
@@ -426,20 +444,23 @@ export function getDashboardHtml() {
|
|
|
426
444
|
if(ft==='custom'&&m.openclawHook==='message_received')ft='msg.in';
|
|
427
445
|
if(ft==='custom'&&m.openclawHook==='message_sending')ft='msg.out';
|
|
428
446
|
|
|
447
|
+
var fl=friendlyLabel(ft);
|
|
448
|
+
|
|
429
449
|
var h='<div class="r-main">';
|
|
430
450
|
h+='<span class="r-time">'+fT(ev.ts)+'</span>';
|
|
451
|
+
h+='<span class="r-icon">'+fl[0]+'</span>';
|
|
431
452
|
if(ev.severity)h+='<span class="sev-dot '+(ev.severity||'')+'" title="'+esc(ev.severity)+'"></span>';
|
|
432
|
-
h+='<span class="r-type '+c+'">'+esc(
|
|
433
|
-
if(oc)h+='<span class="r-oc '+oc+'">'+(oc==='success'?'\\u2713':oc==='error'?'\\u2717':'\\
|
|
453
|
+
h+='<span class="r-type '+c+'">'+esc(fl[1])+'</span>';
|
|
454
|
+
if(oc)h+='<span class="r-oc '+oc+'">'+(oc==='success'?'\\u2713 OK':oc==='error'?'\\u2717 Failed':oc==='timeout'?'\\u23F1 Timeout':'\\u25CB '+oc)+'</span>';
|
|
434
455
|
h+='<span class="r-pills">';
|
|
435
456
|
if(m.toolName)h+='<span class="p t">'+esc(String(m.toolName))+'</span>';
|
|
436
457
|
if(ev.durationMs!=null)h+='<span class="p d">'+fD(ev.durationMs)+'</span>';
|
|
437
|
-
if(ev.tokenCount!=null)h+='<span class="p tk">'+ev.tokenCount+'
|
|
458
|
+
if(ev.tokenCount!=null)h+='<span class="p tk">'+ev.tokenCount+' tokens</span>';
|
|
438
459
|
if(ev.costUsd!=null)h+='<span class="p cost">$'+ev.costUsd.toFixed(4)+'</span>';
|
|
439
460
|
if(ev.agentId)h+='<span class="p agent">'+esc(ev.agentId.length>12?ev.agentId.slice(0,8)+'..':ev.agentId)+'</span>';
|
|
440
|
-
if(ev.queueDepth!=null)h+='<span class="p q">
|
|
461
|
+
if(ev.queueDepth!=null)h+='<span class="p q">Queue: '+ev.queueDepth+'</span>';
|
|
441
462
|
if(m.model)h+='<span class="p m">'+esc(String(m.model))+'</span>';
|
|
442
|
-
if(ev.channel)h+='<span class="p ch">'+esc(ev.channel)+'</span>';
|
|
463
|
+
if(ev.channel)h+='<span class="p ch">via '+esc(ev.channel)+'</span>';
|
|
443
464
|
if(m.messageCount!=null)h+='<span class="p">'+m.messageCount+' msgs</span>';
|
|
444
465
|
if(m.content){var preview=String(m.content);if(preview.length>60)preview=preview.slice(0,57)+'...';h+='<span class="p">'+esc(preview)+'</span>'}
|
|
445
466
|
if(m.source&&String(m.source)!=='simulate')h+='<span class="p s">'+esc(String(m.source))+'</span>';
|
|
@@ -447,8 +468,8 @@ export function getDashboardHtml() {
|
|
|
447
468
|
|
|
448
469
|
var ds=[];
|
|
449
470
|
if(ev.error)ds.push('<span class="err">'+esc(ev.error.length>120?ev.error.slice(0,120)+'...':ev.error)+'</span>');
|
|
450
|
-
if(ev.ageMs!=null)ds.push('
|
|
451
|
-
if(m.sessionState)ds.push('<span class="sc">'+(m.previousState||'?')+' \\u2192 '+m.sessionState+'</span>');
|
|
471
|
+
if(ev.ageMs!=null)ds.push('idle for '+fD(ev.ageMs));
|
|
472
|
+
if(m.sessionState)ds.push('<span class="sc">'+esc(String(m.previousState||'?'))+' \\u2192 '+esc(String(m.sessionState))+'</span>');
|
|
452
473
|
if(m.provider)ds.push('<span class="dim">provider: '+esc(String(m.provider))+'</span>');
|
|
453
474
|
if(m.to)ds.push('<span class="dim">to: '+esc(String(m.to))+'</span>');
|
|
454
475
|
if(ev.sessionKey&&depth===0)ds.push('<span class="dim">session: '+esc(ev.sessionKey.slice(0,12))+'</span>');
|
|
@@ -575,7 +596,7 @@ export function getDashboardHtml() {
|
|
|
575
596
|
function addAlert(a){
|
|
576
597
|
alEmpty.style.display='none';
|
|
577
598
|
var d=document.createElement('div');d.className='al';
|
|
578
|
-
d.innerHTML='<div class="al-sev '+(
|
|
599
|
+
var sv=a.severity||'error';d.innerHTML='<div class="al-sev '+esc(sv)+'">['+esc(sv.toUpperCase())+'] '+esc(a.ruleId||'')+'</div><div class="al-title">'+esc(a.title||'')+'</div><div class="al-detail">'+esc(a.detail||'')+'</div><div class="al-time">'+fT(a.ts)+'</div>';
|
|
579
600
|
alList.insertBefore(d,alList.firstChild);
|
|
580
601
|
while(alList.children.length>MAX_ALERTS+1)alList.removeChild(alList.lastChild);
|
|
581
602
|
}
|
|
@@ -694,8 +715,19 @@ export function getDashboardHtml() {
|
|
|
694
715
|
if($('lA').checked)list.scrollTop=list.scrollHeight;
|
|
695
716
|
}
|
|
696
717
|
|
|
718
|
+
function hasActiveFilter(){
|
|
719
|
+
var fSub=$('lF').value;
|
|
720
|
+
var fSrch=$('lS').value;
|
|
721
|
+
var allLevels=['TRACE','DEBUG','INFO','WARN','ERROR','FATAL'];
|
|
722
|
+
var anyUnchecked=false;
|
|
723
|
+
for(var li=0;li<allLevels.length;li++){var cb=$('lv-'+allLevels[li]);if(cb&&!cb.checked){anyUnchecked=true;break}}
|
|
724
|
+
return !!(fSub||fSrch||anyUnchecked);
|
|
725
|
+
}
|
|
726
|
+
|
|
697
727
|
function refreshLogs(){
|
|
698
|
-
|
|
728
|
+
var fetchAll=$('lAll').checked||hasActiveFilter();
|
|
729
|
+
var limit=fetchAll?0:500;
|
|
730
|
+
fetch('/openalerts/logs?limit='+limit).then(function(r){return r.json()}).then(function(data){
|
|
699
731
|
var list=$('logList');
|
|
700
732
|
var entries=data.entries||[];
|
|
701
733
|
var fSub=$('lF').value, fSrch=$('lS').value.toLowerCase();
|
|
@@ -720,15 +752,16 @@ export function getDashboardHtml() {
|
|
|
720
752
|
}
|
|
721
753
|
|
|
722
754
|
list.innerHTML='';
|
|
723
|
-
|
|
724
|
-
|
|
755
|
+
var shown=0,total=entries.length;
|
|
725
756
|
for(var i=0;i<entries.length;i++){
|
|
726
757
|
var e=entries[i];
|
|
727
758
|
if(fSub&&e.subsystem.indexOf(fSub)<0)continue;
|
|
728
759
|
if(!isLevelEnabled(e.level))continue;
|
|
729
760
|
if(fSrch&&e.message.toLowerCase().indexOf(fSrch)<0&&e.subsystem.toLowerCase().indexOf(fSrch)<0)continue;
|
|
730
761
|
list.appendChild(buildLogTabRow(e));
|
|
762
|
+
shown++;
|
|
731
763
|
}
|
|
764
|
+
if(!shown){list.innerHTML='<div class="empty-msg">No logs match your filters. '+(truncated?'Try checking "All logs" to search the full log file.':'')+'</div>';return}
|
|
732
765
|
if($('lA').checked)list.scrollTop=list.scrollHeight;
|
|
733
766
|
}).catch(function(){$('logList').innerHTML='<div class="empty-msg">Failed to load.</div>'});
|
|
734
767
|
}
|
|
@@ -755,6 +788,7 @@ export function getDashboardHtml() {
|
|
|
755
788
|
$('lR').addEventListener('click',refreshLogs);
|
|
756
789
|
$('lE').addEventListener('click',exportLogs);
|
|
757
790
|
$('lF').addEventListener('change',refreshLogs);
|
|
791
|
+
$('lAll').addEventListener('change',refreshLogs);
|
|
758
792
|
var sDb;$('lS').addEventListener('input',function(){clearTimeout(sDb);sDb=setTimeout(refreshLogs,300)});
|
|
759
793
|
|
|
760
794
|
// Level filter checkboxes
|
|
@@ -821,7 +855,7 @@ export function getDashboardHtml() {
|
|
|
821
855
|
var cds=s.cooldowns||{};
|
|
822
856
|
if(s.rules)for(var i=0;i<s.rules.length;i++){
|
|
823
857
|
var r=s.rules[i];
|
|
824
|
-
var cdTs=cds
|
|
858
|
+
var cdTs=findLastFired(cds,r.id);
|
|
825
859
|
var lastFired=cdTs?fAgo(cdTs):'--';
|
|
826
860
|
html+='<tr><td>'+esc(r.id)+'</td><td>'+(r.status==='fired'?'<span style="color:#f85149;font-weight:700">FIRING</span>':'<span style="color:#3fb950">OK</span>')+'</td><td>'+lastFired+'</td></tr>';
|
|
827
861
|
}
|
|
@@ -833,34 +867,12 @@ export function getDashboardHtml() {
|
|
|
833
867
|
html+='</table></div>';
|
|
834
868
|
}
|
|
835
869
|
|
|
836
|
-
// Circuit Breakers (when backend exposes this)
|
|
837
|
-
if(s.circuitBreakers&&s.circuitBreakers.length){
|
|
838
|
-
html+='<div class="h-sec"><h3>Circuit Breakers</h3><table class="h-tbl"><tr><th>Category</th><th>Name</th><th>State</th><th>Failures</th><th>Last Change</th></tr>';
|
|
839
|
-
for(var k=0;k<s.circuitBreakers.length;k++){
|
|
840
|
-
var cb=s.circuitBreakers[k];
|
|
841
|
-
var stColor=cb.state==='CLOSED'?'#3fb950':cb.state==='OPEN'?'#f85149':'#d29922';
|
|
842
|
-
html+='<tr><td>'+esc(cb.category)+'</td><td>'+esc(cb.name)+'</td><td style="color:'+stColor+';font-weight:700">'+cb.state+'</td><td>'+cb.failures+'</td><td>'+fAgo(cb.lastStateChange)+'</td></tr>';
|
|
843
|
-
}
|
|
844
|
-
html+='</table></div>';
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// Task Timeouts (when backend exposes this)
|
|
848
|
-
if(s.taskTimeouts&&s.taskTimeouts.length){
|
|
849
|
-
html+='<div class="h-sec"><h3>Running Tasks</h3><table class="h-tbl"><tr><th>Type</th><th>ID</th><th>Duration</th><th>Timeout</th><th>Status</th></tr>';
|
|
850
|
-
for(var l=0;l<s.taskTimeouts.length;l++){
|
|
851
|
-
var tt=s.taskTimeouts[l];
|
|
852
|
-
var dur=Date.now()-tt.startedAt;
|
|
853
|
-
var pct=Math.floor((dur/tt.timeoutMs)*100);
|
|
854
|
-
var warn=pct>80;
|
|
855
|
-
html+='<tr><td>'+esc(tt.type)+'</td><td>'+esc(tt.id.slice(0,12))+'</td><td>'+fD(dur)+'</td><td>'+fD(tt.timeoutMs)+'</td><td style="color:'+(warn?'#d29922':'#3fb950')+'">'+pct+'%</td></tr>';
|
|
856
|
-
}
|
|
857
|
-
html+='</table></div>';
|
|
858
|
-
}
|
|
859
|
-
|
|
860
870
|
hEl.innerHTML=html;
|
|
861
871
|
}
|
|
862
872
|
function hCard(l,v,c){return'<div class="h-card"><div class="lb">'+esc(l)+'</div><div class="vl '+(c||'')+'">'+esc(String(v))+'</div></div>'}
|
|
863
873
|
function hTr(l,v){return'<tr><td>'+esc(l)+'</td><td><b>'+esc(String(v))+'</b></td></tr>'}
|
|
874
|
+
/** Find the most recent cooldown timestamp for a rule ID (handles contextual fingerprints like "llm-errors:telegram"). */
|
|
875
|
+
function findLastFired(cds,ruleId){var latest=0;for(var k in cds){if(k===ruleId||k.indexOf(ruleId+':')===0){if(cds[k]>latest)latest=cds[k]}}return latest||null}
|
|
864
876
|
|
|
865
877
|
// ─── Debug tab ──────────────────────
|
|
866
878
|
function refreshDebug(){
|
|
@@ -907,18 +919,13 @@ export function getDashboardHtml() {
|
|
|
907
919
|
var cds=s.cooldowns||{};
|
|
908
920
|
if(s.rules)for(var i=0;i<s.rules.length;i++){
|
|
909
921
|
var r=s.rules[i];
|
|
910
|
-
var cdTs=cds
|
|
922
|
+
var cdTs=findLastFired(cds,r.id);
|
|
911
923
|
var lastFired=cdTs?fAgo(cdTs):'never';
|
|
912
924
|
rulesHtml+='<tr><td>'+esc(r.id)+'</td><td>'+(r.status==='fired'?'<span style="color:#f85149;font-weight:700">FIRING</span>':'<span style="color:#3fb950">OK</span>')+'</td><td>'+lastFired+'</td></tr>';
|
|
913
925
|
}
|
|
914
926
|
rulesHtml+='</table>';
|
|
915
927
|
$('dbRules').innerHTML=rulesHtml;
|
|
916
928
|
|
|
917
|
-
// Circuit breakers (placeholder - will be populated when backend exposes this)
|
|
918
|
-
$('dbCircuit').innerHTML='<div style="color:#8b949e;padding:8px">Circuit breaker state not yet exposed by backend</div>';
|
|
919
|
-
|
|
920
|
-
// Task timeouts (placeholder - will be populated when backend exposes this)
|
|
921
|
-
$('dbTasks').innerHTML='<div style="color:#8b949e;padding:8px">Task timeout state not yet exposed by backend</div>';
|
|
922
929
|
}
|
|
923
930
|
$('dbRefresh').addEventListener('click',refreshDebug);
|
|
924
931
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { DEFAULTS } from "../core/index.js";
|
|
3
4
|
import { getDashboardHtml } from "./dashboard-html.js";
|
|
4
5
|
// ─── SSE connection tracking ─────────────────────────────────────────────────
|
|
5
6
|
const sseConnections = new Set();
|
|
@@ -34,6 +35,16 @@ function getRuleStatuses(engine) {
|
|
|
34
35
|
const now = Date.now();
|
|
35
36
|
const cooldownWindow = 15 * 60 * 1000;
|
|
36
37
|
return RULE_IDS.map((id) => {
|
|
38
|
+
// For gateway-down, reflect current condition: if heartbeats have resumed,
|
|
39
|
+
// show OK even if the rule fired recently.
|
|
40
|
+
if (id === "gateway-down") {
|
|
41
|
+
const silenceMs = state.lastHeartbeatTs > 0
|
|
42
|
+
? now - state.lastHeartbeatTs
|
|
43
|
+
: 0;
|
|
44
|
+
const isCurrentlyDown = state.lastHeartbeatTs > 0 &&
|
|
45
|
+
silenceMs >= DEFAULTS.gatewayDownThresholdMs;
|
|
46
|
+
return { id, status: isCurrentlyDown ? "fired" : "ok" };
|
|
47
|
+
}
|
|
37
48
|
// Cooldown keys are fingerprints like "llm-errors:unknown", not bare rule IDs.
|
|
38
49
|
// Check if ANY cooldown key starting with this rule ID has fired recently.
|
|
39
50
|
let fired = false;
|
|
@@ -126,8 +137,8 @@ function readOpenClawLogs(maxEntries, afterTs) {
|
|
|
126
137
|
entries.push(parsed);
|
|
127
138
|
subsystemSet.add(parsed.subsystem);
|
|
128
139
|
}
|
|
129
|
-
const truncated = entries.length > maxEntries;
|
|
130
|
-
const sliced = entries.slice(-maxEntries);
|
|
140
|
+
const truncated = maxEntries > 0 && entries.length > maxEntries;
|
|
141
|
+
const sliced = maxEntries > 0 ? entries.slice(-maxEntries) : entries;
|
|
131
142
|
const subsystems = Array.from(subsystemSet).sort();
|
|
132
143
|
return { entries: sliced, truncated, subsystems };
|
|
133
144
|
}
|
|
@@ -299,7 +310,9 @@ export function createDashboardHandler(getEngine) {
|
|
|
299
310
|
// ── GET /openalerts/logs → OpenClaw log entries (for Logs tab) ────
|
|
300
311
|
if (url.startsWith("/openalerts/logs") && req.method === "GET") {
|
|
301
312
|
const urlObj = new URL(url, "http://localhost");
|
|
302
|
-
const
|
|
313
|
+
const rawLimit = urlObj.searchParams.get("limit") || "200";
|
|
314
|
+
// limit=0 means "no limit" — return all log entries
|
|
315
|
+
const limit = rawLimit === "0" ? 0 : Math.min(parseInt(rawLimit, 10), 50000);
|
|
303
316
|
const afterTs = urlObj.searchParams.get("after") || undefined;
|
|
304
317
|
const result = readOpenClawLogs(limit, afterTs);
|
|
305
318
|
res.writeHead(200, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steadwing/openalerts",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenAlerts — An alerting layer for agentic frameworks",
|
|
6
6
|
"author": "Steadwing",
|
|
@@ -39,5 +39,8 @@
|
|
|
39
39
|
"keywords": ["openalerts", "openclaw", "monitoring", "alerting", "plugin"],
|
|
40
40
|
"engines": {
|
|
41
41
|
"node": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"overrides": {
|
|
44
|
+
"tar": "^7.5.7"
|
|
42
45
|
}
|
|
43
46
|
}
|