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'

Reply via email to