feat: remove legacy alert rules, superseded by automations

Alert rules already auto-migrate into automations on boot and the old
engine stands down after. Deletes the rule/firing tables, routes, UI,
and AI tools; keeps notification channels since automations still use
them. Also fixes automations' notify step, which still called the
deleted repository.
This commit is contained in:
LukeGus committed 2026-09-22 10:45:12 -05:00
1 parent e0f14f2ecf
commit e372b0eda7
101 files changed
+29118 -5539

No files matched your search

+2 -2
View File
@@ -13,7 +13,7 @@ export function buildSystemPrompt(options: {
}): string {
const lines: string[] = [
"You are the assistant built into Termix, a self-hosted server management app.",
"You help the user manage their servers, snippets, automations, fleets and alerts.",
"You help the user manage their servers, snippets, automations and fleets.",
"",
"How you work:",
"- You start with no knowledge of this user's setup. Call a read tool to find out anything you need.",
@@ -24,7 +24,7 @@ export function buildSystemPrompt(options: {
"- Do exactly what was asked, and nothing beyond it. A question is a request for an answer, not for changes.",
"- Questions like 'what is running on this server', 'check this host' or 'why is this slow' are answered with information. Read, then report. Do not propose anything.",
"- Only propose a change when the user asked for one, in words like 'add', 'create', 'set up', 'fix' or 'change'.",
"- Do not propose follow-up work you thought of yourself: no monitoring, no alert rules, no scripts, no snippets, no cleanup, unless that is what was requested.",
"- Do not propose follow-up work you thought of yourself: no monitoring automations, no scripts, no snippets, no cleanup, unless that is what was requested.",
"- If you think something is worth doing, say so in one sentence and stop. Let the user ask.",
"- One request means one proposal at most. Do not bundle extras alongside it.",
"",
+2 -2
View File
@@ -4,11 +4,11 @@
* This plugin runs on the main thread rather than in a worker for the same
* reason ssh-terminal, docker and host-metrics do (see
* src/backend/plugins/first-party.ts): it writes directly across hosts,
* snippets, fleets, alert rules and automations through their repositories,
* snippets, fleets and automations through their repositories,
* runs approved commands over the shared SSH pool via resolveHostById and
* withConnection, and streams its chat replies as server-sent events. None
* of that fits the worker ctx -- ctx.hosts is a 13-field read-only view with
* no write methods and no snippets/fleets/alerts/automations surface at all,
* no write methods and no snippets/fleets/automations surface at all,
* and ctx.http.route replies with a single postMessage value rather than a
* stream.
*
-30
View File
@@ -1,5 +1,4 @@
import {
createCurrentAlertRepository,
createCurrentAutomationRepository,
createCurrentFleetRepository,
createCurrentHostRepository,
@@ -217,35 +216,6 @@ export async function applyProposal(
};
}
case "propose_create_alert_rule": {
const created = await createCurrentAlertRepository().createAlertRule({
userId,
name: requireString(payload.name, "name"),
hostId:
payload.hostId === null || payload.hostId === undefined
? null
: requireNumber(payload.hostId, "hostId"),
enabled: true,
triggerType: requireString(payload.triggerType, "triggerType"),
thresholdValue:
payload.thresholdValue === null ||
payload.thresholdValue === undefined
? null
: Number(payload.thresholdValue),
thresholdDurationSeconds:
payload.thresholdDurationSeconds === null ||
payload.thresholdDurationSeconds === undefined
? null
: Number(payload.thresholdDurationSeconds),
cooldownMinutes: Number(payload.cooldownMinutes) || 15,
channelIds: [],
} as any);
return {
ok: true,
summary: `Created alert rule ${(created as any)?.name ?? ""}`.trim(),
};
}
case "propose_create_automation": {
// Reuses the same validator the automations route runs, so an
// LLM-authored definition is held to exactly the human standard.
-34
View File
@@ -216,40 +216,6 @@ export const proposeTools: AiTool[] = [
hostIds: args.hostIds ?? [],
}),
},
{
name: "propose_create_alert_rule",
description:
"Propose a new alert rule that fires when a host metric crosses a threshold.",
category: "propose",
parameters: objectSchema(
{
name: str("Rule name"),
hostId: num("Host id to watch, or omit to watch all hosts"),
triggerType: str(
"What to watch, for example cpu, memory, disk or host_status",
),
thresholdValue: num("Threshold to compare against"),
thresholdDurationSeconds: num(
"How long the breach must persist before firing",
),
cooldownMinutes: num("Minimum minutes between repeat firings"),
},
["name", "triggerType"],
),
handler: async (args) =>
proposal(
"propose_create_alert_rule",
`Create alert rule ${String(args.name)}`,
{
name: args.name,
hostId: args.hostId ?? null,
triggerType: args.triggerType,
thresholdValue: args.thresholdValue ?? null,
thresholdDurationSeconds: args.thresholdDurationSeconds ?? null,
cooldownMinutes: args.cooldownMinutes ?? 15,
},
),
},
{
name: "propose_run_command",
description:
+2 -55
View File
@@ -1,5 +1,5 @@
import {
createCurrentAlertRepository,
createCurrentNotificationChannelRepository,
createCurrentAutomationRepository,
createCurrentCommandHistoryRepository,
createCurrentFleetRepository,
@@ -190,30 +190,6 @@ export const readTools: AiTool[] = [
};
},
},
{
name: "list_alert_rules",
description:
"List the user's alert rules with their thresholds and enabled state.",
category: "read",
parameters: objectSchema({}),
handler: async (_args, context) => {
const rules = await createCurrentAlertRepository().listAlertRules(
context.userId,
);
return {
rules: rules.map((rule) => ({
id: rule.id,
name: rule.name,
hostId: rule.host_id,
enabled: rule.enabled === 1,
triggerType: rule.trigger_type,
thresholdValue: rule.threshold_value,
thresholdDurationSeconds: rule.threshold_duration_seconds,
cooldownMinutes: rule.cooldown_minutes,
})),
};
},
},
{
name: "list_notification_channels",
description:
@@ -222,7 +198,7 @@ export const readTools: AiTool[] = [
parameters: objectSchema({}),
handler: async (_args, context) => {
const channels =
await createCurrentAlertRepository().listNotificationChannels(
await createCurrentNotificationChannelRepository().listNotificationChannels(
context.userId,
);
return {
@@ -235,35 +211,6 @@ export const readTools: AiTool[] = [
};
},
},
{
name: "get_alert_firings",
description:
"Recent alert firings, newest first. Use this to answer questions about what has been alerting.",
category: "read",
parameters: objectSchema({
limit: num("How many firings to return (default 25, max 100)"),
}),
handler: async (args, context) => {
const limit = Math.min(Math.max(Number(args.limit) || 25, 1), 100);
const result = await createCurrentAlertRepository().listAlertFirings({
userId: context.userId,
limit,
offset: 0,
});
return {
firings: (result.firings ?? []).map((firing) => ({
id: firing.id,
ruleName: firing.rule_name,
hostName: firing.host_name,
firedAt: firing.fired_at,
resolvedAt: firing.resolved_at,
severity: firing.severity,
message: firing.message,
acknowledged: firing.acknowledged === 1,
})),
};
},
},
{
name: "list_homepage_items",
description: "List the user's homepage service-link tiles.",