This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
The following commit(s) were added to refs/heads/main by this push:
new e24ec2a bff: split http/ into query/ config/ admin/ sub-folders
e24ec2a is described below
commit e24ec2a9c13b8e841fc0b13698436545ea81e5c4
Author: Wu Sheng <[email protected]>
AuthorDate: Sat May 16 22:50:16 2026 +0800
bff: split http/ into query/ config/ admin/ sub-folders
Drop the flat http/ folder and group the 23 route files by functional
role per CLAUDE.md's layering:
http/query/ read-only OAP queries (menu, landing, log, trace,
zipkin, instance, endpoint, endpoint-dependency,
topology, profile, ebpf, async-profile, info,
preflight + the runtime POST /api/layer/:key/dashboard
+ GET /api/alarms*)
http/config/ CRUD for templates / settings (dashboard config,
layer-template admin, alarms config, setup, overview)
http/admin/ operational admin (dsl/{catalog,rule,dump,oal},
cluster, live-debug, inspect)
Three mixed-scope files were split:
dashboard.ts (697 LOC) →
query/dashboard.ts POST /api/layer/:key/dashboard
config/dashboard.ts GET /api/layer/:key/dashboard/config
config/layer-template.ts GET/POST /api/admin/layer-templates*
alarms.ts (601 LOC) →
query/alarms.ts GET /api/alarms{,/traffic,/services}
config/alarms.ts GET/POST /api/alarms/config
The shared ServiceLayerMap is now constructed in server.ts and
passed to both files so a config save still invalidates the cache
backing the list call.
dsl.ts (482 LOC) →
admin/dsl/_shared.ts DslRouteDeps + verb/audit/parse helpers
admin/dsl/catalog.ts GET /api/catalog{,/list,/bundled}
admin/dsl/rule.ts /api/rule{,/inactivate,/delete}
admin/dsl/dump.ts GET /api/dump{,/:catalog}
admin/dsl/oal.ts /api/oal/{files,rules}{,/:name}
admin/cluster.ts GET /api/cluster/state (was lumped in dsl)
server.ts is now organised into User / Query / Config / Admin bands
mirroring the folder layout. tsc / esbuild bundle / license-eye all
green. Pure restructure — no behavior changes.
---
apps/bff/src/client/cluster.ts | 2 +-
apps/bff/src/http/admin/cluster.ts | 54 +++
apps/bff/src/http/admin/dsl/_shared.ts | 172 ++++++++
apps/bff/src/http/admin/dsl/catalog.ts | 74 ++++
apps/bff/src/http/admin/dsl/dump.ts | 71 +++
apps/bff/src/http/admin/dsl/oal.ts | 101 +++++
apps/bff/src/http/admin/dsl/rule.ts | 166 +++++++
apps/bff/src/http/{ => admin}/inspect.ts | 20 +-
.../bff/src/http/{debug.ts => admin/live-debug.ts} | 14 +-
apps/bff/src/http/config/alarms.ts | 91 ++++
apps/bff/src/http/config/dashboard.ts | 61 +++
apps/bff/src/http/config/layer-template.ts | 166 +++++++
apps/bff/src/http/{ => config}/overview.ts | 8 +-
apps/bff/src/http/{ => config}/setup.ts | 12 +-
apps/bff/src/http/dsl.ts | 482 ---------------------
apps/bff/src/http/{ => query}/alarms.ts | 74 +---
apps/bff/src/http/{ => query}/async-profile.ts | 8 +-
apps/bff/src/http/{ => query}/dashboard.ts | 164 +------
apps/bff/src/http/{ => query}/ebpf.ts | 8 +-
.../src/http/{ => query}/endpoint-dependency.ts | 10 +-
apps/bff/src/http/{ => query}/endpoint.ts | 8 +-
apps/bff/src/http/{ => query}/info.ts | 8 +-
apps/bff/src/http/{ => query}/instance.ts | 8 +-
apps/bff/src/http/{ => query}/landing.ts | 10 +-
apps/bff/src/http/{ => query}/log.ts | 8 +-
apps/bff/src/http/{ => query}/menu.ts | 12 +-
apps/bff/src/http/{ => query}/preflight.ts | 8 +-
apps/bff/src/http/{ => query}/profile.ts | 8 +-
apps/bff/src/http/{ => query}/topology.ts | 10 +-
apps/bff/src/http/{ => query}/trace-tag.ts | 8 +-
apps/bff/src/http/{ => query}/trace.ts | 14 +-
apps/bff/src/http/{ => query}/zipkin.ts | 8 +-
apps/bff/src/http/user.ts | 4 +-
apps/bff/src/logic/alarms/service-layer-map.ts | 6 +-
apps/bff/src/logic/alarms/store.ts | 2 +-
apps/bff/src/logic/inspect/exec.ts | 2 +-
apps/bff/src/logic/preflight/preflight.ts | 2 +-
apps/bff/src/logic/setup/store.ts | 2 +-
apps/bff/src/rbac/policy.ts | 4 +-
apps/bff/src/server.ts | 100 +++--
apps/bff/src/util/trace-protocol-cache.ts | 2 +-
apps/ui/src/views/auth/LoginView.vue | 11 -
42 files changed, 1155 insertions(+), 848 deletions(-)
diff --git a/apps/bff/src/client/cluster.ts b/apps/bff/src/client/cluster.ts
index 9257618..a0182b7 100644
--- a/apps/bff/src/client/cluster.ts
+++ b/apps/bff/src/client/cluster.ts
@@ -32,7 +32,7 @@ import type {
LocalState,
RuleStatus,
} from '@skywalking-horizon-ui/api-client';
-import type { OapClients } from './clients.js';
+import type { OapClients } from './index.js';
export interface NodeListResult {
url: string;
diff --git a/apps/bff/src/http/admin/cluster.ts
b/apps/bff/src/http/admin/cluster.ts
new file mode 100644
index 0000000..e90636d
--- /dev/null
+++ b/apps/bff/src/http/admin/cluster.ts
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * `GET /api/cluster/state` — fan-out `/runtime/rule/list` to every
+ * configured OAP admin URL and pivot the result into the per-rule ×
+ * per-node matrix the SPA renders. Gated on `cluster:read`. Not part
+ * of the DSL family proper — kept here under admin/ because it shares
+ * the same OAP-admin auth surface.
+ */
+
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
+import type { FetchLike } from '@skywalking-horizon-ui/api-client';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { requireAuth } from '../../user/middleware.js';
+import { ensureVerb, makeClients } from './dsl/_shared.js';
+import { fetchPerNode, pivotClusterState } from '../../client/cluster.js';
+import type { AuditLogger } from '../../audit/logger.js';
+
+export interface ClusterRouteDeps {
+ config: ConfigSource;
+ sessions: SessionStore;
+ audit: AuditLogger;
+ fetch?: FetchLike;
+}
+
+export function registerClusterRoutes(app: FastifyInstance, deps:
ClusterRouteDeps): void {
+ const auth = requireAuth(deps);
+ const clients = makeClients(deps);
+ app.get(
+ '/api/cluster/state',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ if (!ensureVerb(req, reply, deps, 'cluster:read')) return;
+ const perNode = await fetchPerNode(clients());
+ return reply.send(pivotClusterState(perNode));
+ },
+ );
+}
diff --git a/apps/bff/src/http/admin/dsl/_shared.ts
b/apps/bff/src/http/admin/dsl/_shared.ts
new file mode 100644
index 0000000..c3583b3
--- /dev/null
+++ b/apps/bff/src/http/admin/dsl/_shared.ts
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Shared deps + parsers + audit/verb helpers for every DSL admin handler
+ * (catalog / rule / dump / oal). Lives next to the routes so each handler
+ * file stays small enough to read in one screen.
+ */
+
+import type { FastifyReply, FastifyRequest } from 'fastify';
+import {
+ RuntimeRuleApiError,
+ isCatalog,
+ type Catalog,
+ type DeleteMode,
+ type FetchLike,
+} from '@skywalking-horizon-ui/api-client';
+import type { ConfigSource } from '../../../config/loader.js';
+import type { AuditLogger } from '../../../audit/logger.js';
+import type { Session, SessionStore } from '../../../user/sessions.js';
+import { sessionHasVerb } from '../../../rbac/policy.js';
+import { buildOapClients, type OapClients } from '../../../client/index.js';
+
+export interface DslRouteDeps {
+ config: ConfigSource;
+ sessions: SessionStore;
+ audit: AuditLogger;
+ /** Test seam — replaces `globalThis.fetch` in every OAP call. */
+ fetch?: FetchLike;
+}
+
+export function makeClients(deps: DslRouteDeps): () => OapClients {
+ return () => buildOapClients(deps.config.current, { fetch: deps.fetch });
+}
+
+const TRUTHY = new Set(['true', '1', 'yes']);
+export function parseBoolean(v: string | undefined, fallback: boolean):
boolean {
+ if (v === undefined) return fallback;
+ return TRUTHY.has(v.toLowerCase());
+}
+
+export function hasCatalogParam(q: unknown): boolean {
+ return typeof q === 'object' && q !== null && 'catalog' in q;
+}
+
+/** When `catalog` is missing → returns `undefined`. When present but
+ * invalid → sends 400 and returns `undefined`; callers use
+ * {@link hasCatalogParam} to disambiguate. */
+export function parseOptionalCatalog(q: unknown, reply: FastifyReply): Catalog
| undefined {
+ const raw = (q as Record<string, string | undefined>).catalog;
+ if (raw === undefined || raw === '') return undefined;
+ if (!isCatalog(raw)) {
+ reply.code(400).send({ error: 'invalid_catalog', value: raw });
+ return undefined;
+ }
+ return raw;
+}
+
+export function parseRequiredCatalog(q: unknown, reply: FastifyReply): Catalog
| null {
+ const raw = (q as Record<string, string | undefined>).catalog;
+ if (!raw) {
+ reply.code(400).send({ error: 'missing_catalog' });
+ return null;
+ }
+ if (!isCatalog(raw)) {
+ reply.code(400).send({ error: 'invalid_catalog', value: raw });
+ return null;
+ }
+ return raw;
+}
+
+export function parseDeleteMode(raw: string | undefined, reply: FastifyReply):
DeleteMode | null {
+ if (raw === undefined || raw === '') return '';
+ if (raw === 'revertToBundled') return 'revertToBundled';
+ reply.code(400).send({ error: 'invalid_delete_mode', value: raw });
+ return null;
+}
+
+export function ensureVerb(
+ req: FastifyRequest,
+ reply: FastifyReply,
+ deps: DslRouteDeps,
+ verb: string,
+): boolean {
+ const session: Session | undefined = req.session;
+ if (!session) {
+ reply.code(401).send({ error: 'unauthenticated' });
+ return false;
+ }
+ if (!sessionHasVerb(deps.config.current, session.roles, verb)) {
+ reply.code(403).send({ error: 'permission_denied', verb });
+ return false;
+ }
+ return true;
+}
+
+export function passOapError(err: unknown, reply: FastifyReply): FastifyReply {
+ if (err instanceof RuntimeRuleApiError) {
+ return reply.code(err.status).send(err.body);
+ }
+ return reply.code(502).send({
+ error: 'oap_unreachable',
+ message: err instanceof Error ? err.message : String(err),
+ });
+}
+
+export function passOapErrorAudit(
+ err: unknown,
+ reply: FastifyReply,
+ deps: DslRouteDeps,
+ req: FastifyRequest,
+ action: string,
+ verb: string,
+ catalog: Catalog,
+ name: string,
+ details: Record<string, unknown> = {},
+): FastifyReply {
+ let outcome = 'oap_unreachable';
+ if (err instanceof RuntimeRuleApiError) {
+ const apiErr: RuntimeRuleApiError = err;
+ const body = apiErr.body;
+ outcome =
+ typeof body === 'object' && body !== null && 'applyStatus' in body
+ ? body.applyStatus
+ : `http_${apiErr.status}`;
+ }
+ deps.audit.record({
+ action,
+ verb,
+ actor: req.session?.username ?? null,
+ outcome,
+ details: { catalog, name, ...details },
+ fromIp: req.ip,
+ sessionId: req.session?.sid,
+ });
+ return passOapError(err, reply);
+}
+
+export function auditMutation(
+ deps: DslRouteDeps,
+ req: FastifyRequest,
+ action: string,
+ verb: string,
+ catalog: Catalog,
+ name: string,
+ outcome: string,
+ details: Record<string, unknown> = {},
+): void {
+ deps.audit.record({
+ action,
+ verb,
+ actor: req.session?.username ?? null,
+ outcome,
+ details: { catalog, name, ...details },
+ fromIp: req.ip,
+ sessionId: req.session?.sid,
+ });
+}
diff --git a/apps/bff/src/http/admin/dsl/catalog.ts
b/apps/bff/src/http/admin/dsl/catalog.ts
new file mode 100644
index 0000000..9bda086
--- /dev/null
+++ b/apps/bff/src/http/admin/dsl/catalog.ts
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * GET /api/catalog/list — runtime + bundled rule list per catalog.
+ * GET /api/catalog/bundled — bundled-only list (with raw content opt).
+ *
+ * Both gated on `rule:read`; read-only — no audit.
+ */
+
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
+import { requireAuth } from '../../../user/middleware.js';
+import {
+ type DslRouteDeps,
+ ensureVerb,
+ hasCatalogParam,
+ makeClients,
+ parseBoolean,
+ parseOptionalCatalog,
+ parseRequiredCatalog,
+ passOapError,
+} from './_shared.js';
+
+export function registerDslCatalogRoutes(app: FastifyInstance, deps:
DslRouteDeps): void {
+ const auth = requireAuth(deps);
+ const clients = makeClients(deps);
+
+ app.get(
+ '/api/catalog/list',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ if (!ensureVerb(req, reply, deps, 'rule:read')) return;
+ const catalog = parseOptionalCatalog(req.query, reply);
+ if (catalog === undefined && hasCatalogParam(req.query)) return;
+ try {
+ const env = await clients().primary().list(catalog);
+ return reply.send(env);
+ } catch (err) {
+ return passOapError(err, reply);
+ }
+ },
+ );
+
+ app.get(
+ '/api/catalog/bundled',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ if (!ensureVerb(req, reply, deps, 'rule:read')) return;
+ const catalog = parseRequiredCatalog(req.query, reply);
+ if (!catalog) return;
+ const withContent = parseBoolean((req.query as Record<string,
string>).withContent, true);
+ try {
+ const list = await clients().primary().listBundled(catalog,
withContent);
+ return reply.send(list);
+ } catch (err) {
+ return passOapError(err, reply);
+ }
+ },
+ );
+}
diff --git a/apps/bff/src/http/admin/dsl/dump.ts
b/apps/bff/src/http/admin/dsl/dump.ts
new file mode 100644
index 0000000..6c54e9d
--- /dev/null
+++ b/apps/bff/src/http/admin/dsl/dump.ts
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * GET /api/dump — dump every catalog (zipped).
+ * GET /api/dump/:catalog — dump one catalog. Streams the upstream
+ * response straight through.
+ *
+ * Gated on `rule:read`; response is a passthrough of OAP's content-type
+ * and content-disposition so the browser's save-as dialog works.
+ */
+
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
+import { isCatalog, type Catalog } from '@skywalking-horizon-ui/api-client';
+import { requireAuth } from '../../../user/middleware.js';
+import {
+ type DslRouteDeps,
+ ensureVerb,
+ makeClients,
+ passOapError,
+} from './_shared.js';
+
+export function registerDslDumpRoutes(app: FastifyInstance, deps:
DslRouteDeps): void {
+ const auth = requireAuth(deps);
+ const clients = makeClients(deps);
+
+ const dumpHandler = (catalog: Catalog | null) =>
+ async function (req: FastifyRequest, reply: FastifyReply) {
+ if (!ensureVerb(req, reply, deps, 'rule:read')) return;
+ try {
+ const upstream = await clients()
+ .primary()
+ .dump(catalog ?? undefined);
+ const ct = upstream.headers.get('content-type') ??
'application/octet-stream';
+ const cd = upstream.headers.get('content-disposition');
+ reply.header('content-type', ct);
+ if (cd) reply.header('content-disposition', cd);
+ return reply.send(upstream.body ?? '');
+ } catch (err) {
+ return passOapError(err, reply);
+ }
+ };
+
+ app.get('/api/dump', { preHandler: auth }, dumpHandler(null));
+
+ app.get(
+ '/api/dump/:catalog',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ const params = req.params as { catalog: string };
+ if (!isCatalog(params.catalog)) {
+ return reply.code(400).send({ error: 'invalid_catalog', value:
params.catalog });
+ }
+ return dumpHandler(params.catalog)(req, reply);
+ },
+ );
+}
diff --git a/apps/bff/src/http/admin/dsl/oal.ts
b/apps/bff/src/http/admin/dsl/oal.ts
new file mode 100644
index 0000000..bc10c3d
--- /dev/null
+++ b/apps/bff/src/http/admin/dsl/oal.ts
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * OAL read-only browse (SWIP-13 §4.1). All gated on `rule:read`, no audit.
+ * GET /api/oal/files — { files: string[], count }
+ * GET /api/oal/files/:name — text/plain raw .oal content
+ * GET /api/oal/rules — per-dispatcher source listing
+ * GET /api/oal/rules/:source — single source detail with `status`
+ */
+
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
+import { requireAuth } from '../../../user/middleware.js';
+import {
+ type DslRouteDeps,
+ ensureVerb,
+ makeClients,
+ passOapError,
+} from './_shared.js';
+
+export function registerDslOalRoutes(app: FastifyInstance, deps:
DslRouteDeps): void {
+ const auth = requireAuth(deps);
+ const clients = makeClients(deps);
+
+ app.get(
+ '/api/oal/files',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ if (!ensureVerb(req, reply, deps, 'rule:read')) return;
+ try {
+ const list = await clients().oal().listFiles();
+ return reply.send(list);
+ } catch (err) {
+ return passOapError(err, reply);
+ }
+ },
+ );
+
+ app.get(
+ '/api/oal/files/:name',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ if (!ensureVerb(req, reply, deps, 'rule:read')) return;
+ const params = req.params as { name: string };
+ if (!params.name) return reply.code(400).send({ error: 'missing_name' });
+ try {
+ const content = await clients().oal().getFileContent(params.name);
+ if (content === null) return reply.code(404).send({ error: 'not_found'
});
+ reply.header('content-type', 'text/plain; charset=utf-8');
+ return reply.send(content);
+ } catch (err) {
+ return passOapError(err, reply);
+ }
+ },
+ );
+
+ app.get(
+ '/api/oal/rules',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ if (!ensureVerb(req, reply, deps, 'rule:read')) return;
+ try {
+ const sources = await clients().oal().listSources();
+ return reply.send(sources);
+ } catch (err) {
+ return passOapError(err, reply);
+ }
+ },
+ );
+
+ app.get(
+ '/api/oal/rules/:source',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ if (!ensureVerb(req, reply, deps, 'rule:read')) return;
+ const params = req.params as { source: string };
+ if (!params.source) return reply.code(400).send({ error:
'missing_source' });
+ try {
+ const detail = await clients().oal().getSource(params.source);
+ if (detail === null) return reply.code(404).send({ error: 'not_found'
});
+ return reply.send(detail);
+ } catch (err) {
+ return passOapError(err, reply);
+ }
+ },
+ );
+}
diff --git a/apps/bff/src/http/admin/dsl/rule.ts
b/apps/bff/src/http/admin/dsl/rule.ts
new file mode 100644
index 0000000..99bc4df
--- /dev/null
+++ b/apps/bff/src/http/admin/dsl/rule.ts
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * GET /api/rule — single rule fetch (`If-None-Match` aware).
+ * POST /api/rule — add or update (audited; structural
+ * writes need `rule:write:structural`).
+ * POST /api/rule/inactivate — `rule:write`, audited.
+ * POST /api/rule/delete — `rule:delete`; `mode=revertToBundled`
+ * is treated as a structural write.
+ */
+
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
+import { requireAuth } from '../../../user/middleware.js';
+import {
+ auditMutation,
+ type DslRouteDeps,
+ ensureVerb,
+ makeClients,
+ parseBoolean,
+ parseDeleteMode,
+ parseRequiredCatalog,
+ passOapError,
+ passOapErrorAudit,
+} from './_shared.js';
+
+export function registerDslRuleRoutes(app: FastifyInstance, deps:
DslRouteDeps): void {
+ const auth = requireAuth(deps);
+ const clients = makeClients(deps);
+
+ app.get('/api/rule', { preHandler: auth }, async (req: FastifyRequest,
reply: FastifyReply) => {
+ if (!ensureVerb(req, reply, deps, 'rule:read')) return;
+ const q = req.query as Record<string, string | undefined>;
+ const catalog = parseRequiredCatalog(q, reply);
+ if (!catalog) return;
+ if (!q.name) return reply.code(400).send({ error: 'missing_name' });
+ const source = q.source as 'runtime' | 'bundled' | undefined;
+ if (source !== undefined && source !== 'runtime' && source !== 'bundled') {
+ return reply.code(400).send({ error: 'invalid_source', value: source });
+ }
+ const ifNoneMatch = req.headers['if-none-match'] as string | undefined;
+ try {
+ const got = await clients()
+ .primary()
+ .get({
+ catalog,
+ name: q.name,
+ ...(source !== undefined ? { source } : {}),
+ ...(ifNoneMatch !== undefined ? { ifNoneMatch } : {}),
+ });
+ if ('notModified' in got) {
+ reply.header('etag', got.etag);
+ reply.header('x-sw-content-hash', got.contentHash);
+ reply.header('x-sw-status', got.status);
+ return reply.code(304).send();
+ }
+ reply.header('content-type', 'application/x-yaml; charset=utf-8');
+ reply.header('etag', got.etag);
+ reply.header('x-sw-content-hash', got.contentHash);
+ reply.header('x-sw-status', got.status);
+ reply.header('x-sw-source', got.source);
+ reply.header('x-sw-update-time', String(got.updateTime));
+ return reply.send(got.content);
+ } catch (err) {
+ return passOapError(err, reply);
+ }
+ });
+
+ app.post('/api/rule', { preHandler: auth }, async (req: FastifyRequest,
reply: FastifyReply) => {
+ const q = req.query as Record<string, string | undefined>;
+ const catalog = parseRequiredCatalog(q, reply);
+ if (!catalog) return;
+ if (!q.name) return reply.code(400).send({ error: 'missing_name' });
+ if (typeof req.body !== 'string' || req.body.length === 0) {
+ return reply.code(400).send({ error: 'empty_body' });
+ }
+ const allowStorageChange = parseBoolean(q.allowStorageChange, false);
+ const force = parseBoolean(q.force, false);
+ const verb = allowStorageChange || force ? 'rule:write:structural' :
'rule:write';
+ if (!ensureVerb(req, reply, deps, verb)) return;
+
+ try {
+ const result = await clients().primary().addOrUpdate({
+ catalog,
+ name: q.name,
+ body: req.body,
+ allowStorageChange,
+ force,
+ });
+ auditMutation(deps, req, 'addOrUpdate', verb, catalog, q.name,
result.applyStatus, {
+ allowStorageChange,
+ force,
+ });
+ return reply.send(result);
+ } catch (err) {
+ return passOapErrorAudit(err, reply, deps, req, 'addOrUpdate', verb,
catalog, q.name);
+ }
+ });
+
+ app.post(
+ '/api/rule/inactivate',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ const q = req.query as Record<string, string | undefined>;
+ const catalog = parseRequiredCatalog(q, reply);
+ if (!catalog) return;
+ if (!q.name) return reply.code(400).send({ error: 'missing_name' });
+ if (!ensureVerb(req, reply, deps, 'rule:write')) return;
+ try {
+ const result = await clients().primary().inactivate(catalog, q.name);
+ auditMutation(deps, req, 'inactivate', 'rule:write', catalog, q.name,
result.applyStatus);
+ return reply.send(result);
+ } catch (err) {
+ return passOapErrorAudit(
+ err,
+ reply,
+ deps,
+ req,
+ 'inactivate',
+ 'rule:write',
+ catalog,
+ q.name,
+ );
+ }
+ },
+ );
+
+ app.post(
+ '/api/rule/delete',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ const q = req.query as Record<string, string | undefined>;
+ const catalog = parseRequiredCatalog(q, reply);
+ if (!catalog) return;
+ if (!q.name) return reply.code(400).send({ error: 'missing_name' });
+ const mode = parseDeleteMode(q.mode, reply);
+ if (mode === null) return;
+ // mode=revertToBundled is structural — swaps the active row's
+ // identity back to the bundled twin. A caller with only
+ // `rule:delete` must not be able to revert.
+ const verb = mode === 'revertToBundled' ? 'rule:write:structural' :
'rule:delete';
+ if (!ensureVerb(req, reply, deps, verb)) return;
+ try {
+ const result = await clients().primary().delete(catalog, q.name, mode);
+ auditMutation(deps, req, 'delete', verb, catalog, q.name,
result.applyStatus, { mode });
+ return reply.send(result);
+ } catch (err) {
+ return passOapErrorAudit(err, reply, deps, req, 'delete', verb,
catalog, q.name, { mode });
+ }
+ },
+ );
+}
diff --git a/apps/bff/src/http/inspect.ts b/apps/bff/src/http/admin/inspect.ts
similarity index 95%
rename from apps/bff/src/http/inspect.ts
rename to apps/bff/src/http/admin/inspect.ts
index cbafcbc..21827bb 100644
--- a/apps/bff/src/http/inspect.ts
+++ b/apps/bff/src/http/admin/inspect.ts
@@ -50,16 +50,16 @@ import {
type InspectMetricType,
type InspectStep,
} from '@skywalking-horizon-ui/api-client';
-import type { ConfigSource } from '../config/loader.js';
-import type { AuditLogger } from '../audit/logger.js';
-import { requireAuth } from '../auth/middleware.js';
-import { sessionHasVerb } from '../rbac/policy.js';
-import type { Session, SessionStore } from '../auth/sessions.js';
-import { buildOapClients, type OapClients } from './clients.js';
-import { AttributionCache, attributeOrUnknown } from
'../inspect/attribution.js';
-import { MqeTargetCache } from './mqe-target.js';
-import { parseExecBody, fireMqe, MqeFireError } from './inspect-exec.js';
-import { ServerTimeCache } from './server-time.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { AuditLogger } from '../../audit/logger.js';
+import { requireAuth } from '../../user/middleware.js';
+import { sessionHasVerb } from '../../rbac/policy.js';
+import type { Session, SessionStore } from '../../user/sessions.js';
+import { buildOapClients, type OapClients } from '../../client/index.js';
+import { AttributionCache, attributeOrUnknown } from
'../../logic/inspect/attribution.js';
+import { MqeTargetCache } from '../../util/mqe-target.js';
+import { parseExecBody, fireMqe, MqeFireError } from
'../../logic/inspect/exec.js';
+import { ServerTimeCache } from '../../util/time.js';
export interface InspectRouteDeps {
config: ConfigSource;
diff --git a/apps/bff/src/http/debug.ts b/apps/bff/src/http/admin/live-debug.ts
similarity index 96%
rename from apps/bff/src/http/debug.ts
rename to apps/bff/src/http/admin/live-debug.ts
index 6021998..9c20221 100644
--- a/apps/bff/src/http/debug.ts
+++ b/apps/bff/src/http/admin/live-debug.ts
@@ -42,13 +42,13 @@ import {
type Granularity,
type StartSessionArgs,
} from '@skywalking-horizon-ui/api-client';
-import type { ConfigSource } from '../config/loader.js';
-import type { AuditLogger } from '../audit/logger.js';
-import { requireAuth } from '../auth/middleware.js';
-import { sessionHasVerb } from '../rbac/policy.js';
-import type { Session } from '../auth/sessions.js';
-import type { SessionStore } from '../auth/sessions.js';
-import { buildOapClients, type OapClients } from './clients.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { AuditLogger } from '../../audit/logger.js';
+import { requireAuth } from '../../user/middleware.js';
+import { sessionHasVerb } from '../../rbac/policy.js';
+import type { Session } from '../../user/sessions.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { buildOapClients, type OapClients } from '../../client/index.js';
import type { FetchLike } from '@skywalking-horizon-ui/api-client';
export interface DebugRouteDeps {
diff --git a/apps/bff/src/http/config/alarms.ts
b/apps/bff/src/http/config/alarms.ts
new file mode 100644
index 0000000..17e9e84
--- /dev/null
+++ b/apps/bff/src/http/config/alarms.ts
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * `/api/alarms/config` — read + write the alarm-page setup
+ * (per-traffic-layer MQE expression list). The `serviceLayer` cache is
+ * invalidated on save so the next `GET /api/alarms` picks up any newly
+ * configured layer immediately instead of waiting for the 60s TTL.
+ */
+
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
+import { z } from 'zod';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import type { AuditLogger } from '../../audit/logger.js';
+import { requireAuth } from '../../user/middleware.js';
+import type { ServiceLayerMap } from '../../logic/alarms/service-layer-map.js';
+import type { AlarmsStore, AlarmsConfig } from '../../logic/alarms/store.js';
+
+export interface AlarmsConfigRouteDeps {
+ config: ConfigSource;
+ sessions: SessionStore;
+ audit: AuditLogger;
+ store: AlarmsStore;
+ serviceLayer: ServiceLayerMap;
+}
+
+const configSaveSchema = z.object({
+ trafficLayers: z
+ .array(
+ z.object({
+ layerKey: z.string().min(1),
+ mqe: z.string().min(1),
+ label: z.string().optional(),
+ }).strict(),
+ )
+ .max(8),
+});
+
+export function registerAlarmsConfigRoutes(
+ app: FastifyInstance,
+ deps: AlarmsConfigRouteDeps,
+): void {
+ const auth = requireAuth(deps);
+
+ app.get(
+ '/api/alarms/config',
+ { preHandler: auth },
+ async (_req: FastifyRequest, reply: FastifyReply) => {
+ const cfg = await deps.store.load();
+ return reply.send(cfg);
+ },
+ );
+
+ app.post(
+ '/api/alarms/config',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ const parsed = configSaveSchema.safeParse(req.body);
+ if (!parsed.success) {
+ return reply.code(400).send({ error: 'invalid_body', detail:
parsed.error.flatten() });
+ }
+ const next: AlarmsConfig = { trafficLayers: parsed.data.trafficLayers };
+ await deps.store.save(next);
+ deps.serviceLayer.invalidate();
+ deps.audit.record({
+ action: 'alarms.config.save',
+ actor: req.session?.username ?? null,
+ outcome: 'ok',
+ details: { layers: next.trafficLayers.map((l) =>
`${l.layerKey}:${l.mqe}`) },
+ fromIp: req.ip,
+ sessionId: req.session?.sid,
+ });
+ return reply.send(next);
+ },
+ );
+}
diff --git a/apps/bff/src/http/config/dashboard.ts
b/apps/bff/src/http/config/dashboard.ts
new file mode 100644
index 0000000..2551e9e
--- /dev/null
+++ b/apps/bff/src/http/config/dashboard.ts
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * `GET /api/layer/:key/dashboard/config` — returns the default widget
+ * set for one (layer, scope) without running any MQE. The SPA renders
+ * the empty grid first, then fires `POST /api/layer/:key/dashboard` to
+ * populate cells. Accepts `?scope=service|instance|endpoint|…` and
+ * defaults to `service`.
+ */
+
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { requireAuth } from '../../user/middleware.js';
+import {
+ getLayerTemplate,
+ widgetsForScope,
+} from '../../logic/layers/loader.js';
+import { defaultWidgetsFor } from '../../logic/dashboard/defaults.js';
+import { scopeSchema } from '../query/dashboard.js';
+
+export interface DashboardConfigDeps {
+ config: ConfigSource;
+ sessions: SessionStore;
+}
+
+export function registerDashboardConfigRoute(app: FastifyInstance, deps:
DashboardConfigDeps): void {
+ const auth = requireAuth(deps);
+ app.get(
+ '/api/layer/:key/dashboard/config',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ const params = req.params as { key: string };
+ const layerKey = params.key;
+ if (!layerKey || !/^[a-z0-9_]+$/i.test(layerKey)) {
+ return reply.code(400).send({ error: 'invalid_layer_key' });
+ }
+ const q = req.query as { scope?: string };
+ const scopeParsed = q.scope ? scopeSchema.safeParse(q.scope) : null;
+ const scope = scopeParsed?.success ? scopeParsed.data : 'service';
+ const tpl = getLayerTemplate(layerKey);
+ const widgets = tpl ? widgetsForScope(tpl, scope) :
defaultWidgetsFor(layerKey);
+ return reply.send({ layer: layerKey, scope, widgets });
+ },
+ );
+}
diff --git a/apps/bff/src/http/config/layer-template.ts
b/apps/bff/src/http/config/layer-template.ts
new file mode 100644
index 0000000..4c46afe
--- /dev/null
+++ b/apps/bff/src/http/config/layer-template.ts
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * `/api/admin/layer-templates*` — admin CRUD for the per-layer JSON
+ * templates that drive the dashboards / service-list / overview blocks.
+ *
+ * GET /api/admin/layer-templates — list every loaded layer.
+ * POST /api/admin/layer-templates/:key — write one template back
+ * to its JSON file; the
+ * in-memory cache is
+ * invalidated so the
+ * next read sees the new
+ * shape immediately.
+ */
+
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
+import { z } from 'zod';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { requireAuth } from '../../user/middleware.js';
+import {
+ allLayerTemplates,
+ getLayerTemplate,
+ writeLayerTemplate,
+ type LayerTemplate,
+} from '../../logic/layers/loader.js';
+import { widgetSchema } from '../query/dashboard.js';
+
+export interface LayerTemplateConfigDeps {
+ config: ConfigSource;
+ sessions: SessionStore;
+}
+
+const adminTemplateSchema = z.object({
+ key: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
+ alias: z.string().optional(),
+ color: z.string().optional(),
+ documentLink: z.string().optional(),
+ slots: z
+ .object({
+ services: z.string().optional(),
+ instances: z.string().optional(),
+ endpoints: z.string().optional(),
+ endpointDependency: z.string().optional(),
+ })
+ .strict(),
+ components: z
+ .object({
+ service: z.boolean().optional(),
+ instances: z.boolean().optional(),
+ endpoints: z.boolean().optional(),
+ endpointDependency: z.boolean().optional(),
+ topology: z.boolean().optional(),
+ traces: z.boolean().optional(),
+ logs: z.boolean().optional(),
+ traceProfiling: z.boolean().optional(),
+ ebpfProfiling: z.boolean().optional(),
+ asyncProfiling: z.boolean().optional(),
+ })
+ .strict(),
+ metrics: z
+ .object({
+ orderBy: z.string().optional(),
+ columns: z
+ .array(
+ z.object({
+ metric: z.string().min(1),
+ label: z.string(),
+ unit: z.string().optional(),
+ mqe: z.string().optional(),
+ aggregation: z.enum(['sum', 'avg']).optional(),
+ scale: z.number().finite().optional(),
+ precision: z.number().int().min(0).max(6).optional(),
+ }),
+ )
+ .max(5)
+ .optional(),
+ })
+ .strict(),
+ overview: z
+ .object({
+ throughput: z.string().optional(),
+ spark: z.string().optional(),
+ })
+ .strict()
+ .optional(),
+ dashboards: z
+ .object({
+ service: z.array(widgetSchema).max(40).optional(),
+ instance: z.array(widgetSchema).max(40).optional(),
+ endpoint: z.array(widgetSchema).max(40).optional(),
+ dependency: z.array(widgetSchema).max(40).optional(),
+ topology: z.array(widgetSchema).max(40).optional(),
+ trace: z.array(widgetSchema).max(40).optional(),
+ logs: z.array(widgetSchema).max(40).optional(),
+ traceProfiling: z.array(widgetSchema).max(40).optional(),
+ ebpfProfiling: z.array(widgetSchema).max(40).optional(),
+ asyncProfiling: z.array(widgetSchema).max(40).optional(),
+ })
+ .strict()
+ .optional(),
+ widgets: z.array(widgetSchema).max(40).optional(),
+ naming: z
+ .object({
+ pattern: z.string().min(1),
+ flags: z.string().optional(),
+ displayGroup: z.string().optional(),
+ valueGroup: z.string().optional(),
+ alias: z.string().min(1),
+ })
+ .strict()
+ .optional(),
+});
+
+export function registerLayerTemplateRoutes(
+ app: FastifyInstance,
+ deps: LayerTemplateConfigDeps,
+): void {
+ const auth = requireAuth(deps);
+ app.get('/api/admin/layer-templates', { preHandler: auth }, async (_req,
reply) => {
+ return reply.send({ templates: allLayerTemplates() });
+ });
+
+ app.post(
+ '/api/admin/layer-templates/:key',
+ { preHandler: auth },
+ async (req: FastifyRequest, reply: FastifyReply) => {
+ const params = req.params as { key: string };
+ const layerKey = params.key.toUpperCase();
+ const parsed = adminTemplateSchema.safeParse(req.body);
+ if (!parsed.success) {
+ return reply.code(400).send({ error: 'invalid_template', detail:
parsed.error.flatten() });
+ }
+ if (parsed.data.key.toUpperCase() !== layerKey) {
+ return reply
+ .code(400)
+ .send({ error: 'key_mismatch', detail: 'URL key does not match body
key' });
+ }
+ try {
+ writeLayerTemplate(parsed.data as LayerTemplate);
+ } catch (err) {
+ return reply.code(500).send({
+ error: 'write_failed',
+ detail: err instanceof Error ? err.message : String(err),
+ });
+ }
+ const refreshed = getLayerTemplate(layerKey);
+ return reply.send({ template: refreshed });
+ },
+ );
+}
diff --git a/apps/bff/src/http/overview.ts
b/apps/bff/src/http/config/overview.ts
similarity index 91%
rename from apps/bff/src/http/overview.ts
rename to apps/bff/src/http/config/overview.ts
index e0c8326..c52c6a5 100644
--- a/apps/bff/src/http/overview.ts
+++ b/apps/bff/src/http/config/overview.ts
@@ -35,10 +35,10 @@ import type {
OverviewDashboardListResponse,
OverviewDashboardResponse,
} from '@skywalking-horizon-ui/api-client';
-import type { ConfigSource } from '../config/loader.js';
-import type { SessionStore } from '../auth/sessions.js';
-import { requireAuth } from '../auth/middleware.js';
-import { getOverviewDashboard, loadOverviewDashboards } from './loader.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { requireAuth } from '../../user/middleware.js';
+import { getOverviewDashboard, loadOverviewDashboards } from
'../../logic/overview/loader.js';
export interface OverviewRouteDeps {
config: ConfigSource;
diff --git a/apps/bff/src/http/setup.ts b/apps/bff/src/http/config/setup.ts
similarity index 92%
rename from apps/bff/src/http/setup.ts
rename to apps/bff/src/http/config/setup.ts
index 965a384..b4272b9 100644
--- a/apps/bff/src/http/setup.ts
+++ b/apps/bff/src/http/config/setup.ts
@@ -18,12 +18,12 @@
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import { z } from 'zod';
import type { SetupResponse, SetupSavePayload } from
'@skywalking-horizon-ui/api-client';
-import type { AuditLogger } from '../audit/logger.js';
-import { requireAuth } from '../auth/middleware.js';
-import type { ConfigSource } from '../config/loader.js';
-import type { SessionStore } from '../auth/sessions.js';
-import { badRequest } from '../errors.js';
-import type { SetupStore } from './store.js';
+import type { AuditLogger } from '../../audit/logger.js';
+import { requireAuth } from '../../user/middleware.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { badRequest } from '../../errors.js';
+import type { SetupStore } from '../../logic/setup/store.js';
export interface SetupRouteDeps {
config: ConfigSource;
diff --git a/apps/bff/src/http/dsl.ts b/apps/bff/src/http/dsl.ts
deleted file mode 100644
index 18076df..0000000
--- a/apps/bff/src/http/dsl.ts
+++ /dev/null
@@ -1,482 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/**
- * Studio's `/api/*` surface — eight route families that together cover
- * everything v1's catalog browse, editor, cluster matrix, and dump UIs
- * need.
- *
- * Each handler:
- * 1. Runs through `requireAuth` (handled by route preHandler).
- * 2. Resolves the verb required for this call.
- * 3. Calls the OAP client, mapping errors back to HTTP via the
- * `RuntimeRuleApiError` envelope so OAP's applyStatus codes
- * reach the SPA verbatim.
- * 4. Audits every mutating call, with the actor / verb / outcome.
- */
-
-import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
-import {
- RuntimeRuleApiError,
- isCatalog,
- type Catalog,
- type DeleteMode,
-} from '@skywalking-horizon-ui/api-client';
-import type { ConfigSource } from '../config/loader.js';
-import type { AuditLogger } from '../audit/logger.js';
-import { requireAuth } from '../auth/middleware.js';
-import { sessionHasVerb } from '../rbac/policy.js';
-import type { Session } from '../auth/sessions.js';
-import type { SessionStore } from '../auth/sessions.js';
-import { buildOapClients, type OapClients } from './clients.js';
-import { fetchPerNode, pivotClusterState } from './cluster.js';
-import type { FetchLike } from '@skywalking-horizon-ui/api-client';
-
-export interface OapRouteDeps {
- config: ConfigSource;
- sessions: SessionStore;
- audit: AuditLogger;
- /** Test seam — replaces `globalThis.fetch` in every OAP call. */
- fetch?: FetchLike;
-}
-
-const TRUTHY = new Set(['true', '1', 'yes']);
-
-/** Register every `/api/*` route on the given Fastify instance. The
- * text/plain content-type parser must already be registered (see
- * server.ts). */
-export function registerOapRoutes(app: FastifyInstance, deps: OapRouteDeps):
void {
- const auth = requireAuth(deps);
-
- function clients(): OapClients {
- return buildOapClients(deps.config.current, { fetch: deps.fetch });
- }
-
- // ── catalog browse ────────────────────────────────────────────────
-
- app.get(
- '/api/catalog/list',
- { preHandler: auth },
- async (req: FastifyRequest, reply: FastifyReply) => {
- if (!ensureVerb(req, reply, deps, 'rule:read')) return;
- const catalog = parseOptionalCatalog(req.query, reply);
- if (catalog === undefined && hasCatalogParam(req.query)) return;
- try {
- const env = await clients().primary().list(catalog);
- return reply.send(env);
- } catch (err) {
- return passOapError(err, reply);
- }
- },
- );
-
- app.get(
- '/api/catalog/bundled',
- { preHandler: auth },
- async (req: FastifyRequest, reply: FastifyReply) => {
- if (!ensureVerb(req, reply, deps, 'rule:read')) return;
- const catalog = parseRequiredCatalog(req.query, reply);
- if (!catalog) return;
- const withContent = parseBoolean((req.query as Record<string,
string>).withContent, true);
- try {
- const list = await clients().primary().listBundled(catalog,
withContent);
- return reply.send(list);
- } catch (err) {
- return passOapError(err, reply);
- }
- },
- );
-
- // ── single rule fetch ─────────────────────────────────────────────
-
- app.get('/api/rule', { preHandler: auth }, async (req: FastifyRequest,
reply: FastifyReply) => {
- if (!ensureVerb(req, reply, deps, 'rule:read')) return;
- const q = req.query as Record<string, string | undefined>;
- const catalog = parseRequiredCatalog(q, reply);
- if (!catalog) return;
- if (!q.name) {
- return reply.code(400).send({ error: 'missing_name' });
- }
- const source = q.source as 'runtime' | 'bundled' | undefined;
- if (source !== undefined && source !== 'runtime' && source !== 'bundled') {
- return reply.code(400).send({ error: 'invalid_source', value: source });
- }
- const ifNoneMatch = req.headers['if-none-match'] as string | undefined;
- try {
- const got = await clients()
- .primary()
- .get({
- catalog,
- name: q.name,
- ...(source !== undefined ? { source } : {}),
- ...(ifNoneMatch !== undefined ? { ifNoneMatch } : {}),
- });
- if ('notModified' in got) {
- reply.header('etag', got.etag);
- reply.header('x-sw-content-hash', got.contentHash);
- reply.header('x-sw-status', got.status);
- return reply.code(304).send();
- }
- reply.header('content-type', 'application/x-yaml; charset=utf-8');
- reply.header('etag', got.etag);
- reply.header('x-sw-content-hash', got.contentHash);
- reply.header('x-sw-status', got.status);
- reply.header('x-sw-source', got.source);
- reply.header('x-sw-update-time', String(got.updateTime));
- return reply.send(got.content);
- } catch (err) {
- return passOapError(err, reply);
- }
- });
-
- // ── write paths (audit on every call) ─────────────────────────────
-
- app.post('/api/rule', { preHandler: auth }, async (req: FastifyRequest,
reply: FastifyReply) => {
- const q = req.query as Record<string, string | undefined>;
- const catalog = parseRequiredCatalog(q, reply);
- if (!catalog) return;
- if (!q.name) return reply.code(400).send({ error: 'missing_name' });
- if (typeof req.body !== 'string' || req.body.length === 0) {
- return reply.code(400).send({ error: 'empty_body' });
- }
- const allowStorageChange = parseBoolean(q.allowStorageChange, false);
- const force = parseBoolean(q.force, false);
- const verb = allowStorageChange || force ? 'rule:write:structural' :
'rule:write';
- if (!ensureVerb(req, reply, deps, verb)) return;
-
- try {
- const result = await clients().primary().addOrUpdate({
- catalog,
- name: q.name,
- body: req.body,
- allowStorageChange,
- force,
- });
- auditMutation(deps, req, 'addOrUpdate', verb, catalog, q.name,
result.applyStatus, {
- allowStorageChange,
- force,
- });
- return reply.send(result);
- } catch (err) {
- return passOapErrorAudit(err, reply, deps, req, 'addOrUpdate', verb,
catalog, q.name);
- }
- });
-
- app.post(
- '/api/rule/inactivate',
- { preHandler: auth },
- async (req: FastifyRequest, reply: FastifyReply) => {
- const q = req.query as Record<string, string | undefined>;
- const catalog = parseRequiredCatalog(q, reply);
- if (!catalog) return;
- if (!q.name) return reply.code(400).send({ error: 'missing_name' });
- if (!ensureVerb(req, reply, deps, 'rule:write')) return;
- try {
- const result = await clients().primary().inactivate(catalog, q.name);
- auditMutation(deps, req, 'inactivate', 'rule:write', catalog, q.name,
result.applyStatus);
- return reply.send(result);
- } catch (err) {
- return passOapErrorAudit(
- err,
- reply,
- deps,
- req,
- 'inactivate',
- 'rule:write',
- catalog,
- q.name,
- );
- }
- },
- );
-
- app.post(
- '/api/rule/delete',
- { preHandler: auth },
- async (req: FastifyRequest, reply: FastifyReply) => {
- const q = req.query as Record<string, string | undefined>;
- const catalog = parseRequiredCatalog(q, reply);
- if (!catalog) return;
- if (!q.name) return reply.code(400).send({ error: 'missing_name' });
- const mode = parseDeleteMode(q.mode, reply);
- if (mode === null) return;
- /* `mode=revertToBundled` is a structural change — it swaps the
- * active row's identity back to the bundled twin, the same
- * write-class that `rule:write:structural` already gates on
- * the addOrUpdate path. A caller with only `rule:delete`
- * must not be able to revert. */
- const verb = mode === 'revertToBundled' ? 'rule:write:structural' :
'rule:delete';
- if (!ensureVerb(req, reply, deps, verb)) return;
- try {
- const result = await clients().primary().delete(catalog, q.name, mode);
- auditMutation(deps, req, 'delete', verb, catalog, q.name,
result.applyStatus, {
- mode,
- });
- return reply.send(result);
- } catch (err) {
- return passOapErrorAudit(err, reply, deps, req, 'delete', verb,
catalog, q.name, {
- mode,
- });
- }
- },
- );
-
- // ── cluster state (BFF fan-out) ───────────────────────────────────
-
- app.get(
- '/api/cluster/state',
- { preHandler: auth },
- async (req: FastifyRequest, reply: FastifyReply) => {
- if (!ensureVerb(req, reply, deps, 'cluster:read')) return;
- const c = clients();
- const perNode = await fetchPerNode(c);
- return reply.send(pivotClusterState(perNode));
- },
- );
-
- // ── dump (streaming passthrough) ──────────────────────────────────
-
- const dumpHandler = (catalog: Catalog | null) =>
- async function (req: FastifyRequest, reply: FastifyReply) {
- if (!ensureVerb(req, reply, deps, 'rule:read')) return;
- try {
- const upstream = await clients()
- .primary()
- .dump(catalog ?? undefined);
- const ct = upstream.headers.get('content-type') ??
'application/octet-stream';
- const cd = upstream.headers.get('content-disposition');
- reply.header('content-type', ct);
- if (cd) reply.header('content-disposition', cd);
- return reply.send(upstream.body ?? '');
- } catch (err) {
- return passOapError(err, reply);
- }
- };
-
- app.get('/api/dump', { preHandler: auth }, dumpHandler(null));
-
- app.get(
- '/api/dump/:catalog',
- { preHandler: auth },
- async (req: FastifyRequest, reply: FastifyReply) => {
- const params = req.params as { catalog: string };
- if (!isCatalog(params.catalog)) {
- return reply.code(400).send({ error: 'invalid_catalog', value:
params.catalog });
- }
- return dumpHandler(params.catalog)(req, reply);
- },
- );
-
- // ── OAL read-only browse (SWIP-13 §4.1) ──────────────────────────
- // Read-only — no audit. `rule:read` gate.
- //
- // Wire shape from RuntimeOalRestHandler.java:
- // /files — { files: string[], count }
- // /files/{name} — text/plain raw .oal content
- // /rules — per-dispatcher listing { sources, count }
- // /rules/{source} — single source detail with `status: live | no_holder`
-
- app.get(
- '/api/oal/files',
- { preHandler: auth },
- async (req: FastifyRequest, reply: FastifyReply) => {
- if (!ensureVerb(req, reply, deps, 'rule:read')) return;
- try {
- const list = await clients().oal().listFiles();
- return reply.send(list);
- } catch (err) {
- return passOapError(err, reply);
- }
- },
- );
-
- app.get(
- '/api/oal/files/:name',
- { preHandler: auth },
- async (req: FastifyRequest, reply: FastifyReply) => {
- if (!ensureVerb(req, reply, deps, 'rule:read')) return;
- const params = req.params as { name: string };
- if (!params.name) return reply.code(400).send({ error: 'missing_name' });
- try {
- const content = await clients().oal().getFileContent(params.name);
- if (content === null) return reply.code(404).send({ error: 'not_found'
});
- reply.header('content-type', 'text/plain; charset=utf-8');
- return reply.send(content);
- } catch (err) {
- return passOapError(err, reply);
- }
- },
- );
-
- app.get(
- '/api/oal/rules',
- { preHandler: auth },
- async (req: FastifyRequest, reply: FastifyReply) => {
- if (!ensureVerb(req, reply, deps, 'rule:read')) return;
- try {
- const sources = await clients().oal().listSources();
- return reply.send(sources);
- } catch (err) {
- return passOapError(err, reply);
- }
- },
- );
-
- app.get(
- '/api/oal/rules/:source',
- { preHandler: auth },
- async (req: FastifyRequest, reply: FastifyReply) => {
- if (!ensureVerb(req, reply, deps, 'rule:read')) return;
- const params = req.params as { source: string };
- if (!params.source) return reply.code(400).send({ error:
'missing_source' });
- try {
- const detail = await clients().oal().getSource(params.source);
- if (detail === null) return reply.code(404).send({ error: 'not_found'
});
- return reply.send(detail);
- } catch (err) {
- return passOapError(err, reply);
- }
- },
- );
-}
-
-// ── helpers ─────────────────────────────────────────────────────────
-
-function parseBoolean(v: string | undefined, fallback: boolean): boolean {
- if (v === undefined) return fallback;
- return TRUTHY.has(v.toLowerCase());
-}
-
-function hasCatalogParam(q: unknown): boolean {
- return typeof q === 'object' && q !== null && 'catalog' in q;
-}
-
-/** When the `catalog` query is missing, returns `undefined` and lets
- * the caller proceed. When present-but-invalid, sends 400 and returns
- * `undefined`; the caller should check `hasCatalogParam` to disambiguate. */
-function parseOptionalCatalog(q: unknown, reply: FastifyReply): Catalog |
undefined {
- const raw = (q as Record<string, string | undefined>).catalog;
- if (raw === undefined || raw === '') return undefined;
- if (!isCatalog(raw)) {
- reply.code(400).send({ error: 'invalid_catalog', value: raw });
- return undefined;
- }
- return raw;
-}
-
-function parseRequiredCatalog(q: unknown, reply: FastifyReply): Catalog | null
{
- const raw = (q as Record<string, string | undefined>).catalog;
- if (!raw) {
- reply.code(400).send({ error: 'missing_catalog' });
- return null;
- }
- if (!isCatalog(raw)) {
- reply.code(400).send({ error: 'invalid_catalog', value: raw });
- return null;
- }
- return raw;
-}
-
-/** Returns the parsed `DeleteMode` or `null` when the wire value was
- * invalid (and a 400 was sent). */
-function parseDeleteMode(raw: string | undefined, reply: FastifyReply):
DeleteMode | null {
- if (raw === undefined || raw === '') return '';
- if (raw === 'revertToBundled') return 'revertToBundled';
- reply.code(400).send({ error: 'invalid_delete_mode', value: raw });
- return null;
-}
-
-function ensureVerb(
- req: FastifyRequest,
- reply: FastifyReply,
- deps: OapRouteDeps,
- verb: string,
-): boolean {
- const session: Session | undefined = req.session;
- if (!session) {
- reply.code(401).send({ error: 'unauthenticated' });
- return false;
- }
- if (!sessionHasVerb(deps.config.current, session.roles, verb)) {
- reply.code(403).send({ error: 'permission_denied', verb });
- return false;
- }
- return true;
-}
-
-function passOapError(err: unknown, reply: FastifyReply): FastifyReply {
- if (err instanceof RuntimeRuleApiError) {
- return reply.code(err.status).send(err.body);
- }
- return reply.code(502).send({
- error: 'oap_unreachable',
- message: err instanceof Error ? err.message : String(err),
- });
-}
-
-function passOapErrorAudit(
- err: unknown,
- reply: FastifyReply,
- deps: OapRouteDeps,
- req: FastifyRequest,
- action: string,
- verb: string,
- catalog: Catalog,
- name: string,
- details: Record<string, unknown> = {},
-): FastifyReply {
- let outcome = 'oap_unreachable';
- if (err instanceof RuntimeRuleApiError) {
- const apiErr: RuntimeRuleApiError = err;
- const body = apiErr.body;
- outcome =
- typeof body === 'object' && body !== null && 'applyStatus' in body
- ? body.applyStatus
- : `http_${apiErr.status}`;
- }
- deps.audit.record({
- action,
- verb,
- actor: req.session?.username ?? null,
- outcome,
- details: { catalog, name, ...details },
- fromIp: req.ip,
- sessionId: req.session?.sid,
- });
- return passOapError(err, reply);
-}
-
-function auditMutation(
- deps: OapRouteDeps,
- req: FastifyRequest,
- action: string,
- verb: string,
- catalog: Catalog,
- name: string,
- outcome: string,
- details: Record<string, unknown> = {},
-): void {
- deps.audit.record({
- action,
- verb,
- actor: req.session?.username ?? null,
- outcome,
- details: { catalog, name, ...details },
- fromIp: req.ip,
- sessionId: req.session?.sid,
- });
-}
diff --git a/apps/bff/src/http/alarms.ts b/apps/bff/src/http/query/alarms.ts
similarity index 89%
rename from apps/bff/src/http/alarms.ts
rename to apps/bff/src/http/query/alarms.ts
index faf101b..fa72a10 100644
--- a/apps/bff/src/http/alarms.ts
+++ b/apps/bff/src/http/query/alarms.ts
@@ -40,19 +40,22 @@
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import type { FetchLike } from '@skywalking-horizon-ui/api-client';
import { z } from 'zod';
-import { requireAuth } from '../auth/middleware.js';
-import type { ConfigSource } from '../config/loader.js';
-import type { SessionStore } from '../auth/sessions.js';
-import type { AuditLogger } from '../audit/logger.js';
-import { badRequest } from '../errors.js';
-import { buildOapOpts, graphqlPost } from '../oap/graphql-client.js';
-import { ServiceLayerMap } from './service-layer-map.js';
-import type { AlarmsStore, AlarmsConfig } from './store.js';
-
-export interface AlarmsRouteDeps {
+import { requireAuth } from '../../user/middleware.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { badRequest } from '../../errors.js';
+import { buildOapOpts, graphqlPost } from '../../client/graphql.js';
+import type { ServiceLayerMap } from '../../logic/alarms/service-layer-map.js';
+import type { AlarmsStore } from '../../logic/alarms/store.js';
+
+export interface AlarmsQueryRouteDeps {
config: ConfigSource;
sessions: SessionStore;
- audit: AuditLogger;
+ /** Shared with config/alarms.ts so a config save can invalidate
+ * the cache and the next list call picks up the new layers. */
+ serviceLayer: ServiceLayerMap;
+ /** Used by `/api/alarms/traffic` to read which layers the operator
+ * configured as traffic backdrops. */
store: AlarmsStore;
fetch?: FetchLike;
}
@@ -322,21 +325,9 @@ const trafficQuerySchema = z.object({
endTime: z.coerce.number().int().positive(),
});
-const configSaveSchema = z.object({
- trafficLayers: z
- .array(
- z.object({
- layerKey: z.string().min(1),
- mqe: z.string().min(1),
- label: z.string().optional(),
- }).strict(),
- )
- .max(8),
-});
-
-export function registerAlarmsRoutes(app: FastifyInstance, deps:
AlarmsRouteDeps): void {
+export function registerAlarmsQueryRoutes(app: FastifyInstance, deps:
AlarmsQueryRouteDeps): void {
const auth = requireAuth(deps);
- const serviceLayer = new ServiceLayerMap({ config: deps.config, fetch:
deps.fetch });
+ const serviceLayer = deps.serviceLayer;
// ── GET /api/alarms ────────────────────────────────────────────────
app.get('/api/alarms', { preHandler: auth }, async (req: FastifyRequest,
reply: FastifyReply) => {
@@ -557,39 +548,6 @@ export function registerAlarmsRoutes(app: FastifyInstance,
deps: AlarmsRouteDeps
},
);
- // ── GET /api/alarms/config ─────────────────────────────────────────
- app.get(
- '/api/alarms/config',
- { preHandler: auth },
- async (_req: FastifyRequest, reply: FastifyReply) => {
- const cfg = await deps.store.load();
- return reply.send(cfg);
- },
- );
-
- // ── POST /api/alarms/config ────────────────────────────────────────
- app.post(
- '/api/alarms/config',
- { preHandler: auth },
- async (req: FastifyRequest, reply: FastifyReply) => {
- const parsed = configSaveSchema.safeParse(req.body);
- if (!parsed.success) {
- return reply.code(400).send({ error: 'invalid_body', detail:
parsed.error.flatten() });
- }
- const next: AlarmsConfig = { trafficLayers: parsed.data.trafficLayers };
- await deps.store.save(next);
- serviceLayer.invalidate();
- deps.audit.record({
- action: 'alarms.config.save',
- actor: req.session?.username ?? null,
- outcome: 'ok',
- details: { layers: next.trafficLayers.map((l) =>
`${l.layerKey}:${l.mqe}`) },
- fromIp: req.ip,
- sessionId: req.session?.sid,
- });
- return reply.send(next);
- },
- );
}
function prettyLayer(key: string): string {
diff --git a/apps/bff/src/http/async-profile.ts
b/apps/bff/src/http/query/async-profile.ts
similarity index 98%
rename from apps/bff/src/http/async-profile.ts
rename to apps/bff/src/http/query/async-profile.ts
index 1c1c6c8..a950dce 100644
--- a/apps/bff/src/http/async-profile.ts
+++ b/apps/bff/src/http/query/async-profile.ts
@@ -48,10 +48,10 @@ import type {
PprofTaskCreationResponse,
PprofTaskListResponse,
} from '@skywalking-horizon-ui/api-client';
-import type { ConfigSource } from '../config/loader.js';
-import type { SessionStore } from '../auth/sessions.js';
-import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost, buildOapOpts } from './graphql-client.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { requireAuth } from '../../user/middleware.js';
+import { graphqlPost, buildOapOpts } from '../../client/graphql.js';
export interface AsyncProfileRouteDeps {
config: ConfigSource;
diff --git a/apps/bff/src/http/dashboard.ts
b/apps/bff/src/http/query/dashboard.ts
similarity index 79%
rename from apps/bff/src/http/dashboard.ts
rename to apps/bff/src/http/query/dashboard.ts
index e3e2883..8d03a12 100644
--- a/apps/bff/src/http/dashboard.ts
+++ b/apps/bff/src/http/query/dashboard.ts
@@ -39,18 +39,15 @@ import type {
DashboardWidgetResult,
FetchLike,
} from '@skywalking-horizon-ui/api-client';
-import type { ConfigSource } from '../config/loader.js';
-import type { SessionStore } from '../auth/sessions.js';
-import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost, buildOapOpts } from '../oap/graphql-client.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { requireAuth } from '../../user/middleware.js';
+import { graphqlPost, buildOapOpts } from '../../client/graphql.js';
import {
- allLayerTemplates,
getLayerTemplate,
widgetsForScope,
- writeLayerTemplate,
- type LayerTemplate,
-} from '../layers/loader.js';
-import { defaultWidgetsFor } from './defaults.js';
+} from '../../logic/layers/loader.js';
+import { defaultWidgetsFor } from '../../logic/dashboard/defaults.js';
export interface DashboardRouteDeps {
config: ConfigSource;
@@ -58,7 +55,10 @@ export interface DashboardRouteDeps {
fetch?: FetchLike;
}
-const widgetSchema = z.object({
+/** Shared with config/layer-template.ts — kept here because the
+ * runtime POST handler is the canonical user; the admin-template
+ * schema reuses widgetSchema for nested validation. */
+export const widgetSchema = z.object({
id: z.string().min(1),
title: z.string(),
tip: z.string().optional(),
@@ -83,7 +83,8 @@ const widgetSchema = z.object({
w: z.number().int().positive().optional(),
h: z.number().int().positive().optional(),
});
-const scopeSchema = z.enum([
+/** Shared with config/dashboard.ts (GET-config handler). */
+export const scopeSchema = z.enum([
'service',
'instance',
'endpoint',
@@ -307,7 +308,7 @@ function parseTopList(
});
}
-export function registerDashboardRoute(app: FastifyInstance, deps:
DashboardRouteDeps): void {
+export function registerDashboardQueryRoute(app: FastifyInstance, deps:
DashboardRouteDeps): void {
const auth = requireAuth(deps);
app.post(
'/api/layer/:key/dashboard',
@@ -555,143 +556,4 @@ export function registerDashboardRoute(app:
FastifyInstance, deps: DashboardRout
return reply.send({ ...baseResp, widgets: results });
},
);
-
- // GET version returns the default widget config without running queries —
- // useful for the SPA to know what to render before invoking POST.
- // Accepts
?scope=service|instance|endpoint|dependency|topology|trace|logs|traceProfiling|ebpfProfiling|asyncProfiling.
- app.get(
- '/api/layer/:key/dashboard/config',
- { preHandler: auth },
- async (req: FastifyRequest, reply: FastifyReply) => {
- const params = req.params as { key: string };
- const layerKey = params.key;
- if (!layerKey || !/^[a-z0-9_]+$/i.test(layerKey)) {
- return reply.code(400).send({ error: 'invalid_layer_key' });
- }
- const q = req.query as { scope?: string };
- const scopeParsed = q.scope ? scopeSchema.safeParse(q.scope) : null;
- const scope = scopeParsed?.success ? scopeParsed.data : 'service';
- const tpl = getLayerTemplate(layerKey);
- const widgets = tpl ? widgetsForScope(tpl, scope) :
defaultWidgetsFor(layerKey);
- return reply.send({ layer: layerKey, scope, widgets });
- },
- );
-
- // Admin: enumerate every loaded JSON layer template. Used by the
- // /admin/layer-dashboards page to render a layer picker + current
- // widget set per layer.
- app.get('/api/admin/layer-templates', { preHandler: auth }, async (_req,
reply) => {
- return reply.send({ templates: allLayerTemplates() });
- });
-
- // Admin: persist an operator-edited template back to its JSON file.
- // Body is the whole template; the BFF rewrites the file and
- // invalidates its in-memory cache so subsequent reads see the new
- // shape immediately.
- const adminTemplateSchema = z.object({
- key: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
- alias: z.string().optional(),
- color: z.string().optional(),
- documentLink: z.string().optional(),
- slots: z
- .object({
- services: z.string().optional(),
- instances: z.string().optional(),
- endpoints: z.string().optional(),
- endpointDependency: z.string().optional(),
- })
- .strict(),
- components: z
- .object({
- service: z.boolean().optional(),
- instances: z.boolean().optional(),
- endpoints: z.boolean().optional(),
- endpointDependency: z.boolean().optional(),
- topology: z.boolean().optional(),
- traces: z.boolean().optional(),
- logs: z.boolean().optional(),
- traceProfiling: z.boolean().optional(),
- ebpfProfiling: z.boolean().optional(),
- asyncProfiling: z.boolean().optional(),
- })
- .strict(),
- metrics: z
- .object({
- orderBy: z.string().optional(),
- columns: z
- .array(
- z.object({
- metric: z.string().min(1),
- label: z.string(),
- unit: z.string().optional(),
- mqe: z.string().optional(),
- aggregation: z.enum(['sum', 'avg']).optional(),
- scale: z.number().finite().optional(),
- precision: z.number().int().min(0).max(6).optional(),
- }),
- )
- .max(5)
- .optional(),
- })
- .strict(),
- overview: z
- .object({
- throughput: z.string().optional(),
- spark: z.string().optional(),
- })
- .strict()
- .optional(),
- dashboards: z
- .object({
- service: z.array(widgetSchema).max(40).optional(),
- instance: z.array(widgetSchema).max(40).optional(),
- endpoint: z.array(widgetSchema).max(40).optional(),
- dependency: z.array(widgetSchema).max(40).optional(),
- topology: z.array(widgetSchema).max(40).optional(),
- trace: z.array(widgetSchema).max(40).optional(),
- logs: z.array(widgetSchema).max(40).optional(),
- traceProfiling: z.array(widgetSchema).max(40).optional(),
- ebpfProfiling: z.array(widgetSchema).max(40).optional(),
- asyncProfiling: z.array(widgetSchema).max(40).optional(),
- })
- .strict()
- .optional(),
- widgets: z.array(widgetSchema).max(40).optional(),
- naming: z
- .object({
- pattern: z.string().min(1),
- flags: z.string().optional(),
- displayGroup: z.string().optional(),
- valueGroup: z.string().optional(),
- alias: z.string().min(1),
- })
- .strict()
- .optional(),
- });
-
- app.post(
- '/api/admin/layer-templates/:key',
- { preHandler: auth },
- async (req: FastifyRequest, reply: FastifyReply) => {
- const params = req.params as { key: string };
- const layerKey = params.key.toUpperCase();
- const parsed = adminTemplateSchema.safeParse(req.body);
- if (!parsed.success) {
- return reply.code(400).send({ error: 'invalid_template', detail:
parsed.error.flatten() });
- }
- if (parsed.data.key.toUpperCase() !== layerKey) {
- return reply.code(400).send({ error: 'key_mismatch', detail: 'URL key
does not match body key' });
- }
- try {
- writeLayerTemplate(parsed.data as LayerTemplate);
- } catch (err) {
- return reply.code(500).send({
- error: 'write_failed',
- detail: err instanceof Error ? err.message : String(err),
- });
- }
- const refreshed = getLayerTemplate(layerKey);
- return reply.send({ template: refreshed });
- },
- );
}
diff --git a/apps/bff/src/http/ebpf.ts b/apps/bff/src/http/query/ebpf.ts
similarity index 98%
rename from apps/bff/src/http/ebpf.ts
rename to apps/bff/src/http/query/ebpf.ts
index 408869c..cbb613b 100644
--- a/apps/bff/src/http/ebpf.ts
+++ b/apps/bff/src/http/query/ebpf.ts
@@ -44,10 +44,10 @@ import type {
NetworkProfilingKeepAliveResponse,
ProcessTopologyResponse,
} from '@skywalking-horizon-ui/api-client';
-import type { ConfigSource } from '../config/loader.js';
-import type { SessionStore } from '../auth/sessions.js';
-import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost, buildOapOpts } from './graphql-client.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { requireAuth } from '../../user/middleware.js';
+import { graphqlPost, buildOapOpts } from '../../client/graphql.js';
export interface EBPFRouteDeps {
config: ConfigSource;
diff --git a/apps/bff/src/http/endpoint-dependency.ts
b/apps/bff/src/http/query/endpoint-dependency.ts
similarity index 98%
rename from apps/bff/src/http/endpoint-dependency.ts
rename to apps/bff/src/http/query/endpoint-dependency.ts
index 34963e6..312d29f 100644
--- a/apps/bff/src/http/endpoint-dependency.ts
+++ b/apps/bff/src/http/query/endpoint-dependency.ts
@@ -29,8 +29,8 @@
*/
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
-import type { ConfigSource } from '../config/loader.js';
-import type { SessionStore } from '../auth/sessions.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
import type {
EndpointDependencyCall,
EndpointDependencyConfig,
@@ -39,9 +39,9 @@ import type {
FetchLike,
TopologyMetricDef,
} from '@skywalking-horizon-ui/api-client';
-import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost, buildOapOpts } from './graphql-client.js';
-import { endpointDependencyConfigFor, getLayerTemplate } from
'../layers/loader.js';
+import { requireAuth } from '../../user/middleware.js';
+import { graphqlPost, buildOapOpts } from '../../client/graphql.js';
+import { endpointDependencyConfigFor, getLayerTemplate } from
'../../logic/layers/loader.js';
export interface EndpointDependencyRouteDeps {
config: ConfigSource;
diff --git a/apps/bff/src/http/endpoint.ts b/apps/bff/src/http/query/endpoint.ts
similarity index 96%
rename from apps/bff/src/http/endpoint.ts
rename to apps/bff/src/http/query/endpoint.ts
index d910341..354c1d8 100644
--- a/apps/bff/src/http/endpoint.ts
+++ b/apps/bff/src/http/query/endpoint.ts
@@ -34,11 +34,11 @@
*/
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
-import type { ConfigSource } from '../config/loader.js';
-import type { SessionStore } from '../auth/sessions.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
import type { FetchLike } from '@skywalking-horizon-ui/api-client';
-import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost, buildOapOpts } from './graphql-client.js';
+import { requireAuth } from '../../user/middleware.js';
+import { graphqlPost, buildOapOpts } from '../../client/graphql.js';
export interface EndpointRouteDeps {
config: ConfigSource;
diff --git a/apps/bff/src/http/info.ts b/apps/bff/src/http/query/info.ts
similarity index 92%
rename from apps/bff/src/http/info.ts
rename to apps/bff/src/http/query/info.ts
index 39941eb..dee427b 100644
--- a/apps/bff/src/http/info.ts
+++ b/apps/bff/src/http/query/info.ts
@@ -17,10 +17,10 @@
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import type { FetchLike } from '@skywalking-horizon-ui/api-client';
-import type { ConfigSource } from '../config/loader.js';
-import type { SessionStore } from '../auth/sessions.js';
-import { requireAuth } from '../auth/middleware.js';
-import { buildOapOpts, graphqlPost } from './graphql-client.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { requireAuth } from '../../user/middleware.js';
+import { buildOapOpts, graphqlPost } from '../../client/graphql.js';
/**
* One round-trip combining `version`, `getTimeInfo`, and `checkHealth`.
diff --git a/apps/bff/src/http/instance.ts b/apps/bff/src/http/query/instance.ts
similarity index 96%
rename from apps/bff/src/http/instance.ts
rename to apps/bff/src/http/query/instance.ts
index 457a93b..699429d 100644
--- a/apps/bff/src/http/instance.ts
+++ b/apps/bff/src/http/query/instance.ts
@@ -32,11 +32,11 @@
*/
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
-import type { ConfigSource } from '../config/loader.js';
-import type { SessionStore } from '../auth/sessions.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
import type { FetchLike } from '@skywalking-horizon-ui/api-client';
-import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost, buildOapOpts } from './graphql-client.js';
+import { requireAuth } from '../../user/middleware.js';
+import { graphqlPost, buildOapOpts } from '../../client/graphql.js';
export interface InstanceRouteDeps {
config: ConfigSource;
diff --git a/apps/bff/src/http/landing.ts b/apps/bff/src/http/query/landing.ts
similarity index 98%
rename from apps/bff/src/http/landing.ts
rename to apps/bff/src/http/query/landing.ts
index d0bdb91..2f25c85 100644
--- a/apps/bff/src/http/landing.ts
+++ b/apps/bff/src/http/query/landing.ts
@@ -45,11 +45,11 @@ import type {
LandingResponse,
LandingServiceRow,
} from '@skywalking-horizon-ui/api-client';
-import type { ConfigSource } from '../config/loader.js';
-import type { SessionStore } from '../auth/sessions.js';
-import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost, buildOapOpts } from './graphql-client.js';
-import { expressionForServiceMetricSeries } from './mqe-catalog.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { requireAuth } from '../../user/middleware.js';
+import { graphqlPost, buildOapOpts } from '../../client/graphql.js';
+import { expressionForServiceMetricSeries } from '../../util/mqe-catalog.js';
export interface LandingRouteDeps {
config: ConfigSource;
diff --git a/apps/bff/src/http/log.ts b/apps/bff/src/http/query/log.ts
similarity index 98%
rename from apps/bff/src/http/log.ts
rename to apps/bff/src/http/query/log.ts
index 9813628..25c82e1 100644
--- a/apps/bff/src/http/log.ts
+++ b/apps/bff/src/http/query/log.ts
@@ -38,10 +38,10 @@ import type {
LogRow,
LogsResponse,
} from '@skywalking-horizon-ui/api-client';
-import type { ConfigSource } from '../config/loader.js';
-import type { SessionStore } from '../auth/sessions.js';
-import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost, buildOapOpts, type GraphqlOptions } from
'./graphql-client.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { requireAuth } from '../../user/middleware.js';
+import { graphqlPost, buildOapOpts, type GraphqlOptions } from
'../../client/graphql.js';
export interface LogRouteDeps {
config: ConfigSource;
diff --git a/apps/bff/src/http/menu.ts b/apps/bff/src/http/query/menu.ts
similarity index 97%
rename from apps/bff/src/http/menu.ts
rename to apps/bff/src/http/query/menu.ts
index 25b8e48..d75486c 100644
--- a/apps/bff/src/http/menu.ts
+++ b/apps/bff/src/http/query/menu.ts
@@ -23,11 +23,11 @@ import type {
LayerSlots,
MenuResponse,
} from '@skywalking-horizon-ui/api-client';
-import type { ConfigSource } from '../config/loader.js';
-import type { SessionStore } from '../auth/sessions.js';
-import { requireAuth } from '../auth/middleware.js';
-import { buildOapOpts, graphqlPost, type GraphqlOptions } from
'./graphql-client.js';
-import { getLayerTemplate, type LayerComponentFlags } from
'../layers/loader.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { requireAuth } from '../../user/middleware.js';
+import { buildOapOpts, graphqlPost, type GraphqlOptions } from
'../../client/graphql.js';
+import { getLayerTemplate, type LayerComponentFlags } from
'../../logic/layers/loader.js';
/**
* Map the JSON config's `components.*` flags onto the wire `caps`
@@ -253,7 +253,7 @@ async function fetchCountsForLayers(
}
// Local re-import to avoid a circular dep — graphqlPost is in the same dir.
-import { graphqlPost as graphqlPostShim } from './graphql-client.js';
+import { graphqlPost as graphqlPostShim } from '../../client/graphql.js';
export function registerMenuRoute(app: FastifyInstance, deps: MenuRouteDeps):
void {
const auth = requireAuth(deps);
diff --git a/apps/bff/src/http/preflight.ts
b/apps/bff/src/http/query/preflight.ts
similarity index 87%
rename from apps/bff/src/http/preflight.ts
rename to apps/bff/src/http/query/preflight.ts
index 960bbc2..99a2902 100644
--- a/apps/bff/src/http/preflight.ts
+++ b/apps/bff/src/http/query/preflight.ts
@@ -17,10 +17,10 @@
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
import type { FetchLike } from '@skywalking-horizon-ui/api-client';
-import type { ConfigSource } from '../config/loader.js';
-import { requireAuth } from '../auth/middleware.js';
-import type { SessionStore } from '../auth/sessions.js';
-import { runPreflight } from './preflight.js';
+import type { ConfigSource } from '../../config/loader.js';
+import { requireAuth } from '../../user/middleware.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { runPreflight } from '../../logic/preflight/preflight.js';
export interface PreflightRouteDeps {
config: ConfigSource;
diff --git a/apps/bff/src/http/profile.ts b/apps/bff/src/http/query/profile.ts
similarity index 97%
rename from apps/bff/src/http/profile.ts
rename to apps/bff/src/http/query/profile.ts
index 669afcd..0c88789 100644
--- a/apps/bff/src/http/profile.ts
+++ b/apps/bff/src/http/query/profile.ts
@@ -44,10 +44,10 @@ import type {
ProfileTaskListResponse,
ProfileTaskLogsResponse,
} from '@skywalking-horizon-ui/api-client';
-import type { ConfigSource } from '../config/loader.js';
-import type { SessionStore } from '../auth/sessions.js';
-import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost, buildOapOpts } from './graphql-client.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { requireAuth } from '../../user/middleware.js';
+import { graphqlPost, buildOapOpts } from '../../client/graphql.js';
export interface ProfileRouteDeps {
config: ConfigSource;
diff --git a/apps/bff/src/http/topology.ts b/apps/bff/src/http/query/topology.ts
similarity index 98%
rename from apps/bff/src/http/topology.ts
rename to apps/bff/src/http/query/topology.ts
index 19de95d..7309b53 100644
--- a/apps/bff/src/http/topology.ts
+++ b/apps/bff/src/http/query/topology.ts
@@ -34,8 +34,8 @@
*/
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
-import type { ConfigSource } from '../config/loader.js';
-import type { SessionStore } from '../auth/sessions.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
import type {
FetchLike,
TopologyCall,
@@ -44,9 +44,9 @@ import type {
TopologyNode,
TopologyResponse,
} from '@skywalking-horizon-ui/api-client';
-import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost, buildOapOpts } from './graphql-client.js';
-import { getLayerTemplate, topologyConfigFor } from '../layers/loader.js';
+import { requireAuth } from '../../user/middleware.js';
+import { graphqlPost, buildOapOpts } from '../../client/graphql.js';
+import { getLayerTemplate, topologyConfigFor } from
'../../logic/layers/loader.js';
export interface TopologyRouteDeps {
config: ConfigSource;
diff --git a/apps/bff/src/http/trace-tag.ts
b/apps/bff/src/http/query/trace-tag.ts
similarity index 95%
rename from apps/bff/src/http/trace-tag.ts
rename to apps/bff/src/http/query/trace-tag.ts
index 2001c00..7acb00d 100644
--- a/apps/bff/src/http/trace-tag.ts
+++ b/apps/bff/src/http/query/trace-tag.ts
@@ -33,10 +33,10 @@
import type { FetchLike } from '@skywalking-horizon-ui/api-client';
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
-import type { ConfigSource } from '../config/loader.js';
-import type { SessionStore } from '../auth/sessions.js';
-import { requireAuth } from '../auth/middleware.js';
-import { buildOapOpts, graphqlPost } from './graphql-client.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { requireAuth } from '../../user/middleware.js';
+import { buildOapOpts, graphqlPost } from '../../client/graphql.js';
export interface TraceTagRouteDeps {
config: ConfigSource;
diff --git a/apps/bff/src/http/trace.ts b/apps/bff/src/http/query/trace.ts
similarity index 97%
rename from apps/bff/src/http/trace.ts
rename to apps/bff/src/http/query/trace.ts
index 709145c..ab42c75 100644
--- a/apps/bff/src/http/trace.ts
+++ b/apps/bff/src/http/query/trace.ts
@@ -47,13 +47,13 @@ import type {
ZipkinTraceDetailResponse,
ZipkinTraceListResponse,
} from '@skywalking-horizon-ui/api-client';
-import type { ConfigSource } from '../config/loader.js';
-import type { SessionStore } from '../auth/sessions.js';
-import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost, buildOapOpts, type GraphqlOptions } from
'./graphql-client.js';
-import { getLayerTemplate, tracesConfigFor } from '../layers/loader.js';
-import { detectTraceProtocol } from './trace-protocol-cache.js';
-import { zipkinFetchTraces, zipkinFetchTraceById, summariseZipkinTrace } from
'./zipkin-client.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { requireAuth } from '../../user/middleware.js';
+import { graphqlPost, buildOapOpts, type GraphqlOptions } from
'../../client/graphql.js';
+import { getLayerTemplate, tracesConfigFor } from
'../../logic/layers/loader.js';
+import { detectTraceProtocol } from '../../util/trace-protocol-cache.js';
+import { zipkinFetchTraces, zipkinFetchTraceById, summariseZipkinTrace } from
'../../client/zipkin.js';
export interface TraceRouteDeps {
config: ConfigSource;
diff --git a/apps/bff/src/http/zipkin.ts b/apps/bff/src/http/query/zipkin.ts
similarity index 98%
rename from apps/bff/src/http/zipkin.ts
rename to apps/bff/src/http/query/zipkin.ts
index ab06a72..f03f287 100644
--- a/apps/bff/src/http/zipkin.ts
+++ b/apps/bff/src/http/query/zipkin.ts
@@ -47,10 +47,10 @@ import type {
ZipkinTraceDetailResponse,
} from '@skywalking-horizon-ui/api-client';
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
-import type { ConfigSource } from '../config/loader.js';
-import type { SessionStore } from '../auth/sessions.js';
-import { requireAuth } from '../auth/middleware.js';
-import { basicAuthHeader } from './graphql-client.js';
+import type { ConfigSource } from '../../config/loader.js';
+import type { SessionStore } from '../../user/sessions.js';
+import { requireAuth } from '../../user/middleware.js';
+import { basicAuthHeader } from '../../client/graphql.js';
export interface ZipkinRouteDeps {
config: ConfigSource;
diff --git a/apps/bff/src/http/user.ts b/apps/bff/src/http/user.ts
index 2fdf0ff..05db26f 100644
--- a/apps/bff/src/http/user.ts
+++ b/apps/bff/src/http/user.ts
@@ -21,8 +21,8 @@ import type { AuditLogger } from '../audit/logger.js';
import { badRequest, unauthorized } from '../errors.js';
import type { ConfigSource } from '../config/loader.js';
import { resolveVerbsForRoles } from '../rbac/verbs.js';
-import { verifyLocalCredentials } from './local.js';
-import type { SessionStore } from './sessions.js';
+import { verifyLocalCredentials } from '../user/local.js';
+import type { SessionStore } from '../user/sessions.js';
const loginBodySchema = z.object({
username: z.string().min(1),
diff --git a/apps/bff/src/logic/alarms/service-layer-map.ts
b/apps/bff/src/logic/alarms/service-layer-map.ts
index f4792b6..d9b7189 100644
--- a/apps/bff/src/logic/alarms/service-layer-map.ts
+++ b/apps/bff/src/logic/alarms/service-layer-map.ts
@@ -26,10 +26,10 @@
* a `listServices` fan-out on every alarms poll.
*/
-import { buildOapOpts, graphqlPost } from '../oap/graphql-client.js';
-import type { ConfigSource } from '../config/loader.js';
+import { buildOapOpts, graphqlPost } from '../../client/graphql.js';
+import type { ConfigSource } from '../../config/loader.js';
import type { FetchLike } from '@skywalking-horizon-ui/api-client';
-import { logger } from '../logger.js';
+import { logger } from '../../logger.js';
interface ServiceLayerMapDeps {
config: ConfigSource;
diff --git a/apps/bff/src/logic/alarms/store.ts
b/apps/bff/src/logic/alarms/store.ts
index 1534da3..4ce091d 100644
--- a/apps/bff/src/logic/alarms/store.ts
+++ b/apps/bff/src/logic/alarms/store.ts
@@ -32,7 +32,7 @@
import { existsSync } from 'node:fs';
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
-import { logger } from '../logger.js';
+import { logger } from '../../logger.js';
/** One background-traffic series the alarms timeline renders. */
export interface AlarmTrafficLayer {
diff --git a/apps/bff/src/logic/inspect/exec.ts
b/apps/bff/src/logic/inspect/exec.ts
index 9e34dc3..66b5eab 100644
--- a/apps/bff/src/logic/inspect/exec.ts
+++ b/apps/bff/src/logic/inspect/exec.ts
@@ -34,7 +34,7 @@
import type { FastifyReply } from 'fastify';
import type { FetchLike, ExpressionResult, MqeEntity } from
'@skywalking-horizon-ui/api-client';
import { INSPECT_STEPS, isInspectDate, type InspectStep } from
'@skywalking-horizon-ui/api-client';
-import type { MqeTarget } from './mqe-target.js';
+import type { MqeTarget } from '../../util/mqe-target.js';
interface DurationInput {
start: string;
diff --git a/apps/bff/src/logic/preflight/preflight.ts
b/apps/bff/src/logic/preflight/preflight.ts
index d10623c..1bc9f72 100644
--- a/apps/bff/src/logic/preflight/preflight.ts
+++ b/apps/bff/src/logic/preflight/preflight.ts
@@ -36,7 +36,7 @@ import type {
PreflightModule,
PreflightResult,
} from '@skywalking-horizon-ui/api-client';
-import type { HorizonConfig } from '../config/schema.js';
+import type { HorizonConfig } from '../../config/schema.js';
export type { PreflightModule, PreflightResult };
diff --git a/apps/bff/src/logic/setup/store.ts
b/apps/bff/src/logic/setup/store.ts
index 1bf20b1..fbabdb5 100644
--- a/apps/bff/src/logic/setup/store.ts
+++ b/apps/bff/src/logic/setup/store.ts
@@ -19,7 +19,7 @@ import { existsSync } from 'node:fs';
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import type { LayerConfig } from '@skywalking-horizon-ui/api-client';
-import { logger } from '../logger.js';
+import { logger } from '../../logger.js';
/**
* File-backed store for per-layer setup overrides.
diff --git a/apps/bff/src/rbac/policy.ts b/apps/bff/src/rbac/policy.ts
index 86c5f5d..7062902 100644
--- a/apps/bff/src/rbac/policy.ts
+++ b/apps/bff/src/rbac/policy.ts
@@ -17,8 +17,8 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import { forbidden, unauthorized } from '../errors.js';
-import type { Session } from '../auth/sessions.js';
-import type { SessionStore } from '../auth/sessions.js';
+import type { Session } from '../user/sessions.js';
+import type { SessionStore } from '../user/sessions.js';
import type { ConfigSource } from '../config/loader.js';
import type { HorizonConfig } from '../config/schema.js';
import { hasVerb, resolveVerbsForRoles, type Verb } from './verbs.js';
diff --git a/apps/bff/src/server.ts b/apps/bff/src/server.ts
index 3e9c50c..c97394d 100644
--- a/apps/bff/src/server.ts
+++ b/apps/bff/src/server.ts
@@ -21,33 +21,46 @@ import Fastify from 'fastify';
import cookie from '@fastify/cookie';
import fastifyStatic from '@fastify/static';
import { AuditLogger } from './audit/logger.js';
-import { registerAuthRoutes } from './auth/routes.js';
-import { SessionStore } from './auth/sessions.js';
+import { SessionStore } from './user/sessions.js';
import { loadConfig, type ConfigSource } from './config/loader.js';
-import { registerDashboardRoute } from './dashboard/routes.js';
-import { registerEndpointRoute } from './oap/endpoint-routes.js';
-import { registerEndpointDependencyRoute } from
'./oap/endpoint-dependency-routes.js';
-import { registerOapInfoRoute } from './oap/info-routes.js';
-import { registerInstanceRoute } from './oap/instance-routes.js';
-import { registerLandingRoute } from './oap/landing-routes.js';
-import { registerLogRoute } from './oap/log-routes.js';
-import { registerMenuRoute } from './oap/menu-routes.js';
-import { registerOapRoutes } from './oap/routes.js';
-import { registerPreflightRoutes } from './oap/preflight-routes.js';
-import { registerDebugRoutes } from './oap/debug-routes.js';
-import { registerInspectRoutes } from './oap/inspect-routes.js';
-import { registerAlarmsRoutes } from './alarms/routes.js';
-import { AlarmsStore } from './alarms/store.js';
-import { registerProfileRoutes } from './oap/profile-routes.js';
-import { registerEBPFRoutes } from './oap/ebpf-routes.js';
-import { registerAsyncProfileRoutes } from './oap/async-profile-routes.js';
-import { registerTopologyRoute } from './oap/topology-routes.js';
-import { registerTraceRoutes } from './oap/trace-routes.js';
-import { registerTraceTagRoutes } from './oap/trace-tag-routes.js';
-import { registerZipkinRoutes } from './oap/zipkin-routes.js';
-import { registerOverviewRoutes } from './overview/routes.js';
-import { registerSetupRoutes } from './setup/routes.js';
-import { SetupStore } from './setup/store.js';
+// User
+import { registerAuthRoutes } from './http/user.js';
+// Query (read-only data from OAP)
+import { registerOapInfoRoute } from './http/query/info.js';
+import { registerMenuRoute } from './http/query/menu.js';
+import { registerLandingRoute } from './http/query/landing.js';
+import { registerInstanceRoute } from './http/query/instance.js';
+import { registerEndpointRoute } from './http/query/endpoint.js';
+import { registerTopologyRoute } from './http/query/topology.js';
+import { registerEndpointDependencyRoute } from
'./http/query/endpoint-dependency.js';
+import { registerTraceRoutes } from './http/query/trace.js';
+import { registerTraceTagRoutes } from './http/query/trace-tag.js';
+import { registerZipkinRoutes } from './http/query/zipkin.js';
+import { registerLogRoute } from './http/query/log.js';
+import { registerDashboardQueryRoute } from './http/query/dashboard.js';
+import { registerAlarmsQueryRoutes } from './http/query/alarms.js';
+import { registerPreflightRoutes } from './http/query/preflight.js';
+import { registerProfileRoutes } from './http/query/profile.js';
+import { registerEBPFRoutes } from './http/query/ebpf.js';
+import { registerAsyncProfileRoutes } from './http/query/async-profile.js';
+// Config (CRUD for templates / settings)
+import { registerDashboardConfigRoute } from './http/config/dashboard.js';
+import { registerLayerTemplateRoutes } from './http/config/layer-template.js';
+import { registerAlarmsConfigRoutes } from './http/config/alarms.js';
+import { registerSetupRoutes } from './http/config/setup.js';
+import { registerOverviewRoutes } from './http/config/overview.js';
+// Admin (operational tools)
+import { registerDslCatalogRoutes } from './http/admin/dsl/catalog.js';
+import { registerDslRuleRoutes } from './http/admin/dsl/rule.js';
+import { registerDslDumpRoutes } from './http/admin/dsl/dump.js';
+import { registerDslOalRoutes } from './http/admin/dsl/oal.js';
+import { registerClusterRoutes } from './http/admin/cluster.js';
+import { registerDebugRoutes } from './http/admin/live-debug.js';
+import { registerInspectRoutes } from './http/admin/inspect.js';
+// Logic / stores
+import { AlarmsStore } from './logic/alarms/store.js';
+import { SetupStore } from './logic/setup/store.js';
+import { ServiceLayerMap } from './logic/alarms/service-layer-map.js';
import { HttpError } from './errors.js';
import { logger, loggerOptions } from './logger.js';
@@ -75,13 +88,18 @@ const setupStore = new
SetupStore(source.current.setup.file);
await setupStore.load();
const alarmsStore = new AlarmsStore(source.current.alarms.file);
await alarmsStore.load();
+// Shared between alarms query (read) + alarms config (write+invalidate).
+const serviceLayer = new ServiceLayerMap({ config: source });
await app.register(cookie);
// Text/plain body parser — the rule editor sends raw YAML to /api/rule.
app.addContentTypeParser('text/plain', { parseAs: 'string' }, (_req, body,
done) => done(null, body));
+// ── User ───────────────────────────────────────────────────────────
registerAuthRoutes(app, source, sessions, audit);
+
+// ── Query ──────────────────────────────────────────────────────────
registerOapInfoRoute(app, { config: source, sessions });
registerMenuRoute(app, { config: source, sessions });
registerLandingRoute(app, { config: source, sessions });
@@ -93,23 +111,29 @@ registerTraceRoutes(app, { config: source, sessions });
registerTraceTagRoutes(app, { config: source, sessions });
registerZipkinRoutes(app, { config: source, sessions });
registerLogRoute(app, { config: source, sessions });
-registerOverviewRoutes(app, { config: source, sessions });
-registerDashboardRoute(app, { config: source, sessions });
-registerSetupRoutes(app, { config: source, sessions, audit, store: setupStore
});
-registerOapRoutes(app, { config: source, sessions, audit });
+registerDashboardQueryRoute(app, { config: source, sessions });
+registerAlarmsQueryRoutes(app, { config: source, sessions, serviceLayer,
store: alarmsStore });
registerPreflightRoutes(app, { config: source, sessions });
-// Live debugger — `/api/debug/*` proxies for SWIP-13's
-// `/dsl-debugging/*` wire (start / poll / stop session, list active
-// sessions, per-node status fan-out).
-registerDebugRoutes(app, { config: source, sessions, audit });
-// Inspect — OAP metric catalog browse + MQE ad-hoc execution.
-registerInspectRoutes(app, { config: source, sessions, audit });
-// Alarms — getAlarm proxy + traffic-background fan-out + config CRUD.
-registerAlarmsRoutes(app, { config: source, sessions, audit, store:
alarmsStore });
registerProfileRoutes(app, { config: source, sessions });
registerEBPFRoutes(app, { config: source, sessions });
registerAsyncProfileRoutes(app, { config: source, sessions });
+// ── Config ─────────────────────────────────────────────────────────
+registerDashboardConfigRoute(app, { config: source, sessions });
+registerLayerTemplateRoutes(app, { config: source, sessions });
+registerAlarmsConfigRoutes(app, { config: source, sessions, audit, store:
alarmsStore, serviceLayer });
+registerSetupRoutes(app, { config: source, sessions, audit, store: setupStore
});
+registerOverviewRoutes(app, { config: source, sessions });
+
+// ── Admin ──────────────────────────────────────────────────────────
+registerDslCatalogRoutes(app, { config: source, sessions, audit });
+registerDslRuleRoutes(app, { config: source, sessions, audit });
+registerDslDumpRoutes(app, { config: source, sessions, audit });
+registerDslOalRoutes(app, { config: source, sessions, audit });
+registerClusterRoutes(app, { config: source, sessions, audit });
+registerDebugRoutes(app, { config: source, sessions, audit });
+registerInspectRoutes(app, { config: source, sessions, audit });
+
// Serve the built SPA out of the BFF when HORIZON_STATIC_DIR points at a
// directory (Docker image layout: /app/static contains the Vite dist).
// Local dev keeps using the Vite dev-server on :9091 so this is a no-op
diff --git a/apps/bff/src/util/trace-protocol-cache.ts
b/apps/bff/src/util/trace-protocol-cache.ts
index b94398e..e7f93a7 100644
--- a/apps/bff/src/util/trace-protocol-cache.ts
+++ b/apps/bff/src/util/trace-protocol-cache.ts
@@ -32,7 +32,7 @@
* again.
*/
-import { graphqlPost } from './graphql-client.js';
+import { graphqlPost } from '../client/graphql.js';
import type { FetchLike } from '@skywalking-horizon-ui/api-client';
export type TraceProtocol = 'v2' | 'v3';
diff --git a/apps/ui/src/views/auth/LoginView.vue
b/apps/ui/src/views/auth/LoginView.vue
index b427fd3..dca2f21 100644
--- a/apps/ui/src/views/auth/LoginView.vue
+++ b/apps/ui/src/views/auth/LoginView.vue
@@ -84,10 +84,6 @@ async function submit(): Promise<void> {
<button class="sw-btn is-primary submit" type="submit"
:disabled="submitting">
{{ submitting ? 'Signing in…' : 'Sign in' }}
</button>
-
- <div class="foot">
- Local + LDAP auth. OIDC and SSO are out of scope for v1.
- </div>
</form>
</div>
</template>
@@ -181,11 +177,4 @@ async function submit(): Promise<void> {
opacity: 0.6;
cursor: not-allowed;
}
-.foot {
- margin-top: 18px;
- font-size: 11px;
- color: var(--sw-fg-3);
- text-align: center;
- line-height: 1.5;
-}
</style>