This is an automated email from the ASF dual-hosted git repository. wu-sheng pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit f1e7e0dcaaf64fa8e7d5be20f309f3d3f5a4e93d Author: Wu Sheng <[email protected]> AuthorDate: Thu May 21 11:27:58 2026 +0800 skills: add local-boot + build-dashboard, drop migrate-layer local-boot: boot the BFF+UI dev env against a local or demo OAP from bundled static configs (horizon.local.yaml / horizon.demo.yaml, demo password via ${OAP_PASSWORD}). Captures the boot gotchas: absolute HORIZON_CONFIG (apps/bff cwd resolves ./horizon.yaml to a missing file -> empty-users default -> login fails), and the proxy/IPv4 trap (Vite binds IPv6 [::1] by default and a local proxy 502s loopback — force --host 127.0.0.1 and bypass the proxy). build-dashboard: from-scratch authoring guide for overview dashboards and per-layer dashboards — schemas, widget types, the entity-scope and card-vs-line MQE rules, and mandatory live-OAP validation. Migration is done, so migrate-layer is removed. Exclude .claude/** from the license-header check (agent/dev tooling, not shippable source). --- .claude/skills/build-dashboard/SKILL.md | 119 +++++++++++++++++++++++++++ .claude/skills/local-boot/SKILL.md | 102 +++++++++++++++++++++++ .claude/skills/local-boot/horizon.demo.yaml | 114 +++++++++++++++++++++++++ .claude/skills/local-boot/horizon.local.yaml | 111 +++++++++++++++++++++++++ .licenserc.yaml | 3 + 5 files changed, 449 insertions(+) diff --git a/.claude/skills/build-dashboard/SKILL.md b/.claude/skills/build-dashboard/SKILL.md new file mode 100644 index 0000000..a55d595 --- /dev/null +++ b/.claude/skills/build-dashboard/SKILL.md @@ -0,0 +1,119 @@ +--- +name: build-dashboard +description: Build a NEW Horizon UI dashboard from scratch — either a cross-layer Overview dashboard (apps/bff/src/bundled_templates/overviews/*.json) or a per-layer dashboard widget set (apps/bff/src/bundled_templates/layers/*.json). Greenfield authoring, NOT migration: design the widgets, write the MQE, validate every expression against a live OAP, then render-check in the browser. +user-invocable: true +--- + +# Build a new Horizon dashboard + +Two dashboard families live in the BFF as static JSON, loaded at boot and served to the SPA's generic renderers. You author JSON; you do not write Vue. + +| Family | JSON dir | TS shape | Served by | Rendered by | +|---|---|---|---|---| +| **Overview** (cross-layer landing) | `apps/bff/src/bundled_templates/overviews/*.json` | `OverviewDashboard` — `packages/api-client/src/overview.ts` | `GET /api/overview/dashboards[/:id]` (`apps/bff/src/http/config/overview.ts`) | `apps/ui/src/render/overview/` + `apps/ui/src/render/widgets/` | +| **Layer dashboard** (per-layer drill-down) | `apps/bff/src/bundled_templates/layers/*.json` | `LayerTemplate` — `apps/bff/src/logic/layers/loader.ts`; widgets are `DashboardWidget` — `packages/api-client/src/dashboard.ts` | `GET /api/admin/layer-templates`, data via `POST /api/layer/:key/dashboard` | `apps/ui/src/render/layer-dashboard/` | + +**Always read the TS interface for the family you're touching before writing JSON** — fields change, and the interface (plus its comments) is the contract. The existing `general.json` (layer) and `services.json` (overview) are the canonical references; copy their shape and swap the metrics, don't invent a new structure. + +--- + +## The non-negotiable rule: validate MQE against a live OAP + +A dashboard is only "done" when every expression has been run against a real OAP and returns the shape the widget expects. Type-checks prove nothing about whether a metric exists or is at the right scope. Boot a local env first: + +> Use the **`local-boot`** skill to start the BFF+UI against a local OAP (or the demo). UI on `:9091`, BFF on `:8081`, login `admin`/`admin`. + +If no OAP is available, **stop and ask for one**. Do not guess metric names, do not fabricate wire shapes. + +### How to validate one expression + +1. **Confirm the metric exists and its catalog scope + value type** — `listMetrics`: + ```bash + curl -s -H 'Content-Type: application/json' -X POST http://localhost:12800/graphql \ + -d '{"query":"query{listMetrics(regex:\"^service_cpm$\"){name catalog type}}"}' + ``` + `catalog` is the entity scope (`SERVICE` / `SERVICE_INSTANCE` / `ENDPOINT` / `SERVICE_RELATION` / …). `type` is `REGULAR_VALUE` vs `LABELED_VALUE` — it decides whether `aggregate_labels(...)` is legal. +2. **Run the MQE** at the scope the widget will render. The simplest path is to exercise the actual BFF route the widget uses (overview tile / layer dashboard) once the env is up, and inspect the JSON. Confirm a `card`/scalar MQE returns one number and a `line`/series MQE returns a time series. + +--- + +## Entity scope is load-bearing (read this before picking metrics) + +Every OAP metric lives under exactly ONE entity scope and OAP does **not** auto-rollup between scopes — querying at the wrong scope returns empty, no error. (CLAUDE.md: "Metric entity-scope is load-bearing".) + +- A `SERVICE_INSTANCE`-scope metric (e.g. `instance_jvm_cpu`) **cannot** be a bare `line`/`card` in a SERVICE-scope dashboard. Options: aggregate to a scalar with `avg(...)`/`sum(...)` for a card/overview tile, OR show per-instance trend as a `top` widget with `top_n(...)`, OR put the bare metric under `dashboards.instance`. +- Same one level deeper for `ENDPOINT`-scope metrics in a service dashboard. +- Never bridge a scope mismatch with a BFF-side rollup — move the metric to the right scope or leave the slot empty. + +## Widget type follows MQE shape (card vs line) + +Verbatim from CLAUDE.md: +> A widget whose MQE collapses to a single scalar must be `type: "card"`, not `type: "line"`. The tell-tale is the outermost call: `latest(...)`, `max(...)`, `min(...)`, `avg(<plain-metric>)`, `sum(<plain-metric>)` all reduce the window to one number. Series-shaped wrappers (`relabels(...)`, `top_n(...)`, `histogram*(...)`, `aggregate_labels(...)` without an outer scalar collapse, `rate(...)`, `increase(...)`) stay `line`. Look at the outermost function first. + +`LABELED_VALUE`-only: `aggregate_labels(metric, sum)` is valid only for labeled metrics; for `REGULAR_VALUE` use plain `sum()` / `avg()`. Mixing throws "result is not a labeled result". + +--- + +## Building an OVERVIEW dashboard + +`OverviewDashboard` top-level: `id`, `title`, `description?`, `visibility?` (`'public'`|`'operate'`), `icon?`, `order?`, `layers?` (auto-hide when none of these layers report), `widgets[]`. + +Widget `type`s (`OverviewWidgetType`): +- `section-break` — row header, layout only. `cols` sets the grid column count for the widgets that follow it. No data. +- `kpi-tile` — one layer's health: `layer`, optional `showCount` (service count header), `kpis[]`. Each KPI: `{ label, mqe, unit?, aggregation?: 'sum'|'avg', style?: 'number'|'progress-bar', max? (required for progress-bar), source?: 'mqe'|'service-count' }`. +- `metric-composite` — multi-KPI tile mixing numbers + progress bars (same `kpis[]` shape). +- `metric` — a single scalar `mqe` (rarely needed; prefer kpi-tile). +- `alarms` — active-alarm rail; layer-agnostic, `limit?`. Omit `layer`. +- `topology` — embedded static service-map for a `layer`, click-through to the full map. +- Grid: `span` (12-col) + `rowSpan`. + +Procedure: +1. Decide the story the page tells (which layers, which 3-ish KPIs each). Overview tiles are a health-at-a-glance strip — prefer RPM / latency / SLA per layer. +2. Author the JSON next to `services.json`. Group with `section-break`s. +3. For each KPI `mqe`: validate scope+type, then confirm it reduces to a scalar (overview KPIs are scalar — the renderer shows one number or a bar). Use `aggregation` to pick sum (throughput) vs avg (everything else). +4. Set `visibility`/`order`/`icon`/`layers`. Boot and eyeball at `http://localhost:9091`. + +## Building a LAYER dashboard + +A `LayerTemplate` (file basename must match `key` UPPER_SNAKE) carries far more than widgets — read `apps/bff/src/logic/layers/loader.ts` for the full interface. The widget sets live under `dashboards.{service,instance,endpoint,dependency,topology,trace,logs,traceProfiling,ebpfProfiling,asyncProfiling}`, each an array of `DashboardWidget`. + +`DashboardWidget`: `id`, `title`, `tip?`, `type` (`'card'|'line'|'top'|'record'`), `expressions[]` (MQE), `expressionLabels?` (legend / `top` tabs), `expressionUnits?`, `expressionAxes?` (0=left,1=right for dual-axis line), `unit?`, `format?` (`'int'|'decimal'|'compact'`), `span?` (default 4, 12-col), `rowSpan?` (default 8), `visibleWhen?` (hide until a metric reports — e.g. for multi-runtime instance widgets), `layerScope?`. + +Widget-type cheatsheet: +- `card` — one scalar (see the card-vs-line rule). Drop a redundant outer `avg()` if the renderer already averages, unless removing it changes shape. +- `line` — one labeled series per expression. Multi-series → set `expressionLabels` (required for the legend); dual-axis → `expressionAxes` + `expressionUnits`. +- `top` — sorted list, usually `top_n(metric, N, des)`. Fold several rankings of the same thing into ONE `top` with multiple `expressions`+`expressionLabels`+`expressionUnits` (rendered as in-widget tabs). +- `record` — RECORD-typed MQE (slow statements/SQL). Use `record`, not `top`. + +Other blocks you may need (each has its own config interface in loader.ts — read before use): `header.columns` (service-list picker columns + `orderBy`), `overview.groups` (hero tile above the picker), `topology` (node/edge metrics for the service map), `endpointDependency`, `processTopology`, `traces`, `log`, plus `components.*` feature flags that light up tabs. + +Procedure: +1. Read `general.json` end-to-end as the reference, and the `LayerTemplate` interface. +2. Decide which scopes the layer has data for (service always; instance/endpoint only if those scopes carry distinct metrics). Set `components.*`. +3. Author widgets per scope. Apply the scope rule and the card-vs-line rule to every expression. +4. Grid: `span` is the 12-col width; bump `rowSpan` for top-lists / percentile charts. +5. Validate every MQE against the live OAP, then render-check each scope in the browser. + +--- + +## Validate (code) then render-check (browser) + +```bash +# from repo root +pnpm --filter @skywalking-horizon-ui/bff run type-check # schema typechecks +pnpm --filter @skywalking-horizon-ui/bff run test:unit # loaders still parse +pnpm license:check # headers (JSON is exempt, but vue/ts edits aren't) +``` + +Then with the local env up (via `local-boot`): +- Overview: open `http://localhost:9091`, find the dashboard in the sidebar (placement = `visibility`+`order`), confirm every tile shows a number (not `—`/blank). +- Layer: navigate the layer's Service/Instance/Endpoint tabs; confirm each widget renders data and that cards are scalars, lines are series. A blank widget almost always means a scope mismatch or a metric that doesn't exist on this OAP — re-run `listMetrics`. + +## Pitfalls + +1. **Scope mismatch returns empty, not an error.** The #1 cause of a blank widget. Verify `catalog` with `listMetrics` and match it to the dashboard's scope. +2. **Card MQE that's actually a series (or vice-versa).** Look at the outermost MQE function; pick the type from it. +3. **`aggregate_labels` on a `REGULAR_VALUE` metric** → "result is not a labeled result". Check `type` first. +4. **File/key mismatch.** A layer file `foo.json` must declare `"key": "FOO"`. +5. **Inventing fields/metrics.** The OAP query-protocol and metric catalog are fixed and owned upstream. If a screen needs data the protocol doesn't expose, flag it — don't fabricate an MQE or a BFF rollup. +6. **No `Co-Authored-By` / AI footers** on commits or PRs (project rule). diff --git a/.claude/skills/local-boot/SKILL.md b/.claude/skills/local-boot/SKILL.md new file mode 100644 index 0000000..b3bb31b --- /dev/null +++ b/.claude/skills/local-boot/SKILL.md @@ -0,0 +1,102 @@ +--- +name: local-boot +description: Boot the Horizon UI dev env (BFF + UI) against a local OAP or the public Apache demo OAP, using the static configs bundled with this skill. Handles the apps/bff cwd / HORIZON_CONFIG gotcha and the demo OAP password (kept out of git via ${OAP_PASSWORD}). +user-invocable: true +--- + +# Boot the Horizon UI local dev env + +Two bundled static configs live next to this file: + +- `horizon.local.yaml` — local OAP on `127.0.0.1:12800`, no OAP network auth. +- `horizon.demo.yaml` — public Apache demo OAP, OAP password read from `${OAP_PASSWORD}`. + +Both define the same throwaway Horizon login users (password == username): +`viewer`, `maintainer`, `operator`, `admin`. + +The stack: **BFF** (Fastify) on `:8081`, **UI** (Vite) on `:9091` proxying `/api` → `:8081`. Open **`http://127.0.0.1:9091`** (use the IPv4 literal, not `localhost` — see the proxy/IPv4 section). + +## The one gotcha that bites every time + +The BFF dev script is `tsx watch src/server.ts` and pnpm runs it with **cwd = `apps/bff`**. The config path is `process.env.HORIZON_CONFIG ?? './horizon.yaml'`, resolved relative to cwd — so a bare `./horizon.yaml` points at the non-existent `apps/bff/horizon.yaml`, and the loader silently falls back to **defaults with zero users** (every login then fails with "invalid credentials", and the boot log warns `auth.local.users is empty`). + +**Always pass `HORIZON_CONFIG` as an ABSOLUTE path.** Use `"$REPO/.claude/skills/local-boot/<file>"`. + +## Proxy + IPv4 (the second gotcha) + +Two things make the UI look "not accessible" even when Vite is running: + +- **Vite binds IPv6 `[::1]` by default**, so `127.0.0.1:9091` (IPv4) has nothing and many browsers / curl fail. **Force IPv4** with `--host 127.0.0.1`. Note `pnpm --filter ui run dev -- --host …` does NOT forward the flag — run the Vite binary directly (`apps/ui/node_modules/.bin/vite --host 127.0.0.1`). +- **A local proxy** (ClashX / v2ray etc. — `http_proxy` / `https_proxy` / `all_proxy` pointing at `127.0.0.1:<port>`) intercepts loopback and returns `502`. Detect and bypass it for the dev hosts. + +Detect a local proxy before booting: +```bash +env | grep -iE '^(http_proxy|https_proxy|all_proxy)=' && echo "local proxy detected — will bypass loopback" +``` + +The browser uses its OWN proxy settings (not the shell's), so the developer must also let `127.0.0.1` / `localhost` go direct (ClashX "bypass localhost" / system proxy no-proxy list). The env-level bypass below only fixes CLI tools and Vite's own fetches. + +## Boot against the local OAP + +```bash +REPO="$(git rev-parse --show-toplevel)" +# 1. Make sure nothing stale holds the BFF port (a wrong-config process +# on :8081 makes a fresh one fail with EADDRINUSE and you keep hitting +# the old one). Kill any prior dev servers first: +pkill -f "tsx watch src/server.ts" 2>/dev/null; pkill -f vite 2>/dev/null; sleep 1 + +# 2. BFF — absolute config path is mandatory (see gotcha above): +HORIZON_CONFIG="$REPO/.claude/skills/local-boot/horizon.local.yaml" \ + pnpm --filter @skywalking-horizon-ui/bff run dev & + +# 3. UI — IPv4 host + loopback proxy bypass (run the binary directly so +# --host actually applies): +( cd "$REPO/apps/ui" && \ + env -u http_proxy -u https_proxy -u all_proxy -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY \ + no_proxy="localhost,127.0.0.1,::1" NO_PROXY="localhost,127.0.0.1,::1" \ + node_modules/.bin/vite --host 127.0.0.1 & ) +``` + +Then open **`http://127.0.0.1:9091`** and log in as `admin` / `admin`. + +## Boot against the public demo OAP + +The demo OAP needs basic-auth. The password is NOT committed — ask the +developer for it and export it before booting: + +```bash +REPO="$(git rev-parse --show-toplevel)" +read -rsp "Demo OAP password: " OAP_PASSWORD && export OAP_PASSWORD && echo + +pkill -f "tsx watch src/server.ts" 2>/dev/null; pkill -f vite 2>/dev/null; sleep 1 +HORIZON_CONFIG="$REPO/.claude/skills/local-boot/horizon.demo.yaml" \ + pnpm --filter @skywalking-horizon-ui/bff run dev & +( cd "$REPO/apps/ui" && \ + env -u http_proxy -u https_proxy -u all_proxy -u HTTP_PROXY -u HTTPS_PROXY -u ALL_PROXY \ + no_proxy="localhost,127.0.0.1,::1" NO_PROXY="localhost,127.0.0.1,::1" \ + node_modules/.bin/vite --host 127.0.0.1 & ) +``` + +When invoked as an agent and `OAP_PASSWORD` is not already set, ask the +developer for it (do not guess, do not hardcode it into a file). The OAP +network username is fixed as `admin` in `horizon.demo.yaml`. + +## Verify the BFF is healthy (no browser) + +```bash +# --noproxy so a local proxy (ClashX etc.) doesn't 502 the loopback call. +until curl -s --noproxy '*' -m2 -o /dev/null http://127.0.0.1:8081/api/auth/health; do sleep 1; done +curl -s --noproxy '*' -c /tmp/sw.cookies -H 'Content-Type: application/json' -X POST \ + http://127.0.0.1:8081/api/auth/login -d '{"username":"admin","password":"admin"}' +# Expect 200 with {username, roles, verbs, landingRoute}. A 401 +# "invalid credentials" almost always means the wrong config loaded — +# re-check the absolute HORIZON_CONFIG path and that no stale BFF holds :8081. +``` + +## Editing the configs + +These files are the source of truth for dev boot. Keep secrets out: +local-user hashes (password == username) are fine to commit; real OAP +passwords must stay in `${OAP_PASSWORD}` (the loader's `interpolateEnv` +expands `${VAR}` / `${VAR:default}` in the raw YAML before parsing). +To mint a new local-user hash: `pnpm --filter @skywalking-horizon-ui/bff cli:hash`. diff --git a/.claude/skills/local-boot/horizon.demo.yaml b/.claude/skills/local-boot/horizon.demo.yaml new file mode 100644 index 0000000..242ac29 --- /dev/null +++ b/.claude/skills/local-boot/horizon.demo.yaml @@ -0,0 +1,114 @@ +# Static demo-boot config for the Horizon UI BFF — points at the public +# Apache demo OAP (demo.skywalking.apache.org). The OAP network basic-auth +# password is NOT committed: it is read from ${OAP_PASSWORD} at boot. +# Ask the developer for it before booting and export it, e.g. +# read -rs OAP_PASSWORD && export OAP_PASSWORD +# Boot via the `local-boot` skill (see SKILL.md), which passes this file +# through HORIZON_CONFIG as an ABSOLUTE path. Horizon login users below +# are throwaway dev credentials (password == username). + +server: + host: 127.0.0.1 + port: 8081 + +oap: + queryUrl: https://demo.skywalking.apache.org:12800 + adminUrl: https://demo.skywalking.apache.org:17128 + zipkinUrl: https://demo.skywalking.apache.org:9412/zipkin + timeoutMs: 15000 + auth: + username: admin + # Supplied via the environment so the demo password stays out of git. + # interpolateEnv() in apps/bff/src/config/loader.ts expands ${OAP_PASSWORD}. + password: "${OAP_PASSWORD}" + +auth: + backend: local + local: + users: + - username: viewer + # password: viewer + passwordHash: "$argon2id$v=19$m=65536,t=3,p=4$Gp175hqr+EF2iZ7v1fndvw$w6w9hDI59/UA+CRARChDoGRlR1TkVt6kqzApa021K+0" + roles: [viewer] + - username: maintainer + # password: maintainer + passwordHash: "$argon2id$v=19$m=65536,t=3,p=4$w7ULwB3/jzH9FxVoHJ238A$y+qGoX6IPeOoGywLQCpfpAN5VJXcaevoWeJQhaybvQU" + roles: [maintainer] + - username: operator + # password: operator + passwordHash: "$argon2id$v=19$m=65536,t=3,p=4$nzoI4RqiobprtzX/mJqe5Q$FY2Hi7mKep0DPHoaE++r/KD++WLUwTgRUFLde87j2Wg" + roles: [operator] + - username: admin + # password: admin + passwordHash: "$argon2id$v=19$m=65536,t=3,p=4$joV9AVlyLS3pqq4mLrYokQ$pJLkTKrz9/LzEH6YaFljdz9k8dyBiryjwSB26Diiz9U" + roles: [admin] + +rbac: + enabled: true + roles: + viewer: + - "metrics:read" + - "alarms:read" + - "traces:read" + - "logs:read" + - "topology:read" + - "profile:read" + - "overview:read" + maintainer: + - "metrics:read" + - "alarms:read" + - "traces:read" + - "logs:read" + - "topology:read" + - "profile:read" + - "overview:read" + - "cluster:read" + - "inspect:read" + - "ttl:read" + - "config:read" + 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: + - "*" + landingByRole: + viewer: / + maintainer: /operate/cluster + operator: / + admin: / + +session: + ttlMinutes: 60 + cookieName: horizon_sid + cookieSecure: false + +audit: { file: ./horizon-audit.jsonl } +setup: { file: ./horizon-setup.json } +alarms: { file: ./horizon-alarms.json } +debugLog: { enabled: false, file: ./horizon-wire.jsonl, maxBodyChars: 8192, redactAuthHeaders: true } diff --git a/.claude/skills/local-boot/horizon.local.yaml b/.claude/skills/local-boot/horizon.local.yaml new file mode 100644 index 0000000..58477e0 --- /dev/null +++ b/.claude/skills/local-boot/horizon.local.yaml @@ -0,0 +1,111 @@ +# Static local-boot config for the Horizon UI BFF — points at a LOCAL +# OAP on 127.0.0.1 with NO network auth. Boot via the `local-boot` skill +# (see SKILL.md) which passes this file through HORIZON_CONFIG as an +# ABSOLUTE path. The local-user password hashes below are throwaway dev +# credentials (password == username) — safe to commit; do NOT add real +# secrets here. For demo-OAP access use horizon.demo.yaml instead, which +# keeps the OAP password out of git via ${OAP_PASSWORD}. + +server: + host: 127.0.0.1 + port: 8081 + +oap: + queryUrl: http://localhost:12800 + # This OAP serves the debugging/admin REST surface on the SAME port + # as GraphQL (12800). Zipkin (9412) may not be exposed locally. + adminUrl: http://localhost:12800 + zipkinUrl: http://localhost:9412/zipkin + timeoutMs: 15000 + +auth: + backend: local + local: + # One account per role for manual menu-RBAC checks. Password == username. + users: + - username: viewer + # password: viewer + passwordHash: "$argon2id$v=19$m=65536,t=3,p=4$Gp175hqr+EF2iZ7v1fndvw$w6w9hDI59/UA+CRARChDoGRlR1TkVt6kqzApa021K+0" + roles: [viewer] + - username: maintainer + # password: maintainer + passwordHash: "$argon2id$v=19$m=65536,t=3,p=4$w7ULwB3/jzH9FxVoHJ238A$y+qGoX6IPeOoGywLQCpfpAN5VJXcaevoWeJQhaybvQU" + roles: [maintainer] + - username: operator + # password: operator + passwordHash: "$argon2id$v=19$m=65536,t=3,p=4$nzoI4RqiobprtzX/mJqe5Q$FY2Hi7mKep0DPHoaE++r/KD++WLUwTgRUFLde87j2Wg" + roles: [operator] + - username: admin + # password: admin + passwordHash: "$argon2id$v=19$m=65536,t=3,p=4$joV9AVlyLS3pqq4mLrYokQ$pJLkTKrz9/LzEH6YaFljdz9k8dyBiryjwSB26Diiz9U" + roles: [admin] + +rbac: + enabled: true + roles: + viewer: + - "metrics:read" + - "alarms:read" + - "traces:read" + - "logs:read" + - "topology:read" + - "profile:read" + - "overview:read" + maintainer: + - "metrics:read" + - "alarms:read" + - "traces:read" + - "logs:read" + - "topology:read" + - "profile:read" + - "overview:read" + - "cluster:read" + - "inspect:read" + - "ttl:read" + - "config:read" + 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: + - "*" + landingByRole: + viewer: / + maintainer: /operate/cluster + operator: / + admin: / + +session: + ttlMinutes: 60 + cookieName: horizon_sid + cookieSecure: false + +audit: { file: ./horizon-audit.jsonl } +setup: { file: ./horizon-setup.json } +alarms: { file: ./horizon-alarms.json } +debugLog: { enabled: false, file: ./horizon-wire.jsonl, maxBodyChars: 8192, redactAuthHeaders: true } diff --git a/.licenserc.yaml b/.licenserc.yaml index 489b30a..c49ab79 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -42,6 +42,9 @@ header: - '**/dist/**' - '**/node_modules/**' - 'docs/**' + # Agent / dev tooling, not shippable source — skill configs (e.g. + # local-boot's horizon.*.yaml) carry no ASF header. + - '.claude/**' - 'pnpm-lock.yaml' - '.browserslistrc' - '.prettierrc'
