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 7bb8fea7bdedbe88a4542e0fa9bb1628198d758c Author: Wu Sheng <[email protected]> AuthorDate: Thu May 21 17:25:09 2026 +0800 docs: rewrite for end users — drop code-workflow internals Docs are for the operator / dashboard author, not the contributor. Removed implementation source pointers (apps/.../src paths, internal function / composable / component names) and step-by-step internal algorithms (e.g. the LDAP "Login flow" code steps) across the tree; kept config, fields, recipes, behavior, and troubleshooting. The LDAP doc now describes observable sign-in behavior + the service-bind group read. Added two CLAUDE.md principles: docs target the end user, and comments carry only non-obvious highlights / edge cases. --- CLAUDE.md | 4 ++- docs/README.md | 11 +------- docs/access-control/admin-pages.md | 30 +++++++-------------- docs/access-control/audit-log.md | 8 +++--- docs/access-control/break-glass.md | 8 +++--- docs/access-control/ldap-backend.md | 37 +++++++++---------------- docs/access-control/local-backend.md | 12 ++++----- docs/access-control/rbac.md | 44 +++++++----------------------- docs/compatibility/cluster-status.md | 6 +---- docs/compatibility/required-modules.md | 4 +-- docs/components/charts.md | 46 ++++++++------------------------ docs/components/dashboard-widgets.md | 12 ++++----- docs/components/overview-widgets.md | 16 ++++------- docs/customization/adding-a-new-layer.md | 2 +- docs/customization/layer-templates.md | 2 +- docs/customization/menu-structure.md | 24 +++++------------ docs/customization/overview-templates.md | 4 +-- docs/design-target.md | 6 ++--- docs/operate/cluster-metadata.md | 2 +- docs/operate/inspect.md | 16 ----------- docs/setup/files.md | 8 +++--- docs/setup/horizon-yaml.md | 4 +-- docs/setup/oap.md | 4 +-- docs/setup/server.md | 6 +---- 24 files changed, 94 insertions(+), 222 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 00a92e8..f6d7248 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,6 +53,8 @@ Design tokens live in the runtime token CSS (`apps/ui/src/assets/styles/tokens.c `docs/` is now the **public website docs** tree (committed, flat layout, see `docs/menu.yml`). Do not put planning notes, research dumps, or design prototypes there — those stay out of git. +**Docs are written for the end user (operator / dashboard author), not the contributor.** Document what a feature *does*, how to *configure* it (YAML, fields, recipes), and how to *operate / troubleshoot* it. Do **not** document the internal code workflow: no step-by-step algorithms ("1. service-bind 2. user search 3. …"), no source-file paths (`apps/bff/src/...`), no internal function / composable / route names, no "the BFF then fans out / chunks / probes …" implementation narration. If [...] + ## Things that are non-negotiable - **TypeScript strict.** No `any` outside `.d.ts` shims. @@ -66,7 +68,7 @@ Design tokens live in the runtime token CSS (`apps/ui/src/assets/styles/tokens.c - **Cascade-clear, then load.** When an upstream control changes (service / instance / endpoint pick, time-range change, layer / scope nav) and the downstream queries have to refire, the dependent area must visibly RESET first and show an explicit "Reading data…" (or equivalent) hint while the new query is in flight. Never leave the prior value sitting under a spinner — operators read it as the new state and trust broken data. Never let the page sit silently between the click and the res [...] - **MQE is a core capability**, not a config-screen afterthought. User-editable, syntax-highlighted, debuggable. - **Admin views use the same look.** LDAP/RBAC/admin are dark, dense, design-tokens — not a separate "settings" UI. Alarms are read-only on the UI side; recovery is backend-automatic (no acknowledge/close/silence actions). -- **Comments earn their keep.** Only write a comment when it does one of two things: (1) introduces an API — what a module / component / exported function is for and how callers should think about it; (2) highlights a non-obvious gotcha — a hidden invariant, a workaround for a specific upstream quirk, a subtle scope/timing constraint. Do **not** write comments that paraphrase the code (`// loop over layers`, `// click handler — toggles open state`, `// returns true when active`). Do **no [...] +- **Comments earn their keep.** A comment should carry only what is **not obvious from reading the core code** — a highlight, a special case, or an edge case worth flagging for a future reviewer. Two valid uses: (1) introduces an API — what a module / component / exported function is for and how callers should think about it; (2) highlights a non-obvious gotcha — a hidden invariant, a workaround for a specific upstream quirk, a subtle scope/timing constraint, an edge case the reader woul [...] - **Layering — BFF.** `apps/bff/src/` is grouped by role, and the flow is one-directional: `http → logic → client → OAP`. - `http/{query,config,admin}/` — Fastify route handlers only. Thin: parse, dispatch to logic, shape the reply. No OAP I/O directly. - `logic/<domain>/` — domain orchestration + background timers (alarms store, layer/overview/setup loaders, preflight status check, inspect parsers, dashboard defaults). No HTTP framework, no OAP fetch detail. diff --git a/docs/README.md b/docs/README.md index c8b4aff..f098e10 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,16 +16,7 @@ The sidebar on the left of this site is the canonical entry point — every sect ## Quick orientation -The UI is a pnpm monorepo: - -| App / package | Purpose | -|---|---| -| `apps/ui/` | Vue 3 + Vite single-page app. | -| `apps/bff/` | Fastify-based Backend For Frontend. The single place that talks to OAP — query GraphQL + admin REST + Zipkin. | -| `packages/api-client/` | TypeScript types shared between BFF and UI. | -| `packages/design-tokens/` | CSS custom properties shipped to both apps (the 5 bundled themes live here). | - -The UI **only** talks to the BFF; the BFF is the single place that talks to OAP. Every OAP-side requirement is enforced once, in the BFF, not scattered through the UI. +Horizon runs as two pieces: a Vue single-page app (the UI) and a Backend For Frontend (the BFF). The UI **only** talks to the BFF; the BFF is the single place that talks to OAP — query GraphQL, admin REST, and Zipkin. Every OAP-side requirement is enforced once, in the BFF, not scattered through the UI. ## Where to start, by role diff --git a/docs/access-control/admin-pages.md b/docs/access-control/admin-pages.md index 448e8d4..f54c799 100644 --- a/docs/access-control/admin-pages.md +++ b/docs/access-control/admin-pages.md @@ -5,7 +5,6 @@ Three pages under `/admin/` surface authentication, user, and RBAC state for liv ## Login page **Path:** `/login` -**File:** `apps/ui/src/features/auth/LoginView.vue` The redesigned login page: @@ -14,23 +13,18 @@ The redesigned login page: - **Inline auth-status pill** that adapts to the active backend: - Green: local backend, **or** LDAP backend with the directory reachable. - Red: LDAP backend with directory unreachable (warns that break-glass may be armed). -- Backend health is polled every 5 seconds via `GET /api/auth/health`. +- Backend health is polled continuously so the pill reflects directory outages in near real time. - Form fields: username, password. Submit is disabled while in flight. - Apache copyright footer with auto-current year. -After successful login, the UI redirects to: - -1. `?redirect=<path>` if the user was bounced from a protected route, **or** -2. `landingByRole[<first role>]` (see [RBAC](rbac.md)). +After successful login, the UI redirects to the page the user was bounced from (if they hit login from a protected route), otherwise to the landing route for their role (see [RBAC](rbac.md)). ## Auth Status **Path:** `/admin/auth-status` **Verb:** `auth:read` (maintainer, admin) -**File:** `apps/ui/src/features/admin/auth-status/AuthStatusView.vue` -**Endpoints:** `GET /api/admin/auth-status` (30 s auto-refresh), `POST /api/admin/auth-status/probe` -The single pane for "is my auth wiring correct?" Shows: +Auto-refreshes every 30 seconds. The single pane for "is my auth wiring correct?" Shows: | Section | Content | |---|---| @@ -39,13 +33,13 @@ The single pane for "is my auth wiring correct?" Shows: | Local users | Count of `auth.local.users` entries (zero in LDAP mode). | | LDAP probe | Reachability, service-bind success, user-search success, latency, last error. | | Group-to-role mappings | The full `groupMappings` table from `horizon.yaml`. | -| Active sessions | Count from the in-memory session map. | +| Active sessions | Count of currently open sessions. | | Break-glass | Configured? Armed (LDAP unhealthy)? Username (hash not shown). | | RBAC policy snapshot | All role names, all known verbs. | ### Live probe -A manual **Probe now** button fires `POST /api/admin/auth-status/probe` for an immediate refresh (does not wait for the 30 s tick). +A manual **Probe now** button triggers an immediate refresh (does not wait for the 30 s tick). ### Username resolver @@ -60,14 +54,12 @@ No login required for the resolution — useful for "if Alice tried to log in ri **Path:** `/admin/users` **Verb:** `user:read` (admin) -**File:** `apps/ui/src/features/admin/users/UsersAdminView.vue` -**Endpoint:** `GET /api/admin/users` (15 s auto-refresh) -Lists users known to this BFF instance. Three sources merged: +Auto-refreshes every 15 seconds. Lists users known to this BFF instance, drawn from three sources: -1. **LDAP users**: from the in-memory seen-cache (anyone who successfully logged in via LDAP on this BFF since startup). -2. **Local users**: static entries from `auth.local.users` in `horizon.yaml`. -3. **Break-glass logins**: seen-cache entries marked `source: break-glass`. +- **LDAP users**: anyone who successfully logged in via LDAP on this BFF since startup. +- **Local users**: static entries from `auth.local.users` in `horizon.yaml`. +- **Break-glass logins**: prior break-glass sessions seen on this BFF. Per-row: @@ -89,14 +81,12 @@ Per-row: ### Operations -The Users page is **read-only**. To add a local user, edit `horizon.yaml`. To remove an LDAP user, do so in the directory; the seen-cache entry persists until BFF restart but is informational only. +The Users page is **read-only**. To add a local user, edit `horizon.yaml`. To remove an LDAP user, do so in the directory; the row persists until BFF restart but is informational only. ## Roles & Permissions **Path:** `/admin/roles` **Verb:** `role:read` (admin) -**File:** `apps/ui/src/features/admin/roles/RolesView.vue` -**Endpoint:** `GET /api/admin/auth-status` (shared with the Auth Status page; reads the `rbac` block) Renders a read-only board of roles × verbs as a check-mark grid. The intent is to answer "what can role X do?" without having to re-derive it from `horizon.yaml`. diff --git a/docs/access-control/audit-log.md b/docs/access-control/audit-log.md index ce7cf4b..6a008ec 100644 --- a/docs/access-control/audit-log.md +++ b/docs/access-control/audit-log.md @@ -4,7 +4,7 @@ The audit log records sensitive operations as JSON Lines, one event per line, ap ## Event schema -Source: `apps/bff/src/audit/logger.ts`. +Each event has these fields: ```ts interface AuditEvent { @@ -107,7 +107,7 @@ The recorded set evolves with the codebase. As of the current build: ## File format - **JSON Lines.** One JSON object per line, `\n`-terminated. -- **Append-only.** The BFF opens the file in append mode and never truncates / rotates. +- **Append-only.** The file is only ever appended to — never truncated or rotated. - **No rotation built in.** Pair with `logrotate`, `vector`, `fluent-bit`, or a sidecar shipper. ## Storage placement @@ -124,11 +124,11 @@ The recorded set evolves with the codebase. As of the current build: ## In-memory "seen cache" -In addition to the on-disk audit log, the BFF maintains an in-memory `UserSeenCache` of successful logins: +In addition to the on-disk audit log, the BFF keeps an in-memory record of recent successful logins: - Records: username, source (`local` / `ldap` / `break-glass`), roles, last-seen timestamp, last IP. - Reset on BFF restart. -- Exposed via `GET /api/admin/users` and visible on the Users admin page. +- Visible on the Users admin page. This is a UX convenience — it lets the Users page show "who has logged in to this BFF instance recently" without parsing the audit log. For historical / cluster-wide analysis, parse the JSONL file directly. diff --git a/docs/access-control/break-glass.md b/docs/access-control/break-glass.md index 97896a7..dea6313 100644 --- a/docs/access-control/break-glass.md +++ b/docs/access-control/break-glass.md @@ -30,7 +30,7 @@ The block is **optional** — leave it out (or commented) to disable break-glass Break-glass is honored at login **only when both** are true: 1. `auth.backend: ldap` (the block is unused in local mode — a startup warning is logged if both are present). -2. `ldapHealth.isUnhealthy()` returns true at the moment of login. The probe runs continuously; "unhealthy" means the last probe (TCP / bind / search) failed. +2. The directory is unreachable at the moment of login. Horizon probes the directory continuously; "unhealthy" means the most recent probe (connect / bind / search) failed. When activated: @@ -43,10 +43,8 @@ When LDAP is healthy again, the break-glass username is rejected at login — ev ## Verification -`apps/bff/src/user/break-glass.ts`: - -- Same Argon2id verifier as the local backend. -- **Timing-safe** — a wrong username still incurs the argon2 cost (verify against a dummy hash) to prevent leaking which break-glass username is configured via timing. +- The password is checked with the same Argon2id verification as the local backend. +- **Timing-safe** — a wrong username still incurs the full argon2 cost, so an attacker cannot learn the configured break-glass username from response timing. ## Audit diff --git a/docs/access-control/ldap-backend.md b/docs/access-control/ldap-backend.md index 7e22288..0694586 100644 --- a/docs/access-control/ldap-backend.md +++ b/docs/access-control/ldap-backend.md @@ -28,20 +28,16 @@ auth: 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 +## How sign-in works -`apps/bff/src/user/ldap.ts`: +Horizon never stores or reads stored passwords. A sign-in attempt authenticates **as the user** against the directory with the typed password, so the directory itself decides whether the password is valid. On success, Horizon reads the user's group memberships and maps them to roles via `groupMappings`. -1. **Service bind** (if `bindDn` is set) or anonymous bind. Used to search for the user's DN. -2. **User search** — apply `userFilter` against `userBaseDn`, substituting `{username}` (RFC 4515 escaped). Expect exactly one result; multiple matches abort with `null`. -3. **User bind** — bind directly as the discovered DN with the typed password. A successful bind proves the password. -4. **Group resolution** — per `groupStrategy`: - - `memberOf`: read the `memberOf` attribute from the user entry (AD-style). - - `search`: search `groupBaseDn` for groups whose `memberAttr` contains the user's DN (OpenLDAP-style). -5. **Group → role mapping** — walk `groupMappings` in order. **First match wins per mapping** (a user matching multiple mappings gets the union of their roles). -6. Return `{ username, roles }` on success, `null` on any failure. +What this means when you configure LDAP: -A failure at any step returns `null` — the UI shows a generic "Invalid credentials" message. No information leak about which step failed. +- **Group membership is read with the service account** (`bindDn`), not the user's own credentials. Many directories deny ordinary users read access to the group subtree, so the **service account must be able to see groups** — otherwise every user falls back to the `*` role. +- **`userFilter` must resolve to a single user.** If it matches more than one entry, the first match is used. +- **Roles are the union of all matching `groupMappings`** — a user in two mapped groups gets both roles' permissions. +- **Failures are deliberately indistinguishable.** A wrong password, a missing user, or no matching group all surface the same "Invalid credentials" message; the specific cause is never revealed to the browser. ## Field reference @@ -64,7 +60,7 @@ See [`auth`](../setup/auth.md) for the field table. | Strategy | When to use | |---|---| | `memberOf` | Active Directory and most modern OpenLDAP deployments. User entries carry a `memberOf` multi-valued attribute. Faster (single read, no second search). | -| `search` | OpenLDAP deployments where users do not carry `memberOf`. Requires `groupBaseDn` and uses `memberAttr` (usually `member` or `uniqueMember`). | +| `search` | OpenLDAP deployments where users do not carry `memberOf`. Requires `groupBaseDn` and uses `memberAttr` (usually `member` or `uniqueMember`). The **service account** (`bindDn`) — not the logging-in user — performs this search, so it must have read access to the group subtree. | When unsure, try `memberOf` first; if a successful user bind returns no groups, switch to `search`. @@ -83,21 +79,11 @@ groupMappings: - A user matching multiple groups gets the **union** of all matching roles. E.g., a user in both `cn=sre` and `cn=platform` ends up with `operator` and `maintainer` roles (effective verbs are the union of both role's grants). - Order matters only in the sense of being listed; all matching entries contribute. -## Health probing +## Health and directory reachability -The BFF exposes `GET /api/auth/health` (polled by the login page every 5 seconds): +The login page continuously reflects whether the directory is reachable, so operators see an outage before a user reports a failed login. When the directory is unreachable, that state is also what arms [Break-Glass Access](break-glass.md). -- `local`: backend is local — returns `reachable: true` unconditionally. -- `ldap reachable`: last LDAP probe succeeded. -- `ldap unreachable`: last probe failed. - -The health probe runs: - -1. TCP / TLS connect to `ldap.url`. -2. Service bind (or anonymous bind). -3. (Optional username resolver) A test search for a known username when invoked from the admin Auth Status page. - -Probe failure is the trigger condition for [Break-Glass Access](break-glass.md). +The admin **Auth Status** page lets you confirm the connection (and the service bind) and test a username against the live directory — it shows the groups returned and the roles those groups resolve to, without the user needing to sign in. Use it to debug `groupMappings` and to verify the service account can read the group subtree. ## TLS @@ -124,5 +110,6 @@ OAP does **not** see Horizon's LDAP credentials. The user authenticates against - **Service bind fails silently.** Wrong `bindDn` or `bindPassword` causes all logins to fail with a generic message. Verify by looking at LDAP server logs. - **`groupStrategy: memberOf` on a directory that doesn't populate it.** Logins succeed but every user gets only the `"*"` fallback role. Switch to `search`. +- **`search` strategy with a locked-down group subtree.** Group resolution runs on the service account (`bindDn`), so grant *that* account read access to `groupBaseDn`. (The logging-in user does not need it — Horizon never uses the user's own bind for group lookup.) - **Forgetting the `"*"` fallback.** A user who authenticates but matches no group mapping is rejected — change to `null` and the UI shows "Invalid credentials". Add `"*" → viewer` for graceful degradation. - **`tlsInsecure: true` in production.** A man-in-the-middle on the LDAP connection can capture every typed password. Use proper certificates instead. diff --git a/docs/access-control/local-backend.md b/docs/access-control/local-backend.md index f1f1cb8..dc01ce7 100644 --- a/docs/access-control/local-backend.md +++ b/docs/access-control/local-backend.md @@ -38,16 +38,14 @@ Paste into `passwordHash`. The CLI does not store anything; copy the hash, lose Algorithm: Argon2id with the `node-argon2` defaults (m=65536, t=3, p=4). Hashes generated by other tools are accepted as long as they validate via the node-argon2 verifier. -## How verification works +## How sign-in works -`apps/bff/src/user/local.ts`: +A sign-in matches the typed username against `local.users` and verifies the password against the stored Argon2id hash. On a match the session is created with that user's roles; otherwise the login is rejected. -1. Lookup user by `username`. If not found, fall through to a **dummy** Argon2id verification against a sentinel hash. This makes "user does not exist" indistinguishable from "wrong password" via timing — preventing username enumeration. -2. If found, `argon2.verify(passwordHash, typedPassword)`. -3. On match, return `{ username, roles }`. -4. On mismatch, return `null`. +What this means in practice: -The BFF logs success / failure through the audit log (see [Audit Log](audit-log.md)) but never logs the typed password. +- **Username enumeration is prevented.** A wrong username and a wrong password are indistinguishable — an unknown username still incurs the full password-verification cost, so response timing never reveals whether a username exists. +- **Successes and failures are recorded** in the audit log (see [Audit Log](audit-log.md)), but the typed password is never logged. ## Operations diff --git a/docs/access-control/rbac.md b/docs/access-control/rbac.md index 3766315..8a410b0 100644 --- a/docs/access-control/rbac.md +++ b/docs/access-control/rbac.md @@ -5,15 +5,15 @@ Horizon enforces access at the BFF on every HTTP request. The UI hides controls ## Model - **Subject**: an authenticated session (`username + roles`). -- **Object**: an HTTP route. -- **Action (verb)**: a dot-namespaced string declared by the route policy table. -- **Decision**: granted iff any of the user's roles has a grant that matches the route's required verb. +- **Object**: a protected request. +- **Action (verb)**: a dot-namespaced string each protected request requires. +- **Decision**: granted if any of the user's roles holds a grant that matches the required verb. -Sessions capture the **role list** at login time. Verbs are computed per request from `session.roles → rbac.roles → grants`. Hot-reloading role definitions takes effect on the next route check; hot-reloading group mappings or local user roles requires the user to re-login (since sessions hold their original role list). +Sessions capture the **role list** at login time, and the verbs they grant are resolved from the current `rbac.roles` definitions on each request. Hot-reloading role definitions takes effect immediately; hot-reloading group mappings or local user roles requires the user to re-login (since sessions hold their original role list). ## Verb vocabulary -Source: `apps/bff/src/rbac/verbs.ts`. Twenty-eight verbs grouped into areas: +Twenty-eight verbs grouped into areas: ### Data reads (the public catalog) @@ -70,7 +70,7 @@ Source: `apps/bff/src/rbac/verbs.ts`. Twenty-eight verbs grouped into areas: ## Grant matching -A user's grant string is matched against a required verb using these rules (`verbs.ts`): +A user's grant string is matched against a required verb using these rules: | Grant pattern | Matches | |---|---| @@ -132,7 +132,7 @@ A user with no role gets no verbs. The session is created (login succeeds) but e ## Landing route per role -After login, the BFF returns a `landingRoute` from `rbac.landingByRole`. The UI router uses it as the post-login destination unless `?redirect=` overrides (set when the user was bounced to login from a protected route — they return to where they came from). +After login, the user lands on the route configured for their role in `rbac.landingByRole` — unless they were bounced to login from a protected route, in which case they return to where they came from. Default mapping: @@ -148,35 +148,9 @@ When a user has multiple roles, the **first role on the user** wins. Order matte ## Enforcement -Source: `apps/bff/src/rbac/route-policy.ts`, `apps/bff/src/rbac/policy.ts`, `apps/bff/src/user/middleware.ts`. +Access is enforced server-side, not in the browser. Every protected request is checked for a valid session (an unauthenticated request is rejected with `401`) and then for the verb that request requires (a session lacking the verb is rejected with `403`). The UI hides controls a session cannot use, but a forged UI cannot bypass these checks. -The BFF builds a route → required-verb table at startup. Every Fastify route is gated by: - -1. `requireAuth()` — looks up the session cookie, returns 401 on missing / expired. -2. `checkVerb(verb)` — looks up the session's effective verbs, returns 403 on mismatch. - -Routes without an explicit policy entry default to `'auth'` (session required, no specific verb) **with a warning at startup**. This is fail-safe: a forgotten route does not become accidentally public. - -### Policy values - -| Policy | Meaning | -|---|---| -| `'public'` | No auth required. Login, logout, health-check endpoints. | -| `'auth'` | Session required; no verb check. Identity-only routes. | -| `'<verb>'` | Session required + verb check. Most application routes. | - -### Example policy entries - -```ts -'POST /api/auth/login': 'public', -'POST /api/auth/logout': 'public', -'GET /api/auth/me': 'auth', -'GET /api/oap/info': 'auth', -'POST /api/layer/:key/dashboard': 'metrics:read', -'GET /api/rule': 'rule:read', -'POST /api/rule/addOrUpdate': 'rule:write', -'POST /api/admin/auth-status/probe': 'auth:read', -``` +Enforcement is fail-safe: a request with no explicit verb still requires a valid session, so a misconfiguration cannot accidentally expose a protected endpoint to anonymous callers. ## Disabling RBAC for dev diff --git a/docs/compatibility/cluster-status.md b/docs/compatibility/cluster-status.md index 8331c6f..f54a8af 100644 --- a/docs/compatibility/cluster-status.md +++ b/docs/compatibility/cluster-status.md @@ -6,8 +6,6 @@ This page is intentionally two-pane: a healthy `:12800` with broken `:17128` is ## Pane A — Query / GraphQL port (`:12800`) -**Source:** `apps/bff/src/http/query/info.ts`, UI composable `apps/ui/src/shell/useOapInfo.ts`. - **Single GraphQL call** fired every 30 seconds: ```graphql @@ -41,8 +39,6 @@ query { ## Pane B — Admin host (`:17128`) -**Source:** `apps/bff/src/http/query/preflight.ts`, UI composable `apps/ui/src/shell/useAdminFeatures.ts`. - **Single admin REST call** fired every 60 seconds: ``` @@ -86,7 +82,7 @@ The sequence is fail-fast: once `admin-server` itself is off, the dump is empty In addition to the two health panes, the page lists OAP cluster members: -- **Source**: `GET <queryUrl>/status/cluster/nodes` (status client, `packages/api-client/src/status.ts`). +- **Source**: `GET <queryUrl>/status/cluster/nodes`. - **Returns**: per-node host, port, role, heartbeat. - **Use**: confirm cluster size matches expectations (e.g., 3-node OAP behind one DNS name should show three rows). diff --git a/docs/compatibility/required-modules.md b/docs/compatibility/required-modules.md index 1a0c420..8d625d3 100644 --- a/docs/compatibility/required-modules.md +++ b/docs/compatibility/required-modules.md @@ -22,9 +22,7 @@ The entire admin-port surface (all four modules) is **OAP 11.x only**. On OAP 10 ## How Horizon detects module state -Source: `apps/bff/src/logic/preflight/preflight.ts`. - -1. BFF fires `GET <adminUrl>/debugging/config/dump` (polled every 60 seconds from the UI). +1. Horizon fires `GET <adminUrl>/debugging/config/dump` (polled every 60 seconds from the UI). 2. OAP returns a flat key/value map in `module.provider.property` form, e.g.: ``` diff --git a/docs/components/charts.md b/docs/components/charts.md index 496c360..6403c6c 100644 --- a/docs/components/charts.md +++ b/docs/components/charts.md @@ -1,15 +1,12 @@ # Charts -Horizon wraps all chart rendering in dedicated components. The widget primitives (overview + dashboard) delegate to these wrappers. Per project rule, **no view ever instantiates ECharts directly** — the wrappers own the lifecycle, handle theming, sync crosshairs, and tear down on unmount. +Horizon renders metrics through a small set of chart kinds. This page describes each kind, the inputs it accepts, and how it behaves. For the dashboard/overview widget types that select these charts, see [Dashboard Widgets](dashboard-widgets.md). -This page is for developers extending or troubleshooting the chart layer. End-users of the templating system do not need it; reach for [Dashboard Widgets](dashboard-widgets.md) instead. +## Time chart -## `TimeChart` - -**Path:** `apps/ui/src/components/charts/TimeChart.vue` **Used by:** `line` dashboard widget; ad-hoc embeds in feature pages. -**Renders:** Multi-series line chart via ECharts. +**Renders:** Multi-series line chart. ### Props @@ -37,17 +34,12 @@ interface Series { - Dual y-axis appears when any series has `yAxisIndex: 1`. - Legend visible iff `series.length > 1`. - Smooth lines with circle point markers. -- Tooltip positioned via callback (appendToBody) so it does not clip near grid edges. -- **Synced crosshairs**: hover events broadcast to peer `TimeChart` instances on the same page via the shared chart hover bus. -- Fingerprinting: data-only updates animate smoothly; structure changes (series count, label set) do a full replace. - -### Adding a new chart kind +- Tooltip is positioned so it does not clip near grid edges. +- **Synced crosshairs**: hovering broadcasts to peer time charts on the same page so they highlight the same time. +- Data-only updates animate smoothly; structure changes (series count, label set) do a full replace. -Any new ECharts-backed visualization should land as a sibling component, not as a fork of `TimeChart`. Share the hover-bus subscription if it should participate in synced crosshairs. +## Top list -## `TopList` - -**Path:** `apps/ui/src/components/charts/TopList.vue` **Used by:** `top` dashboard widget. **Renders:** Sorted list with optional tab switcher. @@ -87,9 +79,8 @@ interface DashboardTopItem { - Background fill bar normalized to the maximum value (per tab in multi-list mode). - Tabs shown when `groups.length > 1`. -## `AlarmsTimeline` +## Alarms timeline -**Path:** `apps/ui/src/components/charts/AlarmsTimeline.vue` **Used by:** Alarms page (full timeline above the alarm table). **Renders:** Per-minute stacked bar chart of firing + recovered alarms, with brush selection. @@ -118,12 +109,11 @@ interface DashboardTopItem { - Brush (`lineX`) for range selection. Snaps to minute boundaries. - Click on non-zero point → selects that single minute. Click on zero → clears selection. -## `Sparkline` +## Sparkline -**Path:** `apps/ui/src/components/charts/Sparkline.vue` **Used by:** Inline tiles, sidebar mini-charts, layer service-list picker (when a column carries a trend). -**Renders:** Tiny inline SVG. No ECharts, no animation — lightweight enough to render dozens per page. +**Renders:** Tiny inline trend line — lightweight enough to render dozens per page. ### Props @@ -150,20 +140,6 @@ interface DashboardTopItem { - Gap bridging on `null` entries (line breaks). - No interactivity beyond hover broadcasting. -## D3 components - -Where ECharts is wrong (custom interactions, non-cartesian layouts), Horizon uses D3 wrappers under `apps/ui/src/components/charts/` named `Native*`. The same lifecycle rule applies — the composable owns mount, render, and tear-down; no view manipulates the DOM directly. - ## Theming -All chart colors derive from the design token CSS (`apps/ui/src/assets/styles/tokens.css`). Per-chart accents take a CSS variable string (`var(--sw-accent)`) by default — the chart resolves to the token's current value at render time, which means a theme switch updates colors live without remount. - -Hex strings are accepted for one-off cases (e.g. severity colors); prefer tokens for anything that should follow theming. - -## Adding a new chart wrapper - -1. Place under `apps/ui/src/components/charts/` (shared) or `apps/ui/src/features/<feature>/` (feature-scoped) per the layering rule. -2. Own the lifecycle: instantiate in `onMounted`, dispose in `onBeforeUnmount`. Never let the chart outlive its component. -3. Resolve theme tokens via `getComputedStyle(document.documentElement)` if you need numeric values; or pass CSS variable strings through directly when the chart supports them. -4. If the chart is time-series with hover semantics, subscribe to the shared hover bus so it joins synced crosshairs. -5. Add a license header (`.ts` and `.vue` files require one — see `.licenserc.yaml`). +Chart colors follow the active design theme. Per-chart accents default to the theme accent and update live when the theme is switched — no reload needed. Hex color strings are accepted for one-off cases (e.g. severity colors); prefer the theme accent for anything that should follow theming. diff --git a/docs/components/dashboard-widgets.md b/docs/components/dashboard-widgets.md index 3e462bf..18930b5 100644 --- a/docs/components/dashboard-widgets.md +++ b/docs/components/dashboard-widgets.md @@ -1,6 +1,6 @@ # Dashboard Widgets -Four widget types render on per-layer dashboards. The renderer (`apps/ui/src/render/layer-dashboard/LayerDashboardsView.vue`) branches on `widget.type` and delegates to a small component per kind. +Four widget types render on per-layer dashboards. Each `widget.type` you set in a template selects one of them. ## Grid context @@ -70,7 +70,7 @@ A `line` widget with a scalar-shaped MQE renders a one-point chart, which is mis ## `line` -**Renders:** Multi-series line chart via the `TimeChart` component (ECharts wrapper). +**Renders:** Multi-series line chart. ### Multi-series @@ -115,9 +115,9 @@ When any series has `yAxisIndex: 1`, the right axis appears. Use for mixed-unit - Smooth lines with circle markers. - Legend visible when more than one series; hidden for single series. -- Tooltip positioned via callback (appendToBody) so it does not clip near grid edges. +- Tooltip is positioned so it does not clip near grid edges. - **Synced crosshairs**: pointing at a time on this chart highlights the same time on every other `line` chart on the page. -- Fingerprinting: data-only updates (same structure, new values) animate smoothly. Structure changes do a full replace. +- Data-only updates (same structure, new values) animate smoothly. Structure changes do a full replace. ### When `line` is wrong @@ -188,7 +188,7 @@ The data source returns a record set (rows × typed columns) rather than a numer ### Behavior - Renders as a dense table with column headers from the record's typed fields. -- Sort, filter, pagination handled in the component. +- Supports sort, filter, and pagination. ## Visibility predicates @@ -229,6 +229,6 @@ The widget editor (planned) will warn on type / MQE mismatches. The schema does ## Per-scope widget sets -The `dashboards.<scope>` map on a layer template lets you define different widget grids for service / instance / endpoint / topology / trace / logs / profiling pages. Scope resolution falls back to `service` if a specific scope is unset (`apps/bff/src/logic/layers/loader.ts:widgetsForScope()`). +The `dashboards.<scope>` map on a layer template lets you define different widget grids for service / instance / endpoint / topology / trace / logs / profiling pages. If a specific scope is unset, it falls back to the `service` scope. See [Customization → Layer Dashboard Templates](../customization/layer-templates.md) for the per-scope structure. diff --git a/docs/components/overview-widgets.md b/docs/components/overview-widgets.md index d2c50e1..5e6f1e7 100644 --- a/docs/components/overview-widgets.md +++ b/docs/components/overview-widgets.md @@ -1,6 +1,6 @@ # Overview Widgets -Six widget types render on overview pages. Each is a single Vue component with a tightly scoped prop interface; the renderer (`apps/ui/src/render/overview/OverviewDashboardView.vue`) branches on `widget.type` and passes the relevant fields. +Six widget types render on overview pages. Each `widget.type` you set in a template selects one of them, and reads its own set of fields. ## Grid context (recap) @@ -11,7 +11,6 @@ Six widget types render on overview pages. Each is a single Vue component with a ## `metric` -**Component:** `MetricWidget.vue` **Renders:** Single scalar with optional unit. Used for headline KPIs on overviews. ### Fields @@ -30,7 +29,7 @@ Six widget types render on overview pages. Each is a single Vue component with a ### Behavior -Value formatting via `formatValue()` (`render/widgets/ValueFormat.ts`): +Values are formatted compactly: - M / k suffixes for large numbers (1.2M, 3.4k). - Two decimal places for fractional values. @@ -54,7 +53,6 @@ Value formatting via `formatValue()` (`render/widgets/ValueFormat.ts`): ## `kpi-tile` -**Component:** `KpiTileWidget.vue` **Renders:** Compound tile — optional service-count header row plus N KPI rows. Each KPI row is either a number readout or a progress bar. ### Fields @@ -79,7 +77,7 @@ Value formatting via `formatValue()` (`render/widgets/ValueFormat.ts`): ### Behavior -- `style: number` — value formatted via `formatValue()`, right-aligned. +- `style: number` — value formatted compactly, right-aligned. - `style: progress-bar` — fill ratio = `value / max`. Color follows the layer accent. - `showCount` row clickable; KPI rows are not (the whole tile is the unit of action). @@ -103,7 +101,6 @@ Value formatting via `formatValue()` (`render/widgets/ValueFormat.ts`): ## `metric-composite` -**Component:** `MetricCompositeWidget.vue` **Renders:** Mixed KPI layout — number-style KPIs go into auto-fit count tiles; progress-bar-style (or `unit: '%'`) KPIs go into the bar grid. One widget can carry both shapes. This is the unified replacement for the old per-feature widgets (`k8s-service-count`, `pilot`, `service-count`). Anything compound now goes through `metric-composite`. @@ -113,7 +110,7 @@ This is the unified replacement for the old per-feature widgets (`k8s-service-co | Field | Type | Notes | |---|---|---| | `id`, `title`, `tip`, `layer`, `span`, `rowSpan` | — | Common. | -| `kpis` | `OverviewKpi[]` | Auto-split by the renderer. | +| `kpis` | `OverviewKpi[]` | Auto-split between count tiles and the bar grid (see below). | ### Layout @@ -152,7 +149,6 @@ Otherwise it lands in the count tiles. This lets you author a Kubernetes-style s ## `alarms` -**Component:** `AlarmsWidget.vue` **Renders:** Active-incident rail. Top-N rows of the most recent firing alarms in the last 60 minutes, plus a total count chip. ### Fields @@ -165,7 +161,7 @@ Otherwise it lands in the count tiles. This lets you author a Kubernetes-style s ### Behavior -- Fetches via `bff.alarms.list()` (60-minute window, server-resolved). +- Fetches the most recent firing alarms over a 60-minute, server-resolved window. - **Dual-mode fetch:** - **Modern** (`queryAlarms` capability present): server-side layer filter, server-side time window. - **Legacy** (`getAlarm` only): all-layers fetch, client-side layer filter. @@ -188,7 +184,6 @@ Otherwise it lands in the count tiles. This lets you author a Kubernetes-style s ## `topology` -**Component:** (renders a topology snapshot via the shared topology component) **Renders:** Service-map for the configured layer. Static snapshot of the current window — the full Topology tab on a per-layer page is interactive; the overview widget is a glanceable view. ### Fields @@ -214,7 +209,6 @@ No MQE — uses the layer's topology metric from the layer template (`topology.m ## `section-break` -**Component:** `SectionBreak.vue` **Renders:** Visual row header with horizontal rules. No data fetch. ### Fields diff --git a/docs/customization/adding-a-new-layer.md b/docs/customization/adding-a-new-layer.md index 74f13ab..aa6200a 100644 --- a/docs/customization/adding-a-new-layer.md +++ b/docs/customization/adding-a-new-layer.md @@ -26,7 +26,7 @@ curl -s -X POST <queryUrl>/graphql \ -d '{"query":"{ listServices(layer:\"<KEY>\") { id name normal } }"}' | jq ``` -A layer with zero services is hidden from the sidebar (it appears in the BFF's menu response but is filtered out by `availableLayers`). Ingest some data first, then proceed. +A layer with zero services is hidden from the sidebar (it appears in the menu response but is filtered out of the visible list). Ingest some data first, then proceed. ### 3. Identify the layer's metric prefix diff --git a/docs/customization/layer-templates.md b/docs/customization/layer-templates.md index 0cad864..c5bcdca 100644 --- a/docs/customization/layer-templates.md +++ b/docs/customization/layer-templates.md @@ -210,7 +210,7 @@ The bulk of the template. A map from scope to an ordered widget array. ### Scope resolution -`apps/bff/src/logic/layers/loader.ts:widgetsForScope()` resolves in this order: +Widgets for a scope resolve in this order: ``` dashboards[scope] → dashboards.service → template.widgets (legacy) diff --git a/docs/customization/menu-structure.md b/docs/customization/menu-structure.md index baeca00..9d606bf 100644 --- a/docs/customization/menu-structure.md +++ b/docs/customization/menu-structure.md @@ -10,25 +10,13 @@ This page documents how those three combine into the live sidebar. ## Data flow -``` -OAP BFF UI -───────────────────── ────────────────────────────── ──────────────────── -listLayers -listServices(layer) /api/menu useLayers() -listLayerLevels → merge with → useLandingOrder() -getMenuItems bundled_templates/layers/ AppSidebar.vue - <key>.json -``` - -1. **OAP discovery.** The BFF calls the four GraphQL queries on every `/api/menu` hit. Layers reported by `listLayers` are "active". -2. **Template merge.** For each active layer, the BFF loads `bundled_templates/layers/<key>.json` (or applies defaults if absent) and merges OAP-provided data with template-provided cosmetics: `alias`, `color`, `group`, `visibility`, `caps`, `slots`, `header`, `overview`, `log`, `traces`, `naming`, `documentLink`. -3. **Counts.** For each layer, `listServices(layer)` is called to get the service count. The count is `-1` if OAP is unreachable. -4. **UI hydration.** The UI receives a `MenuResponse` with the layer list and renders the sidebar via `useLayers` (which layers exist) and `useLandingOrder` (in what order). +1. **OAP discovery.** Layers reported by `listLayers` are "active". +2. **Template merge.** For each active layer, the bundled `bundled_templates/layers/<key>.json` (or defaults if absent) is merged with OAP-provided data, contributing template cosmetics: `alias`, `color`, `group`, `visibility`, `caps`, `slots`, `header`, `overview`, `log`, `traces`, `naming`, `documentLink`. +3. **Counts.** Each layer carries a service count from `listServices(layer)`. The count is `-1` if OAP is unreachable. +4. **Sidebar render.** The sidebar shows the layer list, ordered per the user's landing-order preference. ## The `MenuResponse` shape -`packages/api-client/src/menu.ts`: - ```ts interface MenuResponse { layers: LayerDef[]; @@ -65,7 +53,7 @@ The sidebar has two main sections + the static Operate group: Active, public layers (`visibility: 'public'`, `serviceCount > 0`). Sorted by: -1. `useLandingOrder` — per-user `landing.priority` from the setup store. +1. Per-user `landing.priority` from the setup store. 2. Falls back to `level` from `listLayerLevels` when no user priority is set. A layer with `serviceCount === 0` is hidden from the Layers section but still available for the admin's setup screen ("enable this layer when services appear"). @@ -95,7 +83,7 @@ These are not layer-derived; they are first-class Horizon features. ## Per-layer composition -When a user clicks a layer in the sidebar, `firstLayerTab()` picks the first enabled sub-route from this priority order: +When a user clicks a layer in the sidebar, the first enabled sub-route is picked from this priority order: ``` service → instance → endpoint → topology → trace → logs → profiling diff --git a/docs/customization/overview-templates.md b/docs/customization/overview-templates.md index b96eb22..1fdadd5 100644 --- a/docs/customization/overview-templates.md +++ b/docs/customization/overview-templates.md @@ -36,7 +36,7 @@ Bundled templates: `apps/bff/src/bundled_templates/overviews/<id>.json`. Example | `title` | string | **required** | Display title in the sidebar and page header. | | `description` | string | — | One-line description shown under the title. | | `visibility` | `public` \| `operate` | `public` | Sidebar placement. `operate` puts the overview under the Operate group (admin-only by convention). | -| `icon` | string | — | Sidebar icon name (from the icon set in `apps/ui/src/assets/icons/`). | +| `icon` | string | — | Sidebar icon name (from Horizon's icon set). | | `order` | number | — | Sort order within the visibility bucket (lower = earlier). | | `layers` | string[] | — | Layer enums this overview aggregates. Optional — used as a hint by the sidebar and by widgets that want a default layer for MQE evaluation. | | `widgets` | array | **required** | Ordered widget list. The renderer iterates and lays out per the grid model. | @@ -58,7 +58,7 @@ See [Components → Overview Widgets](../components/overview-widgets.md) for the ## Grid model -The renderer (`apps/ui/src/render/overview/OverviewDashboardView.vue`) uses a CSS grid: +The overview renders on a CSS grid: - Per-section column count, default 12, set by the most recent `section-break.cols`. - Fixed row height 72 px. diff --git a/docs/design-target.md b/docs/design-target.md index 4b43fc9..48be514 100644 --- a/docs/design-target.md +++ b/docs/design-target.md @@ -32,13 +32,13 @@ See [Components → Overview Widgets](components/overview-widgets.md) and [Dashb ### War-room overview support -The overview perspective is a first-class concept. An overview template (`apps/bff/src/bundled_templates/overviews/*.json`) is an ordered list of widgets laid out on a 12-column grid with per-section column overrides. Bundled examples include `services.json` (cross-layer service health + Kubernetes capacity) and `mesh.json` (Istio data-plane services + pilot activity). +The overview perspective is a first-class concept. An overview template is an ordered list of widgets laid out on a 12-column grid with per-section column overrides. Bundled examples include `services.json` (cross-layer service health + Kubernetes capacity) and `mesh.json` (Istio data-plane services + pilot activity). Overviews are scoped per-layer (each widget declares its `layer` key, allowing MQE evaluation against the right entity scope), or layer-agnostic for cross-cutting summaries. ### Integrated trace, log, metric, profiling -The per-layer drill-down (`layer/<key>/` route family) presents a single service through every data type SkyWalking captures: +The per-layer drill-down presents a single service through every data type SkyWalking captures: | Tab | Scope | Data source | |---|---|---| @@ -54,7 +54,7 @@ The renderer is template-driven (see [Customization → Layer Dashboard Template Every visual decision a site operator wants to make is template-driven: -- **Sidebar layer order** — `useLandingOrder` reads per-user priority from the setup store. +- **Sidebar layer order** — per-user layer priority, persisted in the setup state file. - **Layer alias / color / group / visibility** — layer template fields (`alias`, `color`, `group`, `visibility`). - **Which tabs appear on a layer** — `components` flags on the layer template. - **What appears under each tab** — `dashboards.<scope>` widget arrays. diff --git a/docs/operate/cluster-metadata.md b/docs/operate/cluster-metadata.md index 781578e..edffd08 100644 --- a/docs/operate/cluster-metadata.md +++ b/docs/operate/cluster-metadata.md @@ -54,7 +54,7 @@ Two **independent** panes, refreshed in parallel. ## Cluster members -- **Source:** `GET <queryUrl>/status/cluster/nodes` (status client, `packages/api-client/src/status.ts`). +- **Source:** `GET <queryUrl>/status/cluster/nodes`. - **Returns:** per-node host, port, role, last heartbeat. - **Refresh:** Same cadence as the Query pane. diff --git a/docs/operate/inspect.md b/docs/operate/inspect.md index 670092c..0f843f1 100644 --- a/docs/operate/inspect.md +++ b/docs/operate/inspect.md @@ -88,22 +88,6 @@ Horizon converts the page's chosen time range into the correct format automatica OAP does not expose a direct "metrics in layer X" filter. Workaround: most layers have a metric-name prefix (`service_*` for GENERAL, `mesh_service_*` for MESH, `k8s_*` for K8S). Filter the regex by that prefix. -## Data path - -``` -Browser - ↓ GET /api/inspect/metrics?regex=... -BFF (apps/bff/src/http/admin/inspect.ts) - ↓ GET <adminUrl>/inspect/metrics?regex=... -OAP :17128 - ↓ inspect module returns catalog rows -BFF wraps as typed response - ↓ -Browser renders virtualized list -``` - -The BFF is a thin proxy — caching is per-request, not cross-request. The Inspect API is fast enough that this is rarely a bottleneck. - ## Limits and caveats - **404 from OAP** means the `inspect` module is off. Set `SW_INSPECT=default` on OAP and restart it. The Cluster Status page will then show the module green. diff --git a/docs/setup/files.md b/docs/setup/files.md index 5977eed..b6374fa 100644 --- a/docs/setup/files.md +++ b/docs/setup/files.md @@ -22,7 +22,7 @@ Holds: - Layer-level setup state (which layers the user has marked as enabled / disabled in their sidebar). - Other persistent UI preferences that survive sessions. -Read and written by `apps/bff/src/logic/setup/store.ts`. The UI writes via `POST /api/setup`; the file is updated atomically (write-temp-then-rename). +The UI writes this file as users change their landing order and layer enablement; the file is updated atomically (write-temp-then-rename). ## `alarms.file` @@ -30,13 +30,13 @@ Read and written by `apps/bff/src/logic/setup/store.ts`. The UI writes via `POST |---|---|---|---|---| | `alarms.file` | string | `./horizon-alarms.json` | no | Filesystem path to the alarm rules state. | -Holds user-created alarm rules (in addition to whatever the OAP cluster ships bundled). Read and written by `apps/bff/src/logic/alarms/store.ts`. The Alarm Rule Editor (Operate → Alarm Rules) writes here. +Holds user-created alarm rules (in addition to whatever the OAP cluster ships bundled). The Alarm Rule Editor (Operate → Alarm Rules) writes here. ## Env-var fallbacks -When `horizon.yaml` does not supply a `setup.file` or `alarms.file` (or `audit.file` / `debugLog.file`), the config schema seeds its default from an env var: +When `horizon.yaml` does not supply a `setup.file` or `alarms.file` (or `audit.file` / `debugLog.file`), the default is seeded from an env var: -| YAML key | Env-var fallback | Schema baseline | +| YAML key | Env-var fallback | Default | |---|---|---| | `setup.file` | `HORIZON_SETUP_FILE` | `./horizon-setup.json` | | `alarms.file` | `HORIZON_ALARMS_FILE` | `./horizon-alarms.json` | diff --git a/docs/setup/horizon-yaml.md b/docs/setup/horizon-yaml.md index 1b72e3f..c2f8840 100644 --- a/docs/setup/horizon-yaml.md +++ b/docs/setup/horizon-yaml.md @@ -1,6 +1,6 @@ # horizon.yaml Reference -`horizon.yaml` is the single configuration file for the Horizon BFF. The schema is enforced by Zod (`apps/bff/src/config/schema.ts`); validation runs at startup and again on every hot reload. A file that fails validation is **rejected**; the BFF keeps the previously valid config rather than serving with broken settings. +`horizon.yaml` is the single configuration file for the Horizon BFF. Validation runs at startup and again on every hot reload. A file that fails validation is **rejected**; the BFF keeps the previously valid config rather than serving with broken settings. This page is the top-level map. Each subsection has its own detail page: @@ -81,7 +81,7 @@ There is no "default admin/admin" fallback. ## Hot reload behavior -The watcher (`apps/bff/src/config/loader.ts`) re-parses on file change. Listeners registered via `config.onChange()` get the new values: +The config is re-read on file change and the new values take effect without a restart: - Auth backend selection (re-evaluated on next login). - RBAC roles and policy (re-evaluated on next route call). diff --git a/docs/setup/oap.md b/docs/setup/oap.md index c0a605d..c12c500 100644 --- a/docs/setup/oap.md +++ b/docs/setup/oap.md @@ -17,7 +17,7 @@ oap: | Field | Type | Default | Required | Notes | |---|---|---|---|---| -| `queryUrl` | URL string | `http://127.0.0.1:12800` | no | OAP GraphQL query endpoint. Load-balanceable — any OAP node answers. Used by all read pages. Validated via Zod `.url()`. | +| `queryUrl` | URL string | `http://127.0.0.1:12800` | no | OAP GraphQL query endpoint. Load-balanceable — any OAP node answers. Used by all read pages. Must be a valid URL. | | `adminUrl` | URL string | `http://127.0.0.1:17128` | no | OAP admin REST endpoint. Hosts runtime-rule, dsl-debugging, inspect, status, debugging/config endpoints. Single URL; OAP handles cluster-internal fan-out. | | `zipkinUrl` | URL string | `http://127.0.0.1:9412/zipkin` | no | Zipkin v2 REST endpoint. Used when a layer's `traces.source` is `zipkin` or `both`. Defaults assume the standalone Armeria binding; for Docker / shared-port deployments use `<queryUrl>/zipkin`. | | `timeoutMs` | number | `15000` | no | Per-request HTTP timeout (milliseconds) for all OAP calls. Applies to query, admin, Zipkin. Must be positive integer. | @@ -67,7 +67,7 @@ The cache is per-process. After a BFF restart, the next request re-probes. ## Hot reload -Changes to any `oap.*` field are picked up by listeners on the config watcher. The next outbound call uses the new value. **Exception**: capability cache is process-lifetime — flipping a feature on OAP that requires re-introspection needs a BFF restart. +Changes to any `oap.*` field are picked up on file change. The next outbound call uses the new value. **Exception**: capability cache is process-lifetime — flipping a feature on OAP that requires re-introspection needs a BFF restart. ## Common mistakes diff --git a/docs/setup/server.md b/docs/setup/server.md index 6781fd9..5023aa6 100644 --- a/docs/setup/server.md +++ b/docs/setup/server.md @@ -15,7 +15,7 @@ server: |---|---|---|---|---| | `host` | string | `127.0.0.1` | no | Interface to bind. Set `0.0.0.0` to listen on all interfaces (production behind TLS terminator). | | `port` | number | `8081` | no | TCP port. Must be a positive integer. | -| `staticDir` | string | — | no | Filesystem path to a directory of pre-built UI assets (typically `apps/ui/dist`). When set and the directory exists, the BFF serves files from this directory with SPA-style fallback: any 404 returns `index.html` so client-side routing works. When unset, the BFF only serves API routes (`/api/*`) — useful for running the UI dev server separately. | +| `staticDir` | string | — | no | Filesystem path to a directory of pre-built UI assets. When set and the directory exists, the BFF serves files from this directory with SPA-style fallback: any 404 returns `index.html` so client-side routing works. When unset, the BFF only serves API routes (`/api/*`) — useful for running the UI dev server separately. | ## Common shapes @@ -45,7 +45,3 @@ Browser hits a TLS terminator → BFF on port 8081. The BFF serves UI bundles an ### Behind a path prefix There is currently **no built-in base-path / prefix support**. If you need Horizon under `/horizon/` rather than `/`, terminate the prefix at your reverse proxy and rewrite paths there. The UI assumes it is served from the root. - -## Consumers - -- `apps/bff/src/server.ts` — binds Fastify, mounts static serving when `staticDir` is set.
