You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by wu...@apache.org on 2026/05/14 09:38:03 UTC

(skywalking-horizon-ui) 03/04: layers: migrate 43 dashboards from booster + ship image pipeline

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 f6137b6ceb800413c0a107168f0ce074003073b6
Author: Wu Sheng <wu...@foxmail.com>
AuthorDate: Thu May 14 16:38:01 2026 +0800

    layers: migrate 43 dashboards from booster + ship image pipeline
    
    - Migrate all 43 OAP layers' dashboards from booster-ui templates:
      general / mesh / k8s / k8s_service / virtual_{database,cache,mq}
      + 37 new (browser / ios / mini-programs, gateways, databases,
      queues, so11y_*, AWS, OS, banyandb / envoy_ai_gateway / cilium /
      mesh_cp / mesh_dp / flink / virtual_genai).
    - Move layer + overview config out of feature dirs and into one root
      bundled_templates/{layers,overviews}/ — both loaders updated.
    - Add .claude/skills/migrate-layer SKILL.md: booster → horizon widget
      mapping, scope rules (SERVICE_INSTANCE-in-service dashboard must
      use top_n; LABELED vs REGULAR aggregation), P95-over-P99 rule,
      OAL/MAL fallback path when demo has no services for a layer.
    - Validator helper /tmp/validate-layer.mjs (auto-picks a service with
      data on demo OAP and reports OK / EMPTY / UNKNOWN / ERROR per
      expression). Caught real bugs in mesh / k8s_service / so11y_oap /
      virtual_mq during this migration.
    - Sidebar: service-count chip moved to left-aligned 34px fixed-width
      prefix so every layer row's name starts at the same column even
      when counts swing wildly (1 ↔ 1000+).
    - Dockerfile + .github/workflows/publish-image.yaml: multi-stage
      build, multi-arch (amd64+arm64), publishes to GHCR on push to main
      and v* tags. Canonical tag is the full 40-char commit SHA; moving
      tags (:main, :vX.Y.Z, :latest on release) resolve to the same image.
    - BFF: serve the SPA from HORIZON_STATIC_DIR (Docker layout puts the
      Vite dist at /app/static); no-op in local dev where Vite owns the
      SPA on :9091.
    - Also folds in trace / log / overview routes + the topbar
      auto-refresh cluster + trace popout work that had accumulated.
---
 .claude/skills/migrate-layer/SKILL.md              |  263 ++
 .github/workflows/publish-image.yaml               |  105 +
 Dockerfile                                         |   69 +
 .../bff/src/bundled_templates/layers/activemq.json |   59 +
 .../layers/alipay_mini_program.json                |  179 ++
 apps/bff/src/bundled_templates/layers/apisix.json  |   67 +
 .../src/bundled_templates/layers/aws_dynamodb.json |   57 +
 apps/bff/src/bundled_templates/layers/aws_eks.json |   46 +
 .../src/bundled_templates/layers/aws_gateway.json  |   69 +
 apps/bff/src/bundled_templates/layers/aws_s3.json  |   40 +
 .../bff/src/bundled_templates/layers/banyandb.json |   69 +
 .../src/bundled_templates/layers/bookkeeper.json   |   45 +
 apps/bff/src/bundled_templates/layers/browser.json |  244 ++
 .../bundled_templates/layers/cilium_service.json   |   73 +
 .../src/bundled_templates/layers/clickhouse.json   |   60 +
 .../bundled_templates/layers/elasticsearch.json    |   59 +
 .../bundled_templates/layers/envoy_ai_gateway.json |   61 +
 apps/bff/src/bundled_templates/layers/flink.json   |   67 +
 .../layers}/general.json                           |   77 +-
 apps/bff/src/bundled_templates/layers/ios.json     |  254 ++
 apps/bff/src/bundled_templates/layers/k8s.json     |  266 ++
 .../src/bundled_templates/layers/k8s_service.json  |  404 +++
 apps/bff/src/bundled_templates/layers/kafka.json   |   54 +
 apps/bff/src/bundled_templates/layers/kong.json    |   81 +
 apps/bff/src/bundled_templates/layers/mesh.json    |  455 ++++
 apps/bff/src/bundled_templates/layers/mesh_cp.json |   40 +
 apps/bff/src/bundled_templates/layers/mesh_dp.json |   38 +
 apps/bff/src/bundled_templates/layers/mongodb.json |   62 +
 apps/bff/src/bundled_templates/layers/mysql.json   |  228 ++
 apps/bff/src/bundled_templates/layers/nginx.json   |   76 +
 .../bff/src/bundled_templates/layers/os_linux.json |   38 +
 .../src/bundled_templates/layers/os_windows.json   |   33 +
 .../src/bundled_templates/layers/postgresql.json   |  274 ++
 apps/bff/src/bundled_templates/layers/pulsar.json  |   50 +
 .../bff/src/bundled_templates/layers/rabbitmq.json |   52 +
 apps/bff/src/bundled_templates/layers/redis.json   |   55 +
 .../bff/src/bundled_templates/layers/rocketmq.json |   54 +
 .../bundled_templates/layers/so11y_go_agent.json   |   34 +
 .../bundled_templates/layers/so11y_java_agent.json |   34 +
 .../src/bundled_templates/layers/so11y_oap.json    |   55 +
 .../bundled_templates/layers/so11y_satellite.json  |   35 +
 .../bundled_templates/layers/virtual_cache.json    |  184 ++
 .../bundled_templates/layers/virtual_database.json |  101 +
 .../bundled_templates/layers/virtual_genai.json    |   49 +
 .../src/bundled_templates/layers/virtual_mq.json   |  168 ++
 .../layers/wechat_mini_program.json                |  234 ++
 .../src/bundled_templates/overviews/general.json   |   82 +
 apps/bff/src/bundled_templates/overviews/mesh.json |   82 +
 apps/bff/src/config/schema.ts                      |   11 +
 apps/bff/src/dashboard/defaults.ts                 |    2 +-
 apps/bff/src/dashboard/routes.ts                   |    8 +-
 apps/bff/src/layers/config/k8s_service.json        |  109 -
 apps/bff/src/layers/config/mesh.json               |  125 -
 apps/bff/src/layers/config/virtual_cache.json      |   75 -
 apps/bff/src/layers/config/virtual_database.json   |  102 -
 apps/bff/src/layers/config/virtual_mq.json         |   74 -
 apps/bff/src/layers/loader.ts                      |   28 +-
 apps/bff/src/oap/clients.ts                        |   30 +-
 apps/bff/src/oap/endpoint-dependency-routes.ts     |   48 +-
 apps/bff/src/oap/endpoint-routes.ts                |    8 +-
 apps/bff/src/oap/graphql-client.ts                 |   38 +-
 apps/bff/src/oap/info-routes.ts                    |    7 +-
 apps/bff/src/oap/instance-routes.ts                |    8 +-
 apps/bff/src/oap/landing-routes.ts                 |    8 +-
 apps/bff/src/oap/log-routes.ts                     |  313 +++
 apps/bff/src/oap/menu-routes.ts                    |    6 +-
 apps/bff/src/oap/server-time.ts                    |   11 +-
 apps/bff/src/oap/topology-routes.ts                |    8 +-
 apps/bff/src/oap/trace-protocol-cache.ts           |   89 +
 apps/bff/src/oap/trace-routes.ts                   |  472 ++++
 apps/bff/src/oap/trace-tag-routes.ts               |  116 +
 apps/bff/src/oap/zipkin-client.ts                  |  166 ++
 apps/bff/src/overview/loader.ts                    |  120 +
 apps/bff/src/overview/routes.ts                    |   88 +
 apps/bff/src/server.ts                             |   31 +
 apps/ui/src/api/client.ts                          |  126 +
 apps/ui/src/components/charts/Sparkline.vue        |  126 +-
 apps/ui/src/components/shell/AppShell.vue          |    4 +
 apps/ui/src/components/shell/AppSidebar.vue        |   18 +-
 apps/ui/src/components/shell/AppTopbar.vue         |  182 +-
 apps/ui/src/components/trace/TracePopout.vue       |  828 ++++++
 apps/ui/src/composables/useAutoRefreshSubscribe.ts |   36 +
 apps/ui/src/composables/useLayerDashboard.ts       |    3 +
 .../src/composables/useLayerEndpointDependency.ts  |    3 +
 apps/ui/src/composables/useLayerLanding.ts         |    3 +
 apps/ui/src/composables/useLayerLogs.ts            |  118 +
 apps/ui/src/composables/useLayerTopology.ts        |    3 +
 apps/ui/src/composables/useLayerTraces.ts          |  131 +
 apps/ui/src/composables/useOapInfo.ts              |    4 +
 apps/ui/src/composables/useTopologyIcons.ts        |   10 +
 apps/ui/src/composables/useTracePopout.ts          |   66 +
 apps/ui/src/router/index.ts                        |    7 +-
 apps/ui/src/stores/autoRefresh.ts                  |  139 +
 apps/ui/src/stores/setup.ts                        |    2 +-
 apps/ui/src/views/admin/LayerDashboardsAdmin.vue   |  540 +++-
 apps/ui/src/views/layer/LayerDashboardsView.vue    |    2 +-
 .../views/layer/LayerEndpointDependencyView.vue    |  516 +++-
 apps/ui/src/views/layer/LayerLogsView.vue          |  702 ++++++
 apps/ui/src/views/layer/LayerServiceMapView.vue    |  368 ++-
 apps/ui/src/views/layer/LayerTracesView.vue        | 2665 ++++++++++++++++++++
 apps/ui/src/views/setup/LayerSetupCard.vue         |  307 ++-
 apps/ui/src/views/setup/SetupView.vue              |    4 +-
 horizon.example.yaml                               |    7 +
 packages/api-client/src/index.ts                   |   40 +
 packages/api-client/src/logs.ts                    |  100 +
 packages/api-client/src/overview.ts                |  107 +
 packages/api-client/src/setup.ts                   |   16 +
 packages/api-client/src/topology.ts                |   25 +
 packages/api-client/src/trace.ts                   |  221 ++
 109 files changed, 13934 insertions(+), 877 deletions(-)

diff --git a/.claude/skills/migrate-layer/SKILL.md b/.claude/skills/migrate-layer/SKILL.md
new file mode 100644
index 0000000..3fdefdb
--- /dev/null
+++ b/.claude/skills/migrate-layer/SKILL.md
@@ -0,0 +1,263 @@
+---
+name: migrate-layer
+description: Migrate one OAP layer's UI templates from booster-ui (apache/skywalking ui-initialized-templates) into the Horizon UI bundled config — preserves intent, adapts to Horizon's flatter widget grid + extracts metric semantics from upstream docs.
+user-invocable: true
+---
+
+# Migrate one OAP layer into Horizon
+
+Source of truth for the upstream side:
+- Templates: `/Users/wusheng/github/skywalking/oap-server/server-starter/src/main/resources/ui-initialized-templates/<layer>/`
+- Docs index: `/Users/wusheng/github/skywalking/docs/menu.yml` (paths are relative to `/Users/wusheng/github/skywalking/docs/`)
+
+Output goes to: `apps/bff/src/bundled_templates/layers/<layer>.json`
+
+The Horizon JSON shape is defined by `apps/bff/src/layers/loader.ts` (`LayerTemplate` interface). Read it once before each new layer so any new fields are picked up.
+
+---
+
+## When this skill applies
+
+You're staring at a booster-ui template folder (e.g. `k8s/`, `mesh/`, `virtual_database/`) with 1–7 nested `*.json` files and you need to produce **one** Horizon JSON that lights up the corresponding layer page (Services / Instances / Endpoints / Topology / Traces / Logs / Profiling). Some layers have only `*-root.json` + `*-service.json` (service-only); others add `*-instance.json`, `*-endpoint.json`, and `*-*-relation.json` (full mesh).
+
+---
+
+## Booster → Horizon mapping reference
+
+### Top-level shape
+
+| Booster | Horizon | Notes |
+|---|---|---|
+| One file per scope, each wrapping a `Tab` tree | One JSON, scopes flattened into `dashboards.{service,instance,endpoint,dependency,topology}` arrays | The `Tab` containers in booster are dropped — Horizon's layer page already provides the tab strip via the route hierarchy. |
+| `configuration.layer` | top-level `key` (also matches the file basename uppercased) | The loader asserts `file.json` ↔ `"key": "FILE"` (UPPER_SNAKE). |
+| `configuration.entity` (`Service` / `ServiceInstance` / `Endpoint` / …) | implied by which `dashboards.*` bucket the widget lives in | Drop the entity field — placement decides. |
+| `configuration.name` (e.g. `General-Service`) | not needed | Horizon derives titles from `alias` + route. |
+| `Event` widget at the bottom | not yet supported | Skip; revisit when Horizon adds an events strip. |
+
+### Widget renderer mapping
+
+| Booster `graph.type` | Horizon `type` | Comment |
+|---|---|---|
+| `Card` | `card` | Single scalar, avg across window. Card MQE usually wraps with `avg(...)`. Horizon evaluates the average itself; **strip the `avg()` wrapper** unless it changes shape (e.g. `avg(x/100)`). |
+| `Line` (single expression) | `line` | One labeled series. Carry `unit`, `expressions[0]`, `expressionLabels?`. |
+| `Line` (multiple expressions) | `line` | Multi-series. Use `expressionLabels` (mandatory for legend). When the booster widget has a dual-axis trick (e.g. count + latency), set `expressionAxes: [0, 1]` and supply `expressionUnits`. |
+| `TopList` | `top` | Sorted list — usually `top_n(...,N,des)`. When booster ships several `TopList`s on the same layer that are conceptually the same ranking with different sort dimensions (e.g. Top APIs by Traffic / by Slow / by SR), **fold them into a single `top` widget with multiple `expressions` + `expressionLabels` + `expressionUnits`** (the SPA renders these as in-widget tabs). |
+| `TopList` with RECORD-typed MQE (`top_n_service_database_statement`, etc.) | `record` | Use `record`, not `top`. Pair with `relatedTrace.refIdType: traceId` upstream — Horizon's record widget surfaces trace links automatically. |
+| `Topology` | **not a dashboard widget** — pull node/link MQE up to top-level `topology` block | The `linkServerExpressions`, `linkClientExpressions`, `nodeExpressions` become `topology.{nodeMetrics, linkServerMetrics, linkClientMetrics}` arrays. Add `role` (`center`/`ring`/`secondary` for nodes, `lineServer`/`lineClient` for edges). |
+| `InstanceList`, `EndpointList` | **dropped** — Horizon ships built-in pickers | The picker columns come from `header.columns` (or `overview.*`) instead. Don't reproduce these as dashboard widgets. |
+| `Trace`, `Log` | **flip the component flag** in `components.{traces,logs}` | No widget needed — Horizon owns the Traces / Logs views. |
+| `Profile` (trace profiling) | `components.traceProfiling: true` | Horizon's profiling views are component-level, not widget-level. |
+| `Ebpf` | `components.ebpfProfiling: true` | Same. |
+| `AsyncProfiling` | `components.asyncProfiling: true` | Same. |
+| `Text` | dropped | Layer header in Horizon already documents the page; if you need an external link, set `documentLink` at the layer root. |
+| `Event` | dropped (no Horizon equivalent yet) | Note in the PR if a layer leans on this heavily. |
+
+### Field-level mapping
+
+| Booster | Horizon | Notes |
+|---|---|---|
+| `widget.title` | `title` | Drop unit suffixes from the title — Horizon shows the unit separately. e.g. `"Service Avg Response Time (ms)"` → `"Avg Response Time"` + `"unit": "ms"`. |
+| `widget.tips` | `tip` | Keep verbatim where useful; otherwise tighten to one sentence. |
+| `expressions[0]` | `expressions[0]` | MQE stays. For cards, unwrap a single outer `avg(...)`. |
+| `graph.fontSize`, `textAlign`, `showSymbol`, `step`, `smooth`, `color`, `showXAxis`/`Y` | dropped | Horizon's chart wrappers own the rendering style. |
+| `metricConfig[*].unit` | `unit` (or `expressionUnits`) | Use widget-level `unit` for single-series widgets, `expressionUnits` per index for multi-series. |
+| `metricConfig[*].label` | `expressionLabels[i]` | When booster sets a label to give the legend a friendly name, copy it; otherwise rely on the MQE relabel (e.g. `relabels(...,percentile='99')` is already the legend label). |
+| `associate` (cross-widget cursor sync) | dropped | Horizon syncs crosshairs via `useCrosshair()` automatically. |
+| `relatedTrace.enableRelate`, `latency`, `status`, `queryOrder` | dropped (for widget) | The dashboard's trace-list links are auto-derived. For `record` widgets, keep `relatedTrace.refIdType` semantics by ensuring the underlying MQE is RECORD-typed. |
+| `valueRelatedDashboard` | dropped | Horizon routing — clicking a top item drills into the instance/endpoint page automatically. |
+| `x/y/w/h` (24-col grid) | `span` / `rowSpan` (12-col flow) | **Halve the booster width** to convert to Horizon's 12-col grid (`w: 8` → `span: 4`). Use `rowSpan` only when the widget is taller than the default 2 rows (default ~14px × 8 = chart height). Top-lists and percentile charts typically `rowSpan: 3` or `4`. |
+| `subExpressions` (per-row sparkline MQE under `InstanceList`/`EndpointList`) | dropped (UI computes the row-sparklines itself) | These are the picker's per-row spark sources. Horizon's pickers fetch them through `landing` routes. |
+| `legendMQE` + `description.healthy/unhealthy` on Topology | Horizon doesn't yet have a legend-rule UI — capture the rule in a comment for now | Pending UI work. |
+
+### Picker column derivation
+
+The layer page header (services list) needs `header.columns`. Source order of preference:
+1. The booster `*-service.json` Overview tab's top three single-card metrics — these are conceptually the same as the picker columns.
+2. The picker's `subExpressions` array — first 1–3 entries map well.
+3. Default to `cpm` / `sla/100` / `resp_time` if nothing is obvious.
+
+Always set `orderBy` to the column you want as the default sort (usually `cpm`).
+
+### Overview tile derivation
+
+`overview.groups` drives the tall hero card above the picker. Pick 2–4 metrics that tell the layer's health story at a glance — almost always:
+- **one** latency percentile (**P95** — prefer P95 over P99; do not include both, they're redundant in a hero strip)
+- an error rate (`100 - service_sla/100` or layer equivalent)
+- a health score (apdex) where available **or** a throughput indicator — pick the one that's most informative for that layer
+
+Group as `{ "title": "Latency & errors", "size": "auto", "metrics": [...] }` and (optionally) `{ "title": "Health", "size": "square", "metrics": [<single>] }` or `{ "title": "Throughput", "size": "square", "metrics": [<RPM>] }` when the layer has no apdex equivalent.
+
+---
+
+## 5-step procedure for migrating one layer
+
+Run these in order **for each layer**. The order matters: 1–4 build the mental model; 5 produces the JSON. Don't skip ahead to writing JSON.
+
+### Step 1 — Learn the old layout
+
+Read every file under `/Users/wusheng/github/skywalking/oap-server/server-starter/src/main/resources/ui-initialized-templates/<layer>/`. List:
+- Each `*-root.json` Tab name + child widgets — defines the layer landing strip
+- Each `*-service.json` / `*-instance.json` / `*-endpoint.json` Overview tab widget set — these become the `dashboards.<scope>` arrays
+- The `*-relation.json` files — the relation widget sets feed into Horizon's edge/dependency drill panels but **most metrics belong to topology.linkServerMetrics / linkClientMetrics**, not as flat widgets. If the relation file has 4+ widgets, mention them in the migration notes; if it's just RPM/respTime/SLA/percentile, fold into topology.
+- Note which widgets the layer uses (`InstanceList`, `EndpointList`, `Topology`, `Trace`, `Log`, `Profile`, etc.) so you know which `components.*` flags to flip.
+
+Optional `Bash` helper to summarize:
+```bash
+for f in /Users/wusheng/github/skywalking/oap-server/server-starter/src/main/resources/ui-initialized-templates/<LAYER>/*.json; do
+  echo "=== $(basename $f) ==="
+  python3 -c "
+import json
+d = json.load(open('$f'))
+def walk(n, depth=0):
+    if isinstance(n, list):
+        for i in n: walk(i, depth); return
+    if not isinstance(n, dict): return
+    t = n.get('type'); name = n.get('name')
+    title = (n.get('widget') or {}).get('title') if isinstance(n.get('widget'), dict) else None
+    if name or (t and t != 'Tab') or title:
+        print('  '*depth + (t or 'Tab') + ((': ' + (name or title)) if (name or title) else ''))
+    for c in n.get('children') or []: walk(c, depth+1)
+    if n.get('configuration'): walk(n['configuration'], depth)
+walk(d)"
+done
+```
+
+### Step 2 — Learn the Horizon capabilities
+
+Reread `apps/bff/src/layers/loader.ts` (the `LayerTemplate` interface + comments) and `packages/api-client/src/dashboard.ts` (the `DashboardWidget` interface). Decide which Horizon features the migration will use:
+- `dashboards.service` (always)
+- `dashboards.instance` (if booster has instance template **and** the layer has per-instance metrics worth showing)
+- `dashboards.endpoint` (same logic for endpoints)
+- `dashboards.dependency` (only if the relation file has metrics distinct from topology edges)
+- `topology` block (any layer with a non-trivial service map — almost all do)
+- `endpointDependency` block (only if the layer has per-endpoint relations, e.g. general, mesh, k8s_service)
+- `components.{traces, logs, traceProfiling, ebpfProfiling, asyncProfiling}` flags
+- `visibleWhen` for any widget whose MQE only fires conditionally (`instance_jvm_cpu has value` etc.) — common on instance-scope widgets that span multiple runtimes.
+
+Also reread the existing `general.json` (and `mesh.json` if migrating a service-mesh-adjacent layer) as the reference template. Copy its shape verbatim and replace metric names — don't reinvent the structure.
+
+### Step 3 — Read metrics meaning from booster's `widget.tips` + metric names
+
+For each widget you're going to migrate, write a one-line summary of what the metric represents. Sources:
+- `widget.tips` in the booster JSON (often the most direct)
+- The metric name itself (e.g. `instance_jvm_gc_pause_duration` → JVM GC pause time per minute)
+- The OAP query-protocol schemas under `apache/skywalking/oap-server/.../query-protocol/*.graphqls` if the metric is exposed there
+
+If you can't write a one-liner from these alone, move to Step 4.
+
+### Step 4 — Web search the target component + popular key metrics
+
+For domain-specific layers (Kafka, Redis, MongoDB, Envoy, Cilium, etc.), web search a small set:
+- `"<component> top metrics for monitoring"` — usually finds the canonical 5–10 SREs care about
+- `"<component> golden signals"` — RED / USE method articles
+- Vendor docs (Confluent for Kafka, Redis Labs for Redis, etc.) for the metric definitions
+
+The goal is to **prioritize** the booster widgets — if booster ships 30 metrics but only 8 of them are commonly-watched, surface those 8 in the layer landing and stash the rest in a "More" group (or just keep them in the JSON with smaller `span` so they live below the fold).
+
+### Step 5 — Read upstream docs from `docs/menu.yml`
+
+`/Users/wusheng/github/skywalking/docs/menu.yml` has a `catalog:` tree. Find the entry whose name matches the layer (e.g. "MySQL/MariaDB monitoring", "Kafka monitoring", "Redis monitoring") and follow its `path:` to `/Users/wusheng/github/skywalking/docs/<path-without-leading-slash>.md`. These docs:
+- List every metric the OAP module emits, with definitions
+- Often categorize metrics ("RED metrics", "Capacity metrics", "Reliability metrics")
+- Sometimes include the suggested dashboard layout
+
+This is the most authoritative source for metric semantics. **If a booster widget tip is vague but the upstream doc explains the metric, use the doc's wording for the Horizon `tip`.**
+
+### Step 6 — Write the Horizon JSON
+
+Now produce `apps/bff/src/bundled_templates/layers/<layer>.json`. Skeleton:
+
+```jsonc
+{
+  "key": "<UPPER_SNAKE>",
+  "alias": "<Display Name>",
+  "color": "var(--sw-accent)",            // most layers use the orange accent
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/<path-from-menu.yml>",
+  "aliases": {                            // optional — only override slot names when the layer uses non-standard entity terms
+    "services": "Brokers",
+    "instances": "Nodes",
+    "endpoints": "Topics"
+  },
+  "components": {
+    "service": true,
+    "instances": <has *-instance.json>,
+    "endpoints": <has *-endpoint.json>,
+    "endpointDependency": <has endpoint-relation.json>,
+    "topology": <has Topology widget anywhere>,
+    "traces": <has Trace widget>,
+    "logs": <has Log widget>,
+    "traceProfiling": <has Profile widget>,
+    "ebpfProfiling": <has Ebpf widget>,
+    "asyncProfiling": <has AsyncProfiling widget>
+  },
+  "layer-header": {
+    "orderBy": "<column metric key>",
+    "columns": [ /* 2-4 picker columns */ ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Latency & errors", "size": "auto", "metrics": [/* p95 + err (no p99 — redundant with p95 in a hero strip) */] },
+      { "title": "Health", "size": "square", "metrics": [/* single apdex-like */] }
+    ]
+  },
+  "dashboards": {
+    "service": [ /* the *-service.json Overview tab widgets, mapped 1:1 */ ],
+    "instance": [ /* *-instance.json Overview + JVM/CLR/etc tabs flattened */ ],
+    "endpoint": [ /* *-endpoint.json Overview tab */ ]
+  },
+  "topology": {
+    "nodeMetrics": [ /* center, ring, secondary */ ],
+    "linkServerMetrics": [ /* RPM, respTime, p95, SLA — order matters for fallback */ ],
+    "linkClientMetrics": [ /* same shape; omit on layers w/o client-side relations */ ]
+  },
+  "endpointDependency": {                  // only when components.endpointDependency
+    "nodeMetrics": [ /* center, ring, secondary */ ],
+    "linkMetrics":  [ /* server-only — endpoint relations have no client side in OAP */ ]
+  }
+}
+```
+
+### Step 7 — Validate
+
+From `apps/bff`:
+```bash
+pnpm exec tsc --noEmit              # schema still typechecks
+node --import tsx -e "import('./src/layers/loader.ts').then(m => { const t = m.getLayerTemplate('<KEY>'); console.log(JSON.stringify({key:t.key, components:t.components, headerCols:t.header.columns.length, dashScopes:Object.keys(t.dashboards||{}), topo:!!t.topology, epDep:!!t.endpointDependency}, null, 2)); })"
+```
+
+Sanity-check the JSON loads and surfaces all expected fields. If the BFF is running, hit `/api/menu` and `/api/layer/<key>/landing` against the demo OAP to confirm the picker columns resolve.
+
+---
+
+## Common pitfalls
+
+1. **Scope mismatch — SERVICE_INSTANCE metrics in service dashboards.** A bare per-instance metric (e.g. `meter_oap_instance_cpu_percentage`, catalog `SERVICE_INSTANCE`) **does not work** in a service-scope picker / overview / line widget. You have three options:
+   - **Picker column / overview / card** — wrap with `avg(...)` or `sum(...)` to coerce to a single scalar (e.g. `avg(meter_oap_instance_cpu_percentage)`).
+   - **Service-dashboard line widget showing per-instance trends** — must be a **`top_n(...)` `top` widget**, never a bare `line`. The line widget at service scope can only render SERVICE-scope metrics.
+   - **Per-instance metric on the instance scope** — put it under `dashboards.instance` and reference the bare metric; it works fine at instance entity.
+   The same rule applies one level deeper for ENDPOINT-scope metrics in service dashboards — use `top_n` or aggregate.
+2. **Labeled vs regular OAL/MAL output.** `aggregate_labels(metric, sum)` is only valid for LABELED_VALUE metrics. For REGULAR_VALUE metrics use plain `sum(metric)` / `avg(metric)`. Mixing them produces "result is not a labeled result" errors. Check `listMetrics(regex:^<name>$)` on the demo OAP to confirm catalog + type before composing.
+3. **Card MQE includes `avg(...)`** — booster wraps card values in `avg(...)` because their card renderer expects a scalar. Horizon's card path *also* averages, so leaving the `avg(...)` works but is redundant. **For consistency, unwrap a single outer `avg()` from card expressions** unless removing it changes the shape (e.g. `avg(x)/100` becomes `x/100`, but `avg(x{p='99'})/100` stays as-is because the inner `{p='99'}` is the dimension we're averaging across).
+2. **24-col → 12-col grid** — `w: 8` (booster) becomes `span: 4` (Horizon). Don't paste 24-col widths or widgets overflow the grid.
+3. **InstanceList / EndpointList** — these are *not* widgets in Horizon. They're the layer page's built-in pickers. The booster widget's `subExpressions` array (per-row MQE) is the closest analogue; drop it (Horizon's landing route picks per-row metrics from `header.columns`).
+4. **Relation files** — most of a `*-service-relation.json` collapses into the top-level `topology` block (server + client edge metrics). Don't replicate the relation widgets as flat `dashboards.dependency` entries unless they're metrics the topology renderer can't surface (e.g. relation-specific record widgets).
+5. **`visibleWhen`** — booster instance dashboards bundle JVM / CLR / Golang / Python / Ruby / Spring widgets in the same scope, and the SPA hides ones with no data. Carry this over with `"visibleWhen": "<first_metric> has value"` so the Horizon Instance view stays clean for a single-runtime instance.
+6. **No `Co-Authored-By` footers on commits** — per project `CLAUDE.md` and stored memory.
+
+---
+
+## Reference: General layer migration
+
+The General layer (already migrated) is the canonical reference. Compare:
+
+- Upstream files: `general-root.json`, `general-service.json`, `general-instance.json`, `general-endpoint.json`, `general-service-relation.json`, `general-instance-relation.json`, `general-endpoint-relation.json` (4565 lines total across 7 files)
+- Horizon output: `apps/bff/src/bundled_templates/layers/general.json` (989 lines, one file)
+
+Key transformations applied (use as a recipe):
+- 7 nested `Tab` files → flat `dashboards.{service,instance,endpoint}` arrays (dependency + topology folded into top-level blocks)
+- Card MQE unwrapped (`avg(service_apdex)/10000` → in card context Horizon does the avg, but kept as-is when the math is inside)
+- Picker columns derived from the booster Overview tab's three card metrics: cpm / apdex / error rate
+- Topology block merged `general-service-relation.json`'s `linkServerExpressions` / `linkClientExpressions` into a single block with four-metric server + client families (RPM / respTime / p95 / SLA)
+- Slow-statements widget converted from booster's `TopList` with RECORD-typed MQE to Horizon's `record` type
+- Multi-runtime instance widgets (JVM / CLR / Sleuth / Go / Python / Ruby) all carry `visibleWhen: "<first_metric> has value"` so instances only render the relevant rows
+- `components.{traceProfiling, ebpfProfiling, asyncProfiling}` set from the three booster profiling tabs
diff --git a/.github/workflows/publish-image.yaml b/.github/workflows/publish-image.yaml
new file mode 100644
index 0000000..37c0fc3
--- /dev/null
+++ b/.github/workflows/publish-image.yaml
@@ -0,0 +1,105 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Publish a Horizon UI container image to GHCR on:
+#   - push to `main`  (tagged with `main` + the full commit SHA)
+#   - any `v*` tag    (tagged with the version + the full commit SHA)
+#
+# The full commit SHA is the canonical, immutable identifier — moving tags
+# like `main` and `vX.Y.Z` are conveniences that *also* point at the same
+# SHA-pinned image. Operators should pull by SHA in production.
+name: Publish image
+
+on:
+  push:
+    branches:
+      - main
+    tags:
+      - 'v*'
+
+permissions:
+  contents: read
+  packages: write
+
+concurrency:
+  group: publish-image-${{ github.ref }}
+  cancel-in-progress: false
+
+env:
+  REGISTRY: ghcr.io
+  IMAGE_NAME: ${{ github.repository }}
+
+jobs:
+  build-and-push:
+    name: Build + push (${{ github.ref_type }} ${{ github.ref_name }})
+    runs-on: ubuntu-latest
+    timeout-minutes: 30
+    steps:
+      - uses: actions/checkout@v6
+        with:
+          persist-credentials: false
+
+      - name: Set up QEMU
+        uses: docker/setup-qemu-action@v3
+
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v3
+
+      - name: Log in to GHCR
+        uses: docker/login-action@v3
+        with:
+          registry: ${{ env.REGISTRY }}
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+
+      # The canonical tag is the full 40-char commit SHA. We also publish
+      # moving tags so common workflows ("pull :main", "pull :v1.2.3")
+      # work — they all resolve back to the same SHA-tagged image.
+      - name: Compute image tags
+        id: meta
+        uses: docker/metadata-action@v5
+        with:
+          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+          tags: |
+            type=sha,format=long,prefix=
+            type=ref,event=branch,enable=${{ github.ref == 'refs/heads/main' }}
+            type=semver,pattern={{version}},enable=${{ github.ref_type == 'tag' }}
+            type=semver,pattern={{major}}.{{minor}},enable=${{ github.ref_type == 'tag' }}
+            type=raw,value=latest,enable=${{ github.ref_type == 'tag' }}
+          labels: |
+            org.opencontainers.image.source=https://github.com/${{ github.repository }}
+            org.opencontainers.image.revision=${{ github.sha }}
+            org.opencontainers.image.title=Apache SkyWalking Horizon UI
+            org.opencontainers.image.description=Next-generation web UI for Apache SkyWalking.
+            org.opencontainers.image.licenses=Apache-2.0
+
+      - name: Build + push
+        uses: docker/build-push-action@v6
+        with:
+          context: .
+          file: Dockerfile
+          push: true
+          platforms: linux/amd64,linux/arm64
+          tags: ${{ steps.meta.outputs.tags }}
+          labels: ${{ steps.meta.outputs.labels }}
+          cache-from: type=gha
+          cache-to: type=gha,mode=max
+
+      - name: Image SHA tag (canonical)
+        run: |
+          echo "::notice::Published ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha=${{ github.sha }}"
+          echo "All published tags:"
+          echo '${{ steps.meta.outputs.tags }}'
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..6069c06
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,69 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# ---- builder ----------------------------------------------------------------
+# Builds the BFF bundle + the Vite SPA in a single workspace install. Final
+# image only carries the BFF dist + UI dist + the production dependency tree
+# (no source, no devDependencies, no pnpm store).
+FROM node:20-alpine AS builder
+WORKDIR /workspace
+
+# argon2 (password hashing) builds a native module; alpine needs python + a
+# C toolchain at build time. The deps are dropped from the runtime stage.
+RUN apk add --no-cache python3 make g++ libc6-compat
+RUN corepack enable && corepack prepare pnpm@9.12.3 --activate
+
+COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./
+COPY apps/bff/package.json apps/bff/
+COPY apps/ui/package.json apps/ui/
+COPY packages/api-client/package.json packages/api-client/
+COPY packages/design-tokens/package.json packages/design-tokens/
+COPY packages/templates/package.json packages/templates/
+RUN pnpm install --frozen-lockfile
+
+COPY . .
+RUN pnpm --filter @skywalking-horizon-ui/bff build \
+ && pnpm --filter @skywalking-horizon-ui/ui  build
+
+# Deploy a focused production install for the BFF — pnpm copies only the
+# packages it actually needs (workspace deps + runtime npm deps). This is
+# what ships in the final image.
+RUN pnpm deploy --filter @skywalking-horizon-ui/bff --prod /deploy/bff
+
+# ---- runtime ---------------------------------------------------------------
+FROM node:20-alpine
+WORKDIR /app
+
+# Run as a non-root user — the BFF doesn't need any privileged access.
+RUN addgroup -S horizon && adduser -S -G horizon horizon
+
+COPY --from=builder /deploy/bff/dist ./dist
+COPY --from=builder /deploy/bff/node_modules ./node_modules
+COPY --from=builder /deploy/bff/package.json ./package.json
+# The bundled layer + overview JSONs must sit one level up from the
+# compiled server (`/app/dist/server.js`). The loader resolves them via
+# `path.join(__dirname, '..', 'bundled_templates', ...)` — keeping this
+# layout in sync with the source tree means no path remapping at boot.
+COPY --from=builder /workspace/apps/bff/src/bundled_templates ./bundled_templates
+COPY --from=builder /workspace/apps/ui/dist ./static
+COPY --from=builder /workspace/horizon.example.yaml ./horizon.example.yaml
+
+ENV NODE_ENV=production \
+    HORIZON_STATIC_DIR=/app/static \
+    HORIZON_CONFIG=/app/horizon.yaml
+
+USER horizon
+EXPOSE 8081
+CMD ["node", "dist/server.js"]
diff --git a/apps/bff/src/bundled_templates/layers/activemq.json b/apps/bff/src/bundled_templates/layers/activemq.json
new file mode 100644
index 0000000..be8f133
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/activemq.json
@@ -0,0 +1,59 @@
+{
+  "key": "ACTIVEMQ",
+  "alias": "ActiveMQ",
+  "color": "var(--sw-purple)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-activemq-monitoring/",
+  "aliases": { "services": "ActiveMQ clusters", "instances": "Brokers", "endpoints": "Destinations" },
+  "components": { "service": true, "instances": true, "endpoints": true, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "enqueue",
+    "columns": [
+      { "metric": "enqueue", "label": "Enqueue/s", "mqe": "meter_activemq_cluster_enqueue_rate", "aggregation": "avg" },
+      { "metric": "dequeue", "label": "Dequeue/s", "mqe": "meter_activemq_cluster_dequeue_rate", "aggregation": "avg" },
+      { "metric": "load", "label": "System Load", "mqe": "meter_activemq_cluster_system_load_average/10000", "aggregation": "avg" },
+      { "metric": "threads", "label": "Threads", "mqe": "meter_activemq_cluster_thread_count", "aggregation": "avg" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Throughput", "size": "auto", "metrics": [
+        { "id": "enqueue", "label": "Enqueue/s", "tip": "Messages enqueued per second.", "mqe": "meter_activemq_cluster_enqueue_rate", "aggregation": "avg" },
+        { "id": "dequeue", "label": "Dequeue/s", "tip": "Messages dequeued per second.", "mqe": "meter_activemq_cluster_dequeue_rate", "aggregation": "avg" },
+        { "id": "expired", "label": "Expired/s", "tip": "Messages that expired before consumption.", "mqe": "meter_activemq_cluster_expired_rate", "aggregation": "avg" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "load", "title": "System Load Average", "type": "line", "expressions": ["meter_activemq_cluster_system_load_average/10000"], "span": 3, "rowSpan": 2 },
+      { "id": "threads", "title": "Thread Count", "type": "line", "expressions": ["meter_activemq_cluster_thread_count"], "span": 3, "rowSpan": 2 },
+      { "id": "heap_used", "title": "Heap Used (MB)", "type": "line", "unit": "MB", "expressions": ["meter_activemq_cluster_heap_memory_usage_used/1024/1024"], "span": 3, "rowSpan": 2 },
+      { "id": "heap_max", "title": "Heap Max (MB)", "type": "line", "unit": "MB", "expressions": ["latest(aggregate_labels(meter_activemq_cluster_heap_memory_usage_max,sum))/1024/1024"], "span": 3, "rowSpan": 2 },
+      { "id": "enqueue_dequeue", "title": "Enqueue / Dequeue / Dispatch /s", "type": "line", "expressions": ["meter_activemq_cluster_enqueue_rate", "meter_activemq_cluster_dequeue_rate", "meter_activemq_cluster_dispatch_rate"], "expressionLabels": ["enqueue", "dequeue", "dispatch"], "span": 6, "rowSpan": 2 },
+      { "id": "expired_rate", "title": "Expired /s", "type": "line", "expressions": ["meter_activemq_cluster_expired_rate"], "span": 6, "rowSpan": 2 },
+      { "id": "enqueue_time", "title": "Enqueue Time", "type": "line", "unit": "s", "expressions": ["meter_activemq_cluster_average_enqueue_time/1000", "meter_activemq_cluster_max_enqueue_time/1000"], "expressionLabels": ["avg", "max"], "span": 6, "rowSpan": 2 },
+      { "id": "gc_counts", "title": "GC Counts (G1+Parallel)", "type": "line", "expressions": ["view_as_seq(meter_activemq_cluster_gc_g1_old_collection_count,meter_activemq_cluster_gc_parallel_old_collection_count)", "view_as_seq(meter_activemq_cluster_gc_g1_young_collection_count,meter_activemq_cluster_gc_parallel_young_collection_count)"], "expressionLabels": ["old", "young"], "span": 6, "rowSpan": 2 },
+      { "id": "gc_time", "title": "GC Time (ms)", "type": "line", "unit": "ms", "expressions": ["view_as_seq(meter_activemq_cluster_gc_g1_old_collection_time,meter_activemq_cluster_gc_parallel_old_collection_time)", "view_as_seq(meter_activemq_cluster_gc_g1_young_collection_time,meter_activemq_cluster_gc_parallel_young_collection_time)"], "expressionLabels": ["old", "young"], "span": 12, "rowSpan": 2 }
+    ],
+    "instance": [
+      { "id": "slave", "title": "Slave Broker", "type": "line", "expressions": ["latest(meter_activemq_broker_state)"], "span": 3, "rowSpan": 2 },
+      { "id": "uptime", "title": "Uptime (hours)", "type": "line", "expressions": ["latest(meter_activemq_broker_uptime)/1000/60/60"], "span": 3, "rowSpan": 2 },
+      { "id": "conns", "title": "Connections", "type": "line", "expressions": ["meter_activemq_broker_current_connections"], "span": 3, "rowSpan": 2 },
+      { "id": "msg_size", "title": "Message Size (B)", "type": "line", "expressions": ["aggregate_labels(meter_activemq_broker_average_message_size,avg)", "aggregate_labels(meter_activemq_broker_max_message_size,max)"], "expressionLabels": ["avg", "max"], "span": 3, "rowSpan": 2 },
+      { "id": "producer_count", "title": "Producer Count", "type": "line", "expressions": ["aggregate_labels(meter_activemq_broker_producer_count,sum)", "latest(aggregate_labels(meter_activemq_broker_current_producer_count,sum))"], "expressionLabels": ["increased", "current"], "span": 4, "rowSpan": 2 },
+      { "id": "consumer_count", "title": "Consumer Count", "type": "line", "expressions": ["aggregate_labels(meter_activemq_broker_consumer_count,sum)", "latest(aggregate_labels(meter_activemq_broker_current_consumer_count,sum))"], "expressionLabels": ["increased", "current"], "span": 4, "rowSpan": 2 },
+      { "id": "eq_deq", "title": "Enqueue/Dequeue Count", "type": "line", "expressions": ["aggregate_labels(meter_activemq_broker_enqueue_count,sum)", "aggregate_labels(meter_activemq_broker_dequeue_count,sum)"], "expressionLabels": ["enqueue", "dequeue"], "span": 4, "rowSpan": 2 },
+      { "id": "eq_deq_rate", "title": "Enqueue/Dequeue Rate", "type": "line", "expressions": ["aggregate_labels(meter_activemq_broker_enqueue_rate,avg)", "aggregate_labels(meter_activemq_broker_dequeue_rate,avg)"], "expressionLabels": ["enqueue", "dequeue"], "span": 4, "rowSpan": 2 },
+      { "id": "memory_usage", "title": "Memory Usage (MB)", "type": "line", "unit": "MB", "expressions": ["aggregate_labels(meter_activemq_broker_memory_usage,sum)/1024/1024"], "span": 4, "rowSpan": 2 },
+      { "id": "limits", "title": "Usage Limits (GB)", "type": "line", "unit": "GB", "expressions": ["aggregate_labels(meter_activemq_broker_memory_limit,sum)/1024/1024/1024", "aggregate_labels(meter_activemq_broker_store_limit,sum)/1024/1024/1024", "aggregate_labels(meter_activemq_broker_temp_limit,sum)/1024/1024/1024"], "expressionLabels": ["memory", "store", "temp"], "span": 4, "rowSpan": 2 }
+    ],
+    "endpoint": [
+      { "id": "producer", "title": "Producer Count", "type": "line", "expressions": ["meter_activemq_destination_producer_count"], "span": 4, "rowSpan": 2 },
+      { "id": "consumer", "title": "Consumer Count", "type": "line", "expressions": ["meter_activemq_destination_consumer_count"], "span": 4, "rowSpan": 2 },
+      { "id": "queue_size", "title": "Queue Size", "type": "line", "expressions": ["meter_activemq_destination_queue_size"], "span": 4, "rowSpan": 2 },
+      { "id": "memory", "title": "Memory Usage (MB)", "type": "line", "unit": "MB", "expressions": ["meter_activemq_destination_memory_usage/1024/1024"], "span": 4, "rowSpan": 2 },
+      { "id": "messages", "title": "Message Counts", "type": "line", "expressions": ["meter_activemq_destination_enqueue_count", "meter_activemq_destination_dequeue_count", "meter_activemq_destination_dispatch_count", "meter_activemq_destination_expired_count", "meter_activemq_destination_inflight_count"], "expressionLabels": ["enqueue", "dequeue", "dispatch", "expired", "inflight"], "span": 8, "rowSpan": 2 },
+      { "id": "enqueue_time", "title": "Enqueue Time (s)", "type": "line", "unit": "s", "expressions": ["meter_activemq_destination_average_enqueue_time/1000", "meter_activemq_destination_max_enqueue_time/1000"], "expressionLabels": ["avg", "max"], "span": 12, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/alipay_mini_program.json b/apps/bff/src/bundled_templates/layers/alipay_mini_program.json
new file mode 100644
index 0000000..10e7b1e
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/alipay_mini_program.json
@@ -0,0 +1,179 @@
+{
+  "key": "ALIPAY_MINI_PROGRAM",
+  "alias": "Alipay Mini Program",
+  "color": "var(--sw-cyan)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-alipay-mini-program-monitoring/",
+  "aliases": {
+    "services": "Mini-programs",
+    "instances": "Versions",
+    "endpoints": "Pages"
+  },
+  "components": {
+    "service": true,
+    "instances": true,
+    "endpoints": true,
+    "topology": false,
+    "traces": true,
+    "logs": true
+  },
+  "layer-header": {
+    "orderBy": "cpm",
+    "columns": [
+      { "metric": "cpm", "label": "Request RPM", "unit": "rpm", "mqe": "meter_alipay_mp_request_cpm", "aggregation": "sum" },
+      { "metric": "launch", "label": "Launch", "unit": "ms", "mqe": "meter_alipay_mp_app_launch_duration", "aggregation": "avg" },
+      { "metric": "firstRender", "label": "First Render", "unit": "ms", "mqe": "meter_alipay_mp_first_render_duration", "aggregation": "avg" },
+      { "metric": "errors", "label": "Errors", "mqe": "meter_alipay_mp_error_count", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      {
+        "title": "Latency & errors",
+        "size": "auto",
+        "metrics": [
+          { "id": "request_p95", "label": "Request P95", "tip": "95th-percentile request duration from the mini-program network bridge.", "mqe": "meter_alipay_mp_request_duration_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+          { "id": "launch", "label": "Launch", "tip": "Approximate mini-program launch duration.", "mqe": "meter_alipay_mp_app_launch_duration", "unit": "ms", "aggregation": "avg" },
+          { "id": "errors", "label": "Errors", "tip": "Reported error count.", "mqe": "meter_alipay_mp_error_count", "aggregation": "sum" }
+        ]
+      },
+      {
+        "title": "Throughput",
+        "size": "square",
+        "metrics": [
+          { "id": "cpm", "label": "Request RPM", "tip": "Bridge requests per minute.", "mqe": "meter_alipay_mp_request_cpm", "unit": "rpm", "aggregation": "sum" }
+        ]
+      }
+    ]
+  },
+  "dashboards": {
+    "service": [
+      {
+        "id": "launch_duration",
+        "title": "App Launch Duration",
+        "tip": "Approximate cold-launch duration from the Alipay container.",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_alipay_mp_app_launch_duration"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "first_render",
+        "title": "First Render Duration",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_alipay_mp_first_render_duration"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "error_count",
+        "title": "Error Count",
+        "type": "line",
+        "expressions": ["meter_alipay_mp_error_count"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "request_cpm",
+        "title": "Request Load",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["meter_alipay_mp_request_cpm"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "request_percentile",
+        "title": "Request Duration Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(meter_alipay_mp_request_duration_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 6,
+        "rowSpan": 2
+      }
+    ],
+    "instance": [
+      {
+        "id": "v_launch",
+        "title": "Launch Duration",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_alipay_mp_instance_app_launch_duration"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "v_first_render",
+        "title": "First Render Duration",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_alipay_mp_instance_first_render_duration"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "v_request_cpm",
+        "title": "Request Load",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["meter_alipay_mp_instance_request_cpm"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "v_request_percentile",
+        "title": "Request Duration Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(meter_alipay_mp_instance_request_duration_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 12,
+        "rowSpan": 2
+      }
+    ],
+    "endpoint": [
+      {
+        "id": "ep_launch",
+        "title": "Launch Duration on Page",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_alipay_mp_endpoint_app_launch_duration"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "ep_first_render",
+        "title": "First Render Duration",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_alipay_mp_endpoint_first_render_duration"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "ep_request_cpm",
+        "title": "Request Load",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["meter_alipay_mp_endpoint_request_cpm"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "ep_request_percentile",
+        "title": "Request Duration Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(meter_alipay_mp_endpoint_request_duration_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 12,
+        "rowSpan": 2
+      }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/apisix.json b/apps/bff/src/bundled_templates/layers/apisix.json
new file mode 100644
index 0000000..0e97e4b
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/apisix.json
@@ -0,0 +1,67 @@
+{
+  "key": "APISIX",
+  "alias": "APISIX",
+  "color": "var(--sw-ok)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-apisix-monitoring/",
+  "aliases": {
+    "services": "APISIX services",
+    "instances": "Nodes",
+    "endpoints": "Routes"
+  },
+  "components": {
+    "service": true,
+    "instances": true,
+    "endpoints": true,
+    "topology": false,
+    "traces": false,
+    "logs": false
+  },
+  "layer-header": {
+    "orderBy": "rps",
+    "columns": [
+      { "metric": "rps", "label": "RPS", "mqe": "aggregate_labels(meter_apisix_sv_http_requests,sum(service_instance_id))", "aggregation": "sum" },
+      { "metric": "status200", "label": "200/s", "mqe": "aggregate_labels(meter_apisix_sv_http_status_matched{code='200'},sum(code))", "aggregation": "sum" },
+      { "metric": "status404", "label": "404/s", "mqe": "aggregate_labels(meter_apisix_sv_http_status_matched{code='404'},sum(code))", "aggregation": "sum" },
+      { "metric": "status503", "label": "503/s", "mqe": "aggregate_labels(meter_apisix_sv_http_status_matched{code='503'},sum(code))", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      {
+        "title": "Throughput",
+        "size": "auto",
+        "metrics": [
+          { "id": "rps", "label": "RPS", "tip": "Requests per second matched by APISIX routes.", "mqe": "aggregate_labels(meter_apisix_sv_http_requests,sum(service_instance_id))", "aggregation": "sum" },
+          { "id": "status5xx", "label": "5xx", "tip": "Upstream / gateway error class.", "mqe": "aggregate_labels(meter_apisix_sv_http_status_matched{code='503'},sum(code))", "aggregation": "sum" }
+        ]
+      }
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "rps", "title": "HTTP Request Trend", "type": "line", "expressions": ["aggregate_labels(meter_apisix_sv_http_requests,sum(service_instance_id))"], "span": 6, "rowSpan": 2 },
+      { "id": "status", "title": "HTTP Status Trend", "type": "line", "expressions": ["aggregate_labels(meter_apisix_sv_http_status_matched,sum(code))"], "span": 6, "rowSpan": 2 },
+      { "id": "latency", "title": "HTTP Latency", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_apisix_sv_http_latency_matched,avg(type,p))"], "span": 6, "rowSpan": 2 },
+      { "id": "bandwidth", "title": "HTTP Bandwidth", "type": "line", "unit": "KB", "expressions": ["aggregate_labels(meter_apisix_sv_bandwidth_matched/1024,sum(type))"], "span": 6, "rowSpan": 2 },
+      { "id": "conns", "title": "HTTP Connections", "type": "line", "expressions": ["aggregate_labels(meter_apisix_sv_http_connections,sum(state))"], "span": 6, "rowSpan": 2 },
+      { "id": "status_unmatched", "title": "Non-matched Status Trend", "tip": "Requests with no matching APISIX route.", "type": "line", "expressions": ["aggregate_labels(meter_apisix_sv_http_status_unmatched,sum(code))"], "span": 6, "rowSpan": 2 },
+      { "id": "latency_unmatched", "title": "Non-matched Latency", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_apisix_sv_http_latency_unmatched,avg(type,p))"], "span": 6, "rowSpan": 2 },
+      { "id": "bandwidth_unmatched", "title": "Non-matched Bandwidth", "type": "line", "unit": "KB", "expressions": ["aggregate_labels(meter_apisix_sv_bandwidth_unmatched/1024,sum(type))"], "span": 6, "rowSpan": 2 }
+    ],
+    "instance": [
+      { "id": "rps", "title": "HTTP Request Trend", "type": "line", "expressions": ["meter_apisix_instance_http_requests"], "span": 4, "rowSpan": 2 },
+      { "id": "status", "title": "HTTP Status Trend", "type": "line", "expressions": ["meter_apisix_instance_http_status_matched"], "span": 4, "rowSpan": 2 },
+      { "id": "latency", "title": "HTTP Latency", "type": "line", "unit": "ms", "expressions": ["meter_apisix_instance_http_latency_matched"], "span": 4, "rowSpan": 2 },
+      { "id": "bandwidth", "title": "HTTP Bandwidth", "type": "line", "unit": "KB", "expressions": ["meter_apisix_instance_bandwidth_matched/1024"], "span": 4, "rowSpan": 2 },
+      { "id": "conns", "title": "HTTP Connections", "type": "line", "expressions": ["meter_apisix_instance_http_connections"], "span": 4, "rowSpan": 2 },
+      { "id": "shared_dict", "title": "Shared Dict", "tip": "Capacity vs free space.", "type": "line", "unit": "MB", "expressions": ["meter_apisix_instance_shared_dict_capacity_bytes/1024/1024", "meter_apisix_instance_shared_dict_free_space_bytes/1024/1024"], "expressionLabels": ["capacity", "free"], "span": 4, "rowSpan": 2 },
+      { "id": "etcd", "title": "etcd", "type": "line", "expressions": ["meter_apisix_instance_etcd_indexes", "latest(meter_apisix_instance_etcd_reachable)"], "expressionLabels": ["indexes", "reachable"], "span": 6, "rowSpan": 2 },
+      { "id": "unmatched", "title": "Non-matched Traffic", "tip": "Status + latency + bandwidth for requests with no matching route.", "type": "line", "expressions": ["meter_apisix_instance_http_status_unmatched", "meter_apisix_instance_http_latency_unmatched", "meter_apisix_instance_bandwidth_unmatched/1024"], "expressionLabels": ["status", "latency (ms)", "bandwidth (KB)"], "span": 6, "rowSpan": 2 }
+    ],
+    "endpoint": [
+      { "id": "status", "title": "HTTP Status Trend", "type": "line", "expressions": ["aggregate_labels(meter_apisix_endpoint_http_status,sum(code))"], "span": 4, "rowSpan": 2 },
+      { "id": "latency", "title": "HTTP Latency", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_apisix_endpoint_http_latency,avg(type,p))"], "span": 4, "rowSpan": 2 },
+      { "id": "bandwidth", "title": "HTTP Bandwidth", "type": "line", "unit": "KB", "expressions": ["aggregate_labels(meter_apisix_endpoint_bandwidth/1024,sum(type))"], "span": 4, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/aws_dynamodb.json b/apps/bff/src/bundled_templates/layers/aws_dynamodb.json
new file mode 100644
index 0000000..7961ddd
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/aws_dynamodb.json
@@ -0,0 +1,57 @@
+{
+  "key": "AWS_DYNAMODB",
+  "alias": "AWS DynamoDB",
+  "color": "var(--sw-warn)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-aws-dynamodb-monitoring/",
+  "aliases": { "services": "DynamoDB accounts", "endpoints": "Tables" },
+  "components": { "service": true, "instances": false, "endpoints": true, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "throttled",
+    "columns": [
+      { "metric": "throttledR", "label": "Read Throttled", "mqe": "aws_dynamodb_read_throttled_requests", "aggregation": "sum" },
+      { "metric": "throttledW", "label": "Write Throttled", "mqe": "aws_dynamodb_write_throttled_requests", "aggregation": "sum" },
+      { "metric": "errR", "label": "Read Sys Err", "mqe": "aws_dynamodb_read_system_errors", "aggregation": "sum" },
+      { "metric": "errW", "label": "Write Sys Err", "mqe": "aws_dynamodb_write_system_errors", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Throttling", "size": "auto", "metrics": [
+        { "id": "throttledR", "label": "Read Throttled", "tip": "Read requests throttled — capacity exhaustion.", "mqe": "aws_dynamodb_read_throttled_requests", "aggregation": "sum" },
+        { "id": "throttledW", "label": "Write Throttled", "tip": "Write requests throttled.", "mqe": "aws_dynamodb_write_throttled_requests", "aggregation": "sum" },
+        { "id": "userErr", "label": "User Errors", "tip": "Bad-request style user errors.", "mqe": "aws_dynamodb_user_errors", "aggregation": "sum" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "throttled", "title": "Throttled Requests", "type": "line", "expressions": ["aws_dynamodb_read_throttled_requests", "aws_dynamodb_write_throttled_requests"], "expressionLabels": ["read", "write"], "span": 4, "rowSpan": 2 },
+      { "id": "throttle_events", "title": "Throttle Events", "type": "line", "expressions": ["aws_dynamodb_read_throttle_events", "aws_dynamodb_write_throttle_events"], "expressionLabels": ["read", "write"], "span": 4, "rowSpan": 2 },
+      { "id": "sys_errors", "title": "System Errors", "type": "line", "expressions": ["aws_dynamodb_read_system_errors", "aws_dynamodb_write_system_errors"], "expressionLabels": ["read", "write"], "span": 4, "rowSpan": 2 },
+      { "id": "user_errors", "title": "User Errors", "type": "line", "expressions": ["aws_dynamodb_user_errors"], "span": 4, "rowSpan": 2 },
+      { "id": "cond_check_fail", "title": "Conditional Check Failed", "type": "line", "expressions": ["aws_dynamodb_conditional_check_failed_requests"], "span": 4, "rowSpan": 2 },
+      { "id": "txn_conflict", "title": "Transaction Conflict", "type": "line", "expressions": ["aws_dynamodb_transaction_conflict"], "span": 4, "rowSpan": 2 },
+      { "id": "read_usage", "title": "Read Capacity (unit/s)", "type": "line", "expressions": ["aws_dynamodb_provisioned_read_capacity_units", "aws_dynamodb_consumed_write_capacity_units"], "expressionLabels": ["provisioned", "consumed"], "span": 6, "rowSpan": 2 },
+      { "id": "write_usage", "title": "Write Capacity (unit/s)", "type": "line", "expressions": ["aws_dynamodb_provisioned_write_capacity_units", "aws_dynamodb_consumed_write_capacity_units"], "expressionLabels": ["provisioned", "consumed"], "span": 6, "rowSpan": 2 },
+      { "id": "latency", "title": "Successful Request Latency (ms)", "type": "line", "unit": "ms", "expressions": ["aws_dynamodb_get_successful_request_latency", "aws_dynamodb_put_successful_request_latency", "aws_dynamodb_query_successful_request_latency", "aws_dynamodb_scan_successful_request_latency"], "expressionLabels": ["get", "put", "query", "scan"], "span": 12, "rowSpan": 2 },
+      { "id": "ttl_deleted", "title": "TTL Deleted Items", "type": "line", "expressions": ["aws_dynamodb_time_to_live_deleted_item_count"], "span": 4, "rowSpan": 2 },
+      { "id": "scan_items", "title": "Scan Returned Items", "type": "line", "expressions": ["aws_dynamodb_scan_returned_item_count"], "span": 4, "rowSpan": 2 },
+      { "id": "query_items", "title": "Query Returned Items", "type": "line", "expressions": ["aws_dynamodb_query_returned_item_count"], "span": 4, "rowSpan": 2 },
+      { "id": "account_max", "title": "Account Max Reads / Writes", "type": "line", "expressions": ["aws_dynamodb_account_max_table_level_reads", "aws_dynamodb_account_max_table_level_writes", "aws_dynamodb_account_max_reads", "aws_dynamodb_account_max_writes"], "expressionLabels": ["table-level reads", "table-level writes", "reads", "writes"], "span": 6, "rowSpan": 2 },
+      { "id": "account_util", "title": "Account Capacity Utilization", "type": "line", "unit": "%", "expressions": ["aws_dynamodb_account_provisioned_read_capacity_utilization", "aws_dynamodb_account_provisioned_write_capacity_utilization"], "expressionLabels": ["read", "write"], "span": 6, "rowSpan": 2 }
+    ],
+    "endpoint": [
+      { "id": "throttled", "title": "Throttled Requests", "type": "line", "expressions": ["aws_dynamodb_endpoint_read_throttled_requests", "aws_dynamodb_endpoint_write_throttled_requests"], "expressionLabels": ["read", "write"], "span": 4, "rowSpan": 2 },
+      { "id": "throttle_events", "title": "Throttle Events", "type": "line", "expressions": ["aws_dynamodb_endpoint_read_throttle_events", "aws_dynamodb_endpoint_write_throttle_events"], "expressionLabels": ["read", "write"], "span": 4, "rowSpan": 2 },
+      { "id": "sys_errors", "title": "System Errors", "type": "line", "expressions": ["aws_dynamodb_endpoint_read_system_errors", "aws_dynamodb_endpoint_write_system_errors"], "expressionLabels": ["read", "write"], "span": 4, "rowSpan": 2 },
+      { "id": "cond_check", "title": "Conditional Check Failed", "type": "line", "expressions": ["aws_dynamodb_endpoint_conditional_check_failed_requests"], "span": 4, "rowSpan": 2 },
+      { "id": "txn_conflict", "title": "Transaction Conflict", "type": "line", "expressions": ["aws_dynamodb_endpoint_transaction_conflict"], "span": 4, "rowSpan": 2 },
+      { "id": "ttl_deleted", "title": "TTL Deleted Items", "type": "line", "expressions": ["aws_dynamodb_endpoint_time_to_live_deleted_item_count"], "span": 4, "rowSpan": 2 },
+      { "id": "read_usage", "title": "Read Capacity", "type": "line", "expressions": ["aws_dynamodb_endpoint_provisioned_read_capacity_units", "aws_dynamodb_endpoint_consumed_read_capacity_units"], "expressionLabels": ["provisioned", "consumed"], "span": 6, "rowSpan": 2 },
+      { "id": "write_usage", "title": "Write Capacity", "type": "line", "expressions": ["aws_dynamodb_endpoint_provisioned_write_capacity_units", "aws_dynamodb_endpoint_consumed_write_capacity_units"], "expressionLabels": ["provisioned", "consumed"], "span": 6, "rowSpan": 2 },
+      { "id": "latency", "title": "Successful Request Latency (ms)", "type": "line", "unit": "ms", "expressions": ["aws_dynamodb_endpoint_get_successful_request_latency", "aws_dynamodb_endpoint_put_successful_request_latency", "aws_dynamodb_endpoint_query_successful_request_latency", "aws_dynamodb_endpoint_scan_successful_request_latency"], "expressionLabels": ["get", "put", "query", "scan"], "span": 12, "rowSpan": 2 },
+      { "id": "scan_items", "title": "Scan Returned Items", "type": "line", "expressions": ["aws_dynamodb_endpoint_scan_returned_item_count"], "span": 6, "rowSpan": 2 },
+      { "id": "query_items", "title": "Query Returned Items", "type": "line", "expressions": ["aws_dynamodb_endpoint_query_returned_item_count"], "span": 6, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/aws_eks.json b/apps/bff/src/bundled_templates/layers/aws_eks.json
new file mode 100644
index 0000000..d4af024
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/aws_eks.json
@@ -0,0 +1,46 @@
+{
+  "key": "AWS_EKS",
+  "alias": "AWS EKS",
+  "color": "var(--sw-warn)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-aws-eks-monitoring/",
+  "aliases": { "services": "EKS services", "instances": "Nodes" },
+  "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "podCpu",
+    "columns": [
+      { "metric": "podCpu", "label": "Pod CPU %", "unit": "%", "mqe": "avg(eks_cluster_service_pod_cpu_utilization)", "aggregation": "avg" },
+      { "metric": "podMem", "label": "Pod Mem %", "unit": "%", "mqe": "avg(eks_cluster_service_pod_memory_utilization)", "aggregation": "avg" },
+      { "metric": "txErr", "label": "TX Err", "mqe": "avg(eks_cluster_service_pod_net_tx_error)", "aggregation": "avg" },
+      { "metric": "rxErr", "label": "RX Err", "mqe": "avg(eks_cluster_service_pod_net_rx_error)", "aggregation": "avg" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Resource", "size": "auto", "metrics": [
+        { "id": "podCpu", "label": "Pod CPU %", "tip": "Service pods CPU utilization.", "mqe": "avg(eks_cluster_service_pod_cpu_utilization)", "unit": "%", "aggregation": "avg" },
+        { "id": "podMem", "label": "Pod Mem %", "tip": "Service pods memory utilization.", "mqe": "avg(eks_cluster_service_pod_memory_utilization)", "unit": "%", "aggregation": "avg" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "pod_cpu", "title": "Pod CPU Utilization (%)", "type": "line", "unit": "%", "expressions": ["eks_cluster_service_pod_cpu_utilization"], "span": 4, "rowSpan": 2 },
+      { "id": "pod_mem", "title": "Pod Memory Utilization (%)", "type": "line", "unit": "%", "expressions": ["eks_cluster_service_pod_memory_utilization"], "span": 4, "rowSpan": 2 },
+      { "id": "pod_net_rx", "title": "Pod Network RX (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["eks_cluster_service_pod_net_rx_bytes/1024"], "span": 4, "rowSpan": 2 },
+      { "id": "pod_net_rx_err", "title": "Pod RX Errors / s", "type": "line", "expressions": ["eks_cluster_service_pod_net_rx_error"], "span": 4, "rowSpan": 2 },
+      { "id": "pod_net_tx", "title": "Pod Network TX (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["eks_cluster_service_pod_net_tx_bytes/1024"], "span": 4, "rowSpan": 2 },
+      { "id": "pod_net_tx_err", "title": "Pod TX Errors / s", "type": "line", "expressions": ["eks_cluster_service_pod_net_tx_error"], "span": 4, "rowSpan": 2 }
+    ],
+    "instance": [
+      { "id": "pods", "title": "Pod Count", "type": "line", "expressions": ["eks_cluster_node_pod_number"], "span": 3, "rowSpan": 2 },
+      { "id": "node_cpu", "title": "CPU Utilization (%)", "type": "line", "unit": "%", "expressions": ["eks_cluster_node_cpu_utilization"], "span": 3, "rowSpan": 2 },
+      { "id": "node_mem", "title": "Memory Utilization (%)", "type": "line", "unit": "%", "expressions": ["eks_cluster_node_memory_utilization"], "span": 3, "rowSpan": 2 },
+      { "id": "node_fs", "title": "FS Utilization (%)", "type": "line", "unit": "%", "expressions": ["eks_cluster_node_fs_utilization"], "span": 3, "rowSpan": 2 },
+      { "id": "node_net_rx", "title": "Network RX (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["eks_cluster_node_net_rx_bytes/1024", "eks_cluster_node_net_rx_error"], "expressionLabels": ["bytes", "errors"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+      { "id": "node_net_tx", "title": "Network TX (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["eks_cluster_node_net_tx_bytes/1024", "eks_cluster_node_net_tx_error"], "expressionLabels": ["bytes", "errors"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+      { "id": "disk", "title": "Disk IO (B/s)", "type": "line", "expressions": ["eks_cluster_node_disk_io_read", "eks_cluster_node_disk_io_write"], "expressionLabels": ["read", "write"], "span": 6, "rowSpan": 2 },
+      { "id": "pod_cpu", "title": "Pod CPU on Node", "type": "line", "unit": "%", "expressions": ["eks_cluster_node_pod_cpu_utilization"], "span": 6, "rowSpan": 2 },
+      { "id": "pod_mem", "title": "Pod Memory on Node", "type": "line", "unit": "%", "expressions": ["eks_cluster_node_pod_memory_utilization"], "span": 6, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/aws_gateway.json b/apps/bff/src/bundled_templates/layers/aws_gateway.json
new file mode 100644
index 0000000..2571ed5
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/aws_gateway.json
@@ -0,0 +1,69 @@
+{
+  "key": "AWS_GATEWAY",
+  "alias": "AWS API Gateway",
+  "color": "var(--sw-warn)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-aws-api-gateway-monitoring/",
+  "aliases": {
+    "services": "AWS Gateways",
+    "endpoints": "Routes"
+  },
+  "components": {
+    "service": true,
+    "instances": false,
+    "endpoints": true,
+    "topology": false,
+    "traces": false,
+    "logs": false
+  },
+  "layer-header": {
+    "orderBy": "count",
+    "columns": [
+      { "metric": "count", "label": "Requests", "mqe": "aws_gateway_service_count", "aggregation": "sum" },
+      { "metric": "latency", "label": "Latency", "unit": "ms", "mqe": "aws_gateway_service_latency", "aggregation": "avg" },
+      { "metric": "err4xx", "label": "4xx", "mqe": "aws_gateway_service_4xx", "aggregation": "sum" },
+      { "metric": "err5xx", "label": "5xx", "mqe": "aws_gateway_service_5xx", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      {
+        "title": "Errors",
+        "size": "auto",
+        "metrics": [
+          { "id": "err4xx", "label": "4xx", "tip": "Client-error count from CloudWatch.", "mqe": "aws_gateway_service_4xx", "aggregation": "sum" },
+          { "id": "err5xx", "label": "5xx", "tip": "Server-error count from CloudWatch.", "mqe": "aws_gateway_service_5xx", "aggregation": "sum" },
+          { "id": "latency", "label": "Avg Latency", "tip": "Average end-to-end latency.", "mqe": "aws_gateway_service_latency", "unit": "ms", "aggregation": "avg" }
+        ]
+      },
+      {
+        "title": "Throughput",
+        "size": "square",
+        "metrics": [
+          { "id": "count", "label": "Requests", "tip": "Total request count in the window.", "mqe": "aws_gateway_service_count", "aggregation": "sum" }
+        ]
+      }
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "count", "title": "Request Count", "type": "line", "expressions": ["aws_gateway_service_count"], "span": 4, "rowSpan": 2 },
+      { "id": "err4xx", "title": "4xx Count", "type": "line", "expressions": ["aws_gateway_service_4xx"], "span": 4, "rowSpan": 2 },
+      { "id": "err5xx", "title": "5xx Count", "type": "line", "expressions": ["aws_gateway_service_5xx"], "span": 4, "rowSpan": 2 },
+      { "id": "latency", "title": "Request Avg Latency", "type": "line", "unit": "ms", "expressions": ["aws_gateway_service_latency"], "span": 6, "rowSpan": 2 },
+      { "id": "integration_latency", "title": "Integration Avg Latency", "tip": "Latency between the gateway and the backend integration.", "type": "line", "unit": "ms", "expressions": ["aws_gateway_service_integration_latency"], "span": 6, "rowSpan": 2 },
+      { "id": "data", "title": "Data Processed (HTTP API only)", "type": "line", "unit": "KB", "expressions": ["aws_gateway_service_data_processed/1024"], "span": 4, "rowSpan": 2 },
+      { "id": "cache_hit", "title": "Cache Hit Rate (REST API only)", "type": "line", "unit": "%", "expressions": ["aws_gateway_service_cache_hit_rate"], "span": 4, "rowSpan": 2 },
+      { "id": "cache_miss", "title": "Cache Miss Rate (REST API only)", "type": "line", "unit": "%", "expressions": ["aws_gateway_service_cache_miss_rate"], "span": 4, "rowSpan": 2 }
+    ],
+    "endpoint": [
+      { "id": "count", "title": "Request Count", "type": "line", "expressions": ["aws_gateway_endpoint_count"], "span": 4, "rowSpan": 2 },
+      { "id": "err4xx", "title": "4xx Count", "type": "line", "expressions": ["aws_gateway_endpoint_4xx"], "span": 4, "rowSpan": 2 },
+      { "id": "err5xx", "title": "5xx Count", "type": "line", "expressions": ["aws_gateway_endpoint_5xx"], "span": 4, "rowSpan": 2 },
+      { "id": "latency", "title": "Request Avg Latency", "type": "line", "unit": "ms", "expressions": ["aws_gateway_endpoint_latency"], "span": 6, "rowSpan": 2 },
+      { "id": "integration_latency", "title": "Integration Avg Latency", "type": "line", "unit": "ms", "expressions": ["aws_gateway_endpoint_integration_latency"], "span": 6, "rowSpan": 2 },
+      { "id": "data", "title": "Data Processed", "type": "line", "unit": "KB", "expressions": ["aws_gateway_endpoint_data_processed/1024"], "span": 4, "rowSpan": 2 },
+      { "id": "cache_hit", "title": "Cache Hit Rate", "type": "line", "unit": "%", "expressions": ["aws_gateway_endpoint_cache_hit_rate"], "span": 4, "rowSpan": 2 },
+      { "id": "cache_miss", "title": "Cache Miss Rate", "type": "line", "unit": "%", "expressions": ["aws_gateway_endpoint_cache_miss_rate"], "span": 4, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/aws_s3.json b/apps/bff/src/bundled_templates/layers/aws_s3.json
new file mode 100644
index 0000000..22a0ca6
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/aws_s3.json
@@ -0,0 +1,40 @@
+{
+  "key": "AWS_S3",
+  "alias": "AWS S3",
+  "color": "var(--sw-warn)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-aws-s3-monitoring/",
+  "aliases": { "services": "S3 buckets" },
+  "components": { "service": true, "instances": false, "endpoints": false, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "all",
+    "columns": [
+      { "metric": "all", "label": "Requests", "mqe": "latest(aws_s3_all_requests)", "aggregation": "sum" },
+      { "metric": "latency", "label": "Avg Latency", "unit": "ms", "mqe": "avg(aws_s3_request_latency)", "aggregation": "avg" },
+      { "metric": "err4xx", "label": "4xx", "mqe": "latest(aws_s3_4xx)", "aggregation": "sum" },
+      { "metric": "err5xx", "label": "5xx", "mqe": "latest(aws_s3_5xx)", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Errors", "size": "auto", "metrics": [
+        { "id": "err4xx", "label": "4xx Count", "tip": "Client errors observed by CloudWatch.", "mqe": "latest(aws_s3_4xx)", "aggregation": "sum" },
+        { "id": "err5xx", "label": "5xx Count", "tip": "Server errors observed by CloudWatch.", "mqe": "latest(aws_s3_5xx)", "aggregation": "sum" },
+        { "id": "latency", "label": "Avg Latency", "tip": "Average request latency.", "mqe": "avg(aws_s3_request_latency)", "unit": "ms", "aggregation": "avg" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "all_req", "title": "All Request Count", "type": "line", "expressions": ["aws_s3_all_requests"], "span": 3, "rowSpan": 2 },
+      { "id": "get_req", "title": "GET Request Count", "type": "line", "expressions": ["aws_s3_get_requests"], "span": 3, "rowSpan": 2 },
+      { "id": "put_req", "title": "PUT Request Count", "type": "line", "expressions": ["aws_s3_put_requests"], "span": 3, "rowSpan": 2 },
+      { "id": "delete_req", "title": "DELETE Request Count", "type": "line", "expressions": ["aws_s3_delete_requests"], "span": 3, "rowSpan": 2 },
+      { "id": "err4xx", "title": "4xx Count", "type": "line", "expressions": ["aws_s3_4xx"], "span": 3, "rowSpan": 2 },
+      { "id": "err5xx", "title": "5xx Count", "type": "line", "expressions": ["aws_s3_5xx"], "span": 3, "rowSpan": 2 },
+      { "id": "latency", "title": "Request Avg Latency", "type": "line", "unit": "ms", "expressions": ["aws_s3_request_latency"], "span": 3, "rowSpan": 2 },
+      { "id": "first_byte_latency", "title": "First Byte Avg Latency", "type": "line", "unit": "ms", "expressions": ["aws_s3_first_latency_bytes"], "span": 3, "rowSpan": 2 },
+      { "id": "downloaded", "title": "Downloaded (KB)", "type": "line", "unit": "KB", "expressions": ["aws_s3_downloaded_bytes"], "span": 6, "rowSpan": 2 },
+      { "id": "uploaded", "title": "Uploaded (KB)", "type": "line", "unit": "KB", "expressions": ["aws_s3_uploaded_bytes"], "span": 6, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/banyandb.json b/apps/bff/src/bundled_templates/layers/banyandb.json
new file mode 100644
index 0000000..3e11ae8
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/banyandb.json
@@ -0,0 +1,69 @@
+{
+  "key": "BANYANDB",
+  "alias": "BanyanDB",
+  "color": "var(--sw-cyan)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/banyandb/banyandb-introduction/",
+  "aliases": { "services": "BanyanDB clusters", "instances": "Nodes" },
+  "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "writeRate",
+    "columns": [
+      { "metric": "writeRate", "label": "Write/s", "mqe": "latest(meter_banyandb_write_rate)", "aggregation": "sum" },
+      { "metric": "queryRate", "label": "Query/s", "mqe": "latest(meter_banyandb_query_rate{method='query'})", "aggregation": "sum" },
+      { "metric": "errRate", "label": "Err/s", "mqe": "latest(meter_banyandb_write_and_query_errors_rate)", "aggregation": "sum" },
+      { "metric": "active", "label": "Active", "mqe": "meter_banyandb_active_instance", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Throughput", "size": "auto", "metrics": [
+        { "id": "writeRate", "label": "Write/s", "tip": "Writes per second across the cluster.", "mqe": "latest(meter_banyandb_write_rate)", "aggregation": "sum" },
+        { "id": "queryRate", "label": "Query/s", "tip": "Queries per second.", "mqe": "latest(meter_banyandb_query_rate{method='query'})", "aggregation": "sum" },
+        { "id": "errRate", "label": "Errors/s", "tip": "Combined write + query error rate.", "mqe": "latest(meter_banyandb_write_and_query_errors_rate)", "aggregation": "sum" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "write_rate", "title": "Write Rate", "type": "line", "expressions": ["latest(meter_banyandb_write_rate)"], "span": 4, "rowSpan": 2 },
+      { "id": "query_rate", "title": "Query Rate", "type": "line", "expressions": ["latest(meter_banyandb_query_rate{method='query'})"], "span": 4, "rowSpan": 2 },
+      { "id": "err_rate", "title": "Write/Query Error Rate", "type": "line", "expressions": ["latest(meter_banyandb_write_and_query_errors_rate)"], "span": 4, "rowSpan": 2 },
+      { "id": "memory", "title": "Total Memory (GB)", "type": "line", "unit": "GB", "expressions": ["latest(meter_banyandb_total_memory/1024/1024/1024)"], "span": 4, "rowSpan": 2 },
+      { "id": "disk", "title": "Disk Usage (GB)", "type": "line", "unit": "GB", "expressions": ["latest(meter_banyandb_disk_usage/1024/1024/1024)"], "span": 4, "rowSpan": 2 },
+      { "id": "cpu", "title": "Total CPU", "type": "line", "expressions": ["latest(meter_banyandb_total_cpu)"], "span": 4, "rowSpan": 2 },
+      { "id": "active", "title": "Active Instances", "type": "line", "expressions": ["meter_banyandb_active_instance"], "span": 4, "rowSpan": 2 },
+      { "id": "etcd", "title": "Etcd Operation Rate", "type": "line", "expressions": ["latest(meter_banyandb_etcd_operation_rate)"], "span": 4, "rowSpan": 2 },
+      { "id": "cpu_usage", "title": "CPU Usage", "type": "line", "expressions": ["meter_banyandb_cpu_usage/1000"], "span": 4, "rowSpan": 2 },
+      { "id": "rss", "title": "RSS Memory", "type": "line", "expressions": ["meter_banyandb_rss_memory_usage/1000"], "span": 4, "rowSpan": 2 },
+      { "id": "disk_all", "title": "Disk Usage (all)", "type": "line", "expressions": ["meter_banyandb_disk_usage_all/1000"], "span": 4, "rowSpan": 2 },
+      { "id": "network", "title": "Network (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["meter_banyandb_network_usage_recv/1024", "meter_banyandb_network_usage_sent/1024"], "expressionLabels": ["recv", "sent"], "span": 4, "rowSpan": 2 },
+      { "id": "merge", "title": "Merge File Latency", "type": "line", "expressions": ["meter_banyandb_merge_file_latency/1000"], "span": 4, "rowSpan": 2 },
+      { "id": "merge_data", "title": "Merge File Data", "type": "line", "expressions": ["meter_banyandb_merge_file_data"], "span": 4, "rowSpan": 2 },
+      { "id": "merge_parts", "title": "Merge File Partitions", "type": "line", "expressions": ["meter_banyandb_merge_file_partitions"], "span": 4, "rowSpan": 2 },
+      { "id": "stream_write", "title": "Stream Write Rate", "type": "line", "expressions": ["meter_banyandb_stream_write_rate"], "span": 4, "rowSpan": 2 },
+      { "id": "group_write", "title": "Group Write Rate", "type": "line", "expressions": ["meter_banyandb_storage_write_rate/1000"], "span": 4, "rowSpan": 2 },
+      { "id": "series_write", "title": "Series Write Rate", "type": "line", "expressions": ["meter_banyandb_series_write_rate/1000"], "span": 4, "rowSpan": 2 },
+      { "id": "query_latency", "title": "Query Latency", "type": "line", "expressions": ["meter_banyandb_query_latency/1000"], "span": 4, "rowSpan": 2 },
+      { "id": "totals", "title": "Total Data + Series + Documents", "type": "line", "expressions": ["meter_banyandb_total_data", "meter_banyandb_total_series", "meter_banyandb_total_document"], "expressionLabels": ["data", "series", "documents"], "span": 8, "rowSpan": 2 },
+      { "id": "term_search", "title": "Term Search Rate", "type": "line", "expressions": ["meter_banyandb_term_search_rate/1000"], "span": 4, "rowSpan": 2 }
+    ],
+    "instance": [
+      { "id": "write_rate", "title": "Write Rate", "type": "line", "expressions": ["latest(meter_banyandb_instance_write_rate)"], "span": 4, "rowSpan": 2 },
+      { "id": "query_rate", "title": "Query Rate", "type": "line", "expressions": ["latest(meter_banyandb_instance_query_rate{method='query'})"], "span": 4, "rowSpan": 2 },
+      { "id": "err_rate", "title": "Error Rate", "type": "line", "expressions": ["latest(meter_banyandb_instance_write_and_query_errors_rate)"], "span": 4, "rowSpan": 2 },
+      { "id": "memory", "title": "Total Memory (GB)", "type": "line", "unit": "GB", "expressions": ["latest(meter_banyandb_instance_total_memory/1024/1024/1024)"], "span": 4, "rowSpan": 2 },
+      { "id": "disk", "title": "Disk Usage (GB)", "type": "line", "unit": "GB", "expressions": ["latest(meter_banyandb_instance_disk_usage/1024/1024/1024)"], "span": 4, "rowSpan": 2 },
+      { "id": "cpu", "title": "CPU", "type": "line", "expressions": ["meter_banyandb_instance_cpu_usage/1000"], "span": 4, "rowSpan": 2 },
+      { "id": "etcd", "title": "Etcd Operation Rate", "type": "line", "expressions": ["latest(meter_banyandb_instance_etcd_operation_rate)"], "span": 4, "rowSpan": 2 },
+      { "id": "rss", "title": "RSS Memory", "type": "line", "expressions": ["meter_banyandb_instance_rss_memory_usage/1000"], "span": 4, "rowSpan": 2 },
+      { "id": "network", "title": "Network (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["meter_banyandb_instance_network_usage_recv/1024", "meter_banyandb_instance_network_usage_sent/1024"], "expressionLabels": ["recv", "sent"], "span": 4, "rowSpan": 2 },
+      { "id": "totals", "title": "Total Data + Series", "type": "line", "expressions": ["meter_banyandb_instance_total_data", "meter_banyandb_instance_total_series"], "expressionLabels": ["data", "series"], "span": 6, "rowSpan": 2 },
+      { "id": "merge", "title": "Merge File Latency / Data / Partitions", "type": "line", "expressions": ["meter_banyandb_instance_merge_file_latency/1000", "meter_banyandb_instance_merge_file_data", "meter_banyandb_instance_merge_file_partitions"], "expressionLabels": ["latency", "data", "partitions"], "span": 6, "rowSpan": 2 },
+      { "id": "stream_write", "title": "Stream Write Rate", "type": "line", "expressions": ["meter_banyandb_instance_stream_write_rate"], "span": 4, "rowSpan": 2 },
+      { "id": "group_write", "title": "Group Write Rate", "type": "line", "expressions": ["meter_banyandb_instance_storage_write_rate/1000"], "span": 4, "rowSpan": 2 },
+      { "id": "series_write", "title": "Series Write Rate", "type": "line", "expressions": ["meter_banyandb_instance_series_write_rate/1000"], "span": 4, "rowSpan": 2 },
+      { "id": "query_latency", "title": "Query Latency", "type": "line", "expressions": ["meter_banyandb_instance_query_latency/1000"], "span": 6, "rowSpan": 2 },
+      { "id": "term_search", "title": "Term Search Rate", "type": "line", "expressions": ["meter_banyandb_instance_term_search_rate/1000"], "span": 6, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/bookkeeper.json b/apps/bff/src/bundled_templates/layers/bookkeeper.json
new file mode 100644
index 0000000..e5ce3bb
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/bookkeeper.json
@@ -0,0 +1,45 @@
+{
+  "key": "BOOKKEEPER",
+  "alias": "BookKeeper",
+  "color": "var(--sw-purple)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-bookkeeper-monitoring/",
+  "aliases": { "services": "BookKeeper clusters", "instances": "Bookies" },
+  "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "ledgers",
+    "columns": [
+      { "metric": "ledgers", "label": "Ledgers", "mqe": "aggregate_labels(meter_bookkeeper_bookie_ledgers_count,sum)", "aggregation": "sum" },
+      { "metric": "entries", "label": "Entries", "mqe": "aggregate_labels(meter_bookkeeper_bookie_entries_count,sum)", "aggregation": "sum" },
+      { "metric": "writableDirs", "label": "Writable Dirs", "mqe": "aggregate_labels(meter_bookkeeper_bookie_ledger_writable_dirs,sum)", "aggregation": "sum" },
+      { "metric": "dirUsage", "label": "Dir Usage", "mqe": "aggregate_labels(meter_bookkeeper_bookie_ledger_dir_data_bookkeeper_ledgers_usage,sum)", "aggregation": "avg" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Throughput", "size": "auto", "metrics": [
+        { "id": "writeRate", "label": "Write B/s", "tip": "Bookie write throughput across the cluster.", "mqe": "aggregate_labels(meter_bookkeeper_bookie_write_rate,sum)", "aggregation": "sum" },
+        { "id": "readRate", "label": "Read B/s", "tip": "Bookie read throughput across the cluster.", "mqe": "aggregate_labels(meter_bookkeeper_bookie_read_rate,sum)", "aggregation": "sum" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "ledgers", "title": "Bookie Ledgers", "type": "line", "expressions": ["aggregate_labels(meter_bookkeeper_bookie_ledgers_count,sum)"], "span": 4, "rowSpan": 2 },
+      { "id": "entries", "title": "Bookie Entries", "type": "line", "expressions": ["aggregate_labels(meter_bookkeeper_bookie_entries_count,sum)"], "span": 4, "rowSpan": 2 },
+      { "id": "writable_dirs", "title": "Writable Ledger Dirs", "type": "line", "expressions": ["aggregate_labels(meter_bookkeeper_bookie_ledger_writable_dirs,sum)"], "span": 4, "rowSpan": 2 },
+      { "id": "write_cache", "title": "Write Cache", "type": "line", "expressions": ["aggregate_labels(meter_bookkeeper_bookie_write_cache_size,sum)/1024/1024", "aggregate_labels(meter_bookkeeper_bookie_write_cache_count,sum)"], "expressionLabels": ["size (MB)", "entries"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+      { "id": "read_cache", "title": "Read Cache", "type": "line", "expressions": ["aggregate_labels(meter_bookkeeper_bookie_read_cache_size,sum)/1024/1024", "aggregate_labels(meter_bookkeeper_bookie_read_cache_count,sum)"], "expressionLabels": ["size (MB)", "entries"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+      { "id": "io_rates", "title": "Read / Write Rate (B/s)", "type": "line", "expressions": ["aggregate_labels(meter_bookkeeper_bookie_read_rate,sum)", "aggregate_labels(meter_bookkeeper_bookie_write_rate,sum)"], "expressionLabels": ["read", "write"], "span": 6, "rowSpan": 2 },
+      { "id": "dir_usage", "title": "Ledger Dir Usage", "type": "line", "expressions": ["aggregate_labels(meter_bookkeeper_bookie_ledger_dir_data_bookkeeper_ledgers_usage,sum)"], "span": 6, "rowSpan": 2 }
+    ],
+    "instance": [
+      { "id": "jvm_pool", "title": "JVM Memory Pool (MB)", "type": "line", "unit": "MB", "expressions": ["meter_bookkeeper_node_jvm_memory_pool_used/1024/1024"], "span": 6, "rowSpan": 2 },
+      { "id": "jvm_mem", "title": "JVM Memory (MB)", "type": "line", "unit": "MB", "expressions": ["meter_bookkeeper_node_jvm_memory_used/1024/1024", "meter_bookkeeper_node_jvm_memory_committed/1024/1024", "meter_bookkeeper_node_jvm_memory_init/1024/1024"], "expressionLabels": ["used", "committed", "init"], "span": 6, "rowSpan": 2 },
+      { "id": "jvm_threads", "title": "JVM Threads", "type": "line", "expressions": ["meter_bookkeeper_node_jvm_threads_current", "meter_bookkeeper_node_jvm_threads_daemon", "meter_bookkeeper_node_jvm_threads_peak", "meter_bookkeeper_node_jvm_threads_deadlocked"], "expressionLabels": ["current", "daemon", "peak", "deadlocked"], "span": 6, "rowSpan": 2 },
+      { "id": "gc", "title": "GC", "type": "line", "expressions": ["meter_bookkeeper_node_jvm_gc_collection_seconds_sum", "meter_bookkeeper_node_jvm_gc_collection_seconds_count"], "expressionLabels": ["seconds", "count"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+      { "id": "thread_executor", "title": "Thread Executor", "type": "line", "expressions": ["meter_bookkeeper_node_thread_executor_completed", "meter_bookkeeper_node_thread_executor_tasks_completed", "meter_bookkeeper_node_thread_executor_tasks_rejected", "meter_bookkeeper_node_thread_executor_tasks_failed"], "expressionLabels": ["completed", "tasks completed", "rejected", "failed"], "span": 6, "rowSpan": 2 },
+      { "id": "pooled_threads", "title": "Pooled Threads", "type": "line", "expressions": ["meter_bookkeeper_node_high_priority_threads", "meter_bookkeeper_node_read_thread_pool_threads"], "expressionLabels": ["high-priority", "read"], "span": 6, "rowSpan": 2 },
+      { "id": "queue_size", "title": "Pool Max Queue Size", "type": "line", "expressions": ["meter_bookkeeper_node_high_priority_thread_max_queue_size", "meter_bookkeeper_node_read_thread_pool_max_queue_size"], "expressionLabels": ["high-priority", "read"], "span": 12, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/browser.json b/apps/bff/src/bundled_templates/layers/browser.json
new file mode 100644
index 0000000..3c31b33
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/browser.json
@@ -0,0 +1,244 @@
+{
+  "key": "BROWSER",
+  "alias": "Browser",
+  "color": "var(--sw-cyan)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/service-agent/browser-agent/",
+  "aliases": {
+    "services": "Apps",
+    "instances": "Versions",
+    "endpoints": "Pages"
+  },
+  "components": {
+    "service": true,
+    "instances": true,
+    "endpoints": true,
+    "topology": false,
+    "traces": true,
+    "logs": true
+  },
+  "layer-header": {
+    "orderBy": "pv",
+    "columns": [
+      { "metric": "pv", "label": "Page Views", "unit": "/min", "mqe": "browser_app_pv", "aggregation": "sum" },
+      { "metric": "errorRate", "label": "Error Rate", "unit": "%", "mqe": "browser_app_error_rate/100", "aggregation": "avg" },
+      { "metric": "errorSum", "label": "Errors", "mqe": "browser_app_error_sum", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      {
+        "title": "Page health",
+        "size": "auto",
+        "metrics": [
+          { "id": "fmp_p95", "label": "FMP P95", "tip": "95th-percentile first-meaningful-paint latency across the app.", "mqe": "browser_app_page_fmp_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+          { "id": "errorRate", "label": "Error Rate", "tip": "Percentage of page loads that produced an error.", "mqe": "browser_app_error_rate/100", "unit": "%", "aggregation": "avg" }
+        ]
+      },
+      {
+        "title": "Throughput",
+        "size": "square",
+        "metrics": [
+          { "id": "pv", "label": "PV / min", "tip": "Page views per minute.", "mqe": "browser_app_pv", "unit": "/min", "aggregation": "sum" }
+        ]
+      }
+    ]
+  },
+  "dashboards": {
+    "service": [
+      {
+        "id": "app_pv",
+        "title": "App Load (PV)",
+        "type": "line",
+        "unit": "/min",
+        "expressions": ["browser_app_pv"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "app_error_rate",
+        "title": "App Error Rate",
+        "type": "line",
+        "unit": "%",
+        "expressions": ["browser_app_error_rate/100"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "app_error_sum",
+        "title": "App Error Count",
+        "type": "line",
+        "expressions": ["browser_app_error_sum"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "top_pages_pv",
+        "title": "Top Hot Pages",
+        "tip": "Pages ranked by page views, errors, and error rate.",
+        "type": "top",
+        "expressions": [
+          "top_n(browser_app_page_pv,10,des)",
+          "top_n(browser_app_page_error_sum,10,des)",
+          "top_n(browser_app_page_error_rate,10,des)/100"
+        ],
+        "expressionLabels": ["PV", "Errors", "Error Rate"],
+        "expressionUnits": ["/min", "", "%"],
+        "span": 6,
+        "rowSpan": 3
+      },
+      {
+        "id": "top_versions",
+        "title": "Top Versions",
+        "tip": "App version breakdown — load + errors + error rate.",
+        "type": "top",
+        "expressions": [
+          "top_n(browser_app_single_version_pv,10,des)",
+          "top_n(browser_app_single_version_error_sum,10,des)",
+          "top_n(browser_app_single_version_error_rate,10,des)/100"
+        ],
+        "expressionLabels": ["PV", "Errors", "Error Rate"],
+        "expressionUnits": ["/min", "", "%"],
+        "span": 6,
+        "rowSpan": 3
+      }
+    ],
+    "instance": [
+      {
+        "id": "ver_pv",
+        "title": "Version PV",
+        "type": "line",
+        "unit": "/min",
+        "expressions": ["browser_app_single_version_pv"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "ver_error_rate",
+        "title": "Version Error Rate",
+        "type": "line",
+        "unit": "%",
+        "expressions": ["browser_app_single_version_error_rate/100"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "ver_error_sum",
+        "title": "Version Error Count",
+        "type": "line",
+        "expressions": ["browser_app_single_version_error_sum"],
+        "span": 4,
+        "rowSpan": 2
+      }
+    ],
+    "endpoint": [
+      {
+        "id": "page_fmp_percentile",
+        "title": "First Meaningful Paint Percentile",
+        "tip": "p50–p99 of FMP latency for the selected page. Below 1s for p75 is a common target.",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(browser_app_page_fmp_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "page_load_percentile",
+        "title": "Page Load Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(browser_app_page_load_page_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "page_ttl_percentile",
+        "title": "Time-to-Live Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(browser_app_page_ttl_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "page_first_pack_percentile",
+        "title": "First Pack Latency Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(browser_app_page_first_pack_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "page_performance",
+        "title": "Page Performance Breakdown",
+        "tip": "Avg time spent in each phase of page load — resolve, redirect, DNS, TCP, TTFB, transfer, DOM analysis, DOM ready, FPT, full load.",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "browser_app_page_dns_avg",
+          "browser_app_page_redirect_avg",
+          "browser_app_page_tcp_avg",
+          "browser_app_page_ttfb_avg",
+          "browser_app_page_trans_avg",
+          "browser_app_page_dom_analysis_avg",
+          "browser_app_page_dom_ready_avg",
+          "browser_app_page_fpt_avg",
+          "browser_app_page_load_page_avg",
+          "browser_app_page_res_avg"
+        ],
+        "expressionLabels": ["dns", "redirect", "tcp", "ttfb", "transfer", "dom analysis", "dom ready", "fpt", "load", "resource"],
+        "span": 12,
+        "rowSpan": 3
+      },
+      {
+        "id": "page_error_breakdown",
+        "title": "Page Errors by Type",
+        "tip": "Resource / JS / AJAX / unknown error counters per minute.",
+        "type": "line",
+        "expressions": [
+          "browser_app_page_resource_error_sum",
+          "browser_app_page_js_error_sum",
+          "browser_app_page_ajax_error_sum",
+          "browser_app_page_unknown_error_sum"
+        ],
+        "expressionLabels": ["resource", "js", "ajax", "unknown"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "web_vitals",
+        "title": "Web Vitals",
+        "tip": "Core Web Vitals — FMP, LCP, CLS (avg per minute). CLS is divided by 1000 to bring it into the typical 0–1 score range.",
+        "type": "line",
+        "expressions": [
+          "browser_app_web_vitals_fmp_avg",
+          "browser_app_web_vitals_lcp_avg",
+          "browser_app_web_vitals_cls_avg / 1000"
+        ],
+        "expressionLabels": ["FMP (ms)", "LCP (ms)", "CLS"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "inp_percentile",
+        "title": "Interaction to Next Paint Percentile",
+        "tip": "INP — responsiveness metric replacing FID.",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(browser_app_web_interaction_inp_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 12,
+        "rowSpan": 2
+      }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/cilium_service.json b/apps/bff/src/bundled_templates/layers/cilium_service.json
new file mode 100644
index 0000000..327e93f
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/cilium_service.json
@@ -0,0 +1,73 @@
+{
+  "key": "CILIUM_SERVICE",
+  "alias": "Cilium Service",
+  "color": "var(--sw-info)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-k8s-monitoring-cilium/",
+  "aliases": { "services": "Services", "instances": "Pods", "endpoints": "Endpoints" },
+  "components": { "service": true, "instances": true, "endpoints": true, "topology": true, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "cpm",
+    "columns": [
+      { "metric": "cpm", "label": "RPM", "unit": "rpm", "mqe": "cilium_service_protocol_cpm", "aggregation": "sum" },
+      { "metric": "respTime", "label": "Latency", "unit": "ms", "mqe": "cilium_service_protocol_call_duration/cilium_service_protocol_cpm/1000000", "aggregation": "avg" },
+      { "metric": "sla", "label": "Success Rate", "unit": "%", "mqe": "cilium_service_protocol_call_success_count/cilium_service_protocol_cpm*100", "aggregation": "avg" },
+      { "metric": "drop", "label": "TCP Drop", "mqe": "cilium_service_l4_read_pkg_drop_cpm + cilium_service_l4_write_pkg_drop_cpm", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Latency & errors", "size": "auto", "metrics": [
+        { "id": "respTime", "label": "Avg Latency", "tip": "Average protocol latency (ms).", "mqe": "cilium_service_protocol_call_duration/cilium_service_protocol_cpm/1000000", "unit": "ms", "aggregation": "avg" },
+        { "id": "successRate", "label": "Success Rate", "tip": "Successful calls / total calls.", "mqe": "cilium_service_protocol_call_success_count/cilium_service_protocol_cpm*100", "unit": "%", "aggregation": "avg" }
+      ]},
+      { "title": "Throughput", "size": "square", "metrics": [
+        { "id": "cpm", "label": "Protocol RPM", "tip": "Protocol calls per minute.", "mqe": "cilium_service_protocol_cpm", "aggregation": "sum" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "l4_read", "title": "L4 Read Packages/min", "type": "line", "expressions": ["cilium_service_l4_read_pkg_cpm"], "span": 3, "rowSpan": 2 },
+      { "id": "l4_write", "title": "L4 Write Packages/min", "type": "line", "expressions": ["cilium_service_l4_write_pkg_cpm"], "span": 3, "rowSpan": 2 },
+      { "id": "tcp_drop", "title": "TCP Drop / min", "type": "line", "expressions": ["cilium_service_l4_read_pkg_drop_cpm", "cilium_service_l4_write_pkg_drop_cpm"], "expressionLabels": ["read", "write"], "span": 3, "rowSpan": 2 },
+      { "id": "tcp_drop_reason", "title": "TCP Drop by Reason", "type": "line", "expressions": ["cilium_service_l4_drop_reason_count"], "span": 3, "rowSpan": 2 },
+      { "id": "http_load", "title": "HTTP Load", "type": "line", "expressions": ["cilium_service_protocol_http_call_cpm", "cilium_service_protocol_http_call_success_count"], "expressionLabels": ["calls", "success"], "span": 4, "rowSpan": 2 },
+      { "id": "http_duration", "title": "HTTP Duration", "type": "line", "unit": "ms", "expressions": ["cilium_service_protocol_http_call_duration/cilium_service_protocol_http_call_cpm/1000000"], "span": 4, "rowSpan": 2 },
+      { "id": "http_status", "title": "HTTP Non-2xx Status", "type": "line", "expressions": ["cilium_service_protocol_http_status_1xx_cpm", "cilium_service_protocol_http_status_3xx_cpm", "cilium_service_protocol_http_status_4xx_cpm", "cilium_service_protocol_http_status_5xx_cpm"], "expressionLabels": ["1xx", "3xx", "4xx", "5xx"], "span": 4, "rowSpan": 2 },
+      { "id": "dns_load", "title": "DNS Load", "type": "line", "expressions": ["cilium_service_protocol_dns_call_cpm", "cilium_service_protocol_dns_call_success_count"], "expressionLabels": ["calls", "success"], "span": 4, "rowSpan": 2 },
+      { "id": "dns_duration", "title": "DNS Duration", "type": "line", "unit": "ms", "expressions": ["cilium_service_protocol_dns_call_duration/cilium_service_protocol_dns_call_cpm/1000000"], "span": 4, "rowSpan": 2 },
+      { "id": "dns_error", "title": "DNS Errors / min", "type": "line", "expressions": ["cilium_service_protocol_dns_error_count"], "span": 4, "rowSpan": 2 },
+      { "id": "kafka_load", "title": "Kafka Load", "type": "line", "expressions": ["cilium_service_protocol_kafka_call_cpm", "cilium_service_protocol_kafka_call_success_count"], "expressionLabels": ["calls", "success"], "span": 4, "rowSpan": 2 },
+      { "id": "kafka_duration", "title": "Kafka Duration", "type": "line", "unit": "ms", "expressions": ["cilium_service_protocol_kafka_call_duration/cilium_service_protocol_kafka_call_cpm/1000000"], "span": 4, "rowSpan": 2 },
+      { "id": "kafka_error", "title": "Kafka Errors / min", "type": "line", "expressions": ["cilium_service_protocol_kafka_call_error_count"], "span": 4, "rowSpan": 2 }
+    ],
+    "instance": [
+      { "id": "l4_read", "title": "L4 Read Packages/min", "type": "line", "expressions": ["cilium_service_instance_l4_read_pkg_cpm"], "span": 3, "rowSpan": 2 },
+      { "id": "l4_write", "title": "L4 Write Packages/min", "type": "line", "expressions": ["cilium_service_instance_l4_write_pkg_cpm"], "span": 3, "rowSpan": 2 },
+      { "id": "http_load", "title": "HTTP Load", "type": "line", "expressions": ["cilium_service_instance_protocol_http_call_cpm", "cilium_service_instance_protocol_http_call_success_count"], "expressionLabels": ["calls", "success"], "span": 3, "rowSpan": 2 },
+      { "id": "http_status", "title": "HTTP Non-2xx Status", "type": "line", "expressions": ["cilium_service_instance_protocol_http_status_1xx_cpm", "cilium_service_instance_protocol_http_status_3xx_cpm", "cilium_service_instance_protocol_http_status_4xx_cpm", "cilium_service_instance_protocol_http_status_5xx_cpm"], "expressionLabels": ["1xx", "3xx", "4xx", "5xx"], "span": 3, "rowSpan": 2 },
+      { "id": "dns_load", "title": "DNS Load", "type": "line", "expressions": ["cilium_service_instance_protocol_dns_call_cpm", "cilium_service_instance_protocol_dns_call_success_count"], "expressionLabels": ["calls", "success"], "span": 4, "rowSpan": 2 },
+      { "id": "dns_error", "title": "DNS Errors", "type": "line", "expressions": ["cilium_service_instance_protocol_dns_error_count"], "span": 4, "rowSpan": 2 },
+      { "id": "kafka_load", "title": "Kafka Load", "type": "line", "expressions": ["cilium_service_instance_protocol_kafka_call_cpm", "cilium_service_instance_protocol_kafka_call_success_count"], "expressionLabels": ["calls", "success"], "span": 4, "rowSpan": 2 }
+    ],
+    "endpoint": [
+      { "id": "http_load", "title": "HTTP Load", "type": "line", "expressions": ["cilium_endpoint_protocol_http_call_cpm", "cilium_endpoint_protocol_http_call_success_count"], "expressionLabels": ["calls", "success"], "span": 4, "rowSpan": 2 },
+      { "id": "http_duration", "title": "HTTP Duration", "type": "line", "unit": "ms", "expressions": ["cilium_endpoint_protocol_http_call_duration/cilium_endpoint_protocol_http_call_cpm/1000000"], "span": 4, "rowSpan": 2 },
+      { "id": "http_status", "title": "HTTP Non-2xx Status", "type": "line", "expressions": ["cilium_endpoint_protocol_http_status_1xx_cpm", "cilium_endpoint_protocol_http_status_3xx_cpm", "cilium_endpoint_protocol_http_status_4xx_cpm", "cilium_endpoint_protocol_http_status_5xx_cpm"], "expressionLabels": ["1xx", "3xx", "4xx", "5xx"], "span": 4, "rowSpan": 2 },
+      { "id": "dns_load", "title": "DNS Load", "type": "line", "expressions": ["cilium_endpoint_protocol_dns_call_cpm", "cilium_endpoint_protocol_dns_call_success_count"], "expressionLabels": ["calls", "success"], "span": 4, "rowSpan": 2 },
+      { "id": "dns_duration", "title": "DNS Duration", "type": "line", "unit": "ms", "expressions": ["cilium_endpoint_protocol_dns_call_duration/cilium_endpoint_protocol_dns_call_cpm/1000000"], "span": 4, "rowSpan": 2 },
+      { "id": "dns_error", "title": "DNS Errors", "type": "line", "expressions": ["cilium_endpoint_protocol_dns_error_count"], "span": 4, "rowSpan": 2 }
+    ]
+  },
+  "topology": {
+    "nodeMetrics": [
+      { "id": "cpm", "label": "RPM", "mqe": "cilium_service_protocol_cpm", "unit": "rpm", "role": "center", "aggregation": "avg" },
+      { "id": "success", "label": "Success %", "mqe": "cilium_service_protocol_call_success_count/cilium_service_protocol_cpm*100", "unit": "%", "role": "ring", "aggregation": "avg" },
+      { "id": "respTime", "label": "Latency", "mqe": "cilium_service_protocol_call_duration/cilium_service_protocol_cpm/1000000", "unit": "ms", "role": "secondary", "aggregation": "avg" }
+    ],
+    "linkServerMetrics": [
+      { "id": "cpm", "label": "HTTP RPM", "mqe": "cilium_service_relation_server_protocol_http_call_cpm", "unit": "rpm", "role": "lineServer", "aggregation": "avg" },
+      { "id": "respTime", "label": "Avg Latency", "mqe": "cilium_service_relation_server_protocol_http_call_duration/cilium_service_relation_server_protocol_http_call_cpm/1000000", "unit": "ms", "aggregation": "avg" }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/clickhouse.json b/apps/bff/src/bundled_templates/layers/clickhouse.json
new file mode 100644
index 0000000..3d1222a
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/clickhouse.json
@@ -0,0 +1,60 @@
+{
+  "key": "CLICKHOUSE",
+  "alias": "ClickHouse",
+  "color": "var(--sw-cyan)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-clickhouse-monitoring/",
+  "aliases": { "services": "ClickHouse clusters", "instances": "Nodes" },
+  "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "selRate",
+    "columns": [
+      { "metric": "selRate", "label": "Select / s", "mqe": "aggregate_labels(meter_clickhouse_query_select_rate,sum)", "aggregation": "sum" },
+      { "metric": "insRate", "label": "Insert / s", "mqe": "aggregate_labels(meter_clickhouse_query_insert_rate,sum)", "aggregation": "sum" },
+      { "metric": "slowQ", "label": "Slow Reads", "mqe": "aggregate_labels(meter_clickhouse_query_slow,sum)", "aggregation": "sum" },
+      { "metric": "fileOpen", "label": "Open Files", "mqe": "latest(aggregate_labels(meter_clickhouse_file_open,sum))", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Throughput", "size": "auto", "metrics": [
+        { "id": "selRate", "label": "Select QPS", "tip": "Selects per second across the cluster.", "mqe": "aggregate_labels(meter_clickhouse_query_select_rate,sum)", "aggregation": "sum" },
+        { "id": "insRate", "label": "Insert QPS", "tip": "Inserts per second.", "mqe": "aggregate_labels(meter_clickhouse_query_insert_rate,sum)", "aggregation": "sum" },
+        { "id": "slowQ", "label": "Slow Reads", "tip": "Slow file-reads counter.", "mqe": "aggregate_labels(meter_clickhouse_query_slow,sum)", "aggregation": "sum" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "files_open", "title": "Files Open", "type": "line", "expressions": ["latest(aggregate_labels(meter_clickhouse_file_open,sum))"], "span": 3, "rowSpan": 2 },
+      { "id": "qps", "title": "QPS", "type": "line", "expressions": ["aggregate_labels(meter_clickhouse_query_select_rate,sum)", "aggregate_labels(meter_clickhouse_query_insert_rate,sum)"], "expressionLabels": ["select", "insert"], "span": 3, "rowSpan": 2 },
+      { "id": "queries", "title": "Queries", "type": "line", "expressions": ["aggregate_labels(meter_clickhouse_query,sum)", "aggregate_labels(meter_clickhouse_query_select,sum)", "aggregate_labels(meter_clickhouse_query_insert,sum)"], "expressionLabels": ["total", "select", "insert"], "span": 3, "rowSpan": 2 },
+      { "id": "query_time", "title": "Query Time (ms)", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_clickhouse_querytime_microseconds,sum)/aggregate_labels(meter_clickhouse_query,sum)/1000", "aggregate_labels(meter_clickhouse_querytime_select_microseconds,sum)/aggregate_labels(meter_clickhouse_query_select,sum)/1000", "aggregate_labels(meter_clickhouse_querytime_insert_microseconds,sum)/aggregate_labels(meter_clickhouse_query_insert,sum)/1000"], "expressionLabel [...]
+      { "id": "conns", "title": "Connections", "type": "line", "expressions": ["aggregate_labels(meter_clickhouse_tcp_connections,sum)", "aggregate_labels(meter_clickhouse_http_connections,sum)"], "expressionLabels": ["TCP", "HTTP"], "span": 4, "rowSpan": 2 },
+      { "id": "slow", "title": "Slow Reads", "type": "line", "expressions": ["aggregate_labels(meter_clickhouse_query_slow,sum)"], "span": 4, "rowSpan": 2 },
+      { "id": "merge_mut", "title": "Merge / Mutations", "type": "line", "expressions": ["aggregate_labels(meter_clickhouse_background_merge,sum)", "aggregate_labels(meter_clickhouse_mutations,sum)"], "expressionLabels": ["merge", "mutations"], "span": 4, "rowSpan": 2 },
+      { "id": "insert_io", "title": "Insert Throughput", "type": "line", "expressions": ["aggregate_labels(meter_clickhouse_inserted_bytes,sum)", "aggregate_labels(meter_clickhouse_inserted_rows,sum)"], "expressionLabels": ["bytes/s", "rows/s"], "expressionAxes": [0, 1], "span": 4, "rowSpan": 2 },
+      { "id": "delayed", "title": "Delayed Inserts (s)", "type": "line", "expressions": ["aggregate_labels(meter_clickhouse_delayed_inserts,sum)"], "span": 4, "rowSpan": 2 },
+      { "id": "parts_active", "title": "Active Data Parts", "type": "line", "expressions": ["aggregate_labels(meter_clickhouse_parts_active,sum)"], "span": 4, "rowSpan": 2 },
+      { "id": "replicated", "title": "Replicated Fetch / Send", "type": "line", "expressions": ["aggregate_labels(meter_clickhouse_replicated_fetch,sum)", "aggregate_labels(meter_clickhouse_replicated_send,sum)"], "expressionLabels": ["fetch", "send"], "span": 6, "rowSpan": 2 },
+      { "id": "zk", "title": "Zookeeper Activity", "type": "line", "expressions": ["latest(aggregate_labels(meter_clickhouse_zookeeper_session,sum))", "latest(aggregate_labels(meter_clickhouse_zookeeper_watch,sum))", "aggregate_labels(meter_clickhouse_zookeeper_bytes_sent,sum)", "aggregate_labels(meter_clickhouse_zookeeper_bytes_received,sum)"], "expressionLabels": ["sessions", "watches", "bytes sent", "bytes recv"], "span": 6, "rowSpan": 2 },
+      { "id": "keeper", "title": "Keeper", "type": "line", "expressions": ["latest(aggregate_labels(meter_clickhouse_keeper_connections_alive,sum))", "latest(aggregate_labels(meter_clickhouse_keeper_outstanding_requests,sum))"], "expressionLabels": ["alive conns", "outstanding"], "span": 6, "rowSpan": 2 }
+    ],
+    "instance": [
+      { "id": "uptime", "title": "Uptime (days)", "type": "line", "expressions": ["latest(meter_clickhouse_instance_uptime)/3600/24"], "span": 3, "rowSpan": 2 },
+      { "id": "version", "title": "Version", "type": "line", "expressions": ["latest(meter_clickhouse_instance_version)"], "span": 3, "rowSpan": 2 },
+      { "id": "cpu", "title": "CPU (cores)", "type": "line", "expressions": ["meter_clickhouse_instance_cpu_usage/1000000"], "span": 3, "rowSpan": 2 },
+      { "id": "mem", "title": "Memory (%)", "type": "line", "unit": "%", "expressions": ["meter_clickhouse_instance_memory_usage", "meter_clickhouse_instance_memory_available"], "expressionLabels": ["used", "available"], "span": 3, "rowSpan": 2 },
+      { "id": "network", "title": "Network (B)", "type": "line", "expressions": ["meter_clickhouse_instance_network_receive_bytes", "meter_clickhouse_instance_network_send_bytes"], "expressionLabels": ["receive", "send"], "span": 4, "rowSpan": 2 },
+      { "id": "conns", "title": "Connections", "type": "line", "expressions": ["meter_clickhouse_instance_tcp_connections", "meter_clickhouse_instance_http_connections"], "expressionLabels": ["TCP", "HTTP"], "span": 4, "rowSpan": 2 },
+      { "id": "queries", "title": "Queries", "type": "line", "expressions": ["meter_clickhouse_instance_query", "meter_clickhouse_instance_query_select", "meter_clickhouse_instance_query_insert"], "expressionLabels": ["total", "select", "insert"], "span": 4, "rowSpan": 2 },
+      { "id": "qps", "title": "QPS", "type": "line", "expressions": ["meter_clickhouse_instance_query_select_rate", "meter_clickhouse_instance_query_insert_rate"], "expressionLabels": ["select", "insert"], "span": 6, "rowSpan": 2 },
+      { "id": "query_time", "title": "Query Time (ms)", "type": "line", "unit": "ms", "expressions": ["meter_clickhouse_instance_querytime_microseconds/meter_clickhouse_instance_query/1000", "meter_clickhouse_instance_querytime_select_microseconds/meter_clickhouse_instance_query_select/1000", "meter_clickhouse_instance_querytime_insert_microseconds/meter_clickhouse_instance_query_insert/1000"], "expressionLabels": ["avg", "select", "insert"], "span": 6, "rowSpan": 2 },
+      { "id": "slow", "title": "File Slow Read", "type": "line", "expressions": ["meter_clickhouse_instance_query_slow"], "span": 4, "rowSpan": 2 },
+      { "id": "merge", "title": "Background Merge", "type": "line", "expressions": ["meter_clickhouse_instance_background_merge"], "span": 4, "rowSpan": 2 },
+      { "id": "mutations", "title": "Mutations", "type": "line", "expressions": ["meter_clickhouse_instance_mutations"], "span": 4, "rowSpan": 2 },
+      { "id": "files", "title": "Files Open", "type": "line", "expressions": ["latest(meter_clickhouse_instance_file_open)"], "span": 4, "rowSpan": 2 },
+      { "id": "insert_io", "title": "Insert Throughput", "type": "line", "expressions": ["meter_clickhouse_instance_inserted_bytes", "meter_clickhouse_instance_inserted_rows"], "expressionLabels": ["bytes/s", "rows/s"], "expressionAxes": [0, 1], "span": 4, "rowSpan": 2 },
+      { "id": "delayed", "title": "Delayed Inserts (s)", "type": "line", "expressions": ["meter_clickhouse_instance_delayed_inserts"], "span": 4, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/elasticsearch.json b/apps/bff/src/bundled_templates/layers/elasticsearch.json
new file mode 100644
index 0000000..86dd0f8
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/elasticsearch.json
@@ -0,0 +1,59 @@
+{
+  "key": "ELASTICSEARCH",
+  "alias": "Elasticsearch",
+  "color": "var(--sw-cyan)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-elasticsearch-monitoring/",
+  "aliases": { "services": "ES clusters", "instances": "Nodes", "endpoints": "Indices" },
+  "components": { "service": true, "instances": true, "endpoints": true, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "shards",
+    "columns": [
+      { "metric": "health", "label": "Health", "mqe": "meter_elasticsearch_cluster_health_status", "aggregation": "avg" },
+      { "metric": "shards", "label": "Shards", "mqe": "latest(meter_elasticsearch_cluster_shards_total)", "aggregation": "sum" },
+      { "metric": "nodes", "label": "Nodes", "mqe": "latest(meter_elasticsearch_cluster_nodes)", "aggregation": "sum" },
+      { "metric": "unassigned", "label": "Unassigned", "mqe": "latest(meter_elasticsearch_cluster_unassigned_shards_total)", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Cluster", "size": "auto", "metrics": [
+        { "id": "health", "label": "Health", "tip": "0 green / 1 yellow / 2 red.", "mqe": "meter_elasticsearch_cluster_health_status", "aggregation": "avg" },
+        { "id": "unassigned", "label": "Unassigned Shards", "tip": "Shards not yet placed on any node.", "mqe": "latest(meter_elasticsearch_cluster_unassigned_shards_total)", "aggregation": "sum" },
+        { "id": "breakers", "label": "Tripped Breakers", "tip": "Cumulative circuit-breaker trips — high values indicate memory pressure.", "mqe": "latest(meter_elasticsearch_cluster_breakers_tripped)", "aggregation": "sum" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "health", "title": "Cluster Health", "type": "line", "expressions": ["meter_elasticsearch_cluster_health_status"], "span": 3, "rowSpan": 2 },
+      { "id": "nodes", "title": "Nodes", "type": "line", "expressions": ["latest(meter_elasticsearch_cluster_nodes)"], "span": 3, "rowSpan": 2 },
+      { "id": "pending_tasks", "title": "Pending Tasks", "type": "line", "expressions": ["avg(meter_elasticsearch_cluster_pending_tasks_total)"], "span": 3, "rowSpan": 2 },
+      { "id": "primary_shards", "title": "Primary Shards", "type": "line", "expressions": ["latest(meter_elasticsearch_cluster_primary_shards_total)"], "span": 3, "rowSpan": 2 },
+      { "id": "shards_breakdown", "title": "Shards Breakdown", "type": "line", "expressions": ["latest(meter_elasticsearch_cluster_shards_total)", "latest(meter_elasticsearch_cluster_initializing_shards_total)", "latest(meter_elasticsearch_cluster_relocating_shards_total)", "latest(meter_elasticsearch_cluster_unassigned_shards_total)", "latest(meter_elasticsearch_cluster_delayed_unassigned_shards_total)"], "expressionLabels": ["active", "initializing", "relocating", "unassigned", "delaye [...]
+      { "id": "breakers", "title": "Tripped Breakers", "type": "line", "expressions": ["latest(meter_elasticsearch_cluster_breakers_tripped)"], "span": 3, "rowSpan": 2 },
+      { "id": "cluster_cpu", "title": "Cluster CPU Avg", "type": "line", "unit": "%", "expressions": ["avg(meter_elasticsearch_cluster_cpu_usage_avg)"], "span": 3, "rowSpan": 2 },
+      { "id": "cluster_jvm", "title": "JVM Memory Used Avg", "type": "line", "expressions": ["avg(meter_elasticsearch_cluster_jvm_memory_used_avg)"], "span": 4, "rowSpan": 2 },
+      { "id": "open_files", "title": "Open Files Avg", "type": "line", "expressions": ["avg(meter_elasticsearch_cluster_open_file_count)"], "span": 4, "rowSpan": 2 }
+    ],
+    "instance": [
+      { "id": "cpu", "title": "Process CPU (%)", "type": "line", "unit": "%", "expressions": ["meter_elasticsearch_node_process_cpu_percent"], "span": 4, "rowSpan": 2 },
+      { "id": "os_cpu", "title": "OS CPU (%)", "type": "line", "unit": "%", "expressions": ["meter_elasticsearch_node_os_cpu_percent"], "span": 4, "rowSpan": 2 },
+      { "id": "load_avg", "title": "Load Average", "type": "line", "expressions": ["meter_elasticsearch_node_os_load1", "meter_elasticsearch_node_os_load5", "meter_elasticsearch_node_os_load15"], "expressionLabels": ["1m", "5m", "15m"], "span": 4, "rowSpan": 2 },
+      { "id": "jvm_mem", "title": "JVM Memory (MB)", "type": "line", "expressions": ["meter_elasticsearch_node_jvm_memory_heap_used/1024/1024", "meter_elasticsearch_node_jvm_memory_heap_max/1024/1024", "meter_elasticsearch_node_jvm_memory_nonheap_used/1024/1024"], "expressionLabels": ["heap used", "heap max", "non-heap"], "span": 6, "rowSpan": 2 },
+      { "id": "gc", "title": "GC", "type": "line", "expressions": ["meter_elasticsearch_node_jvm_gc_count", "meter_elasticsearch_node_jvm_gc_time"], "expressionLabels": ["count", "time (ms/min)"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+      { "id": "translog", "title": "Translog", "type": "line", "expressions": ["meter_elasticsearch_node_indices_translog_operations", "meter_elasticsearch_node_indices_translog_size/1024/1024"], "expressionLabels": ["operations", "size (MB)"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+      { "id": "breakers", "title": "Breakers", "type": "line", "expressions": ["meter_elasticsearch_node_breakers_tripped", "meter_elasticsearch_node_breakers_estimated_size/1024/1024"], "expressionLabels": ["tripped", "estimated (MB)"], "span": 6, "rowSpan": 2 },
+      { "id": "segments", "title": "Segments", "type": "line", "expressions": ["meter_elasticsearch_node_segment_count", "meter_elasticsearch_node_segment_memory"], "expressionLabels": ["count", "memory (MB)"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+      { "id": "disk", "title": "Disk Usage", "type": "line", "expressions": ["meter_elasticsearch_node_disk_usage/1024/1024/1024", "meter_elasticsearch_node_disk_usage_percent"], "expressionLabels": ["GB", "%"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+      { "id": "network", "title": "Network", "type": "line", "expressions": ["meter_elasticsearch_node_network_send_bytes", "meter_elasticsearch_node_network_receive_bytes"], "expressionLabels": ["send", "receive"], "span": 6, "rowSpan": 2 },
+      { "id": "open_files", "title": "Open Files", "type": "line", "expressions": ["avg(meter_elasticsearch_node_open_file_count)"], "span": 6, "rowSpan": 2 }
+    ],
+    "endpoint": [
+      { "id": "indexing_rate", "title": "Indexing Rate", "type": "line", "expressions": ["meter_elasticsearch_index_stats_indexing_index_total_req_rate", "meter_elasticsearch_index_stats_indexing_index_total_proc_rate"], "expressionLabels": ["req", "proc"], "span": 6, "rowSpan": 2 },
+      { "id": "search_rate", "title": "Search Rate", "type": "line", "expressions": ["meter_elasticsearch_index_stats_search_query_total_req_rate", "meter_elasticsearch_index_stats_search_query_total_proc_rate"], "expressionLabels": ["req", "proc"], "span": 6, "rowSpan": 2 },
+      { "id": "data_size", "title": "Index Size", "type": "line", "unit": "GB", "expressions": ["latest(meter_elasticsearch_index_indices_store_size_bytes_total)/1024/1024/1024", "latest(meter_elasticsearch_index_indices_store_size_bytes_primary)/1024/1024/1024"], "expressionLabels": ["all shards", "primary"], "span": 6, "rowSpan": 2 },
+      { "id": "docs", "title": "Documents", "type": "line", "expressions": ["meter_elasticsearch_index_indices_docs_total", "meter_elasticsearch_index_indices_docs_primary", "latest(meter_elasticsearch_index_indices_deleted_docs_primary)"], "expressionLabels": ["all", "primary", "deleted"], "span": 6, "rowSpan": 2 },
+      { "id": "search_time", "title": "Avg Search Time / Req (s)", "type": "line", "expressions": ["meter_elasticsearch_index_search_fetch_avg_time", "meter_elasticsearch_index_search_query_avg_time", "meter_elasticsearch_index_search_scroll_avg_time", "meter_elasticsearch_index_search_suggest_avg_time"], "expressionLabels": ["fetch", "query", "scroll", "suggest"], "span": 12, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/envoy_ai_gateway.json b/apps/bff/src/bundled_templates/layers/envoy_ai_gateway.json
new file mode 100644
index 0000000..0898304
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/envoy_ai_gateway.json
@@ -0,0 +1,61 @@
+{
+  "key": "ENVOY_AI_GATEWAY",
+  "alias": "Envoy AI Gateway",
+  "color": "var(--sw-pink)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-envoy-ai-gateway-monitoring/",
+  "aliases": { "services": "AI Gateways", "instances": "Nodes" },
+  "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "cpm",
+    "columns": [
+      { "metric": "cpm", "label": "RPM", "unit": "rpm", "mqe": "meter_envoy_ai_gw_request_cpm", "aggregation": "sum" },
+      { "metric": "latency", "label": "Avg Latency", "unit": "ms", "mqe": "meter_envoy_ai_gw_request_latency_avg", "aggregation": "avg" },
+      { "metric": "inTokens", "label": "Input Tokens/min", "mqe": "meter_envoy_ai_gw_input_token_rate", "aggregation": "sum" },
+      { "metric": "outTokens", "label": "Output Tokens/min", "mqe": "meter_envoy_ai_gw_output_token_rate", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Latency", "size": "auto", "metrics": [
+        { "id": "latency", "label": "Avg Latency", "tip": "Average end-to-end request latency.", "mqe": "meter_envoy_ai_gw_request_latency_avg", "unit": "ms", "aggregation": "avg" },
+        { "id": "ttft", "label": "TTFT Avg", "tip": "Time to first token — generative-model latency indicator.", "mqe": "meter_envoy_ai_gw_ttft_avg", "unit": "ms", "aggregation": "avg" }
+      ]},
+      { "title": "Tokens", "size": "square", "metrics": [
+        { "id": "tokens", "label": "Tokens/min", "tip": "Combined input + output token rate.", "mqe": "meter_envoy_ai_gw_input_token_rate + meter_envoy_ai_gw_output_token_rate", "aggregation": "sum" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "cpm", "title": "Request RPM", "type": "line", "unit": "rpm", "expressions": ["meter_envoy_ai_gw_request_cpm"], "span": 3, "rowSpan": 2 },
+      { "id": "latency_avg", "title": "Request Latency Avg", "type": "line", "unit": "ms", "expressions": ["meter_envoy_ai_gw_request_latency_avg"], "span": 3, "rowSpan": 2 },
+      { "id": "input_tokens", "title": "Input Token Rate", "type": "line", "expressions": ["meter_envoy_ai_gw_input_token_rate"], "span": 3, "rowSpan": 2 },
+      { "id": "output_tokens", "title": "Output Token Rate", "type": "line", "expressions": ["meter_envoy_ai_gw_output_token_rate"], "span": 3, "rowSpan": 2 },
+      { "id": "latency_percentile", "title": "Request Latency Percentile", "type": "line", "unit": "ms", "expressions": ["meter_envoy_ai_gw_request_latency_percentile"], "span": 12, "rowSpan": 2 },
+      { "id": "ttft", "title": "TTFT (Time to First Token)", "type": "line", "unit": "ms", "expressions": ["meter_envoy_ai_gw_ttft_avg", "meter_envoy_ai_gw_ttft_percentile"], "expressionLabels": ["avg", "percentile"], "span": 6, "rowSpan": 2 },
+      { "id": "tpot", "title": "TPOT (Time Per Output Token)", "type": "line", "unit": "ms", "expressions": ["meter_envoy_ai_gw_tpot_avg", "meter_envoy_ai_gw_tpot_percentile"], "expressionLabels": ["avg", "percentile"], "span": 6, "rowSpan": 2 },
+      { "id": "provider_cpm", "title": "RPM by Provider", "type": "line", "expressions": ["aggregate_labels(meter_envoy_ai_gw_provider_request_cpm,sum(gen_ai_provider_name))"], "span": 4, "rowSpan": 2 },
+      { "id": "provider_tokens", "title": "Tokens by Provider", "type": "line", "expressions": ["aggregate_labels(meter_envoy_ai_gw_provider_token_rate,sum(gen_ai_provider_name))"], "span": 4, "rowSpan": 2 },
+      { "id": "provider_latency", "title": "Latency Avg by Provider", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_envoy_ai_gw_provider_latency_avg,avg(gen_ai_provider_name))"], "span": 4, "rowSpan": 2 },
+      { "id": "model_cpm", "title": "RPM by Model", "type": "line", "expressions": ["aggregate_labels(meter_envoy_ai_gw_model_request_cpm,sum(gen_ai_response_model))"], "span": 4, "rowSpan": 2 },
+      { "id": "model_tokens", "title": "Tokens by Model", "type": "line", "expressions": ["aggregate_labels(meter_envoy_ai_gw_model_token_rate,sum(gen_ai_response_model))"], "span": 4, "rowSpan": 2 },
+      { "id": "model_latency", "title": "Latency Avg by Model", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_envoy_ai_gw_model_latency_avg,avg(gen_ai_response_model))"], "span": 4, "rowSpan": 2 },
+      { "id": "model_ttft", "title": "TTFT by Model", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_envoy_ai_gw_model_ttft_avg,avg(gen_ai_response_model))"], "span": 6, "rowSpan": 2 },
+      { "id": "model_tpot", "title": "TPOT by Model", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_envoy_ai_gw_model_tpot_avg,avg(gen_ai_response_model))"], "span": 6, "rowSpan": 2 },
+      { "id": "mcp_cpm", "title": "MCP RPM", "tip": "Model Context Protocol — when used.", "type": "line", "expressions": ["meter_envoy_ai_gw_mcp_request_cpm"], "visibleWhen": "meter_envoy_ai_gw_mcp_request_cpm has value", "span": 4, "rowSpan": 2 },
+      { "id": "mcp_latency", "title": "MCP Avg Latency", "type": "line", "unit": "ms", "expressions": ["meter_envoy_ai_gw_mcp_request_latency_avg"], "visibleWhen": "meter_envoy_ai_gw_mcp_request_latency_avg has value", "span": 4, "rowSpan": 2 },
+      { "id": "mcp_err", "title": "MCP Error RPM", "type": "line", "expressions": ["meter_envoy_ai_gw_mcp_error_cpm"], "visibleWhen": "meter_envoy_ai_gw_mcp_error_cpm has value", "span": 4, "rowSpan": 2 },
+      { "id": "mcp_method", "title": "MCP by Method", "type": "line", "expressions": ["aggregate_labels(meter_envoy_ai_gw_mcp_method_cpm,sum(mcp_method_name))"], "visibleWhen": "meter_envoy_ai_gw_mcp_method_cpm has value", "span": 6, "rowSpan": 2 },
+      { "id": "mcp_backend", "title": "MCP by Backend", "type": "line", "expressions": ["aggregate_labels(meter_envoy_ai_gw_mcp_backend_request_cpm,sum(mcp_backend))"], "visibleWhen": "meter_envoy_ai_gw_mcp_backend_request_cpm has value", "span": 6, "rowSpan": 2 }
+    ],
+    "instance": [
+      { "id": "cpm", "title": "Request RPM", "type": "line", "unit": "rpm", "expressions": ["meter_envoy_ai_gw_instance_request_cpm"], "span": 3, "rowSpan": 2 },
+      { "id": "latency_avg", "title": "Request Latency Avg", "type": "line", "unit": "ms", "expressions": ["meter_envoy_ai_gw_instance_request_latency_avg"], "span": 3, "rowSpan": 2 },
+      { "id": "input_tokens", "title": "Input Token Rate", "type": "line", "expressions": ["meter_envoy_ai_gw_instance_input_token_rate"], "span": 3, "rowSpan": 2 },
+      { "id": "output_tokens", "title": "Output Token Rate", "type": "line", "expressions": ["meter_envoy_ai_gw_instance_output_token_rate"], "span": 3, "rowSpan": 2 },
+      { "id": "latency_percentile", "title": "Request Latency Percentile", "type": "line", "unit": "ms", "expressions": ["meter_envoy_ai_gw_instance_request_latency_percentile"], "span": 12, "rowSpan": 2 },
+      { "id": "ttft", "title": "TTFT", "type": "line", "unit": "ms", "expressions": ["meter_envoy_ai_gw_instance_ttft_avg", "meter_envoy_ai_gw_instance_ttft_percentile"], "expressionLabels": ["avg", "percentile"], "span": 6, "rowSpan": 2 },
+      { "id": "tpot", "title": "TPOT", "type": "line", "unit": "ms", "expressions": ["meter_envoy_ai_gw_instance_tpot_avg", "meter_envoy_ai_gw_instance_tpot_percentile"], "expressionLabels": ["avg", "percentile"], "span": 6, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/flink.json b/apps/bff/src/bundled_templates/layers/flink.json
new file mode 100644
index 0000000..397585d
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/flink.json
@@ -0,0 +1,67 @@
+{
+  "key": "FLINK",
+  "alias": "Flink",
+  "color": "var(--sw-pink)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-flink-monitoring/",
+  "aliases": { "services": "Flink JobManagers", "instances": "TaskManagers", "endpoints": "Jobs" },
+  "components": { "service": true, "instances": true, "endpoints": true, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "jobs",
+    "columns": [
+      { "metric": "jobs", "label": "Running Jobs", "mqe": "latest(meter_flink_jobManager_running_job_number)", "aggregation": "sum" },
+      { "metric": "taskMgrs", "label": "TaskManagers", "mqe": "latest(meter_flink_jobManager_taskManagers_registered_number)", "aggregation": "sum" },
+      { "metric": "slotsAvail", "label": "Slots Available", "mqe": "latest(meter_flink_jobManager_taskManagers_slots_available)", "aggregation": "sum" },
+      { "metric": "slotsTotal", "label": "Slots Total", "mqe": "latest(meter_flink_jobManager_taskManagers_slots_total)", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Cluster", "size": "auto", "metrics": [
+        { "id": "jobs", "label": "Running Jobs", "tip": "Active Flink jobs.", "mqe": "latest(meter_flink_jobManager_running_job_number)", "aggregation": "sum" },
+        { "id": "slotsAvail", "label": "Slots Available", "tip": "Free task slots.", "mqe": "latest(meter_flink_jobManager_taskManagers_slots_available)", "aggregation": "sum" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "jobs", "title": "Running Jobs", "type": "line", "expressions": ["latest(meter_flink_jobManager_running_job_number)"], "span": 3, "rowSpan": 2 },
+      { "id": "task_mgrs", "title": "TaskManagers", "type": "line", "expressions": ["latest(meter_flink_jobManager_taskManagers_registered_number)"], "span": 3, "rowSpan": 2 },
+      { "id": "slots", "title": "Slots Total / Available", "type": "line", "expressions": ["latest(meter_flink_jobManager_taskManagers_slots_total)", "latest(meter_flink_jobManager_taskManagers_slots_available)"], "expressionLabels": ["total", "available"], "span": 3, "rowSpan": 2 },
+      { "id": "cpu_load", "title": "JM JVM CPU Load (%)", "type": "line", "unit": "%", "expressions": ["meter_flink_jobManager_jvm_cpu_load/10"], "span": 3, "rowSpan": 2 },
+      { "id": "jm_threads", "title": "JM JVM Thread Count", "type": "line", "expressions": ["meter_flink_jobManager_jvm_thread_count"], "span": 3, "rowSpan": 2 },
+      { "id": "jm_cpu_time", "title": "JM CPU Time (ms)", "type": "line", "unit": "ms", "expressions": ["meter_flink_jobManager_jvm_cpu_time/1000/1000"], "span": 3, "rowSpan": 2 },
+      { "id": "jm_mem_heap", "title": "JM Heap (MB)", "type": "line", "unit": "MB", "expressions": ["meter_flink_jobManager_jvm_memory_heap_used/1024/1024", "(meter_flink_jobManager_jvm_memory_heap_available)/1024/1024"], "expressionLabels": ["used", "available"], "span": 3, "rowSpan": 2 },
+      { "id": "jm_mem_nonheap", "title": "JM NonHeap (MB)", "type": "line", "unit": "MB", "expressions": ["(meter_flink_jobManager_jvm_memory_nonHeap_used)/1024/1024", "(meter_flink_jobManager_jvm_memory_nonHeap_available)/1024/1024"], "expressionLabels": ["used", "available"], "span": 3, "rowSpan": 2 },
+      { "id": "jm_metaspace", "title": "JM Metaspace (MB)", "type": "line", "unit": "MB", "expressions": ["meter_flink_jobManager_jvm_memory_metaspace_used/1024/1024", "(meter_flink_jobManager_jvm_memory_metaspace_available)/1024/1024"], "expressionLabels": ["used", "available"], "span": 6, "rowSpan": 2 },
+      { "id": "g1_young", "title": "G1 Young GC", "type": "line", "expressions": ["meter_flink_jobManager_jvm_g1_young_generation_count", "meter_flink_jobManager_jvm_g1_young_generation_time"], "expressionLabels": ["count", "time (ms)"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+      { "id": "g1_old", "title": "G1 Old GC", "type": "line", "expressions": ["meter_flink_jobManager_jvm_g1_old_generation_count", "meter_flink_jobManager_jvm_g1_old_generation_time"], "expressionLabels": ["count", "time (ms)"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+      { "id": "all_gc", "title": "All GC", "type": "line", "expressions": ["meter_flink_jobManager_jvm_all_garbageCollector_count", "meter_flink_jobManager_jvm_all_garbageCollector_time"], "expressionLabels": ["count", "time (ms)"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+      { "id": "top_running", "title": "Top 10 Running Jobs", "type": "top", "expressions": ["top_n(meter_flink_job_runningTime,10,des)"], "span": 12, "rowSpan": 3 }
+    ],
+    "instance": [
+      { "id": "cpu_load", "title": "JVM CPU Load (%)", "type": "line", "unit": "%", "expressions": ["meter_flink_taskManager_jvm_cpu_load/10"], "span": 3, "rowSpan": 2 },
+      { "id": "threads", "title": "JVM Thread Count", "type": "line", "expressions": ["meter_flink_taskManager_jvm_thread_count"], "span": 3, "rowSpan": 2 },
+      { "id": "cpu_time", "title": "CPU Time (ms)", "type": "line", "unit": "ms", "expressions": ["meter_flink_taskManager_jvm_cpu_time/1000/1000"], "span": 3, "rowSpan": 2 },
+      { "id": "back_pressure", "title": "Back Pressured", "type": "line", "expressions": ["meter_flink_taskManager_isBackPressured"], "span": 3, "rowSpan": 2 },
+      { "id": "mem_heap", "title": "Heap (MB)", "type": "line", "unit": "MB", "expressions": ["meter_flink_taskManager_jvm_memory_heap_used/1024/1024", "(meter_flink_taskManager_jvm_memory_heap_available)/1024/1024"], "expressionLabels": ["used", "available"], "span": 4, "rowSpan": 2 },
+      { "id": "mem_nonheap", "title": "NonHeap (MB)", "type": "line", "unit": "MB", "expressions": ["(meter_flink_taskManager_jvm_memory_nonHeap_used)/1024/1024", "(meter_flink_taskManager_jvm_memory_nonHeap_available)/1024/1024"], "expressionLabels": ["used", "available"], "span": 4, "rowSpan": 2 },
+      { "id": "mem_meta", "title": "Metaspace (MB)", "type": "line", "unit": "MB", "expressions": ["meter_flink_taskManager_jvm_memory_metaspace_used/1024/1024", "(meter_flink_taskManager_jvm_memory_metaspace_available)/1024/1024"], "expressionLabels": ["used", "available"], "span": 4, "rowSpan": 2 },
+      { "id": "records", "title": "Records In / Out", "type": "line", "expressions": ["meter_flink_taskManager_numRecordsIn", "meter_flink_taskManager_numRecordsOut"], "expressionLabels": ["in", "out"], "span": 6, "rowSpan": 2 },
+      { "id": "bytes", "title": "Bytes In / Out / s", "type": "line", "expressions": ["meter_flink_taskManager_numBytesInPerSecond", "meter_flink_taskManager_numBytesOutPerSecond"], "expressionLabels": ["in", "out"], "span": 6, "rowSpan": 2 },
+      { "id": "netty", "title": "Netty Memory (MB)", "type": "line", "unit": "MB", "expressions": ["meter_flink_taskManager_netty_usedMemory/1024/1024", "meter_flink_taskManager_netty_availableMemory/1024/1024"], "expressionLabels": ["used", "available"], "span": 6, "rowSpan": 2 },
+      { "id": "pool_usage", "title": "Pool Usage (%)", "type": "line", "unit": "%", "expressions": ["meter_flink_taskManager_inPoolUsage*10", "meter_flink_taskManager_outPoolUsage"], "expressionLabels": ["in pool", "out pool"], "span": 6, "rowSpan": 2 },
+      { "id": "back_pressure_time", "title": "Back-Pressure Time (ms/s)", "type": "line", "unit": "ms/s", "expressions": ["meter_flink_taskManager_softBackPressuredTimeMsPerSecond", "meter_flink_taskManager_hardBackPressuredTimeMsPerSecond", "meter_flink_taskManager_idleTimeMsPerSecond", "meter_flink_taskManager_busyTimeMsPerSecond"], "expressionLabels": ["soft bp", "hard bp", "idle", "busy"], "span": 12, "rowSpan": 2 }
+    ],
+    "endpoint": [
+      { "id": "running_time", "title": "Job Running Time (min)", "type": "line", "expressions": ["latest(meter_flink_job_runningTime)/1000/60"], "span": 4, "rowSpan": 2 },
+      { "id": "restarting_time", "title": "Job Restarting Time (min)", "type": "line", "expressions": ["latest(meter_flink_job_restartingTime)/1000/60"], "span": 4, "rowSpan": 2 },
+      { "id": "cancelling_time", "title": "Job Cancelling Time (min)", "type": "line", "expressions": ["latest(meter_flink_job_cancellingTime)/1000/60"], "span": 4, "rowSpan": 2 },
+      { "id": "restarts", "title": "Job Restarts", "type": "line", "expressions": ["latest(meter_flink_job_restart_number)"], "span": 4, "rowSpan": 2 },
+      { "id": "checkpoints", "title": "Checkpoints", "type": "line", "expressions": ["meter_flink_job_checkpoints_total", "meter_flink_job_checkpoints_completed", "meter_flink_job_checkpoints_failed", "meter_flink_job_checkpoints_inProgress"], "expressionLabels": ["total", "completed", "failed", "in-progress"], "span": 8, "rowSpan": 2 },
+      { "id": "checkpoint_last", "title": "Last Checkpoint", "type": "line", "expressions": ["meter_flink_job_lastCheckpointSize", "meter_flink_job_lastCheckpointDuration"], "expressionLabels": ["size (bytes)", "duration (ms)"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 },
+      { "id": "emit_lag", "title": "Current Emit Event Time Lag (ms)", "type": "line", "unit": "ms", "expressions": ["meter_flink_job_currentEmitEventTimeLag"], "span": 6, "rowSpan": 2 },
+      { "id": "records", "title": "Records In / Out", "type": "line", "expressions": ["meter_flink_job_numRecordsIn", "meter_flink_job_numRecordsOut"], "expressionLabels": ["in", "out"], "span": 6, "rowSpan": 2 },
+      { "id": "bytes", "title": "Bytes In / Out / s", "type": "line", "expressions": ["meter_flink_job_numBytesInPerSecond", "meter_flink_job_numBytesOutPerSecond"], "expressionLabels": ["in", "out"], "span": 6, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/layers/config/general.json b/apps/bff/src/bundled_templates/layers/general.json
similarity index 90%
rename from apps/bff/src/layers/config/general.json
rename to apps/bff/src/bundled_templates/layers/general.json
index cbb65bc..377fe4a 100644
--- a/apps/bff/src/layers/config/general.json
+++ b/apps/bff/src/bundled_templates/layers/general.json
@@ -32,21 +32,14 @@
   "overview": {
     "groups": [
       {
-        "title": "Throughput",
+        "title": "Latency & errors",
         "size": "auto",
         "metrics": [
           {
-            "id": "rpm",
-            "label": "RPM",
-            "tip": "Requests per minute across the layer.",
-            "mqe": "service_cpm",
-            "aggregation": "sum"
-          },
-          {
-            "id": "p99",
-            "label": "P99",
-            "tip": "99th-percentile response time across the layer.",
-            "mqe": "service_percentile{p='99'}",
+            "id": "p95",
+            "label": "P95",
+            "tip": "95th-percentile response time across the layer.",
+            "mqe": "service_percentile{p='95'}",
             "unit": "ms",
             "aggregation": "avg"
           },
@@ -923,5 +916,65 @@
     "traceProfiling": [],
     "ebpfProfiling": [],
     "asyncProfiling": []
+  },
+  "topology": {
+    "nodeMetrics": [
+      { "id": "cpm", "label": "RPM", "mqe": "service_cpm", "unit": "rpm", "role": "center", "aggregation": "avg" },
+      {
+        "id": "sla",
+        "label": "SLA",
+        "mqe": "service_sla/100",
+        "unit": "%",
+        "role": "ring",
+        "aggregation": "avg",
+        "thresholds": {
+          "invertHealth": true,
+          "invertBase": 100,
+          "ok": 0.1,
+          "warn": 1,
+          "danger": 5
+        }
+      },
+      { "id": "respTime", "label": "Latency", "mqe": "service_resp_time", "unit": "ms", "role": "secondary", "aggregation": "avg" }
+    ],
+    "linkServerMetrics": [
+      { "id": "cpm", "label": "RPM", "mqe": "service_relation_server_cpm", "unit": "rpm", "role": "lineServer", "aggregation": "avg" },
+      { "id": "respTime", "label": "Avg response time", "mqe": "service_relation_server_resp_time", "unit": "ms", "aggregation": "avg" },
+      { "id": "p95", "label": "p95", "mqe": "service_relation_server_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+      { "id": "sla", "label": "SLA", "mqe": "service_relation_server_call_sla/100", "unit": "%", "aggregation": "avg" }
+    ],
+    "linkClientMetrics": [
+      { "id": "cpm", "label": "RPM", "mqe": "service_relation_client_cpm", "unit": "rpm", "role": "lineClient", "aggregation": "avg" },
+      { "id": "respTime", "label": "Avg response time", "mqe": "service_relation_client_resp_time", "unit": "ms", "aggregation": "avg" },
+      { "id": "p95", "label": "p95", "mqe": "service_relation_client_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+      { "id": "sla", "label": "SLA", "mqe": "service_relation_client_call_sla/100", "unit": "%", "aggregation": "avg" }
+    ]
+  },
+  "endpointDependency": {
+    "nodeMetrics": [
+      { "id": "cpm", "label": "RPM", "mqe": "endpoint_cpm", "unit": "rpm", "role": "center", "aggregation": "avg" },
+      {
+        "id": "sla",
+        "label": "SLA",
+        "mqe": "endpoint_sla/100",
+        "unit": "%",
+        "role": "ring",
+        "aggregation": "avg",
+        "thresholds": {
+          "invertHealth": true,
+          "invertBase": 100,
+          "ok": 0.1,
+          "warn": 1,
+          "danger": 5
+        }
+      },
+      { "id": "respTime", "label": "Latency", "mqe": "endpoint_resp_time", "unit": "ms", "role": "secondary", "aggregation": "avg" }
+    ],
+    "linkMetrics": [
+      { "id": "cpm", "label": "RPM", "mqe": "endpoint_relation_cpm", "unit": "rpm", "role": "lineServer", "aggregation": "avg" },
+      { "id": "respTime", "label": "Avg response time", "mqe": "endpoint_relation_resp_time", "unit": "ms", "aggregation": "avg" },
+      { "id": "p95", "label": "p95", "mqe": "endpoint_relation_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+      { "id": "sla", "label": "SLA", "mqe": "endpoint_relation_sla/100", "unit": "%", "aggregation": "avg" }
+    ]
   }
 }
diff --git a/apps/bff/src/bundled_templates/layers/ios.json b/apps/bff/src/bundled_templates/layers/ios.json
new file mode 100644
index 0000000..b6cec0e
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/ios.json
@@ -0,0 +1,254 @@
+{
+  "key": "IOS",
+  "alias": "iOS",
+  "color": "var(--sw-cyan)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-ios-monitoring/",
+  "aliases": {
+    "services": "Apps",
+    "instances": "App Sessions",
+    "endpoints": "Outbound APIs"
+  },
+  "components": {
+    "service": true,
+    "instances": true,
+    "endpoints": true,
+    "topology": false,
+    "traces": false,
+    "logs": true
+  },
+  "layer-header": {
+    "orderBy": "launch",
+    "columns": [
+      { "metric": "launch", "label": "Launch (P95)", "unit": "ms", "mqe": "meter_ios_app_launch_time_percentile{p='95'}", "aggregation": "avg" },
+      { "metric": "hangSum", "label": "Hang Time", "unit": "ms", "mqe": "meter_ios_hang_time_sum", "aggregation": "sum" },
+      { "metric": "abnormalExit", "label": "Crashes", "mqe": "meter_ios_foreground_abnormal_exit_count + meter_ios_background_abnormal_exit_count", "aggregation": "sum" },
+      { "metric": "outboundCpm", "label": "Outbound RPM", "unit": "rpm", "mqe": "service_cpm", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      {
+        "title": "App health",
+        "size": "auto",
+        "metrics": [
+          { "id": "launch_p95", "label": "Launch P95", "tip": "95th-percentile cold-launch time as measured by MetricKit.", "mqe": "meter_ios_app_launch_time_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+          { "id": "hang_p95", "label": "Hang P95", "tip": "95th-percentile UI hang duration.", "mqe": "meter_ios_hang_time_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+          { "id": "crashes", "label": "Crashes", "tip": "Foreground + background abnormal exits.", "mqe": "meter_ios_foreground_abnormal_exit_count + meter_ios_background_abnormal_exit_count", "aggregation": "sum" }
+        ]
+      }
+    ]
+  },
+  "dashboards": {
+    "service": [
+      {
+        "id": "launch_percentile",
+        "title": "App Launch Time Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(meter_ios_app_launch_time_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "hang_percentile",
+        "title": "Hang Time Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(meter_ios_hang_time_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "hang_sum",
+        "title": "Hang Time (sum)",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_ios_hang_time_sum"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "abnormal_exits",
+        "title": "Abnormal Exits (Crashes)",
+        "tip": "MetricKit reports foreground vs background abnormal exits separately.",
+        "type": "line",
+        "expressions": [
+          "meter_ios_foreground_abnormal_exit_count",
+          "meter_ios_background_abnormal_exit_count"
+        ],
+        "expressionLabels": ["foreground", "background"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "oom_kills",
+        "title": "OOM Kill Count",
+        "tip": "Background OOM kills — the iOS system reaping the app due to memory pressure.",
+        "type": "line",
+        "expressions": ["meter_ios_background_oom_kill_count"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "peak_memory",
+        "title": "Peak Memory",
+        "type": "line",
+        "unit": "bytes",
+        "expressions": ["meter_ios_peak_memory"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "scroll_hitch",
+        "title": "Scroll Hitch Ratio",
+        "tip": "Percentage of scroll frames classified as hitched — higher = laggier UI.",
+        "type": "line",
+        "expressions": ["meter_ios_scroll_hitch_ratio"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "network_transfer",
+        "title": "Network Transfer",
+        "tip": "Bytes over wifi vs cellular, in + out.",
+        "type": "line",
+        "unit": "bytes",
+        "expressions": [
+          "meter_ios_wifi_download",
+          "meter_ios_wifi_upload",
+          "meter_ios_cellular_download",
+          "meter_ios_cellular_upload"
+        ],
+        "expressionLabels": ["wifi ↓", "wifi ↑", "cellular ↓", "cellular ↑"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "outbound_http",
+        "title": "Outbound HTTP",
+        "tip": "Calls the app makes to backends — load, latency, success rate.",
+        "type": "line",
+        "expressions": [
+          "service_cpm",
+          "service_resp_time",
+          "service_sla/100"
+        ],
+        "expressionLabels": ["RPM", "Avg latency (ms)", "Success Rate (%)"],
+        "expressionAxes": [0, 0, 1],
+        "span": 6,
+        "rowSpan": 2
+      }
+    ],
+    "instance": [
+      {
+        "id": "instance_launch_p",
+        "title": "Launch Time Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(meter_ios_instance_app_launch_time_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_hang_p",
+        "title": "Hang Time Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(meter_ios_instance_hang_time_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_crashes",
+        "title": "Abnormal Exits",
+        "type": "line",
+        "expressions": [
+          "meter_ios_instance_foreground_abnormal_exit_count",
+          "meter_ios_instance_background_abnormal_exit_count"
+        ],
+        "expressionLabels": ["foreground", "background"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_oom",
+        "title": "OOM Kill Count",
+        "type": "line",
+        "expressions": ["meter_ios_instance_background_oom_kill_count"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_peak_mem",
+        "title": "Peak Memory",
+        "type": "line",
+        "unit": "bytes",
+        "expressions": ["meter_ios_instance_peak_memory"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_outbound",
+        "title": "Outbound HTTP",
+        "type": "line",
+        "expressions": [
+          "service_instance_cpm",
+          "service_instance_resp_time",
+          "service_instance_sla/100"
+        ],
+        "expressionLabels": ["RPM", "Avg latency (ms)", "Success Rate (%)"],
+        "expressionAxes": [0, 0, 1],
+        "span": 12,
+        "rowSpan": 2
+      }
+    ],
+    "endpoint": [
+      {
+        "id": "ep_cpm",
+        "title": "Outbound Load",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["endpoint_cpm"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "ep_resp",
+        "title": "Outbound Avg Latency",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["endpoint_resp_time"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "ep_sla",
+        "title": "Outbound Success Rate",
+        "type": "line",
+        "unit": "%",
+        "expressions": ["endpoint_sla/100"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "ep_percentile",
+        "title": "Outbound Latency Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(endpoint_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 12,
+        "rowSpan": 2
+      }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/k8s.json b/apps/bff/src/bundled_templates/layers/k8s.json
new file mode 100644
index 0000000..bb32eda
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/k8s.json
@@ -0,0 +1,266 @@
+{
+  "key": "K8S",
+  "alias": "Kubernetes",
+  "color": "var(--sw-info)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-k8s-monitoring/",
+  "aliases": {
+    "services": "Clusters",
+    "instances": "Nodes"
+  },
+  "components": {
+    "service": true,
+    "instances": true,
+    "endpoints": false,
+    "endpointDependency": false,
+    "topology": false,
+    "traces": false,
+    "logs": false
+  },
+  "layer-header": {
+    "orderBy": "pods",
+    "columns": [
+      { "metric": "pods", "label": "Pods", "mqe": "k8s_cluster_pod_total", "aggregation": "sum" },
+      { "metric": "nodes", "label": "Nodes", "mqe": "k8s_cluster_node_total", "aggregation": "sum" },
+      { "metric": "namespaces", "label": "Namespaces", "mqe": "k8s_cluster_namespace_total", "aggregation": "sum" },
+      { "metric": "deployments", "label": "Deployments", "mqe": "k8s_cluster_deployment_total", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      {
+        "title": "Capacity",
+        "size": "auto",
+        "metrics": [
+          { "id": "pods", "label": "Pods", "tip": "Total pods reported across the cluster.", "mqe": "k8s_cluster_pod_total", "aggregation": "sum" },
+          { "id": "nodes", "label": "Nodes", "tip": "Node count.", "mqe": "k8s_cluster_node_total", "aggregation": "sum" },
+          { "id": "services", "label": "Services", "tip": "K8s Service objects.", "mqe": "k8s_cluster_service_total", "aggregation": "sum" }
+        ]
+      },
+      {
+        "title": "Trouble",
+        "size": "square",
+        "metrics": [
+          {
+            "id": "podsNotRunning",
+            "label": "Pods not running",
+            "tip": "Pods in any non-Running state — pending, failed, unknown.",
+            "mqe": "latest(k8s_cluster_pod_status_not_running)",
+            "aggregation": "sum"
+          }
+        ]
+      }
+    ]
+  },
+  "dashboards": {
+    "service": [
+      {
+        "id": "cpu_resources",
+        "title": "Cluster CPU Resources",
+        "tip": "Allocatable / capacity / requests / limits across the cluster (millicores).",
+        "type": "line",
+        "unit": "m",
+        "expressions": [
+          "k8s_cluster_cpu_cores",
+          "k8s_cluster_cpu_cores_allocatable",
+          "k8s_cluster_cpu_cores_requests",
+          "k8s_cluster_cpu_cores_limits"
+        ],
+        "expressionLabels": ["total", "allocatable", "requests", "limits"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "mem_resources",
+        "title": "Cluster Memory Resources",
+        "type": "line",
+        "unit": "Gi",
+        "expressions": [
+          "k8s_cluster_memory_total/1024/1024/1024",
+          "k8s_cluster_memory_allocatable/1024/1024/1024",
+          "k8s_cluster_memory_requests/1024/1024/1024",
+          "k8s_cluster_memory_limits/1024/1024/1024"
+        ],
+        "expressionLabels": ["total", "allocatable", "requests", "limits"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "storage_resources",
+        "title": "Cluster Storage Resources",
+        "type": "line",
+        "unit": "Gi",
+        "expressions": [
+          "k8s_cluster_storage_total/1024/1024/1024",
+          "k8s_cluster_storage_allocatable/1024/1024/1024"
+        ],
+        "expressionLabels": ["total", "allocatable"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "pod_totals",
+        "title": "Pod / Container Totals",
+        "type": "line",
+        "expressions": ["k8s_cluster_pod_total", "k8s_cluster_container_total"],
+        "expressionLabels": ["pods", "containers"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "deployment_replicas",
+        "title": "Deployment Replicas",
+        "tip": "Spec vs ready/status replica counts. Divergence indicates rollout issues.",
+        "type": "line",
+        "expressions": [
+          "latest(k8s_cluster_deployment_spec_replicas)",
+          "latest(k8s_cluster_deployment_status)"
+        ],
+        "expressionLabels": ["spec", "status"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "pod_waiting",
+        "title": "Pods Waiting",
+        "type": "line",
+        "expressions": ["latest(k8s_cluster_pod_status_waiting)"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "pod_not_running",
+        "title": "Pods Not Running",
+        "type": "line",
+        "expressions": ["latest(k8s_cluster_pod_status_not_running)"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "service_pod_status",
+        "title": "Service Pod Status",
+        "tip": "Aggregate health of pods backing K8s Service objects.",
+        "type": "line",
+        "expressions": ["latest(k8s_cluster_service_pod_status)"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "node_status",
+        "title": "Node Status",
+        "tip": "Sum of node Ready states.",
+        "type": "line",
+        "expressions": ["latest(k8s_cluster_node_status)"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "namespace_totals",
+        "title": "Workload Totals",
+        "tip": "Namespaces / deployments / statefulsets / daemonsets across the cluster.",
+        "type": "line",
+        "expressions": [
+          "latest(k8s_cluster_namespace_total)",
+          "latest(k8s_cluster_deployment_total)",
+          "latest(k8s_cluster_statefulset_total)",
+          "latest(k8s_cluster_daemonset_total)"
+        ],
+        "expressionLabels": ["namespaces", "deployments", "statefulsets", "daemonsets"],
+        "span": 4,
+        "rowSpan": 2
+      }
+    ],
+    "instance": [
+      {
+        "id": "node_status",
+        "title": "Node Status",
+        "type": "line",
+        "expressions": ["latest(k8s_node_node_status)"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "node_pod_total",
+        "title": "Pods on Node",
+        "type": "line",
+        "expressions": ["k8s_node_pod_total"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "node_cpu_usage",
+        "title": "Node CPU Usage",
+        "tip": "CPU usage in millicores.",
+        "type": "line",
+        "unit": "m",
+        "expressions": ["k8s_node_cpu_usage"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "node_cpu_resources",
+        "title": "Node CPU Resources",
+        "type": "line",
+        "unit": "m",
+        "expressions": [
+          "k8s_node_cpu_cores",
+          "k8s_node_cpu_cores_allocatable",
+          "k8s_node_cpu_cores_requests",
+          "k8s_node_cpu_cores_limits"
+        ],
+        "expressionLabels": ["total", "allocatable", "requests", "limits"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "node_memory_usage",
+        "title": "Node Memory Usage",
+        "type": "line",
+        "unit": "Gi",
+        "expressions": ["k8s_node_memory_usage/1024/1024/1024"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "node_memory_resources",
+        "title": "Node Memory Resources",
+        "type": "line",
+        "unit": "Gi",
+        "expressions": [
+          "k8s_node_memory_total/1024/1024/1024",
+          "k8s_node_memory_allocatable/1024/1024/1024",
+          "k8s_node_memory_requests/1024/1024/1024",
+          "k8s_node_memory_limits/1024/1024/1024"
+        ],
+        "expressionLabels": ["total", "allocatable", "requests", "limits"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "node_network_io",
+        "title": "Node Network I/O",
+        "type": "line",
+        "unit": "KB/s",
+        "expressions": [
+          "k8s_node_network_receive/1024",
+          "k8s_node_network_transmit/1024"
+        ],
+        "expressionLabels": ["receive", "transmit"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "node_storage_resources",
+        "title": "Node Storage Resources",
+        "type": "line",
+        "unit": "Gi",
+        "expressions": [
+          "k8s_node_storage_total/1024/1024/1024",
+          "k8s_node_storage_allocatable/1024/1024/1024"
+        ],
+        "expressionLabels": ["total", "allocatable"],
+        "span": 6,
+        "rowSpan": 2
+      }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/k8s_service.json b/apps/bff/src/bundled_templates/layers/k8s_service.json
new file mode 100644
index 0000000..fb2cceb
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/k8s_service.json
@@ -0,0 +1,404 @@
+{
+  "key": "K8S_SERVICE",
+  "alias": "K8s Service",
+  "color": "var(--sw-purple)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-k8s-network-monitoring/",
+  "aliases": {
+    "services": "K8s services",
+    "instances": "Pods",
+    "endpoints": "Endpoints"
+  },
+  "components": {
+    "service": true,
+    "instances": true,
+    "endpoints": true,
+    "endpointDependency": false,
+    "topology": true,
+    "traces": true,
+    "logs": true,
+    "ebpfProfiling": true
+  },
+  "layer-header": {
+    "orderBy": "httpCpm",
+    "columns": [
+      { "metric": "pods", "label": "Pods", "mqe": "latest(k8s_service_pod_total)", "aggregation": "sum" },
+      { "metric": "httpCpm", "label": "HTTP RPM", "unit": "rpm", "mqe": "kubernetes_service_http_call_cpm", "aggregation": "sum" },
+      { "metric": "httpRespTime", "label": "Latency", "unit": "ms", "mqe": "kubernetes_service_http_call_time", "aggregation": "avg" },
+      { "metric": "httpSuccess", "label": "Success Rate", "unit": "%", "mqe": "kubernetes_service_http_call_successful_rate/100", "aggregation": "avg" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      {
+        "title": "HTTP",
+        "size": "auto",
+        "metrics": [
+          { "id": "httpCpm", "label": "RPM", "tip": "HTTP requests per minute observed by eBPF instrumentation.", "mqe": "kubernetes_service_http_call_cpm", "unit": "rpm", "aggregation": "sum" },
+          { "id": "httpRespTime", "label": "Latency", "tip": "Avg HTTP response time across the cluster.", "mqe": "kubernetes_service_http_call_time", "unit": "ms", "aggregation": "avg" },
+          { "id": "httpErr", "label": "Error Rate", "tip": "100 − HTTP success rate.", "mqe": "100 - kubernetes_service_http_call_successful_rate/100", "unit": "%", "aggregation": "avg" }
+        ]
+      },
+      {
+        "title": "Health",
+        "size": "square",
+        "metrics": [
+          {
+            "id": "apdex",
+            "label": "Apdex",
+            "tip": "User-satisfaction score on a 0 – 1 scale.",
+            "mqe": "kubernetes_service_apdex/10000",
+            "aggregation": "avg"
+          }
+        ]
+      }
+    ]
+  },
+  "dashboards": {
+    "service": [
+      {
+        "id": "pod_capacity",
+        "title": "Service Pods",
+        "tip": "Pod count + waiting / restart counters.",
+        "type": "line",
+        "expressions": [
+          "k8s_service_pod_total",
+          "latest(k8s_service_pod_status_waiting)",
+          "latest(k8s_service_pod_status_restarts_total)"
+        ],
+        "expressionLabels": ["total", "waiting", "restarts"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "cpu_resources",
+        "title": "CPU Resources",
+        "type": "line",
+        "unit": "m",
+        "expressions": [
+          "k8s_service_cpu_cores_requests",
+          "k8s_service_cpu_cores_limits"
+        ],
+        "expressionLabels": ["requests", "limits"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "memory_resources",
+        "title": "Memory Resources",
+        "type": "line",
+        "unit": "Mi",
+        "expressions": [
+          "k8s_service_memory_requests/1024/1024",
+          "k8s_service_memory_limits/1024/1024"
+        ],
+        "expressionLabels": ["requests", "limits"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "pod_cpu_usage",
+        "title": "Pod CPU Usage",
+        "type": "line",
+        "unit": "m",
+        "expressions": ["k8s_service_pod_cpu_usage"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "pod_memory_usage",
+        "title": "Pod Memory Usage",
+        "type": "line",
+        "unit": "Mi",
+        "expressions": ["k8s_service_pod_memory_usage/1024/1024"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "http_call_cpm",
+        "title": "HTTP Request RPM",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["kubernetes_service_http_call_cpm"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "http_call_time",
+        "title": "HTTP Response Time",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["kubernetes_service_http_call_time"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "http_status",
+        "title": "HTTP Status Code RPM",
+        "tip": "Per-class status code counts (1xx / 2xx / 3xx / 4xx / 5xx).",
+        "type": "line",
+        "expressions": [
+          "kubernetes_service_http_status_1xx_cpm",
+          "kubernetes_service_http_status_2xx_cpm",
+          "kubernetes_service_http_status_3xx_cpm",
+          "kubernetes_service_http_status_4xx_cpm",
+          "kubernetes_service_http_status_5xx_cpm"
+        ],
+        "expressionLabels": ["1xx", "2xx", "3xx", "4xx", "5xx"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "http_req_resp_size",
+        "title": "HTTP Request / Response Size",
+        "type": "line",
+        "unit": "KB",
+        "expressions": [
+          "kubernetes_service_http_avg_req_header_size/1024",
+          "kubernetes_service_http_avg_req_body_size/1024",
+          "kubernetes_service_http_avg_resp_header_size/1024",
+          "kubernetes_service_http_avg_resp_body_size/1024"
+        ],
+        "expressionLabels": ["req hdr", "req body", "resp hdr", "resp body"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "tcp_connect",
+        "title": "TCP Connect",
+        "tip": "Client-side connect attempts and successes per minute.",
+        "type": "line",
+        "expressions": [
+          "kubernetes_service_connect_cpm",
+          "kubernetes_service_connect_success_cpm"
+        ],
+        "expressionLabels": ["connect", "success"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "tcp_connect_time",
+        "title": "TCP Connect Duration",
+        "type": "line",
+        "unit": "ns",
+        "expressions": ["kubernetes_service_connect_time"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "tcp_accept",
+        "title": "TCP Accept",
+        "type": "line",
+        "expressions": [
+          "kubernetes_service_accept_cpm"
+        ],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "tcp_packets",
+        "title": "TCP Packets",
+        "tip": "Read + write packet counts per minute, including retransmits.",
+        "type": "line",
+        "expressions": [
+          "kubernetes_service_read_package_cpm",
+          "kubernetes_service_write_package_cpm",
+          "kubernetes_service_write_retrains_package_cpm"
+        ],
+        "expressionLabels": ["read", "write", "write retrans"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "tcp_bytes",
+        "title": "TCP Bytes",
+        "type": "line",
+        "unit": "KB",
+        "expressions": [
+          "kubernetes_service_read_package_size/1024",
+          "kubernetes_service_write_package_size/1024"
+        ],
+        "expressionLabels": ["read", "write"],
+        "span": 6,
+        "rowSpan": 2
+      }
+    ],
+    "instance": [
+      {
+        "id": "pod_http_cpm",
+        "title": "Pod HTTP RPM",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["kubernetes_service_instance_http_call_cpm"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "pod_http_time",
+        "title": "Pod HTTP Response Time",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["kubernetes_service_instance_http_call_time"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "pod_http_status",
+        "title": "Pod HTTP Status Code RPM",
+        "type": "line",
+        "expressions": [
+          "kubernetes_service_instance_http_status_1xx_cpm",
+          "kubernetes_service_instance_http_status_2xx_cpm",
+          "kubernetes_service_instance_http_status_3xx_cpm",
+          "kubernetes_service_instance_http_status_4xx_cpm",
+          "kubernetes_service_instance_http_status_5xx_cpm"
+        ],
+        "expressionLabels": ["1xx", "2xx", "3xx", "4xx", "5xx"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "pod_http_sizes",
+        "title": "Pod HTTP Sizes",
+        "type": "line",
+        "unit": "KB",
+        "expressions": [
+          "kubernetes_service_instance_http_avg_req_header_size/1024",
+          "kubernetes_service_instance_http_avg_req_body_size/1024",
+          "kubernetes_service_instance_http_avg_resp_header_size/1024",
+          "kubernetes_service_instance_http_avg_resp_body_size/1024"
+        ],
+        "expressionLabels": ["req hdr", "req body", "resp hdr", "resp body"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "pod_tcp_connect",
+        "title": "Pod TCP Connect",
+        "type": "line",
+        "expressions": [
+          "kubernetes_service_instance_connect_cpm",
+          "kubernetes_service_instance_connect_success_cpm"
+        ],
+        "expressionLabels": ["connect", "success"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "pod_tcp_packets",
+        "title": "Pod TCP Packets",
+        "type": "line",
+        "expressions": [
+          "kubernetes_service_instance_read_package_cpm",
+          "kubernetes_service_instance_write_package_cpm",
+          "kubernetes_service_instance_write_retrains_package_cpm"
+        ],
+        "expressionLabels": ["read", "write", "write retrans"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "pod_tcp_bytes",
+        "title": "Pod TCP Bytes",
+        "type": "line",
+        "unit": "KB",
+        "expressions": [
+          "kubernetes_service_instance_read_package_size/1024",
+          "kubernetes_service_instance_write_package_size/1024"
+        ],
+        "expressionLabels": ["read", "write"],
+        "span": 4,
+        "rowSpan": 2
+      }
+    ],
+    "endpoint": [
+      {
+        "id": "ep_http_cpm",
+        "title": "Endpoint HTTP RPM",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["kubernetes_service_endpoint_http_call_cpm"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "ep_http_time",
+        "title": "Endpoint HTTP Response Time",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["kubernetes_service_endpoint_http_call_time"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "ep_http_status",
+        "title": "Endpoint Status Code RPM",
+        "type": "line",
+        "expressions": [
+          "kubernetes_service_endpoint_http_status_1xx_cpm",
+          "kubernetes_service_endpoint_http_status_2xx_cpm",
+          "kubernetes_service_endpoint_http_status_3xx_cpm",
+          "kubernetes_service_endpoint_http_status_4xx_cpm",
+          "kubernetes_service_endpoint_http_status_5xx_cpm"
+        ],
+        "expressionLabels": ["1xx", "2xx", "3xx", "4xx", "5xx"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "ep_http_req_sizes",
+        "title": "Endpoint Request Sizes",
+        "type": "line",
+        "unit": "KB",
+        "expressions": [
+          "kubernetes_service_endpoint_http_avg_req_header_size/1024",
+          "kubernetes_service_endpoint_http_avg_req_body_size/1024"
+        ],
+        "expressionLabels": ["header", "body"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "ep_http_resp_sizes",
+        "title": "Endpoint Response Sizes",
+        "type": "line",
+        "unit": "KB",
+        "expressions": [
+          "kubernetes_service_endpoint_http_avg_resp_header_size/1024",
+          "kubernetes_service_endpoint_http_avg_resp_body_size/1024"
+        ],
+        "expressionLabels": ["header", "body"],
+        "span": 6,
+        "rowSpan": 2
+      }
+    ]
+  },
+  "topology": {
+    "nodeMetrics": [
+      { "id": "httpCpm", "label": "RPM", "mqe": "kubernetes_service_http_call_cpm", "unit": "rpm", "role": "center", "aggregation": "avg" },
+      {
+        "id": "httpSuccess",
+        "label": "Success Rate",
+        "mqe": "kubernetes_service_http_call_successful_rate/100",
+        "unit": "%",
+        "role": "ring",
+        "aggregation": "avg",
+        "thresholds": {
+          "invertHealth": true,
+          "invertBase": 100,
+          "ok": 0.1,
+          "warn": 1,
+          "danger": 5
+        }
+      },
+      { "id": "httpRespTime", "label": "Latency", "mqe": "kubernetes_service_http_call_time", "unit": "ms", "role": "secondary", "aggregation": "avg" }
+    ],
+    "linkServerMetrics": [
+      { "id": "cpm", "label": "RPM", "mqe": "kubernetes_service_relation_server_http_call_cpm", "unit": "rpm", "role": "lineServer", "aggregation": "avg" },
+      { "id": "respTime", "label": "Avg response time", "mqe": "kubernetes_service_relation_server_http_call_time", "unit": "ms", "aggregation": "avg" }
+    ],
+    "linkClientMetrics": [
+      { "id": "cpm", "label": "RPM", "mqe": "kubernetes_service_relation_client_http_call_cpm", "unit": "rpm", "role": "lineClient", "aggregation": "avg" },
+      { "id": "respTime", "label": "Avg response time", "mqe": "kubernetes_service_relation_client_http_call_time", "unit": "ms", "aggregation": "avg" }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/kafka.json b/apps/bff/src/bundled_templates/layers/kafka.json
new file mode 100644
index 0000000..32d1280
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/kafka.json
@@ -0,0 +1,54 @@
+{
+  "key": "KAFKA",
+  "alias": "Kafka",
+  "color": "var(--sw-purple)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-kafka-monitoring/",
+  "aliases": { "services": "Kafka clusters", "instances": "Brokers" },
+  "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "partitions",
+    "columns": [
+      { "metric": "partitions", "label": "Partitions", "mqe": "meter_kafka_partition_count", "aggregation": "avg" },
+      { "metric": "offline", "label": "Offline Partitions", "mqe": "meter_kafka_offline_partitions_count", "aggregation": "sum" },
+      { "metric": "maxLag", "label": "Max Lag", "mqe": "meter_kafka_max_lag", "aggregation": "max" },
+      { "metric": "leaders", "label": "Leaders", "mqe": "meter_kafka_leader_count", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Cluster", "size": "auto", "metrics": [
+        { "id": "offline", "label": "Offline Partitions", "tip": "Partitions without an active leader.", "mqe": "meter_kafka_offline_partitions_count", "aggregation": "sum" },
+        { "id": "underReplicated", "label": "Under-Replicated", "tip": "Partitions whose ISR set is below replication factor.", "mqe": "meter_kafka_under_replicated_partitions", "aggregation": "sum" },
+        { "id": "maxLag", "label": "Max Lag", "tip": "Largest consumer lag observed.", "mqe": "meter_kafka_max_lag", "aggregation": "max" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "partitions", "title": "Partition Count", "type": "line", "expressions": ["meter_kafka_partition_count"], "span": 3, "rowSpan": 2 },
+      { "id": "leaders", "title": "Leader Count", "type": "line", "expressions": ["meter_kafka_leader_count"], "span": 3, "rowSpan": 2 },
+      { "id": "active_ctrl", "title": "Active Controllers", "type": "line", "expressions": ["meter_kafka_active_controller_count"], "span": 3, "rowSpan": 2 },
+      { "id": "max_lag", "title": "Max Lag", "type": "line", "expressions": ["meter_kafka_max_lag"], "span": 3, "rowSpan": 2 },
+      { "id": "under_replicated", "title": "Under-Replicated Partitions", "type": "line", "expressions": ["meter_kafka_under_replicated_partitions"], "span": 4, "rowSpan": 2 },
+      { "id": "offline", "title": "Offline Partitions", "type": "line", "expressions": ["meter_kafka_offline_partitions_count"], "span": 4, "rowSpan": 2 },
+      { "id": "leader_election", "title": "Leader Election Rate", "type": "line", "expressions": ["meter_kafka_leader_election_rate", "meter_kafka_unclean_leader_elections_per_second"], "expressionLabels": ["elections", "unclean"], "span": 4, "rowSpan": 2 }
+    ],
+    "instance": [
+      { "id": "cpu", "title": "CPU Usage", "type": "line", "unit": "%", "expressions": ["meter_kafka_broker_cpu_time_total"], "span": 3, "rowSpan": 2 },
+      { "id": "msg_rate", "title": "Incoming Messages / s", "type": "line", "expressions": ["meter_kafka_broker_messages_per_second"], "span": 3, "rowSpan": 2 },
+      { "id": "bytes_in", "title": "Bytes In / s", "type": "line", "expressions": ["meter_kafka_broker_bytes_in_per_second"], "span": 3, "rowSpan": 2 },
+      { "id": "bytes_out", "title": "Bytes Out / s", "type": "line", "expressions": ["meter_kafka_broker_bytes_out_per_second"], "span": 3, "rowSpan": 2 },
+      { "id": "req_rate", "title": "Requests / s", "type": "line", "expressions": ["meter_kafka_broker_requests_per_second"], "span": 3, "rowSpan": 2 },
+      { "id": "purgatory", "title": "Purgatory Size", "type": "line", "expressions": ["meter_kafka_broker_purgatory_size"], "span": 3, "rowSpan": 2 },
+      { "id": "isr", "title": "ISR Shrinks / Expands", "type": "line", "expressions": ["latest(meter_kafka_broker_isr_shrinks_per_second)", "latest(meter_kafka_broker_isr_expands_per_second)"], "expressionLabels": ["shrinks", "expands"], "span": 3, "rowSpan": 2 },
+      { "id": "mem_pct", "title": "Memory Usage (%)", "type": "line", "unit": "%", "expressions": ["meter_kafka_broker_memory_usage_percentage"], "span": 3, "rowSpan": 2 },
+      { "id": "under_replicated", "title": "Under-Replicated", "type": "line", "expressions": ["latest(meter_kafka_broker_under_replicated_partitions)", "latest(meter_kafka_broker_under_min_isr_partition_count)"], "expressionLabels": ["under-replicated", "under-min-isr"], "span": 6, "rowSpan": 2 },
+      { "id": "partitions_leaders", "title": "Partitions + Leaders", "type": "line", "expressions": ["meter_kafka_broker_partition_count", "meter_kafka_broker_leader_count"], "expressionLabels": ["partitions", "leaders"], "span": 6, "rowSpan": 2 },
+      { "id": "queue_times", "title": "Queue / Send Times", "type": "line", "unit": "ms", "expressions": ["meter_kafka_broker_request_queue_time_ms", "meter_kafka_broker_response_queue_time_ms", "meter_kafka_broker_response_send_time_ms", "meter_kafka_broker_remote_time_ms"], "expressionLabels": ["request q", "response q", "response send", "remote"], "span": 6, "rowSpan": 2 },
+      { "id": "topic_rates", "title": "Topic Rates", "type": "line", "expressions": ["meter_kafka_broker_topic_produce_requests_per_second", "meter_kafka_broker_topic_fetch_requests_per_second", "meter_kafka_broker_topic_bytesin_per_second"], "expressionLabels": ["produce req/s", "fetch req/s", "bytes-in/s"], "span": 6, "rowSpan": 2 },
+      { "id": "replication", "title": "Replication", "type": "line", "expressions": ["meter_kafka_broker_replication_bytes_in_per_second", "meter_kafka_broker_replication_bytes_out_per_second"], "expressionLabels": ["bytes in", "bytes out"], "span": 6, "rowSpan": 2 },
+      { "id": "gc", "title": "GC Count", "type": "line", "expressions": ["meter_kafka_broker_garbage_collector_count"], "span": 6, "rowSpan": 2 },
+      { "id": "max_lag", "title": "Max Lag (broker)", "type": "line", "expressions": ["sum(meter_kafka_broker_max_lag)"], "span": 6, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/kong.json b/apps/bff/src/bundled_templates/layers/kong.json
new file mode 100644
index 0000000..9eee3ce
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/kong.json
@@ -0,0 +1,81 @@
+{
+  "key": "KONG",
+  "alias": "Kong",
+  "color": "var(--sw-ok)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-kong-monitoring/",
+  "aliases": {
+    "services": "Kong services",
+    "instances": "Nodes",
+    "endpoints": "Routes"
+  },
+  "components": {
+    "service": true,
+    "instances": true,
+    "endpoints": true,
+    "topology": false,
+    "traces": false,
+    "logs": false
+  },
+  "layer-header": {
+    "orderBy": "rps",
+    "columns": [
+      { "metric": "rps", "label": "RPS", "mqe": "aggregate_labels(meter_kong_service_http_requests,sum)", "aggregation": "sum" },
+      { "metric": "status200", "label": "200/s", "mqe": "aggregate_labels(meter_kong_service_http_status{code='200'}, sum)", "aggregation": "sum" },
+      { "metric": "status404", "label": "404/s", "mqe": "aggregate_labels(meter_kong_service_http_status{code='404'}, sum)", "aggregation": "sum" },
+      { "metric": "status500", "label": "500/s", "mqe": "aggregate_labels(meter_kong_service_http_status{code='500'}, sum)", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      {
+        "title": "Latency",
+        "size": "auto",
+        "metrics": [
+          { "id": "kongLatency", "label": "Kong Latency", "tip": "Time Kong itself spends per request, p50/p95.", "mqe": "aggregate_labels(meter_kong_service_kong_latency,avg(p))", "unit": "ms", "aggregation": "avg" },
+          { "id": "upstreamLatency", "label": "Upstream Latency", "tip": "Time waiting on the upstream service.", "mqe": "aggregate_labels(meter_kong_service_upstream_latency,avg(p))", "unit": "ms", "aggregation": "avg" }
+        ]
+      },
+      {
+        "title": "Throughput",
+        "size": "square",
+        "metrics": [
+          { "id": "rps", "label": "RPS", "tip": "Requests per second through Kong.", "mqe": "aggregate_labels(meter_kong_service_http_requests,sum)", "aggregation": "sum" }
+        ]
+      }
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "rps", "title": "HTTP Request Trend", "type": "line", "expressions": ["aggregate_labels(meter_kong_service_http_requests,sum)"], "span": 4, "rowSpan": 2 },
+      { "id": "status", "title": "HTTP Status Trend", "type": "line", "expressions": ["aggregate_labels(meter_kong_service_http_status,sum(code))"], "span": 4, "rowSpan": 2 },
+      { "id": "bandwidth", "title": "HTTP Bandwidth", "type": "line", "unit": "KB/s", "expressions": ["aggregate_labels(meter_kong_service_http_bandwidth,sum(direction))/1024"], "span": 4, "rowSpan": 2 },
+      { "id": "kong_latency", "title": "Kong Latency", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_kong_service_kong_latency,avg(p))"], "span": 4, "rowSpan": 2 },
+      { "id": "req_latency", "title": "Request Latency", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_kong_service_request_latency,avg(p))"], "span": 4, "rowSpan": 2 },
+      { "id": "upstream_latency", "title": "Upstream Latency", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_kong_service_upstream_latency,avg(p))"], "span": 4, "rowSpan": 2 },
+      { "id": "conns", "title": "Nginx Connections", "type": "line", "expressions": ["aggregate_labels(meter_kong_service_nginx_connections_total,sum(state))"], "span": 6, "rowSpan": 2 },
+      { "id": "timers", "title": "Nginx Timers", "type": "line", "expressions": ["aggregate_labels(meter_kong_service_nginx_timers,sum(state))"], "span": 6, "rowSpan": 2 },
+      { "id": "datastore", "title": "Datastore Reachable", "type": "line", "expressions": ["latest(aggregate_labels(meter_kong_service_datastore_reachable,sum(service_instance_id)))"], "span": 6, "rowSpan": 2 },
+      { "id": "nginx_errors", "title": "Nginx Metric Errors", "type": "line", "expressions": ["latest(aggregate_labels(meter_kong_service_nginx_metric_errors_total,sum))"], "span": 6, "rowSpan": 2 }
+    ],
+    "instance": [
+      { "id": "rps", "title": "HTTP Request Trend", "type": "line", "expressions": ["meter_kong_instance_http_requests"], "span": 4, "rowSpan": 2 },
+      { "id": "status", "title": "HTTP Status Trend", "type": "line", "expressions": ["meter_kong_instance_http_status"], "span": 4, "rowSpan": 2 },
+      { "id": "bandwidth", "title": "HTTP Bandwidth", "type": "line", "unit": "KB/s", "expressions": ["meter_kong_instance_http_bandwidth/1024"], "span": 4, "rowSpan": 2 },
+      { "id": "kong_latency", "title": "Kong Latency", "type": "line", "unit": "ms", "expressions": ["meter_kong_instance_kong_latency"], "span": 4, "rowSpan": 2 },
+      { "id": "req_latency", "title": "Request Latency", "type": "line", "unit": "ms", "expressions": ["meter_kong_instance_request_latency"], "span": 4, "rowSpan": 2 },
+      { "id": "upstream_latency", "title": "Upstream Latency", "type": "line", "unit": "ms", "expressions": ["meter_kong_instance_upstream_latency"], "span": 4, "rowSpan": 2 },
+      { "id": "datastore", "title": "Datastore Reachable", "type": "line", "expressions": ["latest(meter_kong_instance_datastore_reachable)"], "span": 4, "rowSpan": 2 },
+      { "id": "conns", "title": "Nginx Connections", "type": "line", "expressions": ["meter_kong_instance_nginx_connections_total"], "span": 4, "rowSpan": 2 },
+      { "id": "timers", "title": "Nginx Timers", "type": "line", "expressions": ["meter_kong_instance_nginx_timers"], "span": 4, "rowSpan": 2 },
+      { "id": "shared_mem", "title": "Shared Memory Usage", "type": "line", "unit": "%", "expressions": ["(meter_kong_instance_shared_dict_bytes*100)/meter_kong_instance_shared_dict_total_bytes"], "span": 6, "rowSpan": 2 },
+      { "id": "lua_vm", "title": "Worker Lua VM Usage", "type": "line", "unit": "MB", "expressions": ["meter_kong_instance_memory_workers_lua_vms_bytes/1024/1024"], "span": 6, "rowSpan": 2 }
+    ],
+    "endpoint": [
+      { "id": "status", "title": "HTTP Status Trend", "type": "line", "expressions": ["meter_kong_endpoint_http_status"], "span": 4, "rowSpan": 2 },
+      { "id": "bandwidth", "title": "Total Bandwidth", "type": "line", "unit": "KB/s", "expressions": ["meter_kong_endpoint_http_bandwidth/1024"], "span": 4, "rowSpan": 2 },
+      { "id": "kong_latency", "title": "Kong Latency", "type": "line", "unit": "ms", "expressions": ["meter_kong_endpoint_kong_latency"], "span": 4, "rowSpan": 2 },
+      { "id": "req_latency", "title": "Request Latency", "type": "line", "unit": "ms", "expressions": ["meter_kong_endpoint_request_latency"], "span": 6, "rowSpan": 2 },
+      { "id": "upstream_latency", "title": "Upstream Latency", "type": "line", "unit": "ms", "expressions": ["meter_kong_endpoint_upstream_latency"], "span": 6, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/mesh.json b/apps/bff/src/bundled_templates/layers/mesh.json
new file mode 100644
index 0000000..d7b34b1
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/mesh.json
@@ -0,0 +1,455 @@
+{
+  "key": "MESH",
+  "alias": "Service Mesh",
+  "color": "var(--sw-info)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/envoy/als_setting/",
+  "aliases": {
+    "services": "Services",
+    "instances": "Sidecars",
+    "endpoints": "Endpoints"
+  },
+  "components": {
+    "service": true,
+    "instances": true,
+    "endpoints": true,
+    "endpointDependency": false,
+    "topology": true,
+    "traces": true,
+    "logs": true,
+    "ebpfProfiling": true
+  },
+  "layer-header": {
+    "orderBy": "cpm",
+    "columns": [
+      { "metric": "cpm", "label": "Traffic", "unit": "rpm", "mqe": "service_cpm", "aggregation": "sum" },
+      { "metric": "apdex", "label": "Apdex", "mqe": "service_apdex/10000", "aggregation": "avg" },
+      { "metric": "respTime", "label": "Latency", "unit": "ms", "mqe": "service_resp_time", "aggregation": "avg" },
+      { "metric": "err", "label": "Error Rate", "unit": "%", "mqe": "100 - service_sla/100", "aggregation": "avg" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      {
+        "title": "Latency & errors",
+        "size": "auto",
+        "metrics": [
+          {
+            "id": "p95",
+            "label": "P95",
+            "tip": "95th-percentile sidecar-routed response time across the mesh.",
+            "mqe": "service_percentile{p='95'}",
+            "unit": "ms",
+            "aggregation": "avg"
+          },
+          {
+            "id": "err",
+            "label": "Error Rate",
+            "tip": "100 − mesh SLA. Calls with non-2xx response status from sidecar ALS.",
+            "mqe": "100 - service_sla/100",
+            "unit": "%",
+            "aggregation": "avg"
+          }
+        ]
+      },
+      {
+        "title": "Health",
+        "size": "square",
+        "metrics": [
+          {
+            "id": "apdex",
+            "label": "Apdex",
+            "tip": "User-satisfaction score on a 0 – 1 scale.",
+            "mqe": "service_apdex/10000",
+            "aggregation": "avg"
+          }
+        ]
+      }
+    ]
+  },
+  "dashboards": {
+    "service": [
+      {
+        "id": "top_apis",
+        "title": "Top 20 APIs",
+        "tip": "Layer-wide endpoint ranking. Switch tabs to re-rank by traffic, response time, or worst success rate.",
+        "type": "top",
+        "expressions": [
+          "top_n(endpoint_cpm,20,des)",
+          "top_n(endpoint_resp_time,20,des)",
+          "top_n(endpoint_sla,20,asc)/100"
+        ],
+        "expressionLabels": ["Traffic", "Slow", "Successful Rate"],
+        "expressionUnits": ["rpm", "ms", "%"],
+        "layerScope": true,
+        "span": 3,
+        "rowSpan": 4
+      },
+      {
+        "id": "traffic_line",
+        "title": "Traffic",
+        "tip": "Mesh-wide requests per minute observed by Envoy sidecars.",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["service_cpm"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "err_line",
+        "title": "Error Rate",
+        "type": "line",
+        "unit": "%",
+        "expressions": ["100 - service_sla/100"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "apdex_line",
+        "title": "Apdex",
+        "tip": "User-satisfaction score on a 0 – 1 scale.",
+        "type": "line",
+        "expressions": ["service_apdex/10000"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "resp_time_line",
+        "title": "Avg Response Time",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["service_resp_time"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "percentile_line",
+        "title": "Response Time Percentile",
+        "tip": "p50 / p75 / p90 / p95 / p99 latency — the tail of the response-time distribution.",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(service_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "throughput_line",
+        "title": "Service Throughput",
+        "tip": "Bytes per minute through the sidecar — sent + received.",
+        "type": "line",
+        "unit": "B/min",
+        "expressions": [
+          "service_throughput_received",
+          "service_throughput_sent"
+        ],
+        "expressionLabels": ["received", "sent"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "sidecar_internal_latency",
+        "title": "Sidecar Internal Latency",
+        "tip": "Envoy-internal request + response latency, in nanoseconds. Useful when sidecar overhead is suspected.",
+        "type": "line",
+        "unit": "ns",
+        "expressions": [
+          "service_sidecar_internal_req_latency_nanos",
+          "service_sidecar_internal_resp_latency_nanos"
+        ],
+        "expressionLabels": ["request", "response"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "top_instances",
+        "title": "Top 10 sidecars",
+        "tip": "Ranked across the selected service's sidecar instances.",
+        "type": "top",
+        "expressions": [
+          "top_n(service_instance_cpm,10,des)",
+          "top_n(service_instance_resp_time,10,des)",
+          "top_n(service_instance_sla,10,asc)/100"
+        ],
+        "expressionLabels": ["Traffic", "Slow", "Successful Rate"],
+        "expressionUnits": ["rpm", "ms", "%"],
+        "span": 6,
+        "rowSpan": 3
+      },
+      {
+        "id": "top_endpoints_in_service",
+        "title": "Top 10 endpoints in service",
+        "tip": "Ranked across the selected service's endpoints.",
+        "type": "top",
+        "expressions": [
+          "top_n(endpoint_cpm,10,des)",
+          "top_n(endpoint_resp_time,10,des)",
+          "top_n(endpoint_sla,10,asc)/100"
+        ],
+        "expressionLabels": ["Traffic", "Slow", "Successful Rate"],
+        "expressionUnits": ["rpm", "ms", "%"],
+        "span": 6,
+        "rowSpan": 3
+      }
+    ],
+    "instance": [
+      {
+        "id": "instance_cpm_line",
+        "title": "Sidecar Load",
+        "tip": "Calls per minute against the selected sidecar instance.",
+        "type": "line",
+        "unit": "calls / min",
+        "expressions": ["service_instance_cpm"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_resp_line",
+        "title": "Sidecar Latency",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["service_instance_resp_time"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_sla_line",
+        "title": "Sidecar Success Rate",
+        "type": "line",
+        "unit": "%",
+        "expressions": ["service_instance_sla/100"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_throughput",
+        "title": "Sidecar Throughput",
+        "tip": "Bytes through the sidecar — received + sent.",
+        "type": "line",
+        "unit": "B/min",
+        "expressions": [
+          "service_instance_throughput_received",
+          "service_instance_throughput_sent"
+        ],
+        "expressionLabels": ["received", "sent"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_sidecar_internal_latency",
+        "title": "Sidecar Internal Latency",
+        "type": "line",
+        "unit": "ns",
+        "expressions": [
+          "service_instance_sidecar_internal_req_latency_nanos",
+          "service_instance_sidecar_internal_resp_latency_nanos"
+        ],
+        "expressionLabels": ["request", "response"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "envoy_up_rq_active",
+        "title": "Envoy Upstream Request Active",
+        "tip": "In-flight upstream requests per cluster. Renders for Envoy-instrumented sidecars.",
+        "type": "line",
+        "expressions": ["envoy_cluster_up_rq_active"],
+        "visibleWhen": "envoy_cluster_up_rq_active has value",
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "envoy_up_rq_incr",
+        "title": "Envoy Upstream Request Increase",
+        "tip": "Upstream requests added per minute.",
+        "type": "line",
+        "expressions": ["envoy_cluster_up_rq_incr"],
+        "visibleWhen": "envoy_cluster_up_rq_incr has value",
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "envoy_up_rq_pending_active",
+        "title": "Envoy Upstream Pending Active",
+        "tip": "Pending upstream requests — indicates connection-pool back-pressure.",
+        "type": "line",
+        "expressions": ["envoy_cluster_up_rq_pending_active"],
+        "visibleWhen": "envoy_cluster_up_rq_pending_active has value",
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "envoy_up_cx_active",
+        "title": "Envoy Upstream Connection Active",
+        "type": "line",
+        "expressions": ["envoy_cluster_up_cx_active"],
+        "visibleWhen": "envoy_cluster_up_cx_active has value",
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "envoy_up_cx_incr",
+        "title": "Envoy Upstream Connection Increase",
+        "type": "line",
+        "expressions": ["envoy_cluster_up_cx_incr"],
+        "visibleWhen": "envoy_cluster_up_cx_incr has value",
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "envoy_membership_healthy",
+        "title": "Envoy Cluster Healthy Membership",
+        "tip": "Healthy upstream members per cluster — a non-trivial drop signals upstream churn.",
+        "type": "line",
+        "expressions": ["envoy_cluster_membership_healthy"],
+        "visibleWhen": "envoy_cluster_membership_healthy has value",
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "envoy_connections",
+        "title": "Envoy Total Connections",
+        "type": "line",
+        "expressions": [
+          "envoy_total_connections_used",
+          "envoy_parent_connections_used"
+        ],
+        "expressionLabels": ["total", "parent"],
+        "visibleWhen": "envoy_total_connections_used has value",
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "envoy_heap_memory",
+        "title": "Envoy Heap Memory",
+        "tip": "Envoy memory usage. Heap vs physical, allocated vs max, in MB.",
+        "type": "line",
+        "unit": "MB",
+        "expressions": [
+          "envoy_heap_memory_used/1024/1024",
+          "envoy_heap_memory_max_used/1024/1024",
+          "envoy_memory_allocated/1024/1024",
+          "envoy_memory_allocated_max/1024/1024",
+          "envoy_memory_physical_size/1024/1024",
+          "envoy_memory_physical_size_max/1024/1024"
+        ],
+        "expressionLabels": ["heap used", "heap max", "allocated", "allocated max", "physical", "physical max"],
+        "visibleWhen": "envoy_heap_memory_used has value",
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "envoy_worker_threads",
+        "title": "Envoy Worker Threads",
+        "type": "line",
+        "expressions": [
+          "envoy_worker_threads",
+          "envoy_worker_threads_max"
+        ],
+        "expressionLabels": ["live", "max"],
+        "visibleWhen": "envoy_worker_threads has value",
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "envoy_bug_failures",
+        "title": "Envoy Bug Failures",
+        "tip": "Envoy's own assertion / bug counter. Should be zero in healthy clusters.",
+        "type": "line",
+        "expressions": ["envoy_bug_failures"],
+        "visibleWhen": "envoy_bug_failures has value",
+        "span": 3,
+        "rowSpan": 2
+      }
+    ],
+    "endpoint": [
+      {
+        "id": "endpoint_cpm_line",
+        "title": "Endpoint Traffic",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["endpoint_cpm"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "endpoint_resp_line",
+        "title": "Endpoint Avg Response Time",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["endpoint_resp_time"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "endpoint_sla_line",
+        "title": "Endpoint Success Rate",
+        "type": "line",
+        "unit": "%",
+        "expressions": ["endpoint_sla/100"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "endpoint_percentile",
+        "title": "Endpoint Response Time Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(endpoint_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 12,
+        "rowSpan": 2
+      },
+      {
+        "id": "endpoint_sidecar_internal_latency",
+        "title": "Sidecar Internal Latency (endpoint scope)",
+        "tip": "Envoy-internal request + response latency on this endpoint, in nanoseconds.",
+        "type": "line",
+        "unit": "ns",
+        "expressions": [
+          "endpoint_sidecar_internal_req_latency_nanos",
+          "endpoint_sidecar_internal_resp_latency_nanos"
+        ],
+        "expressionLabels": ["request", "response"],
+        "span": 12,
+        "rowSpan": 2
+      }
+    ],
+    "trace": [],
+    "ebpfProfiling": []
+  },
+  "topology": {
+    "nodeMetrics": [
+      { "id": "cpm", "label": "RPM", "mqe": "service_cpm", "unit": "rpm", "role": "center", "aggregation": "avg" },
+      {
+        "id": "sla",
+        "label": "SLA",
+        "mqe": "service_sla/100",
+        "unit": "%",
+        "role": "ring",
+        "aggregation": "avg",
+        "thresholds": {
+          "invertHealth": true,
+          "invertBase": 100,
+          "ok": 0.1,
+          "warn": 1,
+          "danger": 5
+        }
+      },
+      { "id": "respTime", "label": "Latency", "mqe": "service_resp_time", "unit": "ms", "role": "secondary", "aggregation": "avg" }
+    ],
+    "linkServerMetrics": [
+      { "id": "cpm", "label": "RPM", "mqe": "service_relation_server_cpm", "unit": "rpm", "role": "lineServer", "aggregation": "avg" },
+      { "id": "respTime", "label": "Avg response time", "mqe": "service_relation_server_resp_time", "unit": "ms", "aggregation": "avg" },
+      { "id": "p95", "label": "p95", "mqe": "service_relation_server_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+      { "id": "sla", "label": "SLA", "mqe": "service_relation_server_call_sla/100", "unit": "%", "aggregation": "avg" }
+    ],
+    "linkClientMetrics": [
+      { "id": "cpm", "label": "RPM", "mqe": "service_relation_client_cpm", "unit": "rpm", "role": "lineClient", "aggregation": "avg" },
+      { "id": "respTime", "label": "Avg response time", "mqe": "service_relation_client_resp_time", "unit": "ms", "aggregation": "avg" },
+      { "id": "p95", "label": "p95", "mqe": "service_relation_client_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+      { "id": "sla", "label": "SLA", "mqe": "service_relation_client_call_sla/100", "unit": "%", "aggregation": "avg" }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/mesh_cp.json b/apps/bff/src/bundled_templates/layers/mesh_cp.json
new file mode 100644
index 0000000..15e03e5
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/mesh_cp.json
@@ -0,0 +1,40 @@
+{
+  "key": "MESH_CP",
+  "alias": "Mesh Control Plane",
+  "color": "var(--sw-info)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/istio/readme/",
+  "aliases": { "services": "Control Planes" },
+  "components": { "service": true, "instances": false, "endpoints": false, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "cpu",
+    "columns": [
+      { "metric": "cpu", "label": "CPU", "mqe": "meter_istio_cpu", "aggregation": "avg" },
+      { "metric": "goroutines", "label": "Goroutines", "mqe": "meter_istio_go_goroutines", "aggregation": "sum" },
+      { "metric": "xds", "label": "Pilot xDS", "mqe": "meter_istio_pilot_xds", "aggregation": "sum" },
+      { "metric": "services", "label": "Services", "mqe": "meter_istio_pilot_services", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Istio health", "size": "auto", "metrics": [
+        { "id": "cpu", "label": "CPU", "tip": "Istio control-plane process CPU.", "mqe": "meter_istio_cpu", "aggregation": "avg" },
+        { "id": "goroutines", "label": "Goroutines", "tip": "Live Go goroutines.", "mqe": "meter_istio_go_goroutines", "aggregation": "sum" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "cpu", "title": "CPU", "type": "line", "expressions": ["meter_istio_cpu"], "span": 4, "rowSpan": 2 },
+      { "id": "goroutines", "title": "Goroutines", "type": "line", "expressions": ["meter_istio_go_goroutines"], "span": 4, "rowSpan": 2 },
+      { "id": "version", "title": "Istio Versions", "type": "line", "expressions": ["meter_istio_pilot_version"], "span": 4, "rowSpan": 2 },
+      { "id": "memory", "title": "Memory (MB)", "type": "line", "unit": "MB", "expressions": ["meter_istio_go_alloc/1024/1024", "meter_istio_go_heap_inuse/1024/1024", "meter_istio_go_stack_inuse/1024/1024", "meter_istio_virtual_memory/1024/1024", "meter_istio_resident_memory/1024/1024"], "expressionLabels": ["alloc", "heap inuse", "stack inuse", "virtual", "resident"], "span": 12, "rowSpan": 2 },
+      { "id": "pilot_errors", "title": "Pilot Errors", "type": "line", "expressions": ["meter_istio_pilot_xds_cds_reject", "meter_istio_pilot_xds_eds_reject", "meter_istio_pilot_xds_rds_reject", "meter_istio_pilot_xds_lds_reject", "meter_istio_pilot_xds_write_timeout"], "expressionLabels": ["cds reject", "eds reject", "rds reject", "lds reject", "write timeout"], "span": 6, "rowSpan": 2 },
+      { "id": "proxy_push_time", "title": "Proxy Push Time (percentile)", "type": "line", "unit": "ms", "expressions": ["meter_istio_pilot_proxy_push_percentile"], "span": 6, "rowSpan": 2 },
+      { "id": "pilot_pushes", "title": "Pilot Pushes", "type": "line", "expressions": ["meter_istio_pilot_xds_pushes"], "span": 4, "rowSpan": 2 },
+      { "id": "sidecar_inj", "title": "Sidecar Injection Success", "type": "line", "expressions": ["meter_istio_sidecar_injection_success_total"], "span": 4, "rowSpan": 2 },
+      { "id": "ads", "title": "ADS Monitoring", "type": "line", "expressions": ["meter_istio_pilot_xds", "meter_istio_pilot_services", "meter_istio_pilot_virt_services"], "expressionLabels": ["xds", "services", "virt services"], "span": 4, "rowSpan": 2 },
+      { "id": "validation", "title": "Configuration Validation", "type": "line", "expressions": ["meter_istio_galley_validation_passed", "meter_istio_galley_validation_failed"], "expressionLabels": ["passed", "failed"], "span": 6, "rowSpan": 2 },
+      { "id": "conflicts", "title": "Pilot Conflicts", "type": "line", "expressions": ["meter_istio_pilot_conflict_ol_tcp_tcp", "meter_istio_pilot_conflict_il", "meter_istio_pilot_conflict_ol_tcp_http", "meter_istio_pilot_conflict_ol_http_tcp"], "expressionLabels": ["ol tcp/tcp", "il", "ol tcp/http", "ol http/tcp"], "span": 6, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/mesh_dp.json b/apps/bff/src/bundled_templates/layers/mesh_dp.json
new file mode 100644
index 0000000..1a56ef6
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/mesh_dp.json
@@ -0,0 +1,38 @@
+{
+  "key": "MESH_DP",
+  "alias": "Mesh Data Plane",
+  "color": "var(--sw-info)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/envoy/metrics_service_setting/",
+  "aliases": { "services": "Sidecar services", "instances": "Sidecars" },
+  "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": true, "ebpfProfiling": true },
+  "layer-header": {
+    "orderBy": "conns",
+    "columns": [
+      { "metric": "conns", "label": "Connections", "mqe": "avg(envoy_total_connections_used)", "aggregation": "avg" },
+      { "metric": "threads", "label": "Worker Threads", "mqe": "avg(envoy_worker_threads)", "aggregation": "avg" },
+      { "metric": "bugFail", "label": "Bug Failures", "mqe": "avg(envoy_bug_failures)", "aggregation": "avg" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Sidecar health", "size": "auto", "metrics": [
+        { "id": "conns", "label": "Connections", "tip": "Envoy connections used across sidecars.", "mqe": "avg(envoy_total_connections_used)", "aggregation": "avg" },
+        { "id": "bugFail", "label": "Bug Failures", "tip": "Envoy assertion / bug counter — should be 0 in healthy clusters.", "mqe": "avg(envoy_bug_failures)", "aggregation": "avg" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "instance": [
+      { "id": "up_rq_active", "title": "Upstream Request Active", "type": "line", "expressions": ["envoy_cluster_up_rq_active"], "span": 4, "rowSpan": 2 },
+      { "id": "up_rq_pending", "title": "Upstream Request Pending Active", "type": "line", "expressions": ["envoy_cluster_up_rq_pending_active"], "span": 4, "rowSpan": 2 },
+      { "id": "up_rq_incr", "title": "Upstream Request Increase", "type": "line", "expressions": ["envoy_cluster_up_rq_incr"], "span": 4, "rowSpan": 2 },
+      { "id": "up_cx_active", "title": "Upstream Connection Active", "type": "line", "expressions": ["envoy_cluster_up_cx_active"], "span": 4, "rowSpan": 2 },
+      { "id": "up_cx_incr", "title": "Upstream Connection Increase", "type": "line", "expressions": ["envoy_cluster_up_cx_incr"], "span": 4, "rowSpan": 2 },
+      { "id": "membership", "title": "Membership Healthy", "type": "line", "expressions": ["envoy_cluster_membership_healthy"], "span": 4, "rowSpan": 2 },
+      { "id": "connections", "title": "Total Connections", "type": "line", "expressions": ["envoy_total_connections_used", "envoy_parent_connections_used"], "expressionLabels": ["total", "parent"], "span": 6, "rowSpan": 2 },
+      { "id": "concurrency", "title": "Worker Threads", "type": "line", "expressions": ["envoy_worker_threads", "envoy_worker_threads_max"], "expressionLabels": ["live", "max"], "span": 6, "rowSpan": 2 },
+      { "id": "heap_memory", "title": "Heap Memory (Bytes)", "type": "line", "expressions": ["envoy_heap_memory_used", "envoy_heap_memory_max_used", "envoy_memory_allocated", "envoy_memory_allocated_max", "envoy_memory_physical_size", "envoy_memory_physical_size_max"], "expressionLabels": ["heap used", "heap max", "allocated", "allocated max", "physical", "physical max"], "span": 12, "rowSpan": 2 },
+      { "id": "bug_failures", "title": "Bug Failures", "type": "line", "expressions": ["envoy_bug_failures"], "span": 12, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/mongodb.json b/apps/bff/src/bundled_templates/layers/mongodb.json
new file mode 100644
index 0000000..98c164a
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/mongodb.json
@@ -0,0 +1,62 @@
+{
+  "key": "MONGODB",
+  "alias": "MongoDB",
+  "color": "var(--sw-warn)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-mongodb-monitoring/",
+  "aliases": { "services": "MongoDB clusters", "instances": "Nodes" },
+  "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "docQps",
+    "columns": [
+      { "metric": "docQps", "label": "Doc QPS", "mqe": "aggregate_labels(meter_mongodb_cluster_document_avg_qps, sum(doc_op_type))", "aggregation": "sum" },
+      { "metric": "opQps", "label": "Op QPS", "mqe": "aggregate_labels(meter_mongodb_cluster_operation_avg_qps, sum(legacy_op_type))", "aggregation": "sum" },
+      { "metric": "conns", "label": "Conns", "mqe": "aggregate_labels(meter_mongodb_cluster_connections,sum)", "aggregation": "sum" },
+      { "metric": "replLag", "label": "Repl Lag", "unit": "ms", "mqe": "meter_mongodb_cluster_repl_lag", "aggregation": "avg" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Capacity", "size": "auto", "metrics": [
+        { "id": "dataSize", "label": "Data Size (GB)", "tip": "Total data size across the cluster.", "mqe": "latest(aggregate_labels(meter_mongodb_cluster_data_size,sum))/1024/1024/1024", "aggregation": "sum" },
+        { "id": "collCount", "label": "Collections", "tip": "Collection count across the cluster.", "mqe": "latest(aggregate_labels(meter_mongodb_cluster_collection_count,sum))", "aggregation": "sum" },
+        { "id": "objCount", "label": "Objects", "tip": "Document object count.", "mqe": "latest(aggregate_labels(meter_mongodb_cluster_object_count,sum))", "aggregation": "sum" }
+      ]},
+      { "title": "Throughput", "size": "square", "metrics": [
+        { "id": "docQps", "label": "Doc QPS", "tip": "Document operations / second.", "mqe": "aggregate_labels(meter_mongodb_cluster_document_avg_qps, sum(doc_op_type))", "aggregation": "sum" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "uptime", "title": "Cluster Uptime (days)", "type": "line", "expressions": ["latest(aggregate_labels(meter_mongodb_cluster_uptime,max))/3600/24"], "span": 3, "rowSpan": 2 },
+      { "id": "data_size", "title": "Data Size (GB)", "type": "line", "expressions": ["latest(aggregate_labels(meter_mongodb_cluster_data_size,sum))/1024/1024/1024"], "span": 3, "rowSpan": 2 },
+      { "id": "coll_count", "title": "Collection Count", "type": "line", "expressions": ["latest(aggregate_labels(meter_mongodb_cluster_collection_count,sum))"], "span": 3, "rowSpan": 2 },
+      { "id": "obj_count", "title": "Object Count", "type": "line", "expressions": ["latest(aggregate_labels(meter_mongodb_cluster_object_count,sum))"], "span": 3, "rowSpan": 2 },
+      { "id": "doc_qps", "title": "Document QPS", "type": "line", "expressions": ["aggregate_labels(meter_mongodb_cluster_document_avg_qps, sum(doc_op_type))"], "span": 4, "rowSpan": 2 },
+      { "id": "op_qps", "title": "Operation QPS", "type": "line", "expressions": ["aggregate_labels(meter_mongodb_cluster_operation_avg_qps, sum(legacy_op_type))"], "span": 4, "rowSpan": 2 },
+      { "id": "conns", "title": "Total Connections", "type": "line", "expressions": ["aggregate_labels(meter_mongodb_cluster_connections,sum)"], "span": 4, "rowSpan": 2 },
+      { "id": "cursors", "title": "Cursor Total", "type": "line", "expressions": ["aggregate_labels(meter_mongodb_cluster_cursor_avg, sum(csr_type))"], "span": 4, "rowSpan": 2 },
+      { "id": "repl_lag", "title": "Replication Lag (ms)", "type": "line", "unit": "ms", "expressions": ["meter_mongodb_cluster_repl_lag"], "span": 4, "rowSpan": 2 },
+      { "id": "db_data_size", "title": "DB Total Data (GB)", "type": "line", "expressions": ["latest(aggregate_labels(meter_mongodb_cluster_db_data_size, sum(database)))/1024/1024/1024"], "span": 4, "rowSpan": 2 },
+      { "id": "db_index_size", "title": "DB Total Index (GB)", "type": "line", "expressions": ["latest(aggregate_labels(meter_mongodb_cluster_db_index_size, sum(database)))/1024/1024/1024"], "span": 4, "rowSpan": 2 }
+    ],
+    "instance": [
+      { "id": "uptime", "title": "Uptime (days)", "type": "line", "expressions": ["latest(meter_mongodb_node_uptime)/3600/24"], "span": 3, "rowSpan": 2 },
+      { "id": "qps", "title": "QPS", "type": "line", "expressions": ["meter_mongodb_node_qps"], "span": 3, "rowSpan": 2 },
+      { "id": "rs_state", "title": "ReplSet State", "type": "line", "expressions": ["meter_mongodb_node_rs_state"], "span": 3, "rowSpan": 2 },
+      { "id": "conns", "title": "Connections", "type": "line", "expressions": ["meter_mongodb_node_connections"], "span": 3, "rowSpan": 2 },
+      { "id": "cpu", "title": "CPU Usage (%)", "type": "line", "unit": "%", "expressions": ["meter_mongodb_node_cpu_total_percentage"], "span": 4, "rowSpan": 2 },
+      { "id": "mem", "title": "Memory Usage", "type": "line", "expressions": ["meter_mongodb_node_memory_usage"], "span": 4, "rowSpan": 2 },
+      { "id": "mem_free", "title": "Memory Free (GB)", "type": "line", "expressions": ["meter_mongodb_node_memory_free_kb/1024/1024", "meter_mongodb_node_swap_memory_free_kb/1024/1024"], "expressionLabels": ["mem", "swap"], "span": 4, "rowSpan": 2 },
+      { "id": "disk", "title": "Disk (GB)", "type": "line", "expressions": ["meter_mongodb_node_fs_used_size/1024/1024/1024", "meter_mongodb_node_fs_total_size/1024/1024/1024"], "expressionLabels": ["used", "total"], "span": 4, "rowSpan": 2 },
+      { "id": "network", "title": "Network (KB/s)", "type": "line", "expressions": ["meter_mongodb_node_network_bytes_in/1024", "meter_mongodb_node_network_bytes_out/1024"], "expressionLabels": ["in", "out"], "span": 4, "rowSpan": 2 },
+      { "id": "active_clients", "title": "Active Clients", "type": "line", "expressions": ["meter_mongodb_node_active_total_num", "meter_mongodb_node_active_writer_num", "meter_mongodb_node_active_reader_num"], "expressionLabels": ["total", "writers", "readers"], "span": 4, "rowSpan": 2 },
+      { "id": "doc_qps", "title": "Document QPS", "type": "line", "expressions": ["meter_mongodb_node_document_qps"], "span": 4, "rowSpan": 2 },
+      { "id": "op_qps", "title": "Operation QPS", "type": "line", "expressions": ["meter_mongodb_node_operation_qps"], "span": 4, "rowSpan": 2 },
+      { "id": "op_latency", "title": "Op Latency (µs)", "type": "line", "expressions": ["aggregate_labels(meter_mongodb_node_latency_rate,sum(op_type))/aggregate_labels(meter_mongodb_node_op_rate,sum(op_type))"], "span": 4, "rowSpan": 2 },
+      { "id": "transactions", "title": "Transactions", "type": "line", "expressions": ["meter_mongodb_node_transactions_active", "meter_mongodb_node_transactions_inactive"], "expressionLabels": ["active", "inactive"], "span": 4, "rowSpan": 2 },
+      { "id": "repl_buffer", "title": "Repl Buffer", "type": "line", "expressions": ["meter_mongodb_node_repl_buffer_count", "meter_mongodb_node_repl_buffer_size/1024/1024"], "expressionLabels": ["count", "size (MB)"], "span": 4, "rowSpan": 2 },
+      { "id": "queued_op", "title": "Queued Operations", "type": "line", "expressions": ["meter_mongodb_node_queued_operation"], "span": 4, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/mysql.json b/apps/bff/src/bundled_templates/layers/mysql.json
new file mode 100644
index 0000000..4ee21fc
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/mysql.json
@@ -0,0 +1,228 @@
+{
+  "key": "MYSQL",
+  "alias": "MySQL / MariaDB",
+  "color": "var(--sw-warn)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-mysql-monitoring/",
+  "aliases": {
+    "services": "MySQL clusters",
+    "instances": "Nodes"
+  },
+  "components": {
+    "service": true,
+    "instances": true,
+    "endpoints": false,
+    "topology": false,
+    "traces": false,
+    "logs": false
+  },
+  "layer-header": {
+    "orderBy": "qps",
+    "columns": [
+      { "metric": "qps", "label": "QPS", "mqe": "aggregate_labels(meter_mysql_qps,sum)", "aggregation": "sum" },
+      { "metric": "tps", "label": "TPS", "mqe": "aggregate_labels(meter_mysql_tps,sum)", "aggregation": "sum" },
+      { "metric": "slowQps", "label": "Slow QPS", "mqe": "aggregate_labels(meter_mysql_slow_queries_rate,sum)", "aggregation": "sum" },
+      { "metric": "connErr", "label": "Conn Errors", "mqe": "aggregate_labels(meter_mysql_connection_errors_internal,sum) + aggregate_labels(meter_mysql_connection_errors_max_connections,sum)", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      {
+        "title": "Throughput",
+        "size": "auto",
+        "metrics": [
+          { "id": "qps", "label": "QPS", "tip": "Queries per second across the cluster.", "mqe": "aggregate_labels(meter_mysql_qps,sum)", "aggregation": "sum" },
+          { "id": "tps", "label": "TPS", "tip": "Transactions per second.", "mqe": "aggregate_labels(meter_mysql_tps,sum)", "aggregation": "sum" },
+          { "id": "slowRate", "label": "Slow Queries/s", "tip": "Slow query rate across the cluster.", "mqe": "aggregate_labels(meter_mysql_slow_queries_rate,sum)", "aggregation": "sum" }
+        ]
+      }
+    ]
+  },
+  "dashboards": {
+    "service": [
+      {
+        "id": "qps",
+        "title": "QPS",
+        "type": "line",
+        "expressions": ["aggregate_labels(meter_mysql_qps,sum)"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "tps",
+        "title": "TPS",
+        "type": "line",
+        "expressions": ["aggregate_labels(meter_mysql_tps,sum)"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "slow_qps",
+        "title": "Slow Queries / s",
+        "type": "line",
+        "expressions": ["aggregate_labels(meter_mysql_slow_queries_rate,sum)"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "conn_errors",
+        "title": "Connection Errors",
+        "tip": "Internal vs max-connection rejects.",
+        "type": "line",
+        "expressions": [
+          "aggregate_labels(meter_mysql_connection_errors_internal,sum)",
+          "aggregate_labels(meter_mysql_connection_errors_max_connections,sum)"
+        ],
+        "expressionLabels": ["internal", "max-connections"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "commands",
+        "title": "Commands Trend",
+        "tip": "Rows-affected rate per command (select / insert / update / delete).",
+        "type": "line",
+        "expressions": [
+          "aggregate_labels(meter_mysql_commands_select_rate,sum)",
+          "aggregate_labels(meter_mysql_commands_insert_rate,sum)",
+          "aggregate_labels(meter_mysql_commands_update_rate,sum)",
+          "aggregate_labels(meter_mysql_commands_delete_rate,sum)"
+        ],
+        "expressionLabels": ["select", "insert", "update", "delete"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "threads",
+        "title": "Threads",
+        "type": "line",
+        "expressions": [
+          "aggregate_labels(meter_mysql_threads_connected,sum)",
+          "aggregate_labels(meter_mysql_threads_running,sum)",
+          "aggregate_labels(meter_mysql_threads_cached,sum)",
+          "aggregate_labels(meter_mysql_threads_created,sum)"
+        ],
+        "expressionLabels": ["connected", "running", "cached", "created"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "slow_statements",
+        "title": "Slow Statements",
+        "tip": "Top 20 slowest captured statements across the cluster.",
+        "type": "record",
+        "unit": "ms",
+        "expressions": ["top_n(top_n_database_statement, 20, des)"],
+        "span": 12,
+        "rowSpan": 4
+      }
+    ],
+    "instance": [
+      {
+        "id": "uptime",
+        "title": "Uptime",
+        "type": "line",
+        "unit": "days",
+        "expressions": ["latest(meter_mysql_instance_uptime)/3600/24"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "max_conns",
+        "title": "Max Connections",
+        "type": "line",
+        "expressions": ["latest(meter_mysql_instance_max_connections)"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "buffer_pool",
+        "title": "InnoDB Buffer Pool",
+        "type": "line",
+        "unit": "MB",
+        "expressions": ["latest(meter_mysql_instance_innodb_buffer_pool_size)/1024/1024"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "thread_cache",
+        "title": "Thread Cache Size",
+        "type": "line",
+        "expressions": ["latest(meter_mysql_instance_thread_cache_size)"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_qps_tps",
+        "title": "QPS / TPS",
+        "type": "line",
+        "expressions": [
+          "meter_mysql_instance_qps",
+          "meter_mysql_instance_tps"
+        ],
+        "expressionLabels": ["QPS", "TPS"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_slow_queries",
+        "title": "Slow Queries / s",
+        "type": "line",
+        "expressions": ["meter_mysql_instance_slow_queries_rate"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_commands",
+        "title": "Commands Trend",
+        "type": "line",
+        "expressions": [
+          "meter_mysql_instance_commands_select_rate",
+          "meter_mysql_instance_commands_insert_rate",
+          "meter_mysql_instance_commands_update_rate",
+          "meter_mysql_instance_commands_delete_rate"
+        ],
+        "expressionLabels": ["select", "insert", "update", "delete"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_threads",
+        "title": "Threads",
+        "type": "line",
+        "expressions": [
+          "meter_mysql_instance_threads_connected",
+          "meter_mysql_instance_threads_running",
+          "meter_mysql_instance_threads_cached",
+          "meter_mysql_instance_threads_created"
+        ],
+        "expressionLabels": ["connected", "running", "cached", "created"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_connects",
+        "title": "Connects",
+        "type": "line",
+        "expressions": [
+          "meter_mysql_instance_connects_available",
+          "meter_mysql_instance_connects_aborted"
+        ],
+        "expressionLabels": ["available", "aborted"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_conn_errors",
+        "title": "Connection Errors",
+        "type": "line",
+        "expressions": [
+          "meter_mysql_instance_connection_errors_internal",
+          "meter_mysql_instance_connection_errors_max_connections"
+        ],
+        "expressionLabels": ["internal", "max-connections"],
+        "span": 6,
+        "rowSpan": 2
+      }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/nginx.json b/apps/bff/src/bundled_templates/layers/nginx.json
new file mode 100644
index 0000000..83ef787
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/nginx.json
@@ -0,0 +1,76 @@
+{
+  "key": "NGINX",
+  "alias": "Nginx",
+  "color": "var(--sw-ok)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-nginx-monitoring/",
+  "aliases": {
+    "services": "Nginx services",
+    "instances": "Nodes",
+    "endpoints": "Routes"
+  },
+  "components": {
+    "service": true,
+    "instances": true,
+    "endpoints": true,
+    "topology": false,
+    "traces": false,
+    "logs": true
+  },
+  "layer-header": {
+    "orderBy": "rps",
+    "columns": [
+      { "metric": "rps", "label": "RPS", "mqe": "aggregate_labels(meter_nginx_service_http_requests, sum)", "aggregation": "sum" },
+      { "metric": "err5xx", "label": "5xx %", "unit": "%", "mqe": "aggregate_labels(meter_nginx_service_http_5xx_requests_increment, sum)/aggregate_labels(meter_nginx_service_http_requests_increment, sum)*100", "aggregation": "avg" },
+      { "metric": "err4xx", "label": "4xx %", "unit": "%", "mqe": "aggregate_labels(meter_nginx_service_http_4xx_requests_increment, sum)/aggregate_labels(meter_nginx_service_http_requests_increment, sum)*100", "aggregation": "avg" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      {
+        "title": "Errors",
+        "size": "auto",
+        "metrics": [
+          { "id": "err5xx", "label": "5xx Rate", "tip": "Percentage of requests returning 5xx per minute.", "mqe": "aggregate_labels(meter_nginx_service_http_5xx_requests_increment, sum)/aggregate_labels(meter_nginx_service_http_requests_increment, sum)*100", "unit": "%", "aggregation": "avg" },
+          { "id": "err4xx", "label": "4xx Rate", "tip": "Percentage of requests returning 4xx per minute.", "mqe": "aggregate_labels(meter_nginx_service_http_4xx_requests_increment, sum)/aggregate_labels(meter_nginx_service_http_requests_increment, sum)*100", "unit": "%", "aggregation": "avg" }
+        ]
+      },
+      {
+        "title": "Throughput",
+        "size": "square",
+        "metrics": [
+          { "id": "rps", "label": "RPS", "tip": "Requests per second across the Nginx service.", "mqe": "aggregate_labels(meter_nginx_service_http_requests, sum)", "aggregation": "sum" }
+        ]
+      }
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "rps", "title": "HTTP Request Trend", "type": "line", "expressions": ["aggregate_labels(meter_nginx_service_http_requests, sum)"], "span": 4, "rowSpan": 2 },
+      { "id": "latency", "title": "HTTP Latency", "type": "line", "unit": "ms", "expressions": ["aggregate_labels(meter_nginx_service_http_latency, avg(p))"], "span": 4, "rowSpan": 2 },
+      { "id": "bandwidth", "title": "HTTP Bandwidth", "type": "line", "unit": "KB/s", "expressions": ["aggregate_labels(meter_nginx_service_http_bandwidth, sum(type))/1024"], "span": 4, "rowSpan": 2 },
+      { "id": "conns", "title": "HTTP Connections", "type": "line", "expressions": ["aggregate_labels(meter_nginx_service_http_connections, sum(state))"], "span": 6, "rowSpan": 2 },
+      { "id": "status", "title": "HTTP Status Trend", "type": "line", "expressions": ["aggregate_labels(meter_nginx_service_http_status, sum(status))"], "span": 6, "rowSpan": 2 },
+      { "id": "err4xx", "title": "4xx % / min", "type": "line", "unit": "%", "expressions": ["aggregate_labels(meter_nginx_service_http_4xx_requests_increment, sum)/aggregate_labels(meter_nginx_service_http_requests_increment, sum)*100"], "span": 4, "rowSpan": 2 },
+      { "id": "err5xx", "title": "5xx % / min", "type": "line", "unit": "%", "expressions": ["aggregate_labels(meter_nginx_service_http_5xx_requests_increment, sum)/aggregate_labels(meter_nginx_service_http_requests_increment, sum)*100"], "span": 4, "rowSpan": 2 },
+      { "id": "errlog", "title": "Error Log Count", "type": "line", "expressions": ["aggregate_labels(meter_nginx_service_error_log_count, sum(level))"], "span": 4, "rowSpan": 2 }
+    ],
+    "instance": [
+      { "id": "rps", "title": "HTTP Request Trend", "type": "line", "expressions": ["meter_nginx_instance_http_requests"], "span": 4, "rowSpan": 2 },
+      { "id": "latency", "title": "HTTP Latency", "type": "line", "unit": "ms", "expressions": ["meter_nginx_instance_http_latency"], "span": 4, "rowSpan": 2 },
+      { "id": "bandwidth", "title": "HTTP Bandwidth", "type": "line", "unit": "KB/s", "expressions": ["meter_nginx_instance_http_bandwidth/1024"], "span": 4, "rowSpan": 2 },
+      { "id": "conns", "title": "HTTP Connections", "type": "line", "expressions": ["meter_nginx_instance_http_connections"], "span": 6, "rowSpan": 2 },
+      { "id": "status", "title": "HTTP Status Trend", "type": "line", "expressions": ["meter_nginx_instance_http_status"], "span": 6, "rowSpan": 2 },
+      { "id": "err4xx", "title": "4xx % / min", "type": "line", "unit": "%", "expressions": ["(meter_nginx_instance_http_4xx_requests_increment/meter_nginx_instance_http_requests_increment)*100"], "span": 4, "rowSpan": 2 },
+      { "id": "err5xx", "title": "5xx % / min", "type": "line", "unit": "%", "expressions": ["(meter_nginx_instance_http_5xx_requests_increment/meter_nginx_instance_http_requests_increment)*100"], "span": 4, "rowSpan": 2 },
+      { "id": "errlog", "title": "Error Log Count", "type": "line", "expressions": ["meter_nginx_instance_error_log_count"], "span": 4, "rowSpan": 2 }
+    ],
+    "endpoint": [
+      { "id": "rps", "title": "HTTP Request Trend", "type": "line", "expressions": ["meter_nginx_endpoint_http_requests"], "span": 4, "rowSpan": 2 },
+      { "id": "latency", "title": "HTTP Latency", "type": "line", "unit": "ms", "expressions": ["meter_nginx_endpoint_http_latency"], "span": 4, "rowSpan": 2 },
+      { "id": "bandwidth", "title": "HTTP Bandwidth", "type": "line", "unit": "KB", "expressions": ["meter_nginx_endpoint_http_bandwidth/1024"], "span": 4, "rowSpan": 2 },
+      { "id": "status", "title": "HTTP Status Trend", "type": "line", "expressions": ["meter_nginx_endpoint_http_status"], "span": 6, "rowSpan": 2 },
+      { "id": "err4xx", "title": "4xx % / min", "type": "line", "unit": "%", "expressions": ["(meter_nginx_endpoint_http_4xx_requests_increment/meter_nginx_endpoint_http_requests_increment)*100"], "span": 3, "rowSpan": 2 },
+      { "id": "err5xx", "title": "5xx % / min", "type": "line", "unit": "%", "expressions": ["(meter_nginx_endpoint_http_5xx_requests_increment/meter_nginx_endpoint_http_requests_increment)*100"], "span": 3, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/os_linux.json b/apps/bff/src/bundled_templates/layers/os_linux.json
new file mode 100644
index 0000000..2fbc103
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/os_linux.json
@@ -0,0 +1,38 @@
+{
+  "key": "OS_LINUX",
+  "alias": "Linux",
+  "color": "var(--sw-fg-1)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-vm-monitoring/",
+  "aliases": { "services": "Hosts" },
+  "components": { "service": true, "instances": false, "endpoints": false, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "cpu",
+    "columns": [
+      { "metric": "cpu", "label": "CPU %", "unit": "%", "mqe": "meter_vm_cpu_total_percentage", "aggregation": "avg" },
+      { "metric": "memUsed", "label": "Memory MB", "unit": "MB", "mqe": "meter_vm_memory_used/1024/1024", "aggregation": "avg" },
+      { "metric": "load1", "label": "Load 1m", "mqe": "meter_vm_cpu_load1/100", "aggregation": "avg" },
+      { "metric": "fs", "label": "FS %", "unit": "%", "mqe": "meter_vm_filesystem_percentage", "aggregation": "avg" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Resource", "size": "auto", "metrics": [
+        { "id": "cpu", "label": "CPU %", "tip": "Total CPU utilization.", "mqe": "meter_vm_cpu_total_percentage", "unit": "%", "aggregation": "avg" },
+        { "id": "memUsage", "label": "Mem %", "tip": "Memory used / total.", "mqe": "avg(meter_vm_memory_swap_percentage)", "unit": "%", "aggregation": "avg" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "cpu", "title": "CPU Average Used (%)", "type": "line", "unit": "%", "expressions": ["meter_vm_cpu_average_used"], "span": 4, "rowSpan": 2 },
+      { "id": "cpu_load", "title": "CPU Load", "type": "line", "expressions": ["meter_vm_cpu_load1/100", "meter_vm_cpu_load5/100", "meter_vm_cpu_load15/100"], "expressionLabels": ["1m", "5m", "15m"], "span": 4, "rowSpan": 2 },
+      { "id": "filefd", "title": "File FD Allocated", "type": "line", "expressions": ["meter_vm_filefd_allocated"], "span": 4, "rowSpan": 2 },
+      { "id": "memory", "title": "Memory RAM (MB)", "type": "line", "unit": "MB", "expressions": ["meter_vm_memory_used/1024/1024", "meter_vm_memory_total/1024/1024", "meter_vm_memory_available/1024/1024", "meter_vm_memory_buff_cache/1024/1024"], "expressionLabels": ["used", "total", "available", "buff/cache"], "span": 6, "rowSpan": 2 },
+      { "id": "swap", "title": "Memory Swap (MB)", "type": "line", "unit": "MB", "expressions": ["meter_vm_memory_swap_free/1024/1024", "meter_vm_memory_swap_total/1024/1024"], "expressionLabels": ["free", "total"], "span": 6, "rowSpan": 2 },
+      { "id": "network_bandwidth", "title": "Network Bandwidth (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["meter_vm_network_receive/1024", "meter_vm_network_transmit/1024"], "expressionLabels": ["receive", "transmit"], "span": 6, "rowSpan": 2 },
+      { "id": "disk", "title": "Disk R/W (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["meter_vm_disk_read/1024", "meter_vm_disk_written/1024"], "expressionLabels": ["read", "written"], "span": 6, "rowSpan": 2 },
+      { "id": "fs", "title": "Filesystem Usage (%)", "type": "line", "unit": "%", "expressions": ["meter_vm_filesystem_percentage"], "span": 6, "rowSpan": 2 },
+      { "id": "network_status", "title": "Network Status", "type": "line", "expressions": ["meter_vm_tcp_curr_estab", "meter_vm_tcp_tw", "meter_vm_tcp_alloc", "meter_vm_sockets_used", "meter_vm_udp_inuse"], "expressionLabels": ["tcp estab", "tcp time-wait", "tcp alloc", "sockets used", "udp in-use"], "span": 6, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/os_windows.json b/apps/bff/src/bundled_templates/layers/os_windows.json
new file mode 100644
index 0000000..4ee43fb
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/os_windows.json
@@ -0,0 +1,33 @@
+{
+  "key": "OS_WINDOWS",
+  "alias": "Windows",
+  "color": "var(--sw-fg-1)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-win-monitoring/",
+  "aliases": { "services": "Hosts" },
+  "components": { "service": true, "instances": false, "endpoints": false, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "cpu",
+    "columns": [
+      { "metric": "cpu", "label": "CPU %", "unit": "%", "mqe": "meter_win_cpu_total_percentage", "aggregation": "avg" },
+      { "metric": "memUsed", "label": "Memory MB", "unit": "MB", "mqe": "meter_win_memory_used/1024/1024", "aggregation": "avg" },
+      { "metric": "vmem", "label": "VMem %", "unit": "%", "mqe": "avg(meter_win_memory_virtual_memory_percentage)", "aggregation": "avg" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Resource", "size": "auto", "metrics": [
+        { "id": "cpu", "label": "CPU %", "tip": "Total CPU utilization.", "mqe": "meter_win_cpu_total_percentage", "unit": "%", "aggregation": "avg" },
+        { "id": "vmem", "label": "VMem %", "tip": "Virtual memory utilization.", "mqe": "avg(meter_win_memory_virtual_memory_percentage)", "unit": "%", "aggregation": "avg" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "cpu", "title": "CPU Average Used (%)", "type": "line", "unit": "%", "expressions": ["meter_win_cpu_average_used"], "span": 4, "rowSpan": 2 },
+      { "id": "memory", "title": "Memory RAM (MB)", "type": "line", "unit": "MB", "expressions": ["meter_win_memory_used/1024/1024", "meter_win_memory_total/1024/1024", "meter_win_memory_available/1024/1024"], "expressionLabels": ["used", "total", "available"], "span": 4, "rowSpan": 2 },
+      { "id": "virtual_memory", "title": "Virtual Memory (MB)", "type": "line", "unit": "MB", "expressions": ["meter_win_memory_virtual_memory_free/1024/1024", "meter_win_memory_virtual_memory_total/1024/1024"], "expressionLabels": ["free", "total"], "span": 4, "rowSpan": 2 },
+      { "id": "network_bandwidth", "title": "Network Bandwidth (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["meter_win_network_receive/1024", "meter_win_network_transmit/1024"], "expressionLabels": ["receive", "transmit"], "span": 6, "rowSpan": 2 },
+      { "id": "disk", "title": "Disk R/W (KB/s)", "type": "line", "unit": "KB/s", "expressions": ["meter_win_disk_read/1024", "meter_win_disk_written/1024"], "expressionLabels": ["read", "written"], "span": 6, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/postgresql.json b/apps/bff/src/bundled_templates/layers/postgresql.json
new file mode 100644
index 0000000..c6d1dee
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/postgresql.json
@@ -0,0 +1,274 @@
+{
+  "key": "POSTGRESQL",
+  "alias": "PostgreSQL",
+  "color": "var(--sw-warn)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-postgresql-monitoring/",
+  "aliases": {
+    "services": "PostgreSQL clusters",
+    "instances": "Nodes"
+  },
+  "components": {
+    "service": true,
+    "instances": true,
+    "endpoints": false,
+    "topology": false,
+    "traces": false,
+    "logs": false
+  },
+  "layer-header": {
+    "orderBy": "fetched",
+    "columns": [
+      { "metric": "fetched", "label": "Fetched / s", "mqe": "aggregate_labels(meter_pg_fetched_rows_rate,sum)", "aggregation": "sum" },
+      { "metric": "inserted", "label": "Inserted / s", "mqe": "aggregate_labels(meter_pg_inserted_rows_rate,sum)", "aggregation": "sum" },
+      { "metric": "cacheHit", "label": "Cache Hit", "unit": "%", "mqe": "aggregate_labels(meter_pg_cache_hit_rate,avg)", "aggregation": "avg" },
+      { "metric": "deadlocks", "label": "Deadlocks", "mqe": "aggregate_labels(meter_pg_deadlocks_rate,sum)", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      {
+        "title": "Health",
+        "size": "auto",
+        "metrics": [
+          { "id": "cacheHit", "label": "Cache Hit", "tip": "Buffer-cache hit ratio across the cluster.", "mqe": "aggregate_labels(meter_pg_cache_hit_rate,avg)", "unit": "%", "aggregation": "avg" },
+          { "id": "deadlocks", "label": "Deadlocks/s", "tip": "Detected deadlocks per second.", "mqe": "aggregate_labels(meter_pg_deadlocks_rate,sum)", "aggregation": "sum" },
+          { "id": "conflicts", "label": "Conflicts/s", "tip": "Recovery conflicts per second.", "mqe": "aggregate_labels(meter_pg_conflicts_rate,sum)", "aggregation": "sum" }
+        ]
+      },
+      {
+        "title": "Throughput",
+        "size": "square",
+        "metrics": [
+          { "id": "fetched", "label": "Fetched /s", "tip": "Rows fetched per second.", "mqe": "aggregate_labels(meter_pg_fetched_rows_rate,sum)", "aggregation": "sum" }
+        ]
+      }
+    ]
+  },
+  "dashboards": {
+    "service": [
+      {
+        "id": "rows_fetched",
+        "title": "Fetched Rows / s",
+        "type": "line",
+        "expressions": ["aggregate_labels(meter_pg_fetched_rows_rate,sum)"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "rows_inserted",
+        "title": "Inserted Rows / s",
+        "type": "line",
+        "expressions": ["aggregate_labels(meter_pg_inserted_rows_rate,sum)"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "rows_updated",
+        "title": "Updated Rows / s",
+        "type": "line",
+        "expressions": ["aggregate_labels(meter_pg_updated_rows_rate,sum)"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "rows_deleted",
+        "title": "Deleted Rows / s",
+        "type": "line",
+        "expressions": ["aggregate_labels(meter_pg_deleted_rows_rate,sum)"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "rows_returned",
+        "title": "Returned Rows / s",
+        "type": "line",
+        "expressions": ["aggregate_labels(meter_pg_returned_rows_rate,sum)"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "temp_files",
+        "title": "Temporary Files / s",
+        "type": "line",
+        "expressions": ["aggregate_labels(meter_pg_temporary_files_rate,sum)"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "cache_hit",
+        "title": "Cache Hit Rate",
+        "type": "line",
+        "unit": "%",
+        "expressions": ["aggregate_labels(meter_pg_cache_hit_rate,avg)"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "txn_rates",
+        "title": "Transactions / s",
+        "tip": "Committed vs rolled-back.",
+        "type": "line",
+        "expressions": [
+          "aggregate_labels(meter_pg_committed_transactions_rate,sum)",
+          "aggregate_labels(meter_pg_rolled_back_transactions_rate,sum)"
+        ],
+        "expressionLabels": ["committed", "rolled-back"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "conflicts_deadlocks",
+        "title": "Conflicts + Deadlocks / s",
+        "type": "line",
+        "expressions": [
+          "aggregate_labels(meter_pg_conflicts_rate,sum)",
+          "aggregate_labels(meter_pg_deadlocks_rate,sum)"
+        ],
+        "expressionLabels": ["conflicts", "deadlocks"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "sessions",
+        "title": "Sessions",
+        "type": "line",
+        "expressions": [
+          "aggregate_labels(meter_pg_active_sessions,sum)",
+          "aggregate_labels(meter_pg_idle_sessions,sum)",
+          "aggregate_labels(meter_pg_locks_count,sum)"
+        ],
+        "expressionLabels": ["active", "idle", "locks"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "buffers",
+        "title": "Buffers / s",
+        "tip": "Checkpoint / clean / fsync / alloc / backend buffer activity.",
+        "type": "line",
+        "expressions": [
+          "aggregate_labels(meter_pg_buffers_checkpoint,sum)",
+          "aggregate_labels(meter_pg_buffers_clean,sum)",
+          "aggregate_labels(meter_pg_buffers_backend_fsync,sum)",
+          "aggregate_labels(meter_pg_buffers_alloc,sum)",
+          "aggregate_labels(meter_pg_buffers_backend,sum)"
+        ],
+        "expressionLabels": ["checkpoint", "clean", "backend fsync", "alloc", "backend"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "checkpoint_stats",
+        "title": "Checkpoint Stats / s",
+        "type": "line",
+        "expressions": [
+          "aggregate_labels(meter_pg_checkpoints_timed_rate,sum)",
+          "aggregate_labels(meter_pg_checkpoint_req_rate,sum)",
+          "aggregate_labels(meter_pg_checkpoint_write_time_rate,sum)",
+          "aggregate_labels(meter_pg_checkpoint_sync_time_rate,sum)"
+        ],
+        "expressionLabels": ["timed", "requested", "write time", "sync time"],
+        "span": 12,
+        "rowSpan": 2
+      },
+      {
+        "id": "slow_statements",
+        "title": "Slow Statements",
+        "tip": "Top 20 slowest captured statements.",
+        "type": "record",
+        "unit": "ms",
+        "expressions": ["top_n(top_n_database_statement, 20, des)"],
+        "span": 12,
+        "rowSpan": 4
+      }
+    ],
+    "instance": [
+      {
+        "id": "shared_buffers",
+        "title": "Shared Buffers",
+        "type": "line",
+        "unit": "MB",
+        "expressions": ["latest(meter_pg_instance_shared_buffers)/1024/1024"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "effective_cache",
+        "title": "Effective Cache",
+        "type": "line",
+        "unit": "GB",
+        "expressions": ["latest(meter_pg_instance_effective_cache)/1024/1024/1024"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "work_mem",
+        "title": "Work Mem",
+        "type": "line",
+        "unit": "MB",
+        "expressions": ["latest(meter_pg_instance_work_mem)/1024/1024"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "max_wal_size",
+        "title": "Max WAL Size",
+        "type": "line",
+        "unit": "GB",
+        "expressions": ["latest(meter_pg_instance_max_wal_size)/1024/1024/1024"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_fetched",
+        "title": "Fetched Rows / s",
+        "type": "line",
+        "expressions": ["meter_pg_instance_fetched_rows_rate"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_inserted",
+        "title": "Inserted Rows / s",
+        "type": "line",
+        "expressions": ["meter_pg_instance_inserted_rows_rate"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_cache_hit",
+        "title": "Cache Hit Rate",
+        "type": "line",
+        "unit": "%",
+        "expressions": ["meter_pg_instance_cache_hit_rate"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_sessions",
+        "title": "Sessions",
+        "type": "line",
+        "expressions": [
+          "meter_pg_instance_active_sessions",
+          "meter_pg_instance_idle_sessions",
+          "meter_pg_instance_locks_count"
+        ],
+        "expressionLabels": ["active", "idle", "locks"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "instance_conflicts",
+        "title": "Conflicts + Deadlocks / s",
+        "type": "line",
+        "expressions": [
+          "meter_pg_instance_conflicts_rate",
+          "meter_pg_instance_deadlocks_rate"
+        ],
+        "expressionLabels": ["conflicts", "deadlocks"],
+        "span": 6,
+        "rowSpan": 2
+      }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/pulsar.json b/apps/bff/src/bundled_templates/layers/pulsar.json
new file mode 100644
index 0000000..9f4ef4d
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/pulsar.json
@@ -0,0 +1,50 @@
+{
+  "key": "PULSAR",
+  "alias": "Pulsar",
+  "color": "var(--sw-purple)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-pulsar-monitoring/",
+  "aliases": { "services": "Pulsar clusters", "instances": "Brokers" },
+  "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "topics",
+    "columns": [
+      { "metric": "topics", "label": "Topics", "mqe": "aggregate_labels(meter_pulsar_total_topics,sum)", "aggregation": "sum" },
+      { "metric": "subs", "label": "Subscriptions", "mqe": "aggregate_labels(meter_pulsar_total_subscriptions,sum)", "aggregation": "sum" },
+      { "metric": "rateIn", "label": "Msg In", "mqe": "aggregate_labels(meter_pulsar_message_rate_in,sum)", "aggregation": "sum" },
+      { "metric": "rateOut", "label": "Msg Out", "mqe": "aggregate_labels(meter_pulsar_message_rate_out,sum)", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Throughput", "size": "auto", "metrics": [
+        { "id": "rateIn", "label": "Msg In/s", "tip": "Messages produced per second.", "mqe": "aggregate_labels(meter_pulsar_message_rate_in,sum)", "aggregation": "sum" },
+        { "id": "rateOut", "label": "Msg Out/s", "tip": "Messages consumed per second.", "mqe": "aggregate_labels(meter_pulsar_message_rate_out,sum)", "aggregation": "sum" },
+        { "id": "storage", "label": "Storage MB", "tip": "Total managed-ledger storage size.", "mqe": "aggregate_labels(meter_pulsar_storage_size,sum)/1024/1024", "aggregation": "sum" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "topics", "title": "Total Topics", "type": "line", "expressions": ["aggregate_labels(meter_pulsar_total_topics,sum)"], "span": 3, "rowSpan": 2 },
+      { "id": "subs", "title": "Subscriptions", "type": "line", "expressions": ["aggregate_labels(meter_pulsar_total_subscriptions,sum)"], "span": 3, "rowSpan": 2 },
+      { "id": "producers", "title": "Producers", "type": "line", "expressions": ["aggregate_labels(meter_pulsar_total_producers,sum)"], "span": 3, "rowSpan": 2 },
+      { "id": "consumers", "title": "Consumers", "type": "line", "expressions": ["aggregate_labels(meter_pulsar_total_consumers,sum)"], "span": 3, "rowSpan": 2 },
+      { "id": "msg_rate", "title": "Message Rate", "type": "line", "expressions": ["aggregate_labels(meter_pulsar_message_rate_in,sum)", "aggregate_labels(meter_pulsar_message_rate_out,sum)"], "expressionLabels": ["in", "out"], "span": 6, "rowSpan": 2 },
+      { "id": "throughput", "title": "Throughput", "type": "line", "expressions": ["aggregate_labels(meter_pulsar_throughput_in,sum)", "aggregate_labels(meter_pulsar_throughput_out,sum)"], "expressionLabels": ["in", "out"], "span": 6, "rowSpan": 2 },
+      { "id": "storage_rw", "title": "Storage Read/Write Rate", "type": "line", "expressions": ["aggregate_labels(meter_pulsar_storage_read_rate,sum)", "aggregate_labels(meter_pulsar_storage_write_rate,sum)"], "expressionLabels": ["read", "write"], "span": 6, "rowSpan": 2 },
+      { "id": "storage_size", "title": "Storage Size (MB)", "type": "line", "unit": "MB", "expressions": ["aggregate_labels(meter_pulsar_storage_size,sum)/1024/1024", "aggregate_labels(meter_pulsar_storage_logical_size,sum)/1024/1024"], "expressionLabels": ["physical", "logical"], "span": 6, "rowSpan": 2 }
+    ],
+    "instance": [
+      { "id": "active_conns", "title": "Active Connections", "type": "line", "expressions": ["meter_pulsar_broker_active_connections"], "span": 3, "rowSpan": 2 },
+      { "id": "total_conns", "title": "Total Connections", "type": "line", "expressions": ["meter_pulsar_broker_total_connections"], "span": 3, "rowSpan": 2 },
+      { "id": "conn_fail", "title": "Conn Create Fail", "type": "line", "expressions": ["meter_pulsar_broker_connection_create_fail_count"], "span": 3, "rowSpan": 2 },
+      { "id": "conn_success", "title": "Conn Create Success", "type": "line", "expressions": ["meter_pulsar_broker_connection_create_success_count"], "span": 3, "rowSpan": 2 },
+      { "id": "conn_closed", "title": "Connection Closed", "type": "line", "expressions": ["meter_pulsar_broker_connection_closed_total_count"], "span": 4, "rowSpan": 2 },
+      { "id": "jvm_buffer", "title": "JVM Buffer Pool (MB)", "type": "line", "unit": "MB", "expressions": ["meter_pulsar_broker_jvm_buffer_pool_used_bytes/1024/1024"], "span": 4, "rowSpan": 2 },
+      { "id": "jvm_pool", "title": "JVM Memory Pool Used (MB)", "type": "line", "unit": "MB", "expressions": ["meter_pulsar_broker_jvm_memory_pool_used/1024/1024"], "span": 4, "rowSpan": 2 },
+      { "id": "jvm_mem", "title": "JVM Memory (MB)", "type": "line", "unit": "MB", "expressions": ["meter_pulsar_broker_jvm_memory_used/1024/1024", "meter_pulsar_broker_jvm_memory_committed/1024/1024", "meter_pulsar_broker_jvm_memory_init/1024/1024"], "expressionLabels": ["used", "committed", "init"], "span": 6, "rowSpan": 2 },
+      { "id": "jvm_threads", "title": "JVM Threads", "type": "line", "expressions": ["meter_pulsar_broker_jvm_threads_current", "meter_pulsar_broker_jvm_threads_daemon", "meter_pulsar_broker_jvm_threads_peak", "meter_pulsar_broker_jvm_threads_deadlocked"], "expressionLabels": ["current", "daemon", "peak", "deadlocked"], "span": 6, "rowSpan": 2 },
+      { "id": "gc", "title": "GC", "type": "line", "expressions": ["meter_pulsar_broker_jvm_gc_collection_seconds_sum", "meter_pulsar_broker_jvm_gc_collection_seconds_count"], "expressionLabels": ["seconds", "count"], "expressionAxes": [0, 1], "span": 6, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/rabbitmq.json b/apps/bff/src/bundled_templates/layers/rabbitmq.json
new file mode 100644
index 0000000..8849f55
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/rabbitmq.json
@@ -0,0 +1,52 @@
+{
+  "key": "RABBITMQ",
+  "alias": "RabbitMQ",
+  "color": "var(--sw-warn)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-rabbitmq-monitoring/",
+  "aliases": { "services": "RabbitMQ clusters", "instances": "Nodes" },
+  "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "queues",
+    "columns": [
+      { "metric": "queues", "label": "Queues", "mqe": "aggregate_labels(meter_rabbitmq_queues,sum)", "aggregation": "sum" },
+      { "metric": "channels", "label": "Channels", "mqe": "aggregate_labels(meter_rabbitmq_channels,sum)", "aggregation": "sum" },
+      { "metric": "conns", "label": "Connections", "mqe": "aggregate_labels(meter_rabbitmq_connections,sum)", "aggregation": "sum" },
+      { "metric": "unconfirmed", "label": "Unconfirmed", "mqe": "aggregate_labels(meter_rabbitmq_messages_unconfirmed,sum)", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Throughput", "size": "auto", "metrics": [
+        { "id": "published", "label": "Published / min", "tip": "Messages published per minute.", "mqe": "aggregate_labels(meter_rabbitmq_messages_published,sum)", "aggregation": "sum" },
+        { "id": "unconfirmed", "label": "Unconfirmed", "tip": "Messages awaiting publisher confirm.", "mqe": "aggregate_labels(meter_rabbitmq_messages_unconfirmed,sum)", "aggregation": "sum" },
+        { "id": "unroutable", "label": "Unroutable dropped", "tip": "Messages dropped because no exchange route matched.", "mqe": "meter_rabbitmq_messages_unroutable_dropped", "aggregation": "sum" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "mem_available", "title": "Memory Available Before Block (MB)", "type": "line", "unit": "MB", "expressions": ["meter_rabbitmq_memory_available_before_publisher_blocked/1024/1024"], "span": 4, "rowSpan": 2 },
+      { "id": "disk_available", "title": "Disk Available Before Block (GB)", "type": "line", "unit": "GB", "expressions": ["meter_rabbitmq_disk_space_available_before_publisher_blocked/1024/1024/1024"], "span": 4, "rowSpan": 2 },
+      { "id": "fds", "title": "File Descriptors + Sockets", "type": "line", "expressions": ["meter_rabbitmq_file_descriptors_available", "meter_rabbitmq_tcp_socket_available"], "expressionLabels": ["fds", "sockets"], "span": 4, "rowSpan": 2 },
+      { "id": "messages_ready", "title": "Ready Messages", "type": "line", "expressions": ["meter_rabbitmq_message_ready_delivered_consumers"], "span": 6, "rowSpan": 2 },
+      { "id": "messages_pending", "title": "Pending Ack", "type": "line", "expressions": ["meter_rabbitmq_message_unacknowledged_delivered_consumers"], "span": 6, "rowSpan": 2 },
+      { "id": "publish_pipeline", "title": "Publish Pipeline", "type": "line", "expressions": ["meter_rabbitmq_messages_published", "meter_rabbitmq_messages_confirmed", "meter_rabbitmq_messages_routed", "meter_rabbitmq_messages_unconfirmed"], "expressionLabels": ["published", "confirmed", "routed", "unconfirmed"], "span": 6, "rowSpan": 2 },
+      { "id": "unroutable", "title": "Unroutable Messages", "type": "line", "expressions": ["meter_rabbitmq_messages_unroutable_dropped", "meter_rabbitmq_messages_unroutable_returned"], "expressionLabels": ["dropped", "returned"], "span": 6, "rowSpan": 2 },
+      { "id": "queue_totals", "title": "Queues", "type": "line", "expressions": ["meter_rabbitmq_queues", "meter_rabbitmq_queues_declared_total", "meter_rabbitmq_queues_created_total", "meter_rabbitmq_queues_deleted_total"], "expressionLabels": ["total", "declared", "created", "deleted"], "span": 6, "rowSpan": 2 },
+      { "id": "channel_totals", "title": "Channels", "type": "line", "expressions": ["meter_rabbitmq_channels", "meter_rabbitmq_channels_opened_total", "meter_rabbitmq_channels_closed_total"], "expressionLabels": ["total", "opened", "closed"], "span": 6, "rowSpan": 2 },
+      { "id": "conn_totals", "title": "Connections", "type": "line", "expressions": ["meter_rabbitmq_connections", "meter_rabbitmq_connections_opened_total", "meter_rabbitmq_connections_closed_total"], "expressionLabels": ["total", "opened", "closed"], "span": 12, "rowSpan": 2 }
+    ],
+    "instance": [
+      { "id": "ready_msgs", "title": "Ready Messages", "type": "line", "expressions": ["latest(meter_rabbitmq_node_queue_messages_ready)"], "span": 4, "rowSpan": 2 },
+      { "id": "incoming", "title": "Incoming Messages", "type": "line", "expressions": ["latest(meter_rabbitmq_node_incoming_messages)"], "span": 4, "rowSpan": 2 },
+      { "id": "outgoing", "title": "Outgoing Messages", "type": "line", "expressions": ["latest(meter_rabbitmq_node_outgoing_messages_total)"], "span": 4, "rowSpan": 2 },
+      { "id": "unack", "title": "Unacknowledged Messages", "type": "line", "expressions": ["latest(meter_rabbitmq_node_unacknowledged_messages)"], "span": 4, "rowSpan": 2 },
+      { "id": "conns", "title": "Connections / Publishers / Consumers", "type": "line", "expressions": ["latest(meter_rabbitmq_node_connections_total)", "latest(meter_rabbitmq_node_publisher_total)", "latest(meter_rabbitmq_node_consumer_total)"], "expressionLabels": ["connections", "publishers", "consumers"], "span": 4, "rowSpan": 2 },
+      { "id": "channels", "title": "Channels + Queues", "type": "line", "expressions": ["latest(meter_rabbitmq_node_channel_total)", "latest(meter_rabbitmq_node_queue_total)"], "expressionLabels": ["channels", "queues"], "span": 4, "rowSpan": 2 },
+      { "id": "memory_pct", "title": "Allocated Used %", "type": "line", "unit": "%", "expressions": ["latest(meter_rabbitmq_node_allocated_used_percent)/100"], "span": 4, "rowSpan": 2 },
+      { "id": "memory_bytes", "title": "Memory (MB)", "type": "line", "unit": "MB", "expressions": ["meter_rabbitmq_node_allocated_used_bytes/1024/1024", "meter_rabbitmq_node_allocated_unused_bytes/1024/1024", "meter_rabbitmq_node_process_resident_memory_bytes/1024/1024", "latest(meter_rabbitmq_node_allocated_total_bytes)/1024/1024"], "expressionLabels": ["used", "unused", "resident", "total"], "span": 8, "rowSpan": 2 },
+      { "id": "alloc_by_type", "title": "Allocated By Type (MB)", "type": "line", "unit": "MB", "expressions": ["meter_rabbitmq_node_allocated_by_type/1024/1024"], "span": 6, "rowSpan": 2 },
+      { "id": "blocks", "title": "Multi/Single-block Memory (MB)", "type": "line", "unit": "MB", "expressions": ["meter_rabbitmq_node_allocated_multiblock_used/1024/1024", "meter_rabbitmq_node_allocated_multiblock_unused/1024/1024", "meter_rabbitmq_node_allocated_singleblock_used/1024/1024", "meter_rabbitmq_node_allocated_singleblock_unused/1024/1024"], "expressionLabels": ["multi used", "multi unused", "single used", "single unused"], "span": 6, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/redis.json b/apps/bff/src/bundled_templates/layers/redis.json
new file mode 100644
index 0000000..6b45262
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/redis.json
@@ -0,0 +1,55 @@
+{
+  "key": "REDIS",
+  "alias": "Redis",
+  "color": "var(--sw-warn)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-redis-monitoring/",
+  "aliases": { "services": "Redis clusters", "instances": "Nodes" },
+  "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "cmds",
+    "columns": [
+      { "metric": "cmds", "label": "Commands/s", "mqe": "aggregate_labels(meter_redis_total_commands_rate,sum(cmd))", "aggregation": "sum" },
+      { "metric": "hitRate", "label": "Hit Rate", "unit": "%", "mqe": "aggregate_labels(meter_redis_hit_rate,avg)", "aggregation": "avg" },
+      { "metric": "memUsage", "label": "Memory %", "unit": "%", "mqe": "latest(aggregate_labels(meter_redis_memory_used_bytes,sum)/aggregate_labels(meter_redis_memory_max_bytes,sum))*100", "aggregation": "avg" },
+      { "metric": "conns", "label": "Clients", "mqe": "latest(aggregate_labels(meter_redis_connected_clients,sum))", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Health", "size": "auto", "metrics": [
+        { "id": "hitRate", "label": "Hit Rate", "tip": "Cache hit ratio. Trending down = thrash.", "mqe": "aggregate_labels(meter_redis_hit_rate,avg)", "unit": "%", "aggregation": "avg" },
+        { "id": "memUsage", "label": "Memory %", "tip": "Used / max memory. >80% = warn.", "mqe": "latest(aggregate_labels(meter_redis_memory_used_bytes,sum)/aggregate_labels(meter_redis_memory_max_bytes,sum))*100", "unit": "%", "aggregation": "avg" }
+      ]},
+      { "title": "Throughput", "size": "square", "metrics": [
+        { "id": "cmds", "label": "Commands/s", "tip": "Total commands per second across the cluster.", "mqe": "aggregate_labels(meter_redis_total_commands_rate,sum(cmd))", "aggregation": "sum" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "uptime", "title": "Uptime (days)", "type": "line", "expressions": ["latest(aggregate_labels(meter_redis_uptime,max))/3600/24"], "span": 3, "rowSpan": 2 },
+      { "id": "clients", "title": "Connected Clients", "type": "line", "expressions": ["latest(aggregate_labels(meter_redis_connected_clients,sum))"], "span": 3, "rowSpan": 2 },
+      { "id": "blocked", "title": "Blocked Clients", "type": "line", "expressions": ["latest(aggregate_labels(meter_redis_blocked_clients,sum))"], "span": 3, "rowSpan": 2 },
+      { "id": "mem_pct", "title": "Memory Usage", "type": "line", "unit": "%", "expressions": ["latest(aggregate_labels(meter_redis_memory_used_bytes,sum)/aggregate_labels(meter_redis_memory_max_bytes,sum))*100"], "span": 3, "rowSpan": 2 },
+      { "id": "commands", "title": "Total Commands / s", "type": "line", "expressions": ["aggregate_labels(meter_redis_total_commands_rate,sum(cmd))"], "span": 4, "rowSpan": 2 },
+      { "id": "hit_rate", "title": "Hit Rate", "type": "line", "unit": "%", "expressions": ["aggregate_labels(meter_redis_hit_rate,avg)"], "span": 4, "rowSpan": 2 },
+      { "id": "avg_cmd_time", "title": "Avg Command Time / s", "type": "line", "expressions": ["aggregate_labels(meter_redis_commands_duration,sum(cmd))/aggregate_labels(meter_redis_commands_total,sum(cmd))"], "span": 4, "rowSpan": 2 },
+      { "id": "net_io", "title": "Net I/O (KB)", "type": "line", "expressions": ["aggregate_labels(meter_redis_net_input_bytes_total,sum)/1024", "aggregate_labels(meter_redis_net_output_bytes_total,sum)/1024"], "expressionLabels": ["in", "out"], "span": 6, "rowSpan": 2 },
+      { "id": "db_keys", "title": "Keys", "type": "line", "expressions": ["aggregate_labels(meter_redis_db_keys,sum)", "aggregate_labels(meter_redis_evicted_keys_total,sum)", "aggregate_labels(meter_redis_expired_keys_total,sum)"], "expressionLabels": ["total", "evicted", "expired"], "span": 6, "rowSpan": 2 },
+      { "id": "slow_cmds", "title": "Slow Commands", "tip": "Top 10 slowest captured commands. Captured by SkyWalking agent at the call-site.", "type": "record", "unit": "ms", "expressions": ["top_n(top_n_database_statement,10,des)"], "span": 12, "rowSpan": 4 }
+    ],
+    "instance": [
+      { "id": "uptime", "title": "Uptime (days)", "type": "line", "expressions": ["latest(meter_redis_instance_uptime)/3600/24"], "span": 3, "rowSpan": 2 },
+      { "id": "clients", "title": "Connected Clients", "type": "line", "expressions": ["latest(meter_redis_instance_connected_clients)"], "span": 3, "rowSpan": 2 },
+      { "id": "blocked", "title": "Blocked Clients", "type": "line", "expressions": ["latest(meter_redis_instance_redis_blocked_clients)"], "span": 3, "rowSpan": 2 },
+      { "id": "max_mem", "title": "Memory Max (MB)", "type": "line", "expressions": ["latest(meter_redis_instance_memory_max_bytes)/1000/1000"], "span": 3, "rowSpan": 2 },
+      { "id": "mem_pct", "title": "Memory Usage (%)", "type": "line", "unit": "%", "expressions": ["meter_redis_instance_memory_usage"], "span": 4, "rowSpan": 2 },
+      { "id": "cmds", "title": "Commands / s", "type": "line", "expressions": ["meter_redis_instance_total_commands_rate"], "span": 4, "rowSpan": 2 },
+      { "id": "hit_rate", "title": "Hit Rate", "type": "line", "unit": "%", "expressions": ["meter_redis_instance_hit_rate"], "span": 4, "rowSpan": 2 },
+      { "id": "net_io", "title": "Net I/O (KB)", "type": "line", "expressions": ["meter_redis_instance_net_input_bytes_total/1024", "meter_redis_instance_net_output_bytes_total/1024"], "expressionLabels": ["in", "out"], "span": 6, "rowSpan": 2 },
+      { "id": "db_keys", "title": "Keys", "type": "line", "expressions": ["meter_redis_instance_db_keys", "meter_redis_instance_evicted_keys_total", "meter_redis_instance_expired_keys_total"], "expressionLabels": ["total", "evicted", "expired"], "span": 6, "rowSpan": 2 },
+      { "id": "cmd_dur", "title": "Total Command Time (s)", "type": "line", "expressions": ["meter_redis_instance_commands_duration_seconds_total_rate"], "span": 6, "rowSpan": 2 },
+      { "id": "avg_cmd_time", "title": "Avg Command Time", "type": "line", "expressions": ["meter_redis_instance_average_time_spent_by_command"], "span": 6, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/rocketmq.json b/apps/bff/src/bundled_templates/layers/rocketmq.json
new file mode 100644
index 0000000..1454fd1
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/rocketmq.json
@@ -0,0 +1,54 @@
+{
+  "key": "ROCKETMQ",
+  "alias": "RocketMQ",
+  "color": "var(--sw-purple)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-rocketmq-monitoring/",
+  "aliases": { "services": "RocketMQ clusters", "instances": "Brokers", "endpoints": "Topics" },
+  "components": { "service": true, "instances": true, "endpoints": true, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "produceTps",
+    "columns": [
+      { "metric": "produceTps", "label": "Produce TPS", "mqe": "meter_rocketmq_cluster_total_producer_tps", "aggregation": "avg" },
+      { "metric": "consumeTps", "label": "Consume TPS", "mqe": "meter_rocketmq_cluster_total_consumer_tps", "aggregation": "avg" },
+      { "metric": "topics", "label": "Topics", "mqe": "latest(meter_rocketmq_cluster_topic_count)", "aggregation": "sum" },
+      { "metric": "brokers", "label": "Brokers", "mqe": "latest(meter_rocketmq_cluster_broker_count)", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Throughput", "size": "auto", "metrics": [
+        { "id": "produceTps", "label": "Produce TPS", "tip": "Messages produced per second across the cluster.", "mqe": "meter_rocketmq_cluster_total_producer_tps", "aggregation": "avg" },
+        { "id": "consumeTps", "label": "Consume TPS", "tip": "Messages consumed per second.", "mqe": "meter_rocketmq_cluster_total_consumer_tps", "aggregation": "avg" },
+        { "id": "maxLag", "label": "Max Consumer Latency", "tip": "Worst consumer latency across the cluster.", "mqe": "latest(meter_rocketmq_cluster_max_consumer_latency)", "aggregation": "max" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "today_msgs", "title": "Messages Today", "type": "line", "expressions": ["latest(meter_rocketmq_cluster_messages_produced_today)", "latest(meter_rocketmq_cluster_messages_consumed_today)"], "expressionLabels": ["produced", "consumed"], "span": 6, "rowSpan": 2 },
+      { "id": "yesterday_msgs", "title": "Messages Yesterday", "type": "line", "expressions": ["latest(meter_rocketmq_cluster_messages_produced_until_yesterday)", "latest(meter_rocketmq_cluster_messages_consumed_until_yesterday)"], "expressionLabels": ["produced", "consumed"], "span": 6, "rowSpan": 2 },
+      { "id": "tps", "title": "Producer / Consumer TPS", "type": "line", "expressions": ["meter_rocketmq_cluster_total_producer_tps", "meter_rocketmq_cluster_total_consumer_tps"], "expressionLabels": ["producer", "consumer"], "span": 6, "rowSpan": 2 },
+      { "id": "msg_size", "title": "Producer / Consumer Message Size (MB)", "type": "line", "unit": "MB", "expressions": ["meter_rocketmq_cluster_producer_message_size/1024/1024", "meter_rocketmq_cluster_consumer_message_size/1024/1024"], "expressionLabels": ["produced", "consumed"], "span": 6, "rowSpan": 2 },
+      { "id": "consumer_lag", "title": "Max Consumer Latency", "type": "line", "expressions": ["latest(meter_rocketmq_cluster_max_consumer_latency)"], "span": 4, "rowSpan": 2 },
+      { "id": "commitlog_ratio", "title": "CommitLog Disk Ratio (%)", "type": "line", "unit": "%", "expressions": ["meter_rocketmq_cluster_commitLog_disk_ratio", "latest(meter_rocketmq_cluster_max_commitLog_disk_ratio)"], "expressionLabels": ["current", "max"], "span": 4, "rowSpan": 2 },
+      { "id": "queue_wait", "title": "ThreadPool Queue Head Wait (ms)", "type": "line", "unit": "ms", "expressions": ["meter_rocketmq_cluster_pull_threadPool_queue_head_wait_time", "meter_rocketmq_cluster_send_threadPool_queue_head_wait_time"], "expressionLabels": ["pull", "send"], "span": 4, "rowSpan": 2 },
+      { "id": "topics_brokers", "title": "Topics / Brokers", "type": "line", "expressions": ["latest(meter_rocketmq_cluster_topic_count)", "latest(meter_rocketmq_cluster_broker_count)"], "expressionLabels": ["topics", "brokers"], "span": 12, "rowSpan": 2 }
+    ],
+    "instance": [
+      { "id": "produce_tps", "title": "Produce TPS", "type": "line", "expressions": ["meter_rocketmq_broker_produce_tps"], "span": 3, "rowSpan": 2 },
+      { "id": "consume_qps", "title": "Consume QPS", "type": "line", "expressions": ["meter_rocketmq_broker_consume_qps"], "span": 3, "rowSpan": 2 },
+      { "id": "producer_size", "title": "Producer Msg Size (MB)", "type": "line", "unit": "MB", "expressions": ["meter_rocketmq_broker_producer_message_size/1024/1024"], "span": 3, "rowSpan": 2 },
+      { "id": "consumer_size", "title": "Consumer Msg Size (MB)", "type": "line", "unit": "MB", "expressions": ["meter_rocketmq_broker_consumer_message_size/1024/1024"], "span": 3, "rowSpan": 2 }
+    ],
+    "endpoint": [
+      { "id": "tps", "title": "Producer / Consumer Group TPS", "type": "line", "expressions": ["meter_rocketmq_topic_producer_tps", "meter_rocketmq_topic_consumer_group_tps"], "expressionLabels": ["producer", "consumer group"], "span": 6, "rowSpan": 2 },
+      { "id": "msg_size", "title": "Message Size (MB)", "type": "line", "unit": "MB", "expressions": ["meter_rocketmq_topic_producer_message_size/1024/1024", "meter_rocketmq_topic_consumer_message_size/1024/1024"], "expressionLabels": ["produced", "consumed"], "span": 6, "rowSpan": 2 },
+      { "id": "max_msg", "title": "Max Message Size (MB)", "type": "line", "unit": "MB", "expressions": ["latest(meter_rocketmq_topic_max_producer_message_size)/1024/1024", "latest(meter_rocketmq_topic_max_consumer_message_size)/1024/1024"], "expressionLabels": ["producer", "consumer"], "span": 6, "rowSpan": 2 },
+      { "id": "consumer_latency", "title": "Consumer Latency (s)", "type": "line", "unit": "s", "expressions": ["meter_rocketmq_topic_consumer_latency/1000"], "span": 6, "rowSpan": 2 },
+      { "id": "offsets", "title": "Producer / Consumer Offsets", "type": "line", "expressions": ["meter_rocketmq_topic_producer_offset", "meter_rocketmq_topic_consumer_group_offset"], "expressionLabels": ["producer", "consumer group"], "span": 6, "rowSpan": 2 },
+      { "id": "backlog", "title": "Backlogged Messages", "tip": "Producer offset minus consumer-group offset — the topic lag.", "type": "line", "expressions": ["meter_rocketmq_topic_producer_offset-meter_rocketmq_topic_consumer_group_offset"], "span": 6, "rowSpan": 2 },
+      { "id": "consumer_groups", "title": "Consumer Group Count", "type": "line", "expressions": ["latest(meter_rocketmq_topic_consumer_group_count)"], "span": 6, "rowSpan": 2 },
+      { "id": "brokers", "title": "Broker Count", "type": "line", "expressions": ["latest(meter_rocketmq_topic_broker_count)"], "span": 6, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/so11y_go_agent.json b/apps/bff/src/bundled_templates/layers/so11y_go_agent.json
new file mode 100644
index 0000000..a6b83a7
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/so11y_go_agent.json
@@ -0,0 +1,34 @@
+{
+  "key": "SO11Y_GO_AGENT",
+  "alias": "Go Agent (self-obs)",
+  "color": "var(--sw-fg-2)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/service-agent/server-agents/",
+  "aliases": { "services": "Agent services", "instances": "Agents" },
+  "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "created",
+    "columns": [
+      { "metric": "created", "label": "Tracing/min", "mqe": "aggregate_labels(meter_sw_go_created_tracing_context_count,sum)", "aggregation": "sum" },
+      { "metric": "ignored", "label": "Ignored/min", "mqe": "aggregate_labels(meter_sw_go_created_ignored_context_count,sum)", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Agent throughput", "size": "auto", "metrics": [
+        { "id": "created", "label": "Tracing/min", "tip": "Tracing contexts created per minute (sum across instances).", "mqe": "aggregate_labels(meter_sw_go_created_tracing_context_count,sum)", "aggregation": "sum" },
+        { "id": "ignored", "label": "Ignored/min", "tip": "Ignored contexts created per minute.", "mqe": "aggregate_labels(meter_sw_go_created_ignored_context_count,sum)", "aggregation": "sum" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "instance": [
+      { "id": "tracing_created", "title": "Tracing Context Creation / min", "type": "line", "expressions": ["meter_sw_go_created_tracing_context_count"], "span": 6, "rowSpan": 2 },
+      { "id": "tracing_create_complete", "title": "Tracing Created + Finished / min", "type": "line", "expressions": ["aggregate_labels(meter_sw_go_created_tracing_context_count,sum)", "meter_sw_go_finished_tracing_context_count"], "expressionLabels": ["created", "finished"], "span": 6, "rowSpan": 2 },
+      { "id": "ignored_created", "title": "Ignored Context Creation / min", "type": "line", "expressions": ["meter_sw_go_created_ignored_context_count"], "span": 6, "rowSpan": 2 },
+      { "id": "ignored_create_complete", "title": "Ignored Created + Finished / min", "type": "line", "expressions": ["aggregate_labels(meter_sw_go_created_ignored_context_count,sum)", "meter_sw_go_finished_ignored_context_count"], "expressionLabels": ["created", "finished"], "span": 6, "rowSpan": 2 },
+      { "id": "leaked", "title": "Possible Leaked Context / min", "type": "line", "expressions": ["meter_sw_go_possible_leaked_context_count"], "span": 6, "rowSpan": 2 },
+      { "id": "interceptor_err", "title": "Interceptor Error Count / min", "type": "line", "expressions": ["meter_sw_go_interceptor_error_count"], "span": 6, "rowSpan": 2 },
+      { "id": "exec_time", "title": "Tracing Context Execution Time (ms)", "type": "line", "unit": "ms", "expressions": ["relabels(meter_sw_go_tracing_context_execution_time_percentile,p='50,75,90,95,99',p='50,75,90,95,99')/1000000"], "span": 12, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/so11y_java_agent.json b/apps/bff/src/bundled_templates/layers/so11y_java_agent.json
new file mode 100644
index 0000000..fe3c4c7
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/so11y_java_agent.json
@@ -0,0 +1,34 @@
+{
+  "key": "SO11Y_JAVA_AGENT",
+  "alias": "Java Agent (self-obs)",
+  "color": "var(--sw-fg-2)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/service-agent/server-agents/",
+  "aliases": { "services": "Agent services", "instances": "Agents" },
+  "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "created",
+    "columns": [
+      { "metric": "created", "label": "Tracing/min", "mqe": "aggregate_labels(meter_java_agent_created_tracing_context_count,sum)", "aggregation": "sum" },
+      { "metric": "ignored", "label": "Ignored/min", "mqe": "aggregate_labels(meter_java_agent_created_ignored_context_count,sum)", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Agent throughput", "size": "auto", "metrics": [
+        { "id": "created", "label": "Tracing/min", "tip": "Tracing contexts created per minute (sum across instances).", "mqe": "aggregate_labels(meter_java_agent_created_tracing_context_count,sum)", "aggregation": "sum" },
+        { "id": "ignored", "label": "Ignored/min", "tip": "Ignored contexts created per minute.", "mqe": "aggregate_labels(meter_java_agent_created_ignored_context_count,sum)", "aggregation": "sum" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "instance": [
+      { "id": "tracing_created", "title": "Tracing Context Creation / min", "type": "line", "expressions": ["meter_java_agent_created_tracing_context_count"], "span": 6, "rowSpan": 2 },
+      { "id": "tracing_create_complete", "title": "Tracing Created + Finished / min", "type": "line", "expressions": ["aggregate_labels(meter_java_agent_created_tracing_context_count,sum)", "meter_java_agent_finished_tracing_context_count"], "expressionLabels": ["created", "finished"], "span": 6, "rowSpan": 2 },
+      { "id": "ignored_created", "title": "Ignored Context Creation / min", "type": "line", "expressions": ["meter_java_agent_created_ignored_context_count"], "span": 6, "rowSpan": 2 },
+      { "id": "ignored_create_complete", "title": "Ignored Created + Finished / min", "type": "line", "expressions": ["aggregate_labels(meter_java_agent_created_ignored_context_count,sum)", "meter_java_agent_finished_ignored_context_count"], "expressionLabels": ["created", "finished"], "span": 6, "rowSpan": 2 },
+      { "id": "leaked", "title": "Possible Leaked Context / min", "type": "line", "expressions": ["meter_java_agent_possible_leaked_context_count"], "span": 6, "rowSpan": 2 },
+      { "id": "interceptor_err", "title": "Interceptor Error Count / min", "type": "line", "expressions": ["meter_java_agent_interceptor_error_count"], "span": 6, "rowSpan": 2 },
+      { "id": "exec_time", "title": "Tracing Context Execution Time (ms)", "type": "line", "unit": "ms", "expressions": ["relabels(meter_java_agent_tracing_context_execution_time_percentile,p='50,75,90,95,99',p='50,75,90,95,99')/1000000"], "span": 12, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/so11y_oap.json b/apps/bff/src/bundled_templates/layers/so11y_oap.json
new file mode 100644
index 0000000..7f08d2b
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/so11y_oap.json
@@ -0,0 +1,55 @@
+{
+  "key": "SO11Y_OAP",
+  "alias": "OAP (self-obs)",
+  "color": "var(--sw-fg-2)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-setup/",
+  "aliases": { "services": "OAP services", "instances": "OAP nodes" },
+  "components": { "service": true, "instances": true, "endpoints": false, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "cpu",
+    "columns": [
+      { "metric": "cpu", "label": "CPU %", "unit": "%", "mqe": "avg(meter_oap_instance_cpu_percentage)", "aggregation": "avg" },
+      { "metric": "persist", "label": "Persist/min", "mqe": "sum(meter_oap_instance_persistence_execute_count)", "aggregation": "sum" },
+      { "metric": "graphql", "label": "GraphQL/min", "mqe": "sum(meter_oap_instance_graphql_query_count)", "aggregation": "sum" },
+      { "metric": "graphqlErr", "label": "GraphQL Err", "mqe": "sum(meter_oap_instance_graphql_query_error_count)", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "OAP health", "size": "auto", "metrics": [
+        { "id": "cpu", "label": "CPU %", "tip": "OAP process CPU usage averaged across instances.", "mqe": "avg(meter_oap_instance_cpu_percentage)", "unit": "%", "aggregation": "avg" },
+        { "id": "graphqlErr", "label": "GraphQL Err", "tip": "GraphQL query errors per minute summed across instances.", "mqe": "sum(meter_oap_instance_graphql_query_error_count)", "aggregation": "sum" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "instance": [
+      { "id": "cpu", "title": "CPU (%)", "type": "line", "unit": "%", "expressions": ["meter_oap_instance_cpu_percentage"], "span": 3, "rowSpan": 2 },
+      { "id": "mem", "title": "JVM Memory (MB)", "type": "line", "unit": "MB", "expressions": ["meter_oap_instance_jvm_memory_bytes_used/1024/1024"], "span": 3, "rowSpan": 2 },
+      { "id": "gc_count", "title": "GC Count / min", "type": "line", "expressions": ["meter_oap_instance_jvm_gc_count"], "span": 3, "rowSpan": 2 },
+      { "id": "gc_time", "title": "GC Time (ms / min)", "type": "line", "unit": "ms", "expressions": ["meter_oap_instance_jvm_gc_time"], "span": 3, "rowSpan": 2 },
+      { "id": "buffer_pool", "title": "Buffer Pool (MB)", "type": "line", "unit": "MB", "expressions": ["meter_oap_instance_jvm_buffer_pool_bytes_used/1024/1024"], "span": 3, "rowSpan": 2 },
+      { "id": "threads", "title": "Thread Count", "type": "line", "expressions": ["meter_oap_jvm_thread_live_count", "meter_oap_jvm_thread_peak_count", "meter_oap_jvm_thread_daemon_count"], "expressionLabels": ["live", "peak", "daemon"], "span": 5, "rowSpan": 2 },
+      { "id": "thread_states", "title": "Thread States", "type": "line", "expressions": ["meter_oap_jvm_thread_runnable_count", "meter_oap_jvm_thread_timed_waiting_count", "meter_oap_jvm_thread_blocked_count", "meter_oap_jvm_thread_waiting_count"], "expressionLabels": ["runnable", "timed-waiting", "blocked", "waiting"], "span": 4, "rowSpan": 2 },
+      { "id": "classes", "title": "Class Count", "type": "line", "expressions": ["meter_oap_jvm_class_loaded_count", "meter_oap_jvm_class_total_unloaded_count", "meter_oap_jvm_class_total_loaded_count"], "expressionLabels": ["loaded", "unloaded total", "loaded total"], "span": 12, "rowSpan": 2 },
+      { "id": "aggregation", "title": "Aggregation / min", "type": "line", "expressions": ["meter_oap_instance_metrics_aggregation"], "span": 4, "rowSpan": 2 },
+      { "id": "persistence_counts", "title": "Persistence Counts / min", "type": "line", "expressions": ["meter_oap_instance_persistence_prepare_count", "meter_oap_instance_persistence_execute_count"], "expressionLabels": ["prepare", "execute"], "span": 4, "rowSpan": 2 },
+      { "id": "persistence_cache", "title": "Persistent Cache / min", "type": "line", "expressions": ["meter_oap_instance_metrics_persistent_cache"], "span": 4, "rowSpan": 2 },
+      { "id": "persist_prepare_latency", "title": "Persistence Prepare Latency (ms)", "type": "line", "unit": "ms", "expressions": ["relabels(meter_oap_instance_persistence_prepare_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"], "span": 6, "rowSpan": 2 },
+      { "id": "persist_execute_latency", "title": "Persistence Execute Latency (ms)", "type": "line", "unit": "ms", "expressions": ["relabels(meter_oap_instance_persistence_execute_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"], "span": 6, "rowSpan": 2 },
+      { "id": "graphql_latency", "title": "GraphQL Latency (ms)", "type": "line", "unit": "ms", "expressions": ["relabels(meter_oap_graphql_query_latency_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"], "span": 6, "rowSpan": 2 },
+      { "id": "graphql_count", "title": "GraphQL Query Count", "type": "line", "expressions": ["meter_oap_instance_graphql_query_count", "meter_oap_instance_graphql_query_error_count"], "expressionLabels": ["queries", "errors"], "span": 6, "rowSpan": 2 },
+      { "id": "trace_analysis", "title": "Trace Analysis / min", "type": "line", "expressions": ["meter_oap_instance_trace_count", "meter_oap_instance_trace_analysis_error_count"], "expressionLabels": ["total", "errors"], "span": 6, "rowSpan": 2 },
+      { "id": "trace_latency", "title": "Trace Analysis Latency (ms)", "type": "line", "unit": "ms", "expressions": ["relabels(meter_oap_instance_trace_latency_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"], "span": 6, "rowSpan": 2 },
+      { "id": "mesh_analysis", "title": "Mesh Analysis / min", "type": "line", "expressions": ["meter_oap_instance_mesh_count", "meter_oap_instance_mesh_analysis_error_count"], "expressionLabels": ["total", "errors"], "span": 6, "rowSpan": 2 },
+      { "id": "mesh_latency", "title": "Mesh Analysis Latency (ms)", "type": "line", "unit": "ms", "expressions": ["relabels(meter_oap_instance_mesh_latency_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"], "span": 6, "rowSpan": 2 },
+      { "id": "otel_received", "title": "OTEL Received / s", "type": "line", "expressions": ["meter_oap_otel_metrics_received", "meter_oap_otel_logs_received", "meter_oap_otel_spans_received"], "expressionLabels": ["metrics", "logs", "spans"], "span": 6, "rowSpan": 2 },
+      { "id": "k8s_als", "title": "K8S ALS", "type": "line", "expressions": ["meter_oap_instance_k8s_als_count", "meter_oap_instance_k8s_als_drop", "meter_oap_instance_k8s_als_streams", "meter_oap_instance_k8s_als_error_streams"], "expressionLabels": ["count", "dropped", "streams", "err streams"], "span": 6, "rowSpan": 2 },
+      { "id": "watermark", "title": "Watermark Circuit Breaker", "tip": "Cumulative break + recover counters per listener.", "type": "line", "expressions": ["meter_oap_instance_watermark_circuit_breaker_break_count", "meter_oap_instance_watermark_circuit_breaker_recover_count"], "expressionLabels": ["break", "recover"], "span": 6, "rowSpan": 2 },
+      { "id": "aggr_queue", "title": "Aggregation Queue Usage (%)", "type": "line", "unit": "%", "expressions": ["sort_values(meter_oap_instance_metrics_aggregation_queue_used_per_ten_thousand{level='1'},10,des,avg)/100", "sort_values(meter_oap_instance_metrics_aggregation_queue_used_per_ten_thousand{level='2'},10,des,avg)/100"], "expressionLabels": ["L1", "L2"], "span": 6, "rowSpan": 2 },
+      { "id": "banyandb_latency", "title": "BanyanDB Write Latency (ms)", "type": "line", "unit": "ms", "expressions": ["meter_oap_banyandb_write_latency_percentile{catalog='measure',operation='bulk_write'}", "meter_oap_banyandb_write_latency_percentile{catalog='stream',operation='bulk_write'}", "meter_oap_banyandb_write_latency_percentile{catalog='trace',operation='bulk_write'}", "meter_oap_banyandb_write_latency_percentile{catalog='stream',operation='single_write'}", "meter_oap_banyand [...]
+      { "id": "es_latency", "title": "Elasticsearch Write Latency (ms)", "type": "line", "unit": "ms", "expressions": ["meter_oap_elasticsearch_write_latency_percentile{operation='single_write,single_update,single_delete'}", "meter_oap_elasticsearch_write_latency_percentile{operation='bulk_write'}"], "expressionLabels": ["single", "bulk"], "visibleWhen": "meter_oap_elasticsearch_write_latency_percentile has value", "span": 6, "rowSpan": 2 },
+      { "id": "zipkin_drop", "title": "Zipkin Spans Dropped", "type": "line", "expressions": ["meter_oap_instance_spans_dropped_count"], "span": 6, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/so11y_satellite.json b/apps/bff/src/bundled_templates/layers/so11y_satellite.json
new file mode 100644
index 0000000..9cd4722
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/so11y_satellite.json
@@ -0,0 +1,35 @@
+{
+  "key": "SO11Y_SATELLITE",
+  "alias": "Satellite (self-obs)",
+  "color": "var(--sw-fg-2)",
+  "documentLink": "https://skywalking.apache.org/docs/skywalking-satellite/next/readme/",
+  "aliases": { "services": "Satellite services" },
+  "components": { "service": true, "instances": false, "endpoints": false, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "conns",
+    "columns": [
+      { "metric": "conns", "label": "gRPC Conns", "mqe": "satellite_service_grpc_connect_count", "aggregation": "sum" },
+      { "metric": "cpu", "label": "CPU %", "unit": "%", "mqe": "satellite_service_server_cpu_utilization", "aggregation": "avg" },
+      { "metric": "queueUsed", "label": "Queue Used", "mqe": "satellite_service_queue_used_count", "aggregation": "sum" },
+      { "metric": "recvEvents", "label": "Recv Events", "mqe": "satellite_service_receive_event_count", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Satellite health", "size": "auto", "metrics": [
+        { "id": "cpu", "label": "CPU %", "tip": "Satellite process CPU.", "mqe": "satellite_service_server_cpu_utilization", "unit": "%", "aggregation": "avg" },
+        { "id": "queueUsed", "label": "Queue Used", "tip": "Outstanding queue items — backlog indicator.", "mqe": "satellite_service_queue_used_count", "aggregation": "sum" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "conns", "title": "Connection Count", "type": "line", "expressions": ["satellite_service_grpc_connect_count"], "span": 4, "rowSpan": 2 },
+      { "id": "cpu", "title": "CPU (%)", "type": "line", "unit": "%", "expressions": ["satellite_service_server_cpu_utilization"], "span": 4, "rowSpan": 2 },
+      { "id": "queue_used", "title": "Queue Used", "type": "line", "expressions": ["satellite_service_queue_used_count"], "span": 4, "rowSpan": 2 },
+      { "id": "receive", "title": "Receive Events", "type": "line", "expressions": ["satellite_service_receive_event_count"], "span": 4, "rowSpan": 2 },
+      { "id": "fetch", "title": "Fetch Events", "type": "line", "expressions": ["satellite_service_fetch_event_count"], "span": 4, "rowSpan": 2 },
+      { "id": "queue_io", "title": "Queue Input / Output", "type": "line", "expressions": ["satellite_service_queue_input_count", "satellite_service_send_event_count"], "expressionLabels": ["input", "output"], "span": 4, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/virtual_cache.json b/apps/bff/src/bundled_templates/layers/virtual_cache.json
new file mode 100644
index 0000000..a6b8ac3
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/virtual_cache.json
@@ -0,0 +1,184 @@
+{
+  "key": "VIRTUAL_CACHE",
+  "alias": "Virtual Cache",
+  "color": "var(--sw-warn)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/service-agent/virtual-cache/",
+  "aliases": {
+    "services": "Caches"
+  },
+  "components": {
+    "service": true,
+    "instances": false,
+    "endpoints": false,
+    "endpointDependency": false,
+    "topology": false,
+    "traces": false,
+    "logs": false
+  },
+  "layer-header": {
+    "orderBy": "cpm",
+    "columns": [
+      { "metric": "cpm", "label": "Access RPM", "unit": "rpm", "mqe": "cache_access_cpm", "aggregation": "sum" },
+      { "metric": "respTime", "label": "Latency", "unit": "ms", "mqe": "cache_access_resp_time", "aggregation": "avg" },
+      { "metric": "p95", "label": "p95", "unit": "ms", "mqe": "cache_access_percentile{p='95'}", "aggregation": "avg" },
+      { "metric": "err", "label": "Error Rate", "unit": "%", "mqe": "100 - cache_access_sla/100", "aggregation": "avg" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      {
+        "title": "Latency & errors",
+        "size": "auto",
+        "metrics": [
+          { "id": "p95", "label": "P95", "tip": "95th-percentile cache access latency.", "mqe": "cache_access_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+          { "id": "err", "label": "Error Rate", "tip": "100 − cache access success rate. Cache exceptions captured by the calling service's agent.", "mqe": "100 - cache_access_sla/100", "unit": "%", "aggregation": "avg" }
+        ]
+      },
+      {
+        "title": "Throughput",
+        "size": "square",
+        "metrics": [
+          { "id": "cpm", "label": "Access RPM", "tip": "Cache operations per minute (read + write).", "mqe": "cache_access_cpm", "unit": "rpm", "aggregation": "sum" }
+        ]
+      }
+    ]
+  },
+  "dashboards": {
+    "service": [
+      {
+        "id": "access_cpm_line",
+        "title": "Access Traffic",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["cache_access_cpm"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "access_resp_line",
+        "title": "Avg Access Latency",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["cache_access_resp_time"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "access_sla_line",
+        "title": "Access Success Rate",
+        "type": "line",
+        "unit": "%",
+        "expressions": ["cache_access_sla/100"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "access_percentile",
+        "title": "Access Latency Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(cache_access_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 12,
+        "rowSpan": 2
+      },
+      {
+        "id": "read_traffic",
+        "title": "Read Traffic",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["cache_read_cpm"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "read_resp",
+        "title": "Read Avg Latency",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["cache_read_resp_time"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "write_traffic",
+        "title": "Write Traffic",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["cache_write_cpm"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "write_resp",
+        "title": "Write Avg Latency",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["cache_write_resp_time"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "read_sla",
+        "title": "Read Success Rate",
+        "type": "line",
+        "unit": "%",
+        "expressions": ["cache_read_sla/100"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "write_sla",
+        "title": "Write Success Rate",
+        "type": "line",
+        "unit": "%",
+        "expressions": ["cache_write_sla/100"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "read_percentile",
+        "title": "Read Latency Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(cache_read_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "write_percentile",
+        "title": "Write Latency Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(cache_write_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "slow_read",
+        "title": "Slow Read Commands",
+        "tip": "Top 10 slowest captured read commands. Each row is a single execution — click to drill into the originating trace.",
+        "type": "record",
+        "unit": "ms",
+        "expressions": ["top_n(top_n_cache_read_command, 10, des)"],
+        "span": 6,
+        "rowSpan": 4
+      },
+      {
+        "id": "slow_write",
+        "title": "Slow Write Commands",
+        "tip": "Top 10 slowest captured write commands. Each row is a single execution — click to drill into the originating trace.",
+        "type": "record",
+        "unit": "ms",
+        "expressions": ["top_n(top_n_cache_write_command, 10, des)"],
+        "span": 6,
+        "rowSpan": 4
+      }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/virtual_database.json b/apps/bff/src/bundled_templates/layers/virtual_database.json
new file mode 100644
index 0000000..cbbacb9
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/virtual_database.json
@@ -0,0 +1,101 @@
+{
+  "key": "VIRTUAL_DATABASE",
+  "alias": "Virtual Database",
+  "color": "var(--sw-warn)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/service-agent/virtual-database/",
+  "aliases": {
+    "services": "Databases"
+  },
+  "components": {
+    "service": true,
+    "instances": false,
+    "endpoints": false,
+    "endpointDependency": false,
+    "topology": false,
+    "traces": false,
+    "logs": false
+  },
+  "layer-header": {
+    "orderBy": "cpm",
+    "columns": [
+      { "metric": "cpm", "label": "Access RPM", "unit": "rpm", "mqe": "database_access_cpm", "aggregation": "sum" },
+      { "metric": "respTime", "label": "Latency", "unit": "ms", "mqe": "database_access_resp_time", "aggregation": "avg" },
+      { "metric": "p95", "label": "p95", "unit": "ms", "mqe": "database_access_percentile{p='95'}", "aggregation": "avg" },
+      { "metric": "err", "label": "Error Rate", "unit": "%", "mqe": "100 - database_access_sla/100", "aggregation": "avg" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      {
+        "title": "Latency & errors",
+        "size": "auto",
+        "metrics": [
+          { "id": "p95", "label": "P95", "tip": "95th-percentile database access latency.", "mqe": "database_access_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+          { "id": "err", "label": "Error Rate", "tip": "100 − database access success rate. Driven by query exceptions captured by the calling service's agent.", "mqe": "100 - database_access_sla/100", "unit": "%", "aggregation": "avg" }
+        ]
+      },
+      {
+        "title": "Throughput",
+        "size": "square",
+        "metrics": [
+          { "id": "cpm", "label": "Access RPM", "tip": "Queries per minute against this virtual database.", "mqe": "database_access_cpm", "unit": "rpm", "aggregation": "sum" }
+        ]
+      }
+    ]
+  },
+  "dashboards": {
+    "service": [
+      {
+        "id": "traffic_line",
+        "title": "Access Traffic",
+        "tip": "Queries per minute against the virtual database.",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["database_access_cpm"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "resp_time_line",
+        "title": "Avg Response Time",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["database_access_resp_time"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "sla_line",
+        "title": "Success Rate",
+        "tip": "Percentage of queries that returned without throwing.",
+        "type": "line",
+        "unit": "%",
+        "expressions": ["database_access_sla/100"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "percentile_line",
+        "title": "Response Time Percentile",
+        "tip": "p50 / p75 / p90 / p95 / p99 latency — the tail of the access-time distribution.",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(database_access_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 12,
+        "rowSpan": 2
+      },
+      {
+        "id": "slow_statements",
+        "title": "Slow Statements",
+        "tip": "Top 20 slowest captured statements. Each row is a single statement execution — click to drill into the originating trace.",
+        "type": "record",
+        "unit": "ms",
+        "expressions": ["top_n(top_n_database_statement, 20, des)"],
+        "span": 12,
+        "rowSpan": 4
+      }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/virtual_genai.json b/apps/bff/src/bundled_templates/layers/virtual_genai.json
new file mode 100644
index 0000000..bbfc62a
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/virtual_genai.json
@@ -0,0 +1,49 @@
+{
+  "key": "VIRTUAL_GENAI",
+  "alias": "Virtual GenAI",
+  "color": "var(--sw-pink)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/service-agent/virtual-genai/",
+  "aliases": { "services": "GenAI Providers", "endpoints": "Models" },
+  "components": { "service": true, "instances": false, "endpoints": true, "topology": false, "traces": false, "logs": false },
+  "layer-header": {
+    "orderBy": "cpm",
+    "columns": [
+      { "metric": "cpm", "label": "RPM", "unit": "rpm", "mqe": "gen_ai_provider_cpm", "aggregation": "sum" },
+      { "metric": "respTime", "label": "Latency", "unit": "ms", "mqe": "gen_ai_provider_resp_time", "aggregation": "avg" },
+      { "metric": "sla", "label": "Success Rate", "unit": "%", "mqe": "gen_ai_provider_sla/100", "aggregation": "avg" },
+      { "metric": "outTokens", "label": "Output Tokens", "mqe": "latest(gen_ai_provider_output_tokens_sum)", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      { "title": "Latency & errors", "size": "auto", "metrics": [
+        { "id": "respTime", "label": "Avg Latency", "tip": "Avg call latency to the GenAI provider.", "mqe": "gen_ai_provider_resp_time", "unit": "ms", "aggregation": "avg" },
+        { "id": "err", "label": "Error Rate", "tip": "100 − provider SLA.", "mqe": "100 - gen_ai_provider_sla/100", "unit": "%", "aggregation": "avg" }
+      ]},
+      { "title": "Cost", "size": "square", "metrics": [
+        { "id": "totalCost", "label": "Total Estimated Cost", "tip": "Cumulative provider cost estimate.", "mqe": "latest(gen_ai_provider_total_estimated_cost)/1000000", "aggregation": "sum" }
+      ]}
+    ]
+  },
+  "dashboards": {
+    "service": [
+      { "id": "cpm", "title": "Calls / min", "type": "line", "unit": "rpm", "expressions": ["gen_ai_provider_cpm"], "span": 4, "rowSpan": 2 },
+      { "id": "resp_time", "title": "Avg Response Time", "type": "line", "unit": "ms", "expressions": ["gen_ai_provider_resp_time"], "span": 4, "rowSpan": 2 },
+      { "id": "sla", "title": "Success Rate", "type": "line", "unit": "%", "expressions": ["gen_ai_provider_sla/100"], "span": 4, "rowSpan": 2 },
+      { "id": "percentile", "title": "Latency Percentile", "type": "line", "unit": "ms", "expressions": ["gen_ai_provider_latency_percentile"], "span": 12, "rowSpan": 2 },
+      { "id": "input_tokens", "title": "Input Tokens", "type": "line", "expressions": ["latest(gen_ai_provider_input_tokens_sum)", "gen_ai_provider_input_tokens_avg"], "expressionLabels": ["sum", "avg"], "span": 6, "rowSpan": 2 },
+      { "id": "output_tokens", "title": "Output Tokens", "type": "line", "expressions": ["latest(gen_ai_provider_output_tokens_sum)", "gen_ai_provider_output_tokens_avg"], "expressionLabels": ["sum", "avg"], "span": 6, "rowSpan": 2 },
+      { "id": "cost", "title": "Estimated Cost", "type": "line", "expressions": ["latest(gen_ai_provider_total_estimated_cost)/1000000", "gen_ai_provider_avg_estimated_cost/1000000"], "expressionLabels": ["total", "avg"], "span": 12, "rowSpan": 2 }
+    ],
+    "endpoint": [
+      { "id": "cpm", "title": "Calls / min", "type": "line", "unit": "rpm", "expressions": ["gen_ai_model_call_cpm"], "span": 4, "rowSpan": 2 },
+      { "id": "latency_avg", "title": "Avg Latency", "type": "line", "unit": "ms", "expressions": ["gen_ai_model_latency_avg"], "span": 4, "rowSpan": 2 },
+      { "id": "sla", "title": "Success Rate", "type": "line", "unit": "%", "expressions": ["gen_ai_model_sla/100"], "span": 4, "rowSpan": 2 },
+      { "id": "latency_percentile", "title": "Latency Percentile", "type": "line", "unit": "ms", "expressions": ["gen_ai_model_latency_percentile"], "span": 6, "rowSpan": 2 },
+      { "id": "ttft", "title": "TTFT", "type": "line", "unit": "ms", "expressions": ["gen_ai_model_ttft_avg", "gen_ai_model_ttft_percentile"], "expressionLabels": ["avg", "percentile"], "span": 6, "rowSpan": 2 },
+      { "id": "input_tokens", "title": "Input Tokens", "type": "line", "expressions": ["latest(gen_ai_model_input_tokens_sum)", "gen_ai_model_input_tokens_avg"], "expressionLabels": ["sum", "avg"], "span": 6, "rowSpan": 2 },
+      { "id": "output_tokens", "title": "Output Tokens", "type": "line", "expressions": ["latest(gen_ai_model_output_tokens_sum)", "gen_ai_model_output_tokens_avg"], "expressionLabels": ["sum", "avg"], "span": 6, "rowSpan": 2 },
+      { "id": "cost", "title": "Estimated Cost", "type": "line", "expressions": ["latest(gen_ai_model_total_estimated_cost)/1000000", "gen_ai_model_avg_estimated_cost/1000000"], "expressionLabels": ["total", "avg"], "span": 12, "rowSpan": 2 }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/virtual_mq.json b/apps/bff/src/bundled_templates/layers/virtual_mq.json
new file mode 100644
index 0000000..c8d902e
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/virtual_mq.json
@@ -0,0 +1,168 @@
+{
+  "key": "VIRTUAL_MQ",
+  "alias": "Virtual MQ",
+  "color": "var(--sw-warn)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/service-agent/virtual-mq/",
+  "aliases": {
+    "services": "MQ clusters",
+    "endpoints": "Topics"
+  },
+  "components": {
+    "service": true,
+    "instances": false,
+    "endpoints": true,
+    "endpointDependency": false,
+    "topology": false,
+    "traces": false,
+    "logs": false
+  },
+  "layer-header": {
+    "orderBy": "consumeCpm",
+    "columns": [
+      { "metric": "consumeCpm", "label": "Consume RPM", "unit": "rpm", "mqe": "mq_service_consume_cpm", "aggregation": "sum" },
+      { "metric": "produceCpm", "label": "Produce RPM", "unit": "rpm", "mqe": "mq_service_produce_cpm", "aggregation": "sum" },
+      { "metric": "consumeLatency", "label": "Consume Latency", "unit": "ms", "mqe": "mq_service_consume_latency", "aggregation": "avg" },
+      { "metric": "consumeErr", "label": "Consume Error Rate", "unit": "%", "mqe": "100 - mq_service_consume_sla/100", "aggregation": "avg" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      {
+        "title": "Latency & errors",
+        "size": "auto",
+        "metrics": [
+          { "id": "p95", "label": "P95", "tip": "95th-percentile consume latency (timestamp(received) − timestamp(producing)).", "mqe": "mq_service_consume_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+          { "id": "consumeErr", "label": "Consume Error Rate", "tip": "100 − consume success rate.", "mqe": "100 - mq_service_consume_sla/100", "unit": "%", "aggregation": "avg" },
+          { "id": "produceErr", "label": "Produce Error Rate", "tip": "100 − produce success rate.", "mqe": "100 - mq_service_produce_sla/100", "unit": "%", "aggregation": "avg" }
+        ]
+      },
+      {
+        "title": "Throughput",
+        "size": "square",
+        "metrics": [
+          { "id": "consumeCpm", "label": "Consume RPM", "tip": "Messages consumed per minute.", "mqe": "mq_service_consume_cpm", "unit": "rpm", "aggregation": "sum" }
+        ]
+      }
+    ]
+  },
+  "dashboards": {
+    "service": [
+      {
+        "id": "consume_cpm_line",
+        "title": "Consume Traffic",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["mq_service_consume_cpm"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "produce_cpm_line",
+        "title": "Produce Traffic",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["mq_service_produce_cpm"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "consume_latency_line",
+        "title": "Consume Avg Latency",
+        "tip": "Avg time between message production and consumption.",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["mq_service_consume_latency"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "consume_sla_line",
+        "title": "Consume Success Rate",
+        "type": "line",
+        "unit": "%",
+        "expressions": ["mq_service_consume_sla/100"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "produce_sla_line",
+        "title": "Produce Success Rate",
+        "type": "line",
+        "unit": "%",
+        "expressions": ["mq_service_produce_sla/100"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "consume_percentile",
+        "title": "Consume Latency Percentile",
+        "tip": "p50 / p75 / p90 / p95 / p99 of consume latency.",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(mq_service_consume_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 6,
+        "rowSpan": 2
+      }
+    ],
+    "endpoint": [
+      {
+        "id": "ep_consume_cpm",
+        "title": "Topic Consume Traffic",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["mq_endpoint_consume_cpm"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "ep_produce_cpm",
+        "title": "Topic Produce Traffic",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["mq_endpoint_produce_cpm"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "ep_consume_latency",
+        "title": "Topic Consume Avg Latency",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["mq_endpoint_consume_latency"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "ep_consume_sla",
+        "title": "Topic Consume Success Rate",
+        "type": "line",
+        "unit": "%",
+        "expressions": ["mq_endpoint_consume_sla/100"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "ep_produce_sla",
+        "title": "Topic Produce Success Rate",
+        "type": "line",
+        "unit": "%",
+        "expressions": ["mq_endpoint_produce_sla/100"],
+        "span": 6,
+        "rowSpan": 2
+      },
+      {
+        "id": "ep_consume_percentile",
+        "title": "Topic Consume Latency Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(mq_endpoint_consume_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 12,
+        "rowSpan": 2
+      }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/layers/wechat_mini_program.json b/apps/bff/src/bundled_templates/layers/wechat_mini_program.json
new file mode 100644
index 0000000..92ad8b4
--- /dev/null
+++ b/apps/bff/src/bundled_templates/layers/wechat_mini_program.json
@@ -0,0 +1,234 @@
+{
+  "key": "WECHAT_MINI_PROGRAM",
+  "alias": "WeChat Mini Program",
+  "color": "var(--sw-cyan)",
+  "documentLink": "https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-wechat-mini-program-monitoring/",
+  "aliases": {
+    "services": "Mini-programs",
+    "instances": "Versions",
+    "endpoints": "Pages"
+  },
+  "components": {
+    "service": true,
+    "instances": true,
+    "endpoints": true,
+    "topology": false,
+    "traces": true,
+    "logs": true
+  },
+  "layer-header": {
+    "orderBy": "cpm",
+    "columns": [
+      { "metric": "cpm", "label": "Request RPM", "unit": "rpm", "mqe": "meter_wechat_mp_request_cpm", "aggregation": "sum" },
+      { "metric": "launch", "label": "Launch", "unit": "ms", "mqe": "meter_wechat_mp_app_launch_duration", "aggregation": "avg" },
+      { "metric": "firstRender", "label": "First Render", "unit": "ms", "mqe": "meter_wechat_mp_first_render_duration", "aggregation": "avg" },
+      { "metric": "errors", "label": "Errors", "mqe": "meter_wechat_mp_error_count", "aggregation": "sum" }
+    ]
+  },
+  "overview": {
+    "groups": [
+      {
+        "title": "Latency & errors",
+        "size": "auto",
+        "metrics": [
+          { "id": "request_p95", "label": "Request P95", "tip": "95th-percentile request duration from the WeChat network bridge.", "mqe": "meter_wechat_mp_request_duration_percentile{p='95'}", "unit": "ms", "aggregation": "avg" },
+          { "id": "launch", "label": "Launch", "tip": "Mini-program launch duration as observed by the WeChat client.", "mqe": "meter_wechat_mp_app_launch_duration", "unit": "ms", "aggregation": "avg" },
+          { "id": "errors", "label": "Errors", "tip": "Reported error count.", "mqe": "meter_wechat_mp_error_count", "aggregation": "sum" }
+        ]
+      },
+      {
+        "title": "Throughput",
+        "size": "square",
+        "metrics": [
+          { "id": "cpm", "label": "Request RPM", "tip": "Bridge requests per minute.", "mqe": "meter_wechat_mp_request_cpm", "unit": "rpm", "aggregation": "sum" }
+        ]
+      }
+    ]
+  },
+  "dashboards": {
+    "service": [
+      {
+        "id": "launch_duration",
+        "title": "App Launch Duration",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_wechat_mp_app_launch_duration"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "first_render",
+        "title": "First Render Duration",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_wechat_mp_first_render_duration"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "package_load",
+        "title": "Package Load Duration",
+        "tip": "Time to download + parse the mini-program package bundle.",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_wechat_mp_package_load_duration"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "error_count",
+        "title": "Error Count",
+        "type": "line",
+        "expressions": ["meter_wechat_mp_error_count"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "route_duration",
+        "title": "Route Duration",
+        "tip": "Time spent in mini-program page-route transitions.",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_wechat_mp_route_duration"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "script_duration",
+        "title": "Script Duration",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_wechat_mp_script_duration"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "request_cpm",
+        "title": "Request Load",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["meter_wechat_mp_request_cpm"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "request_percentile",
+        "title": "Request Duration Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(meter_wechat_mp_request_duration_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 12,
+        "rowSpan": 2
+      }
+    ],
+    "instance": [
+      {
+        "id": "v_launch",
+        "title": "Launch Duration",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_wechat_mp_instance_app_launch_duration"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "v_first_render",
+        "title": "First Render Duration",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_wechat_mp_instance_first_render_duration"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "v_package_load",
+        "title": "Package Load Duration",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_wechat_mp_instance_package_load_duration"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "v_request_cpm",
+        "title": "Request Load",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["meter_wechat_mp_instance_request_cpm"],
+        "span": 3,
+        "rowSpan": 2
+      },
+      {
+        "id": "v_route_duration",
+        "title": "Route Duration",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_wechat_mp_instance_route_duration"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "v_script_duration",
+        "title": "Script Duration",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_wechat_mp_instance_script_duration"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "v_request_percentile",
+        "title": "Request Duration Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(meter_wechat_mp_instance_request_duration_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 4,
+        "rowSpan": 2
+      }
+    ],
+    "endpoint": [
+      {
+        "id": "ep_launch",
+        "title": "Launch Duration on Page",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_wechat_mp_endpoint_app_launch_duration"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "ep_first_render",
+        "title": "First Render Duration",
+        "type": "line",
+        "unit": "ms",
+        "expressions": ["meter_wechat_mp_endpoint_first_render_duration"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "ep_request_cpm",
+        "title": "Request Load",
+        "type": "line",
+        "unit": "rpm",
+        "expressions": ["meter_wechat_mp_endpoint_request_cpm"],
+        "span": 4,
+        "rowSpan": 2
+      },
+      {
+        "id": "ep_request_percentile",
+        "title": "Request Duration Percentile",
+        "type": "line",
+        "unit": "ms",
+        "expressions": [
+          "relabels(meter_wechat_mp_endpoint_request_duration_percentile{p='50,75,90,95,99'},p='50,75,90,95,99',percentile='50,75,90,95,99')"
+        ],
+        "span": 12,
+        "rowSpan": 2
+      }
+    ]
+  }
+}
diff --git a/apps/bff/src/bundled_templates/overviews/general.json b/apps/bff/src/bundled_templates/overviews/general.json
new file mode 100644
index 0000000..fe17da1
--- /dev/null
+++ b/apps/bff/src/bundled_templates/overviews/general.json
@@ -0,0 +1,82 @@
+{
+  "id": "general",
+  "title": "General service",
+  "description": "Overview of services on the General layer with the Kubernetes service-count cross-referenced.",
+  "widgets": [
+    {
+      "id": "g_count",
+      "title": "Active services",
+      "tip": "Number of services reporting on this layer.",
+      "layer": "GENERAL",
+      "type": "service-count",
+      "span": 3,
+      "rowSpan": 1
+    },
+    {
+      "id": "g_rpm",
+      "title": "Throughput",
+      "tip": "Layer-wide requests per minute.",
+      "layer": "GENERAL",
+      "type": "metric",
+      "mqe": "service_cpm",
+      "unit": "rpm",
+      "aggregation": "sum",
+      "span": 3,
+      "rowSpan": 1
+    },
+    {
+      "id": "g_p99",
+      "title": "P99 latency",
+      "tip": "99th-percentile response time across the layer.",
+      "layer": "GENERAL",
+      "type": "metric",
+      "mqe": "service_percentile{p='99'}",
+      "unit": "ms",
+      "aggregation": "avg",
+      "span": 3,
+      "rowSpan": 1
+    },
+    {
+      "id": "g_err",
+      "title": "Error rate",
+      "tip": "100 − SLA. Higher = worse.",
+      "layer": "GENERAL",
+      "type": "metric",
+      "mqe": "100 - service_sla/100",
+      "unit": "%",
+      "aggregation": "avg",
+      "span": 3,
+      "rowSpan": 1
+    },
+    {
+      "id": "k8s_count",
+      "title": "K8s services",
+      "tip": "Services reporting through the K8s service layer.",
+      "layer": "K8S_SERVICE",
+      "type": "service-count",
+      "span": 3,
+      "rowSpan": 1
+    },
+    {
+      "id": "k8s_p99",
+      "title": "K8s P99",
+      "tip": "99th-percentile latency for K8s-routed traffic.",
+      "layer": "K8S_SERVICE",
+      "type": "metric",
+      "mqe": "k8s_service_resp_time_percentile{p='99'}",
+      "unit": "ms",
+      "aggregation": "avg",
+      "span": 3,
+      "rowSpan": 1
+    },
+    {
+      "id": "g_topo",
+      "title": "General service topology",
+      "tip": "Live service map for GENERAL — same renderer as the per-layer Topology tab.",
+      "layer": "GENERAL",
+      "type": "topology",
+      "span": 12,
+      "rowSpan": 4
+    }
+  ]
+}
diff --git a/apps/bff/src/bundled_templates/overviews/mesh.json b/apps/bff/src/bundled_templates/overviews/mesh.json
new file mode 100644
index 0000000..700f7f2
--- /dev/null
+++ b/apps/bff/src/bundled_templates/overviews/mesh.json
@@ -0,0 +1,82 @@
+{
+  "id": "mesh",
+  "title": "Mesh service",
+  "description": "Service-mesh overview — control-plane + data-plane signals with the K8s service-count cross-referenced.",
+  "widgets": [
+    {
+      "id": "m_count",
+      "title": "Active mesh services",
+      "tip": "Services routed through the mesh data-plane.",
+      "layer": "MESH",
+      "type": "service-count",
+      "span": 3,
+      "rowSpan": 1
+    },
+    {
+      "id": "m_rpm",
+      "title": "Mesh RPM",
+      "tip": "Mesh-wide requests per minute.",
+      "layer": "MESH",
+      "type": "metric",
+      "mqe": "service_mesh_cpm",
+      "unit": "rpm",
+      "aggregation": "sum",
+      "span": 3,
+      "rowSpan": 1
+    },
+    {
+      "id": "m_p99",
+      "title": "P99 latency",
+      "tip": "99th-percentile response time across mesh-routed traffic.",
+      "layer": "MESH",
+      "type": "metric",
+      "mqe": "service_mesh_percentile{p='99'}",
+      "unit": "ms",
+      "aggregation": "avg",
+      "span": 3,
+      "rowSpan": 1
+    },
+    {
+      "id": "m_err",
+      "title": "Error rate",
+      "tip": "100 − mesh SLA.",
+      "layer": "MESH",
+      "type": "metric",
+      "mqe": "100 - service_mesh_sla/100",
+      "unit": "%",
+      "aggregation": "avg",
+      "span": 3,
+      "rowSpan": 1
+    },
+    {
+      "id": "k8s_count",
+      "title": "K8s services",
+      "tip": "Services reporting through the K8s service layer.",
+      "layer": "K8S_SERVICE",
+      "type": "service-count",
+      "span": 3,
+      "rowSpan": 1
+    },
+    {
+      "id": "k8s_p99",
+      "title": "K8s P99",
+      "tip": "99th-percentile latency for K8s-routed traffic.",
+      "layer": "K8S_SERVICE",
+      "type": "metric",
+      "mqe": "k8s_service_resp_time_percentile{p='99'}",
+      "unit": "ms",
+      "aggregation": "avg",
+      "span": 3,
+      "rowSpan": 1
+    },
+    {
+      "id": "m_topo",
+      "title": "Mesh service topology",
+      "tip": "Live service map for the mesh data-plane.",
+      "layer": "MESH",
+      "type": "topology",
+      "span": 12,
+      "rowSpan": 4
+    }
+  ]
+}
diff --git a/apps/bff/src/config/schema.ts b/apps/bff/src/config/schema.ts
index 3103d69..370cacf 100644
--- a/apps/bff/src/config/schema.ts
+++ b/apps/bff/src/config/schema.ts
@@ -32,6 +32,17 @@ const oapSchema = z
     // The OAP query/status host (GraphQL + /status/*).
     statusUrl: z.string().url().default('http://127.0.0.1:12800'),
     timeoutMs: z.number().int().positive().default(15000),
+    // Optional basic-auth credentials sent on every outbound OAP call
+    // (GraphQL, /status, /api/v2/* for Zipkin). The public demo host
+    // (`demo.skywalking.apache.org`) gates calls behind
+    // skywalking:skywalking — set both fields to use it.
+    auth: z
+      .object({
+        username: z.string().min(1),
+        password: z.string().min(1),
+      })
+      .strict()
+      .optional(),
     mqe: z
       .object({
         host: z.string().optional(),
diff --git a/apps/bff/src/dashboard/defaults.ts b/apps/bff/src/dashboard/defaults.ts
index 01c7797..55ff193 100644
--- a/apps/bff/src/dashboard/defaults.ts
+++ b/apps/bff/src/dashboard/defaults.ts
@@ -180,7 +180,7 @@ const GENERIC_WIDGETS: DashboardWidget[] = [
 
 /**
  * Resolve the default widget set for `(layerKey)`. First tries the
- * JSON layer template (`src/layers/config/<key>.json`); falls back to
+ * JSON layer template (`src/bundled_templates/layers/<key>.json`); falls back to
  * the hardcoded TS sets above when no JSON exists for the layer. JSON
  * wins because that's where operators will eventually edit widgets
  * via the admin page.
diff --git a/apps/bff/src/dashboard/routes.ts b/apps/bff/src/dashboard/routes.ts
index 29a8941..a09636b 100644
--- a/apps/bff/src/dashboard/routes.ts
+++ b/apps/bff/src/dashboard/routes.ts
@@ -42,7 +42,7 @@ import type {
 import type { ConfigSource } from '../config/loader.js';
 import type { SessionStore } from '../auth/sessions.js';
 import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost } from '../oap/graphql-client.js';
+import {  graphqlPost, buildOapOpts } from '../oap/graphql-client.js';
 import {
   allLayerTemplates,
   getLayerTemplate,
@@ -335,11 +335,7 @@ export function registerDashboardRoute(app: FastifyInstance, deps: DashboardRout
       let serviceName = parsed.data.service ?? '';
       let normal = true;
       const cfgCurrent = deps.config.current;
-      const opts = {
-        statusUrl: cfgCurrent.oap.statusUrl,
-        timeoutMs: cfgCurrent.oap.timeoutMs,
-        fetch: deps.fetch,
-      };
+      const opts = buildOapOpts(cfgCurrent, deps.fetch);
       const window = defaultWindow();
 
       const baseResp: DashboardResponse = {
diff --git a/apps/bff/src/layers/config/k8s_service.json b/apps/bff/src/layers/config/k8s_service.json
deleted file mode 100644
index 0ea174d..0000000
--- a/apps/bff/src/layers/config/k8s_service.json
+++ /dev/null
@@ -1,109 +0,0 @@
-{
-  "key": "K8S_SERVICE",
-  "alias": "K8s Service",
-  "color": "var(--sw-purple)",
-  "aliases": {
-    "services": "K8s services",
-    "instances": "Pods"
-  },
-  "components": {
-    "service": true,
-    "instances": true,
-    "topology": true,
-    "traces": true,
-    "logs": true
-  },
-  "metrics": {
-    "orderBy": "cpm",
-    "columns": [
-      {
-        "metric": "cpm",
-        "label": "Traffic",
-        "unit": "rpm",
-        "mqe": "service_cpm",
-        "aggregation": "sum"
-      },
-      {
-        "metric": "p99",
-        "label": "p99",
-        "unit": "ms",
-        "mqe": "service_percentile{p='99'}",
-        "aggregation": "avg"
-      },
-      {
-        "metric": "sla",
-        "label": "SLA",
-        "unit": "%",
-        "mqe": "service_sla/100",
-        "aggregation": "avg"
-      },
-      {
-        "metric": "err",
-        "label": "err",
-        "unit": "%",
-        "mqe": "100 - service_sla/100",
-        "aggregation": "avg"
-      }
-    ]
-  },
-  "overview": {
-    "throughput": "cpm",
-    "spark": "cpm"
-  },
-  "widgets": [
-    {
-      "id": "traffic",
-      "title": "Traffic",
-      "type": "card",
-      "unit": "rpm",
-      "expressions": [
-        "avg(service_cpm)"
-      ],
-      "x": 0,
-      "y": 0,
-      "w": 12,
-      "h": 5
-    },
-    {
-      "id": "sla",
-      "title": "Success Rate",
-      "type": "card",
-      "unit": "%",
-      "expressions": [
-        "avg(service_sla)/100"
-      ],
-      "x": 12,
-      "y": 0,
-      "w": 12,
-      "h": 5
-    },
-    {
-      "id": "traffic_line",
-      "title": "Traffic",
-      "type": "line",
-      "unit": "rpm",
-      "expressions": [
-        "service_cpm"
-      ],
-      "x": 0,
-      "y": 5,
-      "w": 12,
-      "h": 12
-    },
-    {
-      "id": "percentile",
-      "title": "Response Time Percentile",
-      "type": "line",
-      "unit": "ms",
-      "expressions": [
-        "service_percentile{p='50'}",
-        "service_percentile{p='95'}",
-        "service_percentile{p='99'}"
-      ],
-      "x": 12,
-      "y": 5,
-      "w": 12,
-      "h": 12
-    }
-  ]
-}
diff --git a/apps/bff/src/layers/config/mesh.json b/apps/bff/src/layers/config/mesh.json
deleted file mode 100644
index dffd4ff..0000000
--- a/apps/bff/src/layers/config/mesh.json
+++ /dev/null
@@ -1,125 +0,0 @@
-{
-  "key": "MESH",
-  "alias": "Service Mesh",
-  "color": "var(--sw-info)",
-  "aliases": {
-    "services": "Services",
-    "instances": "Sidecars",
-    "endpoints": "Endpoints"
-  },
-  "components": {
-    "service": true,
-    "instances": true,
-    "endpoints": true,
-    "endpointDependency": true,
-    "topology": true,
-    "traces": true,
-    "logs": true
-  },
-  "metrics": {
-    "orderBy": "cpm",
-    "columns": [
-      {
-        "metric": "cpm",
-        "label": "Traffic",
-        "unit": "rpm",
-        "mqe": "service_cpm",
-        "aggregation": "sum"
-      },
-      {
-        "metric": "p99",
-        "label": "p99",
-        "unit": "ms",
-        "mqe": "service_percentile{p='99'}",
-        "aggregation": "avg"
-      },
-      {
-        "metric": "sla",
-        "label": "SLA",
-        "unit": "%",
-        "mqe": "service_sla/100",
-        "aggregation": "avg"
-      },
-      {
-        "metric": "err",
-        "label": "err",
-        "unit": "%",
-        "mqe": "100 - service_sla/100",
-        "aggregation": "avg"
-      }
-    ]
-  },
-  "overview": {
-    "throughput": "cpm",
-    "spark": "cpm"
-  },
-  "widgets": [
-    {
-      "id": "sla",
-      "title": "Success Rate",
-      "type": "card",
-      "unit": "%",
-      "expressions": [
-        "avg(service_sla)/100"
-      ],
-      "x": 0,
-      "y": 0,
-      "w": 8,
-      "h": 5
-    },
-    {
-      "id": "traffic",
-      "title": "Traffic",
-      "type": "card",
-      "unit": "rpm",
-      "expressions": [
-        "avg(service_cpm)"
-      ],
-      "x": 8,
-      "y": 0,
-      "w": 8,
-      "h": 5
-    },
-    {
-      "id": "p99",
-      "title": "p99 latency",
-      "type": "card",
-      "unit": "ms",
-      "expressions": [
-        "service_percentile{p='99'}"
-      ],
-      "x": 16,
-      "y": 0,
-      "w": 8,
-      "h": 5
-    },
-    {
-      "id": "traffic_line",
-      "title": "Traffic",
-      "type": "line",
-      "unit": "rpm",
-      "expressions": [
-        "service_cpm"
-      ],
-      "x": 0,
-      "y": 5,
-      "w": 12,
-      "h": 12
-    },
-    {
-      "id": "percentile",
-      "title": "Response Time Percentile",
-      "type": "line",
-      "unit": "ms",
-      "expressions": [
-        "service_percentile{p='50'}",
-        "service_percentile{p='95'}",
-        "service_percentile{p='99'}"
-      ],
-      "x": 12,
-      "y": 5,
-      "w": 12,
-      "h": 12
-    }
-  ]
-}
diff --git a/apps/bff/src/layers/config/virtual_cache.json b/apps/bff/src/layers/config/virtual_cache.json
deleted file mode 100644
index a6c3bde..0000000
--- a/apps/bff/src/layers/config/virtual_cache.json
+++ /dev/null
@@ -1,75 +0,0 @@
-{
-  "key": "VIRTUAL_CACHE",
-  "alias": "Virtual Cache",
-  "color": "var(--sw-warn)",
-  "aliases": {
-    "services": "Caches"
-  },
-  "components": {
-    "service": true
-  },
-  "metrics": {
-    "orderBy": "cpm",
-    "columns": [
-      {
-        "metric": "cpm",
-        "label": "Traffic",
-        "unit": "rpm",
-        "mqe": "service_cpm",
-        "aggregation": "sum"
-      },
-      {
-        "metric": "resp",
-        "label": "resp",
-        "unit": "ms",
-        "mqe": "service_resp_time",
-        "aggregation": "avg"
-      }
-    ]
-  },
-  "overview": {
-    "throughput": "cpm",
-    "spark": "cpm"
-  },
-  "widgets": [
-    {
-      "id": "traffic",
-      "title": "Traffic",
-      "type": "card",
-      "unit": "rpm",
-      "expressions": [
-        "avg(service_cpm)"
-      ],
-      "x": 0,
-      "y": 0,
-      "w": 12,
-      "h": 5
-    },
-    {
-      "id": "resp",
-      "title": "Avg Response Time",
-      "type": "card",
-      "unit": "ms",
-      "expressions": [
-        "avg(service_resp_time)"
-      ],
-      "x": 12,
-      "y": 0,
-      "w": 12,
-      "h": 5
-    },
-    {
-      "id": "traffic_line",
-      "title": "Traffic",
-      "type": "line",
-      "unit": "rpm",
-      "expressions": [
-        "service_cpm"
-      ],
-      "x": 0,
-      "y": 5,
-      "w": 24,
-      "h": 12
-    }
-  ]
-}
diff --git a/apps/bff/src/layers/config/virtual_database.json b/apps/bff/src/layers/config/virtual_database.json
deleted file mode 100644
index cf16f42..0000000
--- a/apps/bff/src/layers/config/virtual_database.json
+++ /dev/null
@@ -1,102 +0,0 @@
-{
-  "key": "VIRTUAL_DATABASE",
-  "alias": "Virtual Database",
-  "color": "var(--sw-warn)",
-  "aliases": {
-    "services": "Databases"
-  },
-  "components": {
-    "service": true
-  },
-  "metrics": {
-    "orderBy": "cpm",
-    "columns": [
-      {
-        "metric": "cpm",
-        "label": "Traffic",
-        "unit": "rpm",
-        "mqe": "service_cpm",
-        "aggregation": "sum"
-      },
-      {
-        "metric": "resp",
-        "label": "resp",
-        "unit": "ms",
-        "mqe": "service_resp_time",
-        "aggregation": "avg"
-      },
-      {
-        "metric": "p99",
-        "label": "p99",
-        "unit": "ms",
-        "mqe": "service_percentile{p='99'}",
-        "aggregation": "avg"
-      },
-      {
-        "metric": "err",
-        "label": "err",
-        "unit": "%",
-        "mqe": "100 - service_sla/100",
-        "aggregation": "avg"
-      }
-    ]
-  },
-  "overview": {
-    "throughput": "cpm",
-    "spark": "cpm"
-  },
-  "widgets": [
-    {
-      "id": "traffic",
-      "title": "Traffic",
-      "type": "card",
-      "unit": "rpm",
-      "expressions": [
-        "avg(service_cpm)"
-      ],
-      "x": 0,
-      "y": 0,
-      "w": 8,
-      "h": 5
-    },
-    {
-      "id": "resp",
-      "title": "Avg Response Time",
-      "type": "card",
-      "unit": "ms",
-      "expressions": [
-        "avg(service_resp_time)"
-      ],
-      "x": 8,
-      "y": 0,
-      "w": 8,
-      "h": 5
-    },
-    {
-      "id": "sla",
-      "title": "Success Rate",
-      "type": "card",
-      "unit": "%",
-      "expressions": [
-        "avg(service_sla)/100"
-      ],
-      "x": 16,
-      "y": 0,
-      "w": 8,
-      "h": 5
-    },
-    {
-      "id": "traffic_line",
-      "title": "Traffic",
-      "type": "line",
-      "unit": "rpm",
-      "expressions": [
-        "service_cpm"
-      ],
-      "x": 0,
-      "y": 5,
-      "w": 24,
-      "h": 12
-    }
-  ]
-}
diff --git a/apps/bff/src/layers/config/virtual_mq.json b/apps/bff/src/layers/config/virtual_mq.json
deleted file mode 100644
index 21bef7c..0000000
--- a/apps/bff/src/layers/config/virtual_mq.json
+++ /dev/null
@@ -1,74 +0,0 @@
-{
-  "key": "VIRTUAL_MQ",
-  "alias": "Virtual MQ",
-  "color": "var(--sw-ok)",
-  "aliases": {
-    "services": "Queues"
-  },
-  "components": {
-    "service": true
-  },
-  "metrics": {
-    "orderBy": "cpm",
-    "columns": [
-      {
-        "metric": "cpm",
-        "label": "msg/s",
-        "mqe": "service_cpm",
-        "aggregation": "sum"
-      },
-      {
-        "metric": "resp",
-        "label": "consume",
-        "unit": "ms",
-        "mqe": "service_resp_time",
-        "aggregation": "avg"
-      }
-    ]
-  },
-  "overview": {
-    "throughput": "cpm",
-    "spark": "cpm"
-  },
-  "widgets": [
-    {
-      "id": "traffic",
-      "title": "Message rate",
-      "type": "card",
-      "unit": "msg / min",
-      "expressions": [
-        "avg(service_cpm)"
-      ],
-      "x": 0,
-      "y": 0,
-      "w": 12,
-      "h": 5
-    },
-    {
-      "id": "resp",
-      "title": "Avg Consume Latency",
-      "type": "card",
-      "unit": "ms",
-      "expressions": [
-        "avg(service_resp_time)"
-      ],
-      "x": 12,
-      "y": 0,
-      "w": 12,
-      "h": 5
-    },
-    {
-      "id": "traffic_line",
-      "title": "Message rate",
-      "type": "line",
-      "unit": "msg / min",
-      "expressions": [
-        "service_cpm"
-      ],
-      "x": 0,
-      "y": 5,
-      "w": 24,
-      "h": 12
-    }
-  ]
-}
diff --git a/apps/bff/src/layers/loader.ts b/apps/bff/src/layers/loader.ts
index 74268ba..0f53a37 100644
--- a/apps/bff/src/layers/loader.ts
+++ b/apps/bff/src/layers/loader.ts
@@ -20,9 +20,9 @@
  * presentation (alias, color, slots, enabled components) AND its data
  * shape (landing card columns, dashboard widgets, MQE expressions).
  *
- * The bundled defaults live under `./config/<key>.json`, one file per
- * OAP layer enum. Operator overrides land in the SetupStore (JSON file
- * on disk) and merge on top.
+ * The bundled defaults live under `../bundled_templates/layers/<key>.json`,
+ * one file per OAP layer enum. Operator overrides land in the SetupStore
+ * (JSON file on disk) and merge on top.
  *
  * Lifting these from TS code into JSON gets us:
  *   - One file per layer to review or copy
@@ -41,9 +41,10 @@ import type {
   EndpointDependencyConfig,
   TopologyConfig,
   TopologyMetricDef,
+  TracesConfig,
 } from '@skywalking-horizon-ui/api-client';
 
-export type { TopologyConfig, EndpointDependencyConfig, TopologyMetricDef };
+export type { TopologyConfig, EndpointDependencyConfig, TopologyMetricDef, TracesConfig };
 
 export interface LayerComponentFlags {
   service?: boolean;
@@ -190,6 +191,12 @@ export interface LayerTemplate {
   /** API-dependency dashboard config — operator-editable node + edge MQE.
    *  When absent the loader fills it from {@link BOOSTER_ENDPOINT_DEP_DEFAULTS}. */
   endpointDependency?: EndpointDependencyConfig;
+  /** Traces tab config. The `source` field picks which trace backend
+   *  the UI's filter selector defaults to (`both` shows two parallel
+   *  tables; `native` / `zipkin` pin to one). Default `both` when
+   *  absent. The v2-vs-v3 split for native traces is decided at
+   *  runtime by probing `hasQueryTracesV2Support`, not in this config. */
+  traces?: TracesConfig;
 }
 
 /**
@@ -255,7 +262,11 @@ export const BOOSTER_ENDPOINT_DEP_DEFAULTS: EndpointDependencyConfig = {
 };
 
 const __dirname = dirname(fileURLToPath(import.meta.url));
-const CONFIG_DIR = join(__dirname, 'config');
+// Bundled layer JSONs live alongside other static templates under
+// `apps/bff/src/bundled_templates/`. In the long run these should be
+// served by OAP; for now they ship inside the BFF so a fresh deploy
+// renders something sensible without any operator setup.
+const CONFIG_DIR = join(__dirname, '..', 'bundled_templates', 'layers');
 
 let cache: Map<string, LayerTemplate> | null = null;
 
@@ -456,6 +467,13 @@ export function endpointDependencyConfigFor(
   return BOOSTER_ENDPOINT_DEP_DEFAULTS;
 }
 
+/** Resolve the traces tab config. Defaults to surfacing both
+ *  SkyWalking-native and Zipkin trace lists side-by-side. */
+export function tracesConfigFor(template: LayerTemplate | null): TracesConfig {
+  if (template?.traces) return template.traces;
+  return { source: 'both' };
+}
+
 /**
  * Persist an operator-edited template back to its JSON file. Validates
  * the basic shape, sorts keys for stable diffs, then refreshes the
diff --git a/apps/bff/src/oap/clients.ts b/apps/bff/src/oap/clients.ts
index 8838928..12af234 100644
--- a/apps/bff/src/oap/clients.ts
+++ b/apps/bff/src/oap/clients.ts
@@ -72,31 +72,41 @@ export function buildOapClients(
 ): OapClients {
   const fetch = opts.fetch;
   const primaryUrl = config.oap.adminUrls[0]!;
-  // Single source of truth for per-call timeout — every client
-  // constructed via this factory inherits it. 0 means no timeout
-  // (passed through verbatim to the clients).
   const timeoutMs = config.oap.timeoutMs;
+  // If config.oap.auth is set, every constructed client gets the
+  // basic-auth Authorization header so all upstream calls (status,
+  // runtime-rule, OAL catalog, DSL debug, inspect) authenticate.
+  const headers: Record<string, string> | undefined = config.oap.auth
+    ? {
+        authorization:
+          'Basic ' +
+          Buffer.from(
+            `${config.oap.auth.username}:${config.oap.auth.password}`,
+            'utf8',
+          ).toString('base64'),
+      }
+    : undefined;
   return {
     forUrl(adminUrl: string): RuntimeRuleClient {
-      return new RuntimeRuleClient({ adminUrl, fetch, timeoutMs });
+      return new RuntimeRuleClient({ adminUrl, fetch, timeoutMs, headers });
     },
     primary(): RuntimeRuleClient {
-      return new RuntimeRuleClient({ adminUrl: primaryUrl, fetch, timeoutMs });
+      return new RuntimeRuleClient({ adminUrl: primaryUrl, fetch, timeoutMs, headers });
     },
     status(): StatusClient {
-      return new StatusClient({ statusUrl: config.oap.statusUrl, fetch, timeoutMs });
+      return new StatusClient({ statusUrl: config.oap.statusUrl, fetch, timeoutMs, headers });
     },
     oal(): OalClient {
-      return new OalClient({ adminUrl: primaryUrl, fetch, timeoutMs });
+      return new OalClient({ adminUrl: primaryUrl, fetch, timeoutMs, headers });
     },
     debug(): DslDebuggingClient {
-      return new DslDebuggingClient({ adminUrl: primaryUrl, fetch, timeoutMs });
+      return new DslDebuggingClient({ adminUrl: primaryUrl, fetch, timeoutMs, headers });
     },
     debugForUrl(adminUrl: string): DslDebuggingClient {
-      return new DslDebuggingClient({ adminUrl, fetch, timeoutMs });
+      return new DslDebuggingClient({ adminUrl, fetch, timeoutMs, headers });
     },
     inspect(): InspectClient {
-      return new InspectClient({ adminUrl: primaryUrl, fetch, timeoutMs });
+      return new InspectClient({ adminUrl: primaryUrl, fetch, timeoutMs, headers });
     },
     adminUrls(): readonly string[] {
       return config.oap.adminUrls;
diff --git a/apps/bff/src/oap/endpoint-dependency-routes.ts b/apps/bff/src/oap/endpoint-dependency-routes.ts
index 24b3183..ec7d186 100644
--- a/apps/bff/src/oap/endpoint-dependency-routes.ts
+++ b/apps/bff/src/oap/endpoint-dependency-routes.ts
@@ -40,7 +40,7 @@ import type {
   TopologyMetricDef,
 } from '@skywalking-horizon-ui/api-client';
 import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost } from './graphql-client.js';
+import {  graphqlPost, buildOapOpts } from './graphql-client.js';
 import { endpointDependencyConfigFor, getLayerTemplate } from '../layers/loader.js';
 
 export interface EndpointDependencyRouteDeps {
@@ -265,11 +265,7 @@ export function registerEndpointDependencyRoute(
       const epCfg: EndpointDependencyConfig = endpointDependencyConfigFor(template);
 
       const cfgCurrent = deps.config.current;
-      const opts = {
-        statusUrl: cfgCurrent.oap.statusUrl,
-        timeoutMs: cfgCurrent.oap.timeoutMs,
-        fetch: deps.fetch,
-      };
+      const opts = buildOapOpts(cfgCurrent, deps.fetch);
       const window = defaultWindow();
       const oapLayer = layerKey.toUpperCase();
       const durationVar = { start: window.start, end: window.end, step: 'MINUTE' };
@@ -399,22 +395,38 @@ export function registerEndpointDependencyRoute(
       }
 
       // ── Per-edge MQE under EndpointRelation. We also capture the
-      // per-bucket series so the UI's edge sidebar can draw sparklines
-      // instead of just printing scalars.
+      // per-bucket series so the UI's edge sidebar can draw sparklines.
+      //
+      // Endpoint relation metrics are SERVER-SIDE only (OAP has no
+      // `endpoint_relation_client_*` family) — they live on the
+      // callee that records the incoming call. So an edge is
+      // queryable when the DEST endpoint is real + named, regardless
+      // of whether the source is real (e.g. `User → consumer` edges
+      // still produce server-side numbers). When the source is
+      // virtual we use a synthetic source service name and
+      // `sourceNormal: false`, which is what booster does too.
       const linkMetrics = epCfg.linkMetrics ?? [];
       const edgeMetricVals = new Map<string, Record<string, number | null>>();
       const edgeMetricSeries = new Map<string, Record<string, Array<number | null> | null>>();
       const realEndpointMap = new Map(realNodes.map((n) => [n.id, n]));
-      const realEdges = graph.calls.filter(
-        (c) => realEndpointMap.has(c.source) && realEndpointMap.has(c.target),
-      );
-      if (realEdges.length > 0 && linkMetrics.length > 0) {
+      const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
+      const candidateEdges = graph.calls.filter((c) => {
+        const dst = nodeById.get(c.target);
+        return !!dst && dst.isReal && !!dst.name && !!dst.serviceName;
+      });
+      if (candidateEdges.length > 0 && linkMetrics.length > 0) {
         const fragments: string[] = [];
         const aliasMap = new Map<string, { callId: string; metric: TopologyMetricDef }>();
-        realEdges.forEach((c, i) => {
-          const src = realEndpointMap.get(c.source)!;
-          const dst = realEndpointMap.get(c.target)!;
-          const srcNormal = src.serviceId === serviceId ? normal : true;
+        candidateEdges.forEach((c, i) => {
+          const dst = realEndpointMap.get(c.target) ?? nodeById.get(c.target)!;
+          const src = nodeById.get(c.source);
+          // Source may be a synthetic node (`User`, `localhost:-1`).
+          // Booster's pattern: pass the source's name + serviceName
+          // through, and set `sourceNormal = isReal`. OAP accepts
+          // virtual sources for endpoint-relation queries.
+          const srcName = src?.name || 'User';
+          const srcServiceName = src?.serviceName || 'User';
+          const srcNormal = src ? src.isReal : false;
           const dstNormal = dst.serviceId === serviceId ? normal : true;
           linkMetrics.forEach((m, j) => {
             const alias = `r${i}_${j}`;
@@ -423,8 +435,8 @@ export function registerEndpointDependencyRoute(
               endpointRelationFragment(
                 alias,
                 m,
-                src.serviceName,
-                src.name,
+                srcServiceName,
+                srcName,
                 srcNormal,
                 dst.serviceName,
                 dst.name,
diff --git a/apps/bff/src/oap/endpoint-routes.ts b/apps/bff/src/oap/endpoint-routes.ts
index e40d7fc..6d6213e 100644
--- a/apps/bff/src/oap/endpoint-routes.ts
+++ b/apps/bff/src/oap/endpoint-routes.ts
@@ -38,7 +38,7 @@ import type { ConfigSource } from '../config/loader.js';
 import type { SessionStore } from '../auth/sessions.js';
 import type { FetchLike } from '@skywalking-horizon-ui/api-client';
 import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost } from './graphql-client.js';
+import {  graphqlPost, buildOapOpts } from './graphql-client.js';
 
 export interface EndpointRouteDeps {
   config: ConfigSource;
@@ -115,11 +115,7 @@ export function registerEndpointRoute(app: FastifyInstance, deps: EndpointRouteD
       const limit = Math.max(20, Math.min(50, Number(q.limit) || 20));
 
       const cfgCurrent = deps.config.current;
-      const opts = {
-        statusUrl: cfgCurrent.oap.statusUrl,
-        timeoutMs: cfgCurrent.oap.timeoutMs,
-        fetch: deps.fetch,
-      };
+      const opts = buildOapOpts(cfgCurrent, deps.fetch);
       const window = defaultWindow();
 
       // Resolve a plain service name to an OAP id when needed (id-shaped
diff --git a/apps/bff/src/oap/graphql-client.ts b/apps/bff/src/oap/graphql-client.ts
index 5b0833a..e90cbb0 100644
--- a/apps/bff/src/oap/graphql-client.ts
+++ b/apps/bff/src/oap/graphql-client.ts
@@ -16,11 +16,43 @@
  */
 
 import type { FetchLike } from '@skywalking-horizon-ui/api-client';
+import type { HorizonConfig } from '../config/schema.js';
 
 export interface GraphqlOptions {
   statusUrl: string;
   timeoutMs: number;
   fetch?: FetchLike;
+  /** Optional basic-auth credentials for outbound OAP calls. When
+   *  set, the client adds an `Authorization: Basic <b64>` header. */
+  auth?: { username: string; password: string };
+}
+
+/**
+ * Build a {@link GraphqlOptions}-shaped object from the live config
+ * plus an injected fetch. Use this in every route to keep `auth`,
+ * `timeoutMs`, etc. flowing consistently from `horizon.yaml`.
+ */
+export function buildOapOpts(
+  cfg: HorizonConfig,
+  fetch?: FetchLike,
+): GraphqlOptions {
+  return {
+    statusUrl: cfg.oap.statusUrl,
+    timeoutMs: cfg.oap.timeoutMs,
+    auth: cfg.oap.auth,
+    fetch,
+  };
+}
+
+/** Build a basic-auth header value from a username/password pair. */
+export function basicAuthHeader(username: string, password: string): string {
+  // btoa handles ASCII; encode utf-8 first for safety.
+  const raw = `${username}:${password}`;
+  const b64 = typeof Buffer !== 'undefined'
+    ? Buffer.from(raw, 'utf8').toString('base64')
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    : (globalThis as any).btoa(unescape(encodeURIComponent(raw)));
+  return `Basic ${b64}`;
 }
 
 export class GraphqlError extends Error {
@@ -51,11 +83,15 @@ export async function graphqlPost<T>(
   const url = opts.statusUrl.replace(/\/$/, '') + '/graphql';
   const controller = new AbortController();
   const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
+  const headers: Record<string, string> = { 'content-type': 'application/json' };
+  if (opts.auth) {
+    headers.authorization = basicAuthHeader(opts.auth.username, opts.auth.password);
+  }
   let res: Response;
   try {
     res = await f(url, {
       method: 'POST',
-      headers: { 'content-type': 'application/json' },
+      headers,
       body: JSON.stringify({ query, variables: variables ?? {} }),
       signal: controller.signal,
     });
diff --git a/apps/bff/src/oap/info-routes.ts b/apps/bff/src/oap/info-routes.ts
index cca0c6e..39941eb 100644
--- a/apps/bff/src/oap/info-routes.ts
+++ b/apps/bff/src/oap/info-routes.ts
@@ -20,7 +20,7 @@ import type { FetchLike } from '@skywalking-horizon-ui/api-client';
 import type { ConfigSource } from '../config/loader.js';
 import type { SessionStore } from '../auth/sessions.js';
 import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost } from './graphql-client.js';
+import { buildOapOpts, graphqlPost } from './graphql-client.js';
 
 /**
  * One round-trip combining `version`, `getTimeInfo`, and `checkHealth`.
@@ -66,10 +66,7 @@ export function registerOapInfoRoute(app: FastifyInstance, deps: InfoRouteDeps):
     const cfg = deps.config.current;
     const statusUrl = cfg.oap.statusUrl;
     try {
-      const raw = await graphqlPost<InfoRaw>(
-        { statusUrl, timeoutMs: cfg.oap.timeoutMs, fetch: deps.fetch },
-        INFO_QUERY,
-      );
+      const raw = await graphqlPost<InfoRaw>(buildOapOpts(cfg, deps.fetch), INFO_QUERY);
       const body: OapInfo = {
         reachable: true,
         statusUrl,
diff --git a/apps/bff/src/oap/instance-routes.ts b/apps/bff/src/oap/instance-routes.ts
index 2464d71..8b62d71 100644
--- a/apps/bff/src/oap/instance-routes.ts
+++ b/apps/bff/src/oap/instance-routes.ts
@@ -36,7 +36,7 @@ import type { ConfigSource } from '../config/loader.js';
 import type { SessionStore } from '../auth/sessions.js';
 import type { FetchLike } from '@skywalking-horizon-ui/api-client';
 import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost } from './graphql-client.js';
+import {  graphqlPost, buildOapOpts } from './graphql-client.js';
 
 export interface InstanceRouteDeps {
   config: ConfigSource;
@@ -128,11 +128,7 @@ export function registerInstanceRoute(app: FastifyInstance, deps: InstanceRouteD
         return reply.code(400).send({ error: 'missing_service' });
       }
       const cfgCurrent = deps.config.current;
-      const opts = {
-        statusUrl: cfgCurrent.oap.statusUrl,
-        timeoutMs: cfgCurrent.oap.timeoutMs,
-        fetch: deps.fetch,
-      };
+      const opts = buildOapOpts(cfgCurrent, deps.fetch);
       const window = defaultWindow();
       // OAP-side ids look like base64-ish blobs (e.g. "Y2hlY2tvdXQ=.1");
       // names are plain words. If we don't see a `.` separator, treat
diff --git a/apps/bff/src/oap/landing-routes.ts b/apps/bff/src/oap/landing-routes.ts
index d8cad1b..1badbad 100644
--- a/apps/bff/src/oap/landing-routes.ts
+++ b/apps/bff/src/oap/landing-routes.ts
@@ -48,7 +48,7 @@ import type {
 import type { ConfigSource } from '../config/loader.js';
 import type { SessionStore } from '../auth/sessions.js';
 import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost } from './graphql-client.js';
+import {  graphqlPost, buildOapOpts } from './graphql-client.js';
 import { expressionForServiceMetricSeries } from './mqe-catalog.js';
 
 export interface LandingRouteDeps {
@@ -270,11 +270,7 @@ export function registerLandingRoute(app: FastifyInstance, deps: LandingRouteDep
       const cfg = parsed.data;
       const oapLayer = layerKey.toUpperCase();
       const cfgCurrent = deps.config.current;
-      const opts = {
-        statusUrl: cfgCurrent.oap.statusUrl,
-        timeoutMs: cfgCurrent.oap.timeoutMs,
-        fetch: deps.fetch,
-      };
+      const opts = buildOapOpts(cfgCurrent, deps.fetch);
       const window = defaultWindow();
 
       // Step 1 — service list.
diff --git a/apps/bff/src/oap/log-routes.ts b/apps/bff/src/oap/log-routes.ts
new file mode 100644
index 0000000..6fd5578
--- /dev/null
+++ b/apps/bff/src/oap/log-routes.ts
@@ -0,0 +1,313 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * `POST /api/layer/:key/logs`
+ *
+ * Wraps OAP's `queryLogs(LogQueryCondition)`. Body shape is the
+ * `LogQueryRequest` from `@skywalking-horizon-ui/api-client/logs`.
+ *
+ * Tag filters + content keyword filters are AND-joined server-side.
+ * We accept a `service` name on the body so the SPA doesn't have to
+ * pre-resolve names → ids; mirror of the topology + endpoint feeds.
+ *
+ * Returns at most one page of logs plus the OAP-reported total so
+ * the UI's "page N of M" + density histogram can scope correctly.
+ */
+
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
+import type {
+  FetchLike,
+  LogFacetsResponse,
+  LogKeyValue,
+  LogQueryRequest,
+  LogRow,
+  LogsResponse,
+} from '@skywalking-horizon-ui/api-client';
+import type { ConfigSource } from '../config/loader.js';
+import type { SessionStore } from '../auth/sessions.js';
+import { requireAuth } from '../auth/middleware.js';
+import {  graphqlPost, buildOapOpts, type GraphqlOptions } from './graphql-client.js';
+
+export interface LogRouteDeps {
+  config: ConfigSource;
+  sessions: SessionStore;
+  fetch?: FetchLike;
+}
+
+const DEFAULT_WINDOW_MIN = 30;
+function fmtMinute(d: Date): string {
+  const yyyy = d.getUTCFullYear();
+  const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
+  const dd = String(d.getUTCDate()).padStart(2, '0');
+  const HH = String(d.getUTCHours()).padStart(2, '0');
+  const MM = String(d.getUTCMinutes()).padStart(2, '0');
+  return `${yyyy}-${mm}-${dd} ${HH}${MM}`;
+}
+function defaultWindow(): { start: string; end: string } {
+  const end = new Date();
+  const start = new Date(end.getTime() - DEFAULT_WINDOW_MIN * 60_000);
+  return { start: fmtMinute(start), end: fmtMinute(end) };
+}
+
+const LIST_SERVICES_FOR_RESOLVE = /* GraphQL */ `
+  query ListServicesForLogs($layer: String!) {
+    services: listServices(layer: $layer) {
+      id
+      name
+    }
+  }
+`;
+
+const QUERY_LOGS = /* GraphQL */ `
+  query QueryLogs($condition: LogQueryCondition) {
+    data: queryLogs(condition: $condition) {
+      logs {
+        serviceName
+        serviceId
+        serviceInstanceName
+        serviceInstanceId
+        endpointName
+        endpointId
+        traceId
+        timestamp
+        contentType
+        content
+        tags { key value }
+      }
+      total
+    }
+  }
+`;
+
+interface OapLogRow {
+  serviceName?: string | null;
+  serviceId?: string | null;
+  serviceInstanceName?: string | null;
+  serviceInstanceId?: string | null;
+  endpointName?: string | null;
+  endpointId?: string | null;
+  traceId?: string | null;
+  timestamp: number;
+  contentType: string;
+  content: string;
+  tags?: LogKeyValue[] | null;
+}
+
+async function resolveServiceId(
+  opts: GraphqlOptions,
+  layer: string,
+  serviceArg: string,
+): Promise<string | null> {
+  if (!serviceArg) return null;
+  if (serviceArg.includes('.') && !/\s/.test(serviceArg)) return serviceArg;
+  const data = await graphqlPost<{ services: Array<{ id: string; name: string }> }>(
+    opts,
+    LIST_SERVICES_FOR_RESOLVE,
+    { layer: layer.toUpperCase() },
+  );
+  return (
+    data.services.find((s) => s.name === serviceArg)?.id ??
+    data.services.find((s) => s.id === serviceArg)?.id ??
+    null
+  );
+}
+
+interface LogBody extends LogQueryRequest {
+  service?: string;
+}
+
+export function registerLogRoute(app: FastifyInstance, deps: LogRouteDeps): void {
+  const auth = requireAuth(deps);
+  app.post(
+    '/api/layer/:key/logs',
+    { preHandler: auth },
+    async (req: FastifyRequest, reply: FastifyReply) => {
+      const params = req.params as { key: string };
+      const layerKey = params.key;
+      if (!layerKey || !/^[a-z0-9_]+$/i.test(layerKey)) {
+        return reply.code(400).send({ error: 'invalid_layer_key' });
+      }
+      const body = (req.body ?? {}) as LogBody;
+      const opts = buildOapOpts(deps.config.current, deps.fetch);
+      const window = defaultWindow();
+
+      // Resolve a service NAME to an id if the caller used one.
+      let serviceId = body.serviceId ?? null;
+      if (!serviceId && body.service) {
+        try {
+          serviceId = await resolveServiceId(opts, layerKey, body.service);
+        } catch (err) {
+          return reply.send({
+            generatedAt: Date.now(),
+            query: body,
+            total: 0,
+            logs: [],
+            reachable: false,
+            error: err instanceof Error ? err.message : String(err),
+          } satisfies LogsResponse);
+        }
+      }
+      const condition = {
+        ...(serviceId ? { serviceId } : {}),
+        ...(body.serviceInstanceId ? { serviceInstanceId: body.serviceInstanceId } : {}),
+        ...(body.endpointId ? { endpointId: body.endpointId } : {}),
+        ...(body.traceId ? { relatedTrace: { traceId: body.traceId } } : {}),
+        ...(body.keywordsOfContent && body.keywordsOfContent.length > 0
+          ? { keywordsOfContent: body.keywordsOfContent }
+          : {}),
+        ...(body.tags && body.tags.length > 0 ? { tags: body.tags } : {}),
+        queryDuration: { start: window.start, end: window.end, step: 'MINUTE' },
+        paging: {
+          pageNum: body.page ?? 1,
+          pageSize: body.pageSize ?? 50,
+        },
+      };
+
+      try {
+        const env = await graphqlPost<{
+          data: { logs: OapLogRow[]; total: number };
+        }>(opts, QUERY_LOGS, { condition });
+        const logs: LogRow[] = (env.data?.logs ?? []).map((r) => ({
+          serviceName: r.serviceName ?? null,
+          serviceId: r.serviceId ?? null,
+          serviceInstanceName: r.serviceInstanceName ?? null,
+          serviceInstanceId: r.serviceInstanceId ?? null,
+          endpointName: r.endpointName ?? null,
+          endpointId: r.endpointId ?? null,
+          traceId: r.traceId ?? null,
+          timestamp: r.timestamp,
+          contentType: r.contentType,
+          content: r.content,
+          tags: r.tags ?? [],
+        }));
+        return reply.send({
+          generatedAt: Date.now(),
+          query: body,
+          total: env.data?.total ?? logs.length,
+          logs,
+          reachable: true,
+        } satisfies LogsResponse);
+      } catch (err) {
+        return reply.send({
+          generatedAt: Date.now(),
+          query: body,
+          total: 0,
+          logs: [],
+          reachable: false,
+          error: err instanceof Error ? err.message : String(err),
+        } satisfies LogsResponse);
+      }
+    },
+  );
+
+  /**
+   * POST /api/layer/:key/logs/facets
+   *
+   * Fetches a larger window-scoped sample (default 200 rows) just for
+   * facet aggregation. The UI calls this in parallel with the page
+   * fetch so the left-rail counts reflect the query window, not the
+   * displayed page.
+   */
+  app.post(
+    '/api/layer/:key/logs/facets',
+    { preHandler: auth },
+    async (req: FastifyRequest, reply: FastifyReply) => {
+      const params = req.params as { key: string };
+      const layerKey = params.key;
+      if (!layerKey || !/^[a-z0-9_]+$/i.test(layerKey)) {
+        return reply.code(400).send({ error: 'invalid_layer_key' });
+      }
+      const body = (req.body ?? {}) as LogBody & { sampleSize?: number };
+      const sampleSize = Math.max(50, Math.min(1000, body.sampleSize ?? 200));
+      const opts = buildOapOpts(deps.config.current, deps.fetch);
+      const window = defaultWindow();
+      let serviceId = body.serviceId ?? null;
+      if (!serviceId && body.service) {
+        try {
+          serviceId = await resolveServiceId(opts, layerKey, body.service);
+        } catch (err) {
+          return reply.send({
+            generatedAt: Date.now(),
+            total: 0,
+            sampled: 0,
+            level: { error: 0, warn: 0, info: 0, debug: 0, other: 0 },
+            services: [],
+            reachable: false,
+            error: err instanceof Error ? err.message : String(err),
+          } satisfies LogFacetsResponse);
+        }
+      }
+      const condition = {
+        ...(serviceId ? { serviceId } : {}),
+        ...(body.serviceInstanceId ? { serviceInstanceId: body.serviceInstanceId } : {}),
+        ...(body.endpointId ? { endpointId: body.endpointId } : {}),
+        ...(body.traceId ? { relatedTrace: { traceId: body.traceId } } : {}),
+        ...(body.keywordsOfContent && body.keywordsOfContent.length > 0
+          ? { keywordsOfContent: body.keywordsOfContent }
+          : {}),
+        // Facet sample intentionally ignores level/tag filters so the
+        // counts show the unfiltered distribution; the user picks a
+        // level from the breakdown.
+        queryDuration: { start: window.start, end: window.end, step: 'MINUTE' },
+        paging: { pageNum: 1, pageSize: sampleSize },
+      };
+
+      try {
+        const env = await graphqlPost<{
+          data: { logs: OapLogRow[]; total: number };
+        }>(opts, QUERY_LOGS, { condition });
+        const rows = env.data?.logs ?? [];
+        const level: LogFacetsResponse['level'] = { error: 0, warn: 0, info: 0, debug: 0, other: 0 };
+        const svcMap = new Map<string, number>();
+        for (const r of rows) {
+          const tag = (r.tags ?? []).find((t) => t.key.toLowerCase() === 'level');
+          const raw = (tag?.value ?? '').toLowerCase();
+          if (raw.includes('error') || raw === 'err' || raw === 'fatal') level.error++;
+          else if (raw.includes('warn')) level.warn++;
+          else if (raw.includes('info')) level.info++;
+          else if (raw.includes('debug') || raw.includes('trace')) level.debug++;
+          else level.other++;
+          const svc = r.serviceName ?? '(none)';
+          svcMap.set(svc, (svcMap.get(svc) ?? 0) + 1);
+        }
+        const services = Array.from(svcMap.entries())
+          .map(([name, count]) => ({ name, count }))
+          .sort((a, b) => b.count - a.count)
+          .slice(0, 12);
+        return reply.send({
+          generatedAt: Date.now(),
+          total: env.data?.total ?? rows.length,
+          sampled: rows.length,
+          level,
+          services,
+          reachable: true,
+        } satisfies LogFacetsResponse);
+      } catch (err) {
+        return reply.send({
+          generatedAt: Date.now(),
+          total: 0,
+          sampled: 0,
+          level: { error: 0, warn: 0, info: 0, debug: 0, other: 0 },
+          services: [],
+          reachable: false,
+          error: err instanceof Error ? err.message : String(err),
+        } satisfies LogFacetsResponse);
+      }
+    },
+  );
+}
diff --git a/apps/bff/src/oap/menu-routes.ts b/apps/bff/src/oap/menu-routes.ts
index b0432bf..29fef8a 100644
--- a/apps/bff/src/oap/menu-routes.ts
+++ b/apps/bff/src/oap/menu-routes.ts
@@ -26,7 +26,7 @@ import type {
 import type { ConfigSource } from '../config/loader.js';
 import type { SessionStore } from '../auth/sessions.js';
 import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost } from './graphql-client.js';
+import { buildOapOpts, graphqlPost, type GraphqlOptions } from './graphql-client.js';
 import { getLayerTemplate, type LayerComponentFlags } from '../layers/loader.js';
 
 /**
@@ -212,7 +212,7 @@ function deriveLayer(
  */
 async function fetchCountsForLayers(
   layers: readonly string[],
-  opts: { statusUrl: string; timeoutMs: number; fetch?: FetchLike },
+  opts: GraphqlOptions,
 ): Promise<Map<string, number>> {
   const map = new Map<string, number>();
   if (layers.length === 0) return map;
@@ -238,7 +238,7 @@ export function registerMenuRoute(app: FastifyInstance, deps: MenuRouteDeps): vo
   app.get('/api/menu', { preHandler: auth }, async (_req: FastifyRequest, reply: FastifyReply) => {
     const cfg = deps.config.current;
     const statusUrl = cfg.oap.statusUrl;
-    const opts = { statusUrl, timeoutMs: cfg.oap.timeoutMs, fetch: deps.fetch };
+    const opts = buildOapOpts(cfg, deps.fetch);
     try {
       const raw = await graphqlPost<MenuRaw>(opts, MENU_QUERY);
 
diff --git a/apps/bff/src/oap/server-time.ts b/apps/bff/src/oap/server-time.ts
index ab49e37..6a174cc 100644
--- a/apps/bff/src/oap/server-time.ts
+++ b/apps/bff/src/oap/server-time.ts
@@ -101,9 +101,18 @@ async function resolveServerTime(deps: ServerTimeDeps): Promise<ServerTime> {
       fetch: deps.fetch,
     });
     mqeBaseUrl = target.baseUrl;
+    const headers: Record<string, string> = {
+      'Content-Type': 'application/json',
+      Accept: 'application/json',
+    };
+    const auth = deps.config().oap.auth;
+    if (auth) {
+      headers.authorization =
+        'Basic ' + Buffer.from(`${auth.username}:${auth.password}`, 'utf8').toString('base64');
+    }
     const init: RequestInit = {
       method: 'POST',
-      headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
+      headers,
       body: JSON.stringify({ query: GRAPHQL_QUERY }),
       ...(ctrl ? { signal: ctrl.signal } : {}),
     };
diff --git a/apps/bff/src/oap/topology-routes.ts b/apps/bff/src/oap/topology-routes.ts
index 8d9681c..7ab9aa4 100644
--- a/apps/bff/src/oap/topology-routes.ts
+++ b/apps/bff/src/oap/topology-routes.ts
@@ -45,7 +45,7 @@ import type {
   TopologyResponse,
 } from '@skywalking-horizon-ui/api-client';
 import { requireAuth } from '../auth/middleware.js';
-import { graphqlPost } from './graphql-client.js';
+import {  graphqlPost, buildOapOpts } from './graphql-client.js';
 import { getLayerTemplate, topologyConfigFor } from '../layers/loader.js';
 
 export interface TopologyRouteDeps {
@@ -268,11 +268,7 @@ export function registerTopologyRoute(app: FastifyInstance, deps: TopologyRouteD
       const topoCfg: TopologyConfig = topologyConfigFor(template);
 
       const cfgCurrent = deps.config.current;
-      const opts = {
-        statusUrl: cfgCurrent.oap.statusUrl,
-        timeoutMs: cfgCurrent.oap.timeoutMs,
-        fetch: deps.fetch,
-      };
+      const opts = buildOapOpts(cfgCurrent, deps.fetch);
       const window = defaultWindow();
       const oapLayer = layerKey.toUpperCase();
       const durationVar = { start: window.start, end: window.end, step: 'MINUTE' };
diff --git a/apps/bff/src/oap/trace-protocol-cache.ts b/apps/bff/src/oap/trace-protocol-cache.ts
new file mode 100644
index 0000000..b94398e
--- /dev/null
+++ b/apps/bff/src/oap/trace-protocol-cache.ts
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Tiny per-process cache for the trace-protocol probe.
+ *
+ * OAP 9.6+ exposes `queryTraces` (v2) which returns the list AND
+ * the spans inline in a single roundtrip. Older OAP only exposes
+ * `queryBasicTraces` (v3) which returns trace summaries; the SPA
+ * fetches each detail via `queryTrace(traceId)`.
+ *
+ * The v2 query type carries a sibling boolean `hasQueryTracesV2Support`
+ * we can probe once and cache. Cached for 5 minutes so an OAP rollover
+ * or a redeploy is picked up without an explicit reload.
+ *
+ * The decision is per OAP target (keyed by `statusUrl`) — when the
+ * operator points horizon at a different cluster, the probe runs
+ * again.
+ */
+
+import { graphqlPost } from './graphql-client.js';
+import type { FetchLike } from '@skywalking-horizon-ui/api-client';
+
+export type TraceProtocol = 'v2' | 'v3';
+
+interface CacheEntry {
+  protocol: TraceProtocol;
+  expiresAt: number;
+}
+
+const CACHE_TTL_MS = 5 * 60_000;
+const cache = new Map<string, CacheEntry>();
+
+interface ProbeOpts {
+  statusUrl: string;
+  timeoutMs: number;
+  fetch?: FetchLike;
+  /** Optional basic-auth — same shape as GraphqlOptions so call
+   *  sites can pass the same `opts` object through. */
+  auth?: { username: string; password: string };
+}
+
+const PROBE_QUERY = /* GraphQL */ `
+  query ProbeQueryTracesV2 {
+    hasQueryTracesV2Support
+  }
+`;
+
+/**
+ * Returns the protocol the BFF should use for trace queries against
+ * this OAP target. Probes once and caches; falls back to v3 on
+ * probe failure since v3's `queryBasicTraces` is supported by every
+ * shipped OAP version.
+ */
+export async function detectTraceProtocol(opts: ProbeOpts): Promise<TraceProtocol> {
+  const now = Date.now();
+  const cached = cache.get(opts.statusUrl);
+  if (cached && cached.expiresAt > now) return cached.protocol;
+  let protocol: TraceProtocol = 'v3';
+  try {
+    const data = await graphqlPost<{ hasQueryTracesV2Support?: boolean }>(opts, PROBE_QUERY);
+    if (data.hasQueryTracesV2Support === true) protocol = 'v2';
+  } catch {
+    // Probe failed — older OAP doesn't have the field, GraphQL errors
+    // out. Cache v3 so we don't re-probe on every list call.
+  }
+  cache.set(opts.statusUrl, { protocol, expiresAt: now + CACHE_TTL_MS });
+  return protocol;
+}
+
+/** Force-invalidate the cache. Wired into the future "Refresh" admin
+ *  affordance; not used by the runtime today. */
+export function invalidateTraceProtocolCache(): void {
+  cache.clear();
+}
diff --git a/apps/bff/src/oap/trace-routes.ts b/apps/bff/src/oap/trace-routes.ts
new file mode 100644
index 0000000..0dc7be3
--- /dev/null
+++ b/apps/bff/src/oap/trace-routes.ts
@@ -0,0 +1,472 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Per-layer Traces feed.
+ *
+ *   POST /api/layer/:key/traces    — list
+ *   GET  /api/trace/:traceId       — detail by id (native or zipkin)
+ *
+ * The route is dual-source aware: when the layer's `traces.source`
+ * is `both` (default) or the operator explicitly asks for both via
+ * the query string, the BFF fans out to SkyWalking-native AND
+ * Zipkin in parallel and returns each backend's results on its own
+ * slot. The UI renders two tables side-by-side; there's no field
+ * mapping between the two — zipkin spans keep their zipkin shape.
+ *
+ * Native v2 vs v3 is auto-detected via {@link detectTraceProtocol}
+ * — the caller doesn't need to know which OAP version is answering.
+ */
+
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
+import type {
+  FetchLike,
+  NativeSpan,
+  NativeTraceDetailResponse,
+  NativeTraceListResponse,
+  TraceDetailResponse,
+  TraceListResponse,
+  TraceQueryOrder,
+  TraceQueryState,
+  TraceSource,
+  TracesConfig,
+  ZipkinTraceDetailResponse,
+  ZipkinTraceListResponse,
+} from '@skywalking-horizon-ui/api-client';
+import type { ConfigSource } from '../config/loader.js';
+import type { SessionStore } from '../auth/sessions.js';
+import { requireAuth } from '../auth/middleware.js';
+import {  graphqlPost, buildOapOpts, type GraphqlOptions } from './graphql-client.js';
+import { getLayerTemplate, tracesConfigFor } from '../layers/loader.js';
+import { detectTraceProtocol } from './trace-protocol-cache.js';
+import { zipkinFetchTraces, zipkinFetchTraceById, summariseZipkinTrace } from './zipkin-client.js';
+
+export interface TraceRouteDeps {
+  config: ConfigSource;
+  sessions: SessionStore;
+  fetch?: FetchLike;
+}
+
+const DEFAULT_WINDOW_MIN = 30;
+const MAX_WINDOW_MIN = 60 * 24 * 7; // 1 week guard
+function fmtMinute(d: Date): string {
+  const yyyy = d.getUTCFullYear();
+  const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
+  const dd = String(d.getUTCDate()).padStart(2, '0');
+  const HH = String(d.getUTCHours()).padStart(2, '0');
+  const MM = String(d.getUTCMinutes()).padStart(2, '0');
+  return `${yyyy}-${mm}-${dd} ${HH}${MM}`;
+}
+function rollingWindow(minutes: number): { start: string; end: string } {
+  const m = Math.max(1, Math.min(MAX_WINDOW_MIN, Math.round(minutes)));
+  const end = new Date();
+  const start = new Date(end.getTime() - m * 60_000);
+  return { start: fmtMinute(start), end: fmtMinute(end) };
+}
+function explicitWindow(startIso: string, endIso: string): { start: string; end: string } | null {
+  const s = new Date(startIso);
+  const e = new Date(endIso);
+  if (!Number.isFinite(s.getTime()) || !Number.isFinite(e.getTime()) || e.getTime() < s.getTime()) {
+    return null;
+  }
+  return { start: fmtMinute(s), end: fmtMinute(e) };
+}
+
+// ── Wire request shape ─────────────────────────────────────────────
+
+interface TraceListBody {
+  source?: TraceSource;
+  service?: string;
+  serviceId?: string;
+  instanceId?: string;
+  endpointId?: string;
+  traceId?: string;
+  traceState?: TraceQueryState;
+  queryOrder?: TraceQueryOrder;
+  minTraceDuration?: number;
+  maxTraceDuration?: number;
+  pageNum?: number;
+  pageSize?: number;
+  /** Free-form span tags (`http.status_code=500`, …). Matches OAP's
+   *  `TraceQueryCondition.tags: [KeyValue]`. */
+  tags?: Array<{ key: string; value: string }>;
+  /** Rolling window in minutes. Default 30; clamped to [1, 10080]. */
+  windowMinutes?: number;
+  /** Explicit ISO start (UTC). When both `start` and `end` are
+   *  provided they override `windowMinutes`. */
+  start?: string;
+  /** Explicit ISO end (UTC). Pair with `start`. */
+  end?: string;
+}
+
+// ── Native GraphQL queries ────────────────────────────────────────
+
+const LIST_SERVICES_FOR_RESOLVE = /* GraphQL */ `
+  query ListServicesForTrace($layer: String!) {
+    services: listServices(layer: $layer) {
+      id
+      name
+      normal
+    }
+  }
+`;
+
+const QUERY_BASIC_TRACES_V3 = /* GraphQL */ `
+  query QueryBasicTraces($condition: TraceQueryCondition) {
+    data: queryBasicTraces(condition: $condition) {
+      traces {
+        key: segmentId
+        endpointNames
+        duration
+        start
+        isError
+        traceIds
+      }
+    }
+  }
+`;
+
+const QUERY_TRACES_V2 = /* GraphQL */ `
+  query QueryTracesV2($condition: TraceQueryCondition) {
+    data: queryTraces(condition: $condition) {
+      traces {
+        spans {
+          traceId
+          segmentId
+          spanId
+          parentSpanId
+          refs { traceId parentSegmentId parentSpanId type }
+          serviceCode
+          serviceInstanceName
+          startTime
+          endTime
+          endpointName
+          type
+          peer
+          component
+          isError
+          layer
+          tags { key value }
+          logs { time data { key value } }
+          attachedEvents {
+            startTime { seconds nanos }
+            event
+            endTime { seconds nanos }
+            tags { key value }
+            summary { key value }
+          }
+        }
+      }
+    }
+  }
+`;
+
+const QUERY_TRACE_DETAIL = /* GraphQL */ `
+  query QueryTrace($traceId: ID!) {
+    trace: queryTrace(traceId: $traceId) {
+      spans {
+        traceId
+        segmentId
+        spanId
+        parentSpanId
+        refs { traceId parentSegmentId parentSpanId type }
+        serviceCode
+        serviceInstanceName
+        startTime
+        endTime
+        endpointName
+        type
+        peer
+        component
+        isError
+        layer
+        tags { key value }
+        logs { time data { key value } }
+        attachedEvents {
+          startTime { seconds nanos }
+          event
+          endTime { seconds nanos }
+          tags { key value }
+          summary { key value }
+        }
+      }
+    }
+  }
+`;
+
+// ── Helpers ────────────────────────────────────────────────────────
+
+async function resolveServiceId(
+  opts: GraphqlOptions,
+  layer: string,
+  serviceArg: string,
+): Promise<string | null> {
+  if (!serviceArg) return null;
+  // Ids contain `.`; names rarely do. Short-circuit when it's already
+  // an id.
+  if (serviceArg.includes('.') && !/\s/.test(serviceArg)) return serviceArg;
+  const data = await graphqlPost<{
+    services: Array<{ id: string; name: string }>;
+  }>(opts, LIST_SERVICES_FOR_RESOLVE, { layer: layer.toUpperCase() });
+  return (
+    data.services.find((s) => s.name === serviceArg)?.id ??
+    data.services.find((s) => s.id === serviceArg)?.id ??
+    null
+  );
+}
+
+function buildTraceCondition(body: TraceListBody, resolvedServiceId: string | null, w: { start: string; end: string }) {
+  return {
+    ...(resolvedServiceId ? { serviceId: resolvedServiceId } : {}),
+    ...(body.instanceId ? { serviceInstanceId: body.instanceId } : {}),
+    ...(body.endpointId ? { endpointId: body.endpointId } : {}),
+    ...(body.traceId ? { traceId: body.traceId } : {}),
+    ...(body.tags && body.tags.length > 0 ? { tags: body.tags } : {}),
+    ...(typeof body.minTraceDuration === 'number' ? { minTraceDuration: body.minTraceDuration } : {}),
+    ...(typeof body.maxTraceDuration === 'number' ? { maxTraceDuration: body.maxTraceDuration } : {}),
+    queryDuration: { start: w.start, end: w.end, step: 'MINUTE' },
+    traceState: (body.traceState ?? 'ALL') as TraceQueryState,
+    queryOrder: (body.queryOrder ?? 'BY_START_TIME') as TraceQueryOrder,
+    paging: {
+      pageNum: body.pageNum ?? 1,
+      pageSize: body.pageSize ?? 20,
+    },
+  };
+}
+
+async function fetchNativeList(
+  opts: GraphqlOptions,
+  body: TraceListBody,
+  layerKey: string,
+): Promise<NativeTraceListResponse> {
+  const protocol = await detectTraceProtocol(opts);
+  // Explicit start+end takes precedence over windowMinutes; falling
+  // back to the rolling default when the explicit range is invalid.
+  const explicit = body.start && body.end ? explicitWindow(body.start, body.end) : null;
+  const window = explicit ?? rollingWindow(body.windowMinutes ?? DEFAULT_WINDOW_MIN);
+  let serviceId: string | null = null;
+  try {
+    serviceId = body.serviceId
+      ? body.serviceId
+      : body.service
+        ? await resolveServiceId(opts, layerKey, body.service)
+        : null;
+  } catch (err) {
+    return {
+      source: 'native',
+      protocol,
+      traces: [],
+      reachable: false,
+      error: err instanceof Error ? err.message : String(err),
+    };
+  }
+  const condition = buildTraceCondition(body, serviceId, window);
+  try {
+    if (protocol === 'v2') {
+      const env = await graphqlPost<{
+        data: { traces: Array<{ spans: NativeSpan[] }> };
+      }>(opts, QUERY_TRACES_V2, { condition });
+      const traces = (env.data?.traces ?? []).map((t) => {
+        const root = t.spans.find((s) => s.parentSpanId === -1) ?? t.spans[0];
+        const ids = Array.from(new Set(t.spans.map((s) => s.traceId)));
+        return {
+          key: root?.segmentId ?? ids[0] ?? '',
+          segmentId: root?.segmentId ?? '',
+          endpointNames: root ? [root.endpointName] : [],
+          duration: root ? root.endTime - root.startTime : 0,
+          start: root ? String(root.startTime) : '',
+          isError: t.spans.some((s) => s.isError),
+          traceIds: ids,
+          spans: t.spans,
+        };
+      });
+      return { source: 'native', protocol, traces, reachable: true };
+    }
+    const env = await graphqlPost<{
+      data: {
+        traces: Array<{
+          key: string;
+          endpointNames: string[];
+          duration: number;
+          start: string;
+          isError: boolean;
+          traceIds: string[];
+        }>;
+      };
+    }>(opts, QUERY_BASIC_TRACES_V3, { condition });
+    const traces = (env.data?.traces ?? []).map((t) => ({
+      key: t.key,
+      segmentId: t.key,
+      endpointNames: t.endpointNames,
+      duration: t.duration,
+      start: t.start,
+      isError: t.isError,
+      traceIds: t.traceIds,
+    }));
+    return { source: 'native', protocol, traces, reachable: true };
+  } catch (err) {
+    return {
+      source: 'native',
+      protocol,
+      traces: [],
+      reachable: false,
+      error: err instanceof Error ? err.message : String(err),
+    };
+  }
+}
+
+async function fetchZipkinList(
+  opts: GraphqlOptions,
+  body: TraceListBody,
+): Promise<ZipkinTraceListResponse> {
+  try {
+    const traces = await zipkinFetchTraces(opts, {
+      serviceName: body.service,
+      minDuration: body.minTraceDuration,
+      maxDuration: body.maxTraceDuration,
+      limit: body.pageSize ?? 20,
+    });
+    return { source: 'zipkin', traces, reachable: true };
+  } catch (err) {
+    return {
+      source: 'zipkin',
+      traces: [],
+      reachable: false,
+      error: err instanceof Error ? err.message : String(err),
+    };
+  }
+}
+
+// ── Routes ─────────────────────────────────────────────────────────
+
+export function registerTraceRoutes(app: FastifyInstance, deps: TraceRouteDeps): void {
+  const auth = requireAuth(deps);
+
+  app.post(
+    '/api/layer/:key/traces',
+    { preHandler: auth },
+    async (req: FastifyRequest, reply: FastifyReply) => {
+      const params = req.params as { key: string };
+      const layerKey = params.key;
+      if (!layerKey || !/^[a-z0-9_]+$/i.test(layerKey)) {
+        return reply.code(400).send({ error: 'invalid_layer_key' });
+      }
+      const body = (req.body ?? {}) as TraceListBody;
+      const template = getLayerTemplate(layerKey);
+      const tracesCfg: TracesConfig = tracesConfigFor(template);
+      const requestedSource: TraceSource = body.source ?? tracesCfg.source;
+      const opts = buildOapOpts(deps.config.current, deps.fetch);
+
+      const wantNative = requestedSource === 'both' || requestedSource === 'native';
+      const wantZipkin = requestedSource === 'both' || requestedSource === 'zipkin';
+      // Fan out in parallel; partial failures don't drop the whole
+      // response — the UI's empty / error states cover each slot.
+      const [native, zipkin] = await Promise.all([
+        wantNative ? fetchNativeList(opts, body, layerKey) : Promise.resolve(undefined),
+        wantZipkin ? fetchZipkinList(opts, body) : Promise.resolve(undefined),
+      ]);
+
+      const response: TraceListResponse = {
+        generatedAt: Date.now(),
+        source: requestedSource,
+        ...(native ? { native } : {}),
+        ...(zipkin ? { zipkin } : {}),
+      };
+      return reply.send(response);
+    },
+  );
+
+  app.get(
+    '/api/trace/:traceId',
+    { preHandler: auth },
+    async (req: FastifyRequest, reply: FastifyReply) => {
+      const params = req.params as { traceId: string };
+      const q = req.query as { source?: 'native' | 'zipkin' };
+      const source: 'native' | 'zipkin' = q.source === 'zipkin' ? 'zipkin' : 'native';
+      const opts = buildOapOpts(deps.config.current, deps.fetch);
+
+      if (source === 'native') {
+        const protocol = await detectTraceProtocol(opts);
+        try {
+          const env = await graphqlPost<{ trace: { spans: NativeSpan[] } }>(
+            opts,
+            QUERY_TRACE_DETAIL,
+            { traceId: params.traceId },
+          );
+          const detail: NativeTraceDetailResponse = {
+            source: 'native',
+            protocol,
+            traceId: params.traceId,
+            spans: env.trace?.spans ?? [],
+            reachable: true,
+          };
+          return reply.send({
+            generatedAt: Date.now(),
+            source,
+            native: detail,
+          } satisfies TraceDetailResponse);
+        } catch (err) {
+          const detail: NativeTraceDetailResponse = {
+            source: 'native',
+            protocol,
+            traceId: params.traceId,
+            spans: [],
+            reachable: false,
+            error: err instanceof Error ? err.message : String(err),
+          };
+          return reply.send({
+            generatedAt: Date.now(),
+            source,
+            native: detail,
+          } satisfies TraceDetailResponse);
+        }
+      }
+      // Zipkin.
+      try {
+        const spans = await zipkinFetchTraceById(opts, params.traceId);
+        const detail: ZipkinTraceDetailResponse = {
+          source: 'zipkin',
+          traceId: params.traceId,
+          spans,
+          reachable: true,
+        };
+        return reply.send({
+          generatedAt: Date.now(),
+          source,
+          zipkin: detail,
+        } satisfies TraceDetailResponse);
+      } catch (err) {
+        const detail: ZipkinTraceDetailResponse = {
+          source: 'zipkin',
+          traceId: params.traceId,
+          spans: [],
+          reachable: false,
+          error: err instanceof Error ? err.message : String(err),
+        };
+        return reply.send({
+          generatedAt: Date.now(),
+          source,
+          zipkin: detail,
+        } satisfies TraceDetailResponse);
+      }
+    },
+  );
+}
+
+// Re-export the summariser so future callers can consume it; unused
+// here but useful for tests.
+export { summariseZipkinTrace };
diff --git a/apps/bff/src/oap/trace-tag-routes.ts b/apps/bff/src/oap/trace-tag-routes.ts
new file mode 100644
index 0000000..86c1a12
--- /dev/null
+++ b/apps/bff/src/oap/trace-tag-routes.ts
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Trace tag autocomplete routes:
+ *   GET /api/trace-tags/keys?windowMinutes=30
+ *   GET /api/trace-tags/values?key=<k>&windowMinutes=30
+ *
+ * Both wrap OAP's `queryTraceTagAutocompleteKeys` /
+ * `queryTraceTagAutocompleteValues` GraphQL endpoints. Same pattern
+ * as booster-ui's `ConditionTags.vue` — operator types in the trace
+ * tab's Tag input, the suggestions list narrows.
+ */
+
+import type { FetchLike } from '@skywalking-horizon-ui/api-client';
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
+import type { ConfigSource } from '../config/loader.js';
+import type { SessionStore } from '../auth/sessions.js';
+import { requireAuth } from '../auth/middleware.js';
+import { buildOapOpts, graphqlPost } from './graphql-client.js';
+
+export interface TraceTagRouteDeps {
+  config: ConfigSource;
+  sessions: SessionStore;
+  fetch?: FetchLike;
+}
+
+const DEFAULT_WINDOW_MIN = 30;
+const MAX_WINDOW_MIN = 60 * 24 * 7;
+function fmtMinute(d: Date): string {
+  const pad = (n: number) => String(n).padStart(2, '0');
+  return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}`;
+}
+function rollingWindow(minutes: number): { start: string; end: string; step: 'MINUTE' } {
+  const m = Math.max(1, Math.min(MAX_WINDOW_MIN, Math.round(minutes)));
+  const end = new Date();
+  const start = new Date(end.getTime() - m * 60_000);
+  return { start: fmtMinute(start), end: fmtMinute(end), step: 'MINUTE' };
+}
+
+const KEYS_QUERY = /* GraphQL */ `
+  query TagKeys($duration: Duration!) {
+    keys: queryTraceTagAutocompleteKeys(duration: $duration)
+  }
+`;
+const VALUES_QUERY = /* GraphQL */ `
+  query TagValues($tagKey: String!, $duration: Duration!) {
+    values: queryTraceTagAutocompleteValues(tagKey: $tagKey, duration: $duration)
+  }
+`;
+
+export function registerTraceTagRoutes(app: FastifyInstance, deps: TraceTagRouteDeps): void {
+  const auth = requireAuth(deps);
+
+  app.get(
+    '/api/trace-tags/keys',
+    { preHandler: auth },
+    async (req: FastifyRequest, reply: FastifyReply) => {
+      const q = req.query as { windowMinutes?: string };
+      const m = q.windowMinutes ? Number(q.windowMinutes) : DEFAULT_WINDOW_MIN;
+      const duration = rollingWindow(Number.isFinite(m) ? m : DEFAULT_WINDOW_MIN);
+      const opts = buildOapOpts(deps.config.current, deps.fetch);
+      try {
+        const env = await graphqlPost<{ keys: string[] | null }>(opts, KEYS_QUERY, { duration });
+        return reply.send({ keys: env.keys ?? [], generatedAt: Date.now() });
+      } catch (err) {
+        return reply
+          .code(200)
+          .send({ keys: [], generatedAt: Date.now(), error: err instanceof Error ? err.message : String(err) });
+      }
+    },
+  );
+
+  app.get(
+    '/api/trace-tags/values',
+    { preHandler: auth },
+    async (req: FastifyRequest, reply: FastifyReply) => {
+      const q = req.query as { key?: string; windowMinutes?: string };
+      const key = (q.key ?? '').trim();
+      if (!key) return reply.code(400).send({ error: 'missing_key' });
+      const m = q.windowMinutes ? Number(q.windowMinutes) : DEFAULT_WINDOW_MIN;
+      const duration = rollingWindow(Number.isFinite(m) ? m : DEFAULT_WINDOW_MIN);
+      const opts = buildOapOpts(deps.config.current, deps.fetch);
+      try {
+        const env = await graphqlPost<{ values: string[] | null }>(opts, VALUES_QUERY, {
+          tagKey: key,
+          duration,
+        });
+        return reply.send({ key, values: env.values ?? [], generatedAt: Date.now() });
+      } catch (err) {
+        return reply
+          .code(200)
+          .send({
+            key,
+            values: [],
+            generatedAt: Date.now(),
+            error: err instanceof Error ? err.message : String(err),
+          });
+      }
+    },
+  );
+}
diff --git a/apps/bff/src/oap/zipkin-client.ts b/apps/bff/src/oap/zipkin-client.ts
new file mode 100644
index 0000000..eafd2f7
--- /dev/null
+++ b/apps/bff/src/oap/zipkin-client.ts
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Tiny REST client for OAP's Zipkin Query plugin endpoints.
+ *
+ * Zipkin lives on the SAME host:port as OAP's GraphQL (the
+ * `oap.statusUrl`) but uses standard zipkin v2 paths instead of
+ * GraphQL:
+ *
+ *   GET /api/v2/services
+ *   GET /api/v2/spans
+ *   GET /api/v2/traces?serviceName=&spanName=&minDuration=&maxDuration=&endTs=&lookback=&limit=
+ *   GET /api/v2/trace/{traceId}
+ *
+ * Wire shape is the standard Zipkin v2 JSON span format. We surface
+ * it verbatim — no field mapping into the SkyWalking-native shape.
+ *
+ * Errors are bubbled as plain `Error` instances; the route handler
+ * wraps them into a soft-fail response so the parallel native fetch
+ * still surfaces results.
+ */
+
+import type {
+  FetchLike,
+  ZipkinSpan,
+  ZipkinTraceListRow,
+} from '@skywalking-horizon-ui/api-client';
+
+export interface ZipkinClientOpts {
+  statusUrl: string;
+  timeoutMs: number;
+  fetch?: FetchLike;
+  /** Optional basic-auth — same shape as the GraphQL client. */
+  auth?: { username: string; password: string };
+}
+
+export interface ZipkinTracesQuery {
+  serviceName?: string;
+  spanName?: string;
+  /** Microseconds. */
+  minDuration?: number;
+  /** Microseconds. */
+  maxDuration?: number;
+  /** Upper bound time in millis since epoch (defaults to now). */
+  endTs?: number;
+  /** Lookback in millis (defaults 30 min). */
+  lookback?: number;
+  limit?: number;
+}
+
+const DEFAULT_LOOKBACK_MS = 30 * 60_000;
+
+async function zipkinFetch<T>(opts: ZipkinClientOpts, path: string): Promise<T> {
+  const f = opts.fetch ?? globalThis.fetch.bind(globalThis);
+  const url = opts.statusUrl.replace(/\/$/, '') + path;
+  const controller = new AbortController();
+  const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
+  const headers: Record<string, string> = { Accept: 'application/json' };
+  if (opts.auth) {
+    const raw = `${opts.auth.username}:${opts.auth.password}`;
+    const b64 = Buffer.from(raw, 'utf8').toString('base64');
+    headers.authorization = `Basic ${b64}`;
+  }
+  let res: Response;
+  try {
+    res = await f(url, {
+      method: 'GET',
+      headers,
+      signal: controller.signal,
+    });
+  } finally {
+    clearTimeout(timer);
+  }
+  if (!res.ok) {
+    const body = await res.text().catch(() => '');
+    throw new Error(`zipkin ${res.status} ${url} — ${body.slice(0, 200)}`);
+  }
+  return (await res.json()) as T;
+}
+
+/**
+ * Summarise a zipkin trace's span array into a list-row. Zipkin
+ * doesn't ship a "traces summary" endpoint; the list endpoint
+ * returns `Span[][]` (one inner array per trace), so we compute
+ * the headline fields ourselves from the root span.
+ */
+export function summariseZipkinTrace(spans: ZipkinSpan[]): ZipkinTraceListRow {
+  if (spans.length === 0) {
+    return {
+      traceId: '',
+      rootName: null,
+      rootService: null,
+      timestamp: null,
+      duration: null,
+      spanCount: 0,
+      errorCount: 0,
+    };
+  }
+  const root = spans.find((s) => !s.parentId) ?? spans[0];
+  let errorCount = 0;
+  for (const s of spans) {
+    if (s.tags && (s.tags.error !== undefined || s.tags['otel.status_code'] === 'ERROR')) {
+      errorCount += 1;
+    }
+  }
+  return {
+    traceId: root.traceId,
+    rootName: root.name ?? null,
+    rootService: root.localEndpoint?.serviceName ?? null,
+    timestamp: root.timestamp ?? null,
+    duration: root.duration ?? null,
+    spanCount: spans.length,
+    errorCount,
+  };
+}
+
+/**
+ * List traces matching the operator's filter. Zipkin's wire
+ * response is `Span[][]` (one inner array per trace); we summarise
+ * each entry into a single list-row for the UI table.
+ */
+export async function zipkinFetchTraces(
+  opts: ZipkinClientOpts,
+  query: ZipkinTracesQuery,
+): Promise<ZipkinTraceListRow[]> {
+  const qs = new URLSearchParams();
+  if (query.serviceName) qs.set('serviceName', query.serviceName);
+  if (query.spanName) qs.set('spanName', query.spanName);
+  if (typeof query.minDuration === 'number') qs.set('minDuration', String(query.minDuration));
+  if (typeof query.maxDuration === 'number') qs.set('maxDuration', String(query.maxDuration));
+  const endTs = query.endTs ?? Date.now();
+  const lookback = query.lookback ?? DEFAULT_LOOKBACK_MS;
+  qs.set('endTs', String(endTs));
+  qs.set('lookback', String(lookback));
+  qs.set('limit', String(query.limit ?? 20));
+  const path = `/api/v2/traces?${qs.toString()}`;
+  const arr = await zipkinFetch<ZipkinSpan[][]>(opts, path);
+  return arr.map(summariseZipkinTrace).filter((r) => r.traceId);
+}
+
+/** Fetch a single trace by id. Returns the full span array
+ *  unmodified — the UI's zipkin waterfall renders zipkin fields
+ *  natively (kind / annotations / localEndpoint / remoteEndpoint /
+ *  tags map). */
+export async function zipkinFetchTraceById(
+  opts: ZipkinClientOpts,
+  traceId: string,
+): Promise<ZipkinSpan[]> {
+  const path = `/api/v2/trace/${encodeURIComponent(traceId)}`;
+  return zipkinFetch<ZipkinSpan[]>(opts, path);
+}
diff --git a/apps/bff/src/overview/loader.ts b/apps/bff/src/overview/loader.ts
new file mode 100644
index 0000000..680c07c
--- /dev/null
+++ b/apps/bff/src/overview/loader.ts
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Overview-dashboard config loader. Reads every `*.json` under
+ * `../bundled_templates/overviews/`, validates the wire shape, and keeps
+ * the parsed dashboards in-memory. The loader is intentionally eager
+ * (all dashboards loaded at boot) — config files are small and the
+ * SPA hits dashboard endpoints often enough that on-demand parsing
+ * isn't worth the ceremony.
+ */
+
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import type { OverviewDashboard } from '@skywalking-horizon-ui/api-client';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+// Bundled overview dashboards live under `bundled_templates/overviews/`
+// (sibling to `bundled_templates/layers/`). Co-locating the static
+// templates keeps the future "operator-editable / OAP-served" migration
+// to one directory swap.
+const CONFIG_DIR = path.join(__dirname, '..', 'bundled_templates', 'overviews');
+
+let cache: OverviewDashboard[] | null = null;
+
+function isString(v: unknown): v is string {
+  return typeof v === 'string' && v.length > 0;
+}
+
+function validate(raw: unknown, file: string): OverviewDashboard | null {
+  if (!raw || typeof raw !== 'object') {
+    console.warn(`overview/${file}: not an object, skipped`);
+    return null;
+  }
+  const r = raw as Record<string, unknown>;
+  if (!isString(r.id) || !isString(r.title) || !Array.isArray(r.widgets)) {
+    console.warn(`overview/${file}: missing id/title/widgets`);
+    return null;
+  }
+  const widgets = (r.widgets as unknown[]).filter((w): w is Record<string, unknown> => {
+    if (!w || typeof w !== 'object') return false;
+    const rec = w as Record<string, unknown>;
+    return (
+      isString(rec.id) &&
+      isString(rec.title) &&
+      isString(rec.layer) &&
+      (rec.type === 'service-count' || rec.type === 'metric' || rec.type === 'topology')
+    );
+  });
+  return {
+    id: r.id,
+    title: r.title,
+    description: isString(r.description) ? r.description : undefined,
+    widgets: widgets.map((w) => ({
+      id: w.id as string,
+      title: w.title as string,
+      tip: isString(w.tip) ? w.tip : undefined,
+      layer: w.layer as string,
+      type: w.type as 'service-count' | 'metric' | 'topology',
+      mqe: isString(w.mqe) ? w.mqe : undefined,
+      unit: isString(w.unit) ? w.unit : undefined,
+      aggregation: w.aggregation === 'sum' ? 'sum' : w.aggregation === 'avg' ? 'avg' : undefined,
+      span: typeof w.span === 'number' ? w.span : undefined,
+      rowSpan: typeof w.rowSpan === 'number' ? w.rowSpan : undefined,
+    })),
+  };
+}
+
+/**
+ * Load every dashboard config under `../bundled_templates/overviews/*.json`.
+ * Memoised after the first call; restart the BFF to pick up file changes
+ * (in line with the layer-template loader's behaviour).
+ */
+export function loadOverviewDashboards(): OverviewDashboard[] {
+  if (cache) return cache;
+  if (!fs.existsSync(CONFIG_DIR)) {
+    cache = [];
+    return cache;
+  }
+  const files = fs.readdirSync(CONFIG_DIR).filter((f) => f.endsWith('.json'));
+  const out: OverviewDashboard[] = [];
+  for (const f of files) {
+    try {
+      const raw = JSON.parse(fs.readFileSync(path.join(CONFIG_DIR, f), 'utf8'));
+      const dash = validate(raw, f);
+      if (dash) out.push(dash);
+    } catch (err) {
+      console.warn(`overview/${f}: parse error`, err instanceof Error ? err.message : err);
+    }
+  }
+  // Keep stable ordering by id so the SPA's tab strip doesn't flicker
+  // on reload.
+  out.sort((a, b) => a.id.localeCompare(b.id));
+  cache = out;
+  return cache;
+}
+
+/** Force a re-read on the next call. Used in tests. */
+export function invalidateOverviewCache(): void {
+  cache = null;
+}
+
+export function getOverviewDashboard(id: string): OverviewDashboard | null {
+  return loadOverviewDashboards().find((d) => d.id === id) ?? null;
+}
diff --git a/apps/bff/src/overview/routes.ts b/apps/bff/src/overview/routes.ts
new file mode 100644
index 0000000..1da30c9
--- /dev/null
+++ b/apps/bff/src/overview/routes.ts
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Overview-dashboard routes.
+ *
+ *   GET  /api/overview/dashboards            — list (id, title, count).
+ *   GET  /api/overview/dashboards/:id        — full dashboard config.
+ *
+ * Widget DATA is fetched by the SPA on a per-widget basis through
+ * existing routes — service-count + topology hit
+ * `/api/layer/:key/landing` and `/api/layer/:key/topology` directly,
+ * and `metric` widgets evaluate their MQE through the dashboard data
+ * route. Keeping the overview route lean (config only) lets the SPA
+ * mix and match — e.g. a "Mesh service" dashboard widget reusing
+ * the same topology endpoint that the per-layer page hits.
+ */
+
+import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
+import type {
+  OverviewDashboardListResponse,
+  OverviewDashboardResponse,
+} from '@skywalking-horizon-ui/api-client';
+import type { ConfigSource } from '../config/loader.js';
+import type { SessionStore } from '../auth/sessions.js';
+import { requireAuth } from '../auth/middleware.js';
+import { getOverviewDashboard, loadOverviewDashboards } from './loader.js';
+
+export interface OverviewRouteDeps {
+  config: ConfigSource;
+  sessions: SessionStore;
+}
+
+export function registerOverviewRoutes(app: FastifyInstance, deps: OverviewRouteDeps): void {
+  const auth = requireAuth(deps);
+
+  app.get('/api/overview/dashboards', { preHandler: auth }, async (_req, reply) => {
+    const dashboards = loadOverviewDashboards();
+    const body: OverviewDashboardListResponse = {
+      generatedAt: Date.now(),
+      dashboards: dashboards.map((d) => ({
+        id: d.id,
+        title: d.title,
+        description: d.description,
+        widgetCount: d.widgets.length,
+      })),
+    };
+    return reply.send(body);
+  });
+
+  app.get(
+    '/api/overview/dashboards/:id',
+    { preHandler: auth },
+    async (req: FastifyRequest, reply: FastifyReply) => {
+      const { id } = req.params as { id: string };
+      const dash = getOverviewDashboard(id);
+      if (!dash) {
+        const body: OverviewDashboardResponse = {
+          generatedAt: Date.now(),
+          dashboard: { id, title: id, widgets: [] },
+          reachable: false,
+          error: `unknown dashboard "${id}"`,
+        };
+        return reply.code(404).send(body);
+      }
+      const body: OverviewDashboardResponse = {
+        generatedAt: Date.now(),
+        dashboard: dash,
+        reachable: true,
+      };
+      return reply.send(body);
+    },
+  );
+}
diff --git a/apps/bff/src/server.ts b/apps/bff/src/server.ts
index 3909a2b..7e3d955 100644
--- a/apps/bff/src/server.ts
+++ b/apps/bff/src/server.ts
@@ -15,8 +15,11 @@
  * limitations under the License.
  */
 
+import { existsSync } from 'node:fs';
+import { resolve as resolvePath } from 'node:path';
 import Fastify from 'fastify';
 import cookie from '@fastify/cookie';
+import fastifyStatic from '@fastify/static';
 import { AuditLogger } from './audit/logger.js';
 import { registerAuthRoutes } from './auth/routes.js';
 import { SessionStore } from './auth/sessions.js';
@@ -27,10 +30,14 @@ import { registerEndpointDependencyRoute } from './oap/endpoint-dependency-route
 import { registerOapInfoRoute } from './oap/info-routes.js';
 import { registerInstanceRoute } from './oap/instance-routes.js';
 import { registerLandingRoute } from './oap/landing-routes.js';
+import { registerLogRoute } from './oap/log-routes.js';
 import { registerMenuRoute } from './oap/menu-routes.js';
 import { registerOapRoutes } from './oap/routes.js';
 import { registerPreflightRoutes } from './oap/preflight-routes.js';
 import { registerTopologyRoute } from './oap/topology-routes.js';
+import { registerTraceRoutes } from './oap/trace-routes.js';
+import { registerTraceTagRoutes } from './oap/trace-tag-routes.js';
+import { registerOverviewRoutes } from './overview/routes.js';
 import { registerSetupRoutes } from './setup/routes.js';
 import { SetupStore } from './setup/store.js';
 import { HttpError } from './errors.js';
@@ -72,11 +79,35 @@ registerInstanceRoute(app, { config: source, sessions });
 registerEndpointRoute(app, { config: source, sessions });
 registerTopologyRoute(app, { config: source, sessions });
 registerEndpointDependencyRoute(app, { config: source, sessions });
+registerTraceRoutes(app, { config: source, sessions });
+registerTraceTagRoutes(app, { config: source, sessions });
+registerLogRoute(app, { config: source, sessions });
+registerOverviewRoutes(app, { config: source, sessions });
 registerDashboardRoute(app, { config: source, sessions });
 registerSetupRoutes(app, { config: source, sessions, audit, store: setupStore });
 registerOapRoutes(app, { config: source, sessions, audit });
 registerPreflightRoutes(app, { config: source, sessions });
 
+// Serve the built SPA out of the BFF when HORIZON_STATIC_DIR points at a
+// directory (Docker image layout: /app/static contains the Vite dist).
+// Local dev keeps using the Vite dev-server on :9091 so this is a no-op
+// when the env var is absent.
+const staticDir = process.env.HORIZON_STATIC_DIR
+  ? resolvePath(process.env.HORIZON_STATIC_DIR)
+  : null;
+if (staticDir && existsSync(staticDir)) {
+  await app.register(fastifyStatic, { root: staticDir, prefix: '/', wildcard: false });
+  // SPA fallback — anything that isn't an `/api/*` request and didn't match
+  // a built file falls through to index.html so client-side routing works.
+  app.setNotFoundHandler((req, reply) => {
+    if (req.url.startsWith('/api/')) {
+      return reply.code(404).send({ code: 'not_found', message: req.url });
+    }
+    return reply.sendFile('index.html');
+  });
+  logger.info({ staticDir }, 'serving SPA from static dir');
+}
+
 app.get('/api/health', async () => ({
   status: 'ok',
   version: process.env.HORIZON_VERSION ?? '0.1.0',
diff --git a/apps/ui/src/api/client.ts b/apps/ui/src/api/client.ts
index 76a8c69..7b7cce2 100644
--- a/apps/ui/src/api/client.ts
+++ b/apps/ui/src/api/client.ts
@@ -19,14 +19,25 @@ import type {
   DashboardConfig,
   DashboardResponse,
   DashboardWidget,
+  EndpointDependencyConfig,
   EndpointDependencyResponse,
   LandingConfig,
   LandingResponse,
+  LogFacetsResponse,
+  LogQueryRequest,
+  LogsResponse,
   MenuResponse,
   OapInfo,
   SetupResponse,
   SetupSavePayload,
+  TopologyConfig,
   TopologyResponse,
+  TraceDetailResponse,
+  TraceListResponse,
+  TraceQueryOrder,
+  TraceQueryState,
+  TraceSource,
+  TracesConfig,
 } from '@skywalking-horizon-ui/api-client';
 
 export type {
@@ -55,6 +66,33 @@ export type {
   EndpointDependencyNode,
   EndpointDependencyCall,
   EndpointDependencyResponse,
+  TraceSource,
+  TracesConfig,
+  TraceKeyValue,
+  TraceLogEntry,
+  TraceAttachedEvent,
+  TraceRef,
+  NativeSpan,
+  NativeTraceListRow,
+  NativeTraceListResponse,
+  NativeTraceDetailResponse,
+  TraceQueryOrder,
+  TraceQueryState,
+  ZipkinEndpoint,
+  ZipkinAnnotation,
+  ZipkinKind,
+  ZipkinSpan,
+  ZipkinTraceListRow,
+  ZipkinTraceListResponse,
+  ZipkinTraceDetailResponse,
+  TraceListResponse,
+  TraceDetailResponse,
+  LogKeyValue,
+  LogRow,
+  LogTagFilter,
+  LogQueryRequest,
+  LogsResponse,
+  LogFacetsResponse,
 } from '@skywalking-horizon-ui/api-client';
 
 
@@ -80,6 +118,9 @@ export interface AdminLayerTemplate {
     traces?: boolean;
     logs?: boolean;
     profiling?: boolean;
+    traceProfiling?: boolean;
+    ebpfProfiling?: boolean;
+    asyncProfiling?: boolean;
   };
   metrics: {
     orderBy?: string;
@@ -98,6 +139,14 @@ export interface AdminLayerTemplate {
     spark?: string;
   };
   widgets: DashboardWidget[];
+  /** Operator-editable service-map config — node + server + client
+   *  metric definitions, with optional thresholds. */
+  topology?: TopologyConfig;
+  /** Operator-editable API-dependency config — node + server-side line
+   *  metrics (OAP has no client family for endpoint relations). */
+  endpointDependency?: EndpointDependencyConfig;
+  /** Per-layer trace source default. */
+  traces?: TracesConfig;
 }
 
 export class BffApiError extends Error {
@@ -295,6 +344,83 @@ export class BffClient {
     );
   }
 
+  /** List traces — fans out to SW-native and / or Zipkin based on
+   *  the layer's config and the operator's override. */
+  layerTraces(
+    layerKey: string,
+    body: {
+      source?: TraceSource;
+      service?: string;
+      serviceId?: string;
+      instanceId?: string;
+      endpointId?: string;
+      traceId?: string;
+      traceState?: TraceQueryState;
+      queryOrder?: TraceQueryOrder;
+      minTraceDuration?: number;
+      maxTraceDuration?: number;
+      pageNum?: number;
+      pageSize?: number;
+      tags?: Array<{ key: string; value: string }>;
+      windowMinutes?: number;
+      start?: string;
+      end?: string;
+    } = {},
+  ): Promise<TraceListResponse> {
+    return this.request<TraceListResponse>(
+      'POST',
+      `/api/layer/${encodeURIComponent(layerKey)}/traces`,
+      body,
+    );
+  }
+
+  /** Trace detail by id. `source=zipkin` swaps to the Zipkin REST
+   *  path; default is native (v2/v3 picked automatically). */
+  traceDetail(traceId: string, source: 'native' | 'zipkin' = 'native'): Promise<TraceDetailResponse> {
+    const qs = new URLSearchParams({ source });
+    return this.request<TraceDetailResponse>(
+      'GET',
+      `/api/trace/${encodeURIComponent(traceId)}?${qs.toString()}`,
+    );
+  }
+
+  /** Log query — body shape mirrors OAP's `LogQueryCondition`. */
+  layerLogs(layerKey: string, body: LogQueryRequest & { service?: string } = {}): Promise<LogsResponse> {
+    return this.request<LogsResponse>(
+      'POST',
+      `/api/layer/${encodeURIComponent(layerKey)}/logs`,
+      body,
+    );
+  }
+
+  /** Tag autocomplete — list known tag keys for the duration window. */
+  traceTagKeys(windowMinutes: number = 30): Promise<{ keys: string[]; generatedAt: number; error?: string }> {
+    return this.request('GET', `/api/trace-tags/keys?windowMinutes=${windowMinutes}`);
+  }
+  /** Tag autocomplete — list values for a specific key. */
+  traceTagValues(
+    key: string,
+    windowMinutes: number = 30,
+  ): Promise<{ key: string; values: string[]; generatedAt: number; error?: string }> {
+    return this.request(
+      'GET',
+      `/api/trace-tags/values?key=${encodeURIComponent(key)}&windowMinutes=${windowMinutes}`,
+    );
+  }
+
+  /** Log facets — same body as `layerLogs`, but returns aggregated
+   *  level + service counts across a larger window-scoped sample. */
+  layerLogFacets(
+    layerKey: string,
+    body: LogQueryRequest & { service?: string; sampleSize?: number } = {},
+  ): Promise<LogFacetsResponse> {
+    return this.request<LogFacetsResponse>(
+      'POST',
+      `/api/layer/${encodeURIComponent(layerKey)}/logs/facets`,
+      body,
+    );
+  }
+
   /** List active instances for a service. The per-layer Instance
    *  dashboard surfaces a second selector below the service picker;
    *  this feeds it. Accepts the service id or name. */
diff --git a/apps/ui/src/components/charts/Sparkline.vue b/apps/ui/src/components/charts/Sparkline.vue
index 2affa81..6125aeb 100644
--- a/apps/ui/src/components/charts/Sparkline.vue
+++ b/apps/ui/src/components/charts/Sparkline.vue
@@ -35,27 +35,77 @@ const props = withDefaults(
     color?: string;
     /** Stroke width in px. */
     stroke?: number;
+    /**
+     * When true, the SVG renders at `width: 100%` of its container
+     * and `width` is used purely as the internal coordinate space
+     * (paths still draw in that resolution; the rendered chart
+     * stretches to fill). Use this whenever the Sparkline sits in a
+     * flex/grid cell whose width is dynamic.
+     */
+    fluid?: boolean;
+    /**
+     * Shared crosshair bucket index (0..values.length-1). When set,
+     * draws a vertical hairline + a dot at that bucket so two
+     * sparklines in the same panel can stay synced on hover. The
+     * parent owns the state; we just render the indicator and emit
+     * `bucket-hover` so siblings can follow.
+     */
+    crosshairBucket?: number | null;
   }>(),
   {
     width: 56,
     height: 14,
     color: 'var(--sw-accent)',
     stroke: 1.25,
+    fluid: false,
+    crosshairBucket: null,
   },
 );
 
+const emit = defineEmits<{
+  /** Mouse moved over the chart; argument is the nearest bucket
+   *  index. Parent panels use it to drive a shared crosshair across
+   *  sibling sparklines + a comparison tooltip. */
+  (e: 'bucket-hover', bucket: number): void;
+  /** Pointer left the chart. */
+  (e: 'bucket-leave'): void;
+}>();
+
+function onPointerMove(ev: PointerEvent): void {
+  const svg = ev.currentTarget as SVGSVGElement | null;
+  if (!svg) return;
+  const rect = svg.getBoundingClientRect();
+  if (rect.width === 0) return;
+  const x = ev.clientX - rect.left;
+  const frac = Math.max(0, Math.min(1, x / rect.width));
+  const n = props.values.length;
+  if (n < 2) return;
+  const bucket = Math.round(frac * (n - 1));
+  emit('bucket-hover', bucket);
+}
+function onPointerLeave(): void {
+  emit('bucket-leave');
+}
+
 interface PlotState {
   d: string;
   fillD: string;
   dotX: number | null;
   dotY: number | null;
+  /** Actual internal-coord width the path was computed against. May
+   *  differ from `props.width` in fluid mode where we scale up so
+   *  high-resolution stretching looks smooth. */
+  internalW: number;
+  /** Per-bucket (x, y) points for the line, in internal coord space.
+   *  Indexed positions match `props.values`. Null entries mark gaps. */
+  points: Array<{ x: number; y: number } | null>;
   empty: boolean;
 }
 
 const plot = computed<PlotState>(() => {
   const n = props.values.length;
   if (n < 2) {
-    return { d: '', fillD: '', dotX: null, dotY: null, empty: true };
+    return { d: '', fillD: '', dotX: null, dotY: null, internalW: props.width, points: [], empty: true };
   }
   let min = Infinity;
   let max = -Infinity;
@@ -67,12 +117,18 @@ const plot = computed<PlotState>(() => {
     if (v > max) max = v;
   }
   if (finiteCount < 2) {
-    return { d: '', fillD: '', dotX: null, dotY: null, empty: true };
+    return { d: '', fillD: '', dotX: null, dotY: null, internalW: props.width, points: [], empty: true };
   }
   const range = max - min || 1;
   // Inset a half-pixel so strokes don't get clipped by the SVG edge.
   const padY = props.stroke;
-  const w = props.width;
+  // In fluid mode the SVG stretches to its container width via
+  // `preserveAspectRatio="none"`. If we kept the default 56-unit
+  // internal width, the line would look blocky when n > 30 (xStep
+  // < 1, segments collapsing). Bump the internal coord space to a
+  // generous data-proportional value so the path is high-resolution
+  // and the stretch looks smooth.
+  const w = props.fluid ? Math.max(props.width, n * 12) : props.width;
   const h = props.height;
   const xStep = (w - 1) / (n - 1);
 
@@ -121,22 +177,39 @@ const plot = computed<PlotState>(() => {
     fillD: fillParts.join(' '),
     dotX: lastFinite?.x ?? null,
     dotY: lastFinite?.y ?? null,
+    internalW: w,
+    points,
     empty: false,
   };
 });
+
+/** The crosshair (x, y) on the path for `crosshairBucket`. Null when
+ *  no bucket is hovered or the bucket has no data. */
+const crosshairPos = computed<{ x: number; y: number } | null>(() => {
+  const b = props.crosshairBucket;
+  if (b === null || b === undefined) return null;
+  const pt = plot.value.points[b];
+  if (!pt) return null;
+  return pt;
+});
 </script>
 
 <template>
   <svg
     v-if="!plot.empty"
     class="sparkline"
-    :width="width"
+    :width="fluid ? '100%' : width"
     :height="height"
-    :viewBox="`0 0 ${width} ${height}`"
+    :viewBox="`0 0 ${plot.internalW} ${height}`"
+    :preserveAspectRatio="fluid ? 'none' : 'xMidYMid meet'"
     role="img"
     aria-label="trend"
+    @pointermove="onPointerMove"
+    @pointerleave="onPointerLeave"
   >
-    <path :d="plot.fillD" :fill="color" fill-opacity="0.12" stroke="none" />
+    <!-- Fill area below the line. Kept faint and skipped in fluid
+         mode where the line-only look reads better at chart size. -->
+    <path v-if="!fluid" :d="plot.fillD" :fill="color" fill-opacity="0.12" stroke="none" />
     <path
       :d="plot.d"
       fill="none"
@@ -144,16 +217,53 @@ const plot = computed<PlotState>(() => {
       :stroke-width="stroke"
       stroke-linecap="round"
       stroke-linejoin="round"
+      vector-effect="non-scaling-stroke"
     />
+    <!-- End dot only on the compact (non-fluid) variant — at chart
+         size it reads as an oddly thick endpoint. -->
     <circle
-      v-if="plot.dotX !== null && plot.dotY !== null"
+      v-if="!fluid && plot.dotX !== null && plot.dotY !== null"
       :cx="plot.dotX"
       :cy="plot.dotY"
       :r="stroke + 0.5"
       :fill="color"
     />
+    <!-- Shared crosshair driven by the parent's hover state. The
+         hairline is drawn full height; the dot marks the point on
+         this series' line. Both render in non-scaling stroke so
+         they stay 1px regardless of the SVG's stretch. -->
+    <template v-if="crosshairPos">
+      <line
+        :x1="crosshairPos.x"
+        :x2="crosshairPos.x"
+        :y1="0"
+        :y2="height"
+        stroke="var(--sw-fg-3)"
+        stroke-width="1"
+        stroke-dasharray="2 3"
+        vector-effect="non-scaling-stroke"
+        pointer-events="none"
+      />
+      <circle
+        :cx="crosshairPos.x"
+        :cy="crosshairPos.y"
+        r="2.6"
+        :fill="color"
+        stroke="var(--sw-bg-0)"
+        stroke-width="1"
+        vector-effect="non-scaling-stroke"
+        pointer-events="none"
+      />
+    </template>
   </svg>
-  <span v-else class="sparkline-empty" :style="{ width: `${width}px`, height: `${height}px` }">—</span>
+  <span
+    v-else
+    class="sparkline-empty"
+    :style="{
+      width: fluid ? '100%' : `${width}px`,
+      height: `${height}px`,
+    }"
+  >—</span>
 </template>
 
 <style scoped>
diff --git a/apps/ui/src/components/shell/AppShell.vue b/apps/ui/src/components/shell/AppShell.vue
index 6482234..8e832d5 100644
--- a/apps/ui/src/components/shell/AppShell.vue
+++ b/apps/ui/src/components/shell/AppShell.vue
@@ -18,6 +18,7 @@
 import { RouterView } from 'vue-router';
 import AppSidebar from './AppSidebar.vue';
 import AppTopbar from './AppTopbar.vue';
+import TracePopout from '@/components/trace/TracePopout.vue';
 </script>
 
 <template>
@@ -27,5 +28,8 @@ import AppTopbar from './AppTopbar.vue';
     <main class="sw-main">
       <RouterView />
     </main>
+    <!-- Global trace-id popout: any page can call useTracePopout().openTrace(id)
+         and this modal renders the waterfall + span detail. -->
+    <TracePopout />
   </div>
 </template>
diff --git a/apps/ui/src/components/shell/AppSidebar.vue b/apps/ui/src/components/shell/AppSidebar.vue
index a4ecf46..699a3dd 100644
--- a/apps/ui/src/components/shell/AppSidebar.vue
+++ b/apps/ui/src/components/shell/AppSidebar.vue
@@ -185,10 +185,10 @@ const sections: NavSection[] = [
           :class="{ 'is-active': isActive(`/layer/${L.key}`) }"
         >
           <span class="layer-dot" :style="{ background: L.color }" />
-          <span class="layer-name">{{ L.name }}</span>
           <span class="layer-count" :title="`${L.serviceCount} service${L.serviceCount === 1 ? '' : 's'} reporting`">
             {{ L.serviceCount }}
           </span>
+          <span class="layer-name">{{ L.name }}</span>
         </RouterLink>
 
         <!-- Multi-feature layer: expander row + children. -->
@@ -199,12 +199,12 @@ const sections: NavSection[] = [
           @click="toggleLayer(L.key)"
         >
           <span class="layer-dot" :style="{ background: L.color }" />
-          <span class="layer-name" :style="{ fontWeight: expandedLayer === L.key ? 600 : 500 }">
-            {{ L.name }}
-          </span>
           <span class="layer-count" :title="`${L.serviceCount} service${L.serviceCount === 1 ? '' : 's'} reporting`">
             {{ L.serviceCount }}
           </span>
+          <span class="layer-name" :style="{ fontWeight: expandedLayer === L.key ? 600 : 500 }">
+            {{ L.name }}
+          </span>
           <span class="caret" :class="{ open: expandedLayer === L.key }">
             <Icon name="caret" :size="10" />
           </span>
@@ -398,9 +398,13 @@ const sections: NavSection[] = [
   background: var(--sw-bg-2);
   border: 1px solid var(--sw-line-2);
   border-radius: 4px;
-  padding: 1px 6px;
-  min-width: 24px;
-  text-align: center;
+  padding: 1px 4px;
+  /* Fixed width so every row's name starts at the same column even when
+     counts swing wildly (1 ↔ 1000+). The chip is right-aligned inside
+     so the digit always sits flush against the name. */
+  width: 34px;
+  flex: 0 0 34px;
+  text-align: right;
 }
 .layer-row.is-active .layer-count {
   color: var(--sw-accent-2);
diff --git a/apps/ui/src/components/shell/AppTopbar.vue b/apps/ui/src/components/shell/AppTopbar.vue
index c2aef3b..d243cf6 100644
--- a/apps/ui/src/components/shell/AppTopbar.vue
+++ b/apps/ui/src/components/shell/AppTopbar.vue
@@ -15,10 +15,11 @@
   limitations under the License.
 -->
 <script setup lang="ts">
-import { computed } from 'vue';
+import { computed, ref, watch } from 'vue';
 import { RouterLink, useRoute } from 'vue-router';
 import Icon from '@/components/icons/Icon.vue';
 import { useOapInfo } from '@/composables/useOapInfo';
+import { useAutoRefreshStore } from '@/stores/autoRefresh';
 
 const route = useRoute();
 
@@ -68,6 +69,76 @@ const tzMismatch = computed<boolean>(() => {
   const browserMin = -new Date().getTimezoneOffset();
   return browserMin !== serverTzMin.value;
 });
+
+/**
+ * Routes that own their own time range — the global topbar picker +
+ * refresh button get disabled (greyed + non-clickable) so the
+ * operator knows the page's local picker is the source of truth.
+ *
+ * Add more routes here as Logs / Traces / etc. each opt out of the
+ * global rolling window in favour of a per-page picker.
+ */
+const TIME_RANGE_OPT_OUT = [/^\/layer\/[^/]+\/trace$/];
+const ownsTimeRange = computed<boolean>(() => TIME_RANGE_OPT_OUT.some((r) => r.test(route.path)));
+const globalTimeTooltip = computed<string>(() => {
+  if (ownsTimeRange.value) {
+    return 'This page uses its own time range — disable the page picker to use the global one.';
+  }
+  return `Browser local time · ${localTzLabel.value}`;
+});
+
+/**
+ * Auto-refresh: store drives the ticker; the topbar drives the UI
+ * (countdown + spinning icon + interval dropdown). When the operator
+ * lands on an opt-out route the ticker suspends; on leaving the
+ * route it resumes + fires one immediate tick so the underlying page
+ * gets fresh data right away.
+ */
+const auto = useAutoRefreshStore();
+watch(
+  ownsTimeRange,
+  (now) => {
+    if (now) auto.suspend();
+    else auto.resume();
+  },
+  { immediate: true },
+);
+
+const REFRESH_PRESETS: Array<{ label: string; sec: number | null }> = [
+  { label: 'Off', sec: null },
+  { label: '5s', sec: 5 },
+  { label: '15s', sec: 15 },
+  { label: '30s', sec: 30 },
+  { label: '1m', sec: 60 },
+  { label: '5m', sec: 300 },
+];
+const refreshMenuOpen = ref(false);
+const refreshClusterEl = ref<HTMLElement | null>(null);
+function pickRefresh(sec: number | null): void {
+  auto.setInterval(sec);
+  refreshMenuOpen.value = false;
+}
+function onWindowClickClose(ev: MouseEvent): void {
+  if (!refreshMenuOpen.value) return;
+  const el = refreshClusterEl.value;
+  if (el && !el.contains(ev.target as Node)) {
+    refreshMenuOpen.value = false;
+  }
+}
+if (typeof window !== 'undefined') {
+  window.addEventListener('click', onWindowClickClose);
+}
+const refreshLabel = computed<string>(() => {
+  if (ownsTimeRange.value) return 'Paused';
+  if (auto.intervalSec === null) return 'Off';
+  if (auto.secondsUntilNext === null) return '—';
+  return `${auto.secondsUntilNext}s`;
+});
+const refreshTooltip = computed<string>(() => {
+  if (ownsTimeRange.value) return 'Auto-refresh paused on this page';
+  if (auto.intervalSec === null) return 'Auto-refresh off · click to refresh now';
+  return `Auto-refresh every ${auto.intervalSec}s · ${auto.secondsUntilNext ?? '—'}s remaining · click to refresh now`;
+});
 </script>
 
 <template>
@@ -94,18 +165,121 @@ const tzMismatch = computed<boolean>(() => {
           {{ tzOffsetLabel }}
         </span>
       </RouterLink>
-      <div class="sw-btn" :title="`Browser local time · ${localTzLabel}`">
+      <div class="sw-btn" :class="{ 'is-disabled': ownsTimeRange }" :title="globalTimeTooltip">
         <Icon name="clock" :size="12" />
-        <span>Last 30 minutes</span>
+        <span>{{ ownsTimeRange ? 'Page time range' : 'Last 30 minutes' }}</span>
         <Icon name="caret" :size="10" />
       </div>
-      <div class="sw-btn is-icon"><Icon name="refresh" :size="12" /></div>
+      <!-- Auto-refresh cluster: countdown + spinning button on the
+           left, dropdown caret on the right. Click the icon to
+           refresh now; click the caret to pick an interval. -->
+      <div ref="refreshClusterEl" class="refresh-cluster" :class="{ 'is-disabled': ownsTimeRange }">
+        <button
+          type="button"
+          class="sw-btn is-icon refresh-now"
+          :class="{ spinning: auto.effectiveEnabled }"
+          :title="refreshTooltip"
+          :disabled="ownsTimeRange"
+          @click="auto.refreshNow()"
+        ><Icon name="refresh" :size="12" /></button>
+        <span class="refresh-countdown mono" :title="refreshTooltip">{{ refreshLabel }}</span>
+        <button
+          type="button"
+          class="sw-btn refresh-caret"
+          :title="'Pick refresh interval'"
+          :disabled="ownsTimeRange"
+          @click="refreshMenuOpen = !refreshMenuOpen"
+        ><Icon name="caret" :size="10" /></button>
+        <transition name="rf-menu">
+          <ul v-if="refreshMenuOpen" class="rf-menu">
+            <li
+              v-for="p in REFRESH_PRESETS"
+              :key="String(p.sec)"
+              :class="{ on: auto.intervalSec === p.sec }"
+              @click="pickRefresh(p.sec)"
+            >{{ p.label }}</li>
+          </ul>
+        </transition>
+      </div>
       <div class="sw-btn is-icon"><Icon name="bell" :size="12" /></div>
     </div>
   </header>
 </template>
 
 <style scoped>
+/* Disabled state for global time-range / refresh chips when the
+   current page owns its own time range. Greys out without removing
+   the chip so the operator still sees the affordance + tooltip. */
+.sw-btn.is-disabled {
+  opacity: 0.45;
+  pointer-events: none;
+  filter: grayscale(0.6);
+}
+
+/* Auto-refresh cluster */
+.refresh-cluster {
+  position: relative;
+  display: inline-flex;
+  align-items: center;
+  gap: 2px;
+}
+.refresh-cluster.is-disabled {
+  opacity: 0.45;
+  filter: grayscale(0.6);
+}
+.refresh-now {
+  cursor: pointer;
+}
+.refresh-now.spinning :deep(svg) {
+  animation: refresh-spin 1.6s linear infinite;
+  transform-origin: 50% 50%;
+}
+@keyframes refresh-spin {
+  to { transform: rotate(360deg); }
+}
+.refresh-countdown {
+  font-size: 10.5px;
+  color: var(--sw-fg-2);
+  min-width: 28px;
+  text-align: center;
+  font-variant-numeric: tabular-nums;
+}
+.refresh-caret {
+  cursor: pointer;
+  padding: 0 4px;
+  min-width: auto;
+}
+.rf-menu {
+  position: absolute;
+  top: 100%;
+  right: 0;
+  margin-top: 4px;
+  list-style: none;
+  padding: 4px;
+  background: var(--sw-bg-1);
+  border: 1px solid var(--sw-line);
+  border-radius: 6px;
+  min-width: 96px;
+  z-index: 10;
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
+}
+.rf-menu li {
+  padding: 4px 10px;
+  font-size: 11px;
+  color: var(--sw-fg-1);
+  cursor: pointer;
+  border-radius: 4px;
+  font-variant-numeric: tabular-nums;
+}
+.rf-menu li:hover { background: var(--sw-bg-2); }
+.rf-menu li.on { background: var(--sw-accent-soft); color: var(--sw-accent-2); font-weight: 600; }
+.rf-menu-enter-from, .rf-menu-leave-to {
+  opacity: 0;
+  transform: translateY(-4px);
+}
+.rf-menu-enter-active, .rf-menu-leave-active {
+  transition: opacity 0.15s ease, transform 0.15s ease;
+}
 .oap-chip {
   text-decoration: none;
   font-family: var(--sw-mono);
diff --git a/apps/ui/src/components/trace/TracePopout.vue b/apps/ui/src/components/trace/TracePopout.vue
new file mode 100644
index 0000000..40fb6c4
--- /dev/null
+++ b/apps/ui/src/components/trace/TracePopout.vue
@@ -0,0 +1,828 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<!--
+  Global trace popout — opened by URL `?traceId=<id>` from anywhere
+  in the app (shareable links, cross-trace refs, log-row trace links,
+  alarm payloads). Distinct from the per-layer trace tab's *in-page*
+  detail layout: the popout is single-focus + horizontal split, while
+  the in-page detail uses a folded list rail + tabbed multi-view.
+
+  Popout layout (intentionally different from in-page):
+    - Waterfall on the left (always List view — no Tree/Table/Stats
+      tabs; the popout is a glance-link experience).
+    - Span detail in a sticky right panel (no nested modal). Click
+      a span row in the waterfall → the right panel updates. Cross-
+      trace refs there hot-swap the popout to the linked trace.
+-->
+<script setup lang="ts">
+import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
+import type { NativeSpan, TraceAttachedEvent, TraceLogEntry } from '@/api/client';
+import { useTraceDetail } from '@/composables/useLayerTraces';
+import { useTracePopout } from '@/composables/useTracePopout';
+import { componentIconOrNull } from '@/composables/useTopologyIcons';
+import { fmtMetric } from '@/utils/formatters';
+
+const { openTraceId, openTrace, closeTrace } = useTracePopout();
+
+const sourceRef = computed<'native'>(() => 'native');
+const traceIdRef = computed<string | null>(() => openTraceId.value);
+const { nativeDetail, isFetching } = useTraceDetail(traceIdRef, sourceRef);
+
+const spans = computed<NativeSpan[]>(() => nativeDetail.value?.spans ?? []);
+
+// Reset selected span when the trace changes (cross-trace ref jump).
+const selectedSpan = ref<NativeSpan | null>(null);
+watch(traceIdRef, () => { selectedSpan.value = null; });
+
+interface WaterfallRow {
+  span: NativeSpan;
+  depth: number;
+  startOffset: number;
+  duration: number;
+}
+const waterfall = computed<WaterfallRow[]>(() => {
+  const s = spans.value;
+  if (s.length === 0) return [];
+  const minStart = Math.min(...s.map((x) => x.startTime));
+  const key = (segId: string, spanId: number) => `${segId}/${spanId}`;
+  const byKey = new Map(s.map((x) => [key(x.segmentId, x.spanId), x]));
+  const childMap = new Map<string, NativeSpan[]>();
+  const roots: NativeSpan[] = [];
+  for (const x of s) {
+    if (x.parentSpanId === -1) {
+      const r = x.refs?.[0];
+      const pk = r ? key(r.parentSegmentId, r.parentSpanId) : null;
+      if (pk && byKey.has(pk)) {
+        const arr = childMap.get(pk) ?? [];
+        arr.push(x);
+        childMap.set(pk, arr);
+      } else {
+        roots.push(x);
+      }
+    } else {
+      const pk = key(x.segmentId, x.parentSpanId);
+      const arr = childMap.get(pk) ?? [];
+      arr.push(x);
+      childMap.set(pk, arr);
+    }
+  }
+  const out: WaterfallRow[] = [];
+  function walk(x: NativeSpan, depth: number): void {
+    const ck = key(x.segmentId, x.spanId);
+    const kids = childMap.get(ck) ?? [];
+    out.push({
+      span: x,
+      depth,
+      startOffset: x.startTime - minStart,
+      duration: Math.max(0, x.endTime - x.startTime),
+    });
+    for (const c of kids) walk(c, depth + 1);
+  }
+  for (const r of roots) walk(r, 0);
+  return out;
+});
+const totalDuration = computed(() => {
+  const r = waterfall.value;
+  if (r.length === 0) return 0;
+  return Math.max(...r.map((x) => x.startOffset + x.duration));
+});
+const rootStart = computed(() => {
+  const s = spans.value;
+  return s.length > 0 ? Math.min(...s.map((x) => x.startTime)) : null;
+});
+
+// Service color palette (matches the trace tab so the operator sees
+// the same colours across the in-page detail and the popout).
+const SERVICE_PALETTE = [
+  'var(--sw-accent)', 'var(--sw-info)', 'var(--sw-cyan)', 'var(--sw-purple)',
+  'var(--sw-ok)', 'var(--sw-warn)', 'var(--sw-pink)',
+  '#a78bfa', '#fb7185', '#34d399', '#fbbf24', '#60a5fa',
+];
+function hashString(s: string): number {
+  let h = 2166136261;
+  for (let i = 0; i < s.length; i++) {
+    h ^= s.charCodeAt(i);
+    h = Math.imul(h, 16777619);
+  }
+  return (h >>> 0);
+}
+const serviceColors = computed<Map<string, string>>(() => {
+  const m = new Map<string, string>();
+  for (const s of spans.value) {
+    if (!m.has(s.serviceCode)) {
+      m.set(s.serviceCode, SERVICE_PALETTE[hashString(s.serviceCode) % SERVICE_PALETTE.length]);
+    }
+  }
+  return m;
+});
+function serviceColor(c: string): string {
+  return serviceColors.value.get(c) ?? 'var(--sw-fg-2)';
+}
+function kindColor(type: string | null | undefined): string {
+  const t = (type ?? '').toUpperCase();
+  if (t.includes('SERVER')) return 'var(--sw-accent)';
+  if (t.includes('CLIENT')) return 'var(--sw-info)';
+  if (t.includes('PRODUCER') || t.includes('CONSUMER')) return 'var(--sw-purple)';
+  if (t.includes('DATABASE') || t.includes('SQL') || t.includes('CACHE')) return 'var(--sw-cyan)';
+  return 'var(--sw-fg-2)';
+}
+function fmtMs(v: number | null | undefined): string {
+  if (v === null || v === undefined) return '—';
+  return `${fmtMetric(v)} ms`;
+}
+function fmtDateTime(ts: number | null | undefined): string {
+  if (!ts || !Number.isFinite(ts)) return '—';
+  const d = new Date(ts);
+  const pad = (n: number) => String(n).padStart(2, '0');
+  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
+}
+function fmtAttachedTs(t: { seconds: number; nanos: number }): string {
+  const ms = t.seconds * 1000 + t.nanos / 1_000_000;
+  return fmtDateTime(ms) + '.' + String(t.nanos).padStart(9, '0').slice(0, 6);
+}
+
+// Copy affordances.
+const copyFlash = ref<'id' | 'url' | null>(null);
+let copyFlashTimer: ReturnType<typeof setTimeout> | null = null;
+function flashCopy(kind: 'id' | 'url'): void {
+  copyFlash.value = kind;
+  if (copyFlashTimer) clearTimeout(copyFlashTimer);
+  copyFlashTimer = setTimeout(() => { copyFlash.value = null; }, 1400);
+}
+function copyId(): void {
+  if (!openTraceId.value) return;
+  navigator.clipboard?.writeText(openTraceId.value).then(() => flashCopy('id'), () => {});
+}
+function copyShareableUrl(): void {
+  if (!openTraceId.value) return;
+  navigator.clipboard?.writeText(window.location.href).then(() => flashCopy('url'), () => {});
+}
+
+function selectSpan(s: NativeSpan): void { selectedSpan.value = s; }
+function clearSpan(): void { selectedSpan.value = null; }
+
+/** Esc-to-close. If a span is selected the first Esc closes the
+ *  side panel (keeps the popout open). A second Esc — or Esc with
+ *  no span selected — closes the popout itself. Always listening
+ *  while the component is mounted; the no-op guard prevents
+ *  intercepting Esc when the popout isn't open. */
+function onKeyDown(e: KeyboardEvent): void {
+  if (e.key !== 'Escape') return;
+  if (!openTraceId.value) return;
+  if (selectedSpan.value) {
+    selectedSpan.value = null;
+    e.preventDefault();
+    e.stopPropagation();
+  } else {
+    closeTrace();
+    e.preventDefault();
+    e.stopPropagation();
+  }
+}
+onMounted(() => window.addEventListener('keydown', onKeyDown, true));
+onBeforeUnmount(() => window.removeEventListener('keydown', onKeyDown, true));
+function isSelfRef(refTraceId: string): boolean { return refTraceId === openTraceId.value; }
+function jumpToTrace(id: string): void { openTrace(id); }
+function nativeSpanError(s: NativeSpan): boolean { return s.isError; }
+</script>
+
+<template>
+  <div v-if="openTraceId" class="tp-backdrop" @click.self="closeTrace">
+    <article class="tp-card sw-card">
+      <header class="tp-head">
+        <div class="tp-title">
+          <span class="dim">Trace</span>
+          <span class="mono trace-id-text" :title="openTraceId">{{ openTraceId }}</span>
+          <button class="sw-btn small ghost copy-btn" type="button" title="Copy trace id" @click="copyId">⧉ id</button>
+          <button class="sw-btn small ghost copy-btn" type="button" title="Copy shareable URL" @click="copyShareableUrl">⧉ url</button>
+          <transition name="copy-flash">
+            <span v-if="copyFlash" class="copy-flash-chip">{{ copyFlash === 'url' ? 'url copied' : 'id copied' }}</span>
+          </transition>
+          <span v-if="isFetching" class="hint">loading…</span>
+        </div>
+        <button class="sw-btn small ghost" type="button" title="Close" @click="closeTrace">×</button>
+      </header>
+
+      <div class="tp-kpis">
+        <div><div class="kpi-label">started</div><div class="kpi-val">{{ fmtDateTime(rootStart) }}</div></div>
+        <div><div class="kpi-label">duration</div><div class="kpi-val">{{ fmtMs(totalDuration) }}</div></div>
+        <div><div class="kpi-label">spans</div><div class="kpi-val">{{ waterfall.length }}</div></div>
+        <div><div class="kpi-label">services</div><div class="kpi-val">{{ serviceColors.size }}</div></div>
+      </div>
+
+      <div v-if="serviceColors.size > 0" class="tp-svc-legend">
+        <span v-for="[code, color] in serviceColors" :key="code" class="svc-chip">
+          <span class="svc-swatch" :style="{ background: color }" />
+          <span class="mono">{{ code }}</span>
+        </span>
+      </div>
+
+      <!-- Body: waterfall (left) + sticky span detail panel (right).
+           Distinct from the per-layer trace tab's modal-on-tabbed
+           detail; here the right panel is always visible, swapping
+           contents on span row click. -->
+      <div class="tp-body">
+        <div v-if="waterfall.length === 0 && !isFetching" class="tp-empty">no span data</div>
+        <div v-else class="tp-split" :class="{ 'no-selection': !selectedSpan }">
+          <div class="tp-waterfall">
+            <!-- Time axis at the top of the waterfall column. -->
+            <div class="tp-time-axis">
+              <span class="t-tick first">0</span>
+              <span class="t-tick">{{ fmtMs(totalDuration * 0.25) }}</span>
+              <span class="t-tick">{{ fmtMs(totalDuration * 0.5) }}</span>
+              <span class="t-tick">{{ fmtMs(totalDuration * 0.75) }}</span>
+              <span class="t-tick last">{{ fmtMs(totalDuration) }}</span>
+            </div>
+            <div
+              v-for="row in waterfall"
+              :key="`${row.span.segmentId}/${row.span.spanId}`"
+              class="tp-row"
+              :class="{ err: row.span.isError, on: selectedSpan && selectedSpan.segmentId === row.span.segmentId && selectedSpan.spanId === row.span.spanId }"
+              @click="selectSpan(row.span)"
+            >
+              <div class="tp-track">
+                <span class="t-grid q1" />
+                <span class="t-grid q2" />
+                <span class="t-grid q3" />
+                <div
+                  class="tp-bar"
+                  :style="{
+                    left: totalDuration > 0 ? (row.startOffset / totalDuration * 100) + '%' : '0%',
+                    width: totalDuration > 0 ? Math.max(0.8, row.duration / totalDuration * 100) + '%' : '0%',
+                    background: serviceColor(row.span.serviceCode),
+                    borderColor: row.span.isError ? 'var(--sw-err)' : 'transparent',
+                  }"
+                >
+                  <span class="bar-inner">
+                    <span
+                      class="status-flag sm"
+                      :class="row.span.isError ? 'flag-err' : 'flag-ok'"
+                      :title="row.span.isError ? 'Span errored' : 'Span OK'"
+                    ><span class="flag-dot" /></span>
+                    <img
+                      v-if="componentIconOrNull(row.span.component)"
+                      class="comp-icon"
+                      :src="componentIconOrNull(row.span.component) ?? ''"
+                      :alt="row.span.component || ''"
+                      :title="row.span.component || ''"
+                    />
+                    <svg
+                      v-else
+                      class="comp-icon comp-icon-generic"
+                      viewBox="0 0 18 18"
+                      :aria-label="row.span.component || 'generic span'"
+                    >
+                      <rect x="3" y="4.5" width="12" height="3" rx="1.5" fill="currentColor" opacity="0.45" />
+                      <rect x="5" y="10.5" width="10" height="3" rx="1.5" fill="currentColor" opacity="0.85" />
+                    </svg>
+                    <span class="bar-text mono">
+                      <span class="bar-svc" :title="row.span.serviceCode">{{ row.span.serviceCode }}</span>
+                      <span class="bar-name" :title="row.span.endpointName || row.span.component || row.span.peer || '—'">{{ row.span.endpointName || row.span.component || row.span.peer || '—' }}</span>
+                    </span>
+                    <span
+                      v-if="row.span.attachedEvents && row.span.attachedEvents.length > 0"
+                      class="evt-badge inside"
+                      :title="`${row.span.attachedEvents.length} attached event${row.span.attachedEvents.length === 1 ? '' : 's'}`"
+                    >
+                      <span class="evt-flag">⚑</span>
+                      <span class="evt-count">{{ row.span.attachedEvents.length }}</span>
+                    </span>
+                  </span>
+                </div>
+                <span
+                  class="bar-dur-outside mono"
+                  :style="{
+                    left: totalDuration > 0
+                      ? `calc(${((row.startOffset + row.duration) / totalDuration) * 100}% + 6px)`
+                      : '0%',
+                  }"
+                >{{ fmtMs(row.duration) }}</span>
+              </div>
+            </div>
+          </div>
+
+          <!-- Span detail panel — replaces the nested modal used in
+               the per-layer trace tab. Shown when a span is picked;
+               hidden otherwise to let the waterfall claim full width. -->
+          <aside v-if="selectedSpan" class="tp-span-panel">
+            <header class="tp-span-head">
+              <h5>Span detail</h5>
+              <button class="sw-btn small ghost" type="button" @click="clearSpan">×</button>
+            </header>
+            <div class="tp-span-body">
+              <section class="sd-section">
+                <h6>Meta</h6>
+                <dl class="kv">
+                  <dt>Service</dt>
+                  <dd class="mono" :style="{ color: serviceColor(selectedSpan.serviceCode) }">
+                    <span class="svc-swatch inline" :style="{ background: serviceColor(selectedSpan.serviceCode) }" />
+                    {{ selectedSpan.serviceCode }}
+                  </dd>
+                  <dt>Instance</dt><dd class="mono wba">{{ selectedSpan.serviceInstanceName }}</dd>
+                  <dt>Endpoint</dt><dd class="mono wba">{{ selectedSpan.endpointName || '—' }}</dd>
+                  <dt>Type</dt><dd><span class="tp-kind" :style="{ background: kindColor(selectedSpan.type) }">{{ selectedSpan.type }}</span></dd>
+                  <dt>Component</dt><dd class="mono">{{ selectedSpan.component || '—' }}</dd>
+                  <dt>Peer</dt><dd class="mono wba">{{ selectedSpan.peer || '—' }}</dd>
+                  <dt>Layer</dt><dd class="mono dim">{{ selectedSpan.layer || '—' }}</dd>
+                  <dt>Start</dt><dd class="mono">{{ fmtDateTime(selectedSpan.startTime) }}</dd>
+                  <dt>Duration</dt><dd class="mono">{{ fmtMs(selectedSpan.endTime - selectedSpan.startTime) }}</dd>
+                  <dt>Error</dt><dd><span class="status-flag" :class="nativeSpanError(selectedSpan) ? 'flag-err' : 'flag-ok'"><span class="flag-dot" />{{ nativeSpanError(selectedSpan) ? 'true' : 'false' }}</span></dd>
+                </dl>
+              </section>
+              <section
+                v-if="(selectedSpan.refs ?? []).some((r) => !isSelfRef(r.traceId))"
+                class="sd-section"
+              >
+                <h6>Cross-trace refs</h6>
+                <table class="kv-table">
+                  <thead><tr><th>Trace ID</th><th class="num">Parent span</th></tr></thead>
+                  <tbody>
+                    <template v-for="(r, i) in selectedSpan.refs" :key="i">
+                      <tr v-if="!isSelfRef(r.traceId)">
+                        <td>
+                          <button class="trace-link mono" type="button" @click="jumpToTrace(r.traceId)">{{ r.traceId }} ↗</button>
+                        </td>
+                        <td class="num mono">{{ r.parentSpanId }}</td>
+                      </tr>
+                    </template>
+                  </tbody>
+                </table>
+              </section>
+              <section v-if="selectedSpan.tags && selectedSpan.tags.length > 0" class="sd-section">
+                <h6>Tags</h6>
+                <dl class="kv">
+                  <template v-for="(t, i) in selectedSpan.tags" :key="i">
+                    <dt class="mono">{{ t.key }}</dt>
+                    <dd class="mono wba">{{ t.value }}</dd>
+                  </template>
+                </dl>
+              </section>
+              <section v-if="selectedSpan.logs && selectedSpan.logs.length > 0" class="sd-section">
+                <h6>Logs</h6>
+                <div v-for="(log, i) in (selectedSpan.logs as TraceLogEntry[])" :key="i" class="span-log">
+                  <div class="span-log-time mono dim">{{ fmtDateTime(log.time) }}</div>
+                  <dl class="kv">
+                    <template v-for="(d, j) in log.data" :key="j">
+                      <dt class="mono">{{ d.key }}</dt>
+                      <dd><pre class="mono pre wba">{{ d.value }}</pre></dd>
+                    </template>
+                  </dl>
+                </div>
+              </section>
+              <section v-if="selectedSpan.attachedEvents && selectedSpan.attachedEvents.length > 0" class="sd-section">
+                <h6>Attached Events</h6>
+                <div v-for="(ev, i) in (selectedSpan.attachedEvents as TraceAttachedEvent[])" :key="i" class="span-event">
+                  <div class="span-event-head">
+                    <span class="mono">{{ ev.event }}</span>
+                    <span class="dim mono">{{ fmtAttachedTs(ev.startTime) }} → {{ fmtAttachedTs(ev.endTime) }}</span>
+                  </div>
+                  <dl v-if="ev.summary && ev.summary.length > 0" class="kv">
+                    <template v-for="(s, j) in ev.summary" :key="`s${j}`">
+                      <dt class="mono">{{ s.key }}</dt>
+                      <dd class="mono wba">{{ s.value }}</dd>
+                    </template>
+                  </dl>
+                  <dl v-if="ev.tags && ev.tags.length > 0" class="kv">
+                    <template v-for="(t, j) in ev.tags" :key="`t${j}`">
+                      <dt class="mono dim">tag · {{ t.key }}</dt>
+                      <dd class="mono wba">{{ t.value }}</dd>
+                    </template>
+                  </dl>
+                </div>
+              </section>
+            </div>
+          </aside>
+        </div>
+      </div>
+    </article>
+  </div>
+</template>
+
+<style scoped>
+.tp-backdrop {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.66);
+  z-index: 999;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 4vh 0;
+}
+.tp-card {
+  width: 80vw;
+  height: 92vh;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+.tp-head {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 10px 14px;
+  border-bottom: 1px solid var(--sw-line);
+  flex: 0 0 auto;
+  flex-wrap: wrap;
+}
+.tp-title { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
+.tp-title .dim { font-size: 12px; font-weight: 500; color: var(--sw-fg-3); }
+.trace-id-text {
+  font-family: var(--sw-mono);
+  color: var(--sw-fg-1);
+  font-size: 12px;
+  max-width: 360px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.tp-kpis {
+  display: flex;
+  gap: 28px;
+  padding: 8px 14px;
+  border-bottom: 1px solid var(--sw-line);
+  flex: 0 0 auto;
+}
+.kpi-label {
+  font-size: 9.5px;
+  text-transform: uppercase;
+  letter-spacing: 0.06em;
+  color: var(--sw-fg-3);
+}
+.kpi-val {
+  font-family: var(--sw-mono);
+  font-size: 14px;
+  color: var(--sw-fg-0);
+  font-weight: 700;
+}
+.tp-svc-legend {
+  display: flex;
+  gap: 6px;
+  flex-wrap: wrap;
+  padding: 6px 14px;
+  border-bottom: 1px solid var(--sw-line);
+  background: var(--sw-bg-1);
+  flex: 0 0 auto;
+}
+.svc-chip {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 10.5px;
+  color: var(--sw-fg-2);
+  padding: 1px 6px 1px 4px;
+  border: 1px solid var(--sw-line-2);
+  border-radius: 10px;
+  background: var(--sw-bg-2);
+}
+.svc-swatch {
+  width: 8px;
+  height: 8px;
+  border-radius: 2px;
+  flex: 0 0 auto;
+}
+.svc-swatch.inline {
+  display: inline-block;
+  margin-right: 4px;
+  vertical-align: middle;
+}
+.tp-empty {
+  padding: 40px;
+  text-align: center;
+  color: var(--sw-fg-3);
+  font-size: 12px;
+}
+.tp-body { flex: 1; overflow: hidden; min-height: 0; display: flex; }
+
+/* Horizontal split: waterfall + sticky span panel. */
+.tp-split {
+  display: grid;
+  grid-template-columns: 1fr 420px;
+  gap: 0;
+  flex: 1;
+  min-height: 0;
+  width: 100%;
+}
+.tp-split.no-selection { grid-template-columns: 1fr; }
+
+/* Waterfall */
+.tp-waterfall {
+  padding: 6px 0;
+  overflow-y: auto;
+  min-width: 0;
+}
+.tp-time-axis {
+  position: sticky;
+  top: 0;
+  z-index: 2;
+  display: flex;
+  justify-content: space-between;
+  padding: 4px 12px;
+  background: var(--sw-bg-1);
+  border-bottom: 1px solid var(--sw-line);
+  font-family: var(--sw-mono);
+  font-size: 10px;
+  color: var(--sw-fg-3);
+}
+.t-tick { white-space: nowrap; }
+.t-tick.first { text-align: left; }
+.t-tick.last { text-align: right; }
+.tp-row {
+  padding: 3px 12px;
+  cursor: pointer;
+}
+.tp-row:hover { background: var(--sw-bg-2); }
+.tp-row.err { background: rgba(239, 68, 68, 0.06); }
+.tp-row.on { background: var(--sw-accent-soft); }
+.tp-track {
+  position: relative;
+  height: 22px;
+  background: transparent;
+}
+.t-grid {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  width: 1px;
+  background: var(--sw-line);
+  opacity: 0.55;
+  pointer-events: none;
+}
+.t-grid.q1 { left: 25%; }
+.t-grid.q2 { left: 50%; }
+.t-grid.q3 { left: 75%; }
+.tp-bar {
+  position: absolute;
+  top: 2px;
+  bottom: 2px;
+  border-radius: 3px;
+  border: 1px solid transparent;
+  display: flex;
+  align-items: center;
+  overflow: hidden;
+  cursor: pointer;
+  transition: filter 0.12s ease;
+}
+.tp-bar:hover { filter: brightness(1.12); }
+.bar-inner {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 0 6px;
+  height: 100%;
+  width: 100%;
+  min-width: 0;
+}
+.bar-text {
+  display: inline-flex;
+  align-items: baseline;
+  gap: 6px;
+  min-width: 0;
+  flex: 1 1 auto;
+  overflow: hidden;
+  color: var(--sw-bg-0);
+  text-shadow: 0 0 1px rgba(0, 0, 0, 0.25);
+}
+.bar-svc {
+  font-weight: 700;
+  font-size: 10.5px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  flex: 0 1 auto;
+  max-width: 40%;
+  opacity: 0.9;
+}
+.bar-name {
+  flex: 1 1 auto;
+  min-width: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  font-size: 11px;
+  font-weight: 500;
+}
+.comp-icon {
+  width: 18px;
+  height: 18px;
+  flex: 0 0 auto;
+  object-fit: contain;
+  background: transparent;
+}
+.comp-icon-generic { color: var(--sw-fg-2); }
+.bar-inner .comp-icon-generic { color: rgba(0, 0, 0, 0.72); }
+.bar-inner .status-flag.sm {
+  background: rgba(255, 255, 255, 0.35);
+  color: var(--sw-bg-0);
+}
+.bar-inner .status-flag.flag-err {
+  background: var(--sw-err);
+  color: #fff;
+}
+.evt-badge {
+  display: inline-flex;
+  align-items: center;
+  gap: 3px;
+  height: 16px;
+  padding: 0 5px;
+  border-radius: 8px;
+  background: var(--sw-accent-soft);
+  color: var(--sw-accent-2);
+  font-size: 9.5px;
+  font-weight: 700;
+  flex: 0 0 auto;
+}
+.evt-badge.inside {
+  background: rgba(255, 255, 255, 0.85);
+  color: var(--sw-bg-0);
+}
+.evt-flag { font-size: 10px; line-height: 1; }
+.evt-count { font-family: var(--sw-mono); }
+.bar-dur-outside {
+  position: absolute;
+  top: 50%;
+  transform: translateY(-50%);
+  font-size: 10px;
+  color: var(--sw-fg-2);
+  white-space: nowrap;
+  pointer-events: none;
+}
+
+/* Span panel (sticky right side) */
+.tp-span-panel {
+  border-left: 1px solid var(--sw-line);
+  background: var(--sw-bg-1);
+  display: flex;
+  flex-direction: column;
+  min-width: 0;
+  overflow: hidden;
+}
+.tp-span-head {
+  display: flex;
+  align-items: center;
+  padding: 8px 12px;
+  border-bottom: 1px solid var(--sw-line);
+  flex: 0 0 auto;
+}
+.tp-span-head h5 {
+  margin: 0;
+  font-size: 11.5px;
+  font-weight: 600;
+  color: var(--sw-fg-0);
+  flex: 1;
+}
+.tp-span-body {
+  flex: 1;
+  overflow-y: auto;
+  padding: 10px 12px 14px;
+}
+.sd-section { margin-bottom: 14px; }
+.sd-section h6 {
+  margin: 0 0 6px;
+  font-size: 9.5px;
+  text-transform: uppercase;
+  letter-spacing: 0.08em;
+  color: var(--sw-accent);
+  font-weight: 700;
+}
+.kv {
+  display: grid;
+  grid-template-columns: 100px 1fr;
+  gap: 4px 10px;
+  margin: 0;
+  font-size: 11px;
+}
+.kv dt { color: var(--sw-fg-3); font-size: 10.5px; }
+.kv dd { margin: 0; color: var(--sw-fg-1); }
+.kv-table {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 11px;
+}
+.kv-table th {
+  text-align: left;
+  font-size: 9.5px;
+  text-transform: uppercase;
+  letter-spacing: 0.06em;
+  color: var(--sw-fg-3);
+  font-weight: 600;
+  padding: 4px 6px;
+  border-bottom: 1px solid var(--sw-line);
+}
+.kv-table th.num { text-align: right; }
+.kv-table td { padding: 4px 6px; border-bottom: 1px solid var(--sw-line); }
+.kv-table td.num { text-align: right; }
+.trace-link {
+  background: transparent;
+  border: none;
+  color: var(--sw-cyan);
+  cursor: pointer;
+  padding: 0;
+  font: inherit;
+  text-decoration: underline;
+  text-underline-offset: 2px;
+}
+.trace-link:hover { color: var(--sw-accent); }
+.span-log, .span-event {
+  border: 1px solid var(--sw-line);
+  border-radius: 4px;
+  padding: 6px 10px;
+  margin-bottom: 6px;
+  background: var(--sw-bg-0);
+}
+.span-log-time { font-size: 10px; margin-bottom: 4px; }
+.span-event-head {
+  display: flex;
+  justify-content: space-between;
+  gap: 10px;
+  font-size: 11px;
+  margin-bottom: 4px;
+}
+.pre {
+  background: var(--sw-bg-2);
+  padding: 4px 6px;
+  border-radius: 3px;
+  font-size: 10.5px;
+  margin: 0;
+  white-space: pre-wrap;
+  word-break: break-all;
+  color: var(--sw-fg-1);
+}
+
+/* Status flag */
+.status-flag {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  height: 18px;
+  padding: 0 6px;
+  border-radius: 9px;
+  font-size: 9.5px;
+  font-weight: 700;
+  letter-spacing: 0.06em;
+  text-transform: uppercase;
+  flex: 0 0 auto;
+}
+.status-flag.sm {
+  height: 12px;
+  width: 12px;
+  padding: 0;
+  border-radius: 50%;
+  justify-content: center;
+}
+.status-flag.sm .flag-dot { margin: 0; }
+.flag-dot {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: currentColor;
+}
+.status-flag.flag-ok { background: rgba(34, 197, 94, 0.14); color: var(--sw-ok); }
+.status-flag.flag-err { background: rgba(239, 68, 68, 0.18); color: var(--sw-err); }
+
+/* Copy affordance */
+.copy-btn:active { transform: scale(0.94); }
+.copy-flash-chip {
+  display: inline-flex;
+  align-items: center;
+  height: 18px;
+  padding: 0 8px;
+  border-radius: 9px;
+  font-size: 9.5px;
+  font-weight: 700;
+  letter-spacing: 0.06em;
+  text-transform: uppercase;
+  background: rgba(34, 197, 94, 0.18);
+  color: var(--sw-ok);
+}
+.copy-flash-enter-from { opacity: 0; transform: translateX(-4px); }
+.copy-flash-enter-active { transition: opacity 0.18s ease, transform 0.18s ease; }
+.copy-flash-leave-to { opacity: 0; transform: translateX(4px); }
+.copy-flash-leave-active { transition: opacity 0.3s ease, transform 0.3s ease; }
+
+.dim { color: var(--sw-fg-3); }
+.mono { font-family: var(--sw-mono); }
+.wba { word-break: break-all; }
+.hint { font-size: 10.5px; color: var(--sw-fg-3); }
+.sw-btn.small { height: 24px; padding: 0 10px; font-size: 11px; }
+.sw-btn.ghost {
+  background: transparent;
+  border: 1px solid var(--sw-line-2);
+  color: var(--sw-fg-2);
+  cursor: pointer;
+}
+</style>
diff --git a/apps/ui/src/composables/useAutoRefreshSubscribe.ts b/apps/ui/src/composables/useAutoRefreshSubscribe.ts
new file mode 100644
index 0000000..81eb441
--- /dev/null
+++ b/apps/ui/src/composables/useAutoRefreshSubscribe.ts
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { watch } from 'vue';
+import { useAutoRefreshStore } from '@/stores/autoRefresh';
+
+/**
+ * Subscribe a refetch callback to the global auto-refresh ticker.
+ * Composables (or views) call this with their query's `refetch` to
+ * pick up manual refreshes + interval-driven ticks from the topbar.
+ * The watch is anchored to the calling scope, so when the component
+ * unmounts the subscription is torn down automatically.
+ */
+export function useAutoRefreshSubscribe(refetch: () => Promise<unknown> | unknown): void {
+  const auto = useAutoRefreshStore();
+  watch(
+    () => auto.tickCount,
+    () => {
+      void refetch();
+    },
+  );
+}
diff --git a/apps/ui/src/composables/useLayerDashboard.ts b/apps/ui/src/composables/useLayerDashboard.ts
index d211623..e16700b 100644
--- a/apps/ui/src/composables/useLayerDashboard.ts
+++ b/apps/ui/src/composables/useLayerDashboard.ts
@@ -29,6 +29,7 @@
 
 import { computed, type Ref } from 'vue';
 import { useQuery } from '@tanstack/vue-query';
+import { useAutoRefreshSubscribe } from './useAutoRefreshSubscribe';
 import { bffClient } from '@/api/client';
 
 export function useLayerDashboardConfig(layerKey: Ref<string>, scope?: Ref<string>) {
@@ -38,6 +39,8 @@ export function useLayerDashboardConfig(layerKey: Ref<string>, scope?: Ref<strin
     enabled: computed(() => layerKey.value.length > 0),
     staleTime: 5 * 60_000,
   });
+  useAutoRefreshSubscribe(() => q.refetch());
+
   return {
     config: computed(() => q.data.value ?? null),
     isLoading: q.isLoading,
diff --git a/apps/ui/src/composables/useLayerEndpointDependency.ts b/apps/ui/src/composables/useLayerEndpointDependency.ts
index 492c5bf..1a8767e 100644
--- a/apps/ui/src/composables/useLayerEndpointDependency.ts
+++ b/apps/ui/src/composables/useLayerEndpointDependency.ts
@@ -23,6 +23,7 @@
 
 import { computed, type Ref } from 'vue';
 import { useQuery } from '@tanstack/vue-query';
+import { useAutoRefreshSubscribe } from './useAutoRefreshSubscribe';
 import { bffClient } from '@/api/client';
 
 export function useLayerEndpointDependency(
@@ -43,6 +44,8 @@ export function useLayerEndpointDependency(
     ),
     staleTime: 30_000,
   });
+  useAutoRefreshSubscribe(() => q.refetch());
+
   return {
     data: computed(() => q.data.value ?? null),
     nodes: computed(() => q.data.value?.nodes ?? []),
diff --git a/apps/ui/src/composables/useLayerLanding.ts b/apps/ui/src/composables/useLayerLanding.ts
index e1fe790..8f4f96c 100644
--- a/apps/ui/src/composables/useLayerLanding.ts
+++ b/apps/ui/src/composables/useLayerLanding.ts
@@ -17,6 +17,7 @@
 
 import { computed, type Ref } from 'vue';
 import { useQuery } from '@tanstack/vue-query';
+import { useAutoRefreshSubscribe } from './useAutoRefreshSubscribe';
 import type { LandingConfig, LandingResponse, LayerDef } from '@skywalking-horizon-ui/api-client';
 import { bffClient } from '@/api/client';
 
@@ -54,6 +55,8 @@ export function useLayerLanding(
     retry: 1,
   });
 
+  useAutoRefreshSubscribe(() => q.refetch());
+
   const data = computed<LandingResponse | null>(() => q.data.value ?? null);
   const rows = computed(() => data.value?.rows ?? []);
   const reachable = computed(() => data.value?.reachable ?? false);
diff --git a/apps/ui/src/composables/useLayerLogs.ts b/apps/ui/src/composables/useLayerLogs.ts
new file mode 100644
index 0000000..33d8b4c
--- /dev/null
+++ b/apps/ui/src/composables/useLayerLogs.ts
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { computed, type Ref } from 'vue';
+import { useQuery } from '@tanstack/vue-query';
+import { useAutoRefreshSubscribe } from './useAutoRefreshSubscribe';
+import { bffClient } from '@/api/client';
+import type { LogFacetsResponse, LogsResponse, LogTagFilter } from '@/api/client';
+
+export interface LogListParams {
+  service: Ref<string | null>;
+  instanceId: Ref<string | null>;
+  endpointId: Ref<string | null>;
+  traceId: Ref<string | null>;
+  keywords: Ref<string[]>;
+  tags: Ref<LogTagFilter[]>;
+  page: Ref<number>;
+  pageSize: Ref<number>;
+}
+
+export function useLayerLogs(layerKey: Ref<string>, params: LogListParams) {
+  const q = useQuery<LogsResponse>({
+    queryKey: [
+      'layer-logs',
+      layerKey,
+      params.service,
+      params.instanceId,
+      params.endpointId,
+      params.traceId,
+      params.keywords,
+      params.tags,
+      params.page,
+      params.pageSize,
+    ],
+    queryFn: () =>
+      bffClient.layerLogs(layerKey.value, {
+        ...(params.service.value ? { service: params.service.value } : {}),
+        ...(params.instanceId.value ? { serviceInstanceId: params.instanceId.value } : {}),
+        ...(params.endpointId.value ? { endpointId: params.endpointId.value } : {}),
+        ...(params.traceId.value ? { traceId: params.traceId.value } : {}),
+        ...(params.keywords.value.length > 0 ? { keywordsOfContent: params.keywords.value } : {}),
+        ...(params.tags.value.length > 0 ? { tags: params.tags.value } : {}),
+        page: params.page.value,
+        pageSize: params.pageSize.value,
+      }),
+    enabled: computed(() => layerKey.value.length > 0),
+    staleTime: 15_000,
+  });
+  useAutoRefreshSubscribe(() => q.refetch());
+
+  return {
+    data: computed(() => q.data.value ?? null),
+    logs: computed(() => q.data.value?.logs ?? []),
+    total: computed(() => q.data.value?.total ?? 0),
+    isLoading: q.isLoading,
+    isFetching: q.isFetching,
+    error: q.error,
+    refetch: q.refetch,
+  };
+}
+
+/**
+ * Window-scoped facets (level + service breakdown). Separate query so
+ * level toggles can update the row list without re-running the bigger
+ * facet sample. Keys intentionally omit `tags` + `page` + `pageSize`
+ * since facets are unfiltered + always sampled from page 1.
+ */
+export interface LogFacetParams {
+  service: Ref<string | null>;
+  instanceId: Ref<string | null>;
+  endpointId: Ref<string | null>;
+  traceId: Ref<string | null>;
+  keywords: Ref<string[]>;
+}
+
+export function useLayerLogFacets(layerKey: Ref<string>, params: LogFacetParams) {
+  const q = useQuery<LogFacetsResponse>({
+    queryKey: [
+      'layer-log-facets',
+      layerKey,
+      params.service,
+      params.instanceId,
+      params.endpointId,
+      params.traceId,
+      params.keywords,
+    ],
+    queryFn: () =>
+      bffClient.layerLogFacets(layerKey.value, {
+        ...(params.service.value ? { service: params.service.value } : {}),
+        ...(params.instanceId.value ? { serviceInstanceId: params.instanceId.value } : {}),
+        ...(params.endpointId.value ? { endpointId: params.endpointId.value } : {}),
+        ...(params.traceId.value ? { traceId: params.traceId.value } : {}),
+        ...(params.keywords.value.length > 0 ? { keywordsOfContent: params.keywords.value } : {}),
+        sampleSize: 200,
+      }),
+    enabled: computed(() => layerKey.value.length > 0),
+    staleTime: 30_000,
+  });
+  return {
+    facets: computed(() => q.data.value ?? null),
+    isFetching: q.isFetching,
+    error: q.error,
+  };
+}
diff --git a/apps/ui/src/composables/useLayerTopology.ts b/apps/ui/src/composables/useLayerTopology.ts
index 77ad159..4cc1574 100644
--- a/apps/ui/src/composables/useLayerTopology.ts
+++ b/apps/ui/src/composables/useLayerTopology.ts
@@ -23,6 +23,7 @@
 
 import { computed, type Ref } from 'vue';
 import { useQuery } from '@tanstack/vue-query';
+import { useAutoRefreshSubscribe } from './useAutoRefreshSubscribe';
 import { bffClient } from '@/api/client';
 
 export function useLayerTopology(
@@ -37,6 +38,8 @@ export function useLayerTopology(
     enabled: computed(() => layerKey.value.length > 0),
     staleTime: 30_000,
   });
+  useAutoRefreshSubscribe(() => q.refetch());
+
   return {
     data: computed(() => q.data.value ?? null),
     nodes: computed(() => q.data.value?.nodes ?? []),
diff --git a/apps/ui/src/composables/useLayerTraces.ts b/apps/ui/src/composables/useLayerTraces.ts
new file mode 100644
index 0000000..d6442a1
--- /dev/null
+++ b/apps/ui/src/composables/useLayerTraces.ts
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
... 6643 lines suppressed ...