@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 CHANGED
@@ -14,7 +14,7 @@
14
14
  <p align="center">
15
15
  <a href="#quickstart">Quickstart</a> &middot;
16
16
  <a href="#alert-rules">Alert Rules</a> &middot;
17
- <a href="#configuration">Configuration</a> &middot;
17
+ <a href="#llm-enriched-alerts">LLM Enrichment</a> &middot;
18
18
  <a href="#dashboard">Dashboard</a> &middot;
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
- | **llm-errors** | 1+ LLM/agent failure in 1 minute | ERROR |
92
- | **infra-errors** | 1+ infrastructure error in 1 minute | ERROR |
93
- | **gateway-down** | No heartbeat for 30+ seconds | CRITICAL |
94
- | **session-stuck** | Session idle for 120+ seconds | WARN |
95
- | **high-error-rate** | 50%+ of last 20 messages failed | ERROR |
96
- | **queue-depth** | 10+ items queued | WARN |
97
- | **tool-errors** | 1+ tool failure in 1 minute | WARN |
98
- | **heartbeat-fail** | 3 consecutive heartbeat failures | ERROR |
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
- All thresholds and cooldowns are [configurable per-rule](#advanced-configuration).
101
-
102
- ## LLM-Enriched Alerts
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 disable LLM enrichment, set `"llmEnriched": false` in your plugin config:
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
- "llmEnriched": false
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
- ## Advanced Configuration
126
+ Set `"quiet": true` at the config level for log-only mode (no messages sent).
135
127
 
136
- Each rule can be individually tuned or disabled. You can also set global options like `cooldownMinutes` (default: `15`) and `quiet: true` for log-only mode.
128
+ ## LLM-Enriched Alerts
137
129
 
138
- **Step 1.** Add a `rules` object inside `plugins.entries.openalerts.config` in your `~/.openclaw/openclaw.json`:
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
- "rules": {
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
- **Step 2.** Restart the gateway to apply:
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
- ### Rule reference
167
-
168
- | Rule | `threshold` unit | Default |
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
- Every rule also accepts:
180
- - **`enabled`** `false` to disable the rule (default: `true`)
181
- - **`cooldownMinutes`** minutes before the same rule can fire again (default: `15`)
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 or API key can't be resolved.
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 or API key can't be resolved.
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: true)
23
- const enricher = config.llmEnriched !== false
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
@@ -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
- return createLlmEnricher({ modelString: primary, logger });
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:13px;overflow:hidden;height:100vh}
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:14px;font-weight:600;color:#f0f6fc;letter-spacing:0.5px}
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:11px}
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:12px;font-weight:600;color:#8b949e;cursor:pointer;border-bottom:2px solid transparent;transition:all 0.15s}
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:11px;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}
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:11px;line-height:1.5;animation:fi 0.15s ease}
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:10px;min-width:55px;flex-shrink:0}
76
- .r-icon{width:14px;text-align:center;flex-shrink:0;font-size:11px}
77
- .r-type{font-weight:600;min-width:90px;font-size:10px;flex-shrink:0}
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:9px;background:#21262d;padding:0 5px;border-radius:3px;white-space:nowrap}
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:10px}
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:11px;animation:fi 0.2s}
105
- .al-sev{font-weight:700;text-transform:uppercase;font-size:9px;letter-spacing:0.4px}
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:11px}
108
- .al-detail{color:#8b949e;margin-top:1px;font-size:10px}
109
- .al-time{color:#484f58;font-size:10px;margin-top:1px}
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:10px;color:#8b949e;text-transform:uppercase;letter-spacing:0.7px;margin-bottom:4px}
114
- .rl{display:flex;align-items:center;gap:5px;font-size:11px;padding:1px 0}
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:10px}
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:11px}
122
- .log-bar select,.log-bar input{background:#0d1117;border:1px solid #30363d;color:#c9d1d9;font-family:inherit;font-size:10px;padding:3px 6px;border-radius:3px}
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:11px}
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:10px;min-width:70px;flex-shrink:0}
139
- .log-lv{font-size:9px;font-weight:700;min-width:42px;flex-shrink:0}
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:10px;min-width:110px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
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:11px;color:#8b949e;text-transform:uppercase;letter-spacing:0.7px;margin-bottom:6px;padding-bottom:3px;border-bottom:1px solid #21262d}
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:10px;margin-bottom:2px}
153
- .h-card .vl{color:#c9d1d9;font-size:13px;font-weight:600}
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:3px 8px;font-size:11px;border-bottom:1px solid #161b22}
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:3px 8px;font-size:10px;color:#8b949e;text-align:left;border-bottom:1px solid #30363d;font-weight:600}
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 - showing most recent entries only</div>
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">active</span><span class="flow-info" data-r="info">'+fT(ev.ts||ev.tsMs)+'</span>';
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
- if(sEl){sEl.className='flow-badge '+st;sEl.textContent=st}
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+' 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+' llm');
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>Errors: <span class="fs-err">'+f.errCount+'</span></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(ft)+'</span>';
433
- if(oc)h+='<span class="r-oc '+oc+'">'+(oc==='success'?'\\u2713':oc==='error'?'\\u2717':'\\u25CB')+' '+oc+'</span>';
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+' tok</span>';
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">q='+ev.queueDepth+'</span>';
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('stuck '+fD(ev.ageMs));
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 '+(a.severity||'error')+'">['+((a.severity||'ERROR').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>';
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
- fetch('/openalerts/logs?limit=500').then(function(r){return r.json()}).then(function(data){
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
- if(!entries.length){list.innerHTML='<div class="empty-msg">No logs found.</div>';return}
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[r.id];
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[r.id];
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 limit = Math.min(parseInt(urlObj.searchParams.get("limit") || "200", 10), 1000);
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.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
  }