This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
The following commit(s) were added to refs/heads/main by this push:
new a17043c release: 0.4.0 prep — CHANGELOG + doc drift fixes
a17043c is described below
commit a17043c13b7aed36946a7140971ee07583e744a5
Author: Wu Sheng <[email protected]>
AuthorDate: Tue May 19 21:38:11 2026 +0800
release: 0.4.0 prep — CHANGELOG + doc drift fixes
CHANGELOG.md (new):
Feature-perspective notes covering 0.1.0 → 0.4.0. Each release is
grouped by operator-visible area (data plane, operate stack, auth,
reliability, deployment, cleanup) rather than per-commit detail.
Doc drift fixes for the 0.4.0 release:
- docs/setup/container-image.md realigned to the real `/app/server.js`
layout produced by the image (the Dockerfile flattens `dist/` to
`/app/`). Documents env-overridable bind defaults
(`HORIZON_SERVER_HOST`, `HORIZON_SERVER_PORT`).
- docs/setup/auth.md polished — local + LDAP backend tables now match
the schema 1:1, break-glass rules clarified.
- docs/setup/horizon-yaml.md polished — bootstrap-rules section
rewritten so the soft / hard distinction reads cleanly.
- docs/setup/overview.md polished — quick-start phrasing tightened.
- docs/access-control/ldap-backend.md + local-backend.md polished.
- horizon.example.yaml polished — comments match current behavior
(no "default admin/admin" hint, boot-and-warn flow described).
- README.md polished alongside.
Note on release tagging:
- Git tag is `v0.4.0` (with `v` prefix, matches v0.1.0 / v0.2.0 /
v0.3.0).
- OCI image tags produced by the publish workflow are `0.4.0` and
`0.4` (bare, no prefix).
- CHANGELOG.md uses bare `0.4.0` headings per release-notes convention.
---
CHANGELOG.md | 235 +++++++++++++++++++++++++++++++++++
README.md | 2 +-
docs/access-control/ldap-backend.md | 2 +-
docs/access-control/local-backend.md | 2 +-
docs/setup/auth.md | 12 +-
docs/setup/container-image.md | 29 +++--
docs/setup/horizon-yaml.md | 7 +-
docs/setup/overview.md | 4 +-
horizon.example.yaml | 3 +-
9 files changed, 270 insertions(+), 26 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..6a7da78
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,235 @@
+# Changelog
+
+Notable changes to Apache SkyWalking Horizon UI, written from the operator's
+point of view — what's new on screen and what's now possible, not the
+file-by-file implementation. For per-commit detail, see the git log.
+
+The version line is shared by every package in the monorepo (apps + shared
+packages) plus the BFF's `HORIZON_VERSION` default.
+
+## 0.4.0
+
+OAP becomes the runtime source of truth for UI templates, the 5-theme system
+lands, and the app supports being served behind a gateway prefix.
+
+### Templates synced to OAP
+
+- Five reserved template families now live on OAP's UI-template REST surface
+ (`/ui-management/templates*` on the admin port): overview dashboards,
per-layer
+ dashboards, alert page setup, theme selection, time-defaults. Bundled JSON
+ ships as the seed + read-only fallback.
+- One-shot seed on BFF boot pushes any missing bundled template to OAP; runtime
+ sync is read-only with a 30-second single-flight cache.
+- New admin endpoints: `GET /api/admin/templates/sync-status`,
+ `POST /api/admin/templates/save`, `POST /api/admin/templates/resync`,
+ `POST /api/admin/templates/:name/push-bundled`.
+- When the admin port is unreachable, every admin page goes read-only with a
+ red banner; Save / Create / Delete are disabled; render falls back to
bundled.
+- Diverged rows surface a "Show diff & reset" Monaco modal with a
+ destructive-confirm (type the template key to arm reset).
+
+### Themes
+
+- Five bundled themes — **Horizon** (default), **Meridian**, **Obsidian**,
+ **Daybreak**, **Aurora** — each shipping a complete token set (bg, fg,
+ accent, info/ok/warn/err, font, radius, density).
+- New `/admin/global-defaults` admin page replaces the old "Setup" link.
+ Theme picker uses preview cards lifted from the design (hero strip,
+ mini-app mockup with Primary/Tonal/Ghost buttons, KPI tiles, sparkline,
+ density/font/radius badges).
+- Per-user theme override via a labelled topbar chip — three-tier resolution
+ `localStorage user → OAP org default → bundled`, written to
+ `<html data-theme>` / `<html data-appearance>` synchronously on boot so
+ the pre-auth login page already respects the local override.
+- Sidebar SkyWalking logo swaps to the official brand blue (`#1368B3`) on
+ light-appearance themes.
+- Widget series colors (Zipkin trace palette, AlarmSnapshotChart,
+ AlarmsTimeline) track the active theme's `--sw-accent` via a shared
+ `readAccent()` util.
+- Sign-in button gradient derives both stops from the theme accent.
+
+### Time defaults
+
+- `/admin/global-defaults` also owns the global picker's default window
+ (60 minutes shipped). OAP `step` precision is derived from window size —
+ ≤ 4 h MINUTE, 6 h–14 d HOUR, ≥ 30 d DAY — and surfaced inline on the page.
+- Per-user override in the topbar time picker: "Save as my default" /
+ "Reset to org default".
+
+### Reliability + diagnostics
+
+- Topology cluster boundary now grows to encompass dragged nodes; the chip
+ moved inside the cluster header so it stays visible at any drag position.
+- Alarms page gains an **Other** KPI tile that surfaces the residual count
+ between `Active` and the sum of pinned-layer chips — `Active = General +
+ Mesh + Other` reconciles even when alarms land in unmapped layers.
+- Overview "Active alarms" widget now reads the admin's configured
+ `defaultWindowMs` from `/admin/alert-page-setup`; all three alarm
+ surfaces (overview widget, alarms page, topbar badge) share one window.
+- Every backend call failure (network throw or non-2xx) writes a
+ `pushEvent('api', 'err', …)` into the debug event log with the BFF's
+ `code` / `message` envelope inlined when present.
+- Dashboards with more than 40 widgets (e.g. the General/instance page,
+ 56 widgets) now succeed: the UI splits oversize requests into ≤40-widget
+ chunks fired in parallel, then merges results.
+
+### Deployment
+
+- Gateway-prefix support: `BffClient.request()` prepends
+ `import.meta.env.BASE_URL` to every API path. Build with
+ `vite build --base=/horizon/` and a gateway that strips the prefix and
+ the SPA + every API call resolves cleanly under the sub-path.
+- Cluster Status route corrected from `/admin/cluster` → `/operate/cluster`
+ (the prior default 404'd because no route by that name existed).
+
+### Cleanup
+
+- Documentation rewritten as an orientation map; the left-side menu is the
+ canonical navigation now. All `SWIP-*` references removed from
+ user-visible text and docs.
+- "Coming in Phase 6 / 7" placeholder strip on Cluster Status removed.
+- Dead code dropped — `LandingView.vue`, `LayerTabPlaceholder.vue`, the
+ orphaned disk-write template routes (`POST /api/admin/overview-templates/:id`
+ + `POST /api/admin/layer-templates/:key`), and stale `Phase X` markers
+ across BFF + UI + docs.
+- The OAP UTC-offset chip is gone from the topbar; the health dot stays.
+
+## 0.3.0
+
+The shell unifies, the operate stack lands, and the first round of public
+documentation ships.
+
+### Operate stack
+
+- **Alarms page** — incident-merged active-alarms view, severity tabs,
+ alarm list with right-side detail (trigger expression, channel routing),
+ inline Live Debug card (Run / Step / Pause / Copy as MQE, execution-trace
+ ladder with per-step output + latency, matched entities, eval-window
+ chart, raw OAP response).
+- **Inspect** — metric catalog + entity enumerator with search, type
+ filter, scope (Service / Instance / Endpoint / Process / All), and
+ source attribution.
+- **Live Debugger** — MAL / LAL / OAL session start, poll, stop. Per-node
+ status fan-out, sample payloads, capture history with replay-ready
+ recordings.
+- **Profiling** — flame graph + stack table over five profilers:
+ trace-driven thread profiling, eBPF CPU/off-CPU, JVM async-profiler,
+ network profiling (process conversation graph), Go pprof.
+- **Zipkin trace explorer** — service / span search, waterfall popout
+ with per-service color bands, sticky time-axis.
+- **Overview dashboards** — cross-layer war-room views (Services, Mesh)
+ with per-layer KPI tiles, alarm rails, and the existing chart widgets.
+
+### Auth + access control
+
+- Local + LDAP authentication backends. Break-glass admin honored only
+ when `backend: ldap` AND the LDAP probe is failing.
+- Three admin pages — Users, Auth status, Roles & permissions.
+- 4 built-in roles (viewer / maintainer / operator / admin) and a
+ 28-verb permission model. Every BFF route gated by a single policy
+ table.
+- Login view redesigned (canyon hero, status pill, configured-backend
+ banner).
+
+### Reliability + UX
+
+- Cascade-clear, then load — every dependent area visibly resets and
+ shows "Reading data…" between an upstream control change (service /
+ instance / endpoint pick, time-range change, layer / scope nav) and
+ the new data landing. No silent freezes; no stale value sitting under
+ a spinner.
+- Global time picker in the topbar wired into the landing + widget
+ query keys; the picker only applies to dashboards / overviews (triage
+ pages keep their own per-page time).
+- Single-shot bundle preload: layer dashboards + overview list arrive
+ in one round-trip, cached in localStorage with ETag revalidation.
+- Framework event ticker in the topbar replaces breadcrumb+search;
+ Admin-toggled debug panel surfaces a 200-event buffer with operator
+ click capture.
+- Auto-pick first instance / endpoint when a scope needs one and the
+ list is non-empty.
+- Topology + dashboard fixes, multi-layer service attribution, sticky
+ service selection across navigations.
+
+### Documentation
+
+- First public docs tree (`docs/`) — Setup, Compatibility, Access
+ Control, Customization, Components, Operate. Lives in-repo and
+ publishes to skywalking.apache.org.
+
+### Container + CI
+
+- Real `packages/*` builds + self-contained `dist/` + copy-in image
+ (no compile in the container).
+- Zero-config boot: image defaults `HORIZON_SERVER_HOST=0.0.0.0`.
+- Multi-arch publish-image — native amd64 + arm64 builds, OCI manifest
+ list.
+- Unit-test job in CI; 107 UTs covering entity-scope construction +
+ routing decisions.
+
+## 0.2.0
+
+Per-layer dashboards become real, the layer-template editor ships, and
+topology gets its booster-ui port.
+
+### Per-layer dashboards
+
+- Real widget grid per layer driven by JSON templates. 43 layer
+ dashboards migrated from booster-ui.
+- Per-scope widget sets: each layer template defines its own `service`,
+ `instance`, `endpoint`, `topology`, `traces`, `logs`, profiling
+ variants.
+- Visibility predicates per widget (`visibleWhen`) so MQ / DB widgets
+ only render when the relevant metrics are reporting.
+
+### Layer admin
+
+- Read-only template browser, then full edit UI: components editor
+ (toggle which per-layer views exist), metrics editor (header columns),
+ separate Overview tile card, scope-aware visibleWhen hints.
+
+### Service deep-dive
+
+- APIs widget (formerly Services), MQ widgets gated by visibleWhen,
+ TopList multi-expression switcher with MQE preview in tooltip,
+ smaller widget height, per-metric color alignment, dual-axis MQ.
+
+### Topology
+
+- Polished linear-chain variant, dual-panel detail, per-side line
+ charts. Drag-to-move + barycentric layout for smaller graphs.
+ RPM-only chip variant. Istio renamed.
+
+### Logs
+
+- Legend at top of table (drop service facet duplication), workflow
+ notes.
+
+### Charting
+
+- TimeChart: legend formatting fix for dual-axis widgets, value dots,
+ tooltip escape for clipped charts, no more legend / axis-name
+ crowding at chart top.
+
+### Sidebar + chrome
+
+- Group toggle + group click cascades to first layer's first tab.
+- Topbar 60m widget format hints (int / decimal / compact).
+- Per-layer image pipeline (icons) shipped.
+
+## 0.1.0
+
+Foundational scaffolding. The shell renders, auth works, OAP is reachable,
+and CI is green. No operator-facing data surfaces yet.
+
+- pnpm monorepo: `apps/ui` (Vue 3 + Vite), `apps/bff` (Fastify), shared
+ `packages/api-client` (typed REST + GraphQL clients), shared
+ `packages/design-tokens` (CSS custom properties).
+- BFF — Fastify skeleton with `horizon.yaml` config + hot reload, local
+ auth (argon2 + cookie sessions), RBAC verb gating + JSONL audit log,
+ OAP proxy with cluster fan-out + preflight.
+- UI — AppShell (sidebar, topbar) with design tokens, Pinia auth store
+ with on-401 redirect, login view with route guard + sign-out, stub
+ admin / operate pages.
+- CI — monorepo workspace build + dependency license check via
+ `skywalking-eyes`.
diff --git a/README.md b/README.md
index 611110a..cffc23b 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ Full docs ship at [skywalking.apache.org → Horizon
UI](https://skywalking.apac
## Tech stack
-Vue 3 + TypeScript on Vite 5, Pinia, vue-router 4, Apache ECharts 5, D3 v7,
vue-grid-layout, d3-flame-graph, Monaco, Vitest. The BFF is Fastify on Node 20+.
+Vue 3 + TypeScript on Vite 5, Pinia, vue-router 4, Apache ECharts 6, D3 v7,
vue-grid-layout, d3-flame-graph, Monaco, Vitest. The BFF is Fastify on Node 20+.
## Development
diff --git a/docs/access-control/ldap-backend.md
b/docs/access-control/ldap-backend.md
index b9ccbb7..7e22288 100644
--- a/docs/access-control/ldap-backend.md
+++ b/docs/access-control/ldap-backend.md
@@ -26,7 +26,7 @@ auth:
- { group: "*", role: viewer }
```
-Bootstrap rule: `ldap.groupMappings` must be non-empty.
+Bootstrap rule: `ldap.groupMappings` must be non-empty before LDAP users can
sign in. The BFF boots and surfaces the setup-required state on the login page,
but no LDAP login succeeds until at least one mapping is configured.
## Login flow
diff --git a/docs/access-control/local-backend.md
b/docs/access-control/local-backend.md
index ebcebaa..f1f1cb8 100644
--- a/docs/access-control/local-backend.md
+++ b/docs/access-control/local-backend.md
@@ -20,7 +20,7 @@ auth:
roles: [viewer]
```
-Bootstrap rule: `local.users` must be non-empty when `backend: local`. The BFF
refuses to start otherwise — there is **no built-in default admin/admin**.
+Bootstrap rule: `local.users` must be non-empty when `backend: local` before
anyone can sign in. The BFF boots and surfaces the setup-required state on the
login page, but no login succeeds until a user is configured. There is **no
built-in default admin/admin**.
## Generate a password hash
diff --git a/docs/setup/auth.md b/docs/setup/auth.md
index 59e992a..f21a3aa 100644
--- a/docs/setup/auth.md
+++ b/docs/setup/auth.md
@@ -48,7 +48,7 @@ The two blocks are **mutually exclusive at runtime**. Leaving
the inactive block
| Field | Type | Default | Required | Notes |
|---|---|---|---|---|
-| `local.users` | array | `[]` | required when `backend: local` (must be
non-empty) | Array of user objects. |
+| `local.users` | array | `[]` | required for local login | Array of user
objects. Empty means the BFF boots but every local login is rejected. |
| `local.users[].username` | string (min 1) | — | yes | Unique login name. |
| `local.users[].passwordHash` | string (min 1) | — | yes | Argon2id hash.
Generate via `pnpm --filter bff cli:hash`. Never store plain passwords. |
| `local.users[].roles` | string[] | `[]` | no | Roles assigned to this user.
Empty array means no permissions (sessions still created; UI shows "no access"
for everything). |
@@ -57,7 +57,7 @@ See [Local Backend](../access-control/local-backend.md) for
hash generation and
## `auth.ldap`
-Required when `backend: ldap`. `groupMappings` must be non-empty.
+Required for LDAP login when `backend: ldap`. `groupMappings` must be
non-empty before any LDAP user can sign in.
| Field | Type | Default | Required | Notes |
|---|---|---|---|---|
@@ -72,7 +72,7 @@ Required when `backend: ldap`. `groupMappings` must be
non-empty.
| `ldap.memberAttr` | string | `member` | no | Group attribute listing
members. Only used when `groupStrategy: search`. |
| `ldap.timeoutMs` | number | `5000` | no | LDAP bind / search timeout in
milliseconds. Positive integer. |
| `ldap.tlsInsecure` | boolean | `false` | no | Skip TLS certificate
validation. **Never use in production.** |
-| `ldap.groupMappings` | array | `[]` | required when `backend: ldap` (must be
non-empty) | Group DN → Horizon role bindings. |
+| `ldap.groupMappings` | array | `[]` | required for LDAP login | Group DN →
Horizon role bindings. Empty means the BFF boots but every LDAP login is
rejected. |
| `ldap.groupMappings[].group` | string (min 1) | — | yes | LDAP group DN, or
the literal `"*"` (matches any authenticated user — fallback). |
| `ldap.groupMappings[].role` | string (min 1) | — | yes | Horizon role
assigned when the user's groups include `group`. First match wins; multiple
matches union. |
@@ -94,8 +94,8 @@ See [Break-Glass Access](../access-control/break-glass.md)
for the trigger condi
| Condition | Result |
|---|---|
-| `backend: local` and `local.users` empty | startup fails |
-| `backend: ldap` and `ldap` missing | startup fails |
-| `backend: ldap` and `ldap.groupMappings` empty | startup fails |
+| `backend: local` and `local.users` empty | startup warning; login rejected
until a user is configured |
+| `backend: ldap` and `ldap` missing | startup warning; login rejected until
LDAP is configured |
+| `backend: ldap` and `ldap.groupMappings` empty | startup warning; login
rejected until a mapping is configured |
| `backend: ldap` and `local.users` populated | warning at startup |
| `breakGlass` populated but `backend: local` | warning at startup (block is
unused in local mode) |
diff --git a/docs/setup/container-image.md b/docs/setup/container-image.md
index 3d31312..9035650 100644
--- a/docs/setup/container-image.md
+++ b/docs/setup/container-image.md
@@ -9,13 +9,13 @@ Registry: **GitHub Container Registry (GHCR)** at
`ghcr.io/apache/skywalking-hor
| Tag | Points at | Use case |
|---|---|---|
| `<40-char-sha>` | Exact commit. Immutable. | **Production.** Pin to a SHA so
deploys are reproducible. |
-| `vX.Y.Z` | Tagged release. | Stable release. Same image as the SHA it was
built from. |
+| `X.Y.Z` | Tagged release, produced from git tag `vX.Y.Z`. | Stable release.
Same image as the SHA it was built from. |
| `X.Y` | Latest patch on a minor line. Moves over time. | Track a minor
release line. |
-| `latest` | Newest `vX.Y.Z` tag. Moves. | Demos / dev only — do not pin
production to `latest`. |
+| `latest` | Newest git `vX.Y.Z` tag. Moves. | Demos / dev only — do not pin
production to `latest`. |
| `main` | Head of `main`. Moves on every merge. | Smoke-test the development
branch. |
```sh
-docker pull ghcr.io/apache/skywalking-horizon-ui:v1.2.3
+docker pull ghcr.io/apache/skywalking-horizon-ui:0.4.0
docker pull ghcr.io/apache/skywalking-horizon-ui:<sha>
```
@@ -25,7 +25,7 @@ The full commit SHA is the canonical, immutable identifier.
Moving tags are conv
| Path inside the container | Owner | Writable by `horizon`? | What it is |
|---|---|---|---|
-| `/app/dist/server.js` | root | no | Compiled BFF entry point. `CMD` runs
`node dist/server.js`. |
+| `/app/server.js` | root | no | Compiled BFF entry point. `CMD` runs `node
server.js`. |
| `/app/node_modules/` | root | no | Production npm dependencies. |
| `/app/static/` | root | no | Built UI assets (Vite `dist/`). |
| `/app/horizon.example.yaml` | root | no | Example config — **read-only
reference**, copy from it. |
@@ -50,7 +50,7 @@ The runtime stage runs as the non-root user `horizon`. Two
locations are owned b
The four `HORIZON_*_FILE` env vars seed the **defaults** the config schema
uses when `horizon.yaml` doesn't supply a value. An explicit value in
`horizon.yaml` always wins. The intent: an operator who runs the published
image with only a minimal `horizon.yaml` (no `audit/setup/alarms/debugLog`
blocks) gets state files routed to `/data/` automatically, no manual path
overrides needed.
-`server.host` and `server.port` come from the YAML — not from env vars. The
image sets `EXPOSE 8081`; if you change `server.port`, also publish the new
port.
+`server.host` and `server.port` come from the YAML when present. If they are
omitted, the image supplies defaults via `HORIZON_SERVER_HOST=0.0.0.0` and
`HORIZON_SERVER_PORT=8081`. The image sets `EXPOSE 8081`; if you change
`server.port`, also publish the new port.
## How to load `horizon.yaml` into the container
@@ -65,20 +65,20 @@ docker run -d \
--name horizon \
-p 8081:8081 \
-v "$PWD/horizon.yaml:/app/horizon.yaml:ro" \
- ghcr.io/apache/skywalking-horizon-ui:v1.2.3
+ ghcr.io/apache/skywalking-horizon-ui:0.4.0
```
Notes:
- `:ro` — read-only mount. The BFF only reads the file; preventing writes
catches mistakes.
-- `server.host` in your YAML should be `0.0.0.0`, not the default `127.0.0.1`
— otherwise the BFF only binds the container's loopback and `-p 8081:8081`
cannot reach it.
+- If your YAML sets `server.host`, use `0.0.0.0` in containers. `127.0.0.1`
binds container loopback only, so `-p 8081:8081` cannot reach it.
### 2. Bake it in (custom image)
For immutable single-tenant deployments, build a child image that includes
your config:
```dockerfile
-FROM ghcr.io/apache/skywalking-horizon-ui:v1.2.3
+FROM ghcr.io/apache/skywalking-horizon-ui:0.4.0
COPY horizon.yaml /app/horizon.yaml
```
@@ -146,7 +146,7 @@ spec:
fsGroup: 101
containers:
- name: horizon
- image: ghcr.io/apache/skywalking-horizon-ui:v1.2.3
+ image: ghcr.io/apache/skywalking-horizon-ui:0.4.0
ports:
- containerPort: 8081
envFrom:
@@ -188,7 +188,7 @@ docker run -d --name horizon \
-p 8081:8081 \
-v "$PWD/horizon.yaml:/app/horizon.yaml:ro" \
-v horizon-state:/data \
- ghcr.io/apache/skywalking-horizon-ui:v1.2.3
+ ghcr.io/apache/skywalking-horizon-ui:0.4.0
```
Without a mounted volume the writes still land in the container's writable
layer at `/data/` (ephemeral, but at least non-failing). Mounting a volume is
what makes them durable.
@@ -299,7 +299,14 @@ so session cookies are flagged `Secure` and the browser
refuses to send them ove
## Building locally
-The image is built from the `Dockerfile` in the repo root. Same `docker
buildx` invocation as CI:
+The image is built from the pre-packaged `./dist/` directory in the repo root.
Build that artifact first:
+
+```sh
+pnpm install
+pnpm package
+```
+
+Then use the same `docker buildx` invocation shape as CI:
```sh
docker buildx build \
diff --git a/docs/setup/horizon-yaml.md b/docs/setup/horizon-yaml.md
index c051171..1b72e3f 100644
--- a/docs/setup/horizon-yaml.md
+++ b/docs/setup/horizon-yaml.md
@@ -63,14 +63,15 @@ ldap:
## Bootstrap rules
-The BFF refuses to start when any of:
+The BFF validates the file shape at startup and on every hot reload. Schema
errors still reject the file; auth bootstrap gaps are softer so a first-run
container can render the login page with a setup-required banner.
+
+Auth gaps that boot with a warning but reject login:
1. `auth.backend: local` and `auth.local.users` is empty.
2. `auth.backend: ldap` and `auth.ldap` block is missing.
3. `auth.backend: ldap` and `auth.ldap.groupMappings` is empty.
-4. `rbac.enabled: true` and no roles defined (the four built-ins are used by
default; this only trips if you wipe them).
-A startup failure logs the reason and exits with non-zero. There is no
"default admin/admin" fallback.
+There is no "default admin/admin" fallback.
## Warnings (do not block startup)
diff --git a/docs/setup/overview.md b/docs/setup/overview.md
index 6efdbd8..0b2a1ab 100644
--- a/docs/setup/overview.md
+++ b/docs/setup/overview.md
@@ -6,7 +6,7 @@ This page is the smallest possible path from "no Horizon" to
"Horizon in front o
- Apache SkyWalking **OAP 11.x** (native). OAP 10.x runs the data-plane stack
(dashboards, traces, logs, topology, alarms, profiling) but the entire admin
port — Inspect, DSL Management, Live Debugger, Alarm Rule editor, Cluster
Status → Admin pane, and OAP UI-template sync — is v11-only. See [Compatibility
→ OAP Version](../compatibility/oap-version.md) for the feature-vs-version
matrix.
- Network reachability from the Horizon BFF to the OAP query port (`:12800`)
and admin port (`:17128`). See [Network Ports](../compatibility/ports.md).
-- Node.js 20+, pnpm 9+ (for source builds). A pre-built artifact will not need
either.
+- Node.js 20+, pnpm 10+ (for source builds; pinned via Corepack). A pre-built
artifact only needs Node.js.
## Five-step start
@@ -36,7 +36,7 @@ oap:
### 3. Add at least one local user
-The BFF refuses to start with no users. Generate an argon2 hash:
+With no users configured, the BFF boots so the login page can show the
setup-required state, but no login can succeed. Generate an argon2 hash:
```sh
pnpm --filter bff cli:hash
diff --git a/horizon.example.yaml b/horizon.example.yaml
index 10ea283..3e9b50a 100644
--- a/horizon.example.yaml
+++ b/horizon.example.yaml
@@ -55,7 +55,8 @@ oap:
#
# Bootstrap rule: if `backend: local` and `auth.local.users` is empty,
# OR `backend: ldap` and `auth.ldap` is missing / `groupMappings` empty,
-# the BFF refuses to start. There is no "default admin/admin" password.
+# the BFF boots but no login succeeds until auth is configured. There is
+# no "default admin/admin" password.
# Generate hashes with: pnpm --filter bff cli:hash
# ────────────────────────────────────────────────────────────────────
auth: