// src/server.ts
import { existsSync as existsSync5 } from "node:fs";
import { resolve as resolvePath } from "node:path";
import Fastify from "fastify";
import cookie from "@fastify/cookie";
import fastifyStatic from "@fastify/static";

// src/audit/logger.ts
import { createWriteStream } from "node:fs";
import { mkdir } from "node:fs/promises";
import { dirname, resolve } from "node:path";

// src/logger.ts
import pino from "pino";
var isDev = process.env.NODE_ENV === "development";
var loggerOptions = {
  level: process.env.LOG_LEVEL ?? (isDev ? "debug" : "error"),
  ...isDev ? {
    transport: {
      target: "pino-pretty",
      options: {
        colorize: true,
        translateTime: "HH:MM:ss.l",
        ignore: "pid,hostname"
      }
    }
  } : {}
};
var logger = pino(loggerOptions);

// src/audit/logger.ts
var AuditLogger = class {
  stream = null;
  absPath;
  constructor(filePath) {
    this.absPath = resolve(filePath);
  }
  async open() {
    await mkdir(dirname(this.absPath), { recursive: true });
    this.stream = createWriteStream(this.absPath, { flags: "a" });
    this.stream.on("error", (err) => logger.error({ err }, "audit stream error"));
  }
  record(evt) {
    const line = { ts: (/* @__PURE__ */ new Date()).toISOString(), ...evt };
    if (!this.stream) {
      logger.warn({ evt: line }, "audit logged before open()");
      return;
    }
    this.stream.write(JSON.stringify(line) + "\n");
  }
  async close() {
    if (!this.stream) return;
    await new Promise((resolveDone) => this.stream.end(() => resolveDone()));
    this.stream = null;
  }
};

// src/user/sessions.ts
import { randomBytes } from "node:crypto";
var SessionStore = class {
  sessions = /* @__PURE__ */ new Map();
  ttlMs;
  reaper;
  constructor(opts) {
    this.ttlMs = opts.ttlMinutes * 6e4;
    this.reaper = setInterval(() => this.reap(), opts.reapIntervalMs ?? 6e4);
    this.reaper.unref?.();
  }
  create(username, roles) {
    const sid = randomBytes(32).toString("base64url");
    const now = Date.now();
    const session = { sid, username, roles, createdAt: now, lastSeenAt: now };
    this.sessions.set(sid, session);
    return session;
  }
  touch(sid) {
    const session = this.sessions.get(sid);
    if (!session) return void 0;
    if (Date.now() - session.lastSeenAt > this.ttlMs) {
      this.sessions.delete(sid);
      return void 0;
    }
    session.lastSeenAt = Date.now();
    return session;
  }
  // Read-without-touch — used by route handlers that just need identity
  // and don't want to slide the TTL window. Returns `undefined` for
  // expired sessions.
  get(sid) {
    const session = this.sessions.get(sid);
    if (!session) return void 0;
    if (Date.now() - session.lastSeenAt > this.ttlMs) {
      this.sessions.delete(sid);
      return void 0;
    }
    return session;
  }
  destroy(sid) {
    this.sessions.delete(sid);
  }
  size() {
    return this.sessions.size;
  }
  reap() {
    const now = Date.now();
    for (const [sid, s] of this.sessions) {
      if (now - s.lastSeenAt > this.ttlMs) this.sessions.delete(sid);
    }
  }
  async close() {
    clearInterval(this.reaper);
    this.sessions.clear();
  }
};

// src/user/ldap.ts
import { Client } from "ldapts";
function escapeFilterValue(input) {
  let out = "";
  for (const ch of input) {
    switch (ch) {
      case "\\":
        out += "\\5c";
        break;
      case "*":
        out += "\\2a";
        break;
      case "(":
        out += "\\28";
        break;
      case ")":
        out += "\\29";
        break;
      case "\0":
        out += "\\00";
        break;
      default:
        out += ch;
    }
  }
  return out;
}
function clientFor(cfg) {
  return new Client({
    url: cfg.url,
    timeout: cfg.timeoutMs,
    connectTimeout: cfg.timeoutMs,
    tlsOptions: cfg.tlsInsecure ? { rejectUnauthorized: false } : void 0
  });
}
async function safeUnbind(client) {
  try {
    await client.unbind();
  } catch {
  }
}
async function searchUser(client, cfg, username) {
  const filter = cfg.userFilter.replaceAll("{username}", escapeFilterValue(username));
  const attrs = ["dn", cfg.displayNameAttr];
  if (cfg.groupStrategy === "memberOf") attrs.push("memberOf");
  const opts = {
    scope: "sub",
    filter,
    attributes: attrs,
    sizeLimit: 2,
    timeLimit: Math.ceil(cfg.timeoutMs / 1e3)
  };
  const { searchEntries } = await client.search(cfg.userBaseDn, opts);
  if (searchEntries.length === 0) return null;
  if (searchEntries.length > 1) {
    logger.warn(
      { username, count: searchEntries.length },
      "ldap user search returned multiple entries; using first"
    );
  }
  const e = searchEntries[0];
  const displayName = typeof e[cfg.displayNameAttr] === "string" ? e[cfg.displayNameAttr] : Array.isArray(e[cfg.displayNameAttr]) ? e[cfg.displayNameAttr][0] ?? String(e.dn) : String(e.dn);
  const memberOfRaw = e.memberOf;
  const memberOf = Array.isArray(memberOfRaw) ? memberOfRaw.map(String) : typeof memberOfRaw === "string" ? [memberOfRaw] : [];
  return { dn: String(e.dn), displayName, memberOfFromEntry: memberOf };
}
async function searchGroupsByMember(client, cfg, userDn) {
  if (!cfg.groupBaseDn) return [];
  const filter = `(${cfg.memberAttr}=${escapeFilterValue(userDn)})`;
  const opts = {
    scope: "sub",
    filter,
    attributes: ["dn"],
    sizeLimit: 256,
    timeLimit: Math.ceil(cfg.timeoutMs / 1e3)
  };
  const { searchEntries } = await client.search(cfg.groupBaseDn, opts);
  return searchEntries.map((e) => String(e.dn));
}
function mapGroupsToRoles(cfg, groupDns) {
  const set = /* @__PURE__ */ new Set();
  const groups = new Set(groupDns.map((g) => g.toLowerCase()));
  for (const mapping of cfg.groupMappings) {
    if (mapping.group === "*") {
      set.add(mapping.role);
    } else if (groups.has(mapping.group.toLowerCase())) {
      set.add(mapping.role);
    }
  }
  return [...set];
}
async function verifyLdapCredentials(cfg, username, password) {
  const searchClient = clientFor(cfg);
  let user = null;
  try {
    if (cfg.bindDn) {
      await searchClient.bind(cfg.bindDn, cfg.bindPassword);
    }
    user = await searchUser(searchClient, cfg, username);
  } catch (err) {
    logger.warn({ err: errMsg(err), username }, "ldap user search failed");
    await safeUnbind(searchClient);
    return null;
  }
  if (!user) {
    await safeUnbind(searchClient);
    return null;
  }
  const userClient = clientFor(cfg);
  try {
    await userClient.bind(user.dn, password);
  } catch {
    await safeUnbind(userClient);
    await safeUnbind(searchClient);
    return null;
  }
  await safeUnbind(userClient);
  let groups = [];
  try {
    if (cfg.groupStrategy === "memberOf") {
      groups = user.memberOfFromEntry;
    } else {
      groups = await searchGroupsByMember(searchClient, cfg, user.dn);
    }
  } catch (err) {
    logger.warn({ err: errMsg(err), dn: user.dn }, "ldap group resolution failed");
    groups = [];
  }
  await safeUnbind(searchClient);
  const roles = mapGroupsToRoles(cfg, groups);
  if (roles.length === 0) {
    logger.warn(
      { username, groups },
      "ldap user authenticated but matched zero group mappings; rejecting login"
    );
    return null;
  }
  return {
    username,
    displayName: user.displayName,
    dn: user.dn,
    groups,
    roles
  };
}
async function probeLdap(cfg) {
  const start = Date.now();
  const client = clientFor(cfg);
  const result = {
    reachable: false,
    serviceBindOk: null,
    userSearchOk: null,
    userEntriesVisible: null,
    latencyMs: null
  };
  try {
    if (cfg.bindDn) {
      await client.bind(cfg.bindDn, cfg.bindPassword);
      result.reachable = true;
      result.serviceBindOk = true;
    } else {
      result.reachable = true;
      result.serviceBindOk = null;
    }
    try {
      const { searchEntries } = await client.search(cfg.userBaseDn, {
        scope: "one",
        filter: "(objectClass=*)",
        attributes: ["dn"],
        sizeLimit: 10,
        timeLimit: Math.ceil(cfg.timeoutMs / 1e3)
      });
      result.userSearchOk = true;
      result.userEntriesVisible = searchEntries.length;
    } catch (err) {
      result.userSearchOk = false;
      result.error = errMsg(err);
    }
  } catch (err) {
    result.reachable = false;
    result.serviceBindOk = cfg.bindDn ? false : null;
    result.error = errMsg(err);
  } finally {
    result.latencyMs = Date.now() - start;
    await safeUnbind(client);
  }
  return result;
}
async function resolveLdapUser(cfg, username) {
  const client = clientFor(cfg);
  try {
    if (cfg.bindDn) await client.bind(cfg.bindDn, cfg.bindPassword);
    const user = await searchUser(client, cfg, username);
    if (!user) {
      await safeUnbind(client);
      return { found: false, dn: null, groups: [], roles: [] };
    }
    let groups = user.memberOfFromEntry;
    if (cfg.groupStrategy === "search") {
      groups = await searchGroupsByMember(client, cfg, user.dn);
    }
    await safeUnbind(client);
    return {
      found: true,
      dn: user.dn,
      groups,
      roles: mapGroupsToRoles(cfg, groups)
    };
  } catch (err) {
    await safeUnbind(client);
    return { found: false, dn: null, groups: [], roles: [], error: errMsg(err) };
  }
}
function errMsg(err) {
  return err instanceof Error ? err.message : String(err);
}

// src/user/ldap-health.ts
var LdapHealth = class {
  last = null;
  lastAt = null;
  /** Coalesce concurrent probes — only one in-flight at a time. */
  inFlight = null;
  async probe(cfg) {
    if (this.inFlight) return this.inFlight;
    this.inFlight = probeLdap(cfg).then((r) => {
      this.last = r;
      this.lastAt = Date.now();
      return r;
    }).finally(() => {
      this.inFlight = null;
    });
    return this.inFlight;
  }
  /** Returns the cached probe result if it's still fresh, else re-probes. */
  async getOrProbe(cfg, freshnessMs = 3e4) {
    if (this.last && this.lastAt && Date.now() - this.lastAt < freshnessMs) {
      return this.last;
    }
    return this.probe(cfg);
  }
  snapshot() {
    return { result: this.last, at: this.lastAt };
  }
  /**
   * Currently considered unhealthy if the last probe failed, or if no
   * probe has run yet. The caller (break-glass guard) should call
   * `probe()` first to make sure this reflects the current state.
   */
  isUnhealthy() {
    if (!this.last) return true;
    return !this.last.reachable || this.last.userSearchOk === false;
  }
};

// src/user/seen-cache.ts
var UserSeenCache = class {
  users = /* @__PURE__ */ new Map();
  record(u) {
    this.users.set(u.username, {
      username: u.username,
      source: u.source,
      roles: u.roles,
      lastSeenAt: Date.now(),
      lastIp: u.ip ?? null
    });
  }
  list() {
    return [...this.users.values()].sort((a, b) => b.lastSeenAt - a.lastSeenAt);
  }
  get(username) {
    return this.users.get(username);
  }
  size() {
    return this.users.size;
  }
  clear() {
    this.users.clear();
  }
};

// src/config/loader.ts
import { readFileSync } from "node:fs";
import { resolve as resolve2 } from "node:path";
import chokidar from "chokidar";
import YAML from "yaml";

// src/config/schema.ts
import { z } from "zod";
var serverHostDefault = process.env.HORIZON_SERVER_HOST ?? "127.0.0.1";
var serverPortDefault = (() => {
  const raw = process.env.HORIZON_SERVER_PORT;
  if (!raw) return 8081;
  const n = Number(raw);
  return Number.isFinite(n) && n > 0 ? Math.floor(n) : 8081;
})();
var serverSchema = z.object({
  host: z.string().default(serverHostDefault),
  port: z.number().int().positive().default(serverPortDefault),
  staticDir: z.string().optional()
}).strict();
var oapSchema = z.object({
  /** OAP query host (GraphQL + `/status/*`). Single URL — query traffic
   *  is load-balanceable, any OAP node can answer. */
  queryUrl: z.string().url().default("http://127.0.0.1:12800"),
  /** OAP admin host (runtime rule mgmt, DSL/MQE/OAL, inspect, live
   *  debug). Single URL. Most endpoints get a single fire (OAP routes
   *  cluster-internal); live-debug status performs a DNS lookup on
   *  the hostname to discover all node IPs and probes each. */
  adminUrl: z.string().url().default("http://127.0.0.1:17128"),
  timeoutMs: z.number().int().positive().default(15e3),
  auth: z.object({
    username: z.string().min(1),
    password: z.string().min(1)
  }).strict().optional(),
  mqe: z.object({
    host: z.string().optional(),
    port: z.number().int().positive().optional()
  }).strict().default({}),
  zipkinUrl: z.string().url().default("http://127.0.0.1:9412/zipkin")
}).strict();
var localUserSchema = z.object({
  username: z.string().min(1),
  passwordHash: z.string().min(1),
  roles: z.array(z.string().min(1)).default([])
}).strict();
var ldapGroupMappingSchema = z.object({
  /** LDAP group DN (or the literal "*" to match any authenticated user). */
  group: z.string().min(1),
  /** Horizon role assigned when the user's group memberships include `group`. */
  role: z.string().min(1)
}).strict();
var ldapSchema = z.object({
  /** Directory URL, e.g. `ldaps://ldap.corp:636` or `ldap://localhost:389`. */
  url: z.string().min(1),
  /** Optional service-account DN used for user/group searches.
   *  When empty, an anonymous bind is attempted for searches. */
  bindDn: z.string().default(""),
  /** Service-account password. Supports `${VAR:default}` interpolation
   *  in the YAML; empty means anonymous search. */
  bindPassword: z.string().default(""),
  /** Base DN under which user entries live (e.g. `ou=people,dc=corp`). */
  userBaseDn: z.string().min(1),
  /** Search filter; `{username}` is substituted with the typed username,
   *  escaped per RFC 4515. Default targets the common `uid` attribute. */
  userFilter: z.string().default("(uid={username})"),
  /** Attribute on the user entry that holds the display name. */
  displayNameAttr: z.string().default("cn"),
  /** Group membership strategy:
   *  - `memberOf`  → read the user entry's `memberOf` attribute (AD-style).
   *  - `search`    → search `groupBaseDn` for groups whose `memberAttr`
   *                   contains the user's DN (OpenLDAP-style). */
  groupStrategy: z.enum(["memberOf", "search"]).default("memberOf"),
  /** Base DN under which group entries live. Only used when
   *  `groupStrategy: 'search'`. */
  groupBaseDn: z.string().default(""),
  /** Group attribute that lists members (e.g. `member`, `uniqueMember`).
   *  Only used when `groupStrategy: 'search'`. */
  memberAttr: z.string().default("member"),
  /** Group DN → Horizon role bindings. First match wins; `"*"` matches
   *  every authenticated user. Order matters — put the highest-privilege
   *  rule first if you only want one role per user, or use multiple
   *  entries to assign multiple roles. */
  groupMappings: z.array(ldapGroupMappingSchema).default([]),
  /** Bind / search timeout in ms. */
  timeoutMs: z.number().int().positive().default(5e3),
  /** When `true`, skip TLS certificate validation. Use only for dev
   *  directories with self-signed certs; never in production. */
  tlsInsecure: z.boolean().default(false)
}).strict();
var breakGlassSchema = z.object({
  /** Username allowed to log in via break-glass. When unset, break-glass
   *  is disabled. */
  username: z.string().min(1),
  /** Argon2id hash of the break-glass password (use `pnpm --filter bff cli:hash`). */
  passwordHash: z.string().min(1),
  /** Roles granted when the break-glass session is established.
   *  Defaults to `['admin']` since the whole point of break-glass is
   *  recovering from a broken auth config. */
  roles: z.array(z.string().min(1)).default(["admin"])
}).strict();
var authSchema = z.object({
  /** Active auth backend. Switching to `ldap` causes `auth.local` to be
   *  ignored (a warning is logged at startup if both are populated). */
  backend: z.enum(["local", "ldap"]).default("local"),
  local: z.object({
    users: z.array(localUserSchema).default([])
  }).strict().default({ users: [] }),
  ldap: ldapSchema.optional(),
  /** Optional break-glass account, only honored when `backend: 'ldap'`
   *  AND the LDAP probe is currently failing. Logged loudly in the
   *  audit file on every use. */
  breakGlass: breakGlassSchema.optional()
}).strict().default({ backend: "local", local: { users: [] } });
var rbacSchema = z.object({
  /** When false, every authenticated session is granted `*`. */
  enabled: z.boolean().default(true),
  roles: z.record(z.string(), z.array(z.string().min(1))).default({
    // Data catalog only — public dashboards, alarms, traces, logs,
    // topology, profiling results. Deliberately NOT `*:read` so a
    // viewer can't accidentally see rule definitions, live-debug
    // sessions, setup screens, or platform internals.
    viewer: [
      "metrics:read",
      "alarms:read",
      "traces:read",
      "logs:read",
      "topology:read",
      "profile:read",
      "overview:read"
    ],
    // Viewer baseline plus the platform-monitoring reads (cluster
    // health + OAP internals). Maintainer's whole job is watching
    // SkyWalking itself.
    maintainer: [
      "metrics:read",
      "alarms:read",
      "traces:read",
      "logs:read",
      "topology:read",
      "profile:read",
      "overview:read",
      "cluster:read",
      "inspect:read",
      "ttl:read",
      "config:read"
    ],
    // Configures observability: dashboards, alarm rules, DSL/OAL,
    // diagnostics. Inherits viewer + platform reads so operators
    // can verify their changes against live data.
    operator: [
      "metrics:read",
      "alarms:read",
      "traces:read",
      "logs:read",
      "topology:read",
      "profile:read",
      "cluster:read",
      "inspect:read",
      "ttl:read",
      "config:read",
      "overview:read",
      "overview:write",
      "setup:read",
      "setup:write",
      "dashboard:read",
      "dashboard:write",
      "alarm-setup:read",
      "alarm-setup:write",
      "alarm-rule:read",
      "alarm-rule:write",
      "rule:read",
      "rule:write",
      "rule:write:structural",
      "rule:delete",
      "rule:debug",
      "live-debug:read",
      "live-debug:write",
      "profile:enable"
    ],
    admin: ["*"]
  }),
  /** Landing route per role; the UI uses this to send users to the
   *  page that fits their job after login. Cluster status lives at
   *  `/operate/cluster` (operator tooling against OAP) — the prior
   *  `/admin/cluster` defaults 404'd because no such route exists. */
  landingByRole: z.record(z.string(), z.string()).default({
    viewer: "/",
    maintainer: "/operate/cluster",
    operator: "/",
    admin: "/operate/cluster"
  })
}).strict().default({});
var sessionSchema = z.object({
  ttlMinutes: z.number().int().positive().default(60),
  cookieName: z.string().default("horizon_sid"),
  cookieSecure: z.boolean().default(false)
}).strict().default({ ttlMinutes: 60, cookieName: "horizon_sid", cookieSecure: false });
var auditDefault = process.env.HORIZON_AUDIT_FILE ?? "./horizon-audit.jsonl";
var setupDefault = process.env.HORIZON_SETUP_FILE ?? "./horizon-setup.json";
var alarmsDefault = process.env.HORIZON_ALARMS_FILE ?? "./horizon-alarms.json";
var wireLogDefault = process.env.HORIZON_WIRE_LOG_FILE ?? "./horizon-wire.jsonl";
var auditSchema = z.object({
  file: z.string().default(auditDefault)
}).strict().default({ file: auditDefault });
var setupSchema = z.object({
  file: z.string().default(setupDefault)
}).strict().default({ file: setupDefault });
var alarmsSchema = z.object({
  file: z.string().default(alarmsDefault)
}).strict().default({ file: alarmsDefault });
var debugLogSchema = z.object({
  enabled: z.boolean().default(false),
  file: z.string().default(wireLogDefault),
  maxBodyChars: z.number().int().nonnegative().default(8192),
  redactAuthHeaders: z.boolean().default(true)
}).strict().default({
  enabled: false,
  file: wireLogDefault,
  maxBodyChars: 8192,
  redactAuthHeaders: true
});
var configSchema = z.object({
  server: serverSchema.default({}),
  oap: oapSchema.default({}),
  auth: authSchema,
  rbac: rbacSchema,
  session: sessionSchema,
  audit: auditSchema,
  setup: setupSchema,
  alarms: alarmsSchema,
  debugLog: debugLogSchema
}).strict();

// src/config/loader.ts
function interpolateEnv(raw, env = process.env) {
  return raw.replace(/\$\{([A-Z_][A-Z0-9_]*)(?::([^}]*))?\}/gi, (_m, name, def) => {
    const v = env[name];
    if (v !== void 0 && v !== "") return v;
    return def ?? "";
  });
}
var BootstrapError = class extends Error {
  constructor(message) {
    super(message);
    this.name = "BootstrapError";
  }
};
function isAuthConfigured(cfg) {
  if (cfg.auth.backend === "local") {
    return cfg.auth.local.users.length > 0;
  }
  if (cfg.auth.backend === "ldap") {
    return !!cfg.auth.ldap && cfg.auth.ldap.groupMappings.length > 0;
  }
  return false;
}
function validateBootstrap(cfg) {
  if (cfg.auth.backend === "local" && cfg.auth.local.users.length === 0) {
    logger.warn(
      'auth.backend is "local" but auth.local.users is empty. BFF is booting but no login will succeed until you add at least one user (use `pnpm --filter bff cli:hash` for the password hash) or switch to LDAP. The login page will surface this state to the operator.'
    );
  } else if (cfg.auth.backend === "ldap") {
    if (!cfg.auth.ldap) {
      logger.warn(
        'auth.backend is "ldap" but auth.ldap is missing. BFF is booting but every login attempt will fail until you configure the directory connection or switch to local users.'
      );
    } else if (cfg.auth.ldap.groupMappings.length === 0) {
      logger.warn(
        'auth.ldap.groupMappings is empty \u2014 no LDAP user would be assigned any role, so every login will fail. Add at least one mapping (use `group: "*"` to assign a fallback role to everyone). BFF is booting; the login page will surface this state.'
      );
    }
  }
  return cfg;
}
function parseFile(absPath) {
  let raw = "";
  try {
    raw = readFileSync(absPath, "utf8");
  } catch (err) {
    if (err.code === "ENOENT") {
      return configSchema.parse({});
    }
    throw err;
  }
  const interpolated = interpolateEnv(raw);
  const parsed = YAML.parse(interpolated) ?? {};
  return configSchema.parse(parsed);
}
function loadConfig(configPath2) {
  const absPath = resolve2(configPath2);
  let current = parseFile(absPath);
  validateBootstrap(current);
  const listeners = /* @__PURE__ */ new Set();
  const watcher2 = chokidar.watch(absPath, { ignoreInitial: true, awaitWriteFinish: true });
  watcher2.on("change", () => {
    try {
      const next = parseFile(absPath);
      validateBootstrap(next);
      current = next;
      for (const fn of listeners) fn(next);
    } catch {
    }
  });
  return {
    get current() {
      return current;
    },
    current_: () => current,
    path: absPath,
    onChange(fn) {
      listeners.add(fn);
      return () => listeners.delete(fn);
    },
    async close() {
      await watcher2.close();
    }
  };
}

// src/rbac/verbs.ts
var VERBS = {
  // Public data reads — covered by `*:read` for the viewer baseline. Per-area
  // verbs let an admin grant read on one area without granting all reads.
  metricsRead: "metrics:read",
  alarmsRead: "alarms:read",
  tracesRead: "traces:read",
  logsRead: "logs:read",
  topologyRead: "topology:read",
  profileRead: "profile:read",
  // Operate writes
  ruleRead: "rule:read",
  ruleWrite: "rule:write",
  ruleWriteStructural: "rule:write:structural",
  ruleDelete: "rule:delete",
  ruleDebug: "rule:debug",
  dashboardRead: "dashboard:read",
  dashboardWrite: "dashboard:write",
  overviewRead: "overview:read",
  overviewWrite: "overview:write",
  setupRead: "setup:read",
  setupWrite: "setup:write",
  alarmSetupRead: "alarm-setup:read",
  alarmSetupWrite: "alarm-setup:write",
  alarmRuleRead: "alarm-rule:read",
  alarmRuleWrite: "alarm-rule:write",
  liveDebugRead: "live-debug:read",
  liveDebugWrite: "live-debug:write",
  /** Single verb covering task-creation across all profiling families
   *  (agent / async / pprof / eBPF cpu / eBPF network). Reads ride on
   *  `*:read` via `profile:read`. */
  profileEnable: "profile:enable",
  // Platform monitoring (read-only screens that focus on OAP itself).
  clusterRead: "cluster:read",
  inspectRead: "inspect:read",
  ttlRead: "ttl:read",
  configRead: "config:read",
  // Admin surface
  userRead: "user:read",
  userWrite: "user:write",
  roleRead: "role:read",
  roleWrite: "role:write",
  authRead: "auth:read",
  auditRead: "audit:read",
  admin: "admin"
};
function matchOne(grant, required) {
  if (grant === "*" || grant === "admin") return true;
  if (grant === required) return true;
  const [grantArea, grantAction, grantSub] = grant.split(":", 3);
  const [reqArea, reqAction, reqSub] = required.split(":", 3);
  if (grantArea === reqArea && grantAction === "*") return true;
  if (grantArea === "*" && grantAction === reqAction && (grantSub ?? "") === (reqSub ?? "")) return true;
  if (grantArea === reqArea && grantAction === reqAction && (grantSub ?? "") === (reqSub ?? ""))
    return true;
  return false;
}
function hasVerb(grantedVerbs, required) {
  for (const g of grantedVerbs) if (matchOne(g, required)) return true;
  return false;
}
function resolveVerbsForRoles(rolePolicy, userRoles, rbacEnabled) {
  if (!rbacEnabled) return ["*"];
  const set = /* @__PURE__ */ new Set();
  for (const r of userRoles) for (const v of rolePolicy[r] ?? []) set.add(v);
  return [...set];
}

// src/rbac/policy.ts
function sessionHasVerb(config, roles, required) {
  const verbs = resolveVerbsForRoles(config.rbac.roles, roles, config.rbac.enabled);
  return hasVerb(verbs, required);
}

// src/user/middleware.ts
function requireAuth(deps) {
  return async function authPreHandler(req, reply) {
    const cookieName = deps.config.current.session.cookieName;
    const sid = req.cookies?.[cookieName];
    if (!sid) {
      return void reply.code(401).send({ error: "unauthenticated" });
    }
    const session = deps.sessions.touch(sid);
    if (!session) {
      return void reply.code(401).send({ error: "unauthenticated" });
    }
    req.session = session;
  };
}

// src/rbac/route-policy.ts
function checkVerb(deps, verb) {
  return async function verbOnlyPreHandler(req, reply) {
    const session = req.session;
    if (!session) {
      return void reply.code(401).send({ error: "unauthenticated" });
    }
    if (!sessionHasVerb(deps.config.current, session.roles, verb)) {
      return void reply.code(403).send({ error: "permission_denied", verb });
    }
  };
}
var ROUTE_POLICY = {
  // ── Public (no auth) ─────────────────────────────────────────────
  "POST /api/auth/login": "public",
  "POST /api/auth/logout": "public",
  "GET /api/health": "public",
  "GET /api/auth/health": "public",
  // ── Auth-only (every authenticated user) ─────────────────────────
  // Anything the shell needs to render before the user lands anywhere.
  "GET /api/auth/me": "auth",
  "GET /api/oap/info": "auth",
  "GET /api/menu": "auth",
  "GET /api/preflight": "auth",
  "GET /api/configs/bundle": "auth",
  // ── Alarms (read) ────────────────────────────────────────────────
  "GET /api/alarms": "alarms:read",
  "GET /api/alarms/count": "alarms:read",
  "GET /api/alarms/services": "alarms:read",
  // ── Traces (read) ────────────────────────────────────────────────
  "POST /api/layer/:key/traces": "traces:read",
  "GET /api/trace/:traceId": "traces:read",
  "GET /api/trace-tags/keys": "traces:read",
  "GET /api/trace-tags/values": "traces:read",
  "GET /api/zipkin/services": "traces:read",
  "GET /api/zipkin/spans": "traces:read",
  "GET /api/zipkin/remote-services": "traces:read",
  "GET /api/zipkin/traces": "traces:read",
  "GET /api/zipkin/trace/:traceId": "traces:read",
  "GET /api/zipkin/autocomplete/keys": "traces:read",
  "GET /api/zipkin/autocomplete/values": "traces:read",
  "GET /api/zipkin/traceMany": "traces:read",
  // ── Logs (read) ──────────────────────────────────────────────────
  "POST /api/layer/:key/logs": "logs:read",
  "POST /api/layer/:key/logs/facets": "logs:read",
  "GET /api/log-tags/keys": "logs:read",
  "GET /api/log-tags/values": "logs:read",
  // ── Topology (read) ──────────────────────────────────────────────
  "GET /api/layer/:key/topology": "topology:read",
  "GET /api/layer/:key/endpoint-dependency": "topology:read",
  // ── Metrics & layer-level reads ──────────────────────────────────
  "POST /api/layer/:key/dashboard": "metrics:read",
  "GET /api/layer/:key/dashboard/config": "metrics:read",
  "POST /api/layer/:key/landing": "metrics:read",
  "GET /api/layer/:key/instances": "metrics:read",
  "GET /api/layer/:key/endpoints": "metrics:read",
  // ── Profiling — agent / async / pprof / eBPF / eBPF network ──────
  // GETs + analyze are reads; POST <family>/tasks creates a task.
  "GET /api/layer/:key/profile/tasks": "profile:read",
  "POST /api/layer/:key/profile/tasks": "profile:enable",
  "GET /api/profile/tasks/:taskId/segments": "profile:read",
  "GET /api/profile/tasks/:taskId/logs": "profile:read",
  "POST /api/profile/analyze": "profile:read",
  "GET /api/layer/:key/async/tasks": "profile:read",
  "POST /api/layer/:key/async/tasks": "profile:enable",
  "GET /api/async/tasks/:taskId/progress": "profile:read",
  "POST /api/async/analyze": "profile:read",
  "GET /api/layer/:key/pprof/tasks": "profile:read",
  "POST /api/layer/:key/pprof/tasks": "profile:enable",
  "GET /api/pprof/tasks/:taskId/progress": "profile:read",
  "POST /api/pprof/analyze": "profile:read",
  "GET /api/layer/:key/ebpf/tasks": "profile:read",
  "POST /api/layer/:key/ebpf/tasks": "profile:enable",
  "GET /api/ebpf/tasks/:taskId/schedules": "profile:read",
  "POST /api/ebpf/analyze": "profile:read",
  // eBPF network sub-family (extra surface beyond the cpu profiler).
  "GET /api/layer/:key/ebpf/network/tasks": "profile:read",
  "POST /api/layer/:key/ebpf/network/tasks": "profile:enable",
  "GET /api/ebpf/network/tasks": "profile:read",
  "POST /api/ebpf/network/tasks": "profile:enable",
  "GET /api/ebpf/network/topology": "profile:read",
  "POST /api/layer/:key/ebpf/network/process-relation-metrics": "profile:read",
  "POST /api/ebpf/network/tasks/:taskId/keep-alive": "profile:enable",
  // ── Config — alarm-page setup, layer setup, overview, dashboards ─
  "GET /api/alarms/config": "alarm-setup:read",
  "POST /api/alarms/config": "alarm-setup:write",
  "GET /api/setup": "setup:read",
  "POST /api/setup": "setup:write",
  "GET /api/overview/dashboards": "overview:read",
  "GET /api/overview/dashboards/:id": "overview:read",
  "GET /api/admin/layer-templates": "dashboard:read",
  // POST /api/admin/layer-templates/:key removed — updates go through
  // `/api/admin/templates/save` (OAP-backed). See template-sync.ts.
  // ── DSL / OAL / MQE rules (admin operate) ────────────────────────
  "GET /api/rule": "rule:read",
  "POST /api/rule": "rule:write",
  "POST /api/rule/inactivate": "rule:write",
  "POST /api/rule/delete": "rule:delete",
  "GET /api/dump": "rule:debug",
  "GET /api/dump/:catalog": "rule:debug",
  "GET /api/oal/files": "rule:read",
  "GET /api/oal/files/:name": "rule:read",
  "GET /api/oal/rules": "rule:read",
  "GET /api/oal/rules/:source": "rule:read",
  "GET /api/catalog/list": "rule:read",
  "GET /api/catalog/bundled": "rule:read",
  // ── Platform monitoring (read) ───────────────────────────────────
  "GET /api/cluster/state": "cluster:read",
  "GET /api/inspect/metrics": "inspect:read",
  "GET /api/inspect/catalog": "inspect:read",
  "GET /api/inspect/mqe-target": "inspect:read",
  "GET /api/inspect/server-time": "inspect:read",
  "POST /api/inspect/exec": "inspect:read",
  "GET /api/inspect/entities": "inspect:read",
  "GET /api/oap/ttl": "ttl:read",
  "GET /api/oap/config": "config:read",
  // ── Live debugger (admin operate) ────────────────────────────────
  "POST /api/debug/session": "live-debug:write",
  "GET /api/debug/session/:id": "live-debug:read",
  "POST /api/debug/session/:id/stop": "live-debug:write",
  "GET /api/debug/sessions": "live-debug:read",
  "GET /api/debug/status": "live-debug:read",
  // ── Alarm-rule catalog (admin read-only) ─────────────────────────
  "GET /api/admin/alarm-rules": "alarm-rule:read",
  "GET /api/admin/alarm-rules/:id": "alarm-rule:read",
  // ── Overview-template editor (admin) ─────────────────────────────
  // The admin editor is an operate-only surface — even reading the
  // template catalog here needs `overview:write` (operator / admin).
  // Plain viewers/maintainers consume rendered overviews via
  // `GET /api/overview/dashboards`, which stays `overview:read`.
  "GET /api/admin/overview-templates": "overview:write",
  "GET /api/admin/overview-templates/:id": "overview:write",
  "POST /api/admin/overview-templates": "overview:write",
  // POST /api/admin/overview-templates/:id removed — operator updates
  // now go through `/api/admin/templates/save` (OAP-backed). Bundled
  // JSON is immutable at runtime.
  "DELETE /api/admin/overview-templates/:id": "overview:write",
  // ── Template sync (admin) — OAP UI-template REST overlay ─────────
  // Read = anyone with overview:read can see status. Write actions
  // (push-bundled, save, resync) need overview:write because save is
  // the only path that mutates OAP UI-templates.
  "GET /api/admin/templates/sync-status": "overview:read",
  "POST /api/admin/templates/resync": "overview:write",
  "POST /api/admin/templates/save": "overview:write",
  "POST /api/admin/templates/save-local": "overview:write",
  "POST /api/admin/templates/revert-local": "overview:write",
  "POST /api/admin/templates/:name/push-bundled": "overview:write",
  "POST /api/admin/templates/sync-all": "overview:write",
  // ── Auth/admin self-introspection ────────────────────────────────
  "GET /api/admin/auth-status": "auth:read",
  "POST /api/admin/auth-status/probe": "auth:read",
  "GET /api/admin/users": "user:read"
};
function makeRouteAuthHook(deps) {
  return function onRoute(route) {
    const methods = Array.isArray(route.method) ? route.method : [route.method];
    let chosen = null;
    let chosenKey = null;
    for (const m of methods) {
      const M = String(m).toUpperCase();
      const key = `${M} ${route.url}`;
      const p = ROUTE_POLICY[key] ?? (M === "HEAD" ? ROUTE_POLICY[`GET ${route.url}`] : void 0);
      if (p === void 0) continue;
      if (chosen !== null && chosen !== p) {
        logger.warn(
          { url: route.url, methods, conflicting: [chosenKey, key] },
          "rbac: route registered for multiple methods with divergent policies; using the first"
        );
        break;
      }
      chosen = p;
      chosenKey = key;
    }
    if (chosen === null) {
      if (route.url === "*" || route.url.startsWith("/metrics")) return;
      if (route.url.startsWith("/api/")) {
        const msg = `rbac: route ${String(methods)} ${route.url} has no entry in ROUTE_POLICY; add one in apps/bff/src/rbac/route-policy.ts`;
        logger.error({ method: methods, url: route.url }, msg);
        throw new Error(msg);
      }
      return;
    }
    if (chosen === "public") return;
    const existing = Array.isArray(route.preHandler) ? [...route.preHandler] : route.preHandler ? [route.preHandler] : [];
    const hasAuth = existing.some(
      (h) => typeof h === "function" && h.name === "authPreHandler"
    );
    const newHandlers = [];
    if (!hasAuth) newHandlers.push(requireAuth(deps));
    if (chosen !== "auth") newHandlers.push(checkVerb(deps, chosen));
    route.preHandler = [...existing, ...newHandlers];
  };
}

// src/http/user.ts
import { z as z2 } from "zod";

// src/errors.ts
var HttpError = class extends Error {
  statusCode;
  code;
  details;
  constructor(statusCode, code, message, details) {
    super(message);
    this.name = "HttpError";
    this.statusCode = statusCode;
    this.code = code;
    this.details = details;
  }
};
var unauthorized = (msg = "unauthorized") => new HttpError(401, "unauthorized", msg);
var badRequest = (msg = "bad request", details) => new HttpError(400, "bad_request", msg, details);

// src/user/local.ts
import argon2 from "argon2";
var DUMMY_HASH = "$argon2id$v=19$m=65536,t=3,p=4$dummysaltdummysalt$dummyhashdummyhashdummyhashdummyhash";
async function verifyLocalCredentials(cfg, username, password) {
  const user = cfg.auth.local.users.find((u) => u.username === username);
  const hash = user?.passwordHash ?? DUMMY_HASH;
  let ok = false;
  try {
    ok = await argon2.verify(hash, password);
  } catch {
    ok = false;
  }
  if (!user || !ok) return null;
  return { username: user.username, roles: user.roles };
}

// src/user/break-glass.ts
import argon22 from "argon2";
var DUMMY_HASH2 = "$argon2id$v=19$m=65536,t=3,p=4$dummysaltdummysalt$dummyhashdummyhashdummyhashdummyhash";
async function verifyBreakGlass(cfg, username, password) {
  const target = username === cfg.username ? cfg.passwordHash : DUMMY_HASH2;
  let ok = false;
  try {
    ok = await argon22.verify(target, password);
  } catch {
    ok = false;
  }
  if (!ok || username !== cfg.username) return null;
  return { username: cfg.username, roles: cfg.roles };
}

// src/http/user.ts
var loginBodySchema = z2.object({
  username: z2.string().min(1),
  password: z2.string().min(1)
});
function registerAuthRoutes(app2, deps) {
  const { config: source2, sessions: sessions2, audit: audit2, ldapHealth: ldapHealth2, seenCache: seenCache2 } = deps;
  const cookieName = () => source2.current.session.cookieName;
  const cookieSecure = () => source2.current.session.cookieSecure;
  const ttlMs = () => source2.current.session.ttlMinutes * 6e4;
  app2.post("/api/auth/login", async (req, reply) => {
    const parsed = loginBodySchema.safeParse(req.body);
    if (!parsed.success) throw badRequest("invalid login body", parsed.error.flatten());
    const { username, password } = parsed.data;
    const cfg = source2.current;
    const fromIp = req.ip;
    let verified = null;
    let outcomeDetail = "success";
    let source_ = cfg.auth.backend === "ldap" ? "ldap" : "local";
    if (cfg.auth.backend === "local") {
      verified = await verifyLocalCredentials(cfg, username, password);
      source_ = "local";
    } else if (cfg.auth.backend === "ldap" && cfg.auth.ldap) {
      verified = await verifyLdapCredentials(cfg.auth.ldap, username, password);
      if (!verified && cfg.auth.breakGlass) {
        await ldapHealth2.probe(cfg.auth.ldap).catch(() => void 0);
        if (ldapHealth2.isUnhealthy()) {
          const bg = await verifyBreakGlass(cfg.auth.breakGlass, username, password);
          if (bg) {
            verified = bg;
            outcomeDetail = "break-glass";
            source_ = "break-glass";
            logger.warn({ username, fromIp }, "auth: break-glass login granted (LDAP unhealthy)");
          }
        }
      }
    } else {
      logger.error(
        { backend: cfg.auth.backend },
        "auth: backend is ldap but auth.ldap is missing; refusing login"
      );
    }
    if (!verified) {
      audit2.record({
        actor: username,
        action: "auth.login",
        outcome: "failure",
        fromIp,
        details: { backend: cfg.auth.backend }
      });
      throw unauthorized("invalid credentials");
    }
    const session = sessions2.create(verified.username, verified.roles);
    seenCache2.record({
      username: verified.username,
      source: source_,
      roles: verified.roles,
      ip: fromIp
    });
    audit2.record({
      actor: session.username,
      action: outcomeDetail === "break-glass" ? "auth.login.break-glass" : "auth.login",
      outcome: outcomeDetail,
      fromIp,
      sessionId: session.sid,
      details: { backend: cfg.auth.backend, roles: session.roles }
    });
    reply.setCookie(cookieName(), session.sid, {
      httpOnly: true,
      sameSite: "strict",
      secure: cookieSecure(),
      path: "/",
      maxAge: Math.floor(ttlMs() / 1e3)
    });
    const verbs = resolveVerbsForRoles(cfg.rbac.roles, session.roles, cfg.rbac.enabled);
    return {
      username: session.username,
      roles: session.roles,
      verbs,
      landingRoute: pickLandingRoute(cfg.rbac.landingByRole, session.roles)
    };
  });
  app2.post("/api/auth/logout", async (req, reply) => {
    const sid = req.cookies[cookieName()];
    if (sid) {
      const session = sessions2.touch(sid);
      if (session) {
        audit2.record({
          actor: session.username,
          action: "auth.logout",
          outcome: "success",
          fromIp: req.ip,
          sessionId: sid
        });
      }
      sessions2.destroy(sid);
    }
    reply.clearCookie(cookieName(), { path: "/" });
    return { status: "ok" };
  });
  app2.get("/api/auth/me", async (req) => {
    const sid = req.cookies[cookieName()];
    if (!sid) throw unauthorized();
    const session = sessions2.touch(sid);
    if (!session) throw unauthorized();
    const cfg = source2.current;
    const verbs = resolveVerbsForRoles(cfg.rbac.roles, session.roles, cfg.rbac.enabled);
    const landing = pickLandingRoute(cfg.rbac.landingByRole, session.roles);
    return {
      username: session.username,
      roles: session.roles,
      verbs,
      landingRoute: landing
    };
  });
}
function pickLandingRoute(landingByRole, roles) {
  for (const r of roles) {
    if (landingByRole[r]) return landingByRole[r];
  }
  return "/";
}

// src/client/graphql.ts
function buildOapOpts(cfg, fetch) {
  return {
    queryUrl: cfg.oap.queryUrl,
    timeoutMs: cfg.oap.timeoutMs,
    auth: cfg.oap.auth,
    fetch
  };
}
function basicAuthHeader(username, password) {
  const raw = `${username}:${password}`;
  const b64 = typeof Buffer !== "undefined" ? Buffer.from(raw, "utf8").toString("base64") : globalThis.btoa(unescape(encodeURIComponent(raw)));
  return `Basic ${b64}`;
}
var GraphqlError = class extends Error {
  statusCode;
  errors;
  constructor(statusCode, message, errors) {
    super(message);
    this.name = "GraphqlError";
    this.statusCode = statusCode;
    this.errors = errors;
  }
};
async function graphqlPost(opts, query, variables) {
  const f = opts.fetch ?? globalThis.fetch.bind(globalThis);
  const url = opts.queryUrl.replace(/\/$/, "") + "/graphql";
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
  const headers = { "content-type": "application/json" };
  if (opts.auth) {
    headers.authorization = basicAuthHeader(opts.auth.username, opts.auth.password);
  }
  let res;
  try {
    res = await f(url, {
      method: "POST",
      headers,
      body: JSON.stringify({ query, variables: variables ?? {} }),
      signal: controller.signal
    });
  } finally {
    clearTimeout(timer);
  }
  if (!res.ok) {
    const text = await res.text().catch(() => "");
    throw new GraphqlError(res.status, `graphql http ${res.status}: ${text.slice(0, 200)}`);
  }
  const raw = await res.text();
  const ctype = res.headers.get("content-type") ?? "";
  const looksJson = ctype.includes("json") || /^\s*[{[]/.test(raw);
  if (!looksJson) {
    const snippet = raw.replace(/\s+/g, " ").trim().slice(0, 120);
    throw new GraphqlError(
      res.status,
      `OAP query endpoint returned non-JSON (HTTP ${res.status}, content-type "${ctype || "unknown"}") \u2014 expected GraphQL JSON. Check oap.queryUrl points at OAP's GraphQL port, not a UI/proxy. Body: ${snippet}`
    );
  }
  let body;
  try {
    body = JSON.parse(raw);
  } catch (e) {
    throw new GraphqlError(res.status, `OAP returned malformed JSON: ${e instanceof Error ? e.message : String(e)}`);
  }
  if (body.errors && body.errors.length) {
    throw new GraphqlError(200, body.errors.map((e) => e.message).join("; "), body.errors);
  }
  if (body.data === void 0 || body.data === null) {
    throw new GraphqlError(200, "graphql response had no data field");
  }
  return body.data;
}

// src/logic/oap/capabilities.ts
var INTROSPECTION_QUERY = (
  /* GraphQL */
  `
  query HorizonOapCapabilities {
    __type(name: "Query") { fields { name } }
  }
`
);
var cache = /* @__PURE__ */ new Map();
var CAPS_TTL_MS = 5 * 6e4;
var CAPS_FAILURE_TTL_MS = 6e4;
async function getOapCapabilities(config, fetchImpl) {
  const key = config.oap.queryUrl;
  const now = Date.now();
  const hit = cache.get(key);
  if (hit && now - hit.fetchedAt < CAPS_TTL_MS) return hit.result;
  let raw;
  try {
    raw = await graphqlPost(buildOapOpts(config, fetchImpl), INTROSPECTION_QUERY);
  } catch {
    const conservative = { queryAlarms: false };
    cache.set(key, { result: conservative, fetchedAt: now - CAPS_TTL_MS + CAPS_FAILURE_TTL_MS });
    return conservative;
  }
  const fieldSet = /* @__PURE__ */ new Set();
  for (const f of raw.__type?.fields ?? []) {
    if (f?.name) fieldSet.add(f.name);
  }
  const result = {
    queryAlarms: fieldSet.has("queryAlarms")
  };
  cache.set(key, { result, fetchedAt: now });
  return result;
}

// src/http/query/info.ts
var INFO_QUERY = (
  /* GraphQL */
  `
  query HorizonOapInfo {
    version
    time: getTimeInfo {
      timezone
      currentTimestamp
    }
    health: checkHealth {
      score
      details
    }
  }
`
);
async function probeZipkin(cfg, fetchFn) {
  const f = fetchFn ?? globalThis.fetch.bind(globalThis);
  const url = cfg.oap.zipkinUrl.replace(/\/$/, "") + "/api/v2/services";
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), cfg.oap.timeoutMs);
  const headers = { accept: "application/json" };
  if (cfg.oap.auth) {
    headers.authorization = basicAuthHeader(cfg.oap.auth.username, cfg.oap.auth.password);
  }
  try {
    const res = await f(url, { method: "GET", headers, signal: controller.signal });
    if (!res.ok) return { reachable: false, error: `HTTP ${res.status} at ${url}` };
    return { reachable: true };
  } catch (err) {
    return { reachable: false, error: err instanceof Error ? err.message : String(err) };
  } finally {
    clearTimeout(timer);
  }
}
function registerOapInfoRoute(app2, deps) {
  const auth = requireAuth(deps);
  app2.get("/api/oap/info", { preHandler: auth }, async (_req, reply) => {
    const cfg = deps.config.current;
    const queryUrl = cfg.oap.queryUrl;
    const zipkinUrl = cfg.oap.zipkinUrl;
    const zipkinP = probeZipkin(cfg, deps.fetch);
    try {
      const [raw, capabilities, zipkin] = await Promise.all([
        graphqlPost(buildOapOpts(cfg, deps.fetch), INFO_QUERY),
        getOapCapabilities(cfg, deps.fetch),
        zipkinP
      ]);
      const body = {
        reachable: true,
        queryUrl,
        version: raw.version ?? void 0,
        timezone: raw.time?.timezone ?? void 0,
        currentTimestamp: raw.time?.currentTimestamp ?? void 0,
        healthScore: raw.health?.score ?? void 0,
        healthDetails: raw.health?.details ?? void 0,
        capabilities,
        zipkinUrl,
        zipkinReachable: zipkin.reachable,
        zipkinError: zipkin.error
      };
      return reply.send(body);
    } catch (err) {
      const zipkin = await zipkinP;
      const body = {
        reachable: false,
        queryUrl,
        error: err instanceof Error ? err.message : String(err),
        zipkinUrl,
        zipkinReachable: zipkin.reachable,
        zipkinError: zipkin.error
      };
      return reply.status(200).send(body);
    }
  });
}

// src/logic/layers/loader.ts
import { readdirSync, readFileSync as readFileSync2, watch as fsWatch, writeFileSync } from "node:fs";
import { dirname as dirname2, join, basename } from "node:path";
import { fileURLToPath } from "node:url";
var BOOSTER_TOPOLOGY_DEFAULTS = {
  nodeMetrics: [
    { id: "cpm", label: "RPM", mqe: "service_cpm", unit: "rpm", role: "center", aggregation: "avg" },
    { id: "sla", label: "SLA", mqe: "service_sla/100", unit: "%", role: "ring", aggregation: "avg" },
    { id: "respTime", label: "Latency", mqe: "service_resp_time", unit: "ms", role: "secondary", aggregation: "avg" }
  ],
  // Per-edge line metrics — mirror booster-ui's per-side family:
  // RPM, avg response time, p95 percentile, and SLA%. The ids match
  // across server and client so the right-sidebar pairing renders
  // both sides aligned per row.
  linkServerMetrics: [
    { id: "cpm", label: "RPM", mqe: "service_relation_server_cpm", unit: "rpm", role: "lineServer", aggregation: "avg" },
    { id: "respTime", label: "Avg response time", mqe: "service_relation_server_resp_time", unit: "ms", aggregation: "avg" },
    { id: "p95", label: "p95", mqe: "service_relation_server_percentile{p='95'}", unit: "ms", aggregation: "avg" },
    { id: "sla", label: "SLA", mqe: "service_relation_server_call_sla/100", unit: "%", aggregation: "avg" }
  ],
  linkClientMetrics: [
    { id: "cpm", label: "RPM", mqe: "service_relation_client_cpm", unit: "rpm", role: "lineClient", aggregation: "avg" },
    { id: "respTime", label: "Avg response time", mqe: "service_relation_client_resp_time", unit: "ms", aggregation: "avg" },
    { id: "p95", label: "p95", mqe: "service_relation_client_percentile{p='95'}", unit: "ms", aggregation: "avg" },
    { id: "sla", label: "SLA", mqe: "service_relation_client_call_sla/100", unit: "%", aggregation: "avg" }
  ]
};
var BOOSTER_ENDPOINT_DEP_DEFAULTS = {
  nodeMetrics: [
    { id: "cpm", label: "RPM", mqe: "endpoint_cpm", unit: "rpm", role: "center", aggregation: "avg" },
    { id: "sla", label: "SLA", mqe: "endpoint_sla/100", unit: "%", role: "ring", aggregation: "avg" },
    { id: "respTime", label: "Latency", mqe: "endpoint_resp_time", unit: "ms", role: "secondary", aggregation: "avg" }
  ],
  // Server-side only (OAP has no `endpoint_relation_client_*` family).
  // Same four-metric set as the service map for visual consistency.
  linkMetrics: [
    { id: "cpm", label: "RPM", mqe: "endpoint_relation_cpm", unit: "rpm", role: "lineServer", aggregation: "avg" },
    { id: "respTime", label: "Avg response time", mqe: "endpoint_relation_resp_time", unit: "ms", aggregation: "avg" },
    { id: "p95", label: "p95", mqe: "endpoint_relation_percentile{p='95'}", unit: "ms", aggregation: "avg" },
    { id: "sla", label: "SLA", mqe: "endpoint_relation_sla/100", unit: "%", aggregation: "avg" }
  ]
};
var BOOSTER_PROCESS_TOPOLOGY_DEFAULTS = {
  // Default ProcessRelation TCP metric set — fully operator-editable via
  // the admin network-profiling config. ids pair across client/server so
  // the edge dashboard renders matching rows side-by-side; bytes are
  // /1024 → KB and exe times /1000000 → ms for readable units.
  edgeClientMetrics: [
    { id: "write_cpm", label: "Write OP / min", mqe: "process_relation_client_write_cpm", aggregation: "avg" },
    { id: "read_cpm", label: "Read OP / min", mqe: "process_relation_client_read_cpm", aggregation: "avg" },
    { id: "write_kb", label: "Write package size", mqe: "process_relation_client_write_total_bytes/1024", unit: "KB", aggregation: "avg" },
    { id: "read_kb", label: "Read package size", mqe: "process_relation_client_read_total_bytes/1024", unit: "KB", aggregation: "avg" },
    { id: "write_avg_ms", label: "Write OP avg time", mqe: "process_relation_client_write_avg_exe_time/1000000", unit: "ms", aggregation: "avg" },
    { id: "read_avg_ms", label: "Read OP avg time", mqe: "process_relation_client_read_avg_exe_time/1000000", unit: "ms", aggregation: "avg" },
    { id: "connect_cpm", label: "Connect OP / min", mqe: "process_relation_client_connect_cpm", aggregation: "avg" },
    { id: "close_cpm", label: "Close OP / min", mqe: "process_relation_client_close_cpm", aggregation: "avg" }
  ],
  edgeServerMetrics: [
    { id: "write_cpm", label: "Write OP / min", mqe: "process_relation_server_write_cpm", aggregation: "avg" },
    { id: "read_cpm", label: "Read OP / min", mqe: "process_relation_server_read_cpm", aggregation: "avg" },
    { id: "write_kb", label: "Write package size", mqe: "process_relation_server_write_total_bytes/1024", unit: "KB", aggregation: "avg" },
    { id: "read_kb", label: "Read package size", mqe: "process_relation_server_read_total_bytes/1024", unit: "KB", aggregation: "avg" },
    { id: "write_avg_ms", label: "Write OP avg time", mqe: "process_relation_server_write_avg_exe_time/1000000", unit: "ms", aggregation: "avg" },
    { id: "read_avg_ms", label: "Read OP avg time", mqe: "process_relation_server_read_avg_exe_time/1000000", unit: "ms", aggregation: "avg" },
    { id: "connect_cpm", label: "Connect OP / min", mqe: "process_relation_server_connect_cpm", aggregation: "avg" },
    { id: "close_cpm", label: "Close OP / min", mqe: "process_relation_server_close_cpm", aggregation: "avg" }
  ]
};
var __dirname = dirname2(fileURLToPath(import.meta.url));
function locateConfigDir() {
  const candidates = [
    // Self-contained flat dist (`pnpm package` output: server.js +
    // bundled_templates as siblings inside ./dist/).
    join(__dirname, "bundled_templates", "layers"),
    // Dev (tsx watch: apps/bff/src/logic/layers/loader.ts).
    join(__dirname, "..", "..", "bundled_templates", "layers"),
    // Docker image (/app/dist/server.js → /app/bundled_templates).
    join(__dirname, "..", "bundled_templates", "layers"),
    join(process.cwd(), "bundled_templates", "layers")
  ];
  for (const dir of candidates) {
    try {
      readdirSync(dir);
      return dir;
    } catch {
    }
  }
  return candidates[0];
}
var CONFIG_DIR = locateConfigDir();
var cache2 = null;
function load() {
  const out = /* @__PURE__ */ new Map();
  for (const file of readdirSync(CONFIG_DIR)) {
    if (!file.endsWith(".json")) continue;
    const raw = readFileSync2(join(CONFIG_DIR, file), "utf-8");
    let parsed;
    try {
      parsed = JSON.parse(raw);
    } catch (err) {
      throw new Error(`failed to parse layer config ${file}: ${err instanceof Error ? err.message : err}`);
    }
    const expected = basename(file, ".json").toUpperCase();
    if (parsed.key && parsed.key.toUpperCase() !== expected) {
      throw new Error(
        `layer config ${file}: file basename does not match \`key\` (${parsed.key})`
      );
    }
    const aliases = parsed.aliases;
    if (!parsed.slots && aliases) {
      parsed.slots = aliases;
    }
    if (parsed.widgets && (!parsed.dashboards || !parsed.dashboards.service)) {
      parsed.dashboards = { ...parsed.dashboards, service: parsed.widgets };
    }
    const legacyComponents = parsed.components;
    if (legacyComponents && legacyComponents.profiling !== void 0) {
      if (legacyComponents.traceProfiling === void 0) {
        legacyComponents.traceProfiling = legacyComponents.profiling;
      }
      delete legacyComponents.profiling;
    }
    const legacyDashboards = parsed.dashboards;
    if (legacyDashboards && legacyDashboards.profiling) {
      if (!legacyDashboards.traceProfiling) {
        legacyDashboards.traceProfiling = legacyDashboards.profiling;
      }
      delete legacyDashboards.profiling;
    }
    const headerSrc = parsed["layer-header"];
    if (headerSrc && !parsed.header) parsed.header = headerSrc;
    if (!parsed.header && parsed.metrics) parsed.header = parsed.metrics;
    if (!parsed.header) parsed.header = { columns: [] };
    parsed.metrics = parsed.header;
    const legacyMetrics = parsed.metrics;
    if (legacyMetrics && (legacyMetrics.throughput || legacyMetrics.spark)) {
      const ov = { ...parsed.overview ?? {} };
      if (!ov.throughput && legacyMetrics.throughput) ov.throughput = legacyMetrics.throughput;
      if (!ov.spark && legacyMetrics.spark) ov.spark = legacyMetrics.spark;
      parsed.overview = ov;
      delete legacyMetrics.throughput;
      delete legacyMetrics.spark;
    }
    if (parsed.overview) {
      const ov = parsed.overview;
      const columns = parsed.header?.columns ?? [];
      const findCol = (key) => columns.find((c) => c.metric === key);
      const fromRef = (key, fallbackLabel) => {
        const col = findCol(key);
        return {
          id: key,
          label: col?.label ?? fallbackLabel ?? key,
          mqe: col?.mqe ?? key,
          ...col?.unit ? { unit: col.unit } : {},
          ...col?.aggregation ? { aggregation: col.aggregation } : {},
          ...col?.scale !== void 0 ? { scale: col.scale } : {},
          ...col?.precision !== void 0 ? { precision: col.precision } : {}
        };
      };
      let resolved = [];
      if (Array.isArray(ov.metrics)) {
        for (const m of ov.metrics) {
          if (typeof m === "string") {
            resolved.push(fromRef(m));
          } else if (m && typeof m === "object" && "mqe" in m) {
            resolved.push(m);
          } else if (m && typeof m === "object" && "label" in m) {
            resolved.push(fromRef(m.label, m.label));
          }
        }
      }
      if (resolved.length === 0) {
        if (ov.throughput) resolved.push(fromRef(ov.throughput));
        if (ov.spark && ov.spark !== ov.throughput) resolved.push(fromRef(ov.spark));
      }
      resolved = resolved.map((m, i) => ({ id: m.id ?? `ov_${i}`, ...m }));
      const ovGroups = ov.groups;
      if (!ovGroups || ovGroups.length === 0) {
        ov.groups = resolved.length > 0 ? [{ title: "", size: "auto", metrics: resolved }] : [];
      } else {
        let counter = 0;
        ov.groups = ovGroups.map((g) => ({
          title: g.title ?? "",
          size: g.size === "square" ? "square" : "auto",
          metrics: (g.metrics ?? []).map((m) => ({
            id: m.id ?? `ov_${counter++}`,
            ...m
          }))
        }));
      }
      ov.metrics = (ov.groups ?? []).flatMap((g) => g.metrics);
      delete ov.throughput;
      delete ov.spark;
    }
    out.set(parsed.key.toUpperCase(), parsed);
  }
  return out;
}
function widgetsForScope(template, scope) {
  const d = template.dashboards;
  if (!d) return template.widgets ?? [];
  return d[scope] ?? d.service ?? template.widgets ?? [];
}
function topologyConfigFor(template) {
  if (template?.topology) return template.topology;
  return BOOSTER_TOPOLOGY_DEFAULTS;
}
function endpointDependencyConfigFor(template) {
  if (template?.endpointDependency) return template.endpointDependency;
  return BOOSTER_ENDPOINT_DEP_DEFAULTS;
}
function processTopologyConfigFor(template) {
  if (template?.processTopology) return template.processTopology;
  return BOOSTER_PROCESS_TOPOLOGY_DEFAULTS;
}
function tracesConfigFor(template) {
  if (template?.traces) return template.traces;
  return { source: "both" };
}
function writeLayerTemplate(template) {
  if (!template.key || !/^[A-Z][A-Z0-9_]*$/.test(template.key)) {
    throw new Error("invalid template key (must be UPPER_SNAKE_CASE)");
  }
  const file = join(CONFIG_DIR, `${template.key.toLowerCase()}.json`);
  const serialised = JSON.stringify(template, null, 2) + "\n";
  writeFileSync(file, serialised, "utf-8");
  reloadLayerTemplates();
}
function getLayerTemplate(layerKey) {
  if (!cache2) cache2 = load();
  return cache2.get(layerKey.toUpperCase()) ?? null;
}
function allLayerTemplates() {
  if (!cache2) cache2 = load();
  return Array.from(cache2.values());
}
function reloadLayerTemplates() {
  cache2 = null;
}
var watchTimer = null;
var watcher = null;
function startLayerTemplateWatcher() {
  if (watcher) return;
  try {
    watcher = fsWatch(CONFIG_DIR, (_event, filename) => {
      if (!filename || !filename.endsWith(".json")) return;
      if (watchTimer) clearTimeout(watchTimer);
      watchTimer = setTimeout(() => {
        cache2 = null;
        watchTimer = null;
      }, 50);
    });
  } catch {
  }
}

// src/http/query/menu.ts
function componentsToCaps(components) {
  return {
    dashboards: components.service !== false,
    instances: !!components.instances,
    endpoints: !!components.endpoints,
    endpointDependency: !!components.endpointDependency,
    serviceMap: !!components.topology,
    instanceTopology: !!components.topology,
    processTopology: !!components.topology,
    traces: !!components.traces,
    logs: !!components.logs,
    traceProfiling: !!components.traceProfiling,
    ebpfProfiling: !!components.ebpfProfiling,
    asyncProfiling: !!components.asyncProfiling,
    networkProfiling: !!components.networkProfiling,
    pprofProfiling: !!components.pprofProfiling,
    events: false,
    // Bundled service-count tile defaults on — every layer benefits
    // from the headline count, and operators can opt out per-layer
    // from the setup card's Features section.
    serviceCountTile: true
  };
}
var MENU_QUERY = (
  /* GraphQL */
  `
  query HorizonMenu {
    layers: listLayers
    levels: listLayerLevels {
      layer
      level
    }
  }
`
);
var LAYER_ALIAS = {
  CACHE: "VIRTUAL_CACHE",
  DATABASE: "VIRTUAL_DATABASE",
  MQ: "VIRTUAL_MQ",
  GENAI: "VIRTUAL_GENAI"
};
function canonical(layer) {
  return LAYER_ALIAS[layer] ?? layer;
}
var LAYER_DEFAULTS = {
  GENERAL: {
    color: "var(--sw-accent)",
    slots: { services: "Services", instances: "Instances", endpoints: "API", endpointDependency: "API dependency" },
    caps: {
      serviceMap: true,
      endpointDependency: true,
      instanceTopology: true,
      processTopology: true,
      dashboards: true,
      traces: true,
      logs: true,
      traceProfiling: true,
      ebpfProfiling: true,
      asyncProfiling: true,
      events: true
    }
  },
  MESH: {
    color: "var(--sw-info)",
    slots: { services: "Services", instances: "Sidecars", endpoints: "Endpoints" },
    caps: {
      serviceMap: true,
      endpointDependency: true,
      instanceTopology: true,
      dashboards: true,
      traces: true,
      logs: true,
      events: true
    }
  },
  MESH_CP: { color: "var(--sw-info)", slots: { services: "Control-plane services" }, caps: { dashboards: true } },
  MESH_DP: { color: "var(--sw-info)", slots: { services: "Data-plane services", instances: "Sidecars" }, caps: { dashboards: true, instanceTopology: true } },
  K8S: { color: "var(--sw-purple)", slots: { services: "Workloads", instances: "Pods" }, caps: { serviceMap: true, instanceTopology: true, dashboards: true, events: true } },
  K8S_SERVICE: { color: "var(--sw-purple)", slots: { services: "K8s services", instances: "Pods" }, caps: { serviceMap: true, instanceTopology: true, dashboards: true } },
  BROWSER: { color: "var(--sw-cyan)", slots: { services: "Applications", instances: "Versions", endpoints: "Pages" }, caps: { dashboards: true, traces: true, logs: true } },
  MYSQL: { color: "var(--sw-warn)", slots: { services: "Instances" }, caps: { dashboards: true } },
  POSTGRESQL: { color: "var(--sw-warn)", slots: { services: "Instances" }, caps: { dashboards: true } },
  ELASTICSEARCH: { color: "var(--sw-warn)", slots: { services: "Clusters", instances: "Nodes" }, caps: { dashboards: true } },
  REDIS: { color: "var(--sw-warn)", slots: { services: "Instances" }, caps: { dashboards: true } },
  MONGODB: { color: "var(--sw-warn)", slots: { services: "Clusters", instances: "Nodes" }, caps: { dashboards: true } },
  CLICKHOUSE: { color: "var(--sw-warn)", slots: { services: "Services", instances: "Instances" }, caps: { dashboards: true } },
  KAFKA: { color: "var(--sw-ok)", slots: { services: "Clusters", instances: "Brokers" }, caps: { dashboards: true } },
  PULSAR: { color: "var(--sw-ok)", slots: { services: "Clusters", instances: "Brokers" }, caps: { dashboards: true } },
  ROCKETMQ: { color: "var(--sw-ok)", slots: { services: "Clusters", instances: "Brokers", endpoints: "Topics" }, caps: { dashboards: true } },
  RABBITMQ: { color: "var(--sw-ok)", slots: { services: "Clusters", instances: "Nodes" }, caps: { dashboards: true } },
  ACTIVEMQ: { color: "var(--sw-ok)", slots: { services: "Clusters", instances: "Brokers", endpoints: "Destinations" }, caps: { dashboards: true } },
  VIRTUAL_DATABASE: { color: "var(--sw-warn)", slots: { services: "Databases" }, caps: { dashboards: true } },
  VIRTUAL_CACHE: { color: "var(--sw-warn)", slots: { services: "Caches" }, caps: { dashboards: true } },
  VIRTUAL_MQ: { color: "var(--sw-ok)", slots: { services: "Queues" }, caps: { dashboards: true } },
  VIRTUAL_GENAI: { color: "var(--sw-purple)", slots: { services: "Providers", instances: "Models" }, caps: { dashboards: true } }
};
var DEFAULT_FOR_UNKNOWN_LAYER = {
  color: "var(--sw-fg-2)",
  slots: { services: "Services" },
  caps: { dashboards: true }
};
function deriveLayer(rawKey, active, level, serviceCount, normal) {
  const tpl = getLayerTemplate(rawKey);
  if (tpl) {
    return {
      key: rawKey.toLowerCase(),
      name: tpl.alias || rawKey,
      color: tpl.color || "var(--sw-fg-2)",
      serviceCount,
      active,
      level,
      normal,
      group: tpl.group,
      visibility: tpl.visibility,
      documentLink: tpl.documentLink ?? void 0,
      slots: tpl.slots,
      caps: componentsToCaps(tpl.components),
      header: tpl.header,
      metrics: tpl.metrics,
      overview: tpl.overview,
      log: tpl.log,
      traces: tpl.traces,
      naming: tpl.naming
    };
  }
  const def = LAYER_DEFAULTS[rawKey] ?? DEFAULT_FOR_UNKNOWN_LAYER;
  return {
    key: rawKey.toLowerCase(),
    name: rawKey.replace(/_/g, " ").toLowerCase().replace(/\b\w/g, (c) => c.toUpperCase()),
    color: def.color,
    serviceCount,
    active,
    level,
    normal,
    slots: def.slots,
    caps: def.caps
  };
}
async function fetchCountsForLayers(layers, opts) {
  const map = /* @__PURE__ */ new Map();
  if (layers.length === 0) return map;
  const aliased = layers.map((l, i) => `_${i}: listServices(layer: ${JSON.stringify(l)}) { id normal }`).join("\n");
  const query = `query HorizonCounts { ${aliased} }`;
  try {
    const data = await graphqlPost(opts, query);
    layers.forEach((l, i) => {
      const rows = data[`_${i}`] ?? [];
      const first = rows[0];
      const normal = first ? first.normal === false ? false : first.normal === true ? true : null : null;
      map.set(l, { count: rows.length, normal });
    });
  } catch {
  }
  return map;
}
function registerMenuRoute(app2, deps) {
  const auth = requireAuth(deps);
  app2.get("/api/menu", { preHandler: auth }, async (_req, reply) => {
    const cfg = deps.config.current;
    const queryUrl = cfg.oap.queryUrl;
    const opts = buildOapOpts(cfg, deps.fetch);
    try {
      const raw = await graphqlPost(opts, MENU_QUERY);
      const activeCanonical = new Set(raw.layers.map(canonical));
      const levelByCanonical = new Map(raw.levels.map((l) => [canonical(l.layer), l.level]));
      const counts = await fetchCountsForLayers(raw.layers, opts);
      const countByCanonical = /* @__PURE__ */ new Map();
      const normalByCanonical = /* @__PURE__ */ new Map();
      for (const rawLayer of raw.layers) {
        const key = canonical(rawLayer);
        const c = counts.get(rawLayer);
        countByCanonical.set(key, (countByCanonical.get(key) ?? 0) + (c?.count ?? 0));
        if (c?.normal !== void 0 && c.normal !== null && !normalByCanonical.has(key)) {
          normalByCanonical.set(key, c.normal);
        }
      }
      const ordered = [];
      const seen = /* @__PURE__ */ new Set();
      for (const tpl of allLayerTemplates()) {
        const k = canonical(tpl.key.toUpperCase());
        if (seen.has(k)) continue;
        seen.add(k);
        ordered.push(k);
      }
      for (const rawLayer of raw.layers) {
        const k = canonical(rawLayer);
        if (seen.has(k)) continue;
        seen.add(k);
        ordered.push(k);
      }
      const HIDDEN_LAYERS = /* @__PURE__ */ new Set(["BANYANDB"]);
      const layers = ordered.filter((key) => !HIDDEN_LAYERS.has(key)).map(
        (key) => deriveLayer(
          key,
          activeCanonical.has(key),
          levelByCanonical.has(key) ? levelByCanonical.get(key) ?? null : null,
          countByCanonical.get(key) ?? (activeCanonical.has(key) ? 0 : -1),
          normalByCanonical.get(key) ?? null
        )
      );
      const body = {
        layers,
        generatedAt: Date.now(),
        oap: { reachable: true, queryUrl }
      };
      return reply.send(body);
    } catch (err) {
      const HIDDEN_LAYERS = /* @__PURE__ */ new Set(["BANYANDB"]);
      const seen = /* @__PURE__ */ new Set();
      const layers = [];
      for (const tpl of allLayerTemplates()) {
        const key = canonical(tpl.key.toUpperCase());
        if (seen.has(key) || HIDDEN_LAYERS.has(key)) continue;
        seen.add(key);
        layers.push(deriveLayer(key, false, null, -1, null));
      }
      const body = {
        layers,
        generatedAt: Date.now(),
        oap: {
          reachable: false,
          queryUrl,
          error: err instanceof Error ? err.message : String(err)
        }
      };
      return reply.status(200).send(body);
    }
  });
}

// src/http/query/landing.ts
import { z as z3 } from "zod";

// src/util/mqe-catalog.ts
function layerCategory(layerKey) {
  const k = layerKey.toLowerCase();
  if (k === "general") return "general";
  if (k === "mesh" || k === "mesh_cp" || k === "mesh_dp") return "mesh";
  if (k === "k8s" || k === "k8s_service") return "k8s";
  if (k === "browser") return "browser";
  if (k === "virtual_genai") return "genai";
  if (k === "mysql" || k === "postgresql" || k === "mongodb" || k === "elasticsearch" || k === "redis" || k === "clickhouse" || k === "virtual_database" || k === "virtual_cache") return "database";
  if (k === "kafka" || k === "pulsar" || k === "rocketmq" || k === "rabbitmq" || k === "activemq" || k === "virtual_mq") return "mq";
  return "general";
}
var RPC_SERVICE = {
  cpm: "avg(service_cpm)",
  resp: "avg(service_resp_time)",
  p50: "service_percentile{p='50'}",
  p75: "service_percentile{p='75'}",
  p95: "service_percentile{p='95'}",
  p99: "service_percentile{p='99'}",
  sla: "avg(service_sla)/100",
  apdex: "avg(service_apdex)/10000",
  err: "100 - avg(service_sla)/100"
};
var BROWSER_SERVICE = {
  "browser.pv": "avg(browser_app_pv)",
  "browser.js-err": "avg(browser_app_error_sum)",
  err: "avg(browser_app_error_rate)/100"
  // Page-level metrics aren't service-scope — they bind to endpoint
  // (page) entities. Operators viewing the service card see app-level
  // aggregates; deep-dives happen on the per-layer page.
};
var DATABASE_SERVICE = {
  cpm: "avg(service_cpm)",
  resp: "avg(service_resp_time)",
  p99: "service_percentile{p='99'}",
  err: "100 - avg(service_sla)/100"
};
var MQ_SERVICE = {
  cpm: "avg(service_cpm)",
  resp: "avg(service_resp_time)",
  err: "100 - avg(service_sla)/100"
};
var TABLES = {
  general: RPC_SERVICE,
  mesh: RPC_SERVICE,
  k8s: RPC_SERVICE,
  browser: BROWSER_SERVICE,
  database: DATABASE_SERVICE,
  mq: MQ_SERVICE
  // genai / faas / others — no first-class mapping yet, fall through to
  // RPC_SERVICE so cpm / resp / sla still render.
};
function expressionForServiceMetric(metricKey, layerKey) {
  const cat = layerCategory(layerKey);
  const table = TABLES[cat] ?? RPC_SERVICE;
  return table[metricKey] ?? RPC_SERVICE[metricKey] ?? null;
}
function expressionForServiceMetricSeries(metricKey, layerKey) {
  const scalar = expressionForServiceMetric(metricKey, layerKey);
  if (!scalar) return null;
  return scalar.replace(/avg\(([^()]+)\)/g, "$1");
}

// src/http/query/landing.ts
var LIST_SERVICES_QUERY = (
  /* GraphQL */
  `
  query LandingServices($layer: String!) {
    services: listServices(layer: $layer) {
      id
      value: name
      shortName
      group
      normal
    }
  }
`
);
var DEFAULT_WINDOW_MIN = 60;
var SERVICE_QUERY_CAP = 25;
var aggSchema = z3.enum(["sum", "avg"]);
var columnSchema = z3.object({
  metric: z3.string().min(1),
  label: z3.string().min(1),
  tip: z3.string().optional(),
  unit: z3.string().optional(),
  mqe: z3.string().optional(),
  aggregation: aggSchema.optional(),
  scale: z3.number().finite().optional(),
  precision: z3.number().int().min(0).max(6).optional()
});
var throughputSchema = z3.object({
  metric: z3.string().min(1),
  label: z3.string().optional(),
  unit: z3.string().optional(),
  mqe: z3.string().optional(),
  aggregation: aggSchema.optional(),
  scale: z3.number().finite().optional(),
  precision: z3.number().int().min(0).max(6).optional()
});
var bodySchema = z3.object({
  topN: z3.number().int().min(1).max(8),
  orderBy: z3.string().min(1),
  // Bumped from 5 to 10: Overview tile metrics are now self-contained
  // and threaded as synthetic columns in the same query as the
  // per-layer header columns. Up to 3 + 5 = 8 in the worst case; 10
  // gives headroom without making the BFF wide-open.
  columns: z3.array(columnSchema).max(10),
  spark: z3.object({ metric: z3.string().min(1), height: z3.number().int().positive() }).optional(),
  throughput: throughputSchema.optional()
});
function fmtMinute(d) {
  const yyyy = d.getUTCFullYear();
  const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
  const dd = String(d.getUTCDate()).padStart(2, "0");
  const hh = String(d.getUTCHours()).padStart(2, "0");
  const mi = String(d.getUTCMinutes()).padStart(2, "0");
  return `${yyyy}-${mm}-${dd} ${hh}${mi}`;
}
function defaultWindow() {
  const end = /* @__PURE__ */ new Date();
  end.setUTCSeconds(0, 0);
  const start = new Date(end.getTime() - DEFAULT_WINDOW_MIN * 6e4);
  return { start: fmtMinute(start), end: fmtMinute(end), step: "MINUTE" };
}
function resolveMqe(metric, mqe, layerKey) {
  if (mqe && mqe.trim().length > 0) return mqe.trim();
  return expressionForServiceMetricSeries(metric, layerKey);
}
function postProcess(v, scale, precision) {
  if (v === null || !Number.isFinite(v)) return null;
  let out = scale ? v * scale : v;
  if (precision !== void 0) {
    const factor = Math.pow(10, precision);
    out = Math.round(out * factor) / factor;
  }
  return out;
}
function collapseToSeries(r) {
  if (!r || r.error) return null;
  const values = r.results?.[0]?.values ?? [];
  if (values.length === 0) return null;
  return values.map((v) => {
    if (v.value === null || v.value === void 0) return null;
    const n = Number(v.value);
    return Number.isFinite(n) ? n : null;
  });
}
function collapseToScalar(r) {
  const series = collapseToSeries(r);
  if (!series) return null;
  const ns = series.filter((x) => x !== null);
  if (ns.length === 0) return null;
  return ns.reduce((a, b) => a + b, 0) / ns.length;
}
function aggregate(values, kind) {
  const finite = values.filter((v) => v !== null && Number.isFinite(v));
  if (finite.length === 0) return null;
  const sum = finite.reduce((a, b) => a + b, 0);
  return kind === "avg" ? sum / finite.length : sum;
}
function aggregateSeries(serieses, kind) {
  const real = serieses.filter((s) => Array.isArray(s) && s.length > 0);
  if (real.length === 0) return null;
  const len = Math.max(...real.map((s) => s.length));
  const out = [];
  for (let i = 0; i < len; i++) {
    const pts = real.map((s) => s[i]).filter((v) => v !== null && v !== void 0 && Number.isFinite(v));
    if (pts.length === 0) {
      out.push(null);
    } else {
      const sum = pts.reduce((a, b) => a + b, 0);
      out.push(kind === "avg" ? sum / pts.length : sum);
    }
  }
  return out;
}
function alias(serviceIdx, columnIdx) {
  return `r${serviceIdx}_c${columnIdx}`;
}
function buildMqeFragment(aliasName, req, w) {
  return `${aliasName}: execExpression(
      expression: ${JSON.stringify(req.expression)},
      entity: { scope: Service, serviceName: ${JSON.stringify(req.serviceName)}, normal: ${req.normal ? "true" : "false"} },
      duration: { start: ${JSON.stringify(w.start)}, end: ${JSON.stringify(w.end)}, step: MINUTE }
    ) { type error results { values { value } } }`;
}
function registerLandingRoute(app2, deps) {
  const auth = requireAuth(deps);
  app2.post(
    "/api/layer/:key/landing",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      const layerKey = params.key;
      if (!layerKey || !/^[a-z0-9_]+$/i.test(layerKey)) {
        return reply.code(400).send({ error: "invalid_layer_key" });
      }
      const parsed = bodySchema.safeParse(req.body);
      if (!parsed.success) {
        return reply.code(400).send({ error: "invalid_body", detail: parsed.error.flatten() });
      }
      const cfg = parsed.data;
      const oapLayer = layerKey.toUpperCase();
      const cfgCurrent = deps.config.current;
      const opts = buildOapOpts(cfgCurrent, deps.fetch);
      const window = defaultWindow();
      let services;
      try {
        const data = await graphqlPost(
          opts,
          LIST_SERVICES_QUERY,
          { layer: oapLayer }
        );
        services = data.services ?? [];
      } catch (err) {
        const body2 = {
          layer: layerKey,
          topN: cfg.topN,
          orderBy: cfg.orderBy,
          generatedAt: Date.now(),
          step: "MINUTE",
          durationStart: window.start,
          durationEnd: window.end,
          rows: [],
          aggregates: { serviceCount: 0, metrics: {}, seriesByMetric: {} },
          reachable: false,
          error: err instanceof Error ? err.message : String(err)
        };
        return reply.send(body2);
      }
      const totalServiceCount = services.length;
      if (services.length === 0) {
        const body2 = {
          layer: layerKey,
          topN: cfg.topN,
          orderBy: cfg.orderBy,
          generatedAt: Date.now(),
          step: "MINUTE",
          durationStart: window.start,
          durationEnd: window.end,
          rows: [],
          aggregates: { serviceCount: 0, metrics: {}, seriesByMetric: {} },
          reachable: true
        };
        return reply.send(body2);
      }
      const sampled = services.slice(0, SERVICE_QUERY_CAP);
      const resolved = cfg.columns.map((c) => ({
        column: c,
        expression: resolveMqe(c.metric, c.mqe, layerKey)
      }));
      const fragments = [];
      sampled.forEach((svc, sIdx) => {
        resolved.forEach(({ expression }, cIdx) => {
          if (!expression) return;
          fragments.push(
            buildMqeFragment(
              alias(sIdx, cIdx),
              { expression, serviceName: svc.value, normal: svc.normal !== false },
              window
            )
          );
        });
      });
      let mqeData = {};
      if (fragments.length > 0) {
        const batchQuery = `query LandingMqe { ${fragments.join("\n    ")} }`;
        try {
          mqeData = await graphqlPost(opts, batchQuery);
        } catch {
          mqeData = {};
        }
      }
      const seriesByServiceMetric = /* @__PURE__ */ new Map();
      const rows = sampled.map((svc, sIdx) => {
        const metrics = {};
        const seriesMap = /* @__PURE__ */ new Map();
        resolved.forEach(({ column }, cIdx) => {
          const raw = mqeData[alias(sIdx, cIdx)];
          const series = collapseToSeries(raw);
          if (series) {
            seriesMap.set(
              column.metric,
              series.map((v) => postProcess(v, column.scale, column.precision))
            );
          }
          metrics[column.metric] = postProcess(
            collapseToScalar(raw),
            column.scale,
            column.precision
          );
        });
        seriesByServiceMetric.set(svc.id, seriesMap);
        return {
          serviceId: svc.id,
          serviceName: svc.value,
          ...svc.shortName ? { shortName: svc.shortName } : {},
          ...svc.group ? { group: svc.group } : {},
          metrics
        };
      });
      rows.sort((a, b) => {
        const av = a.metrics[cfg.orderBy];
        const bv = b.metrics[cfg.orderBy];
        if (av == null && bv == null) return a.serviceName.localeCompare(b.serviceName);
        if (av == null) return 1;
        if (bv == null) return -1;
        return bv - av;
      });
      const topRows = rows.slice(0, cfg.topN);
      const sparkExpr = cfg.spark ? resolveMqe(cfg.spark.metric, void 0, layerKey) : null;
      const throughputCol = cfg.throughput;
      const throughputExpr = throughputCol ? resolveMqe(throughputCol.metric, throughputCol.mqe, layerKey) : null;
      const sparkSeriesByRow = /* @__PURE__ */ new Map();
      const throughputSeriesByRow = /* @__PURE__ */ new Map();
      if ((sparkExpr || throughputExpr) && topRows.length > 0) {
        const sparkFragments = [];
        topRows.forEach((row, i) => {
          const svc = sampled.find((s) => s.id === row.serviceId);
          if (!svc) return;
          const r = { expression: "", serviceName: svc.value, normal: svc.normal !== false };
          if (sparkExpr) {
            sparkFragments.push(buildMqeFragment(`s${i}`, { ...r, expression: sparkExpr }, window));
          }
          if (throughputExpr && throughputExpr !== sparkExpr) {
            sparkFragments.push(buildMqeFragment(`t${i}`, { ...r, expression: throughputExpr }, window));
          }
        });
        if (sparkFragments.length > 0) {
          const sparkQuery = `query LandingSpark { ${sparkFragments.join("\n    ")} }`;
          try {
            const sparkData = await graphqlPost(opts, sparkQuery);
            topRows.forEach((row, i) => {
              const sk = sparkExpr ? collapseToSeries(sparkData[`s${i}`]) : null;
              if (sk && cfg.spark) {
                const matchedCol = cfg.columns.find((c) => c.metric === cfg.spark.metric);
                const scaled = sk.map(
                  (v) => postProcess(v, matchedCol?.scale, matchedCol?.precision)
                );
                row.spark = scaled;
                sparkSeriesByRow.set(i, scaled);
              }
              if (throughputExpr) {
                const series = throughputExpr === sparkExpr ? sk : collapseToSeries(sparkData[`t${i}`]);
                if (series) {
                  const scaled = series.map(
                    (v) => postProcess(v, throughputCol?.scale, throughputCol?.precision)
                  );
                  throughputSeriesByRow.set(i, scaled);
                }
              }
            });
          } catch {
          }
        }
      }
      const aggregates = {
        serviceCount: totalServiceCount,
        metrics: {},
        seriesByMetric: {}
      };
      for (const col of cfg.columns) {
        const kind = col.aggregation ?? "avg";
        aggregates.metrics[col.metric] = aggregate(
          topRows.map((r) => r.metrics[col.metric] ?? null),
          kind
        );
        const colSeries = topRows.map(
          (r) => seriesByServiceMetric.get(r.serviceId)?.get(col.metric)
        );
        const agg = aggregateSeries(colSeries, kind);
        if (agg) aggregates.seriesByMetric[col.metric] = agg;
      }
      if (throughputCol) {
        const kind = throughputCol.aggregation ?? "sum";
        const matchingCol = cfg.columns.find((c) => c.metric === throughputCol.metric);
        const values = matchingCol ? topRows.map((r) => r.metrics[throughputCol.metric] ?? null) : topRows.map((_, i) => {
          const series = throughputSeriesByRow.get(i);
          if (!series) return null;
          const finite = series.filter((v) => v !== null);
          if (finite.length === 0) return null;
          return finite.reduce((a, b) => a + b, 0) / finite.length;
        });
        aggregates.throughputMetric = throughputCol.metric;
        aggregates.throughputValue = aggregate(values, kind);
        const seriesList = topRows.map((_, i) => throughputSeriesByRow.get(i));
        aggregates.spark = aggregateSeries(seriesList, kind);
      } else if (cfg.spark) {
        const kind = "avg";
        const seriesList = topRows.map((_, i) => sparkSeriesByRow.get(i));
        aggregates.spark = aggregateSeries(seriesList, kind);
      }
      const body = {
        layer: layerKey,
        topN: cfg.topN,
        orderBy: cfg.orderBy,
        generatedAt: Date.now(),
        step: "MINUTE",
        durationStart: window.start,
        durationEnd: window.end,
        rows: topRows,
        // `rows` is already sorted desc by orderBy and sliced to topN;
        // `sampledRows` is the full set the BFF probed (post-sort), so
        // per-layer views can render the long tail without a second call.
        sampledRows: rows,
        aggregates,
        reachable: true
      };
      return reply.send(body);
    }
  );
}

// src/http/query/instance.ts
var LIST_SERVICES_FOR_RESOLVE = (
  /* GraphQL */
  `
  query ListServicesForResolve($layer: String!) {
    services: listServices(layer: $layer) {
      id
      name
      normal
    }
  }
`
);
var LIST_INSTANCES = (
  /* GraphQL */
  `
  query LayerInstances($serviceId: ID!, $duration: Duration!) {
    instances: listInstances(serviceId: $serviceId, duration: $duration) {
      id
      name
      language
      attributes {
        name
        value
      }
    }
  }
`
);
var DEFAULT_WINDOW_MIN2 = 60;
function fmtMinute2(d) {
  const yyyy = d.getUTCFullYear();
  const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
  const dd = String(d.getUTCDate()).padStart(2, "0");
  const HH = String(d.getUTCHours()).padStart(2, "0");
  const MM = String(d.getUTCMinutes()).padStart(2, "0");
  return `${yyyy}-${mm}-${dd} ${HH}${MM}`;
}
function defaultWindow2() {
  const end = /* @__PURE__ */ new Date();
  const start = new Date(end.getTime() - DEFAULT_WINDOW_MIN2 * 6e4);
  return { start: fmtMinute2(start), end: fmtMinute2(end) };
}
function registerInstanceRoute(app2, deps) {
  const auth = requireAuth(deps);
  app2.get(
    "/api/layer/:key/instances",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      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;
      const serviceArg = (q.service ?? "").trim();
      if (!serviceArg) {
        return reply.code(400).send({ error: "missing_service" });
      }
      const cfgCurrent = deps.config.current;
      const opts = buildOapOpts(cfgCurrent, deps.fetch);
      const window = defaultWindow2();
      let serviceId = serviceArg;
      if (!/^[A-Za-z0-9+/=]+\.\d+$/.test(serviceArg)) {
        try {
          const data = await graphqlPost(opts, LIST_SERVICES_FOR_RESOLVE, {
            layer: layerKey.toUpperCase()
          });
          const match = data.services.find((s) => s.name === serviceArg) ?? data.services.find((s) => s.id === serviceArg) ?? null;
          if (!match) {
            return reply.send({
              layer: layerKey,
              service: serviceArg,
              generatedAt: Date.now(),
              instances: [],
              reachable: true,
              error: "service not found"
            });
          }
          serviceId = match.id;
        } catch (err) {
          return reply.send({
            layer: layerKey,
            service: serviceArg,
            generatedAt: Date.now(),
            instances: [],
            reachable: false,
            error: err instanceof Error ? err.message : String(err)
          });
        }
      }
      try {
        const data = await graphqlPost(opts, LIST_INSTANCES, {
          serviceId,
          duration: { start: window.start, end: window.end, step: "MINUTE" }
        });
        const rows = (data.instances ?? []).map((i) => ({
          id: i.id,
          name: i.name,
          language: i.language ?? null,
          attributes: i.attributes ?? []
        }));
        return reply.send({
          layer: layerKey,
          service: serviceArg,
          generatedAt: Date.now(),
          instances: rows,
          reachable: true
        });
      } catch (err) {
        return reply.send({
          layer: layerKey,
          service: serviceArg,
          generatedAt: Date.now(),
          instances: [],
          reachable: false,
          error: err instanceof Error ? err.message : String(err)
        });
      }
    }
  );
}

// src/http/query/endpoint.ts
var LIST_SERVICES_FOR_RESOLVE2 = (
  /* GraphQL */
  `
  query ListServicesForEndpointResolve($layer: String!) {
    services: listServices(layer: $layer) {
      id
      name
      normal
    }
  }
`
);
var FIND_ENDPOINTS = (
  /* GraphQL */
  `
  query LayerEndpoints($serviceId: ID!, $keyword: String!, $limit: Int!, $duration: Duration!) {
    endpoints: findEndpoint(serviceId: $serviceId, keyword: $keyword, limit: $limit, duration: $duration) {
      id
      name
    }
  }
`
);
var DEFAULT_WINDOW_MIN3 = 60;
function fmtMinute3(d) {
  const yyyy = d.getUTCFullYear();
  const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
  const dd = String(d.getUTCDate()).padStart(2, "0");
  const HH = String(d.getUTCHours()).padStart(2, "0");
  const MM = String(d.getUTCMinutes()).padStart(2, "0");
  return `${yyyy}-${mm}-${dd} ${HH}${MM}`;
}
function defaultWindow3() {
  const end = /* @__PURE__ */ new Date();
  const start = new Date(end.getTime() - DEFAULT_WINDOW_MIN3 * 6e4);
  return { start: fmtMinute3(start), end: fmtMinute3(end) };
}
function registerEndpointRoute(app2, deps) {
  const auth = requireAuth(deps);
  app2.get(
    "/api/layer/:key/endpoints",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      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;
      const serviceArg = (q.service ?? "").trim();
      if (!serviceArg) return reply.code(400).send({ error: "missing_service" });
      const keyword = (q.q ?? "").trim();
      const limit = Math.max(20, Math.min(50, Number(q.limit) || 20));
      const cfgCurrent = deps.config.current;
      const opts = buildOapOpts(cfgCurrent, deps.fetch);
      const window = defaultWindow3();
      let serviceId = serviceArg;
      if (!/^[A-Za-z0-9+/=]+\.\d+$/.test(serviceArg)) {
        try {
          const data = await graphqlPost(opts, LIST_SERVICES_FOR_RESOLVE2, { layer: layerKey.toUpperCase() });
          const match = data.services.find((s) => s.name === serviceArg) ?? data.services.find((s) => s.id === serviceArg) ?? null;
          if (!match) {
            return reply.send({
              layer: layerKey,
              service: serviceArg,
              query: keyword,
              limit,
              generatedAt: Date.now(),
              endpoints: [],
              reachable: true,
              error: "service not found"
            });
          }
          serviceId = match.id;
        } catch (err) {
          return reply.send({
            layer: layerKey,
            service: serviceArg,
            query: keyword,
            limit,
            generatedAt: Date.now(),
            endpoints: [],
            reachable: false,
            error: err instanceof Error ? err.message : String(err)
          });
        }
      }
      try {
        const data = await graphqlPost(opts, FIND_ENDPOINTS, {
          serviceId,
          keyword,
          limit,
          duration: { start: window.start, end: window.end, step: "MINUTE" }
        });
        return reply.send({
          layer: layerKey,
          service: serviceArg,
          query: keyword,
          limit,
          generatedAt: Date.now(),
          endpoints: data.endpoints ?? [],
          reachable: true
        });
      } catch (err) {
        return reply.send({
          layer: layerKey,
          service: serviceArg,
          query: keyword,
          limit,
          generatedAt: Date.now(),
          endpoints: [],
          reachable: false,
          error: err instanceof Error ? err.message : String(err)
        });
      }
    }
  );
}

// src/http/query/topology.ts
var SERVICES_TOPOLOGY = (
  /* GraphQL */
  `
  query ServicesTopology($duration: Duration!, $serviceIds: [ID!]!) {
    topology: getServicesTopology(duration: $duration, serviceIds: $serviceIds) {
      nodes { id name type isReal layers }
      calls { id source target detectPoints }
    }
  }
`
);
var LIST_SERVICES_FOR_RESOLVE3 = (
  /* GraphQL */
  `
  query ListServicesForTopology($layer: String!) {
    services: listServices(layer: $layer) {
      id
      name
      normal
    }
  }
`
);
var DEFAULT_WINDOW_MIN4 = 60;
function fmtMinute4(d) {
  const yyyy = d.getUTCFullYear();
  const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
  const dd = String(d.getUTCDate()).padStart(2, "0");
  const HH = String(d.getUTCHours()).padStart(2, "0");
  const MM = String(d.getUTCMinutes()).padStart(2, "0");
  return `${yyyy}-${mm}-${dd} ${HH}${MM}`;
}
function defaultWindow4() {
  const end = /* @__PURE__ */ new Date();
  const start = new Date(end.getTime() - DEFAULT_WINDOW_MIN4 * 6e4);
  return { start: fmtMinute4(start), end: fmtMinute4(end) };
}
function nodeFragment(alias2, m, serviceName, normal, w) {
  return `${alias2}: execExpression(
      expression: ${JSON.stringify(m.mqe)},
      entity: { scope: Service, serviceName: ${JSON.stringify(serviceName)}, normal: ${normal ? "true" : "false"} },
      duration: { start: ${JSON.stringify(w.start)}, end: ${JSON.stringify(w.end)}, step: MINUTE }
    ) { type error results { values { value } } }`;
}
function relationFragment(alias2, m, sourceName, sourceNormal, destName, destNormal, w) {
  return `${alias2}: execExpression(
      expression: ${JSON.stringify(m.mqe)},
      entity: { serviceName: ${JSON.stringify(sourceName)}, normal: ${sourceNormal ? "true" : "false"}, destServiceName: ${JSON.stringify(destName)}, destNormal: ${destNormal ? "true" : "false"} },
      duration: { start: ${JSON.stringify(w.start)}, end: ${JSON.stringify(w.end)}, step: MINUTE }
    ) { type error results { values { value } } }`;
}
function aggregateMqe(env, kind) {
  if (!env || env.error) return null;
  const values = env.results?.[0]?.values ?? [];
  const nums = [];
  for (const v of values) {
    if (v.value === null || v.value === void 0) continue;
    const n = Number(v.value);
    if (Number.isFinite(n)) nums.push(n);
  }
  if (nums.length === 0) return null;
  const sum = nums.reduce((a, b) => a + b, 0);
  return kind === "sum" ? sum : sum / nums.length;
}
function seriesFromMqe(env) {
  if (!env || env.error) return null;
  const values = env.results?.[0]?.values ?? [];
  if (values.length === 0) return null;
  return values.map((v) => {
    if (v.value === null || v.value === void 0) return null;
    const n = Number(v.value);
    return Number.isFinite(n) ? n : null;
  });
}
function emptyResponse(layerKey, serviceArg, depth, cfg, reachable, err) {
  return {
    layer: layerKey,
    service: serviceArg,
    depth,
    generatedAt: Date.now(),
    config: cfg,
    nodes: [],
    calls: [],
    reachable,
    ...err ? { error: err } : {}
  };
}
function legacyNodeView(metrics) {
  return {
    cpm: metrics.cpm ?? null,
    respTime: metrics.respTime ?? null,
    sla: metrics.sla ?? null
  };
}
function legacyEdgeView(serverMetrics, clientMetrics) {
  return {
    serverCpm: serverMetrics.cpm ?? null,
    serverRespTime: serverMetrics.respTime ?? null,
    clientCpm: clientMetrics.cpm ?? null,
    clientRespTime: clientMetrics.respTime ?? null
  };
}
function registerTopologyRoute(app2, deps) {
  const auth = requireAuth(deps);
  app2.get(
    "/api/layer/:key/topology",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      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;
      const serviceArg = (q.service ?? "").trim();
      const depth = Math.max(1, Math.min(3, Number(q.depth) || 1));
      const template = getLayerTemplate(layerKey);
      const topoCfg = topologyConfigFor(template);
      const cfgCurrent = deps.config.current;
      const opts = buildOapOpts(cfgCurrent, deps.fetch);
      const window = defaultWindow4();
      const oapLayer = layerKey.toUpperCase();
      const durationVar = { start: window.start, end: window.end, step: "MINUTE" };
      let seedIds = [];
      const knownServices = /* @__PURE__ */ new Map();
      try {
        const data = await graphqlPost(opts, LIST_SERVICES_FOR_RESOLVE3, { layer: oapLayer });
        for (const s of data.services) {
          knownServices.set(s.id, { id: s.id, name: s.name, normal: s.normal !== false });
        }
        if (serviceArg) {
          const wants = serviceArg.split(",").map((s) => s.trim()).filter(Boolean);
          const matches = wants.map(
            (w) => data.services.find((s) => s.id === w) ?? data.services.find((s) => s.name === w) ?? null
          );
          const missing = wants.filter((_, i) => matches[i] === null);
          if (matches.every((m) => m === null)) {
            return reply.send(
              emptyResponse(layerKey, serviceArg, depth, topoCfg, true, `service${wants.length === 1 ? "" : "s"} not found: ${missing.join(", ")}`)
            );
          }
          seedIds = matches.filter((m) => m !== null).map((m) => m.id);
        } else {
          seedIds = data.services.map((s) => s.id);
          console.log(`[topology] layer=${oapLayer} seed-services=${seedIds.length}`);
        }
      } catch (err) {
        return reply.send(
          emptyResponse(
            layerKey,
            serviceArg || null,
            depth,
            topoCfg,
            false,
            err instanceof Error ? err.message : String(err)
          )
        );
      }
      const nodes = /* @__PURE__ */ new Map();
      const calls = /* @__PURE__ */ new Map();
      let frontier = seedIds.slice();
      const seen = new Set(frontier);
      try {
        for (let d = 0; d < depth && frontier.length > 0; d++) {
          const data = await graphqlPost(opts, SERVICES_TOPOLOGY, {
            duration: durationVar,
            serviceIds: frontier
          });
          for (const n of data.topology.nodes) {
            if (!nodes.has(n.id)) nodes.set(n.id, n);
          }
          for (const c of data.topology.calls) {
            if (!calls.has(c.id)) calls.set(c.id, c);
          }
          const next = [];
          for (const n of data.topology.nodes) {
            if (!seen.has(n.id)) {
              seen.add(n.id);
              if (n.isReal) next.push(n.id);
            }
          }
          frontier = next;
        }
      } catch (err) {
        return reply.send(
          emptyResponse(
            layerKey,
            serviceArg || null,
            depth,
            topoCfg,
            false,
            err instanceof Error ? err.message : String(err)
          )
        );
      }
      console.log(`[topology] layer=${oapLayer} returned-nodes=${nodes.size} edges=${calls.size}`);
      const realNodes = [...nodes.values()].filter((n) => n.isReal);
      const nodeMetricVals = /* @__PURE__ */ new Map();
      if (realNodes.length > 0 && topoCfg.nodeMetrics.length > 0) {
        const fragments = [];
        const aliasMap = /* @__PURE__ */ new Map();
        realNodes.forEach((n, i) => {
          const meta = knownServices.get(n.id);
          const normal = meta?.normal ?? true;
          topoCfg.nodeMetrics.forEach((m, j) => {
            const alias2 = `n${i}_${j}`;
            aliasMap.set(alias2, { nodeId: n.id, metric: m });
            fragments.push(nodeFragment(alias2, m, n.name, normal, window));
          });
        });
        const CHUNK = 150;
        for (let i = 0; i < fragments.length; i += CHUNK) {
          const slice = fragments.slice(i, i + CHUNK);
          const query = `query NodeMetrics {
  ${slice.join("\n  ")}
}`;
          let env;
          try {
            env = await graphqlPost(opts, query);
          } catch {
            break;
          }
          for (const [alias2, shape] of Object.entries(env)) {
            const info = aliasMap.get(alias2);
            if (!info) continue;
            const v = aggregateMqe(shape, info.metric.aggregation ?? "avg");
            const rec = nodeMetricVals.get(info.nodeId) ?? {};
            rec[info.metric.id] = v;
            nodeMetricVals.set(info.nodeId, rec);
          }
        }
      }
      const serverMetricVals = /* @__PURE__ */ new Map();
      const clientMetricVals = /* @__PURE__ */ new Map();
      const serverMetricSeries = /* @__PURE__ */ new Map();
      const clientMetricSeries = /* @__PURE__ */ new Map();
      const candidateEdges = [...calls.values()].filter((c) => {
        const a = nodes.get(c.source);
        const b = nodes.get(c.target);
        return !!a && !!b && !!a.name && !!b.name;
      });
      const linkSrv = topoCfg.linkServerMetrics ?? [];
      const linkCli = topoCfg.linkClientMetrics ?? [];
      if (candidateEdges.length > 0 && (linkSrv.length > 0 || linkCli.length > 0)) {
        const fragments = [];
        const aliasMap = /* @__PURE__ */ new Map();
        candidateEdges.forEach((c, i) => {
          const src = nodes.get(c.source);
          const dst = nodes.get(c.target);
          const srcMeta = knownServices.get(src.id);
          const dstMeta = knownServices.get(dst.id);
          const srcNormal = srcMeta?.normal ?? src.isReal;
          const dstNormal = dstMeta?.normal ?? dst.isReal;
          if (dst.isReal) {
            linkSrv.forEach((m, j) => {
              const alias2 = `s${i}_${j}`;
              aliasMap.set(alias2, { callId: c.id, metric: m, side: "server" });
              fragments.push(relationFragment(alias2, m, src.name, srcNormal, dst.name, dstNormal, window));
            });
          }
          if (src.isReal) {
            linkCli.forEach((m, j) => {
              const alias2 = `c${i}_${j}`;
              aliasMap.set(alias2, { callId: c.id, metric: m, side: "client" });
              fragments.push(relationFragment(alias2, m, src.name, srcNormal, dst.name, dstNormal, window));
            });
          }
        });
        const CHUNK = 200;
        for (let i = 0; i < fragments.length; i += CHUNK) {
          const slice = fragments.slice(i, i + CHUNK);
          const query = `query EdgeMetrics {
  ${slice.join("\n  ")}
}`;
          let env;
          try {
            env = await graphqlPost(opts, query);
          } catch {
            break;
          }
          for (const [alias2, shape] of Object.entries(env)) {
            const info = aliasMap.get(alias2);
            if (!info) continue;
            const v = aggregateMqe(shape, info.metric.aggregation ?? "avg");
            const seriesBucket = info.side === "server" ? serverMetricSeries : clientMetricSeries;
            const valBucket = info.side === "server" ? serverMetricVals : clientMetricVals;
            const valRec = valBucket.get(info.callId) ?? {};
            valRec[info.metric.id] = v;
            valBucket.set(info.callId, valRec);
            const sRec = seriesBucket.get(info.callId) ?? {};
            sRec[info.metric.id] = seriesFromMqe(shape);
            seriesBucket.set(info.callId, sRec);
          }
        }
      }
      const connectedNodeIds = /* @__PURE__ */ new Set();
      for (const c of calls.values()) {
        connectedNodeIds.add(c.source);
        connectedNodeIds.add(c.target);
      }
      const liveNodes = [];
      for (const n of nodes.values()) {
        if (!connectedNodeIds.has(n.id)) continue;
        const m = nodeMetricVals.get(n.id) ?? {};
        const filled = {};
        for (const def of topoCfg.nodeMetrics) {
          filled[def.id] = m[def.id] ?? null;
        }
        liveNodes.push({
          id: n.id,
          name: n.name,
          type: n.type,
          isReal: n.isReal,
          layers: n.layers ?? [],
          metrics: filled,
          ...legacyNodeView(filled)
        });
      }
      const liveNodeIds = new Set(liveNodes.map((n) => n.id));
      const liveCalls = [];
      for (const c of calls.values()) {
        if (!liveNodeIds.has(c.source) || !liveNodeIds.has(c.target)) continue;
        const sm = serverMetricVals.get(c.id) ?? {};
        const cm = clientMetricVals.get(c.id) ?? {};
        const ss = serverMetricSeries.get(c.id) ?? {};
        const cs = clientMetricSeries.get(c.id) ?? {};
        const filledSrv = {};
        const filledSrvSeries = {};
        for (const def of linkSrv) {
          filledSrv[def.id] = sm[def.id] ?? null;
          filledSrvSeries[def.id] = ss[def.id] ?? null;
        }
        const filledCli = {};
        const filledCliSeries = {};
        for (const def of linkCli) {
          filledCli[def.id] = cm[def.id] ?? null;
          filledCliSeries[def.id] = cs[def.id] ?? null;
        }
        liveCalls.push({
          id: c.id,
          source: c.source,
          target: c.target,
          detectPoints: c.detectPoints ?? [],
          serverMetrics: filledSrv,
          clientMetrics: filledCli,
          serverMetricSeries: filledSrvSeries,
          clientMetricSeries: filledCliSeries,
          ...legacyEdgeView(filledSrv, filledCli)
        });
      }
      return reply.send({
        layer: layerKey,
        service: serviceArg || null,
        depth,
        generatedAt: Date.now(),
        config: topoCfg,
        nodes: liveNodes,
        calls: liveCalls,
        reachable: true
      });
    }
  );
}

// src/http/query/endpoint-dependency.ts
var LIST_SERVICES_FOR_RESOLVE4 = (
  /* GraphQL */
  `
  query ListServicesForEndpointDep($layer: String!) {
    services: listServices(layer: $layer) {
      id
      name
      normal
    }
  }
`
);
var FIND_ENDPOINT = (
  /* GraphQL */
  `
  query FindEndpointForDep($serviceId: ID!, $keyword: String!, $duration: Duration!) {
    endpoints: findEndpoint(serviceId: $serviceId, keyword: $keyword, limit: 50, duration: $duration) {
      id
      name
    }
  }
`
);
var ENDPOINT_DEPENDENCY = (
  /* GraphQL */
  `
  query EndpointDependency($endpointId: ID!, $duration: Duration!) {
    topology: getEndpointDependencies(endpointId: $endpointId, duration: $duration) {
      nodes { id name serviceId serviceName type isReal }
      calls { id source target detectPoints }
    }
  }
`
);
var DEFAULT_WINDOW_MIN5 = 60;
function fmtMinute5(d) {
  const yyyy = d.getUTCFullYear();
  const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
  const dd = String(d.getUTCDate()).padStart(2, "0");
  const HH = String(d.getUTCHours()).padStart(2, "0");
  const MM = String(d.getUTCMinutes()).padStart(2, "0");
  return `${yyyy}-${mm}-${dd} ${HH}${MM}`;
}
function defaultWindow5() {
  const end = /* @__PURE__ */ new Date();
  const start = new Date(end.getTime() - DEFAULT_WINDOW_MIN5 * 6e4);
  return { start: fmtMinute5(start), end: fmtMinute5(end) };
}
function endpointFragment(alias2, m, serviceName, endpointName, normal, w) {
  return `${alias2}: execExpression(
      expression: ${JSON.stringify(m.mqe)},
      entity: { scope: Endpoint, serviceName: ${JSON.stringify(serviceName)}, endpointName: ${JSON.stringify(endpointName)}, normal: ${normal ? "true" : "false"} },
      duration: { start: ${JSON.stringify(w.start)}, end: ${JSON.stringify(w.end)}, step: MINUTE }
    ) { type error results { values { value } } }`;
}
function endpointRelationFragment(alias2, m, sourceServiceName, sourceEndpointName, sourceNormal, destServiceName, destEndpointName, destNormal, w) {
  return `${alias2}: execExpression(
      expression: ${JSON.stringify(m.mqe)},
      entity: { serviceName: ${JSON.stringify(sourceServiceName)}, endpointName: ${JSON.stringify(sourceEndpointName)}, normal: ${sourceNormal ? "true" : "false"}, destServiceName: ${JSON.stringify(destServiceName)}, destEndpointName: ${JSON.stringify(destEndpointName)}, destNormal: ${destNormal ? "true" : "false"} },
      duration: { start: ${JSON.stringify(w.start)}, end: ${JSON.stringify(w.end)}, step: MINUTE }
    ) { type error results { values { value } } }`;
}
function seriesFromMqe2(env) {
  if (!env || env.error) return null;
  const values = env.results?.[0]?.values ?? [];
  if (values.length === 0) return null;
  return values.map((v) => {
    if (v.value === null || v.value === void 0) return null;
    const n = Number(v.value);
    return Number.isFinite(n) ? n : null;
  });
}
function aggregateMqe2(env, kind) {
  if (!env || env.error) return null;
  const values = env.results?.[0]?.values ?? [];
  const nums = [];
  for (const v of values) {
    if (v.value === null || v.value === void 0) continue;
    const n = Number(v.value);
    if (Number.isFinite(n)) nums.push(n);
  }
  if (nums.length === 0) return null;
  const sum = nums.reduce((a, b) => a + b, 0);
  return kind === "sum" ? sum : sum / nums.length;
}
function emptyResponse2(layerKey, serviceArg, endpointArg, endpointId, cfg, reachable, err) {
  return {
    layer: layerKey,
    service: serviceArg,
    endpoint: endpointArg,
    endpointId,
    generatedAt: Date.now(),
    config: cfg,
    nodes: [],
    calls: [],
    reachable,
    ...err ? { error: err } : {}
  };
}
function legacyNodeView2(metrics) {
  return {
    cpm: metrics.cpm ?? null,
    respTime: metrics.respTime ?? null,
    sla: metrics.sla ?? null
  };
}
function legacyEdgeView2(metrics) {
  return {
    cpm: metrics.cpm ?? null,
    respTime: metrics.respTime ?? null
  };
}
function registerEndpointDependencyRoute(app2, deps) {
  const auth = requireAuth(deps);
  app2.get(
    "/api/layer/:key/endpoint-dependency",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      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;
      const serviceArg = (q.service ?? "").trim();
      const endpointArg = (q.endpoint ?? "").trim();
      if (!serviceArg) return reply.code(400).send({ error: "missing_service" });
      if (!endpointArg) return reply.code(400).send({ error: "missing_endpoint" });
      const template = getLayerTemplate(layerKey);
      const epCfg = endpointDependencyConfigFor(template);
      const cfgCurrent = deps.config.current;
      const opts = buildOapOpts(cfgCurrent, deps.fetch);
      const window = defaultWindow5();
      const oapLayer = layerKey.toUpperCase();
      const durationVar = { start: window.start, end: window.end, step: "MINUTE" };
      let serviceId = serviceArg;
      let normal = true;
      try {
        const data = await graphqlPost(opts, LIST_SERVICES_FOR_RESOLVE4, { layer: oapLayer });
        const match = data.services.find((s) => s.name === serviceArg) ?? data.services.find((s) => s.id === serviceArg) ?? null;
        if (!match) {
          return reply.send(emptyResponse2(layerKey, serviceArg, endpointArg, null, epCfg, true, "service not found"));
        }
        serviceId = match.id;
        normal = match.normal !== false;
      } catch (err) {
        return reply.send(
          emptyResponse2(
            layerKey,
            serviceArg,
            endpointArg,
            null,
            epCfg,
            false,
            err instanceof Error ? err.message : String(err)
          )
        );
      }
      let endpointId = endpointArg;
      if (!/\.0_/.test(endpointArg)) {
        try {
          const data = await graphqlPost(
            opts,
            FIND_ENDPOINT,
            { serviceId, keyword: endpointArg, duration: durationVar }
          );
          const match = data.endpoints.find((e) => e.name === endpointArg) ?? data.endpoints[0] ?? null;
          if (!match) {
            return reply.send(
              emptyResponse2(layerKey, serviceArg, endpointArg, null, epCfg, true, "endpoint not found")
            );
          }
          endpointId = match.id;
        } catch (err) {
          return reply.send(
            emptyResponse2(
              layerKey,
              serviceArg,
              endpointArg,
              null,
              epCfg,
              false,
              err instanceof Error ? err.message : String(err)
            )
          );
        }
      }
      let graph;
      try {
        const data = await graphqlPost(opts, ENDPOINT_DEPENDENCY, {
          endpointId,
          duration: durationVar
        });
        graph = data.topology;
      } catch (err) {
        return reply.send(
          emptyResponse2(
            layerKey,
            serviceArg,
            endpointArg,
            endpointId,
            epCfg,
            false,
            err instanceof Error ? err.message : String(err)
          )
        );
      }
      const realNodes = graph.nodes.filter(
        (n) => n.isReal && n.serviceName && n.name && n.name !== "User"
      );
      const nodeMetricVals = /* @__PURE__ */ new Map();
      if (realNodes.length > 0 && epCfg.nodeMetrics.length > 0) {
        const fragments = [];
        const aliasMap = /* @__PURE__ */ new Map();
        realNodes.forEach((n, i) => {
          const isFocus = n.serviceId === serviceId;
          const useNormal = isFocus ? normal : true;
          epCfg.nodeMetrics.forEach((m, j) => {
            const alias2 = `e${i}_${j}`;
            aliasMap.set(alias2, { nodeId: n.id, metric: m });
            fragments.push(endpointFragment(alias2, m, n.serviceName, n.name, useNormal, window));
          });
        });
        const CHUNK = 150;
        for (let i = 0; i < fragments.length; i += CHUNK) {
          const slice = fragments.slice(i, i + CHUNK);
          const query = `query EndpointMetrics {
  ${slice.join("\n  ")}
}`;
          let env;
          try {
            env = await graphqlPost(opts, query);
          } catch {
            break;
          }
          for (const [alias2, shape] of Object.entries(env)) {
            const info = aliasMap.get(alias2);
            if (!info) continue;
            const v = aggregateMqe2(shape, info.metric.aggregation ?? "avg");
            const rec = nodeMetricVals.get(info.nodeId) ?? {};
            rec[info.metric.id] = v;
            nodeMetricVals.set(info.nodeId, rec);
          }
        }
      }
      const linkMetrics = epCfg.linkMetrics ?? [];
      const edgeMetricVals = /* @__PURE__ */ new Map();
      const edgeMetricSeries = /* @__PURE__ */ new Map();
      const realEndpointMap = new Map(realNodes.map((n) => [n.id, n]));
      const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
      const candidateEdges = graph.calls.filter((c) => {
        const dst = nodeById.get(c.target);
        return !!dst && dst.isReal && !!dst.name && !!dst.serviceName;
      });
      if (candidateEdges.length > 0 && linkMetrics.length > 0) {
        const fragments = [];
        const aliasMap = /* @__PURE__ */ new Map();
        candidateEdges.forEach((c, i) => {
          const dst = realEndpointMap.get(c.target) ?? nodeById.get(c.target);
          const src = nodeById.get(c.source);
          const srcName = src?.name || "User";
          const srcServiceName = src?.serviceName || "User";
          const srcNormal = src ? src.isReal : false;
          const dstNormal = dst.serviceId === serviceId ? normal : true;
          linkMetrics.forEach((m, j) => {
            const alias2 = `r${i}_${j}`;
            aliasMap.set(alias2, { callId: c.id, metric: m });
            fragments.push(
              endpointRelationFragment(
                alias2,
                m,
                srcServiceName,
                srcName,
                srcNormal,
                dst.serviceName,
                dst.name,
                dstNormal,
                window
              )
            );
          });
        });
        const CHUNK = 200;
        for (let i = 0; i < fragments.length; i += CHUNK) {
          const slice = fragments.slice(i, i + CHUNK);
          const query = `query EndpointEdgeMetrics {
  ${slice.join("\n  ")}
}`;
          let env;
          try {
            env = await graphqlPost(opts, query);
          } catch {
            break;
          }
          for (const [alias2, shape] of Object.entries(env)) {
            const info = aliasMap.get(alias2);
            if (!info) continue;
            const v = aggregateMqe2(shape, info.metric.aggregation ?? "avg");
            const rec = edgeMetricVals.get(info.callId) ?? {};
            rec[info.metric.id] = v;
            edgeMetricVals.set(info.callId, rec);
            const sRec = edgeMetricSeries.get(info.callId) ?? {};
            sRec[info.metric.id] = seriesFromMqe2(shape);
            edgeMetricSeries.set(info.callId, sRec);
          }
        }
      }
      function hasAnyValue(r) {
        for (const v of Object.values(r)) if (v !== null) return true;
        return false;
      }
      const liveNodes = [];
      for (const n of graph.nodes) {
        const m = nodeMetricVals.get(n.id) ?? {};
        const filled = {};
        for (const def of epCfg.nodeMetrics) filled[def.id] = m[def.id] ?? null;
        if (n.isReal && n.name !== "User" && !hasAnyValue(filled)) continue;
        liveNodes.push({
          id: n.id,
          name: n.name,
          serviceId: n.serviceId,
          serviceName: n.serviceName,
          type: n.type,
          isReal: n.isReal,
          metrics: filled,
          ...legacyNodeView2(filled)
        });
      }
      const liveIds = new Set(liveNodes.map((n) => n.id));
      const liveCalls = [];
      for (const c of graph.calls) {
        if (!liveIds.has(c.source) || !liveIds.has(c.target)) continue;
        const m = edgeMetricVals.get(c.id) ?? {};
        const s = edgeMetricSeries.get(c.id) ?? {};
        const filled = {};
        const filledSeries = {};
        for (const def of linkMetrics) {
          filled[def.id] = m[def.id] ?? null;
          filledSeries[def.id] = s[def.id] ?? null;
        }
        liveCalls.push({
          id: c.id,
          source: c.source,
          target: c.target,
          detectPoints: c.detectPoints ?? [],
          metrics: filled,
          metricSeries: filledSeries,
          ...legacyEdgeView2(filled)
        });
      }
      return reply.send({
        layer: layerKey,
        service: serviceArg,
        endpoint: endpointArg,
        endpointId,
        generatedAt: Date.now(),
        config: epCfg,
        nodes: liveNodes,
        calls: liveCalls,
        reachable: true
      });
    }
  );
}

// src/util/trace-protocol-cache.ts
var CACHE_TTL_MS = 5 * 6e4;
var cache3 = /* @__PURE__ */ new Map();
var PROBE_QUERY = (
  /* GraphQL */
  `
  query ProbeQueryTracesSupport {
    hasQueryTracesV2Support
  }
`
);
async function detectTraceQueryApi(opts) {
  const now = Date.now();
  const cached2 = cache3.get(opts.queryUrl);
  if (cached2 && cached2.expiresAt > now) return cached2.api;
  let api = "queryBasicTraces";
  try {
    const data = await graphqlPost(opts, PROBE_QUERY);
    if (data.hasQueryTracesV2Support === true) api = "queryTraces";
  } catch {
  }
  cache3.set(opts.queryUrl, { api, expiresAt: now + CACHE_TTL_MS });
  return api;
}

// src/client/zipkin.ts
var DEFAULT_LOOKBACK_MS = 30 * 6e4;
async function zipkinFetch(opts, path2) {
  const f = opts.fetch ?? globalThis.fetch.bind(globalThis);
  const url = opts.queryUrl.replace(/\/$/, "") + path2;
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
  const headers = { Accept: "application/json" };
  if (opts.auth) {
    const raw = `${opts.auth.username}:${opts.auth.password}`;
    const b64 = Buffer.from(raw, "utf8").toString("base64");
    headers.authorization = `Basic ${b64}`;
  }
  let res;
  try {
    res = await f(url, {
      method: "GET",
      headers,
      signal: controller.signal
    });
  } finally {
    clearTimeout(timer);
  }
  if (!res.ok) {
    const body = await res.text().catch(() => "");
    throw new Error(`zipkin ${res.status} ${url} \u2014 ${body.slice(0, 200)}`);
  }
  return await res.json();
}
function summariseZipkinTrace(spans) {
  if (spans.length === 0) {
    return {
      traceId: "",
      rootName: null,
      rootService: null,
      timestamp: null,
      duration: null,
      spanCount: 0,
      errorCount: 0
    };
  }
  const root = spans.find((s) => !s.parentId) ?? spans[0];
  let errorCount = 0;
  for (const s of spans) {
    if (s.tags && (s.tags.error !== void 0 || s.tags["otel.status_code"] === "ERROR")) {
      errorCount += 1;
    }
  }
  return {
    traceId: root.traceId,
    rootName: root.name ?? null,
    rootService: root.localEndpoint?.serviceName ?? null,
    timestamp: root.timestamp ?? null,
    duration: root.duration ?? null,
    spanCount: spans.length,
    errorCount
  };
}
async function zipkinFetchTraces(opts, query) {
  const qs = new URLSearchParams();
  if (query.serviceName) qs.set("serviceName", query.serviceName);
  if (query.spanName) qs.set("spanName", query.spanName);
  if (typeof query.minDuration === "number") qs.set("minDuration", String(query.minDuration));
  if (typeof query.maxDuration === "number") qs.set("maxDuration", String(query.maxDuration));
  const endTs = query.endTs ?? Date.now();
  const lookback = query.lookback ?? DEFAULT_LOOKBACK_MS;
  qs.set("endTs", String(endTs));
  qs.set("lookback", String(lookback));
  qs.set("limit", String(query.limit ?? 20));
  const path2 = `/api/v2/traces?${qs.toString()}`;
  const arr = await zipkinFetch(opts, path2);
  return arr.map(summariseZipkinTrace).filter((r) => r.traceId);
}
async function zipkinFetchTraceById(opts, traceId) {
  const path2 = `/api/v2/trace/${encodeURIComponent(traceId)}`;
  return zipkinFetch(opts, path2);
}

// src/http/query/trace.ts
var DEFAULT_WINDOW_MIN6 = 30;
var MAX_WINDOW_MIN = 60 * 24 * 7;
function fmtMinute6(d) {
  const yyyy = d.getUTCFullYear();
  const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
  const dd = String(d.getUTCDate()).padStart(2, "0");
  const HH = String(d.getUTCHours()).padStart(2, "0");
  const MM = String(d.getUTCMinutes()).padStart(2, "0");
  return `${yyyy}-${mm}-${dd} ${HH}${MM}`;
}
function rollingWindow(minutes) {
  const m = Math.max(1, Math.min(MAX_WINDOW_MIN, Math.round(minutes)));
  const end = /* @__PURE__ */ new Date();
  const start = new Date(end.getTime() - m * 6e4);
  return { start: fmtMinute6(start), end: fmtMinute6(end) };
}
function explicitWindow(startIso, endIso) {
  const s = new Date(startIso);
  const e = new Date(endIso);
  if (!Number.isFinite(s.getTime()) || !Number.isFinite(e.getTime()) || e.getTime() < s.getTime()) {
    return null;
  }
  return { start: fmtMinute6(s), end: fmtMinute6(e) };
}
var LIST_SERVICES_FOR_RESOLVE5 = (
  /* GraphQL */
  `
  query ListServicesForTrace($layer: String!) {
    services: listServices(layer: $layer) {
      id
      name
      normal
    }
  }
`
);
var QUERY_BASIC_TRACES = (
  /* GraphQL */
  `
  query QueryBasicTraces($condition: TraceQueryCondition) {
    data: queryBasicTraces(condition: $condition) {
      traces {
        key: segmentId
        endpointNames
        duration
        start
        isError
        traceIds
      }
    }
  }
`
);
var QUERY_TRACES = (
  /* GraphQL */
  `
  query QueryTraces($condition: TraceQueryCondition) {
    data: queryTraces(condition: $condition) {
      traces {
        spans {
          traceId
          segmentId
          spanId
          parentSpanId
          refs { traceId parentSegmentId parentSpanId type }
          serviceCode
          serviceInstanceName
          startTime
          endTime
          endpointName
          type
          peer
          component
          isError
          layer
          tags { key value }
          logs { time data { key value } }
          attachedEvents {
            startTime { seconds nanos }
            event
            endTime { seconds nanos }
            tags { key value }
            summary { key value }
          }
        }
      }
    }
  }
`
);
var QUERY_TRACE_DETAIL = (
  /* GraphQL */
  `
  query QueryTrace($traceId: ID!) {
    trace: queryTrace(traceId: $traceId) {
      spans {
        traceId
        segmentId
        spanId
        parentSpanId
        refs { traceId parentSegmentId parentSpanId type }
        serviceCode
        serviceInstanceName
        startTime
        endTime
        endpointName
        type
        peer
        component
        isError
        layer
        tags { key value }
        logs { time data { key value } }
        attachedEvents {
          startTime { seconds nanos }
          event
          endTime { seconds nanos }
          tags { key value }
          summary { key value }
        }
      }
    }
  }
`
);
var OAP_SERVICE_ID_RE = /^[A-Za-z0-9+/=]+\.\d+$/;
async function resolveServiceId(opts, layer, serviceArg) {
  if (!serviceArg) return null;
  if (OAP_SERVICE_ID_RE.test(serviceArg)) return serviceArg;
  const data = await graphqlPost(opts, LIST_SERVICES_FOR_RESOLVE5, { layer: layer.toUpperCase() });
  return data.services.find((s) => s.name === serviceArg)?.id ?? data.services.find((s) => s.id === serviceArg)?.id ?? null;
}
function buildTraceCondition(body, resolvedServiceId, w) {
  return {
    ...resolvedServiceId ? { serviceId: resolvedServiceId } : {},
    ...body.instanceId ? { serviceInstanceId: body.instanceId } : {},
    ...body.endpointId ? { endpointId: body.endpointId } : {},
    ...body.traceId ? { traceId: body.traceId } : {},
    ...body.tags && body.tags.length > 0 ? { tags: body.tags } : {},
    ...typeof body.minTraceDuration === "number" ? { minTraceDuration: body.minTraceDuration } : {},
    ...typeof body.maxTraceDuration === "number" ? { maxTraceDuration: body.maxTraceDuration } : {},
    queryDuration: { start: w.start, end: w.end, step: "MINUTE" },
    traceState: body.traceState ?? "ALL",
    queryOrder: body.queryOrder ?? "BY_START_TIME",
    paging: {
      pageNum: body.pageNum ?? 1,
      pageSize: body.pageSize ?? 20
    }
  };
}
async function fetchNativeList(opts, body, layerKey) {
  const api = await detectTraceQueryApi(opts);
  const explicit = body.start && body.end ? explicitWindow(body.start, body.end) : null;
  const window = explicit ?? rollingWindow(body.windowMinutes ?? DEFAULT_WINDOW_MIN6);
  let serviceId = null;
  try {
    serviceId = body.serviceId ? body.serviceId : body.service ? await resolveServiceId(opts, layerKey, body.service) : null;
  } catch (err) {
    return {
      source: "native",
      api,
      traces: [],
      reachable: false,
      error: err instanceof Error ? err.message : String(err)
    };
  }
  const condition = buildTraceCondition(body, serviceId, window);
  try {
    if (api === "queryTraces") {
      const env2 = await graphqlPost(opts, QUERY_TRACES, { condition });
      const traces2 = (env2.data?.traces ?? []).map((t) => {
        const root = t.spans.find((s) => s.parentSpanId === -1) ?? t.spans[0];
        const ids = Array.from(new Set(t.spans.map((s) => s.traceId)));
        return {
          key: root?.segmentId ?? ids[0] ?? "",
          segmentId: root?.segmentId ?? "",
          endpointNames: root ? [root.endpointName] : [],
          duration: root ? root.endTime - root.startTime : 0,
          start: root ? String(root.startTime) : "",
          isError: t.spans.some((s) => s.isError),
          traceIds: ids,
          spans: t.spans
        };
      });
      return { source: "native", api, traces: traces2, reachable: true };
    }
    const env = await graphqlPost(opts, QUERY_BASIC_TRACES, { condition });
    const traces = (env.data?.traces ?? []).map((t) => ({
      key: t.key,
      segmentId: t.key,
      endpointNames: t.endpointNames,
      duration: t.duration,
      start: t.start,
      isError: t.isError,
      traceIds: t.traceIds
    }));
    return { source: "native", api, traces, reachable: true };
  } catch (err) {
    return {
      source: "native",
      api,
      traces: [],
      reachable: false,
      error: err instanceof Error ? err.message : String(err)
    };
  }
}
async function fetchZipkinList(opts, body) {
  try {
    const traces = await zipkinFetchTraces(opts, {
      serviceName: body.service,
      minDuration: body.minTraceDuration,
      maxDuration: body.maxTraceDuration,
      limit: body.pageSize ?? 20
    });
    return { source: "zipkin", traces, reachable: true };
  } catch (err) {
    return {
      source: "zipkin",
      traces: [],
      reachable: false,
      error: err instanceof Error ? err.message : String(err)
    };
  }
}
function registerTraceRoutes(app2, deps) {
  const auth = requireAuth(deps);
  app2.post(
    "/api/layer/:key/traces",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      const layerKey = params.key;
      if (!layerKey || !/^[a-z0-9_]+$/i.test(layerKey)) {
        return reply.code(400).send({ error: "invalid_layer_key" });
      }
      const body = req.body ?? {};
      const template = getLayerTemplate(layerKey);
      const tracesCfg = tracesConfigFor(template);
      const requestedSource = body.source ?? tracesCfg.source;
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      const wantNative = requestedSource === "both" || requestedSource === "native";
      const wantZipkin = requestedSource === "both" || requestedSource === "zipkin";
      const [native, zipkin] = await Promise.all([
        wantNative ? fetchNativeList(opts, body, layerKey) : Promise.resolve(void 0),
        wantZipkin ? fetchZipkinList(opts, body) : Promise.resolve(void 0)
      ]);
      const response = {
        generatedAt: Date.now(),
        source: requestedSource,
        ...native ? { native } : {},
        ...zipkin ? { zipkin } : {}
      };
      return reply.send(response);
    }
  );
  app2.get(
    "/api/trace/:traceId",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      const q = req.query;
      const source2 = q.source === "zipkin" ? "zipkin" : "native";
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      if (source2 === "native") {
        const api = await detectTraceQueryApi(opts);
        try {
          const env = await graphqlPost(
            opts,
            QUERY_TRACE_DETAIL,
            { traceId: params.traceId }
          );
          const detail = {
            source: "native",
            api,
            traceId: params.traceId,
            spans: env.trace?.spans ?? [],
            reachable: true
          };
          return reply.send({
            generatedAt: Date.now(),
            source: source2,
            native: detail
          });
        } catch (err) {
          const detail = {
            source: "native",
            api,
            traceId: params.traceId,
            spans: [],
            reachable: false,
            error: err instanceof Error ? err.message : String(err)
          };
          return reply.send({
            generatedAt: Date.now(),
            source: source2,
            native: detail
          });
        }
      }
      try {
        const spans = await zipkinFetchTraceById(opts, params.traceId);
        const detail = {
          source: "zipkin",
          traceId: params.traceId,
          spans,
          reachable: true
        };
        return reply.send({
          generatedAt: Date.now(),
          source: source2,
          zipkin: detail
        });
      } catch (err) {
        const detail = {
          source: "zipkin",
          traceId: params.traceId,
          spans: [],
          reachable: false,
          error: err instanceof Error ? err.message : String(err)
        };
        return reply.send({
          generatedAt: Date.now(),
          source: source2,
          zipkin: detail
        });
      }
    }
  );
}

// src/http/query/trace-tag.ts
var DEFAULT_WINDOW_MIN7 = 30;
var MAX_WINDOW_MIN2 = 60 * 24 * 7;
function fmtMinute7(d) {
  const pad2 = (n) => String(n).padStart(2, "0");
  return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())} ${pad2(d.getUTCHours())}${pad2(d.getUTCMinutes())}`;
}
function rollingWindow2(minutes) {
  const m = Math.max(1, Math.min(MAX_WINDOW_MIN2, Math.round(minutes)));
  const end = /* @__PURE__ */ new Date();
  const start = new Date(end.getTime() - m * 6e4);
  return { start: fmtMinute7(start), end: fmtMinute7(end), step: "MINUTE" };
}
var TRACE_KEYS_QUERY = (
  /* GraphQL */
  `
  query TraceTagKeys($duration: Duration!) {
    keys: queryTraceTagAutocompleteKeys(duration: $duration)
  }
`
);
var TRACE_VALUES_QUERY = (
  /* GraphQL */
  `
  query TraceTagValues($tagKey: String!, $duration: Duration!) {
    values: queryTraceTagAutocompleteValues(tagKey: $tagKey, duration: $duration)
  }
`
);
var LOG_KEYS_QUERY = (
  /* GraphQL */
  `
  query LogTagKeys($duration: Duration!) {
    keys: queryLogTagAutocompleteKeys(duration: $duration)
  }
`
);
var LOG_VALUES_QUERY = (
  /* GraphQL */
  `
  query LogTagValues($tagKey: String!, $duration: Duration!) {
    values: queryLogTagAutocompleteValues(tagKey: $tagKey, duration: $duration)
  }
`
);
function registerTraceTagRoutes(app2, deps) {
  const auth = requireAuth(deps);
  function tagsKeysHandler(query) {
    return async (req, reply) => {
      const q = req.query;
      const m = q.windowMinutes ? Number(q.windowMinutes) : DEFAULT_WINDOW_MIN7;
      const duration = rollingWindow2(Number.isFinite(m) ? m : DEFAULT_WINDOW_MIN7);
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const env = await graphqlPost(opts, query, { duration });
        return reply.send({ keys: env.keys ?? [], generatedAt: Date.now() });
      } catch (err) {
        return reply.code(200).send({
          keys: [],
          generatedAt: Date.now(),
          error: err instanceof Error ? err.message : String(err)
        });
      }
    };
  }
  function tagsValuesHandler(query) {
    return async (req, reply) => {
      const q = req.query;
      const key = (q.key ?? "").trim();
      if (!key) return reply.code(400).send({ error: "missing_key" });
      const m = q.windowMinutes ? Number(q.windowMinutes) : DEFAULT_WINDOW_MIN7;
      const duration = rollingWindow2(Number.isFinite(m) ? m : DEFAULT_WINDOW_MIN7);
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const env = await graphqlPost(opts, query, {
          tagKey: key,
          duration
        });
        return reply.send({ key, values: env.values ?? [], generatedAt: Date.now() });
      } catch (err) {
        return reply.code(200).send({
          key,
          values: [],
          generatedAt: Date.now(),
          error: err instanceof Error ? err.message : String(err)
        });
      }
    };
  }
  app2.get("/api/trace-tags/keys", { preHandler: auth }, tagsKeysHandler(TRACE_KEYS_QUERY));
  app2.get("/api/trace-tags/values", { preHandler: auth }, tagsValuesHandler(TRACE_VALUES_QUERY));
  app2.get("/api/log-tags/keys", { preHandler: auth }, tagsKeysHandler(LOG_KEYS_QUERY));
  app2.get("/api/log-tags/values", { preHandler: auth }, tagsValuesHandler(LOG_VALUES_QUERY));
}

// src/http/query/zipkin.ts
function summariseTrace(spans) {
  const root = spans.find((s) => !s.parentId) ?? spans.slice().sort((a, b) => (a.timestamp ?? 0) - (b.timestamp ?? 0))[0] ?? null;
  const errorCount = spans.reduce((n, s) => {
    const t = s.tags ?? {};
    if (t["error"] != null || t["http.status_code"]?.startsWith("5") || t["otel.status_code"] === "ERROR") {
      return n + 1;
    }
    return n;
  }, 0);
  return {
    traceId: root?.traceId ?? (spans[0]?.traceId ?? ""),
    rootName: root?.name ?? null,
    rootService: root?.localEndpoint?.serviceName ?? null,
    timestamp: root?.timestamp ?? null,
    duration: root?.duration ?? null,
    spanCount: spans.length,
    errorCount
  };
}
async function zipkinFetch2(cfg, fetchFn, path2, query) {
  const f = fetchFn ?? globalThis.fetch.bind(globalThis);
  const base = cfg.oap.zipkinUrl.replace(/\/$/, "");
  const url = new URL(base + path2);
  if (query) {
    for (const [k, v] of Object.entries(query)) {
      if (v === void 0 || v === null || v === "") continue;
      url.searchParams.set(k, String(v));
    }
  }
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), cfg.oap.timeoutMs);
  const headers = { accept: "application/json" };
  if (cfg.oap.auth) {
    headers.authorization = basicAuthHeader(cfg.oap.auth.username, cfg.oap.auth.password);
  }
  try {
    const res = await f(url.toString(), {
      method: "GET",
      headers,
      signal: controller.signal
    });
    const text = await res.text();
    let body;
    try {
      body = text ? JSON.parse(text) : null;
    } catch {
      body = text;
    }
    return { status: res.status, body };
  } finally {
    clearTimeout(timer);
  }
}
function registerZipkinRoutes(app2, deps) {
  const auth = requireAuth(deps);
  app2.get("/api/zipkin/services", { preHandler: auth }, async (_req, reply) => {
    try {
      const { status, body } = await zipkinFetch2(deps.config.current, deps.fetch, "/api/v2/services");
      return reply.code(status).send(body);
    } catch (err) {
      return reply.code(200).send({ services: [], reachable: false, error: err instanceof Error ? err.message : String(err) });
    }
  });
  app2.get("/api/zipkin/spans", { preHandler: auth }, async (req, reply) => {
    const q = req.query;
    if (!q.serviceName) return reply.code(400).send({ error: "missing_serviceName" });
    try {
      const { status, body } = await zipkinFetch2(deps.config.current, deps.fetch, "/api/v2/spans", {
        serviceName: q.serviceName
      });
      return reply.code(status).send(body);
    } catch (err) {
      return reply.code(200).send({ spans: [], reachable: false, error: String(err) });
    }
  });
  app2.get("/api/zipkin/remote-services", { preHandler: auth }, async (req, reply) => {
    const q = req.query;
    if (!q.serviceName) return reply.code(400).send({ error: "missing_serviceName" });
    try {
      const { status, body } = await zipkinFetch2(
        deps.config.current,
        deps.fetch,
        "/api/v2/remoteServices",
        { serviceName: q.serviceName }
      );
      return reply.code(status).send(body);
    } catch (err) {
      return reply.code(200).send({ remoteServices: [], reachable: false, error: String(err) });
    }
  });
  app2.get("/api/zipkin/traces", { preHandler: auth }, async (req, reply) => {
    const q = req.query;
    const limit = q.limit ? Math.max(1, Math.min(200, Number(q.limit))) : 30;
    const lookback = q.lookback ? Number(q.lookback) : 30 * 6e4;
    const endTs = q.endTs ? Number(q.endTs) : Date.now();
    try {
      const { status, body } = await zipkinFetch2(
        deps.config.current,
        deps.fetch,
        "/api/v2/traces",
        {
          serviceName: q.serviceName,
          remoteServiceName: q.remoteServiceName,
          spanName: q.spanName,
          annotationQuery: q.annotationQuery,
          minDuration: q.minDuration,
          maxDuration: q.maxDuration,
          endTs,
          lookback,
          limit
        }
      );
      const raw = Array.isArray(body) ? body : [];
      const summaries = raw.map(summariseTrace);
      const response = {
        source: "zipkin",
        traces: summaries,
        reachable: true
      };
      return reply.code(status).send(response);
    } catch (err) {
      const response = {
        source: "zipkin",
        traces: [],
        reachable: false,
        error: err instanceof Error ? err.message : String(err)
      };
      return reply.code(200).send(response);
    }
  });
  app2.get(
    "/api/zipkin/trace/:traceId",
    { preHandler: auth },
    async (req, reply) => {
      const { traceId } = req.params;
      if (!traceId || !/^[0-9a-fA-F]+$/.test(traceId)) {
        return reply.code(400).send({ error: "invalid_trace_id" });
      }
      try {
        const { status, body } = await zipkinFetch2(
          deps.config.current,
          deps.fetch,
          `/api/v2/trace/${encodeURIComponent(traceId)}`
        );
        const detail = {
          source: "zipkin",
          traceId,
          spans: Array.isArray(body) ? body : [],
          reachable: status !== 404,
          ...status === 404 ? { error: "trace not found" } : {}
        };
        return reply.code(status === 404 ? 200 : status).send(detail);
      } catch (err) {
        const detail = {
          source: "zipkin",
          traceId,
          spans: [],
          reachable: false,
          error: err instanceof Error ? err.message : String(err)
        };
        return reply.code(200).send(detail);
      }
    }
  );
  app2.get("/api/zipkin/autocomplete/keys", { preHandler: auth }, async (_req, reply) => {
    try {
      const { status, body } = await zipkinFetch2(
        deps.config.current,
        deps.fetch,
        "/api/v2/autocompleteKeys"
      );
      return reply.code(status).send(body);
    } catch (err) {
      return reply.code(200).send({ keys: [], reachable: false, error: String(err) });
    }
  });
  app2.get("/api/zipkin/autocomplete/values", { preHandler: auth }, async (req, reply) => {
    const q = req.query;
    if (!q.key) return reply.code(400).send({ error: "missing_key" });
    try {
      const { status, body } = await zipkinFetch2(
        deps.config.current,
        deps.fetch,
        "/api/v2/autocompleteValues",
        { key: q.key }
      );
      return reply.code(status).send(body);
    } catch (err) {
      return reply.code(200).send({ values: [], reachable: false, error: String(err) });
    }
  });
  app2.get("/api/zipkin/traceMany", { preHandler: auth }, async (req, reply) => {
    const q = req.query;
    const ids = (q.traceIds ?? "").split(",").map((s) => s.trim()).filter(Boolean);
    if (ids.length === 0) return reply.code(400).send({ error: "missing_traceIds" });
    try {
      const { status, body } = await zipkinFetch2(
        deps.config.current,
        deps.fetch,
        "/api/v2/traceMany",
        { traceIds: ids.join(",") }
      );
      return reply.code(status).send({
        traces: Array.isArray(body) ? body : [],
        reachable: true
      });
    } catch (err) {
      return reply.code(200).send({
        traces: [],
        reachable: false,
        error: err instanceof Error ? err.message : String(err)
      });
    }
  });
}

// src/http/query/log.ts
var DEFAULT_WINDOW_MIN8 = 30;
function fmtMinute8(d) {
  const yyyy = d.getUTCFullYear();
  const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
  const dd = String(d.getUTCDate()).padStart(2, "0");
  const HH = String(d.getUTCHours()).padStart(2, "0");
  const MM = String(d.getUTCMinutes()).padStart(2, "0");
  return `${yyyy}-${mm}-${dd} ${HH}${MM}`;
}
function defaultWindow6(minutes, explicit) {
  if (explicit?.startTime && explicit.endTime) {
    return { start: explicit.startTime, end: explicit.endTime };
  }
  const m = Number.isFinite(minutes) && minutes > 0 ? Math.min(60 * 24 * 7, Math.round(minutes)) : DEFAULT_WINDOW_MIN8;
  const end = /* @__PURE__ */ new Date();
  const start = new Date(end.getTime() - m * 6e4);
  return { start: fmtMinute8(start), end: fmtMinute8(end) };
}
var LIST_SERVICES_FOR_RESOLVE6 = (
  /* GraphQL */
  `
  query ListServicesForLogs($layer: String!) {
    services: listServices(layer: $layer) {
      id
      name
    }
  }
`
);
var QUERY_LOGS = (
  /* GraphQL */
  `
  query QueryLogs($condition: LogQueryCondition) {
    data: queryLogs(condition: $condition) {
      logs {
        serviceName
        serviceId
        serviceInstanceName
        serviceInstanceId
        endpointName
        endpointId
        traceId
        timestamp
        contentType
        content
        tags { key value }
      }
    }
  }
`
);
var OAP_SERVICE_ID_RE2 = /^[A-Za-z0-9+/=]+\.\d+$/;
async function resolveServiceId2(opts, layer, serviceArg) {
  if (!serviceArg) return null;
  if (OAP_SERVICE_ID_RE2.test(serviceArg)) return serviceArg;
  const data = await graphqlPost(
    opts,
    LIST_SERVICES_FOR_RESOLVE6,
    { layer: layer.toUpperCase() }
  );
  return data.services.find((s) => s.name === serviceArg)?.id ?? data.services.find((s) => s.id === serviceArg)?.id ?? null;
}
function registerLogRoute(app2, deps) {
  const auth = requireAuth(deps);
  app2.post(
    "/api/layer/:key/logs",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      const layerKey = params.key;
      if (!layerKey || !/^[a-z0-9_]+$/i.test(layerKey)) {
        return reply.code(400).send({ error: "invalid_layer_key" });
      }
      const body = req.body ?? {};
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      const window = defaultWindow6(body.windowMinutes, { startTime: body.startTime, endTime: body.endTime });
      let serviceId = body.serviceId ?? null;
      if (!serviceId && body.service) {
        try {
          serviceId = await resolveServiceId2(opts, layerKey, body.service);
        } catch (err) {
          return reply.send({
            generatedAt: Date.now(),
            query: body,
            total: 0,
            logs: [],
            reachable: false,
            error: err instanceof Error ? err.message : String(err)
          });
        }
      }
      const condition = {
        ...serviceId ? { serviceId } : {},
        ...body.serviceInstanceId ? { serviceInstanceId: body.serviceInstanceId } : {},
        ...body.endpointId ? { endpointId: body.endpointId } : {},
        ...body.traceId ? { relatedTrace: { traceId: body.traceId } } : {},
        ...body.keywordsOfContent && body.keywordsOfContent.length > 0 ? { keywordsOfContent: body.keywordsOfContent } : {},
        ...body.tags && body.tags.length > 0 ? { tags: body.tags } : {},
        queryDuration: { start: window.start, end: window.end, step: "MINUTE" },
        paging: {
          pageNum: body.page ?? 1,
          pageSize: body.pageSize ?? 50
        }
      };
      try {
        const env = await graphqlPost(opts, QUERY_LOGS, { condition });
        const logs = (env.data?.logs ?? []).map((r) => ({
          serviceName: r.serviceName ?? null,
          serviceId: r.serviceId ?? null,
          serviceInstanceName: r.serviceInstanceName ?? null,
          serviceInstanceId: r.serviceInstanceId ?? null,
          endpointName: r.endpointName ?? null,
          endpointId: r.endpointId ?? null,
          traceId: r.traceId ?? null,
          timestamp: r.timestamp,
          contentType: r.contentType,
          content: r.content,
          tags: r.tags ?? []
        }));
        return reply.send({
          generatedAt: Date.now(),
          query: body,
          total: logs.length,
          logs,
          reachable: true
        });
      } catch (err) {
        return reply.send({
          generatedAt: Date.now(),
          query: body,
          total: 0,
          logs: [],
          reachable: false,
          error: err instanceof Error ? err.message : String(err)
        });
      }
    }
  );
  app2.post(
    "/api/layer/:key/logs/facets",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      const layerKey = params.key;
      if (!layerKey || !/^[a-z0-9_]+$/i.test(layerKey)) {
        return reply.code(400).send({ error: "invalid_layer_key" });
      }
      const body = req.body ?? {};
      const sampleSize = Math.max(50, Math.min(1e3, body.sampleSize ?? 200));
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      const window = defaultWindow6(body.windowMinutes, { startTime: body.startTime, endTime: body.endTime });
      let serviceId = body.serviceId ?? null;
      if (!serviceId && body.service) {
        try {
          serviceId = await resolveServiceId2(opts, layerKey, body.service);
        } catch (err) {
          return reply.send({
            generatedAt: Date.now(),
            total: 0,
            sampled: 0,
            level: { error: 0, warn: 0, info: 0, debug: 0, other: 0 },
            services: [],
            reachable: false,
            error: err instanceof Error ? err.message : String(err)
          });
        }
      }
      const condition = {
        ...serviceId ? { serviceId } : {},
        ...body.serviceInstanceId ? { serviceInstanceId: body.serviceInstanceId } : {},
        ...body.endpointId ? { endpointId: body.endpointId } : {},
        ...body.traceId ? { relatedTrace: { traceId: body.traceId } } : {},
        ...body.keywordsOfContent && body.keywordsOfContent.length > 0 ? { keywordsOfContent: body.keywordsOfContent } : {},
        // Facet sample intentionally ignores level/tag filters so the
        // counts show the unfiltered distribution; the user picks a
        // level from the breakdown.
        queryDuration: { start: window.start, end: window.end, step: "MINUTE" },
        paging: { pageNum: 1, pageSize: sampleSize }
      };
      try {
        const env = await graphqlPost(opts, QUERY_LOGS, { condition });
        const rows = env.data?.logs ?? [];
        const level = { error: 0, warn: 0, info: 0, debug: 0, other: 0 };
        const svcMap = /* @__PURE__ */ new Map();
        for (const r of rows) {
          const tag = (r.tags ?? []).find((t) => t.key.toLowerCase() === "level");
          const raw = (tag?.value ?? "").toLowerCase();
          if (raw.includes("error") || raw === "err" || raw === "fatal") level.error++;
          else if (raw.includes("warn")) level.warn++;
          else if (raw.includes("info")) level.info++;
          else if (raw.includes("debug") || raw.includes("trace")) level.debug++;
          else level.other++;
          const svc = r.serviceName ?? "(none)";
          svcMap.set(svc, (svcMap.get(svc) ?? 0) + 1);
        }
        const services = Array.from(svcMap.entries()).map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count).slice(0, 12);
        return reply.send({
          generatedAt: Date.now(),
          total: rows.length,
          sampled: rows.length,
          level,
          services,
          reachable: true
        });
      } catch (err) {
        return reply.send({
          generatedAt: Date.now(),
          total: 0,
          sampled: 0,
          level: { error: 0, warn: 0, info: 0, debug: 0, other: 0 },
          services: [],
          reachable: false,
          error: err instanceof Error ? err.message : String(err)
        });
      }
    }
  );
}

// src/http/query/dashboard.ts
import { z as z4 } from "zod";

// src/logic/dashboard/defaults.ts
var SERVICE_WIDGETS = [
  {
    id: "apdex",
    title: "Service Apdex",
    tip: "User satisfaction score on a 0\u20131 scale. service_apdex is integer-times-10000 server-side; expression divides to bring it into a familiar 0\u20131 range.",
    type: "card",
    expressions: ["avg(service_apdex)/10000"],
    x: 0,
    y: 0,
    w: 8,
    h: 5
  },
  {
    id: "sla",
    title: "Success Rate",
    tip: "Percentage of successful requests. Source field is integer-times-100.",
    type: "card",
    unit: "%",
    expressions: ["avg(service_sla)/100"],
    x: 8,
    y: 0,
    w: 8,
    h: 5
  },
  {
    id: "traffic",
    title: "Traffic",
    tip: "Requests per minute (cpm). For HTTP / gRPC / RPC services this reflects request throughput.",
    type: "card",
    unit: "rpm",
    expressions: ["avg(service_cpm)"],
    x: 16,
    y: 0,
    w: 8,
    h: 5
  },
  {
    id: "resp_time",
    title: "Service Avg Response Time",
    tip: "Mean latency across all calls in the window.",
    type: "line",
    unit: "ms",
    expressions: ["service_resp_time"],
    x: 0,
    y: 5,
    w: 12,
    h: 14
  },
  {
    id: "percentile",
    title: "Service Response Time Percentile",
    tip: "Latency at p50 / p75 / p90 / p95 / p99 \u2014 useful for tail behavior.",
    type: "line",
    unit: "ms",
    expressions: [
      "service_percentile{p='50'}",
      "service_percentile{p='75'}",
      "service_percentile{p='90'}",
      "service_percentile{p='95'}",
      "service_percentile{p='99'}"
    ],
    x: 12,
    y: 5,
    w: 12,
    h: 14
  },
  {
    id: "traffic_line",
    title: "Traffic (line)",
    type: "line",
    unit: "rpm",
    expressions: ["service_cpm"],
    x: 0,
    y: 19,
    w: 12,
    h: 12
  },
  {
    id: "sla_line",
    title: "Success Rate (line)",
    type: "line",
    unit: "%",
    expressions: ["service_sla/100"],
    x: 12,
    y: 19,
    w: 12,
    h: 12
  }
];
var BROWSER_WIDGETS = [
  {
    id: "app_pv",
    title: "Page Views",
    type: "card",
    expressions: ["avg(browser_app_pv)"],
    x: 0,
    y: 0,
    w: 8,
    h: 5
  },
  {
    id: "app_error_rate",
    title: "Page Error Rate",
    type: "card",
    unit: "%",
    expressions: ["avg(browser_app_page_error_rate)/100"],
    x: 8,
    y: 0,
    w: 8,
    h: 5
  },
  {
    id: "app_error_sum",
    title: "Page Errors",
    type: "card",
    expressions: ["avg(browser_app_error_sum)"],
    x: 16,
    y: 0,
    w: 8,
    h: 5
  },
  {
    id: "app_pv_line",
    title: "Page Views (over time)",
    type: "line",
    expressions: ["browser_app_pv"],
    x: 0,
    y: 5,
    w: 12,
    h: 12
  },
  {
    id: "app_error_line",
    title: "Page Errors (over time)",
    type: "line",
    expressions: ["browser_app_error_sum"],
    x: 12,
    y: 5,
    w: 12,
    h: 12
  }
];
var GENERIC_WIDGETS = [
  {
    id: "cpm",
    title: "Calls per minute",
    type: "card",
    unit: "calls / min",
    expressions: ["avg(service_cpm)"],
    x: 0,
    y: 0,
    w: 8,
    h: 5
  },
  {
    id: "sla",
    title: "Success Rate",
    type: "card",
    unit: "%",
    expressions: ["avg(service_sla)/100"],
    x: 8,
    y: 0,
    w: 8,
    h: 5
  },
  {
    id: "resp",
    title: "Avg Response Time",
    type: "card",
    unit: "ms",
    expressions: ["avg(service_resp_time)"],
    x: 16,
    y: 0,
    w: 8,
    h: 5
  },
  {
    id: "cpm_line",
    title: "Calls per minute (line)",
    type: "line",
    unit: "calls / min",
    expressions: ["service_cpm"],
    x: 0,
    y: 5,
    w: 24,
    h: 12
  }
];
function defaultWidgetsFor(layerKey) {
  const tpl = getLayerTemplate(layerKey);
  if (tpl && tpl.widgets && tpl.widgets.length > 0) return tpl.widgets;
  const k = layerKey.toUpperCase();
  if (k === "MESH_CP" || k === "MESH_DP") return SERVICE_WIDGETS;
  if (k === "BROWSER") return BROWSER_WIDGETS;
  return GENERIC_WIDGETS;
}

// src/http/query/dashboard.ts
var widgetSchema = z4.object({
  id: z4.string().min(1),
  title: z4.string(),
  tip: z4.string().optional(),
  type: z4.enum(["card", "line", "top", "record", "table"]),
  // Bumped from 8 to 16: JVM Memory Detail carries 11 pool metrics
  // (code cache + young/old/survivor/permgen/metaspace + z-heap +
  // compressed class space + 3 segmented codeheaps), and a few of the
  // language families approach the same range. 16 gives headroom for
  // future relabeled bundles without blowing the cap.
  expressions: z4.array(z4.string().min(1)).min(1).max(16),
  expressionLabels: z4.array(z4.string()).max(16).optional(),
  expressionUnits: z4.array(z4.string()).max(16).optional(),
  expressionAxes: z4.array(z4.number().int().min(0).max(1)).max(16).optional(),
  unit: z4.string().optional(),
  tableHeaders: z4.tuple([z4.string(), z4.string()]).optional(),
  showTableValues: z4.boolean().optional(),
  span: z4.number().int().min(1).max(12).optional(),
  rowSpan: z4.number().int().min(1).max(64).optional(),
  visibleWhen: z4.string().optional(),
  layerScope: z4.boolean().optional(),
  // Legacy x/y/w/h kept optional for back-compat.
  x: z4.number().int().min(0).optional(),
  y: z4.number().int().min(0).optional(),
  w: z4.number().int().positive().optional(),
  h: z4.number().int().positive().optional()
});
var scopeSchema = z4.enum([
  "service",
  "instance",
  "endpoint",
  "dependency",
  "topology",
  "trace",
  "logs",
  "traceProfiling",
  "ebpfProfiling",
  "asyncProfiling"
]);
var bodySchema2 = z4.object({
  service: z4.string().optional(),
  /** Selected instance name. Honored only when `scope === 'instance'`
   *  (or any instance-derived scope) — the BFF flips the MQE entity to
   *  `{ scope: ServiceInstance, serviceName, serviceInstanceName }` so
   *  every widget on the Instance page evaluates against the chosen
   *  pair instead of the parent service. */
  serviceInstance: z4.string().optional(),
  /** Selected endpoint name, analogous to `serviceInstance` but for
   *  the Endpoint page. Switches the entity to
   *  `{ scope: Endpoint, serviceName, endpointName }`. */
  endpoint: z4.string().optional(),
  // Hard cap per request — protects OAP's storage page-size cliffs
  // (CLAUDE.md warns about backend-specific thresholds). The UI is
  // responsible for chunking widget sets larger than this across
  // multiple requests; the BFF refuses oversized bodies up-front so
  // an accidentally-huge template never reaches OAP.
  widgets: z4.array(widgetSchema).max(40).optional(),
  scope: scopeSchema.optional(),
  /** Global time-range, forwarded by the SPA's time picker. When all
   *  three are present the BFF queries OAP at the requested precision
   *  and window; otherwise it falls back to the last-hour MINUTE
   *  default. `step` must match OAP's downsampling tiers and drives the
   *  date-string format (verifyDateTimeString rejects a mismatch). */
  step: z4.enum(["MINUTE", "HOUR", "DAY"]).optional(),
  startMs: z4.number().int().positive().optional(),
  endMs: z4.number().int().positive().optional()
});
var LIST_FIRST_SERVICE = (
  /* GraphQL */
  `
  query FirstService($layer: String!) {
    services: listServices(layer: $layer) { id name normal }
  }
`
);
var LIST_FIRST_INSTANCE = (
  /* GraphQL */
  `
  query FirstInstance($serviceId: ID!, $duration: Duration!) {
    instances: listInstances(serviceId: $serviceId, duration: $duration) {
      id name
    }
  }
`
);
var FIND_FIRST_ENDPOINT = (
  /* GraphQL */
  `
  query FirstEndpoint($serviceId: ID!, $duration: Duration!) {
    endpoints: findEndpoint(serviceId: $serviceId, keyword: "", limit: 1, duration: $duration) {
      id name
    }
  }
`
);
var DEFAULT_WINDOW_MIN9 = 60;
function pad(n) {
  return String(n).padStart(2, "0");
}
function fmtMinute9(d) {
  return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}`;
}
function fmtHour(d) {
  return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}`;
}
function fmtDay(d) {
  return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
}
function fmtForStep(step, d) {
  if (step === "DAY") return fmtDay(d);
  if (step === "HOUR") return fmtHour(d);
  return fmtMinute9(d);
}
function defaultWindow7() {
  const end = /* @__PURE__ */ new Date();
  end.setUTCSeconds(0, 0);
  const start = new Date(end.getTime() - DEFAULT_WINDOW_MIN9 * 6e4);
  return { start: fmtMinute9(start), end: fmtMinute9(end), step: "MINUTE" };
}
function windowFromRange(step, startMs, endMs) {
  if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs) return null;
  return {
    start: fmtForStep(step, new Date(startMs)),
    end: fmtForStep(step, new Date(endMs)),
    step
  };
}
function buildFragment(alias2, expression, serviceName, normal, w, opts = {}) {
  let entity;
  if (opts.layerScope) {
    entity = "{ scope: All }";
  } else if (opts.serviceInstanceName) {
    entity = `{ scope: ServiceInstance, serviceName: ${JSON.stringify(serviceName)}, serviceInstanceName: ${JSON.stringify(opts.serviceInstanceName)}, normal: ${normal ? "true" : "false"} }`;
  } else if (opts.endpointName) {
    entity = `{ scope: Endpoint, serviceName: ${JSON.stringify(serviceName)}, endpointName: ${JSON.stringify(opts.endpointName)}, normal: ${normal ? "true" : "false"} }`;
  } else {
    entity = `{ scope: Service, serviceName: ${JSON.stringify(serviceName)}, normal: ${normal ? "true" : "false"} }`;
  }
  return `${alias2}: execExpression(
      expression: ${JSON.stringify(expression)},
      entity: ${entity},
      duration: { start: ${JSON.stringify(w.start)}, end: ${JSON.stringify(w.end)}, step: ${w.step} }
    ) {
      type error
      results {
        metric { labels { key value } }
        values { id value owner { scope serviceName serviceInstanceName endpointName } }
      }
    }`;
}
function parseSeries(r) {
  if (!r || r.error) return null;
  const values = r.results?.[0]?.values ?? [];
  if (values.length === 0) return null;
  return values.map((v) => {
    if (v.value === null || v.value === void 0) return null;
    const n = Number(v.value);
    return Number.isFinite(n) ? n : null;
  });
}
function avgOf(series) {
  if (!series) return null;
  const xs = series.filter((v) => v !== null);
  if (xs.length === 0) return null;
  return xs.reduce((a, b) => a + b, 0) / xs.length;
}
function parseLabeledSeries(r, fallbackLabel) {
  if (!r || r.error) return null;
  const out = [];
  for (const rs of r.results ?? []) {
    const values = rs.values ?? [];
    if (values.length === 0) continue;
    const data = values.map((v) => {
      if (v.value === null || v.value === void 0) return null;
      const n = Number(v.value);
      return Number.isFinite(n) ? n : null;
    });
    const labels = rs.metric?.labels ?? [];
    const lbl = labels.length > 0 ? labels[labels.length - 1].value : fallbackLabel;
    out.push({ label: lbl, data });
  }
  return out.length > 0 ? out : null;
}
function parseTopList(r) {
  if (!r || r.error) return null;
  const values = r.results?.[0]?.values ?? [];
  if (values.length === 0) return null;
  const services = /* @__PURE__ */ new Set();
  for (const v of values) {
    if (v.owner?.serviceName) services.add(v.owner.serviceName);
  }
  const multiService = services.size > 1;
  return values.map((v) => {
    const o = v.owner;
    let name = "\u2014";
    if (o?.endpointName) {
      name = multiService && o.serviceName ? `${o.serviceName} \xB7 ${o.endpointName}` : o.endpointName;
    } else if (o?.serviceInstanceName) {
      name = multiService && o.serviceName ? `${o.serviceName} \xB7 ${o.serviceInstanceName}` : o.serviceInstanceName;
    } else if (o?.serviceName) {
      name = o.serviceName;
    } else if (v.id) {
      name = v.id;
    }
    const num = v.value !== null && v.value !== void 0 ? Number(v.value) : null;
    return { name, value: Number.isFinite(num) ? num : null };
  });
}
function parseTable(r) {
  if (!r || r.error) return null;
  const results = r.results ?? [];
  if (results.length === 0) return null;
  const rows = results.map((rs) => {
    const labels = (rs.metric?.labels ?? []).map((l) => ({ key: l.key, value: l.value }));
    let value = null;
    for (const v of rs.values ?? []) {
      if (v.value === null || v.value === void 0) continue;
      const n = Number(v.value);
      if (Number.isFinite(n)) value = n;
    }
    if (labels.length === 0 && rs.values?.[0]?.id) {
      labels.push({ key: "name", value: rs.values[0].id });
    }
    return { labels, value };
  });
  rows.sort(
    (a, b) => a.labels.map((l) => l.value).join("\xB7").localeCompare(b.labels.map((l) => l.value).join("\xB7"))
  );
  return rows;
}
function registerDashboardQueryRoute(app2, deps) {
  const auth = requireAuth(deps);
  app2.post(
    "/api/layer/:key/dashboard",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      const layerKey = params.key;
      if (!layerKey || !/^[a-z0-9_]+$/i.test(layerKey)) {
        return reply.code(400).send({ error: "invalid_layer_key" });
      }
      const parsed = bodySchema2.safeParse(req.body ?? {});
      if (!parsed.success) {
        return reply.code(400).send({ error: "invalid_body", detail: parsed.error.flatten() });
      }
      const mockTopRaw = req.query.mockTop;
      const mockTopN = mockTopRaw ? Math.max(0, Math.min(40, Number(mockTopRaw))) : 0;
      const scope = parsed.data.scope ?? "service";
      const tpl = getLayerTemplate(layerKey);
      const widgets = parsed.data.widgets ?? (tpl ? widgetsForScope(tpl, scope) : defaultWidgetsFor(layerKey));
      let serviceName = parsed.data.service ?? "";
      let serviceId = "";
      let normal = true;
      const cfgCurrent = deps.config.current;
      const opts = buildOapOpts(cfgCurrent, deps.fetch);
      const window = parsed.data.step && parsed.data.startMs && parsed.data.endMs ? windowFromRange(parsed.data.step, parsed.data.startMs, parsed.data.endMs) ?? defaultWindow7() : defaultWindow7();
      const baseResp = {
        layer: layerKey,
        service: serviceName || null,
        generatedAt: Date.now(),
        step: window.step,
        durationStart: window.start,
        durationEnd: window.end,
        widgets: [],
        reachable: true
      };
      try {
        const data2 = await graphqlPost(
          opts,
          LIST_FIRST_SERVICE,
          { layer: layerKey.toUpperCase() }
        );
        const all = data2.services ?? [];
        let picked;
        if (serviceName) {
          picked = all.find((s) => s.name === serviceName) ?? all.find((s) => s.id === serviceName);
          if (!picked) {
            return reply.send({
              ...baseResp,
              service: serviceName,
              widgets: widgets.map((w) => ({ id: w.id, error: `service "${serviceName}" not in layer` }))
            });
          }
        } else {
          picked = all[0];
          if (!picked) {
            return reply.send({
              ...baseResp,
              widgets: widgets.map((w) => ({ id: w.id, error: "no service in layer" }))
            });
          }
        }
        serviceName = picked.name;
        serviceId = picked.id;
        normal = picked.normal !== false;
        baseResp.service = serviceName;
      } catch (err) {
        return reply.send({
          ...baseResp,
          reachable: false,
          error: err instanceof Error ? err.message : String(err),
          widgets: widgets.map((w) => ({ id: w.id, error: "oap unreachable" }))
        });
      }
      let selectedInstance = parsed.data.serviceInstance ?? null;
      let selectedEndpoint = parsed.data.endpoint ?? null;
      if (scope === "instance" && !selectedInstance && serviceId) {
        try {
          const data2 = await graphqlPost(
            opts,
            LIST_FIRST_INSTANCE,
            {
              serviceId,
              duration: { start: window.start, end: window.end, step: window.step }
            }
          );
          selectedInstance = data2.instances?.[0]?.name ?? null;
        } catch {
        }
      }
      if (scope === "endpoint" && !selectedEndpoint && serviceId) {
        try {
          const data2 = await graphqlPost(
            opts,
            FIND_FIRST_ENDPOINT,
            {
              serviceId,
              duration: { start: window.start, end: window.end, step: window.step }
            }
          );
          selectedEndpoint = data2.endpoints?.[0]?.name ?? null;
        } catch {
        }
      }
      const MAX_WIDGETS_PER_BATCH = 6;
      const aliasMap = /* @__PURE__ */ new Map();
      const scopeHonorsInstance = scope === "instance";
      const scopeHonorsEndpoint = scope === "endpoint";
      const widgetChunks = [];
      for (let i = 0; i < widgets.length; i += MAX_WIDGETS_PER_BATCH) {
        widgetChunks.push(
          widgets.slice(i, i + MAX_WIDGETS_PER_BATCH).map((widget, idxInChunk) => ({
            widget,
            wIdx: i + idxInChunk
          }))
        );
      }
      const data = {};
      try {
        const chunkResults = await Promise.all(
          widgetChunks.map(async (chunk) => {
            const fragments = [];
            for (const { widget, wIdx } of chunk) {
              widget.expressions.forEach((expr, eIdx) => {
                const alias2 = `w${wIdx}_e${eIdx}`;
                aliasMap.set(alias2, { wIdx, eIdx });
                fragments.push(
                  buildFragment(alias2, expr, serviceName, normal, window, {
                    layerScope: widget.layerScope === true,
                    serviceInstanceName: widget.layerScope !== true && scopeHonorsInstance ? selectedInstance : null,
                    endpointName: widget.layerScope !== true && scopeHonorsEndpoint ? selectedEndpoint : null
                  })
                );
              });
            }
            if (fragments.length === 0) return {};
            const query = `query DashboardMqe { ${fragments.join("\n    ")} }`;
            return graphqlPost(opts, query);
          })
        );
        for (const chunk of chunkResults) {
          Object.assign(data, chunk);
        }
      } catch (err) {
        return reply.send({
          ...baseResp,
          reachable: false,
          error: err instanceof Error ? err.message : String(err),
          widgets: widgets.map((w) => ({ id: w.id, error: "mqe batch failed" }))
        });
      }
      const results = widgets.map((widget, wIdx) => {
        if (widget.type === "top") {
          const groups = [];
          widget.expressions.forEach((expr, eIdx) => {
            let items = parseTopList(data[`w${wIdx}_e${eIdx}`]);
            if (mockTopN > 0) {
              const current = items ?? [];
              const padCount = Math.max(0, mockTopN - current.length);
              if (padCount > 0) {
                const last = current[current.length - 1]?.value ?? 100;
                const seed = current[current.length - 1]?.name ?? "mock-service";
                const padded = Array.from({ length: padCount }, (_, i) => {
                  const idx = current.length + i + 1;
                  const decay = 1 - (i + 1) / (padCount + 2);
                  return {
                    name: `${seed} \xB7 mock-${idx}`,
                    value: typeof last === "number" ? Math.round(last * decay * 100) / 100 : 0
                  };
                });
                items = [...current, ...padded];
              }
              if (!items || items.length === 0) {
                items = Array.from({ length: mockTopN }, (_, i) => ({
                  name: `mock-entity-${i + 1}`,
                  value: Math.round((100 - i * 8) * 100) / 100
                }));
              }
            }
            if (!items) return;
            const label = widget.expressionLabels?.[eIdx] ?? expr;
            const unit = widget.expressionUnits?.[eIdx];
            groups.push({
              label,
              expression: expr,
              ...unit ? { unit } : {},
              items
            });
          });
          if (groups.length === 0) return { id: widget.id, error: "no data" };
          return {
            id: widget.id,
            topGroups: groups,
            topList: groups[0].items
          };
        }
        if (widget.type === "table") {
          const rows = parseTable(data[`w${wIdx}_e0`]);
          if (!rows) return { id: widget.id, error: "no data" };
          return { id: widget.id, table: rows };
        }
        if (widget.type === "record") {
          const first = parseTopList(data[`w${wIdx}_e0`]);
          if (!first) return { id: widget.id, error: "no data" };
          return {
            id: widget.id,
            records: first.map((r) => ({ name: r.name, value: r.value }))
          };
        }
        if (widget.type === "card") {
          const first = widget.expressions.map(
            (_, eIdx) => parseSeries(data[`w${wIdx}_e${eIdx}`])
          ).find((s) => s !== null);
          if (!first) return { id: widget.id, error: "no data" };
          return { id: widget.id, value: avgOf(first) };
        }
        const flat = [];
        widget.expressions.forEach((expr, eIdx) => {
          const labeled = parseLabeledSeries(data[`w${wIdx}_e${eIdx}`], expr);
          if (!labeled) return;
          const labelOverride = widget.expressionLabels?.[eIdx];
          const axis = widget.expressionAxes?.[eIdx];
          const unit = widget.expressionUnits?.[eIdx];
          for (const s of labeled) {
            flat.push({
              // When the operator supplied an explicit expressionLabel
              // for a single-series expression, prefer that over the
              // OAP-side relabels value. Multi-series MQEs (relabels)
              // already arrive with sensible labels.
              label: labeled.length === 1 && labelOverride ? labelOverride : s.label,
              data: s.data,
              ...axis !== void 0 ? { yAxisIndex: axis } : {},
              ...unit !== void 0 ? { unit } : {}
            });
          }
        });
        if (flat.length === 0) return { id: widget.id, error: "no data" };
        return { id: widget.id, series: flat };
      });
      return reply.send({ ...baseResp, widgets: results });
    }
  );
}

// src/http/query/alarms.ts
import { z as z5 } from "zod";
var tzCache = null;
var TZ_TTL_MS = 6e4;
var TIME_INFO_QUERY = (
  /* GraphQL */
  `
  query HorizonAlarmsTime {
    time: getTimeInfo {
      timezone
      currentTimestamp
    }
  }
`
);
async function getServerOffsetMinutes(config, fetchImpl) {
  const now = Date.now();
  if (tzCache && now - tzCache.fetchedAt < TZ_TTL_MS) return tzCache.offsetMinutes;
  try {
    const got = await graphqlPost(
      buildOapOpts(config.current, fetchImpl),
      TIME_INFO_QUERY
    );
    const raw = got.time?.timezone ?? "+0000";
    const m = /^([+-])(\d{2})(\d{2})$/.exec(raw);
    if (m) {
      const sign = m[1] === "-" ? -1 : 1;
      const h = parseInt(m[2], 10);
      const mi = parseInt(m[3], 10);
      const offset = sign * (h * 60 + mi);
      tzCache = { offsetMinutes: offset, fetchedAt: now };
      return offset;
    }
  } catch {
  }
  tzCache = { offsetMinutes: 0, fetchedAt: now };
  return 0;
}
function fmtSecond(epochMs, offsetMinutes) {
  const shifted = new Date(epochMs + offsetMinutes * 6e4);
  const y = shifted.getUTCFullYear();
  const mo = String(shifted.getUTCMonth() + 1).padStart(2, "0");
  const d = String(shifted.getUTCDate()).padStart(2, "0");
  const h = String(shifted.getUTCHours()).padStart(2, "0");
  const mi = String(shifted.getUTCMinutes()).padStart(2, "0");
  const s = String(shifted.getUTCSeconds()).padStart(2, "0");
  return `${y}-${mo}-${d} ${h}${mi}${s}`;
}
var GET_ALARM_QUERY = (
  /* GraphQL */
  `
  query HorizonGetAlarm(
    $duration: Duration!
    $scope: Scope
    $keyword: String
    $paging: Pagination!
    $tags: [AlarmTag]
  ) {
    getAlarm(duration: $duration, scope: $scope, keyword: $keyword, paging: $paging, tags: $tags) {
      msgs {
        id startTime recoveryTime scope name message
        tags { key value }
        snapshot {
          expression
          metrics {
            name
            results {
              metric { labels { key value } }
              values { id value traceID }
            }
          }
        }
      }
    }
  }
`
);
var QUERY_ALARMS_QUERY = (
  /* GraphQL */
  `
  query HorizonQueryAlarms($condition: AlarmQueryCondition!) {
    queryAlarms(condition: $condition) {
      msgs {
        id startTime recoveryTime scope name message
        tags { key value }
        snapshot {
          expression
          metrics {
            name
            results {
              metric { labels { key value } }
              values { id value traceID }
            }
          }
        }
      }
    }
  }
`
);
var COUNT_GET_ALARM_QUERY = (
  /* GraphQL */
  `
  query HorizonCountGetAlarm($duration: Duration!, $paging: Pagination!) {
    getAlarm(duration: $duration, paging: $paging) { msgs { id startTime recoveryTime } }
  }
`
);
var COUNT_QUERY_ALARMS_QUERY = (
  /* GraphQL */
  `
  query HorizonCountQueryAlarms($condition: AlarmQueryCondition!) {
    queryAlarms(condition: $condition) { msgs { id startTime recoveryTime } }
  }
`
);
var LIST_SERVICES_QUERY2 = (
  /* GraphQL */
  `
  query HorizonAlarmServices($layer: String!) {
    listServices(layer: $layer) { name normal }
  }
`
);
var WINDOW_CAP_MS = 4 * 60 * 6e4;
var LIST_PAGE_SIZE_CAP = 500;
var COUNT_FETCH_CAP = 200;
var alarmsQuerySchema = z5.object({
  startTime: z5.coerce.number().int().positive(),
  endTime: z5.coerce.number().int().positive(),
  /** Legacy-mode only. Ignored in new mode (use `layer` + entity
   *  fields instead). */
  scope: z5.string().optional(),
  keyword: z5.string().optional(),
  pageNum: z5.coerce.number().int().min(1).default(1),
  pageSize: z5.coerce.number().int().min(1).max(LIST_PAGE_SIZE_CAP).default(LIST_PAGE_SIZE_CAP),
  /** New-mode only. Maps to `condition.layer` (a single String on the
   *  OAP side — an alarm record is persisted with one layer). */
  layer: z5.string().optional(),
  /** New-mode only. Combined with `instance` / `endpoint` to build a
   *  single `Entity` filter. When absent, no entity narrowing. */
  service: z5.string().optional(),
  instance: z5.string().optional(),
  endpoint: z5.string().optional()
});
var countQuerySchema = z5.object({
  startTime: z5.coerce.number().int().positive(),
  endTime: z5.coerce.number().int().positive()
});
function buildEntity(q) {
  if (!q.service) return null;
  const base = { scope: "Service", serviceName: q.service, normal: true };
  if (q.endpoint && !q.instance) {
    return { ...base, scope: "Endpoint", endpointName: q.endpoint };
  }
  if (q.instance) {
    return { ...base, scope: "ServiceInstance", serviceInstanceName: q.instance };
  }
  return base;
}
async function tagWithLayer(msgsRaw, serviceLayer2) {
  const layerIdx = await serviceLayer2.get();
  return msgsRaw.map((m) => {
    const candidates = [m.name, m.name.split("::")[0], m.name.split(" ")[0]];
    let layerKey = null;
    for (const cand of candidates) {
      const hit = layerIdx.byName.get(cand.toLowerCase());
      if (hit) {
        layerKey = hit;
        break;
      }
    }
    return { ...m, layerKey };
  });
}
function registerAlarmsQueryRoutes(app2, deps) {
  const auth = requireAuth(deps);
  const serviceLayer2 = deps.serviceLayer;
  app2.get("/api/alarms", { preHandler: auth }, async (req, reply) => {
    const parsed = alarmsQuerySchema.safeParse(req.query);
    if (!parsed.success) {
      return reply.code(400).send({ error: "invalid_query", detail: parsed.error.flatten() });
    }
    const q = parsed.data;
    if (q.endTime <= q.startTime) return badRequest("endTime must be greater than startTime");
    if (q.endTime - q.startTime > WINDOW_CAP_MS) {
      return badRequest(`window exceeds ${WINDOW_CAP_MS / 6e4}m cap`);
    }
    const offset = await getServerOffsetMinutes(deps.config, deps.fetch);
    const start = fmtSecond(q.startTime, offset);
    const end = fmtSecond(q.endTime, offset);
    const opts = buildOapOpts(deps.config.current, deps.fetch);
    const caps = await getOapCapabilities(deps.config.current, deps.fetch);
    let msgsRaw;
    try {
      if (caps.queryAlarms) {
        const condition = {
          duration: { start, end, step: "SECOND" },
          paging: { pageNum: q.pageNum, pageSize: q.pageSize }
        };
        if (q.keyword) condition.keyword = q.keyword;
        if (q.layer) condition.layer = q.layer;
        const entity = buildEntity(q);
        if (entity) condition.entities = [entity];
        const raw = await graphqlPost(opts, QUERY_ALARMS_QUERY, { condition });
        msgsRaw = raw.queryAlarms?.msgs ?? [];
      } else {
        const variables = {
          duration: { start, end, step: "SECOND" },
          paging: { pageNum: q.pageNum, pageSize: q.pageSize }
        };
        if (q.scope) variables.scope = q.scope;
        if (q.keyword) variables.keyword = q.keyword;
        const raw = await graphqlPost(opts, GET_ALARM_QUERY, variables);
        msgsRaw = raw.getAlarm?.msgs ?? [];
      }
    } catch (err) {
      return reply.code(502).send({
        error: "oap_unreachable",
        message: err instanceof Error ? err.message : String(err)
      });
    }
    const tagged = await tagWithLayer(msgsRaw, serviceLayer2);
    const body = {
      total: tagged.length,
      pageNum: q.pageNum,
      pageSize: q.pageSize,
      truncated: tagged.length >= q.pageSize,
      generatedAt: Date.now(),
      msgs: tagged
    };
    return reply.send(body);
  });
  app2.get(
    "/api/alarms/count",
    { preHandler: auth },
    async (req, reply) => {
      const parsed = countQuerySchema.safeParse(req.query);
      if (!parsed.success) {
        return reply.code(400).send({ error: "invalid_query", detail: parsed.error.flatten() });
      }
      const q = parsed.data;
      if (q.endTime <= q.startTime) return badRequest("endTime must be greater than startTime");
      if (q.endTime - q.startTime > WINDOW_CAP_MS) {
        return badRequest(`window exceeds ${WINDOW_CAP_MS / 6e4}m cap`);
      }
      const offset = await getServerOffsetMinutes(deps.config, deps.fetch);
      const start = fmtSecond(q.startTime, offset);
      const end = fmtSecond(q.endTime, offset);
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      const caps = await getOapCapabilities(deps.config.current, deps.fetch);
      let rows;
      try {
        if (caps.queryAlarms) {
          const condition = {
            duration: { start, end, step: "SECOND" },
            paging: { pageNum: 1, pageSize: COUNT_FETCH_CAP }
          };
          const raw = await graphqlPost(opts, COUNT_QUERY_ALARMS_QUERY, { condition });
          rows = (raw.queryAlarms?.msgs ?? []).map((m) => ({
            id: m.id,
            startTime: m.startTime,
            recoveryTime: m.recoveryTime
          }));
        } else {
          const variables = {
            duration: { start, end, step: "SECOND" },
            paging: { pageNum: 1, pageSize: COUNT_FETCH_CAP }
          };
          const raw = await graphqlPost(opts, COUNT_GET_ALARM_QUERY, variables);
          rows = (raw.getAlarm?.msgs ?? []).map((m) => ({
            id: m.id,
            startTime: m.startTime,
            recoveryTime: m.recoveryTime
          }));
        }
      } catch (err) {
        return reply.code(502).send({
          error: "oap_unreachable",
          message: err instanceof Error ? err.message : String(err)
        });
      }
      const total = rows.length;
      const firing = rows.reduce((n, r) => n + (r.recoveryTime === null ? 1 : 0), 0);
      const latestByGroup = /* @__PURE__ */ new Map();
      for (const r of rows) {
        const cur = latestByGroup.get(r.id);
        if (!cur || r.startTime > cur.startTime) {
          latestByGroup.set(r.id, { startTime: r.startTime, recoveryTime: r.recoveryTime });
        }
      }
      const incidents = latestByGroup.size;
      let activeIncidents = 0;
      for (const v of latestByGroup.values()) {
        if (v.recoveryTime === null) activeIncidents += 1;
      }
      const body = {
        total,
        firing,
        incidents,
        activeIncidents,
        truncated: total >= COUNT_FETCH_CAP,
        startTime: q.startTime,
        endTime: q.endTime,
        generatedAt: Date.now()
      };
      return reply.send(body);
    }
  );
  app2.get(
    "/api/alarms/services",
    { preHandler: auth },
    async (req, reply) => {
      const layer = req.query?.layer;
      if (!layer || typeof layer !== "string") {
        return reply.code(400).send({ error: "missing_layer" });
      }
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const got = await graphqlPost(opts, LIST_SERVICES_QUERY2, { layer });
        const services = (got.listServices ?? []).filter((s) => typeof s?.name === "string" && s.name.length > 0).sort((a, b) => a.name.localeCompare(b.name));
        return reply.send({ layer, services });
      } catch (err) {
        return reply.code(502).send({
          error: "oap_unreachable",
          message: err instanceof Error ? err.message : String(err)
        });
      }
    }
  );
}

// src/logic/preflight/preflight.ts
var REQUIRED_MODULES = [
  {
    name: "admin-server",
    envVar: "SW_ADMIN_SERVER",
    required: true,
    affects: "Everything Studio does against the admin port. Without admin-server, the other three modules fail at boot with ModuleNotFoundException."
  },
  {
    name: "receiver-runtime-rule",
    envVar: "SW_RECEIVER_RUNTIME_RULE",
    required: true,
    affects: "DSL Management (Catalog, OAL catalog), Editor save/load, Cluster status rule matrix, Live debugger rule picker, and the Inspect drawer's source attribution."
  },
  {
    name: "dsl-debugging",
    envVar: "SW_DSL_DEBUGGING",
    required: true,
    affects: "Live debugger across MAL / LAL / OAL (start / poll / stop) and the DSL-debugging health pane on Cluster status."
  },
  {
    name: "inspect",
    envVar: "SW_INSPECT",
    required: true,
    affects: "The Inspect page \u2014 every /api/inspect/* call returns 404 inspect_not_enabled and the page shows a banner instead of the board."
  }
];
async function runPreflight(config, fetch) {
  const adminUrl = config.oap.adminUrl;
  const generatedAt = Date.now();
  const dump = await fetchConfigDump(adminUrl, fetch, config.oap.timeoutMs, config.oap.auth);
  if (!dump.ok) {
    return {
      adminUrl,
      adminReachable: false,
      adminError: dump.error,
      modules: REQUIRED_MODULES.map((m) => ({
        name: m.name,
        envVar: m.envVar,
        required: m.required,
        affects: m.affects,
        enabled: false
      })),
      dumpKeyCount: 0,
      generatedAt
    };
  }
  const keys = Object.keys(dump.body);
  const enabledPrefixes = /* @__PURE__ */ new Set();
  for (const k of keys) {
    const top = k.split(".", 1)[0];
    if (top) enabledPrefixes.add(top);
  }
  const modules = REQUIRED_MODULES.map((m) => ({
    name: m.name,
    envVar: m.envVar,
    required: m.required,
    affects: m.affects,
    enabled: enabledPrefixes.has(m.name)
  }));
  return {
    adminUrl,
    adminReachable: true,
    modules,
    dumpKeyCount: keys.length,
    generatedAt
  };
}
async function fetchConfigDump(adminUrl, fetch, timeoutMs, auth) {
  const url = `${adminUrl.replace(/\/$/, "")}/debugging/config/dump`;
  const headers = { Accept: "application/json" };
  if (auth) {
    const b64 = Buffer.from(`${auth.username}:${auth.password}`, "utf8").toString("base64");
    headers.authorization = `Basic ${b64}`;
  }
  let init = { method: "GET", headers };
  let timer = null;
  if (timeoutMs > 0) {
    const ctrl = new AbortController();
    timer = setTimeout(() => ctrl.abort(), timeoutMs);
    init = { ...init, signal: ctrl.signal };
  }
  try {
    const res = await fetch(url, init);
    if (!res.ok) {
      const text = (await res.text()).slice(0, 200);
      return { ok: false, error: `HTTP ${res.status}${text ? ` \u2014 ${text}` : ""}` };
    }
    const body = await res.json();
    return { ok: true, body };
  } catch (err) {
    return { ok: false, error: err instanceof Error ? err.message : String(err) };
  } finally {
    if (timer) clearTimeout(timer);
  }
}

// src/http/query/preflight.ts
function registerPreflightRoutes(app2, deps) {
  const auth = requireAuth(deps);
  app2.get(
    "/api/preflight",
    { preHandler: auth },
    async (_req, reply) => {
      const fetchImpl = deps.fetch ?? globalThis.fetch.bind(globalThis);
      const result = await runPreflight(deps.config.current, fetchImpl);
      return reply.send(result);
    }
  );
}

// src/http/query/ttl.ts
var TTL_QUERY = (
  /* GraphQL */
  `
  query HorizonOapTtl {
    records: getRecordsTTL {
      normal trace zipkinTrace log browserErrorLog
      coldNormal coldTrace coldZipkinTrace coldLog coldBrowserErrorLog
    }
    metrics: getMetricsTTL {
      minute hour day
      coldMinute coldHour coldDay
    }
  }
`
);
function registerTtlRoute(app2, deps) {
  const auth = requireAuth(deps);
  app2.get("/api/oap/ttl", { preHandler: auth }, async (_req, reply) => {
    const cfg = deps.config.current;
    try {
      const raw = await graphqlPost(buildOapOpts(cfg, deps.fetch), TTL_QUERY);
      const body = {
        reachable: true,
        records: raw.records ?? void 0,
        metrics: raw.metrics ?? void 0
      };
      return reply.send(body);
    } catch (err) {
      const body = {
        reachable: false,
        error: err instanceof Error ? err.message : String(err)
      };
      return reply.status(200).send(body);
    }
  });
}

// src/http/query/profile.ts
var LIST_SERVICES_FOR_RESOLVE7 = (
  /* GraphQL */
  `
  query ListServicesForProfileResolve($layer: String!) {
    services: listServices(layer: $layer) {
      id
      name
      normal
    }
  }
`
);
var GET_PROFILE_TASK_LIST = (
  /* GraphQL */
  `
  query getProfileTaskList($endpointName: String, $serviceId: ID) {
    taskList: getProfileTaskList(endpointName: $endpointName, serviceId: $serviceId) {
      id
      serviceId
      endpointName
      startTime
      duration
      minDurationThreshold
      dumpPeriod
      maxSamplingCount
      logs {
        id
        instanceId
        instanceName
        operationType
        operationTime
      }
    }
  }
`
);
var GET_PROFILE_TASK_SEGMENTS = (
  /* GraphQL */
  `
  query getProfileTaskSegmentList($taskID: ID!) {
    segmentList: getProfileTaskSegments(taskID: $taskID) {
      traceId
      instanceId
      instanceName
      endpointNames
      duration
      start
      spans {
        spanId
        parentSpanId
        segmentId
        refs { traceId parentSegmentId parentSpanId type }
        serviceCode
        serviceInstanceName
        startTime
        endTime
        endpointName
        type
        peer
        component
        isError
        layer
        tags { key value }
        logs { time data { key value } }
        profiled
      }
    }
  }
`
);
var GET_PROFILE_TASK_LOGS = (
  /* GraphQL */
  `
  query profileTaskLogs($taskID: String) {
    taskLogs: getProfileTaskLogs(taskID: $taskID) {
      id
      instanceId
      instanceName
      operationTime
      operationType
    }
  }
`
);
var GET_PROFILE_ANALYZE = (
  /* GraphQL */
  `
  query getProfileAnalyze($queries: [SegmentProfileAnalyzeQuery!]!) {
    analyze: getSegmentsProfileAnalyze(queries: $queries) {
      tip
      trees {
        elements {
          id
          parentId
          codeSignature
          duration
          durationChildExcluded
          count
        }
      }
    }
  }
`
);
var CREATE_PROFILE_TASK = (
  /* GraphQL */
  `
  mutation createProfileTask($creationRequest: ProfileTaskCreationRequest) {
    task: createProfileTask(creationRequest: $creationRequest) {
      id
      errorReason
    }
  }
`
);
function softError(payload, err) {
  payload.reachable = false;
  payload.error = err instanceof Error ? err.message : String(err);
  return payload;
}
async function resolveServiceId3(opts, layerKey, serviceArg) {
  if (/^[A-Za-z0-9+/=]+\.\d+$/.test(serviceArg)) return serviceArg;
  const data = await graphqlPost(opts, LIST_SERVICES_FOR_RESOLVE7, { layer: layerKey.toUpperCase() });
  const match = data.services.find((s) => s.name === serviceArg) ?? data.services.find((s) => s.id === serviceArg) ?? null;
  return match?.id ?? null;
}
function registerProfileRoutes(app2, deps) {
  const auth = requireAuth(deps);
  app2.get(
    "/api/layer/:key/profile/tasks",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      const layerKey = params.key;
      const q = req.query;
      const serviceArg = (q.service ?? "").trim();
      const endpointName = (q.endpoint ?? "").trim();
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      const payload = { tasks: [], reachable: true };
      if (!serviceArg) return reply.send(payload);
      try {
        const serviceId = await resolveServiceId3(opts, layerKey, serviceArg);
        if (!serviceId) return reply.send(payload);
        const data = await graphqlPost(
          opts,
          GET_PROFILE_TASK_LIST,
          { serviceId, endpointName: endpointName || "" }
        );
        payload.tasks = data.taskList ?? [];
        return reply.send(payload);
      } catch (err) {
        return reply.send(softError(payload, err));
      }
    }
  );
  app2.post(
    "/api/layer/:key/profile/tasks",
    { preHandler: auth },
    async (req, reply) => {
      const body = req.body;
      const payload = { reachable: true };
      if (!body || !body.serviceId) {
        payload.errorReason = "missing serviceId";
        return reply.send(payload);
      }
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const data = await graphqlPost(
          opts,
          CREATE_PROFILE_TASK,
          { creationRequest: body }
        );
        payload.id = data.task?.id;
        payload.errorReason = data.task?.errorReason;
        return reply.send(payload);
      } catch (err) {
        return reply.send(softError(payload, err));
      }
    }
  );
  app2.get(
    "/api/profile/tasks/:taskId/segments",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      const payload = { segments: [], reachable: true };
      try {
        const data = await graphqlPost(
          opts,
          GET_PROFILE_TASK_SEGMENTS,
          { taskID: params.taskId }
        );
        payload.segments = data.segmentList ?? [];
        return reply.send(payload);
      } catch (err) {
        return reply.send(softError(payload, err));
      }
    }
  );
  app2.get(
    "/api/profile/tasks/:taskId/logs",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      const payload = { logs: [], reachable: true };
      try {
        const data = await graphqlPost(
          opts,
          GET_PROFILE_TASK_LOGS,
          { taskID: params.taskId }
        );
        payload.logs = data.taskLogs ?? [];
        return reply.send(payload);
      } catch (err) {
        return reply.send(softError(payload, err));
      }
    }
  );
  app2.post(
    "/api/profile/analyze",
    { preHandler: auth },
    async (req, reply) => {
      const body = req.body;
      const payload = {
        tip: null,
        trees: [],
        reachable: true
      };
      const queries = body?.queries ?? [];
      if (!queries.length) return reply.send(payload);
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const data = await graphqlPost(opts, GET_PROFILE_ANALYZE, { queries });
        payload.tip = data.analyze?.tip ?? null;
        payload.trees = data.analyze?.trees ?? [];
        return reply.send(payload);
      } catch (err) {
        return reply.send(softError(payload, err));
      }
    }
  );
}

// src/http/query/ebpf.ts
var LIST_SERVICES_FOR_RESOLVE8 = (
  /* GraphQL */
  `
  query ListServicesForEBPFResolve($layer: String!) {
    services: listServices(layer: $layer) {
      id
      name
      normal
    }
  }
`
);
var QUERY_CREATE_TASK_DATA = (
  /* GraphQL */
  `
  query queryCreateTaskData($serviceId: ID!) {
    createTaskData: queryPrepareCreateEBPFProfilingTaskData(serviceId: $serviceId) {
      couldProfiling
      processLabels
    }
  }
`
);
var QUERY_EBPF_TASKS = (
  /* GraphQL */
  `
  query queryEBPFTasks(
    $serviceId: ID
    $serviceInstanceId: ID
    $targets: [EBPFProfilingTargetType!]
    $triggerType: EBPFProfilingTriggerType
  ) {
    queryEBPFTasks: queryEBPFProfilingTasks(
      serviceId: $serviceId
      serviceInstanceId: $serviceInstanceId
      targets: $targets
      triggerType: $triggerType
    ) {
      taskId
      serviceName
      serviceId
      serviceInstanceId
      serviceInstanceName
      processLabels
      processName
      processId
      taskStartTime
      triggerType
      fixedTriggerDuration
      targetType
      createTime
      continuousProfilingCauses {
        type
        singleValue { threshold current }
        uri { uriRegex uriPath threshold current }
        message
      }
    }
  }
`
);
var QUERY_EBPF_SCHEDULES = (
  /* GraphQL */
  `
  query queryEBPFSchedules($taskId: ID!) {
    eBPFSchedules: queryEBPFProfilingSchedules(taskId: $taskId) {
      scheduleId
      taskId
      process {
        id
        name
        serviceId
        serviceName
        instanceId
        instanceName
        agentId
        detectType
        attributes { name value }
        labels
      }
      startTime
      endTime
    }
  }
`
);
var ANALYSIS_EBPF_RESULT = (
  /* GraphQL */
  `
  query analysisEBPF(
    $scheduleIdList: [ID!]!
    $timeRanges: [EBPFProfilingAnalyzeTimeRange!]!
    $aggregateType: EBPFProfilingAnalyzeAggregateType
  ) {
    analysisEBPFResult: analysisEBPFProfilingResult(
      scheduleIdList: $scheduleIdList
      timeRanges: $timeRanges
      aggregateType: $aggregateType
    ) {
      tip
      trees {
        elements { id parentId symbol stackType dumpCount }
      }
    }
  }
`
);
var GET_PROCESS_TOPOLOGY = (
  /* GraphQL */
  `
  query getProcessTopology($serviceInstanceId: ID!, $duration: Duration!) {
    topology: getProcessTopology(serviceInstanceId: $serviceInstanceId, duration: $duration) {
      nodes {
        id
        name
        isReal
        serviceName
        serviceId
        serviceInstanceId
        serviceInstanceName
      }
      calls {
        id
        source
        target
        detectPoints
        sourceComponents
        targetComponents
      }
    }
  }
`
);
var NEW_NETWORK_PROFILING = (
  /* GraphQL */
  `
  mutation newNetworkProfiling($request: EBPFProfilingNetworkTaskRequest!) {
    createEBPFNetworkProfiling(request: $request) {
      status
      errorReason
      id
    }
  }
`
);
var KEEP_ALIVE_NETWORK_PROFILING = (
  /* GraphQL */
  `
  mutation aliveNetworkProfiling($taskId: ID!) {
    keepEBPFNetworkProfiling(taskId: $taskId) {
      status
      errorReason
    }
  }
`
);
var CREATE_EBPF_FIXED_TASK = (
  /* GraphQL */
  `
  mutation createEBPFFixedTask($request: EBPFProfilingTaskFixedTimeCreationRequest!) {
    createTaskData: createEBPFProfilingFixedTimeTask(request: $request) {
      status
      errorReason
      id
    }
  }
`
);
function softErr(p, e) {
  p.reachable = false;
  p.error = e instanceof Error ? e.message : String(e);
  return p;
}
async function resolveServiceId4(opts, layerKey, serviceArg) {
  if (/^[A-Za-z0-9+/=]+\.\d+$/.test(serviceArg)) return serviceArg;
  const data = await graphqlPost(opts, LIST_SERVICES_FOR_RESOLVE8, { layer: layerKey.toUpperCase() });
  return data.services.find((s) => s.name === serviceArg)?.id ?? data.services.find((s) => s.id === serviceArg)?.id ?? null;
}
function processRelationFragment(alias2, expr, src, dst, w) {
  return `${alias2}: execExpression(
      expression: ${JSON.stringify(expr)},
      entity: { serviceName: ${JSON.stringify(src.serviceName)}, normal: ${src.normal === false ? "false" : "true"}, serviceInstanceName: ${JSON.stringify(src.serviceInstanceName)}, processName: ${JSON.stringify(src.processName)}, destServiceName: ${JSON.stringify(dst.serviceName)}, destNormal: ${dst.normal === false ? "false" : "true"}, destServiceInstanceName: ${JSON.stringify(dst.serviceInstanceName)}, destProcessName: ${JSON.stringify(dst.processName)} },
      duration: { start: ${JSON.stringify(w.start)}, end: ${JSON.stringify(w.end)}, step: MINUTE }
    ) { error results { values { value } } }`;
}
function relationSeries(env) {
  if (!env || env.error) return [];
  const values = env.results?.[0]?.values ?? [];
  return values.map((v) => {
    if (v.value === null || v.value === void 0) return null;
    const n = Number(v.value);
    return Number.isFinite(n) ? n : null;
  });
}
function registerEBPFRoutes(app2, deps) {
  const auth = requireAuth(deps);
  app2.get(
    "/api/layer/:key/ebpf/tasks",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      const q = req.query;
      const serviceArg = (q.service ?? "").trim();
      const payload = {
        tasks: [],
        couldProfiling: false,
        processLabels: [],
        reachable: true
      };
      if (!serviceArg) return reply.send(payload);
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const serviceId = await resolveServiceId4(opts, params.key, serviceArg);
        if (!serviceId) return reply.send(payload);
        const [meta, list] = await Promise.all([
          graphqlPost(
            opts,
            QUERY_CREATE_TASK_DATA,
            { serviceId }
          ).catch(() => ({ createTaskData: { couldProfiling: false, processLabels: [] } })),
          graphqlPost(opts, QUERY_EBPF_TASKS, {
            serviceId,
            targets: ["ON_CPU", "OFF_CPU"],
            triggerType: "FIXED_TIME"
          })
        ]);
        payload.couldProfiling = meta.createTaskData?.couldProfiling ?? false;
        payload.processLabels = meta.createTaskData?.processLabels ?? [];
        payload.tasks = list.queryEBPFTasks ?? [];
        return reply.send(payload);
      } catch (err) {
        return reply.send(softErr(payload, err));
      }
    }
  );
  app2.post(
    "/api/layer/:key/ebpf/tasks",
    { preHandler: auth },
    async (req, reply) => {
      const body = req.body;
      const payload = { status: false, reachable: true };
      if (!body?.serviceId) {
        payload.errorReason = "missing serviceId";
        return reply.send(payload);
      }
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const data = await graphqlPost(opts, CREATE_EBPF_FIXED_TASK, { request: body });
        payload.status = data.createTaskData?.status ?? false;
        payload.errorReason = data.createTaskData?.errorReason;
        payload.id = data.createTaskData?.id;
        return reply.send(payload);
      } catch (err) {
        return reply.send(softErr(payload, err));
      }
    }
  );
  app2.get(
    "/api/ebpf/tasks/:taskId/schedules",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      const payload = { schedules: [], reachable: true };
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const data = await graphqlPost(
          opts,
          QUERY_EBPF_SCHEDULES,
          { taskId: params.taskId }
        );
        payload.schedules = data.eBPFSchedules ?? [];
        return reply.send(payload);
      } catch (err) {
        return reply.send(softErr(payload, err));
      }
    }
  );
  app2.get(
    "/api/layer/:key/ebpf/network/tasks",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      const q = req.query;
      const serviceArg = (q.service ?? "").trim();
      const instanceArg = (q.serviceInstance ?? "").trim();
      const payload = {
        tasks: [],
        couldProfiling: true,
        processLabels: [],
        reachable: true
      };
      if (!serviceArg && !instanceArg) return reply.send(payload);
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const serviceId = serviceArg ? await resolveServiceId4(opts, params.key, serviceArg) : null;
        const data = await graphqlPost(
          opts,
          QUERY_EBPF_TASKS,
          {
            serviceId: serviceId ?? void 0,
            serviceInstanceId: instanceArg || void 0,
            targets: ["NETWORK"],
            triggerType: "FIXED_TIME"
          }
        );
        payload.tasks = data.queryEBPFTasks ?? [];
        return reply.send(payload);
      } catch (err) {
        return reply.send(softErr(payload, err));
      }
    }
  );
  app2.get(
    "/api/ebpf/network/topology",
    { preHandler: auth },
    async (req, reply) => {
      const q = req.query;
      const instance = (q.serviceInstance ?? "").trim();
      const payload = { nodes: [], calls: [], reachable: true };
      if (!instance) return reply.send(payload);
      const startMs = Number(q.startTime);
      const endMs = Number(q.endTime);
      let start;
      let end;
      if (Number.isFinite(startMs) && Number.isFinite(endMs) && endMs > startMs) {
        start = new Date(startMs);
        end = new Date(endMs);
      } else {
        const minutes = Math.max(5, Math.min(180, Number(q.windowMinutes) || 30));
        end = /* @__PURE__ */ new Date();
        start = new Date(end.getTime() - minutes * 6e4);
      }
      const fmt = (d) => {
        const z10 = (n) => String(n).padStart(2, "0");
        return `${d.getUTCFullYear()}-${z10(d.getUTCMonth() + 1)}-${z10(d.getUTCDate())} ${z10(d.getUTCHours())}${z10(d.getUTCMinutes())}`;
      };
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const data = await graphqlPost(opts, GET_PROCESS_TOPOLOGY, {
          serviceInstanceId: instance,
          duration: { start: fmt(start), end: fmt(end), step: "MINUTE" }
        });
        payload.nodes = data.topology?.nodes ?? [];
        payload.calls = data.topology?.calls ?? [];
        return reply.send(payload);
      } catch (err) {
        return reply.send(softErr(payload, err));
      }
    }
  );
  app2.post(
    "/api/ebpf/network/tasks",
    { preHandler: auth },
    async (req, reply) => {
      const body = req.body;
      const payload = { status: false, reachable: true };
      if (!body?.instanceId) {
        payload.errorReason = "missing instanceId";
        return reply.send(payload);
      }
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const data = await graphqlPost(opts, NEW_NETWORK_PROFILING, { request: body });
        payload.status = data.createEBPFNetworkProfiling?.status ?? false;
        payload.errorReason = data.createEBPFNetworkProfiling?.errorReason;
        payload.id = data.createEBPFNetworkProfiling?.id;
        return reply.send(payload);
      } catch (err) {
        return reply.send(softErr(payload, err));
      }
    }
  );
  app2.post(
    "/api/ebpf/network/tasks/:taskId/keep-alive",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      const payload = { status: false, reachable: true };
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const data = await graphqlPost(opts, KEEP_ALIVE_NETWORK_PROFILING, { taskId: params.taskId });
        payload.status = data.keepEBPFNetworkProfiling?.status ?? false;
        payload.errorReason = data.keepEBPFNetworkProfiling?.errorReason;
        return reply.send(payload);
      } catch (err) {
        return reply.send(softErr(payload, err));
      }
    }
  );
  app2.post(
    "/api/ebpf/analyze",
    { preHandler: auth },
    async (req, reply) => {
      const body = req.body;
      const payload = { tip: null, trees: [], reachable: true };
      if (!body || !body.scheduleIdList?.length || !body.timeRanges?.length) {
        return reply.send(payload);
      }
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const data = await graphqlPost(opts, ANALYSIS_EBPF_RESULT, {
          scheduleIdList: body.scheduleIdList,
          timeRanges: body.timeRanges,
          aggregateType: body.aggregateType
        });
        payload.tip = data.analysisEBPFResult?.tip ?? null;
        payload.trees = data.analysisEBPFResult?.trees ?? [];
        return reply.send(payload);
      } catch (err) {
        return reply.send(softErr(payload, err));
      }
    }
  );
  app2.post(
    "/api/layer/:key/ebpf/network/process-relation-metrics",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      const body = req.body;
      const payload = { client: [], server: [], reachable: true };
      const src = body?.source;
      const dst = body?.dest;
      if (!src?.processName || !dst?.processName) {
        payload.error = "missing source/dest process";
        return reply.send(payload);
      }
      const cfg = processTopologyConfigFor(getLayerTemplate(params.key));
      let startMs;
      let endMs;
      if (body?.startTime && body?.endTime && body.endTime > body.startTime) {
        startMs = body.startTime;
        endMs = body.endTime;
      } else {
        const minutes = Math.max(5, Math.min(180, Number(body?.windowMinutes) || 30));
        endMs = Date.now();
        startMs = endMs - minutes * 6e4;
      }
      const fmt = (ms) => {
        const d = new Date(ms);
        const z10 = (n) => String(n).padStart(2, "0");
        return `${d.getUTCFullYear()}-${z10(d.getUTCMonth() + 1)}-${z10(d.getUTCDate())} ${z10(d.getUTCHours())}${z10(d.getUTCMinutes())}`;
      };
      const w = { start: fmt(startMs), end: fmt(endMs) };
      const aliasMap = /* @__PURE__ */ new Map();
      const fragments = [];
      const push = (side, list) => {
        list.forEach((m, i) => {
          const alias2 = `${side}_${i}`;
          aliasMap.set(alias2, { side, metric: m });
          fragments.push(processRelationFragment(alias2, m.mqe, src, dst, w));
        });
      };
      push("client", cfg.edgeClientMetrics);
      push("server", cfg.edgeServerMetrics);
      if (fragments.length === 0) return reply.send(payload);
      const query = `query HorizonProcessRelationMetrics {
  ${fragments.join("\n  ")}
}`;
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const raw = await graphqlPost(opts, query);
        for (const [alias2, { side, metric }] of aliasMap) {
          const out = {
            id: metric.id,
            label: metric.label,
            unit: metric.unit,
            values: relationSeries(raw[alias2])
          };
          if (side === "client") payload.client.push(out);
          else payload.server.push(out);
        }
        return reply.send(payload);
      } catch (err) {
        return reply.send(softErr(payload, err));
      }
    }
  );
}

// src/http/query/async-profile.ts
var DEFAULT_TASK_LIST_LIMIT = 500;
var MAX_TASK_LIST_LIMIT = 5e3;
function clampTaskListLimit(raw) {
  if (!raw) return DEFAULT_TASK_LIST_LIMIT;
  const n = Number(raw);
  if (!Number.isFinite(n) || n <= 0) return DEFAULT_TASK_LIST_LIMIT;
  return Math.min(Math.floor(n), MAX_TASK_LIST_LIMIT);
}
var LIST_SERVICES_FOR_RESOLVE9 = (
  /* GraphQL */
  `
  query ListServicesForAsyncResolve($layer: String!) {
    services: listServices(layer: $layer) {
      id
      name
      normal
    }
  }
`
);
var GET_ASYNC_TASK_LIST = (
  /* GraphQL */
  `
  query GetAsyncTaskList($request: AsyncProfilerTaskListRequest!) {
    asyncTaskList: queryAsyncProfilerTaskList(request: $request) {
      errorReason
      tasks {
        id
        serviceId
        serviceInstanceIds
        createTime
        events
        duration
        execArgs
      }
    }
  }
`
);
var GET_ASYNC_PROGRESS = (
  /* GraphQL */
  `
  query GetAsyncProgress($taskId: String!) {
    taskProgress: queryAsyncProfilerTaskProgress(taskId: $taskId) {
      logs { id instanceId instanceName operationType operationTime }
      errorInstanceIds
      successInstanceIds
    }
  }
`
);
var CREATE_ASYNC_TASK = (
  /* GraphQL */
  `
  mutation CreateAsyncTask($asyncProfilerTaskCreationRequest: AsyncProfilerTaskCreationRequest!) {
    task: createAsyncProfilerTask(asyncProfilerTaskCreationRequest: $asyncProfilerTaskCreationRequest) {
      id
      errorReason
      code
    }
  }
`
);
var GET_ASYNC_ANALYZE = (
  /* GraphQL */
  `
  query GetAsyncAnalyze($request: AsyncProfilerAnalyzationRequest!) {
    analysisResult: queryAsyncProfilerAnalyze(request: $request) {
      tree {
        type
        elements { id parentId symbol: codeSignature dumpCount: total self }
      }
    }
  }
`
);
var GET_PPROF_TASK_LIST = (
  /* GraphQL */
  `
  query GetPprofTaskList($request: PprofTaskListRequest!) {
    pprofTaskList: queryPprofTaskList(request: $request) {
      errorReason
      tasks {
        id
        serviceId
        serviceInstanceIds
        createTime
        events
        duration
        dumpPeriod
      }
    }
  }
`
);
var GET_PPROF_PROGRESS = (
  /* GraphQL */
  `
  query GetPprofProgress($taskId: String!) {
    taskProgress: queryPprofTaskProgress(taskId: $taskId) {
      logs { id instanceId instanceName operationType operationTime }
      errorInstanceIds
      successInstanceIds
    }
  }
`
);
var CREATE_PPROF_TASK = (
  /* GraphQL */
  `
  mutation CreatePprofTask($pprofTaskCreationRequest: PprofTaskCreationRequest!) {
    task: createPprofTask(pprofTaskCreationRequest: $pprofTaskCreationRequest) {
      id
      errorReason
      code
    }
  }
`
);
var GET_PPROF_ANALYZE = (
  /* GraphQL */
  `
  query GetPprofAnalyze($request: PprofAnalyzationRequest!) {
    analysisResult: queryPprofAnalyze(request: $request) {
      tree {
        elements { id parentId symbol: codeSignature dumpCount: total self }
      }
    }
  }
`
);
function softErr2(p, e) {
  p.reachable = false;
  p.error = e instanceof Error ? e.message : String(e);
  return p;
}
async function resolveServiceId5(opts, layerKey, serviceArg) {
  if (/^[A-Za-z0-9+/=]+\.\d+$/.test(serviceArg)) return serviceArg;
  const data = await graphqlPost(opts, LIST_SERVICES_FOR_RESOLVE9, { layer: layerKey.toUpperCase() });
  return data.services.find((s) => s.name === serviceArg)?.id ?? data.services.find((s) => s.id === serviceArg)?.id ?? null;
}
function registerAsyncProfileRoutes(app2, deps) {
  const auth = requireAuth(deps);
  app2.get(
    "/api/layer/:key/async/tasks",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      const q = req.query;
      const serviceArg = (q.service ?? "").trim();
      const payload = { tasks: [], reachable: true };
      if (!serviceArg) return reply.send(payload);
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      const limit = clampTaskListLimit(q.limit);
      try {
        const serviceId = await resolveServiceId5(opts, params.key, serviceArg);
        if (!serviceId) return reply.send(payload);
        const data = await graphqlPost(opts, GET_ASYNC_TASK_LIST, { request: { serviceId, limit } });
        payload.tasks = data.asyncTaskList?.tasks ?? [];
        payload.errorReason = data.asyncTaskList?.errorReason;
        return reply.send(payload);
      } catch (err) {
        return reply.send(softErr2(payload, err));
      }
    }
  );
  app2.post(
    "/api/layer/:key/async/tasks",
    { preHandler: auth },
    async (req, reply) => {
      const body = req.body;
      const payload = { reachable: true };
      if (!body?.serviceId) {
        payload.errorReason = "missing serviceId";
        return reply.send(payload);
      }
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const data = await graphqlPost(opts, CREATE_ASYNC_TASK, { asyncProfilerTaskCreationRequest: body });
        payload.id = data.task?.id;
        payload.code = data.task?.code;
        payload.errorReason = data.task?.errorReason;
        return reply.send(payload);
      } catch (err) {
        return reply.send(softErr2(payload, err));
      }
    }
  );
  app2.get(
    "/api/async/tasks/:taskId/progress",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      const payload = { progress: null, reachable: true };
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const data = await graphqlPost(opts, GET_ASYNC_PROGRESS, { taskId: params.taskId });
        payload.progress = data.taskProgress ?? null;
        return reply.send(payload);
      } catch (err) {
        return reply.send(softErr2(payload, err));
      }
    }
  );
  app2.post(
    "/api/async/analyze",
    { preHandler: auth },
    async (req, reply) => {
      const body = req.body;
      const payload = { tree: null, reachable: true };
      if (!body?.taskId || !body.instanceIds?.length) return reply.send(payload);
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const data = await graphqlPost(opts, GET_ASYNC_ANALYZE, { request: body });
        payload.tree = data.analysisResult?.tree ?? null;
        return reply.send(payload);
      } catch (err) {
        return reply.send(softErr2(payload, err));
      }
    }
  );
  app2.get(
    "/api/layer/:key/pprof/tasks",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      const q = req.query;
      const serviceArg = (q.service ?? "").trim();
      const payload = { tasks: [], reachable: true };
      if (!serviceArg) return reply.send(payload);
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      const limit = clampTaskListLimit(q.limit);
      try {
        const serviceId = await resolveServiceId5(opts, params.key, serviceArg);
        if (!serviceId) return reply.send(payload);
        const data = await graphqlPost(opts, GET_PPROF_TASK_LIST, { request: { serviceId, limit } });
        payload.tasks = data.pprofTaskList?.tasks ?? [];
        payload.errorReason = data.pprofTaskList?.errorReason;
        return reply.send(payload);
      } catch (err) {
        return reply.send(softErr2(payload, err));
      }
    }
  );
  app2.post(
    "/api/layer/:key/pprof/tasks",
    { preHandler: auth },
    async (req, reply) => {
      const body = req.body;
      const payload = { reachable: true };
      if (!body?.serviceId) {
        payload.errorReason = "missing serviceId";
        return reply.send(payload);
      }
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const data = await graphqlPost(opts, CREATE_PPROF_TASK, { pprofTaskCreationRequest: body });
        payload.id = data.task?.id;
        payload.code = data.task?.code;
        payload.errorReason = data.task?.errorReason;
        return reply.send(payload);
      } catch (err) {
        return reply.send(softErr2(payload, err));
      }
    }
  );
  app2.get(
    "/api/pprof/tasks/:taskId/progress",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      const payload = { progress: null, reachable: true };
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const data = await graphqlPost(
          opts,
          GET_PPROF_PROGRESS,
          { taskId: params.taskId }
        );
        payload.progress = data.taskProgress ?? null;
        return reply.send(payload);
      } catch (err) {
        return reply.send(softErr2(payload, err));
      }
    }
  );
  app2.post(
    "/api/pprof/analyze",
    { preHandler: auth },
    async (req, reply) => {
      const body = req.body;
      const payload = { tree: null, reachable: true };
      if (!body?.taskId || !body.instanceIds?.length) return reply.send(payload);
      const opts = buildOapOpts(deps.config.current, deps.fetch);
      try {
        const request = { taskId: body.taskId, instanceIds: body.instanceIds };
        const data = await graphqlPost(opts, GET_PPROF_ANALYZE, { request });
        payload.tree = data.analysisResult?.tree ?? null;
        return reply.send(payload);
      } catch (err) {
        return reply.send(softErr2(payload, err));
      }
    }
  );
}

// src/http/config/dashboard.ts
function registerDashboardConfigRoute(app2, deps) {
  const auth = requireAuth(deps);
  app2.get(
    "/api/layer/:key/dashboard/config",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      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;
      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 });
    }
  );
}

// src/http/config/layer-template.ts
function registerLayerTemplateRoutes(app2, deps) {
  const auth = requireAuth(deps);
  app2.get("/api/admin/layer-templates", { preHandler: auth }, async (_req, reply) => {
    return reply.send({ templates: allLayerTemplates() });
  });
}

// src/http/config/alarms.ts
import { z as z6 } from "zod";

// src/logic/alarms/store.ts
import { existsSync } from "node:fs";
import { mkdir as mkdir2, readFile, rename, writeFile } from "node:fs/promises";
import { dirname as dirname3, resolve as resolve3 } from "node:path";
var ALARMS_WINDOW_OPTIONS = [
  20 * 6e4,
  2 * 60 * 6e4,
  4 * 60 * 6e4
];
var OVERVIEW_ALARMS_LIMIT_MIN = 10;
var OVERVIEW_ALARMS_LIMIT_MAX = 500;
var OVERVIEW_ALARMS_LIMIT_DEFAULT = 200;
var DEFAULT_CONFIG = {
  pinnedLayers: ["GENERAL", "MESH"],
  defaultWindowMs: ALARMS_WINDOW_OPTIONS[0],
  overviewAlarmsLimit: OVERVIEW_ALARMS_LIMIT_DEFAULT
};
var AlarmsStore = class {
  absPath;
  cache = null;
  writing = null;
  constructor(filePath) {
    this.absPath = resolve3(filePath);
  }
  async load() {
    if (this.cache) return this.cache;
    if (!existsSync(this.absPath)) {
      this.cache = { ...DEFAULT_CONFIG, pinnedLayers: [...DEFAULT_CONFIG.pinnedLayers] };
      return this.cache;
    }
    try {
      const raw = await readFile(this.absPath, "utf8");
      const parsed = JSON.parse(raw);
      let pinned = null;
      if (Array.isArray(parsed.pinnedLayers)) {
        pinned = parsed.pinnedLayers.filter(
          (v) => typeof v === "string" && v.length > 0
        );
      } else if (Array.isArray(parsed.trafficLayers)) {
        const seen = /* @__PURE__ */ new Set();
        const out = [];
        for (const l of parsed.trafficLayers) {
          const k = typeof l?.layerKey === "string" ? l.layerKey : "";
          if (!k || seen.has(k)) continue;
          seen.add(k);
          out.push(k);
        }
        pinned = out.length > 0 ? out : null;
      }
      const rawWin = typeof parsed.defaultWindowMs === "number" ? parsed.defaultWindowMs : NaN;
      const defaultWindowMs = ALARMS_WINDOW_OPTIONS.includes(rawWin) ? rawWin : DEFAULT_CONFIG.defaultWindowMs;
      const rawLim = typeof parsed.overviewAlarmsLimit === "number" ? parsed.overviewAlarmsLimit : NaN;
      const overviewAlarmsLimit = Number.isInteger(rawLim) && rawLim >= OVERVIEW_ALARMS_LIMIT_MIN && rawLim <= OVERVIEW_ALARMS_LIMIT_MAX ? rawLim : DEFAULT_CONFIG.overviewAlarmsLimit;
      this.cache = {
        pinnedLayers: pinned ?? [...DEFAULT_CONFIG.pinnedLayers],
        defaultWindowMs,
        overviewAlarmsLimit
      };
      return this.cache;
    } catch (err) {
      logger.warn({ err, path: this.absPath }, "alarms store unreadable; using defaults");
      this.cache = { ...DEFAULT_CONFIG, pinnedLayers: [...DEFAULT_CONFIG.pinnedLayers] };
      return this.cache;
    }
  }
  async save(next) {
    while (this.writing) await this.writing;
    const tmp = `${this.absPath}.tmp`;
    const snapshot = {
      pinnedLayers: [...next.pinnedLayers],
      defaultWindowMs: next.defaultWindowMs,
      overviewAlarmsLimit: next.overviewAlarmsLimit
    };
    const work = (async () => {
      await mkdir2(dirname3(this.absPath), { recursive: true });
      await writeFile(
        tmp,
        JSON.stringify({ generatedAt: Date.now(), ...snapshot }, null, 2),
        "utf8"
      );
      await rename(tmp, this.absPath);
      this.cache = snapshot;
    })();
    this.writing = work;
    try {
      await work;
    } finally {
      this.writing = null;
    }
  }
};

// src/http/config/alarms.ts
var configSaveSchema = z6.object({
  pinnedLayers: z6.array(z6.string().min(1)).max(8),
  /* Locked to the page-picker preset set so the admin's pick always
   * lights up a real tab. Hand-edited values are rejected with a 400
   * rather than silently coerced — keeps debugging predictable. */
  defaultWindowMs: z6.number().int().refine((v) => ALARMS_WINDOW_OPTIONS.includes(v), {
    message: `must be one of ${ALARMS_WINDOW_OPTIONS.join(", ")}`
  }),
  /* Bounded range — too low starves the widget's incident merge of
   * variety; too high pulls more bytes per poll than necessary. */
  overviewAlarmsLimit: z6.number().int().min(OVERVIEW_ALARMS_LIMIT_MIN).max(OVERVIEW_ALARMS_LIMIT_MAX)
});
function registerAlarmsConfigRoutes(app2, deps) {
  const auth = requireAuth(deps);
  app2.get(
    "/api/alarms/config",
    { preHandler: auth },
    async (_req, reply) => {
      const cfg = await deps.store.load();
      return reply.send(cfg);
    }
  );
  app2.post(
    "/api/alarms/config",
    { preHandler: auth },
    async (req, reply) => {
      const parsed = configSaveSchema.safeParse(req.body);
      if (!parsed.success) {
        return reply.code(400).send({ error: "invalid_body", detail: parsed.error.flatten() });
      }
      const seen = /* @__PURE__ */ new Set();
      const pinnedLayers = [];
      for (const raw of parsed.data.pinnedLayers) {
        const k = raw.trim().toUpperCase();
        if (!k || seen.has(k)) continue;
        seen.add(k);
        pinnedLayers.push(k);
      }
      const next = {
        pinnedLayers,
        defaultWindowMs: parsed.data.defaultWindowMs,
        overviewAlarmsLimit: parsed.data.overviewAlarmsLimit
      };
      await deps.store.save(next);
      deps.serviceLayer.invalidate();
      deps.audit.record({
        action: "alarms.config.save",
        actor: req.session?.username ?? null,
        outcome: "ok",
        details: {
          pinnedLayers: next.pinnedLayers,
          defaultWindowMs: next.defaultWindowMs,
          overviewAlarmsLimit: next.overviewAlarmsLimit
        },
        fromIp: req.ip,
        sessionId: req.session?.sid
      });
      return reply.send(next);
    }
  );
}

// src/http/config/setup.ts
import { z as z7 } from "zod";
var aggregationSchema = z7.enum(["sum", "avg"]);
var landingColumnSchema = z7.object({
  metric: z7.string().min(1),
  label: z7.string().min(1),
  unit: z7.string().optional(),
  mqe: z7.string().optional(),
  aggregation: aggregationSchema.optional(),
  scale: z7.number().finite().optional(),
  precision: z7.number().int().min(0).max(6).optional()
}).strict();
var throughputSchema2 = z7.object({
  metric: z7.string().min(1),
  label: z7.string().optional(),
  unit: z7.string().optional(),
  mqe: z7.string().optional(),
  aggregation: aggregationSchema.optional(),
  scale: z7.number().finite().optional(),
  precision: z7.number().int().min(0).max(6).optional()
}).strict();
var landingSchema = z7.object({
  priority: z7.number().int().min(0).max(99),
  topN: z7.number().int().min(5).max(8),
  orderBy: z7.string().min(1),
  columns: z7.array(landingColumnSchema).max(5),
  spark: z7.object({
    metric: z7.string().min(1),
    height: z7.number().int().positive()
  }).strict().optional(),
  throughput: throughputSchema2.optional(),
  style: z7.enum(["table", "bar", "mini-topology"])
}).strict();
var layerConfigSchema = z7.object({
  displayName: z7.string().optional(),
  slots: z7.object({
    services: z7.string().optional(),
    instances: z7.string().optional(),
    endpoints: z7.string().optional(),
    endpointDependency: z7.string().optional()
  }).strict(),
  caps: z7.object({
    serviceMap: z7.boolean().optional(),
    endpointDependency: z7.boolean().optional(),
    instanceTopology: z7.boolean().optional(),
    processTopology: z7.boolean().optional(),
    dashboards: z7.boolean().optional(),
    traces: z7.boolean().optional(),
    logs: z7.boolean().optional(),
    profiling: z7.boolean().optional(),
    events: z7.boolean().optional()
  }).strict(),
  landing: landingSchema
}).strict();
var savePayloadSchema = z7.object({
  layers: z7.record(z7.string().min(1), layerConfigSchema)
}).strict();
function registerSetupRoutes(app2, deps) {
  const auth = requireAuth(deps);
  app2.get("/api/setup", { preHandler: auth }, async (_req, reply) => {
    const layers = await deps.store.load();
    const body = { generatedAt: Date.now(), layers };
    return reply.send(body);
  });
  app2.post("/api/setup", { preHandler: auth }, async (req, reply) => {
    const parsed = savePayloadSchema.safeParse(req.body);
    if (!parsed.success) {
      throw badRequest("invalid setup payload", parsed.error.flatten());
    }
    const payload = parsed.data;
    await deps.store.save(payload.layers);
    deps.audit.record({
      actor: req.session?.username ?? null,
      action: "setup.save",
      outcome: "success",
      details: { layerCount: Object.keys(payload.layers).length }
    });
    const body = { generatedAt: Date.now(), layers: payload.layers };
    return reply.send(body);
  });
}

// src/logic/overview/loader.ts
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath as fileURLToPath2 } from "node:url";
var __dirname2 = path.dirname(fileURLToPath2(import.meta.url));
function locateConfigDir2() {
  const candidates = [
    path.join(__dirname2, "bundled_templates", "overviews"),
    path.join(__dirname2, "..", "..", "bundled_templates", "overviews"),
    path.join(__dirname2, "..", "bundled_templates", "overviews"),
    path.join(process.cwd(), "bundled_templates", "overviews")
  ];
  for (const dir of candidates) {
    try {
      fs.readdirSync(dir);
      return dir;
    } catch {
    }
  }
  return candidates[0];
}
var CONFIG_DIR2 = locateConfigDir2();
var cache4 = null;
function isString(v) {
  return typeof v === "string" && v.length > 0;
}
var WIDGET_TYPES = /* @__PURE__ */ new Set([
  "metric",
  "topology",
  "section-break",
  "kpi-tile",
  "alarms",
  "metric-composite"
]);
var LAYERLESS_WIDGETS = /* @__PURE__ */ new Set([
  "section-break",
  "alarms"
]);
function parseKpis(raw) {
  if (!Array.isArray(raw)) return void 0;
  const out = [];
  for (const k of raw) {
    if (!k || typeof k !== "object") continue;
    const rec = k;
    if (!isString(rec.label)) continue;
    const source2 = rec.source === "service-count" ? "service-count" : "mqe";
    if (source2 === "mqe" && !isString(rec.mqe)) continue;
    const style = rec.style === "progress-bar" ? "progress-bar" : rec.style === "number" ? "number" : void 0;
    const max = style === "progress-bar" && typeof rec.max === "number" && Number.isFinite(rec.max) && rec.max > 0 ? rec.max : void 0;
    out.push({
      label: rec.label,
      mqe: isString(rec.mqe) ? rec.mqe : void 0,
      unit: isString(rec.unit) ? rec.unit : void 0,
      aggregation: rec.aggregation === "sum" ? "sum" : rec.aggregation === "avg" ? "avg" : void 0,
      style,
      max,
      source: source2
    });
  }
  return out.length > 0 ? out : void 0;
}
function validate(raw, file) {
  if (!raw || typeof raw !== "object") {
    console.warn(`overview/${file}: not an object, skipped`);
    return null;
  }
  const r = raw;
  if (!isString(r.id) || !isString(r.title) || !Array.isArray(r.widgets)) {
    console.warn(`overview/${file}: missing id/title/widgets`);
    return null;
  }
  const widgets = r.widgets.filter((w) => {
    if (!w || typeof w !== "object") return false;
    const rec = w;
    if (!isString(rec.id) || !isString(rec.title)) return false;
    if (!WIDGET_TYPES.has(rec.type)) return false;
    if (!LAYERLESS_WIDGETS.has(rec.type) && !isString(rec.layer)) {
      return false;
    }
    return true;
  });
  const visibility = r.visibility === "operate" ? "operate" : r.visibility === "public" ? "public" : void 0;
  const layers = Array.isArray(r.layers) ? r.layers.filter(isString) : void 0;
  return {
    id: r.id,
    title: r.title,
    description: isString(r.description) ? r.description : void 0,
    visibility,
    icon: isString(r.icon) ? r.icon : void 0,
    order: typeof r.order === "number" ? r.order : void 0,
    layers,
    widgets: widgets.map((w) => ({
      id: w.id,
      title: w.title,
      tip: isString(w.tip) ? w.tip : void 0,
      layer: isString(w.layer) ? w.layer : void 0,
      type: w.type,
      mqe: isString(w.mqe) ? w.mqe : void 0,
      unit: isString(w.unit) ? w.unit : void 0,
      aggregation: w.aggregation === "sum" ? "sum" : w.aggregation === "avg" ? "avg" : void 0,
      cols: typeof w.cols === "number" ? w.cols : void 0,
      kpis: parseKpis(w.kpis),
      showCount: w.showCount === true ? true : void 0,
      limit: typeof w.limit === "number" ? w.limit : void 0,
      span: typeof w.span === "number" ? w.span : void 0,
      rowSpan: typeof w.rowSpan === "number" ? w.rowSpan : void 0
    }))
  };
}
function loadOverviewDashboards() {
  if (cache4) return cache4;
  if (!fs.existsSync(CONFIG_DIR2)) {
    cache4 = [];
    return cache4;
  }
  const files = fs.readdirSync(CONFIG_DIR2).filter((f) => f.endsWith(".json"));
  const out = [];
  for (const f of files) {
    try {
      const raw = JSON.parse(fs.readFileSync(path.join(CONFIG_DIR2, f), "utf8"));
      const dash = validate(raw, f);
      if (dash) out.push(dash);
    } catch (err) {
      console.warn(`overview/${f}: parse error`, err instanceof Error ? err.message : err);
    }
  }
  out.sort((a, b) => {
    const ao = a.order ?? 1e3;
    const bo = b.order ?? 1e3;
    if (ao !== bo) return ao - bo;
    const av = a.visibility ?? "public";
    const bv = b.visibility ?? "public";
    if (av !== bv) return av === "public" ? -1 : 1;
    return a.id.localeCompare(b.id);
  });
  cache4 = out;
  return cache4;
}
function invalidateOverviewCache() {
  cache4 = null;
}
function getOverviewDashboard(id) {
  return loadOverviewDashboards().find((d) => d.id === id) ?? null;
}
function findOverviewFile(id) {
  if (!fs.existsSync(CONFIG_DIR2)) return null;
  for (const f of fs.readdirSync(CONFIG_DIR2)) {
    if (!f.endsWith(".json")) continue;
    try {
      const raw = JSON.parse(fs.readFileSync(path.join(CONFIG_DIR2, f), "utf8"));
      if (raw && typeof raw === "object" && raw.id === id) {
        return path.join(CONFIG_DIR2, f);
      }
    } catch {
    }
  }
  return null;
}
function writeOverviewDashboard(id, dash) {
  const file = findOverviewFile(id);
  if (!file) {
    throw new Error(`overview/${id}: no source file to write back to`);
  }
  const tmp = `${file}.tmp`;
  fs.writeFileSync(tmp, JSON.stringify(dash, null, 2), "utf8");
  fs.renameSync(tmp, file);
  invalidateOverviewCache();
}
function createOverviewDashboard(dash) {
  if (findOverviewFile(dash.id)) {
    throw new Error(`overview/${dash.id}: dashboard already exists`);
  }
  if (!fs.existsSync(CONFIG_DIR2)) fs.mkdirSync(CONFIG_DIR2, { recursive: true });
  const safe = dash.id.replace(/[^A-Za-z0-9_-]/g, "_");
  const file = path.join(CONFIG_DIR2, `${safe}.json`);
  fs.writeFileSync(file, JSON.stringify(dash, null, 2), "utf8");
  invalidateOverviewCache();
}
function deleteOverviewDashboard(id) {
  const file = findOverviewFile(id);
  if (!file) return;
  fs.unlinkSync(file);
  invalidateOverviewCache();
}

// src/http/config/overview.ts
function registerOverviewRoutes(app2, deps) {
  const auth = requireAuth(deps);
  app2.get("/api/overview/dashboards", { preHandler: auth }, async (_req, reply) => {
    const dashboards = loadOverviewDashboards();
    const body = {
      generatedAt: Date.now(),
      dashboards: dashboards.map((d) => ({
        id: d.id,
        title: d.title,
        description: d.description,
        visibility: d.visibility,
        icon: d.icon,
        order: d.order,
        layers: d.layers,
        widgetCount: d.widgets.length
      }))
    };
    return reply.send(body);
  });
  app2.get(
    "/api/overview/dashboards/:id",
    { preHandler: auth },
    async (req, reply) => {
      const { id } = req.params;
      const dash = getOverviewDashboard(id);
      if (!dash) {
        const body2 = {
          generatedAt: Date.now(),
          dashboard: { id, title: id, widgets: [] },
          reachable: false,
          error: `unknown dashboard "${id}"`
        };
        return reply.code(404).send(body2);
      }
      const body = {
        generatedAt: Date.now(),
        dashboard: dash,
        reachable: true
      };
      return reply.send(body);
    }
  );
}

// src/http/config/bundle.ts
import { createHash } from "node:crypto";

// src/logic/templates/names.ts
var ALERT_PAGE_SETUP_KEY = "page-setup";
var THEME_ACTIVE_KEY = "active";
var TIME_DEFAULTS_KEY = "global";
var NAME_RE = /^horizon\.(overview|layer|alert|theme|time-defaults)\.([A-Za-z0-9_-]+)$/;
function formatName(kind, key) {
  if (!/^[A-Za-z0-9_-]+$/.test(key)) {
    throw new Error(`invalid template key for kind=${kind}: ${JSON.stringify(key)}`);
  }
  return `horizon.${kind}.${key}`;
}
function parseName(name) {
  const m = NAME_RE.exec(name);
  if (!m) return null;
  return { kind: m[1], key: m[2] };
}
var ENVELOPE_VERSION = 1;
function buildEnvelope(kind, key, content) {
  return {
    name: formatName(kind, key),
    kind,
    version: ENVELOPE_VERSION,
    content
  };
}
function serializeEnvelope(envelope) {
  return canonicalStringify(envelope);
}
function canonicalStringify(value) {
  if (value === null || typeof value !== "object") {
    return JSON.stringify(value);
  }
  if (Array.isArray(value)) {
    return "[" + value.map((v) => canonicalStringify(v)).join(",") + "]";
  }
  const obj = value;
  const keys = Object.keys(obj).sort();
  const parts = keys.filter((k) => obj[k] !== void 0).map((k) => `${JSON.stringify(k)}:${canonicalStringify(obj[k])}`);
  return "{" + parts.join(",") + "}";
}
function parseEnvelope(configuration) {
  let parsed;
  try {
    parsed = JSON.parse(configuration);
  } catch {
    return null;
  }
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
  const e = parsed;
  if (typeof e.name !== "string" || typeof e.kind !== "string") return null;
  const parsedName = parseName(e.name);
  if (!parsedName || parsedName.kind !== e.kind) return null;
  if (typeof e.version !== "number") return null;
  if (e.content === void 0) return null;
  return {
    name: e.name,
    kind: parsedName.kind,
    version: e.version,
    content: e.content
  };
}

// src/logic/templates/sync.ts
var CACHE_TTL_MS2 = 3e4;
var cache5 = null;
var inFlight = null;
var lastSuccessfulSyncAt = null;
function invalidateSyncCache() {
  cache5 = null;
}
async function getSyncStatus(deps) {
  const now = (deps.now ?? Date.now)();
  if (cache5 && now - cache5.at < CACHE_TTL_MS2) {
    return cache5.status;
  }
  if (inFlight) return inFlight;
  inFlight = (async () => {
    try {
      const status = await runOnce(deps, { write: false });
      cache5 = { at: (deps.now ?? Date.now)(), status };
      return status;
    } finally {
      inFlight = null;
    }
  })();
  return inFlight;
}
async function bootSeed(deps) {
  const status = await runOnce(deps, { write: true });
  cache5 = { at: (deps.now ?? Date.now)(), status };
  return status;
}
function resync() {
  invalidateSyncCache();
}
async function runOnce(deps, opts) {
  const now = (deps.now ?? Date.now)();
  const bundledRows = buildBundledRows(deps.bundled());
  let oapRows;
  try {
    oapRows = await deps.client.list();
  } catch (err) {
    deps.logger.warn(
      { err: errMsg2(err), action: opts.write ? "boot-seed" : "runtime-sync" },
      "OAP UI-template list failed \u2014 rendering bundled, admin read-only"
    );
    return {
      unreachable: true,
      lastSuccessfulSyncAt,
      generatedAt: now,
      rows: bundledOnlyRows(bundledRows, "bundled-fallback")
    };
  }
  lastSuccessfulSyncAt = (deps.now ?? Date.now)();
  const parsedRemote = parseRemoteRows(oapRows, deps.logger);
  if (opts.write) {
    const seedCount = await seedMissing(deps, bundledRows, parsedRemote);
    if (seedCount > 0) {
      try {
        const refreshed = await deps.client.list();
        parsedRemote.clear();
        for (const [k, v] of parseRemoteRows(refreshed, deps.logger)) parsedRemote.set(k, v);
        deps.logger.info({ seedCount }, "OAP UI-template boot seed complete");
      } catch (err) {
        deps.logger.warn(
          { err: errMsg2(err) },
          "OAP UI-template re-list after seed failed \u2014 sync status may lag the next runtime pull"
        );
      }
    }
  }
  const rows = mergeRows(bundledRows, parsedRemote);
  return {
    unreachable: false,
    lastSuccessfulSyncAt,
    generatedAt: now,
    rows
  };
}
function buildBundledRows(bundled) {
  const out = /* @__PURE__ */ new Map();
  for (const b of bundled) {
    const envelope = buildEnvelope(b.kind, b.key, b.content);
    out.set(envelope.name, {
      name: envelope.name,
      kind: b.kind,
      key: b.key,
      configuration: serializeEnvelope(envelope),
      content: b.content
    });
  }
  return out;
}
function parseRemoteRows(rows, logger2) {
  const out = /* @__PURE__ */ new Map();
  let skipped = 0;
  for (const r of rows) {
    const env = parseEnvelope(r.configuration);
    if (!env) {
      skipped++;
      continue;
    }
    out.set(env.name, {
      name: env.name,
      kind: env.kind,
      key: env.name.split(".").slice(2).join("."),
      id: r.id,
      configuration: r.configuration,
      disabled: r.disabled
    });
  }
  if (skipped > 0) {
    logger2.debug(
      { skipped },
      "OAP UI-template rows ignored (not Horizon-namespaced) \u2014 operator may have other tools writing to this OAP"
    );
  }
  return out;
}
async function seedMissing(deps, bundled, remote) {
  let count = 0;
  for (const [name, b] of bundled) {
    if (remote.has(name)) continue;
    try {
      const ack = await deps.client.create(b.configuration);
      if (!ack.status) {
        deps.logger.warn(
          { name, message: ack.message },
          "OAP UI-template seed rejected \u2014 name conflict on OAP side, manual reconcile needed"
        );
        continue;
      }
      count++;
      deps.logger.info({ name, id: ack.id }, "OAP UI-template seeded");
    } catch (err) {
      deps.logger.warn(
        { name, err: errMsg2(err) },
        "OAP UI-template seed failed \u2014 will retry at next BFF boot"
      );
    }
  }
  return count;
}
function mergeRows(bundled, remote) {
  const out = [];
  const seen = /* @__PURE__ */ new Set();
  for (const [name, b] of bundled) {
    seen.add(name);
    const r = remote.get(name);
    if (!r) {
      out.push({
        name,
        kind: b.kind,
        key: b.key,
        status: "bundled-fallback",
        effective: "bundled",
        remote: null,
        bundled: { configuration: b.configuration }
      });
      continue;
    }
    if (r.disabled) {
      out.push({
        name,
        kind: b.kind,
        key: b.key,
        status: "disabled",
        effective: null,
        remote: { id: r.id, configuration: r.configuration, disabled: true },
        bundled: { configuration: b.configuration }
      });
      continue;
    }
    const status = r.configuration === b.configuration ? "synced" : "diverged";
    out.push({
      name,
      kind: b.kind,
      key: b.key,
      status,
      effective: "remote",
      remote: { id: r.id, configuration: r.configuration, disabled: false },
      bundled: { configuration: b.configuration }
    });
  }
  for (const [name, r] of remote) {
    if (seen.has(name)) continue;
    out.push({
      name,
      kind: r.kind,
      key: r.key,
      status: r.disabled ? "disabled" : "remote-only",
      effective: r.disabled ? null : "remote",
      remote: { id: r.id, configuration: r.configuration, disabled: r.disabled },
      bundled: null
    });
  }
  out.sort((a, b) => a.name.localeCompare(b.name));
  return out;
}
function bundledOnlyRows(bundled, status) {
  return Array.from(bundled.values()).sort((a, b) => a.name.localeCompare(b.name)).map((b) => ({
    name: b.name,
    kind: b.kind,
    key: b.key,
    status,
    effective: "bundled",
    remote: null,
    bundled: { configuration: b.configuration }
  }));
}
function errMsg2(err) {
  if (err instanceof Error) return err.message;
  return String(err);
}

// src/logic/alarms/bundled.ts
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
import { dirname as dirname4, resolve as resolve4 } from "node:path";
import { fileURLToPath as fileURLToPath3 } from "node:url";
var HERE = dirname4(fileURLToPath3(import.meta.url));
var cached = null;
function loadBundledAlertPageSetup() {
  if (cached) return cached;
  const file = locateAlertBundle();
  const raw = readFileSync3(file, "utf8");
  const parsed = JSON.parse(raw);
  cached = parsed;
  return parsed;
}
function locateAlertBundle() {
  const candidates = [
    resolve4(HERE, "bundled_templates/alert/page-setup.json"),
    resolve4(HERE, "../../bundled_templates/alert/page-setup.json"),
    resolve4(process.cwd(), "bundled_templates/alert/page-setup.json")
  ];
  for (const c of candidates) {
    if (existsSync2(c)) return c;
  }
  throw new Error(
    `bundled alert page-setup not found in: ${candidates.join(", ")}`
  );
}

// src/logic/templates/global-defaults-bundled.ts
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
import { dirname as dirname5, resolve as resolve5 } from "node:path";
import { fileURLToPath as fileURLToPath4 } from "node:url";
var HERE2 = dirname5(fileURLToPath4(import.meta.url));
var cachedTheme = null;
var cachedTimeDefaults = null;
function loadBundledThemeActive() {
  if (cachedTheme) return cachedTheme;
  const raw = readFileSync4(locate("theme/active.json"), "utf8");
  cachedTheme = JSON.parse(raw);
  return cachedTheme;
}
function loadBundledTimeDefaults() {
  if (cachedTimeDefaults) return cachedTimeDefaults;
  const raw = readFileSync4(locate("time-defaults/global.json"), "utf8");
  cachedTimeDefaults = JSON.parse(raw);
  return cachedTimeDefaults;
}
function locate(rel) {
  const candidates = [
    resolve5(HERE2, `bundled_templates/${rel}`),
    resolve5(HERE2, `../../bundled_templates/${rel}`),
    resolve5(process.cwd(), `bundled_templates/${rel}`)
  ];
  for (const c of candidates) {
    if (existsSync3(c)) return c;
  }
  throw new Error(`bundled file not found: ${rel} (tried: ${candidates.join(", ")})`);
}

// src/logic/templates/aggregator.ts
function* iterateBundledTemplates() {
  for (const tpl of allLayerTemplates()) {
    yield { kind: "layer", key: tpl.key, content: tpl };
  }
  for (const dash of loadOverviewDashboards()) {
    yield { kind: "overview", key: dash.id, content: dash };
  }
  yield {
    kind: "alert",
    key: ALERT_PAGE_SETUP_KEY,
    content: loadBundledAlertPageSetup()
  };
  yield {
    kind: "theme",
    key: THEME_ACTIVE_KEY,
    content: loadBundledThemeActive()
  };
  yield {
    kind: "time-defaults",
    key: TIME_DEFAULTS_KEY,
    content: loadBundledTimeDefaults()
  };
}

// src/http/config/bundle.ts
function registerConfigBundleRoute(app2, deps) {
  const auth = requireAuth(deps);
  app2.get(
    "/api/configs/bundle",
    { preHandler: auth },
    async (req, reply) => {
      const preferLocal = req.query.prefer === "local";
      const body = await buildBundle(deps, preferLocal);
      const inm = req.headers["if-none-match"];
      if (typeof inm === "string" && inm === body.etag) {
        return reply.code(304).send();
      }
      reply.header("ETag", body.etag);
      reply.header("Cache-Control", "private, max-age=0, must-revalidate");
      return reply.send(body);
    }
  );
}
async function buildBundle(deps, preferLocal = false) {
  const sync = await getSyncStatus({
    client: deps.uiTemplateClient(),
    bundled: () => iterateBundledTemplates(),
    logger
  });
  const remoteByName = /* @__PURE__ */ new Map();
  for (const row of sync.rows) remoteByName.set(row.name, row);
  const layers = {};
  for (const tpl of allLayerTemplates()) {
    const effective = pickLayerContent(tpl, remoteByName, preferLocal);
    if (effective === null) continue;
    const scopes = {};
    for (const scope of ["service", "instance", "endpoint"]) {
      const ws = widgetsForScope(effective, scope);
      if (ws.length > 0) scopes[scope] = ws;
    }
    layers[effective.key.toLowerCase()] = scopes;
  }
  const overviews = [];
  for (const dash of loadOverviewDashboards()) {
    const effective = pickOverviewContent(dash, remoteByName, preferLocal);
    if (effective === null) continue;
    overviews.push(effective);
  }
  const syncStatus = {
    unreachable: sync.unreachable,
    lastSuccessfulSyncAt: sync.lastSuccessfulSyncAt,
    generatedAt: sync.generatedAt,
    badges: sync.rows.map((r) => ({
      name: r.name,
      kind: r.kind,
      key: r.key,
      status: r.status
    }))
  };
  const body = { layers, overviews, syncStatus };
  const etag = createHash("md5").update(JSON.stringify(body)).digest("hex");
  return { etag, generatedAt: Date.now(), ...body };
}
function pickLayerContent(bundled, byName, preferLocal = false) {
  const row = byName.get(formatName("layer", bundled.key));
  if (!row) return bundled;
  if (row.status === "disabled") return null;
  if (preferLocal && row.status === "diverged") return bundled;
  if (row.effective === "remote" && row.remote) {
    const env = parseEnvelope(row.remote.configuration);
    if (env && isLayerLike(env.content)) {
      return env.content;
    }
  }
  return bundled;
}
function pickOverviewContent(bundled, byName, preferLocal = false) {
  const row = byName.get(formatName("overview", bundled.id));
  if (!row) return bundled;
  if (row.status === "disabled") return null;
  if (preferLocal && row.status === "diverged") return bundled;
  if (row.effective === "remote" && row.remote) {
    const env = parseEnvelope(row.remote.configuration);
    if (env && isOverviewLike(env.content)) {
      return env.content;
    }
  }
  return bundled;
}
function isLayerLike(v) {
  return !!v && typeof v === "object" && "key" in v;
}
function isOverviewLike(v) {
  return !!v && typeof v === "object" && "id" in v;
}

// src/http/admin/template-sync.ts
function registerTemplateSyncAdminRoutes(app2, deps) {
  const auth = requireAuth(deps);
  app2.get(
    "/api/admin/templates/sync-status",
    { preHandler: auth },
    async (_req, reply) => {
      const status = await loadStatus(deps);
      return reply.send(status);
    }
  );
  app2.post(
    "/api/admin/templates/resync",
    { preHandler: auth },
    async (_req, reply) => {
      resync();
      const status = await loadStatus(deps);
      return reply.send(status);
    }
  );
  app2.post(
    "/api/admin/templates/:name/push-bundled",
    { preHandler: auth },
    async (req, reply) => {
      const { name } = req.params;
      const parsed = parseName(name);
      if (!parsed) {
        return reply.code(400).send({
          code: "invalid_template_name",
          message: `expected horizon.<overview|layer|alert>.<key>, got ${JSON.stringify(name)}`
        });
      }
      const status = await loadStatus(deps);
      if (status.unreachable) {
        return reply.code(409).send({
          code: "oap_unreachable",
          message: "OAP admin port unreachable \u2014 templates are read-only"
        });
      }
      const row = status.rows.find((r) => r.name === name);
      if (!row?.bundled) {
        return reply.code(404).send({
          code: "no_bundled",
          message: `no bundled template for ${name} \u2014 nothing to push`
        });
      }
      try {
        if (row.remote) {
          await deps.uiTemplateClient().update(row.remote.id, row.bundled.configuration);
        } else {
          await deps.uiTemplateClient().create(row.bundled.configuration);
        }
        resync();
        const fresh = await loadStatus(deps);
        return reply.send(fresh);
      } catch (err) {
        logger.warn({ err: errMsg3(err), name }, "push-bundled to OAP failed");
        return reply.code(502).send({
          code: "oap_write_failed",
          message: errMsg3(err)
        });
      }
    }
  );
  app2.post("/api/admin/templates/sync-all", { preHandler: auth }, async (req, reply) => {
    const kind = req.body?.kind;
    const status = await loadStatus(deps);
    if (status.unreachable) {
      return reply.code(409).send({
        code: "oap_unreachable",
        message: "OAP admin port unreachable \u2014 templates are read-only"
      });
    }
    const targets = status.rows.filter(
      (r) => (!kind || r.kind === kind) && !!r.bundled && (r.status === "diverged" || r.status === "bundled-fallback")
    );
    const synced = [];
    const failed = [];
    for (const row of targets) {
      try {
        if (row.remote) {
          await deps.uiTemplateClient().update(row.remote.id, row.bundled.configuration);
        } else {
          await deps.uiTemplateClient().create(row.bundled.configuration);
        }
        synced.push(row.name);
      } catch (err) {
        logger.warn({ err: errMsg3(err), name: row.name }, "sync-all push failed");
        failed.push({ name: row.name, error: errMsg3(err) });
      }
    }
    resync();
    const fresh = await loadStatus(deps);
    return reply.send({ ...fresh, synced, failed });
  });
  app2.post("/api/admin/templates/revert-local", { preHandler: auth }, async (req, reply) => {
    const kind = req.body?.kind;
    const status = await loadStatus(deps);
    if (status.unreachable) {
      return reply.code(409).send({
        code: "oap_unreachable",
        message: "OAP admin port unreachable \u2014 cannot fetch the remote version"
      });
    }
    const targets = status.rows.filter(
      (r) => (!kind || r.kind === kind) && r.status === "diverged" && !!r.remote
    );
    const reverted = [];
    const failed = [];
    for (const row of targets) {
      try {
        const env = parseEnvelope(row.remote.configuration);
        if (!env) throw new Error("remote envelope not parseable");
        if (row.kind === "layer") {
          writeLayerTemplate(env.content);
        } else if (row.kind === "overview") {
          writeOverviewDashboard(row.key, env.content);
        } else {
          throw new Error(`revert supports layer + overview, not ${row.kind}`);
        }
        reverted.push(row.name);
      } catch (err) {
        logger.warn({ err: errMsg3(err), name: row.name }, "revert-local failed");
        failed.push({ name: row.name, error: errMsg3(err) });
      }
    }
    resync();
    const fresh = await loadStatus(deps);
    return reply.send({ ...fresh, reverted, failed });
  });
  app2.post("/api/admin/templates/save-local", { preHandler: auth }, async (req, reply) => {
    const { name, content } = req.body ?? {};
    if (typeof name !== "string" || content === void 0 || content === null || typeof content !== "object") {
      return reply.code(400).send({
        code: "invalid_save_body",
        message: "body must be { name: string, content: object }"
      });
    }
    const parsed = parseName(name);
    if (!parsed) {
      return reply.code(400).send({
        code: "invalid_template_name",
        message: `expected horizon.<overview|layer|alert>.<key>, got ${JSON.stringify(name)}`
      });
    }
    try {
      if (parsed.kind === "layer") {
        writeLayerTemplate(content);
      } else if (parsed.kind === "overview") {
        writeOverviewDashboard(parsed.key, content);
      } else {
        return reply.code(400).send({
          code: "unsupported_kind",
          message: `local save supports layer + overview templates, not ${parsed.kind}`
        });
      }
    } catch (err) {
      logger.warn({ err: errMsg3(err), name }, "save-local (bundled write) failed");
      return reply.code(500).send({ code: "local_write_failed", message: errMsg3(err) });
    }
    resync();
    const fresh = await loadStatus(deps);
    return reply.send(fresh);
  });
  app2.post("/api/admin/templates/save", { preHandler: auth }, async (req, reply) => {
    const { name, content } = req.body ?? {};
    if (typeof name !== "string" || content === void 0) {
      return reply.code(400).send({
        code: "invalid_save_body",
        message: "body must be { name: string, content: object }"
      });
    }
    const parsed = parseName(name);
    if (!parsed) {
      return reply.code(400).send({
        code: "invalid_template_name",
        message: `expected horizon.<overview|layer|alert>.<key>, got ${JSON.stringify(name)}`
      });
    }
    const status = await loadStatus(deps);
    if (status.unreachable) {
      return reply.code(409).send({
        code: "oap_unreachable",
        message: "OAP admin port unreachable \u2014 templates are read-only"
      });
    }
    const envelope = buildEnvelope(parsed.kind, parsed.key, content);
    const configuration = serializeEnvelope(envelope);
    const existing = status.rows.find((r) => r.name === name);
    try {
      if (existing?.remote) {
        await deps.uiTemplateClient().update(existing.remote.id, configuration);
      } else {
        await deps.uiTemplateClient().create(configuration);
      }
      resync();
      const fresh = await loadStatus(deps);
      return reply.send(fresh);
    } catch (err) {
      logger.warn({ err: errMsg3(err), name }, "save to OAP failed");
      return reply.code(502).send({
        code: "oap_write_failed",
        message: errMsg3(err)
      });
    }
  });
}
async function loadStatus(deps) {
  return getSyncStatus({
    client: deps.uiTemplateClient(),
    bundled: () => iterateBundledTemplates(),
    logger
  });
}
function errMsg3(err) {
  if (err instanceof Error) return err.message;
  return String(err);
}

// src/client/index.ts
import {
  AlarmStatusClient,
  DslDebuggingClient,
  InspectClient,
  OalClient,
  RuntimeRuleClient,
  StatusClient,
  UITemplateClient
} from "@skywalking-horizon-ui/api-client";
function buildOapClients(config, opts = {}) {
  const fetch = opts.fetch;
  const primaryUrl = config.oap.adminUrl;
  const timeoutMs = config.oap.timeoutMs;
  const headers = config.oap.auth ? {
    authorization: "Basic " + Buffer.from(
      `${config.oap.auth.username}:${config.oap.auth.password}`,
      "utf8"
    ).toString("base64")
  } : void 0;
  return {
    forUrl(adminUrl) {
      return new RuntimeRuleClient({ adminUrl, fetch, timeoutMs, headers });
    },
    primary() {
      return new RuntimeRuleClient({ adminUrl: primaryUrl, fetch, timeoutMs, headers });
    },
    status() {
      return new StatusClient({ queryUrl: config.oap.queryUrl, fetch, timeoutMs, headers });
    },
    oal() {
      return new OalClient({ adminUrl: primaryUrl, fetch, timeoutMs, headers });
    },
    debug() {
      return new DslDebuggingClient({ adminUrl: primaryUrl, fetch, timeoutMs, headers });
    },
    debugForUrl(adminUrl) {
      return new DslDebuggingClient({ adminUrl, fetch, timeoutMs, headers });
    },
    inspect() {
      return new InspectClient({ adminUrl: primaryUrl, fetch, timeoutMs, headers });
    },
    alarmStatus() {
      return new AlarmStatusClient({ adminUrl: primaryUrl, fetch, timeoutMs, headers });
    },
    uiTemplate() {
      return new UITemplateClient({ adminUrl: primaryUrl, fetch, timeoutMs, headers });
    },
    adminUrl() {
      return config.oap.adminUrl;
    }
  };
}

// src/http/admin/dsl/_shared.ts
import {
  RuntimeRuleApiError,
  isCatalog
} from "@skywalking-horizon-ui/api-client";
function makeClients(deps) {
  return () => buildOapClients(deps.config.current, { fetch: deps.fetch });
}
var TRUTHY = /* @__PURE__ */ new Set(["true", "1", "yes"]);
function parseBoolean(v, fallback2) {
  if (v === void 0) return fallback2;
  return TRUTHY.has(v.toLowerCase());
}
function hasCatalogParam(q) {
  return typeof q === "object" && q !== null && "catalog" in q;
}
function parseOptionalCatalog(q, reply) {
  const raw = q.catalog;
  if (raw === void 0 || raw === "") return void 0;
  if (!isCatalog(raw)) {
    reply.code(400).send({ error: "invalid_catalog", value: raw });
    return void 0;
  }
  return raw;
}
function parseRequiredCatalog(q, reply) {
  const raw = q.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;
}
function parseDeleteMode(raw, reply) {
  if (raw === void 0 || raw === "") return "";
  if (raw === "revertToBundled") return "revertToBundled";
  reply.code(400).send({ error: "invalid_delete_mode", value: raw });
  return null;
}
function ensureVerb(req, reply, deps, verb) {
  const session = 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, reply) {
  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, reply, deps, req, action, verb, catalog, name, details = {}) {
  let outcome = "oap_unreachable";
  if (err instanceof RuntimeRuleApiError) {
    const apiErr = 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, req, action, verb, catalog, name, outcome, details = {}) {
  deps.audit.record({
    action,
    verb,
    actor: req.session?.username ?? null,
    outcome,
    details: { catalog, name, ...details },
    fromIp: req.ip,
    sessionId: req.session?.sid
  });
}

// src/http/admin/dsl/catalog.ts
function registerDslCatalogRoutes(app2, deps) {
  const auth = requireAuth(deps);
  const clients = makeClients(deps);
  app2.get(
    "/api/catalog/list",
    { preHandler: auth },
    async (req, reply) => {
      if (!ensureVerb(req, reply, deps, "rule:read")) return;
      const catalog = parseOptionalCatalog(req.query, reply);
      if (catalog === void 0 && hasCatalogParam(req.query)) return;
      try {
        const env = await clients().primary().list(catalog);
        return reply.send(env);
      } catch (err) {
        return passOapError(err, reply);
      }
    }
  );
  app2.get(
    "/api/catalog/bundled",
    { preHandler: auth },
    async (req, reply) => {
      if (!ensureVerb(req, reply, deps, "rule:read")) return;
      const catalog = parseRequiredCatalog(req.query, reply);
      if (!catalog) return;
      const withContent = parseBoolean(req.query.withContent, true);
      try {
        const list = await clients().primary().listBundled(catalog, withContent);
        return reply.send(list);
      } catch (err) {
        return passOapError(err, reply);
      }
    }
  );
}

// src/http/admin/dsl/rule.ts
function registerDslRuleRoutes(app2, deps) {
  const auth = requireAuth(deps);
  const clients = makeClients(deps);
  app2.get("/api/rule", { preHandler: auth }, async (req, reply) => {
    if (!ensureVerb(req, reply, deps, "rule:read")) return;
    const q = req.query;
    const catalog = parseRequiredCatalog(q, reply);
    if (!catalog) return;
    if (!q.name) return reply.code(400).send({ error: "missing_name" });
    const source2 = q.source;
    if (source2 !== void 0 && source2 !== "runtime" && source2 !== "bundled") {
      return reply.code(400).send({ error: "invalid_source", value: source2 });
    }
    const ifNoneMatch = req.headers["if-none-match"];
    try {
      const got = await clients().primary().get({
        catalog,
        name: q.name,
        ...source2 !== void 0 ? { source: source2 } : {},
        ...ifNoneMatch !== void 0 ? { 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);
    }
  });
  app2.post("/api/rule", { preHandler: auth }, async (req, reply) => {
    const q = req.query;
    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);
    }
  });
  app2.post(
    "/api/rule/inactivate",
    { preHandler: auth },
    async (req, reply) => {
      const q = req.query;
      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
        );
      }
    }
  );
  app2.post(
    "/api/rule/delete",
    { preHandler: auth },
    async (req, reply) => {
      const q = req.query;
      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;
      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 });
      }
    }
  );
}

// src/http/admin/dsl/dump.ts
import { isCatalog as isCatalog2 } from "@skywalking-horizon-ui/api-client";
function registerDslDumpRoutes(app2, deps) {
  const auth = requireAuth(deps);
  const clients = makeClients(deps);
  const dumpHandler = (catalog) => async function(req, reply) {
    if (!ensureVerb(req, reply, deps, "rule:read")) return;
    try {
      const upstream = await clients().primary().dump(catalog ?? void 0);
      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);
    }
  };
  app2.get("/api/dump", { preHandler: auth }, dumpHandler(null));
  app2.get(
    "/api/dump/:catalog",
    { preHandler: auth },
    async (req, reply) => {
      const params = req.params;
      if (!isCatalog2(params.catalog)) {
        return reply.code(400).send({ error: "invalid_catalog", value: params.catalog });
      }
      return dumpHandler(params.catalog)(req, reply);
    }
  );
}

// src/http/admin/dsl/oal.ts
function registerDslOalRoutes(app2, deps) {
  const auth = requireAuth(deps);
  const clients = makeClients(deps);
  app2.get(
    "/api/oal/files",
    { preHandler: auth },
    async (req, reply) => {
      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);
      }
    }
  );
  app2.get(
    "/api/oal/files/:name",
    { preHandler: auth },
    async (req, reply) => {
      if (!ensureVerb(req, reply, deps, "rule:read")) return;
      const params = req.params;
      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);
      }
    }
  );
  app2.get(
    "/api/oal/rules",
    { preHandler: auth },
    async (req, reply) => {
      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);
      }
    }
  );
  app2.get(
    "/api/oal/rules/:source",
    { preHandler: auth },
    async (req, reply) => {
      if (!ensureVerb(req, reply, deps, "rule:read")) return;
      const params = req.params;
      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);
      }
    }
  );
}

// src/client/cluster.ts
async function fetchClusterState(clients) {
  const adminUrl = clients.adminUrl();
  const generatedAt = Date.now();
  let envelope;
  try {
    envelope = await clients.primary().list();
  } catch (err) {
    return {
      generatedAt,
      adminUrl,
      ok: false,
      error: err instanceof Error ? err.message : String(err),
      rules: []
    };
  }
  return {
    generatedAt,
    adminUrl,
    ok: true,
    rules: envelope.rules.map((rule) => ({
      catalog: rule.catalog,
      name: rule.name,
      status: rule.status,
      localState: rule.localState,
      contentHash: rule.contentHash,
      lastApplyError: extractLastApplyError(rule)
    }))
  };
}
function extractLastApplyError(row) {
  return "lastApplyError" in row ? row.lastApplyError : "";
}

// src/http/admin/cluster.ts
function registerClusterRoutes(app2, deps) {
  const auth = requireAuth(deps);
  const clients = makeClients(deps);
  app2.get(
    "/api/cluster/state",
    { preHandler: auth },
    async (req, reply) => {
      if (!ensureVerb(req, reply, deps, "cluster:read")) return;
      return reply.send(await fetchClusterState(clients()));
    }
  );
}

// src/http/admin/live-debug.ts
import {
  MAX_RECORD_CAP,
  RuntimeRuleApiError as RuntimeRuleApiError2,
  isDebugCatalog,
  isGranularity
} from "@skywalking-horizon-ui/api-client";

// src/util/dns-fanout.ts
import { promises as dnsPromises } from "node:dns";
import { isIP } from "node:net";
async function resolveTargets(originalUrl) {
  let parsed;
  try {
    parsed = new URL(originalUrl);
  } catch {
    return [{ url: originalUrl, resolved: false, originalHost: "", ip: null }];
  }
  const host2 = parsed.hostname;
  if (isIP(host2)) {
    return [{ url: originalUrl, resolved: false, originalHost: host2, ip: host2 }];
  }
  let addresses;
  try {
    addresses = await dnsPromises.lookup(host2, { all: true });
  } catch {
    return [{ url: originalUrl, resolved: false, originalHost: host2, ip: null }];
  }
  if (addresses.length === 0) {
    return [{ url: originalUrl, resolved: false, originalHost: host2, ip: null }];
  }
  const seen = /* @__PURE__ */ new Set();
  const out = [];
  for (const { address } of addresses) {
    if (seen.has(address)) continue;
    seen.add(address);
    const url = new URL(originalUrl);
    url.hostname = address.includes(":") ? `[${address}]` : address;
    out.push({ url: url.toString(), resolved: true, originalHost: host2, ip: address });
  }
  return out;
}

// src/http/admin/live-debug.ts
function registerDebugRoutes(app2, deps) {
  const auth = requireAuth(deps);
  function clients() {
    return buildOapClients(deps.config.current, { fetch: deps.fetch });
  }
  app2.post(
    "/api/debug/session",
    { preHandler: auth },
    async (req, reply) => {
      if (!ensureVerb2(req, reply, deps, "rule:debug")) return;
      const parsed = parseStartArgs(req.body, reply);
      if (!parsed) return;
      try {
        const result = await clients().debug().startSession(parsed);
        const local = result.priorCleanup?.local;
        const peers = result.priorCleanup?.peers ?? [];
        const totalPriorCleanup = (local?.stoppedCount ?? 0) + peers.reduce((n, p) => n + (p.stoppedCount ?? p.stoppedSessionIds?.length ?? 0), 0);
        deps.audit.record({
          action: "debug.start",
          verb: "rule:debug",
          actor: req.session?.username ?? null,
          outcome: "ok",
          details: {
            sessionId: result.sessionId,
            clientId: parsed.clientId,
            catalog: parsed.catalog,
            name: parsed.name,
            ruleName: parsed.ruleName,
            granularity: result.granularity,
            localInstalled: result.localInstalled,
            installedCreated: result.installed?.created,
            installedTotal: result.installed?.total,
            recordCap: parsed.recordCap,
            retentionMillis: parsed.retentionMillis,
            priorCleanupCount: totalPriorCleanup
          },
          fromIp: req.ip,
          sessionId: req.session?.sid
        });
        return reply.send(result);
      } catch (err) {
        deps.audit.record({
          action: "debug.start",
          verb: "rule:debug",
          actor: req.session?.username ?? null,
          outcome: outcomeOf(err),
          details: {
            clientId: parsed.clientId,
            catalog: parsed.catalog,
            name: parsed.name,
            ruleName: parsed.ruleName
          },
          fromIp: req.ip,
          sessionId: req.session?.sid
        });
        return passOapError2(err, reply);
      }
    }
  );
  app2.get(
    "/api/debug/session/:id",
    { preHandler: auth },
    async (req, reply) => {
      if (!ensureVerb2(req, reply, deps, "rule:debug")) return;
      const params = req.params;
      if (!params.id) return reply.code(400).send({ error: "missing_id" });
      try {
        const got = await clients().debug().getSession(params.id);
        if (got === null) return reply.code(404).send({ error: "not_found" });
        return reply.send(got);
      } catch (err) {
        return passOapError2(err, reply);
      }
    }
  );
  app2.post(
    "/api/debug/session/:id/stop",
    { preHandler: auth },
    async (req, reply) => {
      if (!ensureVerb2(req, reply, deps, "rule:debug")) return;
      const params = req.params;
      if (!params.id) return reply.code(400).send({ error: "missing_id" });
      try {
        const result = await clients().debug().stopSession(params.id);
        deps.audit.record({
          action: "debug.stop",
          verb: "rule:debug",
          actor: req.session?.username ?? null,
          outcome: "ok",
          details: { sessionId: params.id, localStopped: result.localStopped },
          fromIp: req.ip,
          sessionId: req.session?.sid
        });
        return reply.send(result);
      } catch (err) {
        deps.audit.record({
          action: "debug.stop",
          verb: "rule:debug",
          actor: req.session?.username ?? null,
          outcome: outcomeOf(err),
          details: { sessionId: params.id },
          fromIp: req.ip,
          sessionId: req.session?.sid
        });
        return passOapError2(err, reply);
      }
    }
  );
  app2.get(
    "/api/debug/sessions",
    { preHandler: auth },
    async (req, reply) => {
      if (!ensureVerb2(req, reply, deps, "rule:debug")) return;
      try {
        const list = await clients().debug().listSessions();
        return reply.send(list);
      } catch (err) {
        return passOapError2(err, reply);
      }
    }
  );
  app2.get(
    "/api/debug/status",
    { preHandler: auth },
    async (req, reply) => {
      if (!ensureVerb2(req, reply, deps, "cluster:read")) return;
      const c = clients();
      const targets = await resolveTargets(c.adminUrl());
      const nodes = await Promise.all(
        targets.map(async (t) => {
          try {
            const status = await c.debugForUrl(t.url).getStatus();
            return { url: t.url, ok: true, status };
          } catch (err) {
            return {
              url: t.url,
              ok: false,
              error: err instanceof Error ? err.message : String(err)
            };
          }
        })
      );
      const body = { generatedAt: Date.now(), nodes };
      return reply.send(body);
    }
  );
}
function ensureVerb2(req, reply, deps, verb) {
  const session = 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 parseStartArgs(raw, reply) {
  if (typeof raw !== "object" || raw === null) {
    reply.code(400).send({ error: "invalid_body" });
    return null;
  }
  const b = raw;
  if (typeof b.clientId !== "string" || b.clientId.length === 0) {
    reply.code(400).send({ error: "missing_clientId" });
    return null;
  }
  if (typeof b.catalog !== "string" || !isDebugCatalog(b.catalog)) {
    reply.code(400).send({ error: "invalid_catalog", value: b.catalog });
    return null;
  }
  if (typeof b.name !== "string" || b.name.length === 0) {
    reply.code(400).send({ error: "missing_name" });
    return null;
  }
  if (typeof b.ruleName !== "string" || b.ruleName.length === 0) {
    reply.code(400).send({ error: "missing_ruleName" });
    return null;
  }
  const out = {
    clientId: b.clientId,
    catalog: b.catalog,
    name: b.name,
    ruleName: b.ruleName
  };
  if (b.granularity !== void 0) {
    if (!isGranularity(b.granularity)) {
      reply.code(400).send({ error: "invalid_granularity", value: b.granularity });
      return null;
    }
    out.granularity = b.granularity;
  }
  if (b.recordCap !== void 0) {
    if (typeof b.recordCap !== "number" || !Number.isFinite(b.recordCap) || b.recordCap <= 0 || b.recordCap > MAX_RECORD_CAP) {
      reply.code(400).send({ error: "invalid_recordCap" });
      return null;
    }
    out.recordCap = b.recordCap;
  }
  if (b.retentionMillis !== void 0) {
    if (typeof b.retentionMillis !== "number" || !Number.isFinite(b.retentionMillis) || b.retentionMillis <= 0 || b.retentionMillis > 36e5) {
      reply.code(400).send({ error: "invalid_retentionMillis" });
      return null;
    }
    out.retentionMillis = b.retentionMillis;
  }
  return out;
}
function outcomeOf(err) {
  if (err instanceof RuntimeRuleApiError2) {
    const body = err.body;
    if (typeof body === "object" && body !== null) {
      const o = body;
      if (typeof o.code === "string") return o.code;
      if (typeof o.applyStatus === "string") return o.applyStatus;
    }
    return `http_${err.status}`;
  }
  return "oap_unreachable";
}
function passOapError2(err, reply) {
  if (err instanceof RuntimeRuleApiError2) {
    return reply.code(err.status).send(err.body);
  }
  return reply.code(502).send({
    error: "oap_unreachable",
    message: err instanceof Error ? err.message : String(err)
  });
}

// src/http/admin/inspect.ts
import {
  InspectApiError,
  INSPECT_ENTITY_LIMIT_MAX,
  INSPECT_STEPS as INSPECT_STEPS2,
  isInspectDate as isInspectDate2
} from "@skywalking-horizon-ui/api-client";

// src/logic/inspect/parser-oal.ts
var METRIC_NAME_RE = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/;
function stripComments(src) {
  const noBlock = src.replace(/\/\*[\s\S]*?\*\//g, "");
  return noBlock.replace(/\/\/[^\n]*/g, "");
}
function parseOalMetricNames(content) {
  const cleaned = stripComments(content);
  const out = [];
  const seen = /* @__PURE__ */ new Set();
  for (const line of cleaned.split("\n")) {
    const m = METRIC_NAME_RE.exec(line);
    if (!m) continue;
    const name = m[1];
    if (seen.has(name)) continue;
    seen.add(name);
    out.push(name);
  }
  return out;
}

// src/logic/inspect/parser-mal.ts
import { parse as parseYaml } from "yaml";
function parseMalMetricNames(content) {
  let doc;
  try {
    doc = parseYaml(content);
  } catch {
    return [];
  }
  if (typeof doc !== "object" || doc === null) return [];
  const root = doc;
  const prefix = typeof root.metricPrefix === "string" && root.metricPrefix.length > 0 ? `${root.metricPrefix}_` : "";
  const out = [];
  const seen = /* @__PURE__ */ new Set();
  const collect = (rules) => {
    if (!Array.isArray(rules)) return;
    for (const r of rules) {
      if (typeof r !== "object" || r === null) continue;
      const node = r;
      const raw = typeof node.name === "string" ? node.name : typeof node.metricsName === "string" ? node.metricsName : null;
      if (!raw) continue;
      const full = prefix + raw;
      if (seen.has(full)) continue;
      seen.add(full);
      out.push(full);
    }
  };
  collect(root.metricsRules);
  collect(root.rules);
  return out;
}

// src/logic/inspect/attribution.ts
var AttributionCache = class {
  state = { index: null, inflight: null };
  /** Force the next call to `get()` to fully rebuild the index. */
  invalidate() {
    this.state = { index: null, inflight: null };
  }
  /** Returns the cached index when its fingerprint still matches the
   *  current inputs; rebuilds otherwise. Concurrent callers share one
   *  in-flight rebuild. */
  async get(deps) {
    const cached2 = this.state.index;
    if (cached2) {
      try {
        const fp = await computeFingerprint(deps);
        if (fp === cached2.fingerprint) return cached2;
      } catch {
        return cached2;
      }
    }
    if (this.state.inflight) return this.state.inflight;
    const p = buildIndex(deps).then((idx) => {
      this.state = { index: idx, inflight: null };
      return idx;
    });
    this.state.inflight = p;
    return p;
  }
};
var MAL_CATALOGS = ["otel-rules", "telegraf-rules", "log-mal-rules"];
async function computeFingerprint(deps) {
  const rules = deps.rules();
  const [oalFiles, malRuntimeRows, ...bundledLists] = await Promise.all([
    deps.oal().listFiles().then((r) => r.files).catch(() => []),
    rules.list().then((r) => r.rules.map((row) => `${row.catalog}/${row.name}@${row.contentHash}`)).catch(() => []),
    ...MAL_CATALOGS.map(
      (c) => rules.listBundled(c, false).then((rows) => rows.map((b) => `${c}/${b.name}@${b.contentHash}`)).catch(() => [])
    )
  ]);
  const oalPart = [...oalFiles].sort().join("|");
  const runtimePart = [...malRuntimeRows].sort().join("|");
  const bundledPart = bundledLists.flat().sort().join("|");
  return `oal:${oalPart}||rt:${runtimePart}||bn:${bundledPart}`;
}
async function buildIndex(deps) {
  const oal = deps.oal();
  const rules = deps.rules();
  const [filesEnvSafe, listEnvSafe, ...bundledLists] = await Promise.all([
    oal.listFiles().catch(() => ({ files: [], count: 0 })),
    rules.list().catch(() => ({
      generatedAt: 0,
      loaderStats: { active: 0, pending: 0 },
      rules: []
    })),
    ...MAL_CATALOGS.map(
      (c) => rules.listBundled(c, true).then((rows) => ({ catalog: c, rows })).catch(() => ({ catalog: c, rows: [] }))
    )
  ]);
  const oalContents = await Promise.all(
    filesEnvSafe.files.map(async (name) => {
      try {
        return { name, content: await oal.getFileContent(name) };
      } catch {
        return { name, content: null };
      }
    })
  );
  const malRuntimeRows = listEnvSafe.rules.filter((r) => r.catalog !== "lal");
  const malRuntimeContents = await Promise.all(
    malRuntimeRows.map(async (row) => {
      try {
        const got = await rules.get({ catalog: row.catalog, name: row.name });
        if ("notModified" in got) return { catalog: row.catalog, name: row.name, content: null };
        return { catalog: row.catalog, name: row.name, content: got.content };
      } catch {
        return { catalog: row.catalog, name: row.name, content: null };
      }
    })
  );
  const malBundledContents = bundledLists.flatMap(
    (b) => b.rows.map((row) => ({ catalog: b.catalog, name: row.name, content: row.content ?? null }))
  );
  const malContents = [...malRuntimeContents, ...malBundledContents];
  const byMetric = /* @__PURE__ */ new Map();
  const claims = /* @__PURE__ */ new Map();
  const claim = (metric, source2, file) => {
    const arr = claims.get(metric) ?? [];
    arr.push({ source: source2, file });
    claims.set(metric, arr);
  };
  for (const { name, content } of oalContents) {
    if (content === null) continue;
    for (const m of parseOalMetricNames(content)) claim(m, "OAL", name);
  }
  const seenMal = /* @__PURE__ */ new Set();
  for (const { catalog, name, content } of malContents) {
    if (content === null) continue;
    const file = `${catalog}/${name}`;
    if (seenMal.has(file)) continue;
    seenMal.add(file);
    const source2 = catalog === "otel-rules" ? "MAL\xB7OTEL" : catalog === "telegraf-rules" ? "MAL\xB7Telegraf" : catalog === "log-mal-rules" ? "LAL\u2192MAL" : "unknown";
    for (const m of parseMalMetricNames(content)) claim(m, source2, file);
  }
  for (const [metric, arr] of claims) {
    if (arr.length === 1) {
      const only = arr[0];
      byMetric.set(metric, { source: only.source, file: only.file });
    } else {
      const first = arr[0];
      byMetric.set(metric, {
        source: first.source,
        file: first.file,
        candidates: arr.map((c) => `${c.source}:${c.file}`)
      });
    }
  }
  const fingerprint = await computeFingerprint(deps);
  return { fingerprint, byMetric };
}
function attributeOrUnknown(index, metric) {
  return index.byMetric.get(metric) ?? { source: "unknown", file: null };
}

// src/util/mqe-target.ts
var CACHE_TTL_MS3 = 6e4;
var MqeTargetCache = class {
  entry = null;
  invalidate() {
    this.entry = null;
  }
  async resolve(deps) {
    const now = Date.now();
    if (this.entry && this.entry.expiresAt > now) return this.entry.target;
    const target = await resolveMqeTarget(deps);
    this.entry = { target, expiresAt: now + CACHE_TTL_MS3 };
    return target;
  }
};
async function resolveMqeTarget(deps) {
  const cfg = deps.config().oap;
  const configured = {};
  if (cfg.mqe?.host !== void 0) configured.host = cfg.mqe.host;
  if (cfg.mqe?.port !== void 0) configured.port = cfg.mqe.port;
  if (configured.host !== void 0 && configured.port !== void 0) {
    return {
      baseUrl: `http://${configured.host}:${configured.port}`,
      via: "studio.yaml override (host + port)",
      configured
    };
  }
  if (configured.host === void 0 && configured.port === void 0) {
    return { baseUrl: cfg.queryUrl.replace(/\/$/, ""), via: "oap.queryUrl", configured };
  }
  const adminUrl = cfg.adminUrl;
  const dump = await fetchConfigDump2(adminUrl, deps.fetch, cfg.timeoutMs, cfg.auth);
  const adminHost = new URL(adminUrl).hostname;
  const picked = pickFromDump(dump, adminHost);
  const finalHost = configured.host ?? picked.host;
  const finalPort = configured.port ?? picked.port;
  if (finalPort === void 0) {
    throw new Error(
      "mqe target: could not discover REST port from /debugging/config/dump (neither sharing-server nor core REST appears in the dump)"
    );
  }
  const viaParts = [];
  viaParts.push(
    configured.host !== void 0 ? "host from studio.yaml" : `host from ${picked.hostFrom}`
  );
  viaParts.push(
    configured.port !== void 0 ? "port from studio.yaml" : `port from ${picked.portFrom}`
  );
  return {
    baseUrl: `http://${finalHost}:${finalPort}`,
    via: viaParts.join(", "),
    configured
  };
}
function pickFromDump(dump, adminHost) {
  const sharingHost = dump["sharing-server.default.restHost"];
  const sharingPortStr = dump["sharing-server.default.restPort"];
  const coreHost = dump["core.default.restHost"];
  const corePortStr = dump["core.default.restPort"];
  const sharingPort = parsePort(sharingPortStr);
  const corePort = parsePort(corePortStr);
  const preferSharing = sharingPort !== void 0;
  const host2 = preferSharing ? sharingHost : coreHost;
  const port2 = preferSharing ? sharingPort : corePort;
  const moduleLabel = preferSharing ? "sharing-server.restHost" : "core.restHost";
  const portLabel = preferSharing ? "sharing-server.restPort" : "core.restPort";
  const isWildcard = !host2 || host2 === "0.0.0.0" || host2 === "::" || host2 === "";
  const resolvedHost = isWildcard ? adminHost : host2;
  const hostFrom = isWildcard ? `admin URL host (${moduleLabel} was wildcard)` : moduleLabel;
  return { host: resolvedHost, port: port2, hostFrom, portFrom: portLabel };
}
function parsePort(raw) {
  if (raw === void 0 || raw === "") return void 0;
  const n = Number.parseInt(raw, 10);
  if (!Number.isFinite(n) || n <= 0 || n > 65535) return void 0;
  return n;
}
async function fetchConfigDump2(adminUrl, fetch, timeoutMs, auth) {
  const base = adminUrl.replace(/\/$/, "");
  const url = `${base}/debugging/config/dump`;
  const headers = { Accept: "application/json" };
  if (auth) {
    const b64 = Buffer.from(`${auth.username}:${auth.password}`, "utf8").toString("base64");
    headers.authorization = `Basic ${b64}`;
  }
  let init = { method: "GET", headers };
  let timer = null;
  if (timeoutMs > 0) {
    const ctrl = new AbortController();
    timer = setTimeout(() => ctrl.abort(), timeoutMs);
    init = { ...init, signal: ctrl.signal };
  }
  try {
    const res = await fetch(url, init);
    if (!res.ok) {
      const body = await res.text();
      throw new Error(`config dump failed: HTTP ${res.status} from ${url} \u2014 ${body.slice(0, 200)}`);
    }
    return await res.json();
  } finally {
    if (timer) clearTimeout(timer);
  }
}

// src/logic/inspect/exec.ts
import { INSPECT_STEPS, isInspectDate } from "@skywalking-horizon-ui/api-client";
var GRAPHQL_QUERY = "query Exec($expression: String!, $entity: Entity!, $duration: Duration!, $debug: Boolean) {\n  execExpression(expression: $expression, entity: $entity, duration: $duration, debug: $debug) {\n    type\n    error\n    results {\n      metric { labels { key value } }\n      values { id value traceID owner { scope serviceID serviceName normal serviceInstanceID serviceInstanceName endpointID endpointName } }\n    }\n  }\n}\n";
function parseExecBody(body, reply) {
  if (typeof body !== "object" || body === null) {
    reply.code(400).send({ error: "invalid_body" });
    return null;
  }
  const b = body;
  if (typeof b.expression !== "string" || b.expression.length === 0) {
    reply.code(400).send({ error: "missing_expression" });
    return null;
  }
  if (typeof b.entity !== "object" || b.entity === null) {
    reply.code(400).send({ error: "missing_entity" });
    return null;
  }
  if (typeof b.entity.scope !== "string") {
    reply.code(400).send({ error: "invalid_entity", detail: "scope is required" });
    return null;
  }
  if (typeof b.duration !== "object" || b.duration === null) {
    reply.code(400).send({ error: "missing_duration" });
    return null;
  }
  const d = b.duration;
  if (typeof d.start !== "string" || typeof d.end !== "string") {
    reply.code(400).send({ error: "invalid_duration", detail: "start and end must be strings" });
    return null;
  }
  if (typeof d.step !== "string" || !INSPECT_STEPS.includes(d.step.toUpperCase())) {
    reply.code(400).send({
      error: "invalid_duration",
      detail: `step must be one of ${INSPECT_STEPS.join(", ")}`
    });
    return null;
  }
  const step = d.step.toUpperCase();
  if (!isInspectDate(d.start, step)) {
    reply.code(400).send({ error: "invalid_duration", detail: `start does not match ${step} format` });
    return null;
  }
  if (!isInspectDate(d.end, step)) {
    reply.code(400).send({ error: "invalid_duration", detail: `end does not match ${step} format` });
    return null;
  }
  return {
    expression: b.expression,
    entity: b.entity,
    duration: {
      start: d.start,
      end: d.end,
      step,
      ...d.coldStage !== void 0 ? { coldStage: !!d.coldStage } : {}
    },
    ...b.debug !== void 0 ? { debug: !!b.debug } : {}
  };
}
async function fireMqe(target, req, deps) {
  const url = `${target.baseUrl.replace(/\/$/, "")}/graphql`;
  const payload = {
    query: GRAPHQL_QUERY,
    variables: {
      expression: req.expression,
      entity: req.entity,
      duration: req.duration,
      debug: req.debug ?? false
    }
  };
  const headers = {
    "Content-Type": "application/json",
    Accept: "application/json"
  };
  if (deps.auth) {
    headers.authorization = "Basic " + Buffer.from(`${deps.auth.username}:${deps.auth.password}`, "utf8").toString("base64");
  }
  let init = {
    method: "POST",
    headers,
    body: JSON.stringify(payload)
  };
  let timer = null;
  if (deps.timeoutMs > 0) {
    const ctrl = new AbortController();
    timer = setTimeout(() => ctrl.abort(), deps.timeoutMs);
    init = { ...init, signal: ctrl.signal };
  }
  try {
    const res = await deps.fetch(url, init);
    if (!res.ok) {
      const text = await res.text();
      throw new MqeFireError(`MQE HTTP ${res.status}: ${text.slice(0, 200)}`, []);
    }
    const env = await res.json();
    if (env.errors && env.errors.length > 0) {
      const msg = env.errors.map((e) => e.message).join("; ");
      throw new MqeFireError(`MQE error: ${msg}`, env.errors);
    }
    if (!env.data || !env.data.execExpression) {
      throw new MqeFireError("MQE response missing data.execExpression", []);
    }
    return env.data.execExpression;
  } finally {
    if (timer) clearTimeout(timer);
  }
}
var MqeFireError = class extends Error {
  constructor(message, graphqlErrors) {
    super(message);
    this.graphqlErrors = graphqlErrors;
    this.name = "MqeFireError";
  }
};

// src/util/time.ts
var GRAPHQL_QUERY2 = "query ServerTime {\n  getTimeInfo {\n    timezone\n    currentTimestamp\n  }\n}\n";
var CACHE_TTL_MS4 = 5 * 6e4;
var ServerTimeCache = class {
  entry = null;
  invalidate() {
    this.entry = null;
  }
  async get(deps) {
    const now = Date.now();
    if (this.entry && this.entry.expiresAt > now) return this.entry.value;
    const value = await resolveServerTime(deps);
    const ttl = value.source === "oap" ? CACHE_TTL_MS4 : 15e3;
    this.entry = { value, expiresAt: now + ttl };
    return value;
  }
};
async function resolveServerTime(deps) {
  let mqeBaseUrl;
  const timeoutMs = deps.config().oap.timeoutMs;
  const ctrl = timeoutMs > 0 ? new AbortController() : null;
  const timer = ctrl ? setTimeout(() => ctrl.abort(), timeoutMs) : null;
  try {
    const target = await deps.mqeTarget.resolve({
      config: () => deps.config(),
      fetch: deps.fetch
    });
    mqeBaseUrl = target.baseUrl;
    const headers = {
      "Content-Type": "application/json",
      Accept: "application/json"
    };
    const auth = deps.config().oap.auth;
    if (auth) {
      headers.authorization = "Basic " + Buffer.from(`${auth.username}:${auth.password}`, "utf8").toString("base64");
    }
    const init = {
      method: "POST",
      headers,
      body: JSON.stringify({ query: GRAPHQL_QUERY2 }),
      ...ctrl ? { signal: ctrl.signal } : {}
    };
    const res = await deps.fetch(`${target.baseUrl.replace(/\/$/, "")}/graphql`, init);
    if (!res.ok) {
      const txt = (await res.text()).slice(0, 200);
      return fallback(`HTTP ${res.status}: ${txt}`, mqeBaseUrl);
    }
    const env = await res.json();
    if (env.errors && env.errors.length > 0) {
      return fallback(env.errors.map((e) => e.message).join("; "), mqeBaseUrl);
    }
    const info = env.data?.getTimeInfo;
    if (!info || info.timezone === void 0 || info.timezone === null || typeof info.currentTimestamp !== "number") {
      return fallback("getTimeInfo missing timezone / currentTimestamp", mqeBaseUrl);
    }
    const offsetMinutes = parseTimezone(info.timezone);
    if (offsetMinutes === null) {
      return fallback(
        `getTimeInfo timezone is not parseable: ${JSON.stringify(info.timezone)}`,
        mqeBaseUrl
      );
    }
    return {
      offsetMinutes,
      currentTimestampMillis: info.currentTimestamp,
      source: "oap",
      mqeBaseUrl
    };
  } catch (err) {
    return fallback(err instanceof Error ? err.message : String(err), mqeBaseUrl);
  } finally {
    if (timer) clearTimeout(timer);
  }
}
function fallback(error, mqeBaseUrl) {
  return {
    offsetMinutes: -(/* @__PURE__ */ new Date()).getTimezoneOffset(),
    currentTimestampMillis: Date.now(),
    source: "fallback",
    error,
    ...mqeBaseUrl !== void 0 ? { mqeBaseUrl } : {}
  };
}
function parseTimezone(tz) {
  if (typeof tz === "number" && Number.isFinite(tz)) return hhmmIntegerToMinutes(tz);
  if (typeof tz !== "string") return null;
  const trimmed = tz.trim();
  const m = /^([+-]?)(\d{1,2}):?(\d{2})$/.exec(trimmed);
  if (!m) return null;
  const sign = m[1] === "-" ? -1 : 1;
  const hours = Number(m[2]);
  const mins = Number(m[3]);
  if (mins >= 60) return null;
  return sign * (hours * 60 + mins);
}
function hhmmIntegerToMinutes(tz) {
  const sign = tz < 0 ? -1 : 1;
  const abs = Math.abs(tz);
  const hours = Math.trunc(abs / 100);
  const mins = abs % 100;
  return sign * (hours * 60 + mins);
}

// src/http/admin/inspect.ts
var VALID_METRIC_TYPES = /* @__PURE__ */ new Set([
  "REGULAR_VALUE",
  "LABELED_VALUE",
  "HEATMAP",
  "SAMPLED_RECORD"
]);
var TRUTHY2 = /* @__PURE__ */ new Set(["true", "1", "yes"]);
function registerInspectRoutes(app2, deps) {
  const auth = requireAuth(deps);
  const attribution = new AttributionCache();
  const mqeTarget = new MqeTargetCache();
  const serverTime = new ServerTimeCache();
  function clients() {
    return buildOapClients(deps.config.current, { fetch: deps.fetch });
  }
  app2.get(
    "/api/inspect/metrics",
    { preHandler: auth },
    async (req, reply) => {
      if (!ensureVerb3(req, reply, deps, "inspect:read")) return;
      const q = req.query;
      const types = parseTypes(q.type, reply);
      if (types === null) return;
      const catalogs = parseCatalogs(q.catalog);
      const args = {};
      if (typeof q.regex === "string" && q.regex.length > 0) args.regex = q.regex;
      if (types.length > 0) args.type = types;
      if (catalogs.length > 0) args.catalog = catalogs;
      if (typeof q.mqeQueryable === "string" && TRUTHY2.has(q.mqeQueryable.toLowerCase())) {
        args.mqeQueryable = true;
      }
      try {
        const got = await clients().inspect().listMetrics(args);
        return reply.send(got);
      } catch (err) {
        return passInspectError(err, reply, "/inspect/metrics");
      }
    }
  );
  app2.get(
    "/api/inspect/catalog",
    { preHandler: auth },
    async (req, reply) => {
      if (!ensureVerb3(req, reply, deps, "inspect:read")) return;
      const q = req.query;
      if (q.refresh === "true" || q.refresh === "1") attribution.invalidate();
      try {
        const c = clients();
        const [metricsRes, idx] = await Promise.all([
          c.inspect().listMetrics(),
          attribution.get({ oal: () => c.oal(), rules: () => c.primary() })
        ]);
        const entries = metricsRes.metrics.map((m) => {
          const attr = attributeOrUnknown(idx, m.name);
          return {
            ...m,
            attribution: attr
          };
        });
        const summary = {};
        for (const e of entries) {
          summary[e.attribution.source] = (summary[e.attribution.source] ?? 0) + 1;
        }
        return reply.send({
          metrics: entries,
          summary,
          attributionFingerprint: idx.fingerprint
        });
      } catch (err) {
        return passInspectError(err, reply, "/inspect/metrics");
      }
    }
  );
  app2.get(
    "/api/inspect/mqe-target",
    { preHandler: auth },
    async (req, reply) => {
      if (!ensureVerb3(req, reply, deps, "inspect:read")) return;
      const q = req.query;
      if (q.refresh === "true" || q.refresh === "1") mqeTarget.invalidate();
      try {
        const target = await mqeTarget.resolve({
          config: () => deps.config.current,
          fetch: deps.fetch ?? globalThis.fetch.bind(globalThis)
        });
        return reply.send(target);
      } catch (err) {
        return reply.code(502).send({
          error: "mqe_target_unresolved",
          message: err instanceof Error ? err.message : String(err)
        });
      }
    }
  );
  app2.get(
    "/api/inspect/server-time",
    { preHandler: auth },
    async (req, reply) => {
      if (!ensureVerb3(req, reply, deps, "inspect:read")) return;
      const q = req.query;
      if (q.refresh === "true" || q.refresh === "1") serverTime.invalidate();
      const fetchImpl = deps.fetch ?? globalThis.fetch.bind(globalThis);
      const value = await serverTime.get({
        config: () => deps.config.current,
        fetch: fetchImpl,
        mqeTarget
      });
      return reply.send(value);
    }
  );
  app2.post(
    "/api/inspect/exec",
    { preHandler: auth },
    async (req, reply) => {
      if (!ensureVerb3(req, reply, deps, "inspect:read")) return;
      const body = parseExecBody(req.body, reply);
      if (!body) return;
      const cfg = deps.config.current;
      const fetchImpl = deps.fetch ?? globalThis.fetch.bind(globalThis);
      try {
        const target = await mqeTarget.resolve({
          config: () => cfg,
          fetch: fetchImpl
        });
        const result = await fireMqe(target, body, {
          fetch: fetchImpl,
          timeoutMs: cfg.oap.timeoutMs,
          ...cfg.oap.auth ? { auth: cfg.oap.auth } : {}
        });
        return reply.send(result);
      } catch (err) {
        if (err instanceof MqeFireError) {
          return reply.code(502).send({
            error: "mqe_error",
            message: err.message,
            graphqlErrors: err.graphqlErrors
          });
        }
        return reply.code(502).send({
          error: "mqe_target_unresolved",
          message: err instanceof Error ? err.message : String(err)
        });
      }
    }
  );
  app2.get(
    "/api/inspect/entities",
    { preHandler: auth },
    async (req, reply) => {
      if (!ensureVerb3(req, reply, deps, "inspect:read")) return;
      const q = req.query;
      if (!q.metric) return reply.code(400).send({ error: "missing_metric" });
      if (!q.start) return reply.code(400).send({ error: "missing_start" });
      if (!q.end) return reply.code(400).send({ error: "missing_end" });
      if (!q.step) return reply.code(400).send({ error: "missing_step" });
      const step = parseStep(q.step, reply);
      if (step === null) return;
      if (!isInspectDate2(q.start, step)) {
        return reply.code(400).send({ error: "invalid_start_format", step, value: q.start });
      }
      if (!isInspectDate2(q.end, step)) {
        return reply.code(400).send({ error: "invalid_end_format", step, value: q.end });
      }
      let limit;
      if (q.limit !== void 0) {
        const parsed = Number.parseInt(q.limit, 10);
        if (!Number.isInteger(parsed) || parsed < 1 || parsed > INSPECT_ENTITY_LIMIT_MAX) {
          return reply.code(400).send({
            error: "invalid_limit",
            min: 1,
            max: INSPECT_ENTITY_LIMIT_MAX,
            value: q.limit
          });
        }
        limit = parsed;
      }
      try {
        const got = await clients().inspect().listEntities({
          metric: q.metric,
          start: q.start,
          end: q.end,
          step,
          ...limit !== void 0 ? { limit } : {}
        });
        return reply.send(got);
      } catch (err) {
        return passInspectError(err, reply, "/inspect/entities");
      }
    }
  );
}
function parseStep(raw, reply) {
  const upper = raw.toUpperCase();
  if (INSPECT_STEPS2.includes(upper)) return upper;
  reply.code(400).send({
    error: "invalid_step",
    value: raw,
    allowed: INSPECT_STEPS2
  });
  return null;
}
function parseTypes(raw, reply) {
  if (raw === void 0) return [];
  const arr = Array.isArray(raw) ? raw : [raw];
  const out = [];
  for (const v of arr) {
    const upper = v.toUpperCase();
    if (!VALID_METRIC_TYPES.has(upper)) {
      reply.code(400).send({ error: "invalid_type", value: v });
      return null;
    }
    if (!out.includes(upper)) out.push(upper);
  }
  return out;
}
function parseCatalogs(raw) {
  if (raw === void 0) return [];
  const arr = Array.isArray(raw) ? raw : [raw];
  const out = [];
  for (const v of arr) {
    const upper = v.toUpperCase();
    if (!out.includes(upper)) out.push(upper);
  }
  return out;
}
function ensureVerb3(req, reply, deps, verb) {
  const session = 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 passInspectError(err, reply, path2) {
  if (err instanceof InspectApiError) {
    if (err.status === 404) {
      return reply.code(404).send({
        error: "inspect_not_enabled",
        message: "OAP did not bind the inspect routes. Set SW_INSPECT=default on the admin-server.",
        path: path2
      });
    }
    return reply.code(err.status).send(err.body);
  }
  return reply.code(502).send({
    error: "oap_unreachable",
    message: err instanceof Error ? err.message : String(err),
    path: path2
  });
}

// src/client/config-dump.ts
async function fetchConfigDump3(adminUrl, opts) {
  const f = opts.fetch ?? globalThis.fetch.bind(globalThis);
  const url = `${adminUrl.replace(/\/$/, "")}/debugging/config/dump`;
  const headers = { accept: "application/json" };
  if (opts.auth) {
    headers.authorization = basicAuthHeader(opts.auth.username, opts.auth.password);
  }
  let init = { method: "GET", headers };
  let timer = null;
  if (opts.timeoutMs > 0) {
    const ctrl = new AbortController();
    timer = setTimeout(() => ctrl.abort(), opts.timeoutMs);
    init = { ...init, signal: ctrl.signal };
  }
  try {
    const res = await f(url, init);
    if (!res.ok) {
      const text = (await res.text().catch(() => "")).slice(0, 200);
      throw new Error(`HTTP ${res.status} at ${url}${text ? ` \u2014 ${text}` : ""}`);
    }
    return await res.json();
  } finally {
    if (timer) clearTimeout(timer);
  }
}

// src/http/admin/oap-config.ts
function registerOapConfigRoute(app2, deps) {
  const auth = requireAuth(deps);
  app2.get("/api/oap/config", { preHandler: auth }, async (_req, reply) => {
    const cfg = deps.config.current;
    const adminUrl = cfg.oap.adminUrl;
    try {
      const dump = await fetchConfigDump3(adminUrl, {
        fetch: deps.fetch,
        timeoutMs: cfg.oap.timeoutMs,
        auth: cfg.oap.auth
      });
      const entries = Object.entries(dump).map(([key, value]) => ({ key, value: String(value), module: key.split(".", 1)[0] })).sort((a, b) => a.key.localeCompare(b.key));
      const body = {
        reachable: true,
        adminUrl,
        entries,
        generatedAt: Date.now()
      };
      return reply.send(body);
    } catch (err) {
      const body = {
        reachable: false,
        error: err instanceof Error ? err.message : String(err),
        adminUrl,
        entries: [],
        generatedAt: Date.now()
      };
      return reply.status(200).send(body);
    }
  });
}

// src/http/admin/alarm-rules.ts
import { AlarmStatusApiError } from "@skywalking-horizon-ui/api-client";
function pivot(listResp, detailById) {
  const nodes = listResp.oapInstances.map((i) => ({
    address: i.address,
    ok: !i.errorMsg && !!i.status,
    error: i.errorMsg ?? void 0
  }));
  const totalNodes = nodes.length || 1;
  const perRuleNodes = /* @__PURE__ */ new Map();
  for (const inst of listResp.oapInstances) {
    if (!inst.status) continue;
    for (const r of inst.status.ruleList ?? []) {
      let set = perRuleNodes.get(r.id);
      if (!set) {
        set = /* @__PURE__ */ new Set();
        perRuleNodes.set(r.id, set);
      }
      set.add(inst.address);
    }
  }
  const rules = [];
  for (const [ruleId, loadedAddrs] of perRuleNodes) {
    const detailEnvOrErr = detailById.get(ruleId);
    let bestDetail = null;
    if (detailEnvOrErr && !(detailEnvOrErr instanceof Error)) {
      for (const inst of detailEnvOrErr.oapInstances) {
        if (inst.status) {
          bestDetail = inst.status;
          break;
        }
      }
    }
    rules.push({
      ruleId,
      detail: bestDetail,
      nodes: listResp.oapInstances.map((i) => ({
        address: i.address,
        ok: !i.errorMsg && !!i.status,
        error: i.errorMsg ?? void 0,
        loaded: loadedAddrs.has(i.address)
      })),
      loadedOn: loadedAddrs.size,
      totalNodes
    });
  }
  rules.sort((a, b) => a.ruleId.localeCompare(b.ruleId));
  return { rules, nodes };
}
function registerAlarmRulesRoutes(app2, deps) {
  const auth = requireAuth(deps);
  function client() {
    return buildOapClients(deps.config.current, { fetch: deps.fetch }).alarmStatus();
  }
  app2.get(
    "/api/admin/alarm-rules",
    { preHandler: auth },
    async (_req, reply) => {
      const c = client();
      let listResp;
      try {
        listResp = await c.listRules();
      } catch (err) {
        return reply.send({
          generatedAt: Date.now(),
          reachable: false,
          error: err instanceof Error ? err.message : String(err),
          rules: [],
          nodes: []
        });
      }
      const ids = /* @__PURE__ */ new Set();
      for (const inst of listResp.oapInstances) {
        for (const r of inst.status?.ruleList ?? []) ids.add(r.id);
      }
      const detailById = /* @__PURE__ */ new Map();
      await Promise.all(
        Array.from(ids).map(async (id) => {
          try {
            const env = await c.ruleDetail(id);
            detailById.set(id, env);
          } catch (err) {
            detailById.set(id, err instanceof Error ? err : new Error(String(err)));
          }
        })
      );
      const { rules, nodes } = pivot(listResp, detailById);
      const body = {
        generatedAt: Date.now(),
        reachable: nodes.some((n) => n.ok),
        rules,
        nodes
      };
      return reply.send(body);
    }
  );
  app2.get(
    "/api/admin/alarm-rules/:id",
    { preHandler: auth },
    async (req, reply) => {
      const id = req.params.id;
      if (!id) return reply.code(400).send({ error: "missing_id" });
      const c = client();
      let env;
      try {
        env = await c.ruleDetail(id);
      } catch (err) {
        const status = err instanceof AlarmStatusApiError && err.status === 404 ? 404 : 502;
        return reply.code(status).send({
          ruleId: id,
          generatedAt: Date.now(),
          reachable: false,
          error: err instanceof Error ? err.message : String(err),
          detail: null,
          nodes: []
        });
      }
      const perNode = env.oapInstances.map((i) => ({
        address: i.address,
        ok: !i.errorMsg && !!i.status,
        error: i.errorMsg ?? void 0,
        detail: i.status ?? null
      }));
      const detail = perNode.find((n) => n.detail !== null)?.detail ?? null;
      const body = {
        ruleId: id,
        generatedAt: Date.now(),
        reachable: perNode.some((n) => n.ok),
        detail,
        nodes: perNode
      };
      return reply.send(body);
    }
  );
}

// src/http/admin/overview-templates.ts
import { z as z8 } from "zod";
var WIDGET_TYPES2 = [
  "metric",
  "topology",
  "section-break",
  "kpi-tile",
  "alarms",
  "metric-composite"
];
var kpiSchema = z8.object({
  label: z8.string().min(1),
  /* `mqe` is required when source is 'mqe' (the default), optional
   * when source is 'service-count'. Cross-field check below. */
  mqe: z8.string().optional(),
  unit: z8.string().optional(),
  aggregation: z8.enum(["sum", "avg"]).optional(),
  style: z8.enum(["number", "progress-bar"]).optional(),
  /* `max` is only meaningful when style is `progress-bar` — but
   * we accept it for any style and let the renderer ignore it
   * otherwise. Validating that pairing here would add false
   * positives during in-flight edits. */
  max: z8.number().positive().optional(),
  source: z8.enum(["mqe", "service-count"]).optional()
}).refine(
  (k) => {
    const src = k.source ?? "mqe";
    if (src === "mqe") return typeof k.mqe === "string" && k.mqe.length > 0;
    return true;
  },
  { message: 'mqe is required when source is "mqe"' }
).refine((k) => k.style !== "progress-bar" || typeof k.max === "number" && k.max > 0, {
  message: "progress-bar style requires `max > 0`"
});
var widgetSchema2 = z8.object({
  id: z8.string().min(1),
  title: z8.string().min(1),
  tip: z8.string().optional(),
  layer: z8.string().optional(),
  type: z8.enum(WIDGET_TYPES2),
  mqe: z8.string().optional(),
  unit: z8.string().optional(),
  aggregation: z8.enum(["sum", "avg"]).optional(),
  cols: z8.number().int().optional(),
  kpis: z8.array(kpiSchema).optional(),
  showCount: z8.boolean().optional(),
  limit: z8.number().int().min(1).max(100).optional(),
  span: z8.number().int().min(1).max(12).optional(),
  rowSpan: z8.number().int().min(1).max(12).optional()
});
var dashboardSchema = z8.object({
  id: z8.string().min(1),
  title: z8.string().min(1),
  description: z8.string().optional(),
  visibility: z8.enum(["public", "operate"]).optional(),
  icon: z8.string().optional(),
  order: z8.number().optional(),
  layers: z8.array(z8.string()).optional(),
  widgets: z8.array(widgetSchema2).max(64)
});
function registerOverviewTemplatesAdminRoutes(app2, deps) {
  const auth = requireAuth(deps);
  app2.get(
    "/api/admin/overview-templates",
    { preHandler: auth },
    async (_req, reply) => {
      const dashboards = loadOverviewDashboards();
      return reply.send({
        generatedAt: Date.now(),
        dashboards: dashboards.map((d) => ({
          id: d.id,
          title: d.title,
          description: d.description,
          widgetCount: d.widgets.length,
          editable: !!findOverviewFile(d.id)
        }))
      });
    }
  );
  app2.get(
    "/api/admin/overview-templates/:id",
    { preHandler: auth },
    async (req, reply) => {
      const { id } = req.params;
      const dash = getOverviewDashboard(id);
      if (!dash) return reply.code(404).send({ error: "not_found", id });
      return reply.send({
        generatedAt: Date.now(),
        dashboard: dash,
        editable: !!findOverviewFile(id)
      });
    }
  );
  app2.post(
    "/api/admin/overview-templates",
    { preHandler: auth },
    async (req, reply) => {
      const parsed = dashboardSchema.safeParse(req.body);
      if (!parsed.success) {
        return reply.code(400).send({ error: "invalid_body", detail: parsed.error.flatten() });
      }
      try {
        createOverviewDashboard(parsed.data);
      } catch (err) {
        return reply.code(409).send({
          error: "create_failed",
          message: err instanceof Error ? err.message : String(err)
        });
      }
      return reply.send({ ok: true, id: parsed.data.id });
    }
  );
  app2.delete(
    "/api/admin/overview-templates/:id",
    { preHandler: auth },
    async (req, reply) => {
      const { id } = req.params;
      try {
        deleteOverviewDashboard(id);
      } catch (err) {
        return reply.code(500).send({
          error: "delete_failed",
          message: err instanceof Error ? err.message : String(err)
        });
      }
      return reply.send({ ok: true, id });
    }
  );
}

// src/http/admin/auth-status.ts
import { statSync } from "node:fs";
import { z as z9 } from "zod";
var probeBodySchema = z9.object({
  username: z9.string().min(1).optional()
});
function hostnameOf(url) {
  try {
    return new URL(url).host;
  } catch {
    return url;
  }
}
function registerAuthStatusRoutes(app2, deps) {
  const { config: source2, ldapHealth: ldapHealth2, sessions: sessions2 } = deps;
  app2.get("/api/admin/auth-status", async () => {
    const cfg = source2.current;
    let configMtime = null;
    let configSizeBytes = null;
    try {
      const st = statSync(source2.path);
      configMtime = st.mtimeMs;
      configSizeBytes = st.size;
    } catch {
    }
    const bothPresent = cfg.auth.backend === "ldap" && cfg.auth.local.users.length > 0;
    let ldapBlock = null;
    let breakGlassArmed = false;
    if (cfg.auth.backend === "ldap" && cfg.auth.ldap) {
      const snap = ldapHealth2.snapshot();
      const reachable = snap.result?.reachable ?? false;
      ldapBlock = {
        url: cfg.auth.ldap.url,
        host: hostnameOf(cfg.auth.ldap.url),
        bindDn: cfg.auth.ldap.bindDn,
        anonymous: cfg.auth.ldap.bindDn === "",
        userBaseDn: cfg.auth.ldap.userBaseDn,
        groupStrategy: cfg.auth.ldap.groupStrategy,
        groupMappings: cfg.auth.ldap.groupMappings.map((m) => ({ group: m.group, role: m.role })),
        probe: {
          reachable,
          serviceBindOk: snap.result?.serviceBindOk ?? null,
          userSearchOk: snap.result?.userSearchOk ?? null,
          userEntriesVisible: snap.result?.userEntriesVisible ?? null,
          latencyMs: snap.result?.latencyMs ?? null,
          error: snap.result?.error,
          at: snap.at
        }
      };
      breakGlassArmed = !!cfg.auth.breakGlass && !reachable;
    }
    const body = {
      configPath: source2.path,
      configMtime,
      configSizeBytes,
      backend: cfg.auth.backend,
      bothPresent,
      sessions: { active: sessions2.size() },
      local: {
        users: cfg.auth.local.users.length,
        role: cfg.auth.backend === "local" ? "primary" : "break-glass-only"
      },
      ldap: ldapBlock,
      breakGlass: {
        configured: !!cfg.auth.breakGlass,
        armed: breakGlassArmed,
        username: cfg.auth.breakGlass?.username ?? null
      },
      rbac: {
        enabled: cfg.rbac.enabled,
        roles: cfg.rbac.roles,
        landingByRole: cfg.rbac.landingByRole,
        knownVerbs: Object.values(VERBS).sort()
      }
    };
    return body;
  });
  app2.post("/api/admin/auth-status/probe", async (req, reply) => {
    const cfg = source2.current;
    if (cfg.auth.backend !== "ldap" || !cfg.auth.ldap) {
      return reply.code(409).send({ error: "ldap_not_configured" });
    }
    const parsed = probeBodySchema.safeParse(req.body ?? {});
    if (!parsed.success) return reply.code(400).send({ error: "invalid_body" });
    const fresh = await probeLdap(cfg.auth.ldap);
    await ldapHealth2.probe(cfg.auth.ldap).catch(() => void 0);
    let resolved = null;
    if (parsed.data.username) {
      const r = await resolveLdapUser(cfg.auth.ldap, parsed.data.username);
      resolved = { username: parsed.data.username, ...r };
    }
    return { probe: fresh, resolved };
  });
}

// src/http/admin/users.ts
import { hostname } from "node:os";
function registerAdminUsersRoute(app2, deps) {
  const { config: source2, seenCache: seenCache2 } = deps;
  app2.get("/api/admin/users", async () => {
    const cfg = source2.current;
    const seenByName = new Map(seenCache2.list().map((u) => [u.username, u]));
    const localByName = new Map(cfg.auth.local.users.map((u) => [u.username, u]));
    const allNames = /* @__PURE__ */ new Set([...seenByName.keys(), ...localByName.keys()]);
    const rows = [];
    for (const name of allNames) {
      const seen = seenByName.get(name);
      const local = localByName.get(name);
      const fallbackOnly = cfg.auth.backend === "ldap" && !!local;
      if (seen) {
        rows.push({
          username: name,
          source: seen.source,
          roles: seen.roles,
          lastSeenAt: seen.lastSeenAt,
          lastIp: seen.lastIp,
          staticOnly: false,
          fallbackOnly
        });
      } else if (local) {
        rows.push({
          username: name,
          source: "local",
          roles: local.roles,
          lastSeenAt: null,
          lastIp: null,
          staticOnly: true,
          fallbackOnly
        });
      }
    }
    rows.sort((a, b) => (b.lastSeenAt ?? 0) - (a.lastSeenAt ?? 0));
    const now = Date.now();
    const dayMs = 24 * 60 * 60 * 1e3;
    const counts = {
      total: rows.length,
      fromLdap: rows.filter((r) => r.source === "ldap").length,
      local: rows.filter((r) => r.source === "local" || r.source === "break-glass").length,
      activeLast24h: rows.filter((r) => r.lastSeenAt !== null && now - r.lastSeenAt <= dayMs).length
    };
    const body = {
      generatedAt: now,
      backend: cfg.auth.backend,
      node: hostname(),
      rows,
      counts
    };
    return body;
  });
}

// src/http/auth-health.ts
function setupHintFor(cfg) {
  if (cfg.auth.backend === "local" && cfg.auth.local.users.length === 0) {
    return "No users configured. Add at least one entry to auth.local.users in horizon.yaml (use `pnpm --filter bff cli:hash` for the password hash) or switch to LDAP.";
  }
  if (cfg.auth.backend === "ldap") {
    if (!cfg.auth.ldap) {
      return "LDAP backend selected but auth.ldap block is missing. Configure the directory connection in horizon.yaml or switch to local users.";
    }
    if (cfg.auth.ldap.groupMappings.length === 0) {
      return 'LDAP backend has no group \u2192 role mappings. Add at least one auth.ldap.groupMappings entry (use `group: "*"` to assign a fallback role to every authenticated user).';
    }
  }
  return "";
}
function hostnameOf2(url) {
  try {
    return new URL(url).host;
  } catch {
    return url;
  }
}
function registerAuthHealthRoute(app2, deps) {
  const { config: source2, ldapHealth: ldapHealth2 } = deps;
  app2.get("/api/auth/health", async () => {
    const cfg = source2.current;
    let ldap = null;
    if (cfg.auth.backend === "ldap" && cfg.auth.ldap) {
      await ldapHealth2.getOrProbe(cfg.auth.ldap, 3e4).catch(() => void 0);
      const snap = ldapHealth2.snapshot();
      const reachable = snap.result?.reachable ?? false;
      ldap = {
        reachable,
        host: hostnameOf2(cfg.auth.ldap.url),
        lastProbeAt: snap.at,
        lastSuccessAgoSeconds: reachable && snap.at ? Math.round((Date.now() - snap.at) / 1e3) : null,
        error: snap.result?.error ?? null
      };
    }
    const breakGlassArmed = !!cfg.auth.breakGlass && (ldap === null ? false : !ldap.reachable);
    const configured = isAuthConfigured(cfg);
    const body = {
      backend: cfg.auth.backend,
      configured,
      setupHint: configured ? "" : setupHintFor(cfg),
      ldap,
      breakGlass: { armed: breakGlassArmed }
    };
    return body;
  });
}

// src/logic/setup/store.ts
import { existsSync as existsSync4 } from "node:fs";
import { mkdir as mkdir3, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
import { dirname as dirname6, resolve as resolve6 } from "node:path";
var SetupStore = class {
  absPath;
  cache = null;
  writing = null;
  constructor(filePath) {
    this.absPath = resolve6(filePath);
  }
  async load() {
    if (this.cache) return this.cache;
    if (!existsSync4(this.absPath)) {
      this.cache = {};
      return this.cache;
    }
    try {
      const raw = await readFile2(this.absPath, "utf8");
      const parsed = JSON.parse(raw);
      const layers = parsed.layers ?? parsed;
      this.cache = layers && typeof layers === "object" ? layers : {};
      return this.cache;
    } catch (err) {
      logger.warn({ err, path: this.absPath }, "setup store unreadable; starting empty");
      this.cache = {};
      return this.cache;
    }
  }
  async save(layers) {
    while (this.writing) await this.writing;
    const tmp = `${this.absPath}.tmp`;
    const next = JSON.parse(JSON.stringify(layers));
    const work = (async () => {
      await mkdir3(dirname6(this.absPath), { recursive: true });
      await writeFile2(
        tmp,
        JSON.stringify({ generatedAt: Date.now(), layers: next }, null, 2),
        "utf8"
      );
      await rename2(tmp, this.absPath);
      this.cache = next;
    })();
    this.writing = work.finally(() => {
      this.writing = null;
    });
    await this.writing;
  }
  /** Force reload from disk — useful when an admin edits the JSON externally. */
  invalidate() {
    this.cache = null;
  }
};

// src/logic/alarms/service-layer-map.ts
var LAYERS_QUERY = (
  /* GraphQL */
  `
  query HorizonAlarmsLayers {
    layers: listLayers
  }
`
);
function fragment(idx, layer) {
  return `_${idx}: listServices(layer: ${JSON.stringify(layer)}) { name }`;
}
var ServiceLayerMap = class {
  constructor(deps) {
    this.deps = deps;
  }
  cached = null;
  lastFetchAt = 0;
  inflight = null;
  /** ms */
  ttl = 6e4;
  async get() {
    const now = Date.now();
    if (this.cached && now - this.lastFetchAt < this.ttl) return this.cached;
    if (this.inflight) return this.inflight;
    this.inflight = this.refresh().then((r) => {
      this.cached = r;
      this.lastFetchAt = Date.now();
      return r;
    }).finally(() => {
      this.inflight = null;
    });
    return this.inflight;
  }
  /** Force a refresh on the next `get()`. Called after the alarms-config
   *  layer list changes — the new layer might not be in the existing
   *  cache. */
  invalidate() {
    this.cached = null;
    this.lastFetchAt = 0;
  }
  async refresh() {
    const cfg = this.deps.config.current;
    const opts = buildOapOpts(cfg, this.deps.fetch);
    let layers;
    try {
      const got = await graphqlPost(opts, LAYERS_QUERY);
      layers = Array.isArray(got.layers) ? got.layers : [];
    } catch (err) {
      logger.warn({ err }, "alarms service-layer-map: listLayers failed");
      return { layers: [], byName: /* @__PURE__ */ new Map() };
    }
    if (layers.length === 0) return { layers, byName: /* @__PURE__ */ new Map() };
    const aliased = layers.map((l, i) => fragment(i, l)).join("\n");
    const query = `query HorizonAlarmsServices { ${aliased} }`;
    try {
      const data = await graphqlPost(opts, query);
      const byName = /* @__PURE__ */ new Map();
      layers.forEach((layer, i) => {
        const rows = data[`_${i}`] ?? [];
        for (const row of rows) {
          if (row?.name) byName.set(row.name.toLowerCase(), layer);
        }
      });
      return { layers, byName };
    } catch (err) {
      logger.warn({ err }, "alarms service-layer-map: listServices fan-out failed");
      return { layers, byName: /* @__PURE__ */ new Map() };
    }
  }
};

// src/server.ts
var configPath = process.env.HORIZON_CONFIG ?? "./horizon.yaml";
var source;
try {
  source = loadConfig(configPath);
} catch (err) {
  if (err instanceof BootstrapError) {
    logger.fatal({ err: err.message, configPath }, "BFF refusing to start: bootstrap validation failed");
    process.exit(1);
  }
  throw err;
}
logger.info(
  { configPath: source.path, backend: source.current.auth.backend },
  "config loaded"
);
if (source.current.auth.backend === "ldap" && source.current.auth.local.users.length > 0) {
  logger.warn(
    { users: source.current.auth.local.users.length },
    'auth.local.users is populated but auth.backend is "ldap"; local users are ignored'
  );
}
source.onChange((cfg) => logger.info({ backend: cfg.auth.backend }, "config reloaded"));
var app = Fastify({ logger: loggerOptions });
app.setErrorHandler((err, _req, reply) => {
  if (err instanceof HttpError) {
    return reply.status(err.statusCode).send({ code: err.code, message: err.message, details: err.details });
  }
  const message = err instanceof Error ? err.message : "internal error";
  reply.log.error({ err }, "unhandled");
  return reply.status(500).send({ code: "internal_error", message });
});
var sessions = new SessionStore({ ttlMinutes: source.current.session.ttlMinutes });
var audit = new AuditLogger(source.current.audit.file);
await audit.open();
var ldapHealth = new LdapHealth();
var seenCache = new UserSeenCache();
var setupStore = new SetupStore(source.current.setup.file);
await setupStore.load();
var alarmsStore = new AlarmsStore(source.current.alarms.file);
await alarmsStore.load();
var serviceLayer = new ServiceLayerMap({ config: source });
await app.register(cookie);
app.addContentTypeParser("text/plain", { parseAs: "string" }, (_req, body, done) => done(null, body));
app.addHook("onRoute", makeRouteAuthHook({ config: source, sessions }));
registerAuthRoutes(app, { config: source, sessions, audit, ldapHealth, seenCache });
registerAuthHealthRoute(app, { config: source, ldapHealth });
registerOapInfoRoute(app, { config: source, sessions });
registerMenuRoute(app, { config: source, sessions });
registerLandingRoute(app, { config: source, sessions });
registerInstanceRoute(app, { config: source, sessions });
registerEndpointRoute(app, { config: source, sessions });
registerTopologyRoute(app, { config: source, sessions });
registerEndpointDependencyRoute(app, { config: source, sessions });
registerTraceRoutes(app, { config: source, sessions });
registerTraceTagRoutes(app, { config: source, sessions });
registerZipkinRoutes(app, { config: source, sessions });
registerLogRoute(app, { config: source, sessions });
registerDashboardQueryRoute(app, { config: source, sessions });
registerAlarmsQueryRoutes(app, { config: source, sessions, serviceLayer });
registerPreflightRoutes(app, { config: source, sessions });
registerTtlRoute(app, { config: source, sessions });
registerProfileRoutes(app, { config: source, sessions });
registerEBPFRoutes(app, { config: source, sessions });
registerAsyncProfileRoutes(app, { config: source, sessions });
registerDashboardConfigRoute(app, { config: source, sessions });
registerLayerTemplateRoutes(app, { config: source, sessions });
if (process.env.NODE_ENV !== "test") startLayerTemplateWatcher();
registerAlarmsConfigRoutes(app, { config: source, sessions, audit, store: alarmsStore, serviceLayer });
registerSetupRoutes(app, { config: source, sessions, audit, store: setupStore });
registerOverviewRoutes(app, { config: source, sessions });
registerConfigBundleRoute(app, {
  config: source,
  sessions,
  uiTemplateClient: () => buildOapClients(source.current).uiTemplate()
});
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 });
registerOapConfigRoute(app, { config: source, sessions });
registerAlarmRulesRoutes(app, { config: source, sessions });
registerOverviewTemplatesAdminRoutes(app, { config: source, sessions });
registerAuthStatusRoutes(app, { config: source, ldapHealth, sessions });
registerAdminUsersRoute(app, { config: source, seenCache });
registerTemplateSyncAdminRoutes(app, {
  config: source,
  sessions,
  uiTemplateClient: () => buildOapClients(source.current).uiTemplate()
});
var staticDir = (() => {
  const raw = process.env.HORIZON_STATIC_DIR ?? source.current.server.staticDir;
  return raw ? resolvePath(raw) : null;
})();
if (staticDir && existsSync5(staticDir)) {
  await app.register(fastifyStatic, { root: staticDir, prefix: "/", wildcard: false });
  app.setNotFoundHandler((req, reply) => {
    if (req.url.startsWith("/api/")) {
      return reply.code(404).send({ code: "not_found", message: req.url });
    }
    return reply.sendFile("index.html");
  });
  logger.info({ staticDir }, "serving SPA from static dir");
}
app.get("/api/health", async () => ({
  status: "ok",
  version: process.env.HORIZON_VERSION ?? "0.5.0",
  sessions: sessions.size()
}));
var { host, port } = source.current.server;
app.listen({ host, port }).then(
  () => {
    logger.info(`BFF listening on http://${host}:${port}`);
    void bootSeed({
      client: buildOapClients(source.current).uiTemplate(),
      bundled: () => iterateBundledTemplates(),
      logger
    }).then((status) => {
      if (status.unreachable) {
        logger.warn(
          { lastSuccessfulSyncAt: status.lastSuccessfulSyncAt },
          "OAP UI-template boot seed: admin unreachable, rendering bundled (admin pages will be read-only until OAP comes back)"
        );
      } else {
        const counts = countByStatus(status.rows);
        logger.info(counts, "OAP UI-template boot seed: complete");
      }
    }).catch((err) => {
      logger.error({ err }, "OAP UI-template boot seed: unexpected error");
    });
  },
  (err) => {
    logger.fatal({ err }, "failed to start BFF");
    process.exit(1);
  }
);
function countByStatus(rows) {
  const out = {};
  for (const r of rows) out[r.status] = (out[r.status] ?? 0) + 1;
  return out;
}
async function shutdown(signal) {
  logger.info({ signal }, "shutting down");
  await app.close();
  await sessions.close();
  await audit.close();
  await source.close();
  process.exit(0);
}
process.on("SIGINT", () => void shutdown("SIGINT"));
process.on("SIGTERM", () => void shutdown("SIGTERM"));
