0.6.0
This release is the production-readiness pass for Horizon UI: every page now renders correctly across the eight supported languages on non-UTC OAP deployments, with deliberate caps and validation on the load surfaces that operators reach. The pillars below describe the operator-visible result.
Eight-locale internationalization
Horizon now ships with eight first-class UI languages — English (source) plus zh-CN, ja, ko, es, pt, de, fr — selectable from the top-bar locale chip on every page (including the pre-auth login). The choice persists per device.
- UI chrome. Every routed page and every shared sub-component renders through
vue-i18n; non-English locales now cover every admin page (Roles, Users, Auth status, Alert page setup, Global defaults, 3D-map config), every operate page (Alerting rules, DSL catalog / editor / dump, OAL catalog, Live debugger + MAL / LAL / OAL, Capture history, Metrics inspect, OAP config, TTL), the alarms surface, and the shared modals. Long lede paragraphs that previously rendered asEnglish | one translated word | Englishmid-sentence are now single translation units — inline<code>and links interpolate without splitting the prose. Missing leaves still fall back to English so partial catalogs degrade invisibly. - BFF-shipped templates. All 42 layer dashboards and both overview dashboards carry per-locale overlay catalogs alongside the source template. Coverage is ~2,300 translatable leaves per non-English locale across the layer set. The BFF picks the locale from the request’s
X-Horizon-Localeheader (auto-set by the SPA), merges the overlay onto the source, and serves the localised template to the renderer — translation resolves once on the BFF, never on every chart mount. - Operator-runnable Translations page. A new admin surface (Dashboard setup → Translations) edits the per-locale overlays through the live preview: pick a target language, click any widget in the rendered dashboard, type the translation. Per-locale status chips on the template picker show at a glance which dashboards have drafts, which are synced, which diverge from disk, and which are empty for a given locale. Push writes the sibling overlay row on OAP; pushing zh-CN never touches ja.
- Tech-term policy. Product, project, and protocol names (SkyWalking, Kubernetes, OAP, MQE, eBPF, Zipkin, OpenTelemetry, Istio, GraphQL, etc.), OAP scope enums (Service, ServiceInstance, Endpoint, Process), layer keys, MQE function names, env vars, HTTP status codes, and per-language runtimes (JVM, Go, Python, …) stay verbatim in every locale per CLAUDE.md. Phrases containing tech terms are translated around the term (
HTTP Connections→HTTP 连接/HTTP 接続), not transliterated. - OAP-supplied data is never translated. Service names, alarm rule names, trace span operation names, log messages — anything arriving over the OAP wire — render verbatim regardless of locale.
- Validator gate.
i18n:validateis stricter: every source template must have a sibling overlay file per advertised locale, and empty{}overlays are now a finding (used to pass silently — surfaced as “structurally complete” while every translatable string still rendered in English).
Typography + self-hosted fonts
- Inter + JetBrains Mono are now self-hosted. The Google Fonts CDN dependency is gone — air-gapped or firewalled deployments render the intended typography instead of silently falling back to system fonts.
- One typescale across every page. Older admin pages that drifted to a mixed pixel palette (9.5 / 10 / 10.5 / 11 / 11.5 / 12 / 14 / 18 / 20 / 22) now share the same six-step scale + uppercase-label vocabulary as the newer dashboards. Sidebar, kpi labels, table headers, kickers all line up.
Wire-correctness on non-UTC OAP
Every BFF query route now spells Duration.start / end in the OAP server’s timezone (probed once per minute, cached). Previously only the alarms route did this; dashboards / landing / topology / endpoint / endpoint-dependency / instance / eBPF / traces / logs / trace-tag all emitted UTC, which silently shifted every query on non-UTC OAP installs by the server’s offset.
Traces and logs additionally query at SECOND precision now (records, not metric buckets) — a trace that just finished falls inside the window instead of getting rounded off the MINUTE boundary.
Performance hardening
- Landing batches no longer 5xx on wide layers. The per-layer landing route used to build one GraphQL with up to 250 aliased fragments (25 services × 10 metric columns) and trip OAP’s per-request complexity ceiling, blanking every cell. Chunks at 6 services per round-trip and fires them in parallel — same pattern the dashboard route already uses.
- Trace waterfall opens fast on huge traces. Rows render lazily via the browser’s
content-visibilitywindow — a 5000-span trace no longer freezes the main thread on open. - Backgrounded tabs stop polling. The shared auto-refresh ticker pauses when the tab is hidden and resumes (with one immediate tick) on return. An unattended browser no longer streams queries at the topbar interval × every subscribed widget.
RBAC + input-validation hardening
/api/healthno longer leaks the active session count to unauthenticated callers — the public liveness probe returns only status + version. The authenticated/api/auth/healthsurface still carries detail.pageSizecapped server-side on every trace / log route (trace 200, log 100). OAP forwardspaging.pageSizestraight to the storageLIMIT, so a client postingpageSize: 50000previously cascaded the load to OAP. The UI picker’s matching cap is now defended at the BFF boundary too.- Profiling task bodies validated. Async-profiler, pprof, eBPF fixed-task, and network-profiling create routes now sanitize and bound their bodies — duration caps, target-instance and event-list caps, payload-size clamps. Closes a DoS vector where a user with
profile:enablecould submit a multi-hour profile that pegs the target instance’s CPU.
Diff modal console error fixed
The four admin “Check diff & push” modals (Layer dashboards, Overview templates, Translations, the shared TemplateDiffModal) used to log Uncaught (in promise) Error: Missing requestHandler or method: resetSchema (two per modal — one per diff pane) the moment the operator opened them. Monaco’s JSON language service was sending the message to a worker that didn’t know how to handle it. Now wired correctly — the diff itself rendered all along, but the console is quiet again.
DSL / OAL catalog renames + admin editor seeding
- Catalog pages spell out the language name in the header:
Metrics Analysis Language - OpenTelemetry Rules(and… - Telegraf Rules/… - Log MAL Rules); LAL renders asLog Analysis Language; the OAL browse asObservability Analysis Language. The sidebar keeps the abbreviated MAL · OTEL form for space. - Layer Dashboards + Overview Templates editor opens REMOTE on diverged rows. The runtime menu already renders remote content when the row is diverged, but the editor used to seed from bundled — so operators were editing a copy that silently disagreed with what end users saw. Priority is now local → remote (diverged / remote-only) → bundled; the source pill on both editors reads
from local / from remote / from bundledconsistently. - Rule card arrow stays next to the rule name. Short names like
default/mesh-dp/vmused to float in the middle of the card with a big gap from the ▶ run arrow — the arrow + name are now grouped on the left, with the status pill alone pushed right.
Sync-status counters don’t include translation rows
Per-locale overlay rows on OAP share their parent template’s kind, so they were inflating the remote-only and diverged counts on the Overview Templates and Layer Dashboards admin pages (the banners would read 14 remote-only / 294 remote-only when most of those entries were translation rows that belong on the Translations page only). Filtered at bundle assembly so the sync-status banners count source rows only.
Cluster Status + admin polish
- Cluster Status — Pane B + Pane C fully translatable. Module column headers, gate descriptions (the SWIP-13 affects strings), per-row enabled/missing badges, the admin-host status badge (
loading…/unreachable/all selectors on/{n} selectors off), the admin-host-unreachable hint, the Zipkin / OTLP pane lede, the Endpoint card heading, the Zipkin badge and the Zipkin-unreachable hint all now render in the active locale. - Hide redundant BUNDLED chrome when synced. On the Overview templates and Layer dashboards admin pages, when the row’s sync badge is
syncedthe BUNDLED and REMOTE versions are byte-equal, so thefrom {source}pill and theReset to / Preview Bundleddropdown items add no information and are hidden. LOCAL drafts always show; BUNDLED resurfaces the moment the row diverges. - Translations picker prefers the English bundle. REMOTE-only rows with non-English titles (legacy duplicates from prior import cycles) no longer appear as separate dashboards in the picker — the picker lists the canonical English bundled dashboards once each, and the preview renders the English source as the baseline.
Alerting rules — running entities show their OAP node
The Operate › Alerting rules detail pane’s Currently watching list now spans the whole cluster and tags each entity with the OAP node evaluating it. Each OAP instance evaluates a rule independently over the slice of entities it holds, so the watched set is the union across nodes — the page previously showed only the first responding node’s entities, which misread as “these are all the entities the rule watches.” The list now aggregates every instance’s entities and labels each row with its node (e.g. SERVICE agent::app NODE 10.116.3.26_11800), with the per-entity alarm message on hover. The per-node load-state table is unchanged. Single-instance deployments simply show one node label per row.
Clicking a watched entity now opens a running-context popup — the live evaluation window the rule is computing for that entity, per OAP node. It shows the current state (FIRING / SILENCED_FIRING / RECOVERY_OBSERVATION), the window size and silence / recovery countdowns, the window end, the last-alarm time and message, and the per-metric snapshot the expression was evaluated against — rendered as a sparkline plus per-bucket values so an operator can see exactly why a rule is (or isn’t) firing. Nodes not evaluating the entity are marked as such, and a raw-JSON disclosure carries the full payload.
Live debugger fixes
A clutch of small but visible bugs were caught while exercising the i18n surfaces:
- History → debug deep-link rendered blank. MAL and LAL views crashed silently on
?historyId=…becauseloadHistoricalreset refs (selectedRow,expandedEntities,foldedRecords;selectedCellfor LAL) that the file declared further down. Thewatch(historyId, …, { immediate: true })fires during setup, so the TDZ ReferenceError aborted setup before the page rendered, with no console trace. Resettable refs are now hoisted above their consumer. - Captured records wiped on stop. After
stop(), the per-DSL view did one finalsession()refresh against an already-cleaned- up OAP, which returned nodes-only-with-empty-records and overwrote the rich live snapshot in localStorage.save()now refuses to shrink an existing entry’srecordCountfor the samesessionId— only metadata (retentionDeadline,retentionMillis) updates. - Stable node ordering. MAL / LAL / OAL node cards now sort by
nodeId ?? peerso they don’t reshuffle between polls. - Tab buttons jumped nowhere.
LiveDebuggerView.selectTabpushed/debug/<tab>(a path that doesn’t exist); fixed to/operate/live-debug/<tab>. Same correction inDebugHistoryView.loadEntrydeep-links and surrounding doc-comments inRuleCard.vue/DslEditorView.vue. - Empty-capture placeholder. When a saved session has zero populated nodes,
DebugViewnow shows an explicit “This capture has no records” rather than rendering blank, so an honest empty capture is visibly different from a bug. - Per-locale lookup widened. Per-DSL views look up the historyId via
history.all(not the widget-filteredhistory.entries); the route already pinned us to the right widget, and double-filtering bywidgetwas a silent way for a stale field to drop the entry.
Other small fixes
- MAL / LAL editor gutter glyph. The green ▶ live-debug entrance on every
- name:row was referencing an unstyled CSS class — Monaco reserved the gutter and wired clicks, but the icon was invisible. Restored. - Rule catalog cards. Duplicate BUNDLED pill removed; the header status pill already conveyed it.
- K8s instance Node Status. Switched from a single-scalar card (rendering just
1) to a table of currently-true Kubernetes conditions, matching the cluster-scope sibling and aligning widget heights.
Upstream ui-management compatibility
Aligned with upstream skywalking#13884 (“remove auto generate id for UI templates”): Horizon now sends id = <envelope name> on POST /ui-management/templates. Current OAP requires it (POSTs without id are rejected); legacy OAP releases ignored the field and auto-generated UUIDs, so the same payload works against both. Horizon already treated r.id as opaque; mixed-id deployments self-heal via reconcileDuplicates on the next boot.
Smartscape service hierarchy
OAP 10’s cross-layer service hierarchy is now reachable from any layer’s service map — a logical service projected across observation layers (GENERAL agent ↔ MESH sidecar ↔ MESH_DP data-plane ↔ K8S_SERVICE pod) is one click away on every selected hex.
- Lazy-probed chip on the selected hex. Picking a node fires one
getServiceHierarchycall; if the service has cross-layer peers, a small chevron-stack chip clips to the hex’s right edge. No probe, no chip on services with no peers. - Focus + context + suggestions overlay. Click the chip and the topology dims under a transparent canvas; the focused hex re-renders bright at the exact same screen position and scale as the underlying hex (the topology’s d3 zoom transform is mirrored onto the overlay). Peers fan vertically from the focus column using OAP’s
listLayerLevelsorder — higher-level (request-near) layers above, lower-level (infra-near) layers below, matching booster-ui’s hierarchy rendering rule. - Auto-refresh pauses while the overlay is open so the background topology and KPI panels don’t shift under the operator. Closing the overlay (
×button, ESC, or click-on-dim) resumes the ticker and fires one immediate tick so the page snaps back to live data. - Two-step peer open. First click on a peer hex arms it (selection halo + side
↗ Open in <Layer>action chip); second click on the chip opens the destination layer in a new browser tab, pre-selecting the peer service. Peers in layers Horizon has no template for render dimmed with acursor: not-allowed; clicking them logs “No layer template configured for <Layer>” to the event log instead. - URL-pinned service validator on the destination tab. Every per-layer page now validates the URL-hydrated
?service=<id>against the layer’s real service roster (the newGET /api/layer/:key/services, served from the BFF’s 60s catalog cache so it adds no extra OAP traffic). A genuinely missing id pops aService not found in this layermodal with a one-click fallback to the first available service; a valid id is trusted even when landing’s top-N rollup doesn’t sample it (the cause of the previous silent service-swap on low-traffic deep links). - Service-name resolution on the layer dashboard now consults the roster after landing’s top-N, so deep links to low-traffic services no longer sit on “Resolving service…” forever waiting for a row that won’t arrive.
On-demand pod logs (live tail)
A new per-layer Pod Logs tab live-tails a Kubernetes pod’s container logs, pulled on demand from the K8s API through OAP and never persisted.
- Instance-pinned tail. Pick a pod, pick one of its containers, press Start; the trailing window (30s / 1m / 5m / 15m / 30m) streams into a read-only log pane and refreshes on a chosen interval (2s / 5s / 10s / 30s) until paused. A header strip shows the container, line count, a live dot, and “updated Ns ago”.
- Include / Exclude filtering forwards to OAP’s content keyword filters — full-line regex, so a substring match reads
.*error.*. - Enabled on the Kubernetes-deployed layers — Kubernetes Services (
K8S_SERVICE), Istio Managed Services (MESH), and Istio Data Plane (MESH_DP) — whose service instances resolve to a pod. The tab is gated by a newpodLogscomponent flag added to those bundled layer templates; an existing OAP whose stored template predates the flag still gets the tab, because the flag is back-filled from the bundled default (no re-push needed). - The page owns its own refresh — the global auto-refresh ticker and the topbar time picker are paused while on it, the same as Traces / Logs.
- When the selected instance carries no pod metadata (or the pod has rotated away), OAP’s reason is shown verbatim with a hint to pick a currently-running pod or enable the feature on OAP.
BanyanDB cold-stage query
The cold lifecycle stage is now reachable from the UI on BanyanDB deployments — operators can query data that has aged past the hot + warm window without leaving the page.
- Topbar
Coldpill appears only when the connected OAP is BanyanDB. Toggling it switches every page to read from the cold stage instead of hot + warm — it replaces the read, it does NOT union the two stages, so the pill label flips toCold onlywhile on. The choice is sticky per browser and re-runs every visible query so what you see matches the new mode immediately. - Cold-trap banner. When the pill is on AND the current time range is within the hot + warm window, a yellow strip appears under the topbar: “Cold-only read is active — your time range is within the last N d (hot + warm), where the cold stage returns nothing.” A one-click Turn Cold off sits on the right.
- Trace lookup from a log row now passes the row’s timestamp through the popout so the trace lookup spans a window around that timestamp instead of OAP’s default last-1-day search — paired with the Cold pill, a trace that lives in cold resolves from a cold-era log row instead of silently failing to load.
Data retention page
- Per-data-class data lifecycle bar. One row per data class (Normal / Trace / Zipkin trace / Log / Browser error log / Metadata / Minute metric / Hour metric / Day metric) with proportional Hot+Warm and (when configured) Cold segments. Widths are proportional to total retention across all rows, so a class retained longer visibly stretches further than its peers.
- When every class in a category shares the same TTL pair, the rows collapse to
All records (5)/All metrics (4)— the page never renders nine identical bars. - The page branches sharply by backend. BanyanDB shows the full lifecycle bar + stage vocabulary; on any other backend, the page renders a single
Retentionpane with per-class values and skips the stage vocabulary entirely. - A footer note names the wire-level truth: OAP’s TTL response collapses hot + warm into one number per class, BanyanDB migrates between stages in segments so records near a boundary may briefly exist in both, and property data is omitted (forever-retained, no TTL reported).
Time picker
- Custom range seeds from the last applied range when you re-open the picker, instead of resetting to “half the max ending now”. Reopening also auto-expands the Custom form on the matching precision tab when the current range is custom.
- Locale-bleed fix on the alarms page custom-range stamp and the log row date column (was rendering
5月08日on zh-CN browsers; now uniformMM-DD).
Overview widgets follow the global time picker
- The Services Dashboard (and any overview using metric / KPI / table widgets) now honors the topbar time picker. Previously the per-layer landing and topology routes were hardcoded to the last 60 minutes, so picking 12 days back kept showing recent numbers; now picker + Cold pill flow end-to-end. The layer dashboard’s header KPIs follow the picker too (was showing live numbers while the body honored the picker).
- The Active alarms widget title now shows the actual window (e.g.
· last 10m) and the empty-state copy uses the same value instead of a hardcoded “last 60m”.
Polish
- Sentence-case fixes on a couple of leftover Title-Case labels (
DSL management,Metrics inspect) so the menu and roles tables read consistently; acronyms (DSL / OAP / MAL / LAL / OAL) stay uppercase. - Public-demo (
demo.skywalking.apache.org) references removed from setup docs — the demo doesn’t accept anonymous traffic.
Dashboard authoring
The Layer dashboards and Overview templates admin pages now share one editing model where your work-in-progress lives in your browser, and the live, shared version is whatever OAP serves.
- Edits are local to your browser. “Save (local)” stores your draft in this browser only — it is never written to the server and nobody else sees it. Everyone (including you, in normal viewing) keeps seeing the published remote dashboard until you publish.
- Load any source into the editor. A single Reset to ▾ control loads the Bundled (shipped default) or Remote (OAP live) version into the editor; editing from there becomes your local draft.
- Preview any source on the real page. A Preview ▾ control opens the actual layer / overview page in a new tab rendering your Local draft, the Bundled default, or Remote — via
?mode=preview&source=…, which stays in the URL and propagates as you navigate the menu. A banner names what you’re previewing (dismiss with × or Esc). - Publish with a diff. Check diff & push shows a side-by-side local→remote diff and publishes to OAP; it’s enabled only when your local draft actually differs from remote. Bundled can also be pushed straight to OAP. Resetting to remote clears the local draft. Reset to bundled then publishes correctly when the bundled default differs from remote — Save (local) and Check diff & push now compare the editor against remote, not just against what was first loaded, so a bundled-vs-remote divergence is no longer mistaken for “no changes” (layer + overview editors).
- Preview faithfully reflects your draft’s enabled components / menu labels — disabled tabs disappear and renamed nouns (“Nodes”, “Topics”) show through — without pushing anything to the server. Preview works even for layers OAP currently reports no services for.
- An editors-only reminder lists any unpublished local drafts with quick links to the relevant edit page (no more “use local vs remote” prompt — remote is always the live source).
- Create mirrors edit. “+ New dashboard” writes a local draft (the id is the template name, checked unique) — edit and preview it, then Check diff & push publishes it. A pushed dashboard with no bundled default is remote-only and now renders everywhere (live page + sidebar), not just in the editor.
- Delete = soft-disable (OAP has no hard delete). A local-only draft is removed from the browser; a dashboard on OAP is disabled — dropped from the picker’s live state, the sidebar, and the live page. A disabled status chip shows it. Confirmations are styled in-app dialogs, not the browser’s native box.
Layer dashboards editor
- The layer picker is a single filterable dropdown showing alias + key + sync status. A live menu preview sits beside the Alias / Components / Menu labels (per-layer slot aliases) editor; clicking a menu item jumps to that component’s config. Scope tabs and section headings read in the layer’s own vocabulary.
- The service-list metrics editor gains a sample-data preview (plus a faithful landing KPI tile preview that reuses the real header components), the column remove-button alignment is fixed, and the landing KPI tile config makes clear it just picks which existing column feeds the headline + sparkline.
- The picker’s Diverged / Local filters now sit inside the dropdown (next to the search), and the editor header reads on one line —
Layer: <name> <key> <status>— showing the same sync-status chip the picker does. - Disable / Reactivate a layer. Disabling soft-disables the layer on OAP and drops it from the sidebar (the menu honors disabled templates); a disabled layer offers Reactivate, which re-enables it from the bundled default (the OAP update path clears the disabled flag).
Overview templates editor
- Rebuilt as a layer-style canvas: a 12-column grid you drag to reorder and corner-drag to resize, click-to-edit in a right drawer that appears on selection (Esc / deselect hides it), with section-breaks and the dashboard title selectable and unselected widgets hinted by a dashed outline. The canvas mirrors the live grid (fixed row height), so the layout matches the real page — including side-by-side widgets like topology + alarms.
- The composite-metrics KPI editor stacks each row as a card (label / source / MQE / unit / aggr / style / max) so nothing truncates in the narrow drawer.
Navigation & shell
- The main sidebar folds to a narrow rail to reclaim width; the logo moves into the topbar while folded (the original wordmark is unchanged).
- The active menu item now reliably expands, highlights, and scrolls into view on entering a page (route matching is case-insensitive). Fixed a regression where the sidebar scrolled to the very bottom on every navigation (the “Debug events” toggle’s active state was being treated as the scroll target).
- Overview dashboards appear in the sidebar only when their layers are reporting services. Visibility is derived from each dashboard’s widgets (their
layerfield) ∪ the explicitlayers[]list, gated against the liveavailableLayers. A dashboard you create via “+ New” inherits this automatically — no need to maintain thelayers[]field by hand. Polls on the 60s menu cadence + window focus, so entries appear / disappear as services start and stop reporting. - Smarter landing. Root
/cascades through a sensible chain so the user never sees a blank page: first available public overview → first layer with services → the empty landing (/landing-empty). The cascade only lands on destinations that are also in the sidebar — a bundled-but-inactive layer (no services yet) is deliberately not a fallback, since it would put the user on a page they can’t navigate back to via the menu. The empty page is also a real bookmarkable route, with two distinct copies — “No data is flowing yet” (no agents/receivers reporting) vs “No dashboard configured yet” (services exist but no overview is set up) — each with the right operations-team handoff and no action buttons (a viewer’s role doesn’t include the verbs the old buttons jumped to). - Debug events panel now defaults OFF on every host (was on for localhost). Same baseline for operators and developers so reproductions match what operators see.
- Zipkin trace mode drops the per-layer service-KPI header — the Zipkin explorer is a self-contained, cross-service view.
3D Infrastructure Map
A standalone, bird’s-eye view of the deployment at /3d/map: services render as cubes on stacked tier-planes (apps · service mesh · middleware · infra), each tier subdivided into per-layer zones with the layer’s brand mark stamped on its colored swatch. Drag to rotate, scroll to zoom, arrow keys / WASD to pan; click a cube for its detail card and a link into that layer’s dashboard.
- Live data windows. The map auto-refreshes every minute — per-cube traffic rolls up the last 2h of metrics (HOUR step) and alarmed services light up from the last 20m of alarms. A toolbar chip shows the active scopes (
metrics 2h · alarms 20m · ↻ 1m). An alarmed cube burns red with a radiating ripple, matched to its service by (layer, name) so only the firing service in the right tier is flagged. - Live topology. The deployment structure is read live from OAP rather than a bundled snapshot: each layer’s service roster and service map are fetched one at a time (low concurrency) and assembled into the scene, so the map is correct on any deployment. It refreshes on the same one-minute cycle — an unchanged structure updates metrics/alarms in place without disturbing the camera, while a service appearing or disappearing rebuilds the affected tier. The load progresses stage by stage in the status strip.
- Beacon mode. A toolbar toggle dims every healthy cube to a wireframe ghost and lets only alarming cubes glow, so the services that are firing jump out instantly during an incident.
- Logic groups. Related layers can be clustered into a single labelled block on a tier — the bundled config ships a Self-Observability group (OAP, Satellite, BanyanDB, and the Java / Go agents) on the middleware tier. Members keep their own cube colors but read as one block on the map.
- Configurable tiers + layers. Tier order, per-layer plane mapping, cube colors, the traffic MQE per layer, and the logic groups are all driven by the 3D map config, edited on a structured admin page at
/admin/3d-map. Pin each layer to a tier (with a single global layer filter as the top-level gate), edit each layer’s color + traffic metric, manage logic groups (members, color, icon, tier), and choose the single failover tier for anything unpinned. The config is published to OAP and shared across the deployment the same way as dashboards: edits save to a local draft in your browser, then Check diff & push publishes to OAP — the map renders the remote, with the bundled defaults as fallback. - Topology clustering. Within layers that carry a service map, services group into named clusters drawn as a wireframe frame with the cluster name baked into the frame’s lower-left corner — service-mesh services cluster by their showcase group, Kubernetes services by namespace. Clustering follows each layer’s naming rule, so layers without one keep rendering flat.
- Navigate by tier. The right-side Tiers panel is a two-level tree (tier → layer / logic-group). Clicking any entry resets the view, glides the camera to face that region, zooms in, and flashes the region for a few seconds so it is easy to spot. A Reset button in the panel header restores the initial framing.
- Hover = preview. Hovering a cube shows the same detail card as a click — tier, layer, service, and (when present) group and cluster — anchored at the cube, minus the open-in-dashboard link.
- Call-direction flow. Call relationships animate directional particles so the direction of traffic reads at a glance.
- Focus one layer. The per-layer service map gains a View in 3D link that opens the map focused on just that layer.
- Refresh countdown. The load status strip shows a live countdown to the next refresh, anchored to the stage that will run next.
Reliability
- When the BFF is unreachable the UI now shows a clear “Cannot reach the server” message instead of the cryptic “body stream already read” — the API client reads each error response body once and surfaces the real status/text (or a wrapped network error).
- Server-global service-by-layer catalog. One singleton on the BFF (60s TTL + single-flight) now owns the
listLayers+ aliasedlistServices(layer)fan-out. The sidebar menu’s per-layer counts and the alarms layer-tagger share this one cache instead of each running their own poll, so OAP sees at most one fan-out per minute regardless of how many routes are polling — and the two views can no longer drift by 60s relative to each other. - The brand link and the post-login redirect no longer resolve to an empty address — both now land on the operator’s actual landing route.
- Trace span detail now labels the span direction as Kind (the noun) rather than the mistranslated verb form, and long tag keys wrap inside the panel instead of overflowing their column.