Update ReUI skill documentation to reflect the addition of 3 new components, increasing the total from 17 to 20. Adjust descriptions and examples accordingly, including updates to the data-grid API to align with TanStack Table v9. Ensure all references to components and their usage are consistent across the documentation.
This commit is contained in:
@@ -404,9 +404,8 @@ export async function listGroupViews(db: Db): Promise<ServiceGroupsResponse> {
|
||||
const withHealth = attachServiceHealth(db, allServiceViews);
|
||||
const healthById = new Map(withHealth.map((v) => [v.id, v]));
|
||||
|
||||
const groupHealthById = repos.aggregateIpHealthByRefs(
|
||||
const groupHealthById = repos.aggregateGroupScopeHealthByIds(
|
||||
db,
|
||||
"group",
|
||||
groups.map((g) => g.id),
|
||||
);
|
||||
|
||||
@@ -415,12 +414,17 @@ export async function listGroupViews(db: Db): Promise<ServiceGroupsResponse> {
|
||||
(s) => healthById.get(s.id) ?? { ...s, health_status: "unknown" as const, health_latency_ms: null },
|
||||
);
|
||||
const groupScopeHealth = groupHealthById.get(group.id);
|
||||
const merged = repos.mergeHealthAggregates([
|
||||
groupScopeHealth,
|
||||
...services.map((s) => ({
|
||||
// Only enabled services feed the group badge — a disabled service with a
|
||||
// stale `down` must not keep the group red while live members are OK.
|
||||
const enabledServiceHealth = services
|
||||
.filter((s) => s.enabled)
|
||||
.map((s) => ({
|
||||
health_status: s.health_status,
|
||||
health_latency_ms: s.health_latency_ms,
|
||||
})),
|
||||
}));
|
||||
const merged = repos.mergeHealthAggregates([
|
||||
groupScopeHealth,
|
||||
...enabledServiceHealth,
|
||||
]);
|
||||
return {
|
||||
...group,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildApp } from "../src/app.js";
|
||||
import { loadConfig } from "../src/config.js";
|
||||
import { repos } from "@cfdm/db";
|
||||
import { createMemoryDb, repos, runMigrations } from "@cfdm/db";
|
||||
|
||||
async function authHeaders(app: Awaited<ReturnType<typeof buildApp>>) {
|
||||
const config = loadConfig();
|
||||
@@ -31,8 +31,14 @@ describe("service groups health enrichment", () => {
|
||||
null,
|
||||
"vpn.example.com",
|
||||
);
|
||||
repos.updateServiceGroup(app.db, group.id, "VPN", "vpn", null, "vpn.example.com", {
|
||||
health_check_enabled: true,
|
||||
health_check_type: "tcp",
|
||||
health_check_port: 443,
|
||||
});
|
||||
const service = repos.createService(app.db, "Panel", "panel");
|
||||
repos.setServiceGroup(app.db, service.id, group.id);
|
||||
repos.setServiceEnabled(app.db, service.id, true);
|
||||
const binding = repos.insertBinding(
|
||||
app.db,
|
||||
domain.id,
|
||||
@@ -57,7 +63,7 @@ describe("service groups health enrichment", () => {
|
||||
app.db,
|
||||
"group",
|
||||
group.id,
|
||||
"5.6.7.8",
|
||||
"1.2.3.4",
|
||||
"up",
|
||||
40,
|
||||
0,
|
||||
@@ -91,4 +97,124 @@ describe("service groups health enrichment", () => {
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("group badge ignores disabled service health and pool-only dead IPs", async () => {
|
||||
const app = await buildApp({
|
||||
config: { ...loadConfig(), staticDir: null },
|
||||
memory: true,
|
||||
});
|
||||
const headers = await authHeaders(app);
|
||||
|
||||
const domain = repos.createDomain(app.db, null, "rkns.top", "zone-tg");
|
||||
const group = repos.createServiceGroup(
|
||||
app.db,
|
||||
"TG Proxy",
|
||||
"custom",
|
||||
null,
|
||||
"gt.rkns.top",
|
||||
);
|
||||
repos.updateServiceGroup(app.db, group.id, "TG Proxy", "custom", null, "gt.rkns.top", {
|
||||
health_check_enabled: true,
|
||||
health_check_type: "tcp",
|
||||
health_check_port: 443,
|
||||
});
|
||||
|
||||
const live = repos.createService(app.db, "Live", "live");
|
||||
repos.setServiceGroup(app.db, live.id, group.id);
|
||||
repos.setServiceEnabled(app.db, live.id, true);
|
||||
repos.replaceServiceIps(app.db, live.id, ["10.0.0.1", "10.0.0.99"]);
|
||||
const liveBinding = repos.insertBinding(app.db, domain.id, live.id, "gt", null);
|
||||
repos.replaceBindingIpsWithMeta(app.db, liveBinding.id, [
|
||||
{ ip: "10.0.0.1", weight: 1, priority: 1 },
|
||||
]);
|
||||
|
||||
const dead = repos.createService(app.db, "Dead", "dead");
|
||||
repos.setServiceGroup(app.db, dead.id, group.id);
|
||||
repos.setServiceEnabled(app.db, dead.id, false);
|
||||
const deadBinding = repos.insertBinding(app.db, domain.id, dead.id, "old", null);
|
||||
repos.replaceBindingIpsWithMeta(app.db, deadBinding.id, [
|
||||
{ ip: "10.0.0.2", weight: 1, priority: 1 },
|
||||
]);
|
||||
|
||||
// Live binding + group FQDN are up; pool-only IP and disabled service are down.
|
||||
repos.upsertIpHealthStatus(app.db, "binding", liveBinding.id, "10.0.0.1", "up", 20, 0, null);
|
||||
repos.upsertIpHealthStatus(app.db, "group", group.id, "10.0.0.1", "up", 20, 0, null);
|
||||
repos.upsertIpHealthStatus(app.db, "group", group.id, "10.0.0.99", "down", null, 5, "timeout");
|
||||
repos.upsertIpHealthStatus(app.db, "binding", deadBinding.id, "10.0.0.2", "down", null, 5, "timeout");
|
||||
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/v1/service-groups",
|
||||
headers,
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = res.json() as {
|
||||
groups: Array<{ id: number; health_status: string }>;
|
||||
};
|
||||
const groupView = body.groups.find((g) => g.id === group.id);
|
||||
expect(groupView?.health_status).toBe("up");
|
||||
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("group health check targets", () => {
|
||||
it("probes binding IPs for group scope, not unused pool IPs", () => {
|
||||
const { db, sqlite } = createMemoryDb();
|
||||
runMigrations(sqlite);
|
||||
|
||||
const domain = repos.createDomain(db, null, "rkns.top", "zone-1");
|
||||
const group = repos.createServiceGroup(db, "TG Proxy", "custom", null, "gt.rkns.top");
|
||||
repos.updateServiceGroup(db, group.id, "TG Proxy", "custom", null, "gt.rkns.top", {
|
||||
health_check_enabled: true,
|
||||
health_check_type: "tcp",
|
||||
health_check_port: 443,
|
||||
});
|
||||
const service = repos.createService(db, "Svc", "svc");
|
||||
repos.setServiceGroup(db, service.id, group.id);
|
||||
repos.setServiceEnabled(db, service.id, true);
|
||||
repos.replaceServiceIps(db, service.id, ["10.0.0.1", "10.0.0.99"]);
|
||||
const binding = repos.insertBinding(db, domain.id, service.id, "gt", null);
|
||||
repos.replaceBindingIpsWithMeta(db, binding.id, [
|
||||
{ ip: "10.0.0.1", weight: 1, priority: 1 },
|
||||
]);
|
||||
|
||||
const targets = repos.listHealthCheckTargets(db);
|
||||
const groupScope = targets.filter((t) => t.scope === "group" && t.ref_id === group.id);
|
||||
expect(groupScope.map((t) => t.ip).sort()).toEqual(["10.0.0.1"]);
|
||||
expect(groupScope.every((t) => t.hostname === "gt.rkns.top")).toBe(true);
|
||||
});
|
||||
|
||||
it("pruneStaleIpHealthStatus drops pool-only and orphan group rows", () => {
|
||||
const { db, sqlite } = createMemoryDb();
|
||||
runMigrations(sqlite);
|
||||
|
||||
const domain = repos.createDomain(db, null, "rkns.top", "zone-1");
|
||||
const group = repos.createServiceGroup(db, "TG Proxy", "custom", null, "gt.rkns.top");
|
||||
repos.updateServiceGroup(db, group.id, "TG Proxy", "custom", null, "gt.rkns.top", {
|
||||
health_check_enabled: true,
|
||||
health_check_type: "tcp",
|
||||
health_check_port: 443,
|
||||
});
|
||||
const service = repos.createService(db, "Svc", "svc");
|
||||
repos.setServiceGroup(db, service.id, group.id);
|
||||
repos.setServiceEnabled(db, service.id, true);
|
||||
repos.replaceServiceIps(db, service.id, ["10.0.0.1", "10.0.0.99"]);
|
||||
const binding = repos.insertBinding(db, domain.id, service.id, "gt", null);
|
||||
repos.replaceBindingIpsWithMeta(db, binding.id, [
|
||||
{ ip: "10.0.0.1", weight: 1, priority: 1 },
|
||||
]);
|
||||
|
||||
repos.upsertIpHealthStatus(db, "group", group.id, "10.0.0.1", "up", 10, 0, null);
|
||||
repos.upsertIpHealthStatus(db, "group", group.id, "10.0.0.99", "down", null, 3, "dead pool");
|
||||
repos.upsertIpHealthStatus(db, "group", 999, "1.1.1.1", "down", null, 3, "orphan ref");
|
||||
|
||||
const targets = repos.listHealthCheckTargets(db);
|
||||
const deleted = repos.pruneStaleIpHealthStatus(db, targets);
|
||||
expect(deleted).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const rows = repos.listIpHealthStatus(db, "group", group.id);
|
||||
expect(rows.map((r) => r.ip)).toEqual(["10.0.0.1"]);
|
||||
expect(repos.listIpHealthStatus(db, "group", 999)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user