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:

Reply via email to