From cfc99e2f14992d9950df7a7a0d9ea9eebdefbc4a Mon Sep 17 00:00:00 2001
From: Raj Nandan Sharma
Date: Sat, 6 Jun 2026 13:04:13 +0530
Subject: [PATCH] chore: update documentation and enhance monitor group
functionality, fixes #694
---
CLAUDE.md | 14 ++
CONTEXT.md | 31 ++++
docs/adr/0001-explicit-group-membership.md | 5 +
docs/agents/domain.md | 37 ++++
docs/agents/issue-tracker.md | 22 +++
docs/agents/triage-labels.md | 15 ++
src/lib/components/MonitorPicker.svelte | 98 ++++++++++
.../monitors/[tag]/types/monitor-group.svelte | 167 +++++++++++-------
8 files changed, 322 insertions(+), 67 deletions(-)
create mode 100644 CONTEXT.md
create mode 100644 docs/adr/0001-explicit-group-membership.md
create mode 100644 docs/agents/domain.md
create mode 100644 docs/agents/issue-tracker.md
create mode 100644 docs/agents/triage-labels.md
create mode 100644 src/lib/components/MonitorPicker.svelte
diff --git a/CLAUDE.md b/CLAUDE.md
index 01580c68..a5edd369 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -121,3 +121,17 @@ Read `.claude/skills/` for specialized instructions on:
- **svelte-code-writer** - Svelte component creation/editing
- **documentation-writer** - Editing docs in `src/routes/(docs)/docs/content/`
- **tailwindcss** - Tailwind CSS v4 patterns
+
+## Agent skills
+
+### Issue tracker
+
+Issues and PRDs are tracked in GitHub Issues for `rajnandan1/kener`. See `docs/agents/issue-tracker.md`.
+
+### Triage labels
+
+Triage uses the default mattpocock/skills label vocabulary. See `docs/agents/triage-labels.md`.
+
+### Domain docs
+
+This repo uses a single-context domain-doc layout. See `docs/agents/domain.md`.
diff --git a/CONTEXT.md b/CONTEXT.md
new file mode 100644
index 00000000..80309b65
--- /dev/null
+++ b/CONTEXT.md
@@ -0,0 +1,31 @@
+# Kener
+
+An open-source status page application providing real-time monitoring, uptime tracking, incident management, and customizable dashboards.
+
+## Language
+
+### Monitoring
+
+**Monitor**:
+A single check against a service, with a unique tag, a type (API, Ping, TCP, DNS, SSL, SQL, Heartbeat, GameDig, Group, gRPC, None), and a status (ACTIVE or otherwise).
+_Avoid_: Check, probe, service
+
+**Group Monitor**:
+A monitor whose status is derived from other monitors via a weighted score (UP=1, DEGRADED=0.5, DOWN=0; maintenance counts as UP). A group cannot contain another group.
+_Avoid_: Monitor group, composite monitor
+
+**Member**:
+A monitor belonging to a Group Monitor, carrying a weight and a position in the execution order. Membership is an explicit stored list, never a dynamic rule (e.g. tag wildcards).
+_Avoid_: Child monitor, sub-monitor
+
+**Weight**:
+A member's share of the group score, between 0 and 1. Weights across a group's members must sum to 1. Any membership change (add or remove) redistributes all weights equally; manual tuning happens after membership is settled.
+
+**Execution Order**:
+The stored order in which a Group Monitor's members are checked before aggregation. Manually arranged, not derived.
+
+**Eligible Monitor**:
+A monitor that may become a Member: ACTIVE, not a Group Monitor, and not the group being edited itself.
+
+**Stale Member**:
+A Member whose monitor is no longer an Eligible Monitor (paused or deleted after being added). It remains a Member until explicitly removed, but is excluded from the group score.
diff --git a/docs/adr/0001-explicit-group-membership.md b/docs/adr/0001-explicit-group-membership.md
new file mode 100644
index 00000000..09914f1e
--- /dev/null
+++ b/docs/adr/0001-explicit-group-membership.md
@@ -0,0 +1,5 @@
+# Group membership is an explicit stored list, not a rule
+
+When making group-monitor member selection searchable (#694), the requester also proposed dynamic membership by tag pattern (e.g. `site1-*` auto-adds matching monitors). We decided group membership stays an explicit, stored list of members. Dynamic membership contradicts the group model: each member carries an explicit weight (weights must sum to 1) and a manual execution order — a rule that adds/removes members over time would need an auto-weighting policy, silent weight redistribution when monitors are created or deleted, and an undefined execution order for matched members. Bulk needs are served in the editor instead: search plus "Add all N matching" makes large explicit groups cheap to build.
+
+If wildcard groups are requested again, the answer is here: it's a different feature (a rule-based aggregate without weights or order), not an extension of Group Monitors.
diff --git a/docs/agents/domain.md b/docs/agents/domain.md
new file mode 100644
index 00000000..2ed13ff1
--- /dev/null
+++ b/docs/agents/domain.md
@@ -0,0 +1,37 @@
+# Domain Docs
+
+How the engineering skills should consume this repo's domain documentation when exploring the codebase.
+
+This repo is configured as a **single-context** repo.
+
+## Before exploring, read these
+
+- **`CONTEXT.md`** at the repo root.
+- **`docs/adr/`** — read ADRs that touch the area you're about to work in.
+
+If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The producer skill (`/grill-with-docs`) creates them lazily when terms or decisions actually get resolved.
+
+## File structure
+
+Single-context repo:
+
+```text
+/
+├── CONTEXT.md
+├── docs/adr/
+│ ├── 0001-event-sourced-orders.md
+│ └── 0002-postgres-for-write-model.md
+└── src/
+```
+
+## Use the glossary's vocabulary
+
+When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
+
+If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/grill-with-docs`).
+
+## Flag ADR conflicts
+
+If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
+
+> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_
diff --git a/docs/agents/issue-tracker.md b/docs/agents/issue-tracker.md
new file mode 100644
index 00000000..cce77ecb
--- /dev/null
+++ b/docs/agents/issue-tracker.md
@@ -0,0 +1,22 @@
+# Issue tracker: GitHub
+
+Issues and PRDs for this repo live as GitHub issues. Use the `gh` CLI for all operations.
+
+## Conventions
+
+- **Create an issue**: `gh issue create --title "..." --body "..."`. Use a heredoc for multi-line bodies.
+- **Read an issue**: `gh issue view --comments`, filtering comments by `jq` and also fetching labels.
+- **List issues**: `gh issue list --state open --json number,title,body,labels,comments --jq '[.[] | {number, title, body, labels: [.labels[].name], comments: [.comments[].body]}]'` with appropriate `--label` and `--state` filters.
+- **Comment on an issue**: `gh issue comment --body "..."`
+- **Apply / remove labels**: `gh issue edit --add-label "..."` / `--remove-label "..."`
+- **Close**: `gh issue close --comment "..."`
+
+Infer the repo from `git remote -v` — `gh` does this automatically when run inside a clone.
+
+## When a skill says "publish to the issue tracker"
+
+Create a GitHub issue.
+
+## When a skill says "fetch the relevant ticket"
+
+Run `gh issue view --comments`.
diff --git a/docs/agents/triage-labels.md b/docs/agents/triage-labels.md
new file mode 100644
index 00000000..b716855d
--- /dev/null
+++ b/docs/agents/triage-labels.md
@@ -0,0 +1,15 @@
+# Triage Labels
+
+The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
+
+| Label in mattpocock/skills | Label in our tracker | Meaning |
+| -------------------------- | -------------------- | ---------------------------------------- |
+| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
+| `needs-info` | `needs-info` | Waiting on reporter for more information |
+| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
+| `ready-for-human` | `ready-for-human` | Requires human implementation |
+| `wontfix` | `wontfix` | Will not be actioned |
+
+When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
+
+Edit the right-hand column to match whatever vocabulary you actually use.
diff --git a/src/lib/components/MonitorPicker.svelte b/src/lib/components/MonitorPicker.svelte
new file mode 100644
index 00000000..69e2760e
--- /dev/null
+++ b/src/lib/components/MonitorPicker.svelte
@@ -0,0 +1,98 @@
+
+
+
+
+ {#snippet child({ props })}
+
+ {/snippet}
+
+
+
+
+
+ No monitors found.
+
+ {#each filteredMonitors as monitor (monitor.tag)}
+ {@const selected = selectedTags.includes(monitor.tag)}
+ onToggle(monitor.tag)}>
+
+ {#if monitor.image}
+
+ {:else}
+
+ {monitor.name.charAt(0).toUpperCase()}
+
+ {/if}
+ {monitor.name}
+ {monitor.tag}
+
+ {/each}
+
+ {#if showAddAll}
+
+
+
+
+ Add all {unselectedMatches.length} matching
+
+
+ {/if}
+
+
+
+
diff --git a/src/routes/(manage)/manage/app/monitors/[tag]/types/monitor-group.svelte b/src/routes/(manage)/manage/app/monitors/[tag]/types/monitor-group.svelte
index 66df7799..83521495 100644
--- a/src/routes/(manage)/manage/app/monitors/[tag]/types/monitor-group.svelte
+++ b/src/routes/(manage)/manage/app/monitors/[tag]/types/monitor-group.svelte
@@ -3,12 +3,14 @@
import { Input } from "$lib/components/ui/input/index.js";
import { Button } from "$lib/components/ui/button/index.js";
import * as Select from "$lib/components/ui/select/index.js";
- import { Switch } from "$lib/components/ui/switch/index.js";
+ import { Badge } from "$lib/components/ui/badge/index.js";
+ import MonitorPicker from "$lib/components/MonitorPicker.svelte";
import type { MonitorRecord } from "$lib/server/types/db.js";
import type { GroupMonitorMember } from "$lib/server/types/monitor.js";
import ArrowUp from "@lucide/svelte/icons/arrow-up";
import ArrowDown from "@lucide/svelte/icons/arrow-down";
import GripVertical from "@lucide/svelte/icons/grip-vertical";
+ import X from "@lucide/svelte/icons/x";
import clientResolver from "$lib/client/resolver.js";
import { resolve } from "$app/paths";
@@ -58,16 +60,20 @@
formData.executionDelay = parsedExecutionDelay;
});
- // Filter out GROUP monitors - groups can't contain other groups
- let eligibleMonitors = $derived(availableMonitors.filter((m) => m.monitor_type !== "GROUP" && m.status === "ACTIVE"));
+ // Filter out GROUP monitors - groups can't contain other groups - and the group being edited itself
+ let eligibleMonitors = $derived(
+ availableMonitors.filter((m) => m.monitor_type !== "GROUP" && m.status === "ACTIVE" && m.tag !== tag)
+ );
+ let selectedTags = $derived(formData.monitors.map((m) => m.tag));
+
+ /** A stale member's monitor is no longer eligible (paused or deleted after being added). */
+ function isStale(monitorTag: string): boolean {
+ return !eligibleMonitors.some((m) => m.tag === monitorTag);
+ }
let totalWeight = $derived(Math.round(formData.monitors.reduce((sum, m) => sum + m.weight, 0) * 1000) / 1000);
let weightsValid = $derived(Math.abs(totalWeight - 1) < 0.001 || formData.monitors.length === 0);
- function findMember(monitorTag: string): GroupMonitorMember | undefined {
- return formData.monitors.find((m) => m.tag === monitorTag);
- }
-
function isSelected(monitorTag: string): boolean {
return formData.monitors.some((m) => m.tag === monitorTag);
}
@@ -93,6 +99,22 @@
distributeEqually();
}
+ function addMonitors(tags: string[]) {
+ const newTags = tags.filter((t) => !isSelected(t));
+ if (newTags.length === 0) return;
+ formData.monitors = [...formData.monitors, ...newTags.map((t) => ({ tag: t, weight: 0 }))];
+ distributeEqually();
+ }
+
+ function removeMonitor(monitorTag: string) {
+ formData.monitors = formData.monitors.filter((m) => m.tag !== monitorTag);
+ distributeEqually();
+ }
+
+ function clearAll() {
+ formData.monitors = [];
+ }
+
function setWeight(monitorTag: string, weight: number) {
formData.monitors = formData.monitors.map((m) => {
if (m.tag !== monitorTag) return m;
@@ -129,55 +151,8 @@