Merge pull request #26 from codex/agent-config-tier
feat(agent-config): add typed dashboard mutation API
This commit is contained in:
commit
eeac37e58e
5 changed files with 1902 additions and 6 deletions
|
|
@ -1,6 +1,11 @@
|
||||||
export {
|
export {
|
||||||
DASHBOARD_SCHEMA_VERSION,
|
DASHBOARD_SCHEMA_VERSION,
|
||||||
DashboardDocumentSchema,
|
DashboardDocumentSchema,
|
||||||
|
DatasourceReferenceSchema,
|
||||||
|
ServiceEntrySchema,
|
||||||
|
ServiceGroupSchema,
|
||||||
|
TelemetryCardSchema,
|
||||||
|
ThresholdSchema,
|
||||||
dashboardDocumentJsonSchema,
|
dashboardDocumentJsonSchema,
|
||||||
type DashboardDocument,
|
type DashboardDocument,
|
||||||
type DashboardModule,
|
type DashboardModule,
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ const LinkSchema = Type.Object(
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
const StaticDatasourceSchema = Type.Object(
|
export const StaticDatasourceSchema = Type.Object(
|
||||||
{
|
{
|
||||||
type: Type.Literal("static"),
|
type: Type.Literal("static"),
|
||||||
label: Type.Optional(Type.String({ minLength: 1 })),
|
label: Type.Optional(Type.String({ minLength: 1 })),
|
||||||
|
|
@ -36,7 +36,7 @@ const StaticDatasourceSchema = Type.Object(
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
const PlaceholderDatasourceSchema = Type.Object(
|
export const PlaceholderDatasourceSchema = Type.Object(
|
||||||
{
|
{
|
||||||
type: Type.Literal("placeholder"),
|
type: Type.Literal("placeholder"),
|
||||||
reason: Type.String({ minLength: 1 }),
|
reason: Type.String({ minLength: 1 }),
|
||||||
|
|
@ -44,7 +44,7 @@ const PlaceholderDatasourceSchema = Type.Object(
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
const ExternalDatasourceSchema = Type.Object(
|
export const ExternalDatasourceSchema = Type.Object(
|
||||||
{
|
{
|
||||||
type: Type.Literal("external"),
|
type: Type.Literal("external"),
|
||||||
adapter: Type.Union([
|
adapter: Type.Union([
|
||||||
|
|
@ -58,7 +58,7 @@ const ExternalDatasourceSchema = Type.Object(
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
const DatasourceReferenceSchema = Type.Union([
|
export const DatasourceReferenceSchema = Type.Union([
|
||||||
StaticDatasourceSchema,
|
StaticDatasourceSchema,
|
||||||
PlaceholderDatasourceSchema,
|
PlaceholderDatasourceSchema,
|
||||||
ExternalDatasourceSchema,
|
ExternalDatasourceSchema,
|
||||||
|
|
@ -98,13 +98,13 @@ const TextMetricValueSchema = Type.Object(
|
||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
const MetricValueSchema = Type.Union([
|
export const MetricValueSchema = Type.Union([
|
||||||
PercentMetricValueSchema,
|
PercentMetricValueSchema,
|
||||||
NonNegativeMetricValueSchema,
|
NonNegativeMetricValueSchema,
|
||||||
TextMetricValueSchema,
|
TextMetricValueSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ThresholdSchema = Type.Object(
|
export const ThresholdSchema = Type.Object(
|
||||||
{
|
{
|
||||||
warning: Type.Optional(Type.Number({ minimum: 0 })),
|
warning: Type.Optional(Type.Number({ minimum: 0 })),
|
||||||
danger: Type.Optional(Type.Number({ minimum: 0 })),
|
danger: Type.Optional(Type.Number({ minimum: 0 })),
|
||||||
|
|
|
||||||
595
src/lib/server/agent-config/agent-config.test.ts
Normal file
595
src/lib/server/agent-config/agent-config.test.ts
Normal file
|
|
@ -0,0 +1,595 @@
|
||||||
|
import { existsSync, rmSync } from "node:fs";
|
||||||
|
import { mkdtemp } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { afterEach, describe, expect, test } from "vitest";
|
||||||
|
import type { DashboardDocument } from "$lib/model";
|
||||||
|
import { genericDashboardFixture } from "$lib/model/fixtures/generic";
|
||||||
|
import { createDashboardStore, type DashboardStore } from "$lib/server/db/dashboard-store";
|
||||||
|
import {
|
||||||
|
AgentConfigAuthorizationError,
|
||||||
|
authorizeAgentConfigRequest,
|
||||||
|
handleAgentDashboardRequest,
|
||||||
|
previewDashboardChanges,
|
||||||
|
publishDashboardChanges,
|
||||||
|
rollbackDashboardRevision,
|
||||||
|
type AgentDashboardOperation,
|
||||||
|
type JsonPatchOperation,
|
||||||
|
} from ".";
|
||||||
|
|
||||||
|
const stores: DashboardStore[] = [];
|
||||||
|
const tempRoots: string[] = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
stores.splice(0).forEach((store) => store.close());
|
||||||
|
tempRoots.splice(0).forEach((path) => rmSync(path, { force: true, recursive: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("agent dashboard configuration API", () => {
|
||||||
|
test("previews typed dashboard operations with an RFC 6902-compatible patch", () => {
|
||||||
|
const operations = exampleOperations();
|
||||||
|
|
||||||
|
const result = previewDashboardChanges(genericDashboardFixture, operations);
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (!result.ok) throw new Error("expected preview success");
|
||||||
|
expect(result.document.serviceGroups[0]?.id).toBe("edge");
|
||||||
|
expect(result.document.serviceGroups[0]?.services[0]).toMatchObject({
|
||||||
|
id: "edge-router",
|
||||||
|
datasource: {
|
||||||
|
type: "external",
|
||||||
|
adapter: "http-status",
|
||||||
|
reference: "GET https://edge.example.test/api/status",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(result.document.layout.telemetry).toEqual([
|
||||||
|
"service-uptime",
|
||||||
|
"edge-latency",
|
||||||
|
"queue-depth",
|
||||||
|
]);
|
||||||
|
expect(genericDashboardFixture.serviceGroups.map((group) => group.id)).toEqual([
|
||||||
|
"core-services",
|
||||||
|
]);
|
||||||
|
expect(result.patch.length).toBeGreaterThan(0);
|
||||||
|
expect(result.patch.every((operation) => operation.path.startsWith("/"))).toBe(true);
|
||||||
|
expect(result.patch.map((operation) => operation.op)).toContain("add");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns structured repairable errors for invalid operations", () => {
|
||||||
|
const result = previewDashboardChanges(genericDashboardFixture, [
|
||||||
|
{
|
||||||
|
type: "add_service",
|
||||||
|
groupId: "missing-group",
|
||||||
|
service: exampleService(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (result.ok) throw new Error("expected preview failure");
|
||||||
|
expect(result.errors).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
code: "group_not_found",
|
||||||
|
operationIndex: 0,
|
||||||
|
path: "/serviceGroups",
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects unsupported target-specific mutations", () => {
|
||||||
|
const datasourceResult = previewDashboardChanges(genericDashboardFixture, [
|
||||||
|
{
|
||||||
|
type: "connect_datasource",
|
||||||
|
target: { kind: "statusItem", stripId: "runtime", id: "status" },
|
||||||
|
datasource: {
|
||||||
|
type: "external",
|
||||||
|
adapter: "http-status",
|
||||||
|
reference: "GET https://status.example.test/api",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(datasourceResult.ok).toBe(false);
|
||||||
|
if (datasourceResult.ok) throw new Error("expected datasource failure");
|
||||||
|
expect(datasourceResult.errors[0]).toMatchObject({
|
||||||
|
code: "unsupported_target",
|
||||||
|
operationIndex: 0,
|
||||||
|
path: "/statusStrips",
|
||||||
|
});
|
||||||
|
|
||||||
|
const thresholdResult = previewDashboardChanges(genericDashboardFixture, [
|
||||||
|
{
|
||||||
|
type: "set_status_rule",
|
||||||
|
target: { kind: "service", id: "identity" },
|
||||||
|
thresholds: { warning: 1 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(thresholdResult.ok).toBe(false);
|
||||||
|
if (thresholdResult.ok) throw new Error("expected threshold failure");
|
||||||
|
expect(thresholdResult.errors[0]).toMatchObject({
|
||||||
|
code: "unsupported_target",
|
||||||
|
operationIndex: 0,
|
||||||
|
path: "/serviceGroups",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("removes status strip items through the shared remove operation", () => {
|
||||||
|
const result = previewDashboardChanges(genericDashboardFixture, [
|
||||||
|
{
|
||||||
|
type: "remove_item",
|
||||||
|
target: { kind: "statusItem", stripId: "runtime", id: "sync" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (!result.ok) throw new Error("expected remove success");
|
||||||
|
expect(result.document.statusStrips[0]?.items.map((item) => item.id)).toEqual([
|
||||||
|
"status",
|
||||||
|
]);
|
||||||
|
expect(result.patch.map((operation) => operation.op)).toContain("remove");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns RFC 6902-applicable patches for multiple array removals", () => {
|
||||||
|
const result = previewDashboardChanges(genericDashboardFixture, [
|
||||||
|
{
|
||||||
|
type: "remove_item",
|
||||||
|
target: { kind: "statusItem", stripId: "runtime", id: "status" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "remove_item",
|
||||||
|
target: { kind: "statusItem", stripId: "runtime", id: "sync" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (!result.ok) throw new Error("expected remove success");
|
||||||
|
expect(
|
||||||
|
result.patch
|
||||||
|
.filter((operation) => operation.op === "remove")
|
||||||
|
.map((operation) => operation.path),
|
||||||
|
).toEqual(["/statusStrips/0/items/1", "/statusStrips/0/items/0"]);
|
||||||
|
expect(applyJsonPatch(genericDashboardFixture, result.patch)).toMatchObject({
|
||||||
|
statusStrips: [{ items: [] }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects ambiguous service and status targets", () => {
|
||||||
|
const duplicateDocument = documentWithDuplicateNestedIds();
|
||||||
|
|
||||||
|
const connect = previewDashboardChanges(duplicateDocument, [
|
||||||
|
{
|
||||||
|
type: "connect_datasource",
|
||||||
|
target: { kind: "service", id: "identity" },
|
||||||
|
datasource: {
|
||||||
|
type: "external",
|
||||||
|
adapter: "http-status",
|
||||||
|
reference: "GET https://identity.example.test/health",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(connect.ok).toBe(false);
|
||||||
|
if (connect.ok) throw new Error("expected ambiguous service failure");
|
||||||
|
expect(connect.errors[0]).toMatchObject({
|
||||||
|
code: "ambiguous_target",
|
||||||
|
operationIndex: 0,
|
||||||
|
path: "/serviceGroups",
|
||||||
|
});
|
||||||
|
|
||||||
|
const remove = previewDashboardChanges(duplicateDocument, [
|
||||||
|
{
|
||||||
|
type: "remove_item",
|
||||||
|
target: { kind: "service", id: "identity" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(remove.ok).toBe(false);
|
||||||
|
if (remove.ok) throw new Error("expected ambiguous removal failure");
|
||||||
|
expect(remove.errors[0]).toMatchObject({
|
||||||
|
code: "ambiguous_target",
|
||||||
|
operationIndex: 0,
|
||||||
|
path: "/serviceGroups",
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = previewDashboardChanges(duplicateDocument, [
|
||||||
|
{
|
||||||
|
type: "set_status_rule",
|
||||||
|
target: { kind: "statusItem", id: "status" },
|
||||||
|
value: "Healthy",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(status.ok).toBe(false);
|
||||||
|
if (status.ok) throw new Error("expected ambiguous status failure");
|
||||||
|
expect(status.errors[0]).toMatchObject({
|
||||||
|
code: "ambiguous_target",
|
||||||
|
operationIndex: 0,
|
||||||
|
path: "/statusStrips",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects create_dashboard when it is not the first operation", () => {
|
||||||
|
const result = previewDashboardChanges(genericDashboardFixture, [
|
||||||
|
{
|
||||||
|
type: "remove_item",
|
||||||
|
target: { kind: "telemetry", id: "queue-depth" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "create_dashboard",
|
||||||
|
document: genericDashboardFixture,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (result.ok) throw new Error("expected sequence failure");
|
||||||
|
expect(result.errors[0]).toMatchObject({
|
||||||
|
code: "invalid_operation_sequence",
|
||||||
|
operationIndex: 1,
|
||||||
|
path: "/1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("publishes valid operations as a persisted dashboard revision", async () => {
|
||||||
|
const { dbPath, store } = await createTestStore();
|
||||||
|
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
|
||||||
|
actor: "seed",
|
||||||
|
message: "initial dashboard",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = publishDashboardChanges(store, exampleOperations(), {
|
||||||
|
actor: "agent",
|
||||||
|
message: "add edge router",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (!result.ok) throw new Error("expected publish success");
|
||||||
|
expect(existsSync(dbPath)).toBe(true);
|
||||||
|
expect(result.revision.operation).toBe("commit");
|
||||||
|
expect(result.revision.actor).toBe("agent");
|
||||||
|
expect(result.revision.message).toBe("add edge router");
|
||||||
|
expect(result.previousRevisionId).toBe(seed.id);
|
||||||
|
expect(result.patch.length).toBeGreaterThan(0);
|
||||||
|
expect(store.getActiveDashboard()?.document.serviceGroups[0]?.id).toBe("edge");
|
||||||
|
expect(store.listRevisions()).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not publish invalid operations", async () => {
|
||||||
|
const { store } = await createTestStore();
|
||||||
|
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
|
||||||
|
actor: "seed",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = publishDashboardChanges(store, [
|
||||||
|
{
|
||||||
|
type: "remove_item",
|
||||||
|
target: { kind: "telemetry", id: "missing-metric" },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
if (result.ok) throw new Error("expected publish failure");
|
||||||
|
expect(result.errors[0]).toMatchObject({
|
||||||
|
code: "item_not_found",
|
||||||
|
operationIndex: 0,
|
||||||
|
});
|
||||||
|
expect(store.getActiveDashboard()?.currentRevisionId).toBe(seed.id);
|
||||||
|
expect(store.listRevisions()).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rolls back through the agent-safe revision path", async () => {
|
||||||
|
const { store } = await createTestStore();
|
||||||
|
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
|
||||||
|
actor: "seed",
|
||||||
|
});
|
||||||
|
const publish = publishDashboardChanges(store, exampleOperations(), {
|
||||||
|
actor: "agent",
|
||||||
|
});
|
||||||
|
expect(publish.ok).toBe(true);
|
||||||
|
|
||||||
|
const rollback = rollbackDashboardRevision(store, seed.id, {
|
||||||
|
actor: "agent",
|
||||||
|
message: "restore previous dashboard",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(rollback.operation).toBe("rollback");
|
||||||
|
expect(rollback.actor).toBe("agent");
|
||||||
|
expect(rollback.sourceRevisionId).toBe(seed.id);
|
||||||
|
expect(store.getActiveDashboard()?.document.metadata.title).toBe("Operations Console");
|
||||||
|
expect(store.listRevisions().map((revision) => revision.operation)).toEqual([
|
||||||
|
"rollback",
|
||||||
|
"commit",
|
||||||
|
"seed",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("authenticates agent configuration requests with a shared token", () => {
|
||||||
|
const authorized = new Request("https://dimensionlab.test/api/agent/dashboard", {
|
||||||
|
headers: { authorization: "Bearer shared-secret" },
|
||||||
|
});
|
||||||
|
const rejected = new Request("https://dimensionlab.test/api/agent/dashboard");
|
||||||
|
|
||||||
|
expect(authorizeAgentConfigRequest(authorized, "shared-secret")).toEqual({
|
||||||
|
ok: true,
|
||||||
|
});
|
||||||
|
expect(() => authorizeAgentConfigRequest(rejected, "shared-secret")).toThrow(
|
||||||
|
AgentConfigAuthorizationError,
|
||||||
|
);
|
||||||
|
expect(() => authorizeAgentConfigRequest(authorized, "")).toThrow(
|
||||||
|
AgentConfigAuthorizationError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles preview, publish, and rollback requests through the HTTP adapter", async () => {
|
||||||
|
const { store } = await createTestStore();
|
||||||
|
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
|
||||||
|
actor: "seed",
|
||||||
|
});
|
||||||
|
|
||||||
|
const preview = await handleAgentDashboardRequest(
|
||||||
|
jsonRequest({
|
||||||
|
action: "preview_changes",
|
||||||
|
operations: exampleOperations(),
|
||||||
|
}),
|
||||||
|
{ store, token: "shared-secret" },
|
||||||
|
);
|
||||||
|
expect(preview.status).toBe(200);
|
||||||
|
expect(await preview.json()).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
action: "preview_changes",
|
||||||
|
patch: expect.any(Array),
|
||||||
|
});
|
||||||
|
expect(store.listRevisions()).toHaveLength(1);
|
||||||
|
|
||||||
|
const publish = await handleAgentDashboardRequest(
|
||||||
|
jsonRequest({
|
||||||
|
action: "publish_changes",
|
||||||
|
actor: "agent",
|
||||||
|
message: "publish edge router",
|
||||||
|
operations: exampleOperations(),
|
||||||
|
}),
|
||||||
|
{ store, token: "shared-secret" },
|
||||||
|
);
|
||||||
|
expect(publish.status).toBe(200);
|
||||||
|
expect(await publish.json()).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
action: "publish_changes",
|
||||||
|
revision: { operation: "commit", actor: "agent" },
|
||||||
|
});
|
||||||
|
expect(store.listRevisions()).toHaveLength(2);
|
||||||
|
|
||||||
|
const rollback = await handleAgentDashboardRequest(
|
||||||
|
jsonRequest({
|
||||||
|
action: "rollback_revision",
|
||||||
|
actor: "agent",
|
||||||
|
revisionId: seed.id,
|
||||||
|
}),
|
||||||
|
{ store, token: "shared-secret" },
|
||||||
|
);
|
||||||
|
expect(rollback.status).toBe(200);
|
||||||
|
expect(await rollback.json()).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
action: "rollback_revision",
|
||||||
|
revision: { operation: "rollback", sourceRevisionId: seed.id },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns a structured error for unknown rollback revisions", async () => {
|
||||||
|
const { store } = await createTestStore();
|
||||||
|
store.seedDashboardIfEmpty(genericDashboardFixture, {
|
||||||
|
actor: "seed",
|
||||||
|
});
|
||||||
|
|
||||||
|
const rollback = await handleAgentDashboardRequest(
|
||||||
|
jsonRequest({
|
||||||
|
action: "rollback_revision",
|
||||||
|
actor: "agent",
|
||||||
|
revisionId: "missing-revision",
|
||||||
|
}),
|
||||||
|
{ store, token: "shared-secret" },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(rollback.status).toBe(404);
|
||||||
|
expect(await rollback.json()).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
code: "revision_not_found",
|
||||||
|
path: "/revisionId",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("previews create_dashboard requests against an empty store", async () => {
|
||||||
|
const { store } = await createTestStore();
|
||||||
|
|
||||||
|
const preview = await handleAgentDashboardRequest(
|
||||||
|
jsonRequest({
|
||||||
|
action: "preview_changes",
|
||||||
|
operations: [
|
||||||
|
{
|
||||||
|
type: "create_dashboard",
|
||||||
|
document: genericDashboardFixture,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
{ store, token: "shared-secret" },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(preview.status).toBe(200);
|
||||||
|
expect(await preview.json()).toMatchObject({
|
||||||
|
ok: true,
|
||||||
|
action: "preview_changes",
|
||||||
|
document: {
|
||||||
|
metadata: {
|
||||||
|
title: "Operations Console",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(store.listRevisions()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function createTestStore() {
|
||||||
|
const root = await mkdtemp(join(tmpdir(), "dimensionlab-agent-config-"));
|
||||||
|
tempRoots.push(root);
|
||||||
|
const dbPath = join(root, "dashboard.sqlite");
|
||||||
|
const store = createDashboardStore({
|
||||||
|
databaseUrl: `file:${dbPath}`,
|
||||||
|
});
|
||||||
|
stores.push(store);
|
||||||
|
|
||||||
|
return { dbPath, store };
|
||||||
|
}
|
||||||
|
|
||||||
|
function exampleOperations(): AgentDashboardOperation[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
type: "add_section",
|
||||||
|
section: {
|
||||||
|
id: "edge",
|
||||||
|
title: "Edge",
|
||||||
|
layout: "list",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "add_service",
|
||||||
|
groupId: "edge",
|
||||||
|
service: exampleService(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "add_metric_card",
|
||||||
|
card: {
|
||||||
|
id: "edge-latency",
|
||||||
|
label: "Edge Latency",
|
||||||
|
value: { kind: "latency", value: 12, precision: 0 },
|
||||||
|
severity: "ok",
|
||||||
|
detail: "p95",
|
||||||
|
datasource: {
|
||||||
|
type: "external",
|
||||||
|
adapter: "prometheus",
|
||||||
|
reference: "histogram_quantile(0.95, edge_request_duration_seconds_bucket)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
position: { afterId: "service-uptime" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "connect_datasource",
|
||||||
|
target: {
|
||||||
|
kind: "service",
|
||||||
|
groupId: "edge",
|
||||||
|
id: "edge-router",
|
||||||
|
},
|
||||||
|
datasource: {
|
||||||
|
type: "external",
|
||||||
|
adapter: "http-status",
|
||||||
|
reference: "GET https://edge.example.test/api/status",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "set_status_rule",
|
||||||
|
target: { kind: "telemetry", id: "edge-latency" },
|
||||||
|
thresholds: { warning: 50, danger: 100 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "arrange_item",
|
||||||
|
area: "serviceGroups",
|
||||||
|
id: "edge",
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function exampleService() {
|
||||||
|
return {
|
||||||
|
id: "edge-router",
|
||||||
|
label: "Edge Router",
|
||||||
|
description: "Ingress and routing",
|
||||||
|
icon: "mdi:router-network",
|
||||||
|
severity: "ok" as const,
|
||||||
|
detail: "pending datasource",
|
||||||
|
datasource: { type: "placeholder" as const, reason: "health adapter pending" },
|
||||||
|
link: {
|
||||||
|
href: "https://edge.example.test",
|
||||||
|
label: "Open Edge Router",
|
||||||
|
external: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function documentWithDuplicateNestedIds(): DashboardDocument {
|
||||||
|
const document = structuredClone(genericDashboardFixture);
|
||||||
|
document.layout.serviceGroups.push("secondary-services");
|
||||||
|
document.serviceGroups.push({
|
||||||
|
id: "secondary-services",
|
||||||
|
title: "Secondary Services",
|
||||||
|
layout: "list",
|
||||||
|
services: [
|
||||||
|
{
|
||||||
|
...document.serviceGroups[0].services[0],
|
||||||
|
label: "Shadow Identity",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
document.layout.statusStrips.push("secondary-runtime");
|
||||||
|
document.statusStrips.push({
|
||||||
|
id: "secondary-runtime",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
...document.statusStrips[0].items[0],
|
||||||
|
value: "Healthy",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyJsonPatch<T>(value: T, patch: JsonPatchOperation[]): T {
|
||||||
|
const next = structuredClone(value);
|
||||||
|
for (const operation of patch) {
|
||||||
|
const { parent, key } = jsonPointerTarget(next, operation.path);
|
||||||
|
if (operation.op === "remove") {
|
||||||
|
if (Array.isArray(parent)) {
|
||||||
|
parent.splice(Number(key), 1);
|
||||||
|
} else {
|
||||||
|
delete parent[key];
|
||||||
|
}
|
||||||
|
} else if (operation.op === "add") {
|
||||||
|
if (Array.isArray(parent)) {
|
||||||
|
parent.splice(Number(key), 0, operation.value);
|
||||||
|
} else {
|
||||||
|
parent[key] = operation.value;
|
||||||
|
}
|
||||||
|
} else if (operation.op === "replace") {
|
||||||
|
if (Array.isArray(parent)) {
|
||||||
|
parent[Number(key)] = operation.value;
|
||||||
|
} else {
|
||||||
|
parent[key] = operation.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonPointerTarget(value: unknown, path: string) {
|
||||||
|
const segments = path
|
||||||
|
.split("/")
|
||||||
|
.slice(1)
|
||||||
|
.map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
|
||||||
|
const key = segments.pop();
|
||||||
|
if (key === undefined) throw new Error(`Invalid JSON pointer: ${path}`);
|
||||||
|
|
||||||
|
let parent = value as Record<string, unknown> | unknown[];
|
||||||
|
for (const segment of segments) {
|
||||||
|
parent = Array.isArray(parent)
|
||||||
|
? (parent[Number(segment)] as Record<string, unknown> | unknown[])
|
||||||
|
: (parent[segment] as Record<string, unknown> | unknown[]);
|
||||||
|
}
|
||||||
|
return { parent, key };
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonRequest(body: unknown) {
|
||||||
|
return new Request("https://dimensionlab.test/api/agent/dashboard", {
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: {
|
||||||
|
authorization: "Bearer shared-secret",
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
}
|
||||||
1292
src/lib/server/agent-config/index.ts
Normal file
1292
src/lib/server/agent-config/index.ts
Normal file
File diff suppressed because it is too large
Load diff
4
src/routes/api/agent/dashboard/+server.ts
Normal file
4
src/routes/api/agent/dashboard/+server.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { handleAgentDashboardRequest } from "$lib/server/agent-config";
|
||||||
|
import type { RequestHandler } from "./$types";
|
||||||
|
|
||||||
|
export const POST: RequestHandler = ({ request }) => handleAgentDashboardRequest(request);
|
||||||
Loading…
Add table
Add a link
Reference in a new issue