)]}'
{
  "log": [
    {
      "commit": "d30321509e49dcde2f76023f9b6dbef4948f8589",
      "tree": "45b870f9deaff0fec01878056842ade95dabcce2",
      "parents": [
        "8126ac3de1e4600be50f1b4ee1c9637d2cd461f0"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Tue Jul 07 22:47:41 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Jul 07 22:47:41 2026 +0800"
      },
      "message": "refactor(overview): aggregate KPI tiles server-side + retire dead template config (#98)\n\nrefactor(overview): whole-layer KPI aggregation + per-widget page-side control (#98)\n\nOverview KPI tiles (e.g. \"General services · RPM / Latency / SLA\") aggregated over only a single service instead of the whole layer (a hardcoded topN:1), so a busy layer read far too low. Reported in apache/skywalking#13934.\n\nOverview KPI aggregation\n- Multi-service tiles (General / Mesh / Virtual Database·Cache·MQ·GenAI) now roll up the entire layer server-side via sum/avg(top_n(\u003cmetric\u003e,{{topn}},DES[,attr0\u003d\u0027\u003clayer\u003e\u0027])) — one query per tile instead of a per-service fan-out. Bounded by a new HORIZON_QUERY_OVERVIEW_TOPN setting (default 100).\n- K8s-cluster and Istio-pilot composites stay page-side (their latest() snapshots + label-split metrics can\u0027t be top_n-rolled). Mesh\u0027s second tile is now Latency (avg response time) in place of P95.\n\nPage-side aggregation as a first-class, per-widget choice\n- Explicit aggregateOnPage flag (Server-side vs Page-side) with its own top-N limit, plus a new rankBy — rank the top-N services by one of the widget\u0027s KPIs (default the first) or by a separate ranking metric ({ kpi: n } | { mqe: \"…\" }). Fixes a latent bug where a page-side widget\u0027s first KPI being a LABELED_VALUE metric skewed the ranking; pilot_summary now ranks by xDS connections.\n- Each page-side widget gets its own landing call (own limit + ranking); self-aggregating tiles batch by layer.\n\nEditor UX\n- {{topn}} explained inline via a WidgetTip popover showing its live resolved value (surfaced through /api/oap/info); redesigned aggregation controls (mode radio + nested Top-N / Rank-by); auto-growing dashboard-description textarea; removed a duplicate \"mqe\" option in the KPI Source selector.\n\nCleanup (no behavior change)\n- Retired the dead legacy per-layer-overview machinery (LayerOverviewConfig / headerColumns / overviewGroups / overviewMetrics) and a sweep of verified-dead template config (serviceCountTile, the LayerDef.header twin, LogConfig.defaultTags, LandingConfig.style, LandingColumn.tip, OverviewWidgetResult, the BFF LayerMetricsConfig alias) + dead loader migrations.\n\nNo layer-template or OAP wire-contract change — only the two Overview dashboard templates (services.json, mesh.json) + their i18n overlays changed. Overviews are served remote-only from OAP, so existing deployments need an admin re-sync of the bundled Overview templates (Overview-templates admin → push-bundled); fresh installs get it via boot-seed."
    },
    {
      "commit": "8126ac3de1e4600be50f1b4ee1c9637d2cd461f0",
      "tree": "513506d4cb11591f8883233225ae83248e75bc2b",
      "parents": [
        "ab29bc8080e60dd094f22c611a0b903ffff8c9b3"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Tue Jul 07 16:36:02 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Jul 07 16:36:02 2026 +0800"
      },
      "message": "fix(traces): Zipkin trace list carries its spans so the inline waterfall skips a redundant re-query (#97)\n\nZipkin\u0027s `/api/v2/traces` already returns every span per trace, but the BFF\nsummarised the list rows and discarded the spans, so clicking a trace re-fetched\n`/api/v2/trace/{id}` on the client to draw the waterfall — a redundant second\nround-trip to OAP for data it already had (native BanyanDB traces ship spans\ninline on the row and render with no re-fetch; this brings Zipkin in line).\n\n- ZipkinTraceListRow gains an optional `spans?: ZipkinSpan[]` (mirrors NativeTraceListRow.spans?).\n- The /api/zipkin/traces route keeps the spans it already fetched instead of dropping them.\n- LayerZipkinTracesView renders the inline waterfall from the row\u0027s spans, falling\n  back to the by-id fetch only when a selected id isn\u0027t in the current list.\n- The /api/zipkin/trace/:id route + popout stay for opening a trace by pasted id /\n  a shared deep link / a log-\u003etrace jump (no list row → the by-id query is right there).\n- The AI-assistant Zipkin list path is unaffected (spans optional; it omits them)."
    },
    {
      "commit": "ab29bc8080e60dd094f22c611a0b903ffff8c9b3",
      "tree": "2ff71fc8898b4777be05dbb2dd7c1ef6b7d846f9",
      "parents": [
        "88db62d0b93139b75145f347324d9fe8968669ed"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Mon Jul 06 12:35:01 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jul 06 12:35:01 2026 +0800"
      },
      "message": "refactor(config, ai): config to OAP, harden the AI assistant, fix K8S_SERVICE layout (#96)"
    },
    {
      "commit": "88db62d0b93139b75145f347324d9fe8968669ed",
      "tree": "1dd0f5503f676293870c2774c6c093f0a03b4818",
      "parents": [
        "55f36f3312dc772cac8596e6746a84b3656ba5e7"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sun Jul 05 22:48:35 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Jul 05 22:48:35 2026 +0800"
      },
      "message": "feat(ai): AI assistant — chat that answers with live-data widgets (#95)\n\nAdd an opt-in, read-only AI assistant. A floating launcher (after login) opens a\nchat: ask in plain language (\"what\u0027s unhealthy right now?\", \"why is agent::app\nslow?\") and the assistant reads live OAP data through the same query path as the\ndashboards, then streams an ordered narrative with numbered inline figures —\ncharts, top-N lists, tables — drawn by the same widget components as the rest of\nthe UI. Open it as a side drawer, expand to a full page (/ai), or a new tab;\nconversations persist locally in the browser.\n\nRenders the REAL feature views inline, read-only and focused on the service:\ntopology (focused one-hop ego graph), cross-layer Smartscape hierarchy, native\nAND Zipkin traces (list → match → embed for mesh/k8s layers), layer logs,\nbrowser errors, deployment (intra-service instance graph), the per-pair instance\nmap (source → dest), and API-dependency chains. Each is the actual layer-tab\ncomponent given an additive, default-off `embedded` prop that seeds focus from\nprops and never writes shared state (time range, route, refresh ticker) — so the\nnon-embedded full page is untouched. On-demand Kubernetes pod logs are fetched\nand shown inline as a read-only result (a live tail lives in the Pod Logs tab).\n\nRead-only and permission-inheriting: every data tool re-checks the same read\nverb the operator already holds (metrics / alarms / topology / traces / logs /\nbrowser-errors), so the assistant never sees more than the user and never\nchanges configuration. Its one gated action is proposing a profile — a decision\ncard the user approves in a popout; nothing runs otherwise.\n\nGuided root-cause: built-in investigation playbooks (master method plus latency\n/ error-rate / saturation / middleware / kubernetes-workload / service-mesh\nspecializations) that walk the dependency topology upstream and follow the\ncross-layer hierarchy down into backing infra.\n\nVendor-neutral and off by default: the `ai:` config block (HORIZON_AI_*) takes\nan OpenAI-compatible endpoint (default) or Amazon Bedrock; the API key is\nenv-only, redacted from logs, and excluded from the audit trail. The system\nprompt and starter chips are operator-overridable; a starter naming \u003cservice\u003e\nopens a free-text fill-in the assistant resolves to a real service.\n\nBFF: new ai/ subsystem — chat SSE (reply.hijack), per-request session lifecycle\n+ abort, provider/model build, tools, and prompt/playbook resource files.\nclient/ stays the only OAP-talking layer; every route is RBAC-gated (ai:read).\nUI: new ai/ feature (drawer + /ai page), api façade, and the embedded-mode props\non the reused layer/render views.\n\nDocs: docs/operate/ai-assistant.md (in menu.yml); CHANGELOG under the unreleased\n1.0.0. English-source i18n across all 8 locales."
    },
    {
      "commit": "55f36f3312dc772cac8596e6746a84b3656ba5e7",
      "tree": "3dbdfe12c27f735c53889d7b0089c93cf747cc41",
      "parents": [
        "389e7506d217737eee76e0a37f1d72b22a5f33f6"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Fri Jul 03 22:42:46 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Jul 03 22:42:46 2026 +0800"
      },
      "message": "chore(local-boot): rename demo OAP secret file and align .gitignore (#94)\n\nThe demo OAP basic-auth password file was renamed\noap-password.local -\u003e oap-demo-env-auth.key, and .gitignore now ignores\n.claude/skills/local-boot/*.key (covering both the demo OAP password and the\nBedrock API key). Update the local-boot skill doc to the new filename and point\nthe \"git-ignored via\" note at the actual .claude/skills/local-boot/*.key rule\ninstead of the stale repo-wide *.local rule."
    },
    {
      "commit": "389e7506d217737eee76e0a37f1d72b22a5f33f6",
      "tree": "605bb109faef190cb1aa7a7f717778256a7f4305",
      "parents": [
        "cb479c06680fa0b39422ef663096babe66554f86"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Fri Jul 03 15:49:37 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Jul 03 15:49:37 2026 +0800"
      },
      "message": "fix(traces,logs): custom time range uses absolute epoch-ms (tz-correct) (#93)\n\nThe native trace and log custom-range paths sent a browser-local wall-clock\nstring that the server re-read in its own timezone — trace via new Date() in\nthe BFF process TZ, logs via passthrough to OAP as server-local. When the\nbrowser TZ differs from the server TZ (any containerized deploy), the query\nwindow shifted by the UTC offset and returned no results; the rolling presets\nworked because they derive from an absolute instant.\n\nBoth now send absolute epoch ms (startMs/endMs) and the BFF converts to\nOAP-server-local via fmtSecond(ms, offset) — the same pattern the other\nsurfaces already use (browser errors, events, alarms, global picker, Zipkin).\nThe operate trace-inspect path drops its now-redundant ISO round-trip. One\nconsistent, timezone-proof way everywhere; the metric-\u003etrace drill (same send\npath) is fixed too."
    },
    {
      "commit": "cb479c06680fa0b39422ef663096babe66554f86",
      "tree": "2dccbaac1ab3d595a8ad8b73cd7d074afa98ec4f",
      "parents": [
        "14b9ce6a14675c631777a4735903f98411b44828"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Fri Jul 03 14:33:17 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Jul 03 14:33:17 2026 +0800"
      },
      "message": "live-debug: show the LAL drop reason (#92)"
    },
    {
      "commit": "14b9ce6a14675c631777a4735903f98411b44828",
      "tree": "ff53bf436d0bc39c5dceec5ca540419c14751946",
      "parents": [
        "a9922bd920591efa10310c092575825e5ced9a0e"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu Jul 02 15:39:11 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jul 02 15:39:11 2026 +0800"
      },
      "message": "fix(traces): drill window snapping + HOUR-view axis-label overlap (#91)\n\n* fix(traces): drill window snaps to the clicked hour/day bucket\n\nThe window centered on a reconstructed mid-bucket point and capped the\nhalf-window at 6h, so clicking a DAY bucket opened a 12h mid-day slice\n(08:35–20:35) instead of that day. Now MINUTE → a ±5min neighborhood,\nHOUR → the whole clicked hour, DAY → the whole clicked day (snapped to\nthe local boundary and spanning one bucket).\n\n* fix(charts): hide overlapping x-axis labels on HOUR-step line charts\n\nThe x-axis forced a label every ~length/6 buckets, but the HOUR label\ncarries the date (\"07-02 05:00\"), so several wide labels collided in a\nnarrow widget. Add ECharts `hideOverlap` so any colliding label is dropped\n(interval stays as the density target).\n\n* fix(traces): address drill re-review findings + trim comments\n\n- TimeChart: a line-body click carries no dataIndex/value, so recover the\n  bucket from the click pixel and read the value from series data — clicks\n  between dots now drill, and a real 0 value still drills.\n- Traces tab: defer the drill\u0027s first query until the instance/endpoint id\n  resolves (no unfiltered wrong-scope flash), and seed the endpoint keyword\n  so a \u003e50-endpoint service still resolves the drilled endpoint.\n- Drill window: HOUR/DAY snap via local calendar arithmetic (DST-safe).\n- Trim the feature\u0027s comments to the non-obvious minimum."
    },
    {
      "commit": "a9922bd920591efa10310c092575825e5ced9a0e",
      "tree": "fc4c73a4edc6d15809ac09ba087c8213d52e1666",
      "parents": [
        "048305fbb517e3f05b0b2aa58f80f846936ff834"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu Jul 02 09:13:53 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jul 02 09:13:53 2026 +0800"
      },
      "message": "feat(traces): drill from General-service metric charts to native traces (#90)\n\nImplemented — [apache/skywalking-horizon-ui#90](https://github.com/apache/skywalking-horizon-ui/pull/90).\n\nIt\u0027s **config-driven**, not tied to a specific layer. The whole thing hangs on two conditions: a line widget declares a `traceDrill` mode, and its layer has the **Traces component activated**. Nothing is hardcoded to General service — that\u0027s just where the bundled widgets ship with it turned on.\n\nThe reason it stays simple: only latency and success/error metrics have a natural trace criterion, so the drill is driven by the declared **mode** rather than any metric↔trace mapping.\n\n**How it works** — a drill-capable line widget shows a small **traces** flag in its header and its datapoints are clickable. Click a point and it opens the native Traces list **in a new browser tab**, pre-filtered to that service (and the selected instance / endpoint) and centered on the clicked bucket\u0027s window:\n\n- **latency** widgets (response time / percentiles) → slowest-first, `minTraceDuration` \u003d the value you clicked (click p99 → the tail; click p50 → median-and-slower).\n- **error** widgets (error rate / success rate / Apdex) → the trace list filtered to ERROR status.\n\nThroughput (cpm) and CPU / memory / GC gauges have no meaningful \"which traces\" answer, so they offer no drill.\n\n**Enabling it** — it\u0027s a per-widget config field (`traceDrill: latency | error`) authored in the dashboard widget editor. The editor only lets you set it when the layer\u0027s Traces component is on. So it works on any dashboard — bundled or your own custom ones — as long as that layer has traces. The bundled **General service** layer ships these ten enabled:\n\n| Scope | Widget | Mode |\n|---|---|---|\n| Service | Avg Response Time | latency |\n| Service | Response Time Percentile | latency |\n| Service | Error Rate | error |\n| Service | Apdex | error |\n| Instance | Service Instance Latency | latency |\n| Instance | Service Instance Success Rate | error |\n| Endpoint | Response Time | latency |\n| Endpoint | Response Time Percentile | latency |\n| Endpoint | Success Rate | error |\n| Endpoint | MQ Avg Consuming Latency | latency |\n\nNo backend change was needed — `minTraceDuration` and `traceState` already exist on the trace query, so it\u0027s a client-side classify-then-navigate."
    },
    {
      "commit": "048305fbb517e3f05b0b2aa58f80f846936ff834",
      "tree": "d8e302aa9b498d6c651d9dc64cd9a4cf624ac085",
      "parents": [
        "cf2b97032af2ae7637f55eb761603cde449802a2"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Wed Jul 01 14:13:13 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Jul 01 14:13:13 2026 +0800"
      },
      "message": "feat(events): add a per-service events popout (queryEvents swimlane) (#89)"
    },
    {
      "commit": "cf2b97032af2ae7637f55eb761603cde449802a2",
      "tree": "ae756fa0c8b2cf1c3240849e6d23e85f4974c308",
      "parents": [
        "cf6e846201538f43978e1ad807ed951e6f765344"
      ],
      "author": {
        "name": "songzhendong",
        "email": "xiaodong12315@qq.com",
        "time": "Tue Jun 30 10:18:53 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Jun 30 10:18:53 2026 +0800"
      },
      "message": "Add Node.js runtime dashboard panels for meter_instance_nodejs_* (#87)"
    },
    {
      "commit": "cf6e846201538f43978e1ad807ed951e6f765344",
      "tree": "75a20221960caebde7c4cb459a14341a89c44c77",
      "parents": [
        "eeeef36b71b1f9b11e34179d81d388853d6d07d8"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Tue Jun 30 09:53:43 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Jun 30 09:53:43 2026 +0800"
      },
      "message": "feat(live-debug): Envoy-ALS rendering, LAL matrix UX (filter/inspect/diff), grid/flow (#88)\n\n## Why\n\nDebugging a real **Envoy access-log (ALS)** rule against a live OAP cluster surfaced a chain of gaps in the Live Debugger\u0027s LAL view: first the grid/flow issues (the frozen column drifting, tall results trapped in an inner scroll box); then, once those were fixed, that the matrix only rendered generic `LogData` (Envoy ALS cells were **blank**), that there was no way to find the few records that reached a given stage, that cluster nodes\u0027 grids interfered with each other, and that there was no way to read a cell\u0027s full data or compare pipeline stages.\n\n## What\n\n**Grid \u0026 flow** — the frozen first column stays pinned on horizontal scroll; clicking a source line flashes the whole step row; MAL/LAL/OAL pages scroll as one page for tall captures instead of trapping the result in a fixed-height box.\n\n**Format-agnostic rendering** — cells render whatever fields OAP serialized, not a fixed `LogData` subset: an `EnvoyAccessLogBuilder` snapshot shows its service/endpoint/response data (was blank); a raw proto input OAP couldn\u0027t serialize surfaces the reason (`jsonformat-failed …`) instead of a blank cell; complex object fields pretty-print; each cell names its payload class. Search + the cell popout follow the same rendering.\n\n**Matrix UX** — a per-row \"has-data\" filter (shown only on rows with gaps) narrows the grid to the records that produced data for that step; row counts reflect the whole capture, not the visible page. The row filter and the column-pin highlight are **per-node**, so acting on one cluster node\u0027s grid no longer affects another\u0027s. Cells are height-capped, columns width-capped. Statement-mode labels render `function @7` (the vue-i18n `@` is escaped, clearing the broken `function @{line}` label and its ~33 compile warnings — pre-existing, from #16).\n\n**Inspect \u0026 diff** — a persistent `VIEW` (input) / `DIFF` (builder) button per cell opens the complete payload in a read-only Monaco JSON viewer with the nested log `content` inlined. For builder snapshots a compare picker renders the **captured DSL itself** — per-statement steps marked on their line, the `extractor {…}` / `sink {…}` block snapshots drawn as selectable ranges — and picking one shows a side-by-side Monaco diff of the two snapshots, so you can see exactly what a statement or stage changed."
    },
    {
      "commit": "eeeef36b71b1f9b11e34179d81d388853d6d07d8",
      "tree": "356416c6b2001bcb9bf0d261be0d72d5de5d0fe7",
      "parents": [
        "3839a7e46e1b90d7a58f156bb39dd15baaa34c14"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sun Jun 28 23:42:21 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Jun 28 23:42:21 2026 +0800"
      },
      "message": "Satellite series labels, K8S_SERVICE network profiling, and async/pprof create-dialog fixes (#86)\n\n## Why\n\nA live-review pass against the public demo OAP surfaced a cluster of small defects in the dashboard render path and the per-layer profiling tabs, plus one missing capability. This bundles them.\n\n## What\n\n**Dashboards**\n- **Satellite so11y series were all labelled `all`.** The dashboard parser named each labeled series off the *last* metric label. The Satellite event/queue metrics carry two labels — `pipe` (the series identity) and `status` (a constant aggregation dim whose value is `all`/`success`) — so every series collapsed onto the constant. Now named by the labels that *vary* across the result, dropping constant dims. Fixes Receive/Fetch Events, Queue Input/Output and the queue_io prefix; percentile (`p`), `queue_used` (single `pipeline`) and unlabeled series are unchanged.\n\n**Profiling**\n- **K8S_SERVICE gains a Network Profiling tab** (Rover eBPF, the same capability Mesh has), documented on the layer page.\n- **Create dialogs**: the post-create hint counts down to a single list refresh (`refreshing in Ns`) instead of an opaque `N/4` counter; **Escape** now closes the Async and pprof create dialogs (parity with Trace/eBPF/Network); **Analyze** stays disabled until at least one instance is selected (Async, pprof).\n- **Async event-type picker** is derived from the selected task\u0027s captured events — only the JFR trees those events actually produce — instead of a static list of all five. Drops `PROFILER_LIVE_OBJECT` (no create-modal event produces it; needs async-profiler `--live`) and allocation types on non-ALLOC tasks, which rendered an empty graph. Mirrors booster-ui\u0027s behaviour."
    },
    {
      "commit": "3839a7e46e1b90d7a58f156bb39dd15baaa34c14",
      "tree": "332e40b3dbdafcabba2e9037d3f648edbe82178b",
      "parents": [
        "f4cc470b90911c360168ac1bea7ec82d64b18dd9"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sun Jun 28 12:14:53 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Jun 28 12:14:53 2026 +0800"
      },
      "message": "chore(deps): upgrade to clear 6 security advisories (both highs + all dev-only) (#85)"
    },
    {
      "commit": "f4cc470b90911c360168ac1bea7ec82d64b18dd9",
      "tree": "9d010dcb934c6c06dcfd7d792d3d22a331d89202",
      "parents": [
        "8cdbcb7edd7a7cdb058e08cd593516cfa23fcdcc"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sun Jun 28 11:02:06 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Jun 28 11:02:06 2026 +0800"
      },
      "message": "fix: pre-1.0 audit follow-ups — changelog consolidation, RBAC UI parity, 500 error-leak (#84)"
    },
    {
      "commit": "8cdbcb7edd7a7cdb058e08cd593516cfa23fcdcc",
      "tree": "208102eba9f788dc3416a1901d7d2a3d6484777a",
      "parents": [
        "e36615afef6767c2556053a0f17ab6b0529df733"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sun Jun 28 08:39:41 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Jun 28 08:39:41 2026 +0800"
      },
      "message": "refactor(layer-templates): decompose LayerDashboardsAdmin + enforce a CI source-size budget (#83)\n\n## Why\n\n`LayerDashboardsAdmin.vue` had grown to ~6.4k lines — one file mixing layer/template browsing, five per-scope config editors, the widget canvas, and all persistence/sync. It was effectively unreviewable and the worst offender against the project\u0027s \"keep files small, single-responsibility\" principle. This PR decomposes it, and adds a CI-enforced source-size guardrail so the same class of file can\u0027t silently grow back.\n\n## What\n\nTwo logical commits in one PR (on top of the earlier MetricDefinitionRow extraction already on the branch).\n\n**Guardrail + CI** (`chore(ci)`)\n\n- ESLint `max-lines` raised 1000 → 2000 lines of *code* (comments + blank lines excluded) across ui + bff. The old 1000 cap shredded the cohesive template/style blocks a dense SFC legitimately carries.\n- A comment-volume budget script (`scripts/check-source-budget.mjs`, ≤500 comment lines/file) for the dimension ESLint can\u0027t express, wired into `pnpm lint` + `pnpm lint:budget`.\n- A new CI `lint` job runs both and joins the `Required` status rollup — per-file source size/style is CI-gated for the first time.\n\n**Decomposition** (`refactor(layer-templates)`)\n\n- `LayerDashboardsAdmin.vue` 6365 → 888 lines (now an orchestration shell), split into ~14 cohesive single-responsibility files under `apps/ui/src/features/admin/layer-templates/`:\n- `useLayerTemplateStore` — the keystone: load / source / dirty / save / push / import / reset, the URL⇄state sync, and the single shared reactive `draft`.\n- per-scope config editors (Topology, Deployment, Process, Dependency, ServiceList) — each owns its `draft.template` slice via `defineModel` and mutates it in place (never clones); `RoleMetricRow` shared across deployment metric rows.\n- `WidgetEditorCanvas` (widget grid + drawer + drag/resize/reorder), `LayerSetupEditor`, and the presentational chrome trio (`LayerHeaderBar`, `LayerBrowseRail`, `ScopeTabsBar`).\n- `layer-dashboards.scopes` — the scope/component/slot constants seam.\n- Clears the lone remaining grandfather exemption now the file is within budget."
    },
    {
      "commit": "e36615afef6767c2556053a0f17ab6b0529df733",
      "tree": "d458746bf70b7f6a27ea5c403d9f9f50fd857d6d",
      "parents": [
        "30c11ce5ed580fcb7f0cfad11027a7dd9867c05d"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sat Jun 27 15:35:02 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Jun 27 15:35:02 2026 +0800"
      },
      "message": "feat: K8s Node \"Pod Total\" card and alarm-timeline range-select hint (#82)\n\nK8s Node dashboard — add a \"Pod Total\" card directly under Node Status,\nshowing the current count of pods scheduled on the node\n(`latest(k8s_node_pod_total)`, all phases). It fills the grid cell that\nwas previously left blank beside the single-line Node Status card. The\nnode scope has no Running-only pod metric, so this is total pods on the\nnode — the latest value of the same series the \"Pods on Node\" trend\ncharts. Title/tip added to the English source plus all seven locale\noverlays; node-dashboard doc and changelog updated.\n\nAlarms timeline — the hover tooltip now hints both affordances: click a\nminute to filter, or drag across the timeline to select a range. The\nbrush range-select was previously undiscoverable (no on-screen cue)."
    },
    {
      "commit": "30c11ce5ed580fcb7f0cfad11027a7dd9867c05d",
      "tree": "80cf0e4d4a353ccf0e14980b55363cb92482c656",
      "parents": [
        "d3b66d203d19622f03f976d0b8f62de6e4f9f880"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sat Jun 27 10:05:28 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Jun 27 10:05:28 2026 +0800"
      },
      "message": "UI correctness pass: comment audit, verified gap fixes, manual-fire query model, colored status cards, BFF boot-crash fix (#81)\n\nfeat(ui): colored status-chip cards, manual-fire query model, + correctness pass (#81)\n\nColored value-mapping cards: a format:\u0027enum\u0027 card renders each matched value — or metric label — as a colored status chip (ok/warn/err/info/neutral), editable in the layer-dashboard admin\u0027s value-map editor. The K8s Node Status card consumes it, showing a node\u0027s active conditions (Ready green, *Pressure/NetworkUnavailable amber-red) instead of a raw 1. Also fixes the widget schema silently stripping format/valueMap/valueColors/labelTopN from the resolver\u0027s request body, so the existing enum/valueMap feature is honored on that path too.\n\nBrowser Errors manual-fire: mirrors the Logs tab — stage filter edits into an applied snapshot, fetch only on Run query, and reset to the prompt (clearing the category filter) on service switch.\n\nCritical fix: the BFF crashed on boot because /api/ebpf/network/processes had no ROUTE_POLICY entry and the RBAC guard throws on any unpoliced route. It was masked by a stale dev process and invisible to type-check; caught by booting fresh against the demo OAP.\n\nHygiene + bug fixes: codebase-wide comment audit (strip ~331 non-earning comments, restore 8 over-removed gotchas) feeding a 32-finding gap review that was adversarially verified (5 false-positives dropped) — fixes for a loading-stuck modal, two missing-Escape dropdowns, Logs/Browser-Errors cascade-clear on service switch, Logs page reset, source-map clear on service switch, a partial form reset, and a label-case regression.\n\nProfiling: consistent, honest create gating; network tasks pick their instance in-dialog and validate it has rover-monitored processes before Create.\n\nDocs + i18n: profiling create-gating and Logs/Browser-Errors manual-fire docs; 18 i18n keys across all 8 locales for the new profiling strings.\n\nValidation: type-check 0, lint clean, both builds, license check, 278 unit tests, i18n complete across 8 locales; the colored chips, Browser-Errors manual-fire gate, and the network-processes route all live-validated headless against the demo OAP. The LayerDashboardsAdmin XL-file decomposition is intentionally deferred to its own focused PR."
    },
    {
      "commit": "d3b66d203d19622f03f976d0b8f62de6e4f9f880",
      "tree": "d2513a71f7382345803aad804d12720ddbe94238",
      "parents": [
        "59ebc236acd6f203ca864465638815f77c529aff"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Fri Jun 26 11:15:54 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Jun 26 11:15:54 2026 +0800"
      },
      "message": "feat: offline-capable readonly template mode, env-native config, and reachability-based Cluster Status (#80)\n\n* feat(config): readonly template mode (run without ui_template API) + env-native config (mount-free image)\n\nGoal A — `templates.mode: live | readonly` (HORIZON_TEMPLATES_MODE):\n- `readonly` renders every dashboard / overview / alert / 3D / translation from\n  the local disk bundle, loaded into the same in-memory row shape live mode\n  reads from OAP, so every render consumer resolves it unchanged. The\n  ui_template store is never called (no seed, no readiness wait); OAP\u0027s query API\n  is still used + boot-checked (config-only offline).\n- The config surface is read-only: admin pages still open + show the bundled\n  config, but write controls are gone (the BFF rejects config-template writes at\n  the route edge regardless — UI bypass still fails). Per-page + cluster-status\n  surfaces show the mode; the global banner suppresses the ui_template-store\n  strip in readonly (the query-unreachable strip still fires).\n- `live` (default) is unchanged: seed-to-OAP + read-from-OAP.\n\nGoal B — env-native config:\n- Every field in horizon.example.yaml is now a `${HORIZON_…:default}` token, so\n  the config file is the complete, self-documenting env-var reference. Scalars\n  take a value; lists / optional blocks (users, ldap, oap.auth, rbac.roles,\n  performance, …) take a JSON-string env var; a `:null` token falls through to\n  the schema default via a new `stripNullish` loader pass. Precedence env \u003e file\n  \u003e default.\n- The image bakes the tokenized config AS /app/horizon.yaml, so `docker run`\n  with env vars alone works — no mount, no repackage. A bind-mount still\n  overrides it.\n\nDocs: container-image full env table + env-only / readonly run recipes.\nTests: example parses to defaults (parity); env-merge (scalar / JSON / null /\nmalformed); readonly status. type-check / lint / license / 263 unit tests / i18n\ngreen. Validated live (demo OAP): readonly bundled render + write-409 + query\nlive; live-mode regression; mount-free env-only boot + login.\n\n* fix(config): review pass 1 — readonly translations + env-native robustness\n\n- P1: readonly mode rendered every non-English locale in English. Translation\n  overlays were sourced from `deps.bundledOverlays`, which only the (skipped-in-\n  readonly) boot seed supplies; the render callers omit it. The readonly branch\n  now sources overlays from the canonical `iterateBundledOverlays()` disk\n  iterator. Validated live: zh-CN readonly bundle carries the bundled overlays\n  (8551 CJK chars).\n- Scalar string env values are now injected into a quoted YAML position\n  (`\"${HORIZON_X:default}\"`), so a value with YAML metacharacters can\u0027t break\n  parsing; JSON list/object tokens + numbers/bools stay unquoted for typing.\n- `templates.mode` is fixed at boot (a hot-reload flip can\u0027t run the boot seed),\n  warning if the file later changes it.\n- `setTemplateReadOnly` also clears the in-flight sync probe (no cross-mode\n  backfill); the parity test resolves tokens against the same env the schema\u0027s\n  inline defaults read; the admin `TemplateSyncStatus` UI type gains `mode`;\n  config-bundle cache bumped v2→v3 so a stale cache can\u0027t show the wrong mode.\n\ntype-check / lint / license / 263 unit tests green; readonly + env-native re-validated live.\n\n* test(bff): readonly-mode sync — renders from disk, includes translation overlays, no OAP call\n\n* fix(config): review pass 2 — readonly UI gating, friendly config error, tests\n\n- Translations page is now read-only in readonly mode like its siblings: the\n  widget editor never opens (openPanel early-returns) and pushToOap guards on\n  readOnly. Previously the operator could translate→stage→push and was only\n  stopped by the server 409 at the end.\n- Invalid config (e.g. a bool env var set to no/off/0, a non-numeric port, or\n  malformed JSON env) now fails boot with a field-path-annotated fatal message\n  instead of a raw ZodError dump (the crash itself is correct — fail loud).\n- Tests: the readonly write-deny route hook (isTemplateWriteRoute matches the\n  config-template writes, not runtime-rule / local-state; deny → 409 in\n  readonly, no-op in live); quoted string scalars survive YAML-metachar values;\n  the example-parity test interpolates only the env vars the schema reads inline\n  (a stray HORIZON_* no longer reads as drift).\n\ntype-check / lint / license / 155 BFF + 116 UI unit tests green.\n\n* feat(cluster): reachability-based admin preflight + ui_template unified as a feature\n\nHealth on the Cluster Status admin pane is now the live probe, not config-\npresence. Each admin feature declares the relative REST path it actually calls\non OAP; the BFF fires a safe GET at it and reports whether it responds:\n\n  admin-server           /debugging/config/dump\n  receiver-runtime-rule  /runtime/rule/list\n  dsl-debugging          /dsl-debugging/status\n  inspect                /inspect/metrics\n  ui-management          /ui-management/templates\n\n`reachable` drives the green/red + the per-page warning gates. The config-dump\nprefix check (`enabled`) is demoted to an informational \"selector detected\"\nfootnote — it only tells you the official upstream release advertises that\nselector, so a renamed / forked / on-but-broken module that 404s now reads as\nunreachable instead of a misleading green (validated live: inspect on the demo\nOAP advertises its selector but only /inspect/metrics responds — the catalog\npath 404s).\n\nui_template joins the same table as a feature, mode-aware: in readonly it is\nnever probed (`reachable: null` → \"readonly · bundled\"). Each row shows how long\nago it was last checked (anchored to the BFF\u0027s generatedAt, so it reflects real\nprobe + cache age), and a force re-check bypasses the cache. The 5 probes are\nsingle-flighted for 30s on the BFF so the UI\u0027s 60s poll across N sessions\ndoesn\u0027t fan out on OAP.\n\nValidated live (demo OAP): live \u003d all five reachable; readonly \u003d ui_template\nnull + others probed; admin-port down \u003d all unreachable, no path probes.\ntype-check / lint / license / 161 BFF + 116 UI tests / builds / 8-locale i18n green.\n\n* refactor(cluster): fold the Env var column into the Gates description\n\nDrop the standalone Env var column; the selector now reads as an \u0027Enable on\nOAP: SW_X\u003ddefault\u0027 line under each feature\u0027s Gates description. Four columns\n(Feature / State / Probe path / Gates) read cleaner now that reachability — not\nthe selector — is the verdict.\n\n* fix(cluster): badge no longer claims \u0027all reachable\u0027 when a feature is bundled\n\nreachable\u003d\u003d\u003dnull (ui_template in readonly: not probed, serving bundled\ntemplates) was slipping past the reachable\u003d\u003d\u003dfalse filter, so the summary read\n\u0027all reachable\u0027 while a row showed \u0027readonly · bundled\u0027. Count the null/bundled\nfeatures separately — the badge now reads \u00274 reachable · 1 bundled\u0027.\n\n* fix(cluster): admin badge reads \u0027N/M reachable\u0027 in yellow when not all live\n\n\u00274 reachable · 1 bundled\u0027 wasn\u0027t a proper summary. The badge now shows a plain\nfraction — \u0027all reachable\u0027 (green) only when every feature probes live, else\n\u0027{n}/{total} reachable\u0027 (yellow). Covers both a 404\u0027d path and a bundled /\nnot-probed feature (ui_template in readonly); the per-row chip says which.\n\n* fix(readonly): close alert-page write gap, yellow read-only banner, stronger env parity\n\nThree review findings + a UX fix:\n\n- HIGH: the alert page-setup (/api/alarms/config) was still writable in readonly.\n  The readonly backstop only covered /api/admin/templates* and\n  /api/admin/overview-templates*; /api/alarms/config writes the\n  horizon.alert.page-setup template (file-backed) and slipped through. It\u0027s now\n  in the config-surface write set — a direct POST returns 409 in readonly — and\n  GET serves the bundled alert page-setup (like every other template) instead of\n  the local store. Validated live: POST 409, GET serves the bundle.\n\n- The read-only SyncStatusBanner was muted gray (\"white\") and easy to miss next\n  to the green per-row sync chips. It\u0027s now warning-yellow so it\u0027s unmistakable\n  that the whole config surface is bundle-served and uneditable. (The per-row\n  \"synced\" chip is correct as-is: in readonly the bundle IS the presented remote,\n  so bundled \u003d\u003d remote and the diff legitimately reads synced.)\n\n- The env parity guard only checked top-level sections + four hard-coded tokens.\n  It now recursively walks every schema-default field and asserts each is covered\n  by a ${HORIZON_…} token in horizon.example.yaml (a scalar token, or a JSON-env\n  token for a whole subtree). It immediately caught a real gap — oap.mqe shipped\n  untokenized — now fixed (HORIZON_OAP_MQE). A future nested field without env\n  coverage will now fail the test.\n\ntype-check / lint / license / 162 BFF + 116 UI tests / builds green.\n\n* fix(infra-3d): remove the silent disk-bundle fallback from the live render path\n\nThe 3D map was the lone surface that silently served the disk bundle when its\nremote couldn\u0027t be resolved — in live mode an unreachable ui_template store (or\na missing / invalid remote row) returned the bundled config instead of blocking.\nEvery other surface (bundle.ts) renders ONLY from remote rows and treats an\nunsynced / disabled / unreachable template as simply absent.\n\nresolveEffectiveConfig now does the same: render only from the remote row — a\nreal OAP row in live mode, the synthetic bundled row in readonly — and return\nnull otherwise. The route 503s on null, so the map blocks with its connectivity\nstate (like layer/overview/alert) instead of masking an outage with a stale\nbundle. The bundle still reaches OAP via boot-seed and is served in readonly\nthrough the fake-remote row; it is never a live runtime fallback.\n\nValidated live: readonly 200 (bundle via fake-remote), live+reachable 200\n(remote), live+ui_template-unreachable 503 (blocks). 162 BFF + 116 UI tests green.\n\n* refactor(config): one committed env-driven horizon.yaml (drop the example/copy split)\n\nCollapse `horizon.example.yaml` + the gitignored local copy into a single\ncommitted `horizon.yaml` — the same fully tokenized file the image ships and the\ndefault `pnpm start` reads. Every field is a `${HORIZON_…:default}` token, so all\nconfig is supplied via environment variables; nothing is edited in the file.\n\n- `git mv horizon.example.yaml horizon.yaml`; un-ignore it in .gitignore +\n  .dockerignore (now a tracked build input, not a local secret-bearing file).\n- Dockerfile bakes `dist/horizon.yaml` directly as `/app/horizon.yaml` — no more\n  separate `/app/horizon.example.yaml` reference copy, no rename-on-copy.\n- package.mjs copies the committed `horizon.yaml` into dist/; release.sh + the\n  parity tests (schema/loader) read `horizon.yaml`.\n- Docs: README, setup/overview, container-image, rbac, local-backend, ports no\n  longer reference an example file; local-backend clarifies the committed file\n  holds only `${HORIZON_…}` tokens (real hashes via HORIZON_AUTH_LOCAL_USERS).\n- local-boot skill: boots the repo `horizon.yaml` with OAP target + dev users\n  (`dev-users.json`) / LDAP (`dev-ldap.json`) injected via HORIZON_* env vars;\n  the three per-scenario yaml configs are removed. Documents the single-line-JSON\n  constraint for env-injected lists/objects.\n\nValidated live: env-native boot (root horizon.yaml + env vars only) against the\ndemo OAP — auth configured, login admin/admin → 200, OAP admin URL injected.\ntype-check / lint / license / 162 BFF + 116 UI tests green."
    },
    {
      "commit": "59ebc236acd6f203ca864465638815f77c529aff",
      "tree": "6e8090f7511123d81f457e96961c48c8ba531b68",
      "parents": [
        "f4ef9e0b4473c5ac2f93dedadc9d175650d3c9af"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu Jun 25 23:52:15 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 25 23:52:15 2026 +0800"
      },
      "message": "feat(layer-dashboards): tab widgets — several widgets in one named-panel slot, lazily loaded (#79)\n\nA layer-dashboard widget can now be a `tab` container: one grid slot holding any\nnumber of named tab panels, each its own little dashboard of card / line / top /\nrecord / table widgets in a 12-column sub-grid. Switching a tab swaps the whole\nset, and only the ACTIVE tab is queried — an unopened tab costs nothing and a\npreviously-viewed one stays warm. A tab carries no MQE of its own and cannot\nnest another tab (one level deep).\n\nAuthored inline in the Layer dashboards admin: add a widget, set its type to\n`tab`, size the slot; a segmented tab bar switches the active panel, a per-tab\n`+ widget` drops widgets in place, click a widget to edit it in the drawer,\ndrag its corner to resize, or drag it out to the top level. Tabs are managed\n(add / rename / reorder / delete) from the drawer\u0027s Tabs list.\n\nWidgets inside a tab render at full parity with top-level widgets — multi-entity\nCOMPARE (per-entity card rows / overlaid lines / grouped tables), the top/record\nPOP-OUT, and `visibleWhen` gating (compare-aware union, never the container) all\nwork, via a shared host render context. A unified widget-tree traversal\n(`walkWidgets` / `findWidgetById` / `collectWidgetIds`) is the single place that\ndescends tab panels, so find-by-id / id-uniqueness / enrichment never miss a\ntab child.\n\nQuerying: the SPA flattens to the active tab\u0027s leaves and chunks at the 40-widget\nrequest cap; the BFF flattens a tab to its first (default-active) panel for the\ntemplate-fallback / direct-API paths, re-checks the cap after expansion for a\nhand-built body, and accepts the `tab` type + empty `expressions` in its schema.\nA half-authored (blank-MQE) leaf is dropped from the batch instead of 400-ing the\nscope. `topNOrder` is enriched for top/record widgets inside tabs too.\n\nAlso: the compare bar gains a \"Clear all\" button to exit comparison (the CURRENT\nchip has no per-entity ×); the \"OAP unreachable\" banner / \"refreshing\" badge are\ncompare-aware (a failed primary no longer mislabels a working cohort); and the\ndead single-entity `record` pop-out button is removed.\n\nDocs + CHANGELOG updated; new UI strings translated across all 8 locales."
    },
    {
      "commit": "f4ef9e0b4473c5ac2f93dedadc9d175650d3c9af",
      "tree": "d66a74d3e743688a9efcff76c0397724a42f966d",
      "parents": [
        "b8864147e9b9cbc58838265f1610c6d1c5275ae5"
      ],
      "author": {
        "name": "songzhendong",
        "email": "xiaodong12315@qq.com",
        "time": "Thu Jun 25 10:34:08 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 25 10:34:08 2026 +0800"
      },
      "message": "Add PHP PHM instance widgets and i18n overlays. (#74)\n\nAdd six General Service instance line widgets for meter_instance_php_* with\nvisibleWhen gates and i18n overlays for de/es/fr/ja/ko/pt/zh-CN. Document in\nCHANGELOG 1.1.0."
    },
    {
      "commit": "b8864147e9b9cbc58838265f1610c6d1c5275ae5",
      "tree": "8b6b8682a2b173d47288a2a0b49be9dc2b0f44bd",
      "parents": [
        "15e03601603fef52bc9b64fc3db86ad600d1f642"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu Jun 25 09:50:29 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 25 09:50:29 2026 +0800"
      },
      "message": "fix(layer-dashboards): pin the widget editor in place so it always opens complete (#77)"
    },
    {
      "commit": "15e03601603fef52bc9b64fc3db86ad600d1f642",
      "tree": "ec9c4d9fbdeaaffbae911f289ff58e61ca101823",
      "parents": [
        "ade759f02b34c1ae7bae1aec6a317fe72cc2c446"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu Jun 25 09:20:45 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 25 09:20:45 2026 +0800"
      },
      "message": "fix(layer-dashboards): keep the widget editor\u0027s move / delete controls in view (#75)\n\nThe per-widget editor drawer kept Up / Down / Delete as the last block inside a\nscrolling body, and its fixed height (calc(100vh - 52px)) overshot because the\ndrawer sits below the editor card\u0027s header — so the drawer\u0027s bottom, with the\nDelete button, fell below the viewport, hidden behind a scrollbar even on a\nshort board with few widgets.\n\nMove Up / Down / Delete into a pinned footer outside the scroll region, and size\nthe drawer in JS from its live top offset down to the viewport bottom (re-synced\non open, scroll, and resize) so the footer is always visible regardless of pin\nstate or board height. Also scroll a newly-added widget into view, next to the\neditor that opens for it, instead of leaving it off-screen at the canvas bottom."
    },
    {
      "commit": "ade759f02b34c1ae7bae1aec6a317fe72cc2c446",
      "tree": "4b9de5cb0fa9430e720a762cfdd8902b9af175a9",
      "parents": [
        "97462438983bc15046ef8df94dd4e7055fa50b24"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Wed Jun 24 23:21:11 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Jun 24 23:21:11 2026 +0800"
      },
      "message": "feat(inspect): add \u0026 chart foreign metrics another OAP wrote to shared storage (#73)\n\nMetrics Inspect can now add metrics the connected OAP doesn\u0027t define — ones\nanother OAP (an older version, or a different distribution that doesn\u0027t carry\nthe analysis rule) wrote into the shared storage — and chart their values.\n\nA new \"Foreign metric\" tab in the + add metric drawer takes the metric name,\nscope, and the storage value column + type (read from the catalog of the OAP\nthat does define the metric). Stage several with \"+ add to list\" and bulk-add\nthem, board-cap aware, through the same counting footer as the catalog tab.\nEach widget enumerates the metric\u0027s entities and plots the value series via\nOAP\u0027s new POST /inspect/values (the admin-port counterpart to execExpression,\nwhich can\u0027t evaluate a metric the OAP has no local model for). Foreign widgets\ncarry a FOREIGN pill, behave like any other widget (entity nav, chart-cycle,\nmulti-select), and persist their selection + custom entities across reloads.\n\nAbsent-endpoint and upstream errors are reported honestly: a 404 on\n/inspect/values says the OAP build is too old rather than \"inspect disabled\",\nand OAP\u0027s own {error} text (a wrong value column, a metric that is actually\ndefined locally, an unsupported shape) now reaches the operator instead of a\nbare HTTP 4xx."
    },
    {
      "commit": "97462438983bc15046ef8df94dd4e7055fa50b24",
      "tree": "caee1690fbb068e46e1d8b24ca5ac432eef89c40",
      "parents": [
        "1618a7268be70325293af319dba413bfd85f2f46"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Wed Jun 24 16:09:12 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Jun 24 16:09:12 2026 +0800"
      },
      "message": "feat(explore): Trace inspect + Log inspect — cross-layer trace/log query tools (#72)\n\n## Why\n\nThe per-layer Traces / Logs / Browser-errors / Pod-logs tabs answer \"what\u0027s happening in *this* service?\" — but operators often need to query **across layers**, by trace-id / keyword / category / pod, with no layer in hand. This adds two cross-layer **inspect** power-tools modeled on Metrics inspect: name a service (pick it, type it, or leave it blank), set conditions, get one result. As an inspect surface it deliberately exposes every layer / service / flag rather than pinning defaults.\n\n## What\n\n### Trace inspect (`/operate/trace-inspect`)\n\nCross-layer trace query with a **Native ↔ Zipkin** source toggle. Optional entity — **Pick** (layer→service→instance→endpoint), **Type** (service name + real flag, which the BFF base64-encodes into the OAP id), or **blank** (all services). Native conditions (status / order / duration / tags / trace-id / time) plus the duration-distribution scatter — drag to brush-filter the list, click a dot to open the trace (a brush drag no longer also opens a detail). Zipkin gets its own service / remote-service / span pickers + annotation query. Result and detail reuse the **same shared widgets** as the per-layer Traces tab.\n\n### Log inspect (`/operate/log-inspect`)\n\nCross-layer log query; row-click opens a shared full-payload popout. Three sources:\n\n- **Raw** — OAP `queryLogs` (Tags / Trace ID / Time / Limit); Tags is a custom on-theme key/value autocomplete.\n\n- **Browser** — the BROWSER layer\u0027s JS errors, filtered by **Category, Version and Page** (`serviceVersionId` / `pagePathId`) plus time; **upload and manage source maps** inline, and the row popout resolves the minified stack against them. Entity is Pick / Type / blank with no pinned layer.\n\n- **Kubernetes Pod logs** — on-demand container tail. **Pick or Type** a service (Type is layer-less — the typed name encodes to a service id, and a Real flag is exposed), then the pod + container **auto-select**, and it **live auto-tails** (Interval + Pause/Start + Refresh) with **Include / Exclude** raw-regex filters. Live and never persisted → no cold-stage; surfaces OAP\u0027s `errorReason` (e.g. a terminated pod). A tip notes that each pod is the service instance, surfaced as its live Kubernetes pod, so it must be currently running.\n\n### Shared + cross-cutting\n\n- New shared widgets (`render/widgets/`): `TraceListPanel`, `TraceDetailCard`, `TraceDistribution`, `ZipkinTraceDetailCard`, `LogStreamPanel`, `LogDetailPopout`, `BrowserErrorPopout` — extracted from the per-layer views and reused by **both** the inspect tools and those tabs (no behavior/look change to the per-layer Traces / Logs / Browser tabs).\n\n- New `TagInput` primitive — a custom dark, anchored autocomplete dropdown (tag key suggestions before `\u003d`, per-key values after) replacing the browser-native `\u003cdatalist\u003e`, in the inspect Tags fields and the per-layer Traces / Logs tag inputs.\n\n- BFF: one `POST /api/explore/query` dispatches by kind + source; byte-exact entity-id encoding in `util/entityId.ts` (verified against OAP `IDManager`); `fetchNativeList` / `fetchLogs` / `fetchBrowserErrors` extracted from the per-layer routes; k8s reuses the existing pod-log routes. Cold-stage is honored for native trace / raw logs / browser errors and correctly a no-op for k8s pod logs and Zipkin.\n\n- **RBAC**: both tools gate on **`inspect:read`** — the same verb as Metrics inspect, so one grant covers all three inspectors (the prior dedicated `explore:read` verb was removed). Note: this also grants the **viewer** role Metrics inspect.\n\n- MAL/DSL rule cards now show the full rule name (wrapping to a second line) with the status pills moved to the bottom-right corner.\n\n- i18n: every new string in `en.json` first + all 7 non-English locales."
    },
    {
      "commit": "1618a7268be70325293af319dba413bfd85f2f46",
      "tree": "36f950dd78cd50388c8049967b4b22bb62f8dc45",
      "parents": [
        "6fded53d2f1a4ef5445077e04ece9bb268449350"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Tue Jun 23 23:36:07 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Jun 23 23:36:07 2026 +0800"
      },
      "message": "fix(templates): single-value metrics render as cards, not flat lines (#71)\n\n* fix(templates): single-value metrics render as cards, not flat lines\n\nAuditing the bundled layer dashboards against their booster-ui originals\n(the templates horizon was ported from) surfaced a mis-port class: a\nwidget whose MQE collapses the window to one number (a latest(...) total)\nwas rendered as type:\"line\" — a lone dot that misreads as a time series,\nand where a total was merged with an average into one widget, two scales\nshared one axis. The Virtual GenAI Input/Output Token + Estimated Cost\ntiles were the most visible case.\n\nSplit each affected widget into a single-value card (the total) plus a\ntrend line (the average), matching booster\u0027s original Card/Line split and\nthe \"widget type follows MQE shape\" rule. Audited all 46 bundled\ntemplates one-by-one against booster; 6 had the mis-port: virtual_genai\n(provider+model), elasticsearch, clickhouse, rabbitmq, rocketmq, apisix.\n\nEvery changed dashboard row still tiles to exactly full width (12 cols);\nsome pre-existing partial rows were closed in passing. The index-keyed\ni18n overlays were realigned across all 7 locales; i18n:validate clean.\n\n* fix(templates): close pre-existing layout gaps in clickhouse + elasticsearch\n\nSpan/rowSpan-only reflow so every dashboard section dense-packs the 12-col\ngrid with no empty cells, and single-value cards align to their row-mates\u0027\nheight (rs1 cards beside rs2 line charts were both a gap and a height\nmismatch; rs2 is the standard card height). No widgets added, removed, or\nreordered — the index-keyed i18n overlays are untouched (i18n:validate\nclean)."
    },
    {
      "commit": "6fded53d2f1a4ef5445077e04ece9bb268449350",
      "tree": "7baa8b0bb528ddedd78be969bda6714eb0ae6309",
      "parents": [
        "1acd0ba1f3a910191b02c7f3d276ff901a678e70"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Tue Jun 23 09:28:37 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Jun 23 09:28:37 2026 +0800"
      },
      "message": "feat(traces): Zipkin trace view reaches native parity + unified ?traceId\u003d links (#70)\n\n## Why\n\nThe Zipkin trace view (mesh / k8s / Envoy-ALS layers) looked and behaved like a second-class citizen next to the SkyWalking-native trace view — a different waterfall, a side-panel span detail, browser `\u003cdatalist\u003e` filters that wouldn\u0027t reopen, and no duration distribution. It also used a separate `?openZipkinTraceId\u003d` share param even though `/layer/\u003clayer\u003e/trace` already implies the source.\n\n## What\n\n**Zipkin trace detail + popout now mirror the native trace experience** (copy-and-adapted, since the wire fields differ):\n- KPI strip (started / duration / spans / services) + service legend.\n- **Duration-distribution scatter** — drag to filter the list, click a dot to open the trace (ported from native, adapted to Zipkin\u0027s µs units).\n- Time-positioned **waterfall bars** with the service · operation + kind/status affordances *inside* the bar, a time axis, and the duration flipped inside near-full-width bars (no stray x-scrollbar).\n- **Centered span-detail modal** (was a cramped side panel).\n- Zipkin annotation codes (`cr` / `cs` / `sr` / `ss` …) get an inline plain-language hint; the raw OAP value is kept verbatim.\n\n**Unified shareable links** — both native and Zipkin open from a single `?traceId\u003d` under the layer\u0027s trace tab. The two globally-mounted popouts self-select by trace-ID shape (native is dotted `x.y.z`, Zipkin is bare 16/32-hex), so `/layer/\u003clayer\u003e/trace?traceId\u003d…` always opens the right viewer. The separate `?openZipkinTraceId\u003d` param is gone.\n\n**Searchable, on-theme filter dropdowns** — native Instance/Endpoint and Zipkin Service/Remote/Span name now use the shared `TypeaheadSelect` (dark, type-to-filter, reopens after a pick); small enums (status/order/limit) stay plain `\u003cselect\u003e`."
    },
    {
      "commit": "1acd0ba1f3a910191b02c7f3d276ff901a678e70",
      "tree": "829d1c6fd792a414a276b63b4591408350103558",
      "parents": [
        "17360c1af68fc4c52fec52fbe4b13defe782107e"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Tue Jun 23 08:22:42 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Jun 23 08:22:42 2026 +0800"
      },
      "message": "feat(config): performance tuning section in horizon.yaml (fan-out + caps + 3D pipeline relocation) (#69)\n\n## Why\n\nOperational tuning — how hard the BFF fans metric queries out to OAP, and the caps that protect storage — was scattered in two wrong places: **hardcoded constants** in the query routes, and a **`pipeline` block inside the 3D dashboard template** (which is published to OAP, editable in the admin UI, and travels with export/import — none of which fits per-deployment fan-out tuning). Operators had no single, documented place to tune it, and couldn\u0027t tune it per-instance at all.\n\nThis consolidates all of it into one operator-owned, hot-reloaded `performance` section in `horizon.yaml`, next to the existing `query.landingServiceCap` / `oap.timeoutMs` knobs.\n\n## What\n\n**New `performance` section** (defaults equal today\u0027s built-ins, so it\u0027s optional and behavior-preserving):\n- `performance.bulk` — per-route bulk (request) size + concurrency for the **topology / 3D-map / landing / dashboard** OAP fan-outs.\n- `performance.limits` — the **service-map render valve** (`topologyMaxNodes` 5000 / `topologyMaxEdges` 15000) and per-request **record caps** (`maxPageSize.{traces,logs,browserLogs}` \u003d 100). Every value clamps to a hard ceiling (config can lower, never exceed).\n\n**3D pipeline relocation** — the 3D map\u0027s metric fan-out moved out of the OAP-published template into `performance.bulk.infra3d`. The BFF injects it into the config response, so the UI is unchanged; a stale template still carrying the old `pipeline` block is **accept-and-ignored** (no breakage on upgrade).\n\n**Unified page-size pickers** — Traces, Logs, and Browser Logs now share a `20 / 30 / 50 / 100` dropdown. Browser Logs **gains a picker it never had** (it was a fixed 100); the trace cap drops `200 → 100` to match the dropdown. Each picker\u0027s max equals the server-side cap.\n\n**Memory** — the image sets a default `NODE_OPTIONS\u003d--max-old-space-size`, with docs on sizing the Node heap against the in-memory source-map budget. (It\u0027s a process flag, not a config field — V8 reads it before `horizon.yaml` loads.)\n\n**Drift guard** — a new test (`schema.test.ts`) asserts `configSchema.parse({})` succeeds (every field has a default) **and** that `horizon.example.yaml` is byte-identical to the schema defaults — so the example can\u0027t silently drift again. (It already had: the rbac roles were missing `infra-3d:read`; fixed here.)"
    },
    {
      "commit": "17360c1af68fc4c52fec52fbe4b13defe782107e",
      "tree": "e2f095a9e0fa0e52a17c49867627c080b05cb987",
      "parents": [
        "09be88d4dfdd747188edbe077431ea40032d9fc8"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Mon Jun 22 19:02:09 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 22 19:02:09 2026 +0800"
      },
      "message": "fix: correct malformed next-dev version + validate versions in release.sh (#68)"
    },
    {
      "commit": "09be88d4dfdd747188edbe077431ea40032d9fc8",
      "tree": "a08ef3330e04fa9e1e654f19ebf896e538fe2803",
      "parents": [
        "ce3e940e79010f0a7c04e26fb3440fa134947dd5"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Mon Jun 22 16:43:41 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 22 16:43:41 2026 +0800"
      },
      "message": "Release 0.7.0, bump to 1.0.0-dev-dev (#67)"
    },
    {
      "commit": "ce3e940e79010f0a7c04e26fb3440fa134947dd5",
      "tree": "787729158f31425546777d2477cb0bbaa5f72aee",
      "parents": [
        "c66e2c81273cb5c4ba110a4ead864d1b474bb515"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Mon Jun 22 16:12:01 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 22 16:12:01 2026 +0800"
      },
      "message": "docs: features \u0026 bundled-dashboard reference + README/CHANGELOG release cleanup (#66)"
    },
    {
      "commit": "c66e2c81273cb5c4ba110a4ead864d1b474bb515",
      "tree": "e750c5d98b500ff29f3c45a62c72cb3c7d4cf235",
      "parents": [
        "82a0bfab9d79c52677a12ca327eb58819b3c3989"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Mon Jun 22 10:00:53 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 22 10:00:53 2026 +0800"
      },
      "message": "perf(maps): topology scale caps, partial-metric banners, parallel 3D load (#65)\n\nThree independent scale/resilience hardenings for the topology maps and the\n3D Infra Map. Output-identical on the happy path.\n\n- 3D Infra Map: per-node metric values now load in bounded-concurrency\n  batches (new `metricConcurrency` config, default 4) instead of one chunk\n  at a time, so load rings / traffic values fill in sooner on large layers.\n- Layer service map: when a graph exceeds the render ceiling (5000 services\n  / 15000 calls) the route returns a `tooLarge` envelope and the UI shows a\n  \"too large to render\" notice plus narrow-scope guidance, rather than\n  attempting an unreadable layout. Reject-with-guidance, not truncation.\n- All four topology maps (service map, instance topology, deployment,\n  endpoint dependency) thread per-chunk soft-fail counts and surface a\n  `metricsPartial` advisory when some metric batches fail, so a transient\n  OAP error reads as \"unavailable\" not \"zero\". The endpoint-dependency copy\n  notes nodes/edges may be missing (it prunes valueless nodes). New banner\n  strings translated across all 8 locales."
    },
    {
      "commit": "82a0bfab9d79c52677a12ca327eb58819b3c3989",
      "tree": "a8683e58cebf90f6eba689f68bfb2285175be9cb",
      "parents": [
        "9fd499c686167dfe0d24ec1994ad5efd73b42b5e"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sun Jun 21 21:58:04 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Jun 21 21:58:04 2026 +0800"
      },
      "message": "perf(bff): parallelize topology node/edge MQE fan-out across the 4 map routes (#64)\n\nThe service-map, instance-map, deployment, and endpoint-dependency routes each loaded per-node then per-edge metrics via serial chunked loops. They now build both fragment families up front and fan them out concurrently via a shared fetchAliasedChunks helper (bounded-concurrency mapPool), cutting the metric-load wall-clock toward a single round-trip for typical graphs.\n\nOutput is identical on the success path — same fragments, same merge, same values. On a partial OAP failure the new per-chunk soft-fail degrades only the failed chunk instead of aborting the rest (break-on-first-error before), so strictly more metrics are retained.\n\n- util/mapPool.ts: bounded-concurrency ordered map (+ unit test)\n- client/graphql.ts: fetchAliasedChunks\u003cT\u003e (chunk + pool + per-chunk soft-fail + merge)\n- topology / instance-topology / deployment / endpoint-dependency: node+edge fragments built up front, then Promise.all(fetchAliasedChunks node, edge)"
    },
    {
      "commit": "9fd499c686167dfe0d24ec1994ad5efd73b42b5e",
      "tree": "394a3220d00419b230998f9d89ea60e9dc19cf12",
      "parents": [
        "39b701cb8cf0b758ebd6854606ccd6d14f4cd465"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sun Jun 21 21:11:59 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Jun 21 21:11:59 2026 +0800"
      },
      "message": "RC0: dashboard/alarms query reductions + entity Compare goes standard (#63)\n\n## 1. BFF — fewer redundant OAP queries\n\n**Dashboard — reuse the warm service catalog.** `POST /api/layer/:key/dashboard` used to issue its own `listServices(layer)` on every request just to auto-pick the service and carry its `normal`/`group` entity-scope flags. It now reads the shared per-layer `ServiceLayerCatalog` (60s TTL, already kept warm by the sidebar) first, and only falls back to a live `listServices` on a miss — a cold/empty snapshot, a just-registered service, or OAP being unreachable.\n\n**Alarms — parallelize the startup probes.** `/api/alarms` and `/api/alarms/count` ran the server-time-offset probe and the backend-capability probe serially; they now `Promise.all`.\n\n### Reachability correctness\n`ServiceLayerCatalog.get()` soft-fails to an empty snapshot when OAP is down (it never throws), so a naive cache-first swap would let a *warm* cache mask an outage. Reachability now follows the **actual widget data fetch**: when every batched widget fails, the response is `reachable: false` with the widgets labelled `oap unreachable` — strictly more correct than the old Step-1 probe. (This was the one `major` from an adversarial pre-PR review; 7 other candidates verified as false positives.)\n\n## 2. UI — entity Compare graduates from preview to standard\n\nThe multi-entity lock + cross-check (\"pin entity\") feature, previously gated behind `FF_ENTITY_COMPARE` (off by default), is now **standard on every service / instance / endpoint layer dashboard — no flag, no config**. This removes the `featureFlags` module (it held only this one flag) + its test and ungates the call sites: the lock pins are now always present, while the compare fan-out still only engages once entities are actually pinned (an unlocked dashboard renders exactly as before). The CHANGELOG \"Lock \u0026 compare\" entry drops the preview/flag instructions. A separate adversarial pre-PR review of this diff returned SHIP with zero findings."
    },
    {
      "commit": "39b701cb8cf0b758ebd6854606ccd6d14f4cd465",
      "tree": "6a6dd2fd4d412b5d901d0c31c4562cdd3f5547d3",
      "parents": [
        "9a498a56355f303ed05f5584f1780a8337296909"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sun Jun 21 11:29:10 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Jun 21 11:29:10 2026 +0800"
      },
      "message": "fix(admin): per-kind save-local gate + overview validation; wrap inspect charts + step-formatted time-axis (#62)\n\nThree RC0-review follow-ups: the per-kind template-save validation/gate consistency (§4.5), the InspectView \"charts must be wrapped\" cleanup, and a time-axis formatting fix that fell out of it.\n\n## What\n\n**§4.5 — save validation + gate consistency (BFF)** · \n- `/api/admin/templates/save-local` was gated wholesale on `overview:write`. It now enforces the **same per-kind verb** as the OAP-backed save — `layer → dashboard:write`, else `overview:write` — in the handler (route gated `auth`), so a `dashboard:write`-only role can save layer drafts.\n- **Overview content is now validated before any write.** The `dashboardSchema` guard the legacy overview-create route ran was lost when editing moved to `/templates/save`; a malformed overview (missing required field, unknown widget type) is now rejected with a field-level error on **both** save paths instead of being written to OAP. `dashboardSchema` is exported for reuse.\n\n**InspectView — wrap the board charts (UI)** · \n- The view instantiated ECharts directly (violating \"charts are wrapped\"). The shared `TimeChart` is line/area-only but the inspect board also has a **bar** mode, so I extracted a feature-local `InspectMetricChart.vue` that owns one ECharts instance + lifecycle. A `ResizeObserver` replaces the manual window-resize + density-watch wiring, and Vue\u0027s per-widget mount/unmount replaces the manual instance `Map` + disposal. The chart-option builder moved **verbatim** — behavior unchanged. Net **−160 lines** in the view.\n\n**Inspect time-axis format (UI)** · \n- The inspect charts showed raw epoch-ms on the x-axis and tooltip header. They now format each bucket by the **query step**, identical to the layer-dashboard line widgets: `DAY → MM-DD`, `HOUR → MM-DD HH:00`, `MINUTE → HH:MM` (browser-local). The per-step formatter is extracted to a shared `bucketTimeLabel` in `utils/formatters` and used by **both** the dashboard widgets and the inspect chart, so they can\u0027t drift."
    },
    {
      "commit": "9a498a56355f303ed05f5584f1780a8337296909",
      "tree": "e0dc50807407ac665da85486287c3ba7cec676a9",
      "parents": [
        "213db03b69c9d4c7b435c7572bdda28cc9823d62"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sun Jun 21 09:52:13 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Jun 21 09:52:13 2026 +0800"
      },
      "message": "fix: RC0 Tier-1 correctness + Tier-2 hygiene (#61)\n\n* fix: RC0 Tier-1 correctness + Tier-2 hygiene\n\nTier-1 (correctness):\n- endpoint-dependency: honor the topbar time picker end-to-end (BFF route + client + composable + in-view expand). It was pinned to the last hour; it now queries the selected window like the service/instance maps. Stale \"last 15 minutes\" empty-state copy fixed.\n- dashboard: degrade per metric chunk — a transient failure on one widget batch marks only those widgets instead of blanking the whole dashboard (keeps reachable:true).\n- window: key the server-timezone cache on the OAP query URL, so a config reload that repoints OAP re-probes immediately instead of serving the previous server\u0027s offset for up to the TTL.\n- trace (v2 / BanyanDB): select the root span by empty refs, not just parentSpanId \u003d\u003d\u003d -1, so a multi-segment trace\u0027s list row reflects the true entry span; guard the empty endpoint name.\n- live-debug: gate /api/debug/status on live-debug:read alone (drop the redundant in-handler cluster:read double-gate).\n\nTier-2 (hygiene):\n- Remove the ?mockTop\u003d synthetic-row injector from the dashboard route.\n- Remove two leftover console.log statements from the topology request path.\n- Add baseline security response headers (nosniff / DENY / no-referrer) via an onSend hook.\n- Dead code: drop the unused invalidateServerOffsetCache + defaultSecondWindow, the UTC-formatting chain behind the no-consumer timeRange `duration` export, and the unused requireVerb pre-handler.\n\ni18n: one swapped UI string translated across all 8 locales. CHANGELOG updated under 0.7.0.\n\n* fix(profiling): use more of the page height\n\nThe Trace / eBPF / Async / pprof / network profiling layouts sized their shell off a fixed `calc(100vh - 280px)`, which over-counts the chrome above them (the topbar already sits outside the scrolling content cell) and left dead space at the bottom — most visible on taller screens. Reduce the offset so the shell extends closer to the bottom of the view."
    },
    {
      "commit": "213db03b69c9d4c7b435c7572bdda28cc9823d62",
      "tree": "0aca3b29e29fa820423450a09331f6bc68a4b015",
      "parents": [
        "ef66f70173970b165a0bf837d9219bfd4270a426"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sun Jun 21 08:32:36 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Jun 21 08:32:36 2026 +0800"
      },
      "message": "fix(rbac): per-kind template-save permission, surface infra-3d:read, plus docs + UI fixes (#60)\n\nRBAC:\n- Add `infra-3d:read` to the verb catalog so it appears on the Roles board and Auth Status. It was already enforced and granted to every built-in role (viewer and up) but was invisible there.\n- Enforce template save per kind: a layer template now requires `dashboard:write` (the verb the layer-dashboard editor advertises); overview / alert / 3D-map kinds keep `overview:write`. Built-in roles are unaffected (operator and admin hold both).\n- Regroup `ROUTE_POLICY` by the lowest built-in role that can reach each route (viewer ⊂ maintainer ⊂ operator ⊂ admin) and trim comments. Behavior-neutral — every route→verb mapping is unchanged.\n\nUI fixes:\n- Metrics Inspect: render the crosshair tooltip at body level (appendToBody) so it is no longer clipped by the widget overflow or painted under the sidebar.\n- 3D map config: require a saved local draft before \"Check diff \u0026 push\"; give the push diff a definite height (fit-body modal + flex wrapper) so the side-by-side comparison renders instead of an empty panel.\n\nDocs: operator-perspective accuracy sweep — Cluster Status three panes / no per-node member list, container readiness probe → public /api/health, layer-template `components` default + `aliases` authoring key, removal of the dead `visibleWhen` free-text and embedded-i18n shapes, data-retention cold stage, and RBAC wording reconciled to the per-kind gate.\n\ni18n: two new UI strings translated across all 8 locales. CHANGELOG updated under 0.7.0."
    },
    {
      "commit": "ef66f70173970b165a0bf837d9219bfd4270a426",
      "tree": "b30e2154c893276eeae6b305596522f23b91be9f",
      "parents": [
        "3d5f1ac26d8295b64b7da15d3c36560e7413db5e"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu Jun 18 19:44:24 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 19:44:24 2026 +0800"
      },
      "message": "chore(ci): pin docker actions to the ASF-allow-listed SHAs (#59)\n\nASF tightened the GitHub Actions allow-list, and the docker/setup-buildx-action\nand docker/login-action SHAs in publish-image.yaml are no longer permitted —\nthe image-publish workflow now fails with \"actions ... are not allowed in\napache/skywalking-horizon-ui\".\n\nBump both to the SHAs the main apache/skywalking repo pins (vetted under the\nsame enterprise allow-list):\n  docker/setup-buildx-action  8d2750c → d7f5e7f5 (v4.1.0)\n  docker/login-action         c94ce9f → 650006c6 (v4.2.0)"
    },
    {
      "commit": "3d5f1ac26d8295b64b7da15d3c36560e7413db5e",
      "tree": "9e958c4682847a2df85ff2b3175650a38fa10d25",
      "parents": [
        "84bbbe62f77e27138d886ad3260b6b16a64f540f"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu Jun 18 19:08:11 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 19:08:11 2026 +0800"
      },
      "message": "fix(layer): jump-to-trace + copy on record widgets, fix the unfulfilled tip (#58)\n\nRecord widgets (slow SQL / slow statements / slow cache commands) advertised\n\"click to drill into the originating trace\", but the trace id never reached the\nUI — the BFF selected only id/value/owner from the MQE response and dropped the\nper-sample trace. The tip promised a drill-in that couldn\u0027t happen.\n\nOAP\u0027s MQE exposes traceID on each RECORD_LIST sample (metrics-v3.graphqls).\nSelect it, and forward it per row — omitted when the sample has none, since\nrecords are sampled and the trace can be absent.\n\nOn the UI, the record widget now renders via a dedicated RecordList: each\nsampled row gets a jump-to-trace icon at the row head, shown ONLY when the\nsample carries a trace id, that opens the trace in the global popout (resolved\nby id, so it works even when the trace belongs to a service on another layer).\nThe statement text itself is click-to-copy. Compare mode keeps the plain\nTopList (no single trace to jump to).\n\nCorrect the bundled Virtual Database / Virtual Cache slow-record tips (English\nplus all 7 locale overlays) and the dashboard-widgets doc to describe the new\ncopy + jump interaction instead of the old \"click to drill\" wording."
    },
    {
      "commit": "84bbbe62f77e27138d886ad3260b6b16a64f540f",
      "tree": "534d41a5dbd57ce2e5b3289ab04843ace9489c95",
      "parents": [
        "4761cce1930615d7126a94d281891cce290c0a0b"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu Jun 18 17:31:43 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 17:31:43 2026 +0800"
      },
      "message": "feat(layers): split a layer\u0027s menu by service group, + resizable sidebar (#57)"
    },
    {
      "commit": "4761cce1930615d7126a94d281891cce290c0a0b",
      "tree": "4e77929f0cf1cd3e0d289291ada9f0dc10757c22",
      "parents": [
        "cfa8836c3f82aaebd405ae4bfb1033f8a5635c54"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu Jun 18 15:22:02 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 18 15:22:02 2026 +0800"
      },
      "message": "feat(topology): node filter, component icons, grouped service selector (#56)\n\nThree operator-visible additions to the service map. They live on the one\nLayerServiceMapView component, so both the per-layer Topology tab and the\nembedded overview topology widget inherit all three.\n\n- Technology component icons in the node hexes, resolved from each node\u0027s\n  detected component (the same icon set the trace waterfall uses); falls\n  back to the generic service / external / user glyph when a component\n  ships no icon or can\u0027t be resolved.\n\n- A layer-based \"Show nodes\" filter (top-left of the map): a standalone\n  User toggle plus one auto-derived row per layer, each carrying the\n  layer\u0027s own menu icon and its localized display name (LayerDef.name,\n  reusing the existing template i18n), and an Others bucket for nodes OAP\n  couldn\u0027t resolve (UNDEFINED). Hides nodes and their now-dangling edges\n  client-side; rows self-populate from the live graph; defaults to showing\n  everything.\n\n- The \"All services\" selector groups its list by the service\u0027s own group\n  (OAP Service.group), under a value-first `\u003cname\u003e [GROUP]` header whose\n  marker batch-selects / unselects the whole group (tri-state). Selection\n  markers are CSS-drawn circles so the group and service dots are\n  pixel-identical regardless of font fallback.\n\nFilter chrome strings are inline English (consistent with the rest of this\ncomponent); layer names reuse the BFF menu translation. No new i18n\ncatalog entries."
    },
    {
      "commit": "cfa8836c3f82aaebd405ae4bfb1033f8a5635c54",
      "tree": "e68805b9ec8ca0d51e00f532c05194e945539885",
      "parents": [
        "4a39381892e9ff18cf360ae19a09a5625b84b607"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Tue Jun 16 13:55:44 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Jun 16 13:55:44 2026 +0800"
      },
      "message": "feat(dsl): track runtime-rule applies with a live phase stepper (#55)"
    },
    {
      "commit": "4a39381892e9ff18cf360ae19a09a5625b84b607",
      "tree": "c64523bff9f1c7ad15c99e10e0a8393035622b53",
      "parents": [
        "860bfef75d2a82bdfd28255f14c86f7c4c63db5e"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Mon Jun 15 22:57:08 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 15 22:57:08 2026 +0800"
      },
      "message": "fix(layer): resolve top-N compare sort direction from the MQE, not the data (#54)\n\nIn multi-entity compare, the merged \"All\" tab inferred asc/des from a\nsingle probe entity\u0027s values. That flipped the merge whenever the probe\nentity\u0027s values were flat/equal (e.g. instances all at 100% success rate)\nor when no entity had \u003e\u003d2 rows (it defaulted to descending). So \"Top 10\ninstances by success rate\" — top_n(service_instance_sla,10,asc), which is\ndeliberately worst-first for triage — rendered best-first in the \"All\"\nlist, while the per-entity tabs (native order) stayed correct.\n\nResolve the direction BFF-side at template-resolve time instead: a new\nDashboardWidget.topNOrder field is parsed from the widget\u0027s first top_n()\nexpression in widgetsForScope (top/record widgets only; pass-through and\nreference-stable otherwise). The UI\u0027s multiTopGroups reads topNOrder\n(default des) and drops the data-probe — so the UI no longer parses MQE\nor guesses direction from one entity\u0027s data."
    },
    {
      "commit": "860bfef75d2a82bdfd28255f14c86f7c4c63db5e",
      "tree": "ec49307426fa3eab03d9383e47db81f8fe26375c",
      "parents": [
        "75fcd327ed2ada0efe1267d8ff026f4640fb930b"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Mon Jun 15 19:57:28 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 15 19:57:28 2026 +0800"
      },
      "message": "feat(layer): lock \u0026 compare multiple entities on layer dashboards (#53)\n\n## Why\n\nOperators routinely need to put several services / instances / endpoints next to each other — \"is this instance slower than its siblings?\", \"do these two services share the spike?\". Today you can only look at one entity at a time per layer dashboard.\n\n## What\n\nA **preview** feature (flag-gated, off by default — `VITE_FF_ENTITY_COMPARE\u003d1` or `localStorage[\u0027horizon:ff:entity-compare\u0027]\u003d\u00271\u0027` then reload; with nothing locked every page renders byte-for-byte as before):\n\n- **Lock several entities — across different services — and compare them inline** on the service / instance / endpoint dashboards. Instance and endpoint pins are cross-service.\n- **The entity you\u0027re viewing is always the `CURRENT` member** (accent hue, drives the header KPIs); pins add to it (up to six, stable hues). Comparison-bar chips are **display-only** — they never refocus the banner (no disruptive reload); `×` unpins.\n- **Each widget compares inline in its own tile** — line \u003d N overlaid hued series; card \u003d N rows; top-N / record \u003d per-entity tabs + a merged \"All\"; table \u003d an **Entity** column with an `(others)` fold. Labeled series lead with the label so it survives truncation; a widget shows whenever *any* compared entity has it (e.g. JVM widgets when a Java service is pinned next to a non-JVM one).\n- **Per-entity fan-out** (one dashboard request per entity, bounded concurrency), progressive loading, per-entity error isolation.\n\nFolded in alongside (see commit body):\n- Chart axes/tooltips use **SI suffixes** (`45.1k`, `1.34M`) instead of scientific notation; the multi-series tooltip is a fixed, aligned table.\n- **Fix Go runtime Mspan/Mcache widgets to report KB** (were raw bytes mislabeled as MB, ~1000× too large).\n- Type the infra-3d OrbitControls / camera refs (drop 3 `any`).\n- Pre-existing in-flight DSL rule-editor + banyandb template tweaks ride along."
    },
    {
      "commit": "75fcd327ed2ada0efe1267d8ff026f4640fb930b",
      "tree": "4237423eb327425081e2d4091d89dce37a3976b6",
      "parents": [
        "ec4416b7ca33ddf34bed1843f6c0cc7cd4d8cdda"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sat Jun 13 23:03:36 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Jun 13 23:03:36 2026 +0800"
      },
      "message": "feat(live-debug): summarise \u0026 diff MAL samples and output entities (#52)\n\nThe MAL live-debugger right pane rendered one row per captured sample\n(with its full label set) and one meter card per materialised output\nentity. A stage on a high-cardinality metric fanned out to a wall of\nrows, and a record that materialised a metric for several entities\nrepeated near-identical entity cards — hiding what actually differs.\n\n- Input samples group by metric name into a collapsed summary\n  (`\u003cmetric\u003e · N samples · values\u003d…`); expand for the per-sample labels.\n- Expanding a multi-member group opens in diff mode by default: labels\n  shared by every member collapse into a dimmed \"common\" block and only\n  the differing ones are highlighted (a \"diff\" toggle shows the full\n  list). The common/varying split is computed across the whole group.\n- A record\u0027s run of output samples for one metric folds into one block\n  (shared metric / function / timeBucket header + `N outputs · values\u003d…`)\n  that diffs the entity fields generically — whichever differ surface,\n  not a fixed field — with each output\u0027s value beside it.\n- Long fractional values (rate()/avg()) are trimmed for display so they\n  stop overflowing the value column; integer counters stay exact and the\n  precise value is available on hover.\n\nRendered detail is capped per group with a \"+ N more rows\" note and the\ndiff scan runs lazily. Pure presentation over the existing debug wire\nshape — no OAP query change. New UI strings translated across all 8\nlocales."
    },
    {
      "commit": "ec4416b7ca33ddf34bed1843f6c0cc7cd4d8cdda",
      "tree": "6fb3f715ddf7765ab9a92610acea8799905ea005",
      "parents": [
        "7b5dd77cd75415d9109b488a29f2916d8aeb3f49"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sat Jun 13 17:54:14 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Jun 13 17:54:14 2026 +0800"
      },
      "message": "chore(skills): add the pull-request skill (#51)"
    },
    {
      "commit": "7b5dd77cd75415d9109b488a29f2916d8aeb3f49",
      "tree": "742007acc1f95a1d190505b47552c5a8086a0bac",
      "parents": [
        "b94cdf3c5e268813eae45b50907490f38759c14b"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sat Jun 13 09:05:12 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat Jun 13 09:05:12 2026 +0800"
      },
      "message": "chore(changelog): one-line house style + release-notes reflow (#50)\n\nGitHub Release bodies render every single newline as a \u003cbr\u003e (GFM hard\nline breaks), so the hard-wrapped CHANGELOG showed on the release page as\na ragged column of short lines with right-hand whitespace — invisible in\nthe repo file view, which collapses the same newlines to spaces.\n\n- CHANGELOG.md: normalize to one line per paragraph / list item (no hard\n  wrap). Identical in the file view, flows correctly in release bodies.\n  Byte-identical content under whitespace-collapse; rendered h2/h3/ul/li\n  structure unchanged; \u003cbr\u003e 860 -\u003e 0. Also straightens two bullets that\n  wrapped onto a line starting with a list marker, which CommonMark\n  mis-parsed as a spurious nested bullet (broken in both contexts).\n- scripts/changelog-release-notes.mjs: extract a version\u0027s section and\n  unwrap it for the release body; --whole-file normalizes an entire file.\n- release-finalize.sh: generate release notes through the helper instead\n  of raw awk extraction (backstop if a future entry is hand-wrapped).\n- CLAUDE.md: document the one-line house style for CHANGELOG and docs/."
    },
    {
      "commit": "b94cdf3c5e268813eae45b50907490f38759c14b",
      "tree": "fc10cf57017b59641b0a1a5ba1ffaee5ffea4066",
      "parents": [
        "86b5d220ed47497ebd613d36eb5c7d547d5cf8ee"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Fri Jun 12 23:55:46 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri Jun 12 23:55:46 2026 +0800"
      },
      "message": "feat(layer): BanyanDB self-observability layer (SWIP-15) (#49)\n\n## BanyanDB self-observability layer (SWIP-15)\n\nAdds a new **BanyanDB** layer under **Self-Observability** — Cluster / Container / Group dashboards plus a role-aware **Deployment** topology — and the widget + deployment-config infrastructure it needed. Pairs with OAP backend SWIP-15 (`meter_banyandb_*` cluster / `meter_banyandb_instance_*` container / `meter_banyandb_endpoint_*` group).\n\n### Dashboards\n- **Cluster / Container / Group** views. Container panels adapt to the container\u0027s role (liaison / data / lifecycle); Group panels split per data model (measure / stream / trace / property). Panels self-gate on the role attribute and on data presence.\n- **Disk Used / Total** per data path in one widget. Removed the redundant UI-side `*100` on disk / system-memory percent (the MAL already scales), and the spurious `*1000` on histogram-p99 latencies (the MAL\u0027s `histogram()` already emits ms).\n\n### Widget capabilities (generic)\n- Card `format: \"enum\"` — a translatable value→label map (e.g. `1 → OK`, `0 → Failed`), editable in the dashboards admin and the translations editor.\n- `duration` time-ago format; line-chart axes + tooltips use scientific notation for large magnitudes.\n- Instance-list badge configurable per layer (`instances.badge`).\n\n### Deployment topology\n- **Role-to-role edge metrics**: up to 3 client-side primaries shown inline with short aliases (e.g. `W` / `R`), flowing onto one line or stacking by edge length; the selected-edge panel keeps the full client | server breakdown; the **Flows** sub-tab tables every edge per role-pair.\n- Per-role **health-ring legend** showing each role\u0027s own ring metric and its threshold bands (config-driven, no hard-coded metric).\n- The deployment role model (clustering, per-role node metrics, role-pair edge metrics) is **editable in the Layer-dashboards admin**."
    },
    {
      "commit": "86b5d220ed47497ebd613d36eb5c7d547d5cf8ee",
      "tree": "a131d4590cbb0567b6a57030b0b41b8b9937e2e9",
      "parents": [
        "a1bc540e1c9855ad4cd432c736cfbacee87e9c9f"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu Jun 11 14:06:18 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 11 14:06:18 2026 +0800"
      },
      "message": "feat(layer): Browser Logs tab — JS error logs + source-map de-obfuscation (#48)\n\n## What\n\nAdds a **Browser Logs** tab to the BROWSER layer and **source-map de-obfuscation** for\nthe JS errors the browser agent reports — the long-standing ask in\ncloses [apache/skywalking#6784](https://github.com/apache/skywalking/issues/6784).\n\nThe tab lists `queryBrowserErrorLogs` rows (message, category, page, app version, time,\nminified `line:col`), filterable by category, app version, page path, and time window.\nExpanding a row shows the **raw stack** next to a **de-obfuscated** view: pick a loaded\n`.map` and the BFF resolves each frame back to the original file / line / column / symbol\nname plus a source snippet.\n\n## Why it\u0027s UI-only (no SWIP)\n\nPer the discussion on #6784, this is implemented entirely in Horizon UI — **no OAP, agent,\nor query-protocol change**. It reads the existing `queryBrowserErrorLogs` GraphQL query and\nhosts source maps in the **BFF process memory** as a deliberate, no-backend-change pilot:\n\n- **Uploaded `.map` files are temporary** — held in BFF memory only, evicted\n  least-recently-used past a configurable budget, and lost on restart. The UI surfaces this\n  explicitly (a \"temporary\" badge, a usage bar, and a warning).\n- **Durable provisioning is a static mount** — drop `.map` files into the server\u0027s\n  source-map directory (`HORIZON_SOURCEMAPS_DIR`, `/app/sourcemaps` in the image); those\n  reload automatically and can\u0027t be deleted from the UI.\n\nResolution runs server-side in Node (`@jridgewell/trace-mapping`, pure-JS/sync) so maps —\nand their `sourcesContent` — never ship to the browser.\n\n## Config\n\nNew `sourceMaps` block in `horizon.yaml` (all hot-reloadable except the boot mount and\n*raising* the per-file upload cap):\n\n```yaml\nsourceMaps:\n  enabled: true\n  maxFileBytes: 67108864      # 64 MiB — reject a single .map larger than this\n  maxTotalBytes: 536870912    # 512 MiB — resident in-memory upload budget (LRU past it)\n  maxFileCount: 128           # per-set cap (uploaded set / mounted set each)\n  bootMountDir: /app/sourcemaps\n```\n\nRBAC: uploading/deleting maps needs the new **`source-map:write`** verb; listing errors,\nlisting maps, and resolving ride on **`browser-errors:read`**. Both are granted to the\ndefault roles in the example/demo configs.\n\n## Scope of the change\n\n- **BFF** — `POST /api/layer/:key/browser-errors` (mirrors the logs route; SECOND step,\n  OAP-server-local time, per-page time range, DESC re-sort for BanyanDB segment order);\n  source-map admin routes (multipart upload / list / delete / resolve); a process-global\n  `SourceMapStore` (budget accounting, LRU eviction, lazy mount reads, bounded parsed cache).\n- **UI** — `layer/browser-errors/` (view + composable + embedded source-map manager),\n  api façade, tab wiring across sidebar / router / caps / topbar opt-out. The view matches\n  the general Logs tab vocabulary (histogram, category legend, dense stream); the tab owns\n  its own time range and pauses auto-refresh.\n- **i18n** — all operator strings wrapped in `t()`; 40 new keys added to English and\n  translated into the 7 other catalogs (de/es/fr/ja/ko/pt/zh-CN), placeholders + technical\n  tokens preserved.\n- **Docs** — operator page `docs/operate/browser-source-maps.md` (+ menu entry), plus\n  `horizon.yaml`, container-image, and RBAC doc updates.\n- **Bundled BROWSER layer template** — gains the `browserErrors` widget; the generic `logs`\n  tab is disabled for BROWSER (browser errors replace it there)."
    },
    {
      "commit": "a1bc540e1c9855ad4cd432c736cfbacee87e9c9f",
      "tree": "40511c1249ea2ed80ab8b4b5be8bebe2af677f85",
      "parents": [
        "c4fba8d807dd64326f6d7c44b4795db3e14c983b"
      ],
      "author": {
        "name": "songzhendong",
        "email": "xiaodong12315@qq.com",
        "time": "Thu Jun 11 11:54:49 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu Jun 11 11:54:49 2026 +0800"
      },
      "message": "Add Airflow layer dashboards (SWIP-7) (#42)"
    },
    {
      "commit": "c4fba8d807dd64326f6d7c44b4795db3e14c983b",
      "tree": "b9071d93c5bf36146628ba469f04550a80e84bb1",
      "parents": [
        "61616b4395e69aa90cf98ee9d26b680e17f2059e"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Wed Jun 10 21:27:40 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Jun 10 21:27:40 2026 +0800"
      },
      "message": "feat(layer): Deployment tab — per-service instance topology with clustering and admin scope (#47)\n\n## What\n\nA new per-layer **Deployment** tab: the deployment topology of one service\u0027s instances — the instance-to-instance call graph **within** a single service, via OAP\u0027s `getServiceInstanceTopology(svc, svc)` (same id on both sides collapses the relation filter to intra-service instance relations). Where the existing Instance map drills into the instances *between two* services, this shows how one service\u0027s own instances are deployed and talk to each other (e.g. a clustered store\u0027s nodes calling each other).\n\n### The tab\n\n- Instances render as **health-ring hexagons** with a center metric; pan/zoom, animated edge flow, self-loops and bidirectional pairs drawn distinctly; per-call client/server metric sidebar; node popover with the instance\u0027s **attributes** and an **Open instance dashboard** deep link.\n- Three independent, operator-editable rules shape the graph:\n  - **`clusterBy`** — labelled dashed boxes by one attribute, several attributes (composite key; absent attributes drop out), or a name regex.\n  - **`siblingBy`** — bundle containers into **pods**: the main container as a full hex, siblings attached as half-size hexes; edges resolve to the exact container.\n  - **`roleBy` + `roles`** — per-container-type metrics and main-hex selection.\n- Cluster boxes pack **left→right along the inter-cluster call flow**; pods tier by call depth inside each box, wrap into staggered columns (max 4 per column), and stay draggable.\n- **Opt-in per layer**: `components.deployment` + a top-level `deployment` template block. No bundled layer ships it enabled; the route answers 404 without the config.\n\n### Admin \u0026 preview\n\n- The Layer-dashboards admin gains a **Deployment scope**: node / server-edge / client-edge metric editors (instance scope) plus a four-mode clusterBy picker. Draft **Preview** renders the real page before publishing.\n- The whole admin page is now **layer-list-oriented**: every layer the OAP reports is listed and editable — un-templated layers open on a blank default and publish on first save; a **Not configured** filter and banner counts make the gaps visible.\n\n### Types\n\nThe Deployment wire types live in their **own self-contained `deployment.ts`** in `api-client` (own `DeploymentMetricDef`, `DeploymentConfig`, `ClusterByRule`, `NodeRoleConfig`, …) — they share no config type with the service-map `topology` block, so the two families can evolve independently.\n\n### Also in this branch\n\n- **Every OAP-reported layer appears in the sidebar.** The hard-coded hidden-layer list is gone, replaced by a config-driven `layers.excluded` block in `horizon.yaml` (defaults: `FAAS`, `VIRTUAL_GATEWAY`; clear it to surface everything). Documented in the config reference.\n- The legacy per-layer `overview` template block (parsed but no longer rendered anywhere) is **removed** from every bundled template, its i18n catalogs, and the docs.\n- **Clearer cluster/namespace boundaries** on all topology views (service map, instance map, Deployment): theme-proof dashed `--sw-fg-3` stroke, transparent fill; the Deployment layout packs boxes evenly with no dead corridors.\n- Deployment UI strings localized in **all eight languages**.\n\n## Wire \u0026 scope notes\n\n- Pure consumer of the existing query protocol: `getServiceInstanceTopology`, `listInstances` (attributes), `execExpression` under `ServiceInstance` / instance-relation scopes. No protocol changes.\n- RBAC: the new route maps to `topology:read`.\n- The 3D infra map stays a **service-level** view — no Deployment integration.\n- Validated against the Apache demo OAP end-to-end (route gating, preview flow, empty states, admin scope, all themes); full type-check, BFF + UI tests, license headers green."
    },
    {
      "commit": "61616b4395e69aa90cf98ee9d26b680e17f2059e",
      "tree": "ce81535bc221700312d06d9f2cbefd1a13767486",
      "parents": [
        "58a4c1fb61478936f317136400d6dbd0cdda5ee1"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Mon Jun 08 22:08:25 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 08 22:08:25 2026 +0800"
      },
      "message": "feat(dashboard): structured visibleWhen widget gates with server-side eval (#46)\n\nReplaces the free-text `visibleWhen` widget predicate with a **structured gate evaluated server-side**, so a dashboard widget renders only when it\u0027s relevant to the selected entity — and a gated-out group skips its queries entirely.\n\n`visibleWhen` is now a discriminated object:\n\n```ts\ntype VisibleWhen \u003d\n  | { kind: \u0027mqe\u0027;    expression: string; op: \u0027exists\u0027 }\n  | { kind: \u0027mqe\u0027;    expression: string; op: \u0027gt\u0027 | \u0027lt\u0027; value: number }\n  | { kind: \u0027entity\u0027; attribute: string;  op: \u0027exists\u0027 }\n  | { kind: \u0027entity\u0027; attribute: string;  op: \u0027eq\u0027; value: string };\n```\n\n### Gate semantics\n- **MQE** — `exists` / `\u003e` / `\u003c`, existential across **every labeled series and bucket** of the expression\u0027s result.\n  - Naming the widget\u0027s **own** expression → *self-gate* (evaluated from the widget\u0027s own batch result; **no extra query**).\n  - Naming a **different** metric → *group-gate* on one shared signal: probed **once** (deduped, keyed by expression + `layerScope`) and the whole group\u0027s MQE is **skipped** when empty — a non-JVM instance never runs the JVM widget queries.\n- **Entity** (Instance scope only) — `exists` (present \u0026 non-empty) / `eq` (case-insensitive, e.g. `language` equals `JAVA`) against the selected instance\u0027s attributes. Service / Endpoint entities carry no attributes, so entity gates are ignored there.\n\nEvaluation moved to the BFF; the SPA drops widgets flagged `hidden` (the client-side `isVisible` is retired).\n\n### Migration\nNone. Layer dashboards saved with the old free-text predicate (`\"\u003cmetric\u003e has value\"`, `#entity.*`) are **tolerated** — the schema maps any non-conforming value to `undefined` (ungated) rather than failing the parse — and keep rendering. Re-set the gate in the new editor to restore it.\n\n## Admin editor improvements\n- Structured **Visible when** control (kind → op → value), replacing the unvalidated free-text box; corrected the inaccurate entity hint (`#entity.jvm` was fictional → `language equals JAVA`).\n- **Per-expression rows**: each MQE expression edits its `expression + label + unit + axis` together, replacing the three index-aligned parallel textareas.\n- Reusable **`MqeExpressionInput`** with an expand pop-out for long expressions — used by both the layer and overview editors.\n- Sticky, full-height widget drawer (was a short box stranded in a tall grid cell)."
    },
    {
      "commit": "58a4c1fb61478936f317136400d6dbd0cdda5ee1",
      "tree": "031b053186bf20e7ea28f85bf49eef4f72e1adc7",
      "parents": [
        "69c9036ddca3d2f0cc8fe77c12ceab4124c1fb5f"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Mon Jun 08 09:14:50 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 08 09:14:50 2026 +0800"
      },
      "message": "fix(deps): bump vitest 3.2.4 -\u003e 4.1.8 to patch CVE-2026-47429 (#45)"
    },
    {
      "commit": "69c9036ddca3d2f0cc8fe77c12ceab4124c1fb5f",
      "tree": "08f66c7982e0ab72ffe0a8703321b9a87e20fdc1",
      "parents": [
        "a5451e72c4a79ec686605ca182a2be96947d7b17"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Mon Jun 08 07:32:53 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 08 07:32:53 2026 +0800"
      },
      "message": "feat(layer): API dependency graph — expand-to-walk, draggable nodes, topology-consistent nodes, new-tab drill-downs (#44)\n\nReworks and polishes the per-layer **API dependency** (endpoint-dependency) view into a topology-consistent drill-down, then a full self-review of the branch.\n\n## What an operator sees\n\n- **Direction-aware graph.** Pick an endpoint and its caller → callee chain lays out in columns — callers left, focus centre, callees right — from a single `getEndpointDependencies` call (both directions in one request). Nodes carry the service-map\u0027s health-ring border, SLA-coloured RPM, and latency; edges animate direction and label the heaviest by RPM.\n- **Expand to walk the chain.** A selected endpoint shows one neutral **+** handle that pulls in its own callers + callees in a click (callers land left, callees right). It spins while loading; a leaf endpoint fades and shows a brief \"no further callers or callees\" banner so a silent no-op never reads as a bug.\n- **Draggable nodes.** Drag a box to pull a dense graph apart — edges follow live; distinct from background pan, and a 3px threshold keeps click-to-select working.\n- **Stable scale.** The graph holds a steady node/text size whether or not the detail sidebar is open (fit-scale cap + refit on sidebar toggle / resize), without stomping a manual zoom/pan.\n- **New-tab drill-downs.** The endpoint-dependency **Open endpoint** / **Service →** and the service-map **Open service** / **API map →** / **Instance map →** jumps now open in a new browser tab, so the graph you\u0027re exploring stays put.\n- **Topology vocabulary + i18n + a11y.** Shared SLA-band border, agent badge on instrumented endpoints, focus star; compact boxes with fonts matched to the service map; keyboard-selectable nodes/handle with `:focus-visible` rings; localized across all eight UI languages."
    },
    {
      "commit": "a5451e72c4a79ec686605ca182a2be96947d7b17",
      "tree": "98360e2e0efd33bea10282950bd3db1bcd2447ee",
      "parents": [
        "dee06ac7206ef4c03d57c0971cc01ba6e9f84e01"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sun Jun 07 19:00:33 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun Jun 07 19:00:33 2026 +0800"
      },
      "message": "feat(layer): instance-topology drill-down, all-services landing picker, remote-first templates \u0026 i18n (#43)\n\n## Summary\n\nWork on the layer experience, templates, and i18n, plus a multi-reviewer pass over the diff.\n\n**1 — Instance topology + remote-first templates + preview** \n**2 — Landing picker lists every service + landing/i18n hardening + stale-config cleanup** \n**3 — Review-pass fixes** : tail-service selection on every tab, full localization, admin/topology polish, repo hygiene.\n\n## Operator-visible changes\n\n- **Instance topology drill-down.** The per-layer **Topology** map gains an **instance map**: click a call between two services → **Instance map →** to open each service\u0027s instances as two columns (client/server) with instance-level calls, health rings, per-call metric sidebar, and an **Open instance dashboard** popover. Service pickers are relationship-aware (drawn from the topology call graph); cross-layer / conjectured callees don\u0027t offer an instance dashboard. Configurable per layer (Layer-dashboards admin → Topology → **Enable instance topology**); on by default for General, Service Mesh, Kubernetes Service, Cilium Service.\n- **The landing picker lists every service in the layer**, not just the metric-probed top-N. Services below `query.landingServiceCap` (default **100**) show **`low`** in the order-by column (`—` for the others, never probed) — still searchable and selectable. The header chip reads **\"metrics: top N\"** so the metric trim is explicit. Raise/lower the cap in `horizon.yaml`.\n- **Removed the stale \"Landing KPI tile\" (Headline / Trend line) admin controls.** They no longer matched the rendered layer header — which shows every configured metric column as its own KPI with its own trend — so editing them changed nothing on screen.\n\n## Correctness / internals\n\n- **Remote-first templates \u0026 i18n.** Runtime renders only OAP-published config (or in-code defaults); bundled disk content is never a render-time fallback — only boot-seed / admin-reset / preview. A missing OAP translation overlay falls back to **English source**, not bundled disk overlays.\n- **Dashboard + overview data caches key on the full widget config**, not just widget ids — a remote sync or preview edit that changes an MQE now refires instead of serving stale data.\n- **Config-bundle preload failure no longer strands the shell** on \"Initializing…\": it seeds an empty unreachable bundle so routes render and the connectivity banner can recover.\n- **Off-sample selected service** resolves its name from the full roster, so the header KPIs match the dashboard the body queries.\n- **Removed all dead `throughput`/`spark` landing config** end-to-end (api-client wire types, BFF landing compute + setup schema + loader migration, UI data layer + metric catalog). Scanned all 356 bundled templates — **zero** leftover config keys. The setup-save schema relaxes `.strict()` → `.strip()` so a setup persisted by an older build drops the retired keys gracefully on re-save.\n\n## Review-pass fixes\n\nA multi-reviewer pass over the diff (incl. an external reviewer) surfaced these; all fixed and validated:\n\n- **Tail-service selection now works on *every* per-layer tab.** Once the picker could select a below-cap service, logs / traces / endpoint-dependency / profiling still resolved the picked service\u0027s name from the landing sample only — so a tail service queried as blank (and Logs even snapped the pick back to the top service). Extracted a shared `useLayerServiceName` (sample → full-roster) and adopted it across all six tabs + the dashboard; fixed the Logs clobber-watch. *(Caught by an external reviewer; my own verification workflow had scoped this dimension to the picker, not the consuming tabs — noted.)*\n- **Full localization.** The instance-topology UI, the template-store-unreachable banner, and the remaining alarm / live-debugger strings are now translated in all 7 non-English locales (zh-CN, ja, ko, es, pt, de, fr) — 36 keys each, **252 strings**, placeholders + IT terms (`OAP`, `SkyWalking`, …) kept verbatim. Every catalog is now at parity with English (1323 keys).\n- **Profiling scopes no longer expose a dead widget grid.** Trace / eBPF / async profiling are runtime-configured views, so the admin shows a \"configured at runtime\" note instead of a grid whose widgets never rendered (`networkProfiling` keeps its edge editor).\n- **Instance topology distinguishes blocked from unsupported.** When OAP\u0027s template store is unreachable it serves an empty `reachable:false` response (connectivity banner shows), matching the service-topology route, instead of a misleading `not_supported` 404.\n- **Nits / hygiene.** Dropped the dead `totalCount` picker prop; guarded the `metrics: top 0` chip when landing is unreachable; fixed `.gitignore` so the bare `logs` rule no longer swallows the `apps/ui/src/layer/logs/` feature source."
    },
    {
      "commit": "dee06ac7206ef4c03d57c0971cc01ba6e9f84e01",
      "tree": "24ee82517685ee57772f1e7e69cc8fe60300b2f7",
      "parents": [
        "351941fc43a29ebf64f6b946c9cfce038fc858b9"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Wed Jun 03 16:38:04 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed Jun 03 16:38:04 2026 +0800"
      },
      "message": "fix(layer): apply scope aliases and stop empty-entity dashboard hang (#41)\n\nPer-layer Instance/Endpoint section headers and the service-picker\ncolumn header now resolve the layer\u0027s slot aliases (Brokers, Topics,\nDestinations, \"ActiveMQ clusters\", …) instead of the hardcoded scope\nwords, matching the sidebar nav.\n\nInstance/Endpoint dashboard scopes gate the widget batch on a resolved\nentity; when a service reports no instances/endpoints the query never\nfired and the page spun \"Reading data…\" forever. Detect that terminal\nempty state, suppress the overlay, and fall through to the widget grid\nso every widget renders in its empty \"no data\" state."
    },
    {
      "commit": "351941fc43a29ebf64f6b946c9cfce038fc858b9",
      "tree": "659b14530fe4ab5caf8cd69e92a2076363873a78",
      "parents": [
        "fc6d9d5b5233dc424dc878444f4459bd3a31edb0"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Tue Jun 02 18:51:27 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Jun 02 18:51:27 2026 +0800"
      },
      "message": "feat(admin): import / export for dashboard templates and translations (#39)\n\nAdd Export and Import to all four template admin editors. Export downloads\nthe in-use version (live on OAP, else the bundled default) as a JSON file;\nImport validates a file and loads it as a browser-local draft that the\noperator previews and then publishes with \"Check diff \u0026 push\" — Import never\nwrites OAP directly.\n\n- Overview templates, Layer dashboards, 3D-map config: act on the source\n  template. Import targets the template the file names (overview can create\n  or restore an id; layer targets a layer already loaded here; 3D is the\n  singleton), and rejects a wrong-kind / malformed file.\n- Translations page: per-locale Export/Import of the in-use overlay. Source\n  templates and their translations live on separate pages, so their\n  import/export are separate too — each on its own page.\n\nShared client-side helper (file download/pick, export envelope, per-kind +\noverlay validation) with unit tests; new UI strings translated across all\neight locales; CHANGELOG and operator docs updated."
    },
    {
      "commit": "fc6d9d5b5233dc424dc878444f4459bd3a31edb0",
      "tree": "114937c13d4f69221415ef787dd9c28827bc1f94",
      "parents": [
        "cc41af231112b6ef7fbbec6c6c1247692e740562"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Tue Jun 02 10:44:50 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Jun 02 10:44:50 2026 +0800"
      },
      "message": "docs: accuracy pass + concept-first config docs + principles; release tooling (#38)\n\nBring the website docs in line with the current code after the 0.6.0 work,\nrestructure the config docs around the UI (JSON as reference), codify the\ndoc-writing principles, and make the post-vote finalize script Docker-Hub\nverify-only. CHANGELOG updated under 0.7.0.\n\nStaleness fixes\n- access-control/rbac.md + setup/rbac.md: document the `infra-3d:read` verb.\n- access-control/admin-pages.md: add `/admin/3d-map` (`overview:write`).\n- access-control/audit-log.md: rewrite the recorded-actions table to the\n  actually-emitted actions; drop the never-emitted rows; fix the failure\n  outcome format (`http_\u003ccode\u003e`).\n- operate/inspect.md: correct the BFF routes to `/api/inspect/*`.\n- design-target.md: add the Topology overview widget.\n- customization/layer-templates.md: fix the components example to the real\n  flag keys; add the `dependency` dashboard scope.\n- operate/infra-3d-map.md: rewrite Configuration for the redesigned model;\n  add the Hierarchy pipeline stage.\n\nConfig docs are concept-first\n- infra-3d-map / layer-templates / overview-templates lead with the concept\n  and the admin UI; the JSON shape is a reference appendix. No raw-JSON\n  editor exists on these pages, so the docs no longer imply one.\n\nVersioning\n- Docs no longer hardcode the Horizon version (the website pins docs per\n  release); container-image.md stays as the release-tooling exception.\n\ni18n\n- i18n.md: language x scope coverage matrix.\n- adding-a-new-layer.md: add the \"Add translations (i18n)\" contributor step.\n\ndocs/CLAUDE.md (new)\n- Operator-first, config-UI-first with JSON-as-appendix, no hardcoded\n  versions, plus a carve-out for contributor pages.\n\nRelease tooling\n- release-finalize.sh: the Docker Hub image is published by CI on `v*` tags;\n  Step 5 now verifies the published tags, and the manual local-push fallback\n  plus Docker Hub login preflight were removed."
    },
    {
      "commit": "cc41af231112b6ef7fbbec6c6c1247692e740562",
      "tree": "091bf5185ec22a4641c05c66101297d0bcfcb267",
      "parents": [
        "f79dbe41f095112a77720ba86df3df574f0ed42b"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Tue Jun 02 08:26:33 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue Jun 02 08:26:33 2026 +0800"
      },
      "message": "Release 0.6.0, bump to 0.7.0-dev (#37)"
    },
    {
      "commit": "f79dbe41f095112a77720ba86df3df574f0ed42b",
      "tree": "52ae498e119d5ef5cf885a73eeeb54dbe3ab88ab",
      "parents": [
        "70610c6b9781bcefaecaf05b38d3d8a7d9fb3468"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Mon Jun 01 22:43:56 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 01 22:43:56 2026 +0800"
      },
      "message": "feat(infra-3d): internationalize the 3D-map config admin page (#36)\n\nThe /admin/3d-map config editor was the only admin page still rendering\nhardcoded English in every locale — it landed after the 0.6.0 i18n pass.\nWire it through vue-i18n like the other admin pages and ship full\neight-locale coverage.\n\n- Infra3dAdminView.vue: 58 t() keys plus 5 \u003ci18n-t\u003e blocks for the prose\n  hints with inline \u003ccode\u003e/\u003cstrong\u003e; named-arg interpolation for the\n  dynamic counts, the via tag, and the Push-button state. Layer keys,\n  MQE ids, and the .* regex stay verbatim.\n- en.json: 55 new source keys.\n- de/es/fr/ja/ko/pt/zh-CN: the 55 keys translated, matching each\n  catalog\u0027s existing terminology."
    },
    {
      "commit": "70610c6b9781bcefaecaf05b38d3d8a7d9fb3468",
      "tree": "1780fa5e3aaf086aefd9939dc8f368e52b24e655",
      "parents": [
        "eff25e9c58706735fc45d2a40b302d95779cd20a"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Mon Jun 01 20:53:02 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 01 20:53:02 2026 +0800"
      },
      "message": "feat(infra-3d): redesign the 3D-map config as an OAP template kind (#35)\n\nThe 3D Infrastructure Map config is now a first-class OAP template kind\n(horizon.infra-3d.config) instead of a BFF-local file, so it follows the\nsame bundled -\u003e local(browser) -\u003e remote(OAP) flow as the layer / overview\ndashboards: edit a local draft, \"Check diff \u0026 push\" to publish, remote\nwins at render with bundled as the fallback.\n\nAdmin editor (/admin/3d-map):\n- Combined \"Tiers \u0026 layers\" view: each layer is pinned to one tier via a\n  dropdown; explicit pins + a single failover tier are the only placement\n  mechanism. Removing from a tier unpins (never deletes).\n- One traffic metric per layer (mqe / label / unit) -- the topology/load\n  and per-tier-regex models are gone. The single global filter.layer is the\n  top-level gate: an excluded layer is off the map AND off the live pipeline.\n- Logic-groups editor (members / color / icon / tier); a layer belongs to\n  at most one group; read-only Service-map-layers section.\n- Save-local / Monaco diff / push; OAP-unreachable and BFF-unreachable\n  states surfaced instead of an indefinite spinner.\n\n3D map (/3d/map):\n- Restored the cross-layer Smartscape hierarchy the dynamic loader had\n  dropped (hierarchy: []). Fetched incrementally -- only newly-appeared\n  services are probed, the rest reused from a persistent cache -- and\n  revealed on cube-select. Folded into the scene re-key signature.\n- Fixed live metric resolution after the single-metric migration.\n\nCross-cutting:\n- Auto-refresh + global time picker disabled on all /admin/* and /operate/*\n  routes (no time context).\n- Renamed the topology model Demo* -\u003e Map* (useMapTopology /\n  fallback-topology.json) -- it carries live data, not a demo snapshot.\n- RBAC: 3D-map admin page gated on overview:write (the verb its push uses);\n  removed the orphaned infra-3d:write verb and the dead infra3d.file config\n  (kept accepted-and-ignored so existing configs still boot)."
    },
    {
      "commit": "eff25e9c58706735fc45d2a40b302d95779cd20a",
      "tree": "3741ad6ac8e0917bfbb80304d57f2c0985426d6d",
      "parents": [
        "a8081d1a007837ba17844083e43f9aad478cca55"
      ],
      "author": {
        "name": "Wan Kai",
        "email": "wankai123@foxmail.com",
        "time": "Mon Jun 01 15:59:41 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon Jun 01 15:59:41 2026 +0800"
      },
      "message": "Fix Alerting rules running contest doesn\u0027t show on which OAP node and the running details. (#34)"
    },
    {
      "commit": "a8081d1a007837ba17844083e43f9aad478cca55",
      "tree": "da7a7477802e100394dd1fc2f71e33561dc39b4e",
      "parents": [
        "a0ab0f7782c720dacc013d0aa9d580e42db7aad6"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sun May 31 20:53:51 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 31 20:53:51 2026 +0800"
      },
      "message": "fix: 3D-map fps cap, layer-URL hydration, API dependency zoom (#33)\n\n## 1. 3D map — render-loop CPU cap\nThe `/3d/map` scene renders continuously; on a high-refresh display the loop ran at 60–120fps and pegged a CPU core (the per-frame cientos `\u003cHtml\u003e` reprojection for ~80 chips/labels dominates). Cap the whole loop with TresJS\u0027s native `fpsLimit\u003d30` — render + animation + `\u003cHtml\u003e` updates all throttle together, roughly halving (or quartering at 120Hz) the renderer CPU with no visual change.\n\n## 2. Per-layer URL / dashboard hydration fixes\nA chain of related deep-link / config-readiness bugs in the layer shell + dashboards:\n- **Same-layer rehydrate** — the shell watched only `layerKey`, and `resetForLayer` early-returned on the same layer, so a same-layer deep link with new `?service/?instance/?endpoint` kept the stale pick. Watch the seed params and re-seed on same-layer params (sticky pick preserved for paramless tab nav).\n- **`?service\u003d` outside the landing sample** — validated only against the top-N sample, so a valid low-traffic service was overwritten by the first row (clearing instance/endpoint). Validate against the full roster (`useLayerServices`); only a genuinely-absent id auto-corrects.\n- **`?endpoint\u003d` outside the recent top-N** — discarded; add a targeted name lookup, and make `effectiveEndpoint` accept the confirmed pin so it actually drives the fetch.\n- **Metrics before config** — the dashboard query fired with an empty widget list (→ BFF defaults) before the config bundle resolved. Gate on resolved widgets; an empty resolved config shows **No widgets defined** instead of looping on **Reading data…**.\n- The page-init orchestrator now receives the *effective* (validated) entity refs, matching the actual fetch gate.\n\n## 3. API dependency graph — zoom + fit + name clipping\nThe endpoint-dependency graph rendered at full width in a scroll container, so wider chains clipped and edges to off-screen nodes looked like stray lines. Now:\n- **Fit-to-view by default** (viewBox + `preserveAspectRatio`) — every column visible, no dangling edges.\n- **Zoom** via wheel + drag-pan + ＋/−/fit buttons (toolbar over the canvas).\n- **Clip node text** to the box (clipPath + tighter truncation) so long endpoint names no longer overflow.\n- Drop the `L0 / L+1` column-name headers (operator feedback)."
    },
    {
      "commit": "a0ab0f7782c720dacc013d0aa9d580e42db7aad6",
      "tree": "8253d7f69206657585e8c7986fb31d3e942e8b0a",
      "parents": [
        "94f68e37b7f1d4247e4c636a54ea08ae93f6dadb"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sat May 30 23:22:42 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 30 23:22:42 2026 +0800"
      },
      "message": "feat(infra-3d): live topology + full/light refresh for the 3D map (#32)\n\n## Live topology + refresh model for the 3D Infrastructure Map\n\nBuilds `/3d/map` from **live OAP** (per-layer `services` + `topology`) instead of the committed snapshot, with a deliberate two-tier refresh, a tuned camera, and the experimental label dropped. Backend contracts unchanged.\n\n### Initial load — render once, complete\nHeld behind a **\"Reading topology…\"** gate until the full context (layers, templates, services, topology, clustering) is assembled, then rendered **once** — no snapshot/partial flash. `liveTopo` publishes atomically at the end of the topology stage. `?live\u003d0` forces the committed snapshot for comparison.\n\n### Refresh — full vs. light\n- **Landing + manual ↻** run the **FULL** pipeline; templates + layout/clustering resolve **once** here and the known-layer set is captured.\n- **60s auto-refresh** runs **LIGHT** — only **services + topologies + metrics** re-run (`run()` gained an `only` stage filter); templates + layout keep their last full-run state.\n- A layer appearing only on a light refresh has no template yet → its services are **hidden** until the next full refresh, and **listed in the services stage\u0027s status detail**.\n\n### Camera + interaction\n- Initial framing tuned to a near-front **azimuth 15° / polar 62°** 3/4 heading (spherical, scale-invariant), shifted left of the TIERS panel. The **reset** and **side-panel focus** share the same heading, so panel clicks glide without changing the angle (this also fixed the selection flash appearing a tier low).\n- WASD/arrow keyboard pan step halved.\n\n### Polish\n- Header counts/scope chip removed (the bottom strip already carries them).\n- Stage-detail drawer no longer occluded by the brand mark.\n- Topbar pill no longer says **EXPERIMENTAL**; hover expands to the full **3D Infrastructure Map**.\n\n### Validation \u0026 review\nLive OAP replay: 73 services / 17 layers, 3 topology maps, ~6s, zero errors. UI + BFF `type-check` and `license-eye` clean. A 5-dimension adversarial review (live-data, render-gating, camera, lifecycle/flash, regression) found **no must-fix functional bug** introduced by this branch — confirmed items were by-design (the light/full split), pre-existing on main (cross-edge/pan NaN — the latter unreachable, polar capped at 88°), or a known BFF protocol limitation (metrics keyed by service name)."
    },
    {
      "commit": "94f68e37b7f1d4247e4c636a54ea08ae93f6dadb",
      "tree": "c8942fbe03d2b0de0b2814f59f91af9b913a2d0c",
      "parents": [
        "0fb700f2983ab7d2687f6eb2e70ebec725c686d7"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Fri May 29 22:20:58 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri May 29 22:20:58 2026 +0800"
      },
      "message": "feat(infra-3d): 3D infrastructure map — live metrics \u0026 alarms, topology clustering, tier navigation (#30)\n\n## 3D Infrastructure Map — enhancement pass\n\nBuilds the `/3d/map` proof-of-concept into a usable bird\u0027s-eye view of a deployment: services as cubes on stacked tier-planes, live metrics/alarms, topology clustering, and tier-by-tier navigation. Backend contracts are unchanged (same OAP GraphQL query-protocol + MQE).\n\n### Operator-visible changes\n\n- **Live data windows.** Auto-refreshes every minute — per-cube traffic rolls up the last 2h of metrics (HOUR step), and services with a still-firing alarm light up from the last 20m of alarms (recovered ones don\u0027t), matched to the cube by `(layer, name)`. A toolbar chip shows the active scopes; an alarmed cube burns red with a radiating ripple.\n- **Beacon mode.** Dims healthy cubes to wireframe ghosts so firing services stand out during an incident.\n- **Logic groups.** Related layers cluster into one labelled block on a tier; the bundled config ships a **Self-Observability** group (OAP, Satellite, BanyanDB, Java/Go agents) on the middleware tier.\n- **Topology clustering.** Within layers that carry a service map, services group into named clusters drawn as a wireframe frame with the cluster name baked into the lower-left corner — service-mesh by showcase group, Kubernetes by namespace. Follows each layer\u0027s naming rule; un-ruled layers stay flat.\n- **Navigate by tier.** The right-side **Tiers** panel is a two-level tree (tier → layer / logic-group). Clicking an entry resets the view, glides the camera to face that region, zooms in, and flashes the region briefly. A **Reset** button restores the initial framing.\n- **Hover \u003d preview.** Hovering a cube shows the same detail card as a click (tier / layer / service / group / cluster), anchored at the cube, minus the open-in-dashboard link.\n- **Call-direction flow.** Call relationships animate directional particles so traffic direction reads at a glance.\n- **Focus one layer.** The per-layer service map gains a **View in 3D** link that opens the map focused on just that layer.\n- **Configurable tiers + layers.** Tier order, per-layer plane mapping, cube colors, and the per-layer traffic MQE are all driven by the 3D map config (bundled defaults, admin-overridable; config now validates `group.level` and rejects duplicate group ids).\n\n### Fixes also in this branch\n\n- Brand link + post-login redirect no longer resolve to an empty address.\n- Trace span detail labels span direction as **Kind** (Local/Entry/Exit) instead of the mistranslated verb; the cross-trace refs column is now **Ref type** (its values are OAP `RefType`: CROSS_PROCESS/CROSS_THREAD, a distinct concept) across all eight locales; long tag keys wrap instead of overflowing.\n- The admin \"Topology cluster setup\" card now lives inside the Topology scope instead of a top-of-page banner.\n\n### Validation\n\n- UI + BFF `type-check` clean; `license-eye` header check clean (0 invalid).\n- The 3D scene is still **snapshot-backed** (`data/demo-topology.json`) for the always-on structure; **metrics and alarms are live**. Wiring the scene\u0027s service/topology structure to live OAP is the remaining follow-up (see below) — visual sign-off should be done against the live demo OAP.\n\n### Follow-up (not in this PR)\n\n- **Live sequential delta pipeline:** load the service/topology structure live, one stage at a time (service → topology → hierarchy → metrics) with low concurrency, the countdown anchored to the next stage, and delta-applied to the scene. This is the one remaining item; it rewrites the scene\u0027s data source (highest blast radius) and needs visual validation, so it\u0027s deliberately staged separately."
    },
    {
      "commit": "0fb700f2983ab7d2687f6eb2e70ebec725c686d7",
      "tree": "f36cc896291993faaa4ae05b7c37a1c933af58ce",
      "parents": [
        "10b3ee83fac9d2f586b48b5c9219ce98df018339"
      ],
      "author": {
        "name": "Daniel Jeong",
        "email": "hyun0524e@naver.com",
        "time": "Fri May 29 12:37:36 2026 +0900"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri May 29 11:37:36 2026 +0800"
      },
      "message": "fix(i18n): polish Korean locale follow-up (#31)"
    },
    {
      "commit": "10b3ee83fac9d2f586b48b5c9219ce98df018339",
      "tree": "256a79451b2cd7f725ab3aa3e030eb5c0d0215c3",
      "parents": [
        "3c1215d0ec5ea1dcac0ec2679f53cd2d2ea4cfb1"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 28 21:14:11 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu May 28 21:14:11 2026 +0800"
      },
      "message": "fix: slide session cookie maxAge + clear 3D selection on empty-plane click (#29)\n\n* fix(auth): slide the session cookie maxAge on each request\n\nThe server slides a session\u0027s TTL on every authenticated request\n(`sessions.touch()`), but the cookie\u0027s maxAge was stamped only at\nlogin. So an actively-used session kept sliding server-side while the\nbrowser cookie still expired at login + ttl — the user was silently\nlogged out mid-session. requireAuth now re-stamps the cookie (same\noptions as login, only maxAge refreshed) after a successful touch, so\nthe browser expiry tracks the sliding server TTL.\n\n* fix(infra-3d): clear selection when clicking empty plane space\n\npmndrs `pointermissed` only fires on a true void hit, so clicking a\ncolored zone/tier plane (raycast left on so planes occlude cubes) never\ncleared the selection — only clicking past every plane did. A deselect\nhandler on the planes isn\u0027t an option: operators orbit-drag from empty\nplane space to rotate WITHOUT changing selection.\n\nDetect the empty click at the DOM level on the canvas, guarded so it\nonly fires for a genuine empty click:\n  - target is the \u003ccanvas\u003e (not a portaled \u003cHtml\u003e overlay — clicking\n    the detail card must not deselect)\n  - pointer barely moved (a click, not an orbit/pan drag)\n  - no cube absorbed the gesture (onNodeClick stamped within 120ms)\n\nReplaces the @pointermissed wiring. Needs a live click-test before merge."
    },
    {
      "commit": "3c1215d0ec5ea1dcac0ec2679f53cd2d2ea4cfb1",
      "tree": "16fd658f301b77da7f5d20ef364d1ac25ddf8175",
      "parents": [
        "05d948da7ed3188407f785815a9c63b844454399"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 28 20:35:52 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu May 28 20:35:52 2026 +0800"
      },
      "message": "fix(infra-3d): GPU geometry leak, metrics chunk cap, config-load failure state (#28)\n\n* fix(infra-3d): GPU geometry leak, metrics chunk cap, config-load failure state\n\nAddress review findings on the 3D Infra Map:\n\n- HIGH — TubeGeometry leak. callTubes / crossTubes / verticalTubes /\n  hierarchyTubes mint a fresh TubeGeometry per edge on every recompute\n  (layer-visibility toggle, selection change), but only the current\n  batch was freed on unmount. Dispose the previous batch when each set\n  changes (flush:\u0027post\u0027, after the new geometries have rendered).\n\n- MED — metricChunkSize cap mismatch. Admin/validation allowed up to 50\n  while the metrics route rejects \u003e12 (OAP complexity ceiling), so any\n  oversized chunk 5xx\u0027d. Cap config validation + the admin input at 12.\n\n- MED — config-load failure state. /3d/map\u0027s ensureLoaded() had no\n  try/catch, so a reject (OAP offline, or a role without infra-3d:read)\n  hung on \"Loading 3D map configuration…\" forever. Catch it and show a\n  reason + access hint + Back link instead.\n\n- LOW — remove orphaned .plane-label CSS and a stale \"label rendering\"\n  prop comment (no plane-label render path exists).\n\nValidated: UI + BFF type-check, UI build, eslint clean on touched files.\n\n* fix(infra-3d): keep traffic chips under the chrome panels\n\nThe per-cube traffic chip set no cientos z-index-range, so it inherited\nthe default (~16.7M max) and a near-camera chip painted over the side\npanel / header (z-index 40-70). Pin the chip range to [35, 1] so it\nsits above the canvas but below all chrome, matching the tooltip/card\nwhich already force explicit ranges (theirs high, to stay on top).\n\n* feat(infra-3d): red top-cap alarm style + calmer cube colors\n\n- Alarm style: drop the pulsing red beacon sphere/halo; an alarmed cube\n  now wears a static red cap over its top ~20% (0.22 of the 1.0-tall\n  cube), footprint slightly oversized to clear the cube faces, raycast\n  disabled so it can\u0027t steal the cube\u0027s clicks. Reads as a state, not an\n  animation.\n- Cube colors were too saturated against the dark scene (the cache red\n  especially). Pull each resolved layer color toward a calmer tone\n  (saturation ×0.6, slight lightness bump) for both the default and\n  hover cube materials. Side-panel swatches keep the full tint."
    },
    {
      "commit": "05d948da7ed3188407f785815a9c63b844454399",
      "tree": "3f16cda18db07bbdc04425f00030bdb4d73044c1",
      "parents": [
        "404aa9bc14382ebfa64ee915073733b9b6641577"
      ],
      "author": {
        "name": "Daniel Jeong",
        "email": "hyun0524e@naver.com",
        "time": "Thu May 28 21:12:40 2026 +0900"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu May 28 20:12:40 2026 +0800"
      },
      "message": "fix(i18n): improve Korean locale wording (#26)"
    },
    {
      "commit": "404aa9bc14382ebfa64ee915073733b9b6641577",
      "tree": "2aeb46d1f2f7e1590ffdcfd5bb0d5d13a573f61b",
      "parents": [
        "8a2cccc98cd7de3d472798fbd0fdaaafd0ce149c"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 28 19:45:34 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu May 28 19:45:34 2026 +0800"
      },
      "message": "fix(deps): resolve all 5 Dependabot advisories (js-cookie/ws/esbuild/vite) (#27)\n\n* fix(deps): patch js-cookie / ws / esbuild Dependabot advisories\n\nResolve three dev/test-tooling security advisories via pnpm overrides\n(none ship in the production bundle):\n\n- js-cookie \u003e\u003d3.0.7 (HIGH, GHSA-qjx8-664m-686j — prototype hijack in\n  assign()); pulled in transitively by @vue/test-utils -\u003e js-beautify.\n- ws \u003e\u003d8.20.1 (GHSA-58qx-3vcg-4xpx — uninitialized memory disclosure);\n  via jsdom (test env).\n- esbuild \u003e\u003d0.25.0 (GHSA-67mh-4wv8-2f99 — dev-server request forgery);\n  also bumps the BFF\u0027s direct esbuild devDep to ^0.25.0.\n\nValidated: BFF + UI builds, UI type-check, and all 69 unit tests pass\nwith the forced versions (esbuild 0.25 builds cleanly under vite 5).\n\nThe remaining vite advisory (GHSA-4w7w-66w2-5vf9) has no 5.x patch and\nneeds a coordinated vite 5-\u003e6 upgrade — tracked separately.\n\n* fix(deps): upgrade vite 5-\u003e6 + vitest 2-\u003e3 to clear path-traversal advisory\n\nGHSA-4w7w-66w2-5vf9 (Vite path traversal in optimized-deps .map handling,\ndev-server only) has no 5.x backport — the lowest fix is vite 6.4.2. Bumps\nthe build/test stack together:\n\n- vite ^6.4.2\n- @vitejs/plugin-vue ^6.0.7, @vitejs/plugin-vue-jsx ^5.1.5\n- vitest ^2.1.4 -\u003e ^3.2.4 (vitest 2 peers vite ^5 only; vite 6 needs 3+)\n\nValidated: UI build, type-check, all 69 unit tests, and a vite-6 dev-server\nsmoke (root + entry 200, optimizer re-ran). vite.config needed no changes.\nCloses the last of the five Dependabot advisories.\n\n* fix(deps): bump BFF vitest 2-\u003e3 so no vite 5 lingers transitively\n\nThe BFF\u0027s vitest@2 still pulled vite@5.4.21 (in the advisory\u0027s \u003c\u003d6.4.1\nrange). Align it to ^3.2.4 alongside the UI bump; BFF\u0027s 80 unit tests\npass. The dependency tree now resolves a single vite (6.4.2)."
    },
    {
      "commit": "8a2cccc98cd7de3d472798fbd0fdaaafd0ce149c",
      "tree": "ee10b82aec4837528ba8405030856daa77de5e98",
      "parents": [
        "087ac65508478483937e3788f60ba09c7bd08e95"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 28 18:29:25 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu May 28 18:29:25 2026 +0800"
      },
      "message": "feat(pod-logs): on-demand Kubernetes pod-log live-tail tab (#25)\n\nA per-layer \"Pod Logs\" tab live-tails a pod\u0027s container logs, fetched on\ndemand from the K8s API through OAP (listContainers / ondemandPodLogs)\nand never persisted. Instance-pinned: pick a pod, pick a container,\nStart; the trailing SECOND-precision window streams into a read-only\nMonaco pane on a chosen interval until paused. Include / Exclude forward\nto OAP\u0027s keywordsOfContent / excludingKeywordsOfContent (full-line regex).\n\n- BFF: new GET/POST /api/layer/:key/pod-logs(/containers) routes\n  (logs:read), epoch-seconds -\u003e ms, errorReason passthrough for the\n  feature-disabled and stale-pod cases.\n- Caps: podLogs component flag on k8s_service, mesh, mesh_dp bundled\n  templates; mergeComponentFallback back-fills flags a remote OAP-stored\n  template predates, so the tab surfaces without a re-push.\n- UI: sidebar tab + caps-gated route, layer-template admin toggle, topbar\n  auto-refresh + time-picker opt-out (the page runs its own tail loop).\n- i18n: the new view is fully wrapped in t(); 22 keys seeded across all\n  eight locales.\n\nAlso fixes the template-admin reset-from-bundled flow: Save / Check diff\n\u0026 push now gate on editor-vs-remote (not just vs the load snapshot), so a\nreset to bundled is publishable when bundled differs from remote (layer +\noverview editors); the unpublished-edits nudge is suppressed on the admin\neditor routes; and the save / flash message moved to its own row so it no\nlonger overlaps the action buttons.\n\nCHANGELOG + CLAUDE.md: record the feature and bundled-template changes,\nand add the changelog policy (new features + bundled-template changes go\nin the changelog; released sections take bug fixes only)."
    },
    {
      "commit": "087ac65508478483937e3788f60ba09c7bd08e95",
      "tree": "976f9b9c03eb15734fbc6fa9531d87e7a3e98625",
      "parents": [
        "4b408af07d1f2e99a08b46c6cf4d584015a3a498"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 28 11:53:46 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu May 28 11:53:46 2026 +0800"
      },
      "message": "feat(infra-3d): config-driven 3D Infrastructure Map (#24)\n\nA standalone WebGL view at `/3d/map` that stacks every OAP layer\u0027s\nservices as cubes onto configurable **tiers** (apps / service mesh /\nmiddleware / infra), with live traffic, alarm beacons, and call\nrelationships drawn between them. Plus an admin config editor and an\noperator guide.\n\nThis turns the earlier 3D map PoC into a complete, config-driven feature.\n\n## What\u0027s included\n\n**View (`/3d/map`, standalone full-screen, verb `infra-3d:read`)**\n- Config-driven tiers + per-layer color (replaces the hard-coded 3-plane PoC).\n- Per-cube traffic chips from a staged, chunked MQE fetch; chips share the cube\u0027s depth, hide when zoomed out or occluded by an upper tier.\n- Past-20m alarm beacon — a pulsing red marker on the cube corner; the cube keeps its layer color.\n- Three edge classes: in-layer calls (cyan + animated packets), cross-layer same-tier calls (orange, arrowed), and selection-gated hierarchy links (gray) showing a selected cube\u0027s cross-tier identity.\n- Per-zone project logo stamped into the tier surface as a baked texture (Istio + Kubernetes use their project marks).\n- Bottom loading-timeline strip (services → templates → topologies → layout → metrics) with per-stage detail drawers.\n- Side panel simplified to tiers only (focus + show/hide per tier).\n\n**Admin (`/admin/3d-map`, verb `infra-3d:write`)**\n- Structured config editor over the live OAP layer catalog: global + per-tier layer filters, tier ordering, per-layer color, traffic MQE (topology server/client or single load), edge styling, plus an advanced raw-JSON view.\n\n**BFF**\n- Bundled 3D-map config (4 tiers, 52 layers; traffic MQEs seeded from layer dashboard templates) with a file-backed override store.\n- `GET`/`POST /api/infra-3d/config` (+ bundled baseline) with strict zod validation; `POST /api/infra-3d/metrics` for batched per-service MQE.\n- Verbs `infra-3d:read` (all roles) and `infra-3d:write` (operator/admin).\n\n**Incidental fix**\n- Setup-store `ensure()` ran its reconciliation (incl. an unconditional `headerColumns` reassignment) on every call; reading it from a computed produced \"Maximum recursive updates exceeded\" and froze layer pages. Reconciliation is now once-per-snapshot and `useLandingOrder` reads priority side-effect-free.\n- `RuleCard.vue` `statusLabel` switched to a keyed record (removes a non-returning computed lint error on the default branch).\n\n**Docs**\n- Operator guide: `docs/operate/infra-3d-map.md` (feature overview + what tiers are + the four bundled tiers), linked in `docs/menu.yml`."
    },
    {
      "commit": "4b408af07d1f2e99a08b46c6cf4d584015a3a498",
      "tree": "b6c82e590197f6bb44b71c64af39d13c3be27578",
      "parents": [
        "825014f9f8a44b3af0e11afa715c9580722794f7"
      ],
      "author": {
        "name": "Wan Kai",
        "email": "wankai123@foxmail.com",
        "time": "Thu May 28 10:52:02 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Thu May 28 10:52:02 2026 +0800"
      },
      "message": "Disable trace/logs under k8s service dashboard (#23)"
    },
    {
      "commit": "825014f9f8a44b3af0e11afa715c9580722794f7",
      "tree": "ff0038b553521196ec8f7786ad04ab26e920f8ab",
      "parents": [
        "c13f7b09436b787ea78f25dc124538391d48847d"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Wed May 27 22:35:57 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed May 27 22:35:57 2026 +0800"
      },
      "message": "fix(logs): log-entry popout reads cramped — min-height + wider cap (#22)"
    },
    {
      "commit": "c13f7b09436b787ea78f25dc124538391d48847d",
      "tree": "3e88cc4ff0430bfdee4ca41eaa081cd523611702",
      "parents": [
        "f357ea3f95f82a8c6989e24e464babe54e24b745"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Wed May 27 20:04:57 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed May 27 20:04:57 2026 +0800"
      },
      "message": "chore: env-driven dev boot + zipkin span overlay UX + log row fix (#21)\n\nLocal dev:\n- Vite (`apps/ui/vite.config.ts`) reads `UI_DEV_PORT` and `BFF_PORT` from\n  env, defaults 9091/8081, so two parallel envs can coexist on the same\n  box (e.g. alongside the legacy booster-ui on 8080).\n- All bundled BFF configs (`horizon.local.yaml`, `horizon.demo.yaml`,\n  `horizon.ldap.yaml`) honour `${BFF_PORT:8081}` for `server.port` via\n  the YAML loader\u0027s existing interpolation.\n- `horizon.local.yaml` now accepts `OAP_QUERY_URL` / `OAP_ADMIN_URL` /\n  `OAP_ZIPKIN_URL` / `OAP_TIMEOUT_MS` env overrides, collapsing the\n  separate `horizon.remote.yaml` (deleted) and `horizon.unreachable.yaml`\n  (deleted) into one canonical no-auth config that flexes via env.\n- SKILL.md gains an \"Environment variables (the dev contract)\" reference\n  table at the top, plus deeper \"Custom ports\" (two parallel envs, prod\n  single-port) and \"Custom OAP target\" sections. Boot recipes throughout\n  switched from hard-coded ports to `${BFF_PORT:-8081}` /\n  `${UI_DEV_PORT:-9091}` shell expansions.\n\nZipkin trace UX (`LayerZipkinTracesView.vue`, `ZipkinTracePopout.vue`):\n- Span detail panel now floats over the right edge of the waterfall as a\n  `min(640px, 60%)`-wide overlay instead of compressing the waterfall to\n  a fixed 320px sidebar. Long tag values (URLs, JSON bodies) read on one\n  line without truncating bars.\n- Click-outside the panel or pressing Escape dismisses the pin. In the\n  popout, Escape unwinds one layer at a time: clear span first, then\n  close the popout.\n- Trace-detail card in selection mode uses the same sticky / max-height\n  shape as the rail, and the grid is now `align-items: stretch` so the\n  two cards share row height instead of one collapsing to content.\n- Global keydown + mousedown listeners on the popout are wired through\n  `onMounted` / `onBeforeUnmount` instead of leaking at module scope.\n\nLogs list (`LayerLogsView.vue`):\n- Fix: rows without a `traceId` collapsed the 6-column grid one slot\n  leftward, sizing the 1fr content cell to the 60px trace slot and\n  ellipsing the preview at ~2 characters. The trace-link slot is now\n  always rendered (link or empty placeholder), preserving the grid."
    },
    {
      "commit": "f357ea3f95f82a8c6989e24e464babe54e24b745",
      "tree": "311c5f6446d233b199be7433eec1e5b329dd35f8",
      "parents": [
        "5bfd3dcc361a0618be8746fefa0a9c116ee149d8"
      ],
      "author": {
        "name": "Zixin Zhou",
        "email": "codeprince2020@163.com",
        "time": "Wed May 27 08:10:26 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed May 27 08:10:26 2026 +0800"
      },
      "message": "docs: fix UI dev server port (#20)"
    },
    {
      "commit": "5bfd3dcc361a0618be8746fefa0a9c116ee149d8",
      "tree": "06961e1fde0e02febffe41af945e22db7c6ca3dc",
      "parents": [
        "d43d73fe8545bcc41dedd716b1f46c944e3f21a1"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Wed May 27 08:07:42 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Wed May 27 08:07:42 2026 +0800"
      },
      "message": "fix(bff): wait for OAP admin readiness before bootSeed (#19)\n\n* fix(bff): wait for OAP admin readiness before bootSeed; warn-per-fail; no periodic polling\n\nPreviously `bootSeed()` was fire-and-forget on the very first listen\ncallback — a single `client.list()` attempt, and if OAP\u0027s admin port\nwasn\u0027t bound yet (compose / k8s rollout where OAP starts after the\nBFF binds, or the admin module is wired lazily) the function logged\none warn and returned `unreachable: true` with no retry. Every\nbundled template stayed un-pushed for that BFF lifetime; the only\nrecovery was an operator restart of the BFF after OAP came up.\n\nAdds `waitForOapAdminReady(deps, signal)` in\napps/bff/src/logic/templates/sync.ts that uses `client.list()` as a\nreadiness check, backs off 1s → 2s → 4s → … capped at 60s, and\nlogs one warn per failed attempt. Each warn carries `attempt` and\n`nextRetryInMs` so an operator grepping logs sees the wait progress\nwithout ambiguity. On the first successful list it returns and the\nloop terminates — there\u0027s no steady-state polling: the admin port is\nonly touched again when an operator triggers a UI sync or an\nadmin-edit action.\n\nWired into server.ts as a two-step boot:\n  1. `await waitForOapAdminReady(deps, bootSeedAbort.signal)`\n  2. `await bootSeed(deps)` (unchanged — still absent-only via\n     `seedMissing`, so a fully-seeded OAP from a prior boot is a\n     no-op write-wise).\n\nThe wait is cancellable via an AbortController bound to shutdown so\nthe BFF process can exit cleanly when signal-killed mid-backoff (k8s\nrolling restart, ctrl-c) instead of blocking the close handler.\n\nIf `client.list()` flaps between the readiness probe and the\nsubsequent `bootSeed` (very narrow window) we still log a clear warn\non the partial seed; the absent-only semantics protect operator\nedits in that case.\n\nTested: 80 / 80 BFF unit tests pass.\n\n* fix(bff): gate bundled-template fs.watch behind NODE_ENV\u003ddevelopment\n\nBundled templates ship inside the BFF image — they\u0027re immutable in\nproduction (changing one means a rebuild + redeploy), so the\nfs.watch is pure overhead there: a file descriptor held forever\nplus a debounce timer that never fires. The watcher exists purely\nfor the dev-loop convenience of seeing JSON edits without an\n`Editor: restart` chord; `tsx watch` only reloads on `.ts`\nchanges.\n\nGate tightened from `NODE_ENV !\u003d\u003d \u0027test\u0027` (which left it running\nin every non-test env including prod) to `NODE_ENV \u003d\u003d\u003d \u0027development\u0027`.\nThe dev script in apps/bff/package.json already sets\n`NODE_ENV\u003ddevelopment`, so local dev keeps the hot-reload behavior;\nprod / staging / docker images run without the watcher.\n\n* fix(menu): prefer remote OAP layer template over disk bundled when both exist\n\nThe sidebar\u0027s `/api/menu` route always called `getLayerTemplate(rawKey)`\nto source per-layer metadata (alias, color, components, slots, caps,\ndocumentLink, metrics, overview, log, traces, naming). That helper\nreads ONLY from the bundled in-process cache (disk JSON files), never\nfrom OAP. So after an operator edited a layer template via the admin\nUI and pushed it to OAP, the sidebar kept rendering the disk-bundled\ncopy forever — `components` toggles, `caps`, `slots`, `alias` changes\nwere invisible there even though the per-layer pages (which go\nthrough `/api/configs/bundle` and its `pickLayerContent`) reflected\nthem correctly. Two read paths over the same data, one merging\nremote + bundled and one ignoring remote, surfaced as \"I edited\ncomponents, but the sidebar still shows the bundled set.\"\n\nFix mirrors the bundle endpoint\u0027s precedence:\n  1. remote OAP UI-template row, when present + not disabled\n     (operator\u0027s published edits go live the moment they push)\n  2. disk-bundled template, when remote is absent\n  3. null when neither exists (caller falls back to LAYER_DEFAULTS).\n\nReuses the same `getSyncStatus()` read the disabled-filter already\ndoes — single sync-status call now yields both the disabled set AND\nthe per-name remote row map. Two-key projection on the existing\n30s sync cache; no extra OAP round-trips.\n\n`disabledLayerKeys` is renamed to `layerSyncSnapshot` and returns\n{ disabled, layerRowsByName }; the live-path and OAP-unreachable\nfallback both go through the new `resolveLayerTemplate(rawKey,\nlayerRowsByName)` helper. The unreachable fallback passes an empty\nrow map so it falls cleanly to bundled, identical to the prior\nbehavior on that path.\n\n* fix(landing): block dashboard render when OAP query host is unreachable\n\nPolicy: admin-host unreachable is acceptable (the operator can still\nbrowse existing dashboards, admin pages drop to read-only via\nAdminFeatureWarning), but query-host unreachable means there\u0027s no\nservice data anywhere — rendering an \"empty\" dashboard misleads\noperators into chasing a dashboard-config problem when the real\nproblem is upstream.\n\nOverviewLanding now:\n  1. Suspends its redirect cascade while `oapReachable \u003d\u003d\u003d false`.\n     Previously the cascade would happily redirect into an overview\n     dashboard or layer page, each of which would also render empty\n     and confusingly look like a stale layout.\n  2. Renders a full-page warning card in place of the empty / routing\n     placeholder when OAP is unreachable, with a \"Retry now\" button\n     wired to the useLayers refetch and a hint that the background\n     poll is also retrying.\n\nSidebar + GlobalConnectivityBanner unchanged — the topbar strip\nkeeps surfacing the live reachability state across every route, the\nsidebar still renders so the operator can step over to admin pages\n(which are intentionally still reachable in read-only mode to fix\ntemplate / config issues even with OAP down).\n\nPer-layer pages also unchanged: they keep their existing\nbundled-fallback render so an operator can verify a layer template\nthey just edited even when OAP is down. Only the landing /\noverview-cascade path fully blocks.\n\n12 new i18n keys × 8 locales.\n\n* chore(local-boot): add horizon.unreachable.yaml for previewing OAP-down landing block\n\nPoints queryUrl + adminUrl at 127.0.0.1:12801 (nothing listening) so the\nnew full-page \"OAP query host is unreachable\" landing state can be\nexercised end-to-end without standing up an OAP. Auth / RBAC mirror\nhorizon.local.yaml — admin/admin still logs in; the landing cascade is\nsuppressed and the warning card renders instead.\n\n* chore(asf): dismiss stale PR approvals on new commits\n\nApache INFRA\u0027s .asf.yaml exposes the GitHub branch-protection toggle\nunder required_pull_request_reviews.dismiss_stale_reviews. With this\noff, a reviewer\u0027s LGTM persists across every later push to the PR —\nwhich means follow-up commits ship under an approval of code the\nreviewer never saw. Enable it on main so each fresh push needs a\nfresh review.\n\nNote: .asf.yaml is read from the default branch, so this only takes\neffect once merged."
    },
    {
      "commit": "d43d73fe8545bcc41dedd716b1f46c944e3f21a1",
      "tree": "1cb4ea77e44df00aec598dc220b30d57207e834d",
      "parents": [
        "40c2e92b309955d7fd82164977ab1daa4a2beb89"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Tue May 26 22:15:12 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue May 26 22:15:12 2026 +0800"
      },
      "message": "fix(admin): default editor to remote + reset truly discards local draft (#18)\n\nOperator-facing fixes across the three admin editors (Layer\nDashboards, Overview Templates, Translations):\n\n1. **Editor source default \u003d remote** (Layer + Overview). The runtime\n   bundle serves remote content to end users for synced / diverged /\n   remote-only rows; the editor now opens from remote on every\n   (re-)mount, with local draft winning when present and bundled\n   only on bundled-fallback rows.\n\n2. **Source pill — three distinct states** (Layer + Overview).\n   - Remote (default) → no pill.\n   - After Reset to bundled (no local edits yet) → \"from bundled\".\n   - After save / typed local edits → \"from local\".\n   The bundled-vs-local distinction matters: reset stages bundled\n   for inspection / push, local edits are typed content not on disk\n   anywhere.\n\n3. **Source pill on Translations follows the same vocabulary.** As\n   soon as the operator has a staged local draft for the current\n   (template, locale), a \"from local\" chip surfaces in the editor\n   header. Discard puts the editor back on the default (OAP overlay\n   row) and the chip disappears.\n\n4. **Reset → Bundled stays visible even when synced** (Layer +\n   Overview). Previously hidden via `v-if\u003d\"!isSynced\"`; now always\n   rendered and disabled with a \"(synced)\" tail when bundled equals\n   remote so the affordance stays visible while clearly signalling\n   nothing meaningful to reset to.\n\n5. **Reset really discards** (Layer + Overview). \"Reset to bundled\" /\n   \"Reset to remote\" advertised \"Discard current edits …\", but the\n   implementation called `persistLocal(new_content)` after\n   `loadFrom(src)` — writing the freshly-loaded content into\n   localEdits as a brand-new draft and triggering the\n   TemplateConflictPrompt right after every reset. Now calls\n   `localEdits.remove(editName)`; subsequent edits re-create a\n   draft naturally on the next change.\n\n6. **Picker filters auto-uncheck when their set empties** (Layer +\n   Overview). Reset OAP to bundled / Push / Sync on the last row\n   matching the active filter (e.g. the last Diverged layer) used to\n   leave the operator stranded — the filter stuck on Diverged, the\n   list went empty, and the checkbox itself was disabled because\n   `divergedCount \u003d\u003d\u003d 0`, so the operator couldn\u0027t uncheck to\n   recover. Reactive guard watches both diverged + local counts and\n   clears the matching filter when it goes to zero.\n\nAdds the new pill / dropdown labels + tooltips to all eight locale\ncatalogs."
    },
    {
      "commit": "40c2e92b309955d7fd82164977ab1daa4a2beb89",
      "tree": "73b4ffa5390079abda044f8bc1f1c55735086380",
      "parents": [
        "3f58846f9c6f329871709b3db67a837b8cfb0c9b"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Tue May 26 21:08:56 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue May 26 21:08:56 2026 +0800"
      },
      "message": "feat(ui+bff): product-readiness pass — i18n + perf + RBAC + typography (#17)\n\n* feat(ui+bff): product-readiness pass — i18n + perf + RBAC + typography\n\nBFF\n  * OAP-local timestamps across every query route (dashboard, landing,\n    topology, endpoint, endpoint-dependency, instance, ebpf, trace,\n    log, trace-tag, alarms). Lifted getServerOffsetMinutes + the\n    offset-aware formatters into util/window.ts; alarms.ts now uses the\n    shared helper instead of its private copy. Wrong-window data on\n    non-UTC OAP installs is fixed.\n  * Traces + logs now query at SECOND step (records, not metric\n    buckets — confirmed via DurationUtils.MAX_TIME_RANGE applies only\n    to MetricsQuery*DAO.assembleDurationPoints).\n  * Landing route chunks the N×M MQE fragments at 6 services per\n    round-trip + Promise.all, mirroring dashboard.ts. 250-fragment\n    blanks fixed.\n  * /api/health no longer returns sessions.size() (was an info leak to\n    unauthenticated callers).\n  * pageSize capped (trace 200, log 100) — direct API callers can\u0027t\n    cascade an unbounded LIMIT to OAP.\n  * Profile-task creation bodies validated + bounded: async/pprof\n    duration ≤ 600s, eBPF ≤ 1800s, instances ≤ 32, events ≤ 8;\n    network samplings ≤ 8 with maxRequestSize/maxResponseSize ≤ 64KiB.\n  * Bundle overlay filter: per-locale overlay rows no longer inflate\n    overview/layer remote-only badge counts (translations have their\n    own admin page).\n  * Demo URL stripped from CLAUDE.md + topology.ts comment.\n\nUI\n  * Inter + JetBrains Mono self-hosted via @fontsource-variable. Removed\n    Google Fonts CDN dependency from index.html — firewalled/offline\n    deployments now render the intended typography.\n  * New typography tokens in design-tokens/tokens.css: --sw-fs-*,\n    --sw-fw-*, --sw-lh-*, --sw-ls-*. Plus .sw-uplabel / .sw-kicker\n    utility classes.\n  * Monaco JSON worker registered. Fixes \"Missing requestHandler or\n    method: resetSchema\" console error when opening the JSON diff\n    modal.\n  * Trace waterfall virtualized via content-visibility + intrinsic-size\n    hints. 5000-span traces no longer freeze the main thread.\n  * Auto-refresh ticker pauses when document.hidden — backgrounded\n    tabs stop hitting OAP.\n  * RuleCard arrow stays glued to the rule name (was floating with\n    justify-content: space-between).\n  * Catalog renames: otel-rules → \"Metrics Analysis Language -\n    OpenTelemetry Rules\"; telegraf-rules / log-mal-rules analogous;\n    lal → \"Log Analysis Language\"; OAL catalog → \"Observability\n    Analysis Language\". New catalog-labels.ts helper.\n  * Layer Dashboards + Overview Templates editor now seeds from\n    REMOTE when the row is diverged or remote-only (matches what the\n    live menu renders via pickLayerContent / pickOverviewContent).\n  * Source pill on both editors shows \"from local/remote/bundled\" with\n    consistent wording.\n  * 15 pages swept for typography tokens + i18n: Roles, Users,\n    AuthStatus, AlertPageSetup, GlobalDefaults, AlertingRules, live-\n    debug suite, DebugHistory, Inspect, ConfigView, ClusterStatus,\n    OalCatalog, DslCatalog, DslDump, DslEditor, TtlView, RuleCard,\n    Modal, DestructiveConfirm, AlarmsView. Split-sentence ledes that\n    rendered as \"English | one localized word | English\" mid-sentence\n    converted to single \u003ci18n-t keypath\u003e blocks with named slots.\n\ni18n\n  * en.json: 1158 keys (was 531).\n  * de / zh-CN / ja / ko / es / pt / fr: every locale at full 1158-key\n    parity. AI-assisted seed translations following CLAUDE.md rules —\n    IT terms / OAP enums / catalog slugs / status enums stay English;\n    placeholders preserved verbatim; meaning translated naturally.\n\nVerified\n  * apps/ui — vue-tsc --noEmit clean.\n  * apps/bff — tsc --noEmit clean; 80/80 unit tests pass.\n  * license-eye — 904 checked, 0 invalid.\n\n* docs(changelog): cover product-readiness pass under 0.6.0\n\nAdjusts the existing 0.6.0 section rather than starting a new block.\nAdds an operator-facing summary up top describing the release as the\nproduct-readiness pass, then six concise sub-sections — typography +\nself-hosted fonts, wire-correctness on non-UTC OAP, performance\nhardening (landing batches / trace waterfall / hidden-tab ticker),\nRBAC + input-validation hardening (/api/health, pageSize caps,\nprofile-task body caps), the JSON diff modal console-error fix, the\nDSL / OAL catalog renames + admin editor seeding from remote on\ndiverged rows, and the sync-status counter ignoring translation\noverlay rows. No key counts; just what operators see and what they\ncan now do.\n\n* chore: regenerate binary LICENSE + add i18n step to build-dashboard skill\n\n- dist-material/release-docs/LICENSE — OFL-1.1 entries for the two new\n  @fontsource-variable packages (inter + jetbrains-mono) added by the\n  product-readiness PR. CI Dependency-license check passes now.\n- .claude/skills/build-dashboard/SKILL.md — adds a \"supported locales\"\n  step that makes overlay-per-locale a prerequisite for shipping a new\n  dashboard, with the tech-term policy and the i18n:validate gate.\n\n* chore(licenses): allow OFL-1.1 (Apache Category A)\n\nThe two @fontsource-variable font packages added by the\nproduct-readiness PR ship under OFL-1.1. Apache\u0027s legal page lists\nOFL-1.1 as Category A (compatible with Apache-2.0 redistribution —\nsee https://www.apache.org/legal/resolved.html#category-a), so it\nbelongs in the ALLOWED set in scripts/check-dist-licenses.mjs."
    },
    {
      "commit": "3f58846f9c6f329871709b3db67a837b8cfb0c9b",
      "tree": "d70aed0cd4609111b15576c5fbf004e72ed22b2c",
      "parents": [
        "4dee09616725ecebdc2bb4376d87e945d689ad85"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Tue May 26 14:58:09 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Tue May 26 14:58:09 2026 +0800"
      },
      "message": "feat(i18n): eight-locale parity + admin polish + live-debug + upstream id-required compat (#16)\n\n* feat(i18n): eight-locale parity across UI chrome + layer/overview overlays\n\nBrings every advertised locale (en, zh-CN, ja, ko, es, pt, de, fr) to\nfull coverage across both UI chrome and BFF-shipped template overlays.\n\nUI chrome (apps/ui/src/i18n/locales/*.json)\n  Each non-English catalog now mirrors en.json exactly at 511 keys. Tech\n  terms (SkyWalking, OAP, MQE, eBPF, Zipkin, OTLP, Kubernetes, K8s,\n  Istio, GraphQL, OAL/MAL/LAL, LDAP, BanyanDB, Elasticsearch, JVM,\n  Apdex, RPS/RPM/SLA, hot/warm/cold, scope enums, layer keys, MQE\n  function names, env vars, HTTP status codes, language runtimes, all\n  DB / MQ / cache / gateway product names) stay verbatim in every\n  locale; surrounding English is translated idiomatically. User-specified\n  popular mappings honored (e.g. zh-CN general.alias \u003d 普通应用).\n\nBFF layer templates (apps/bff/src/bundled_templates/layers/*.i18n.\u003cloc\u003e.json)\n  All 42 layer source templates carry a per-locale overlay for each of\n  the seven non-English locales, covering every translatable leaf\n  (alias, title, description, tip, label, group; aliases.* / slots.*\n  values; expressionLabels[] / tableHeaders[] entries). Coverage ~2,300\n  leaves per locale, previously 0–300 in most cases.\n\ni18n wiring across components (~55 .vue files)\n  31 routed components and 24 sub-components now use vue-i18n. Strings\n  that reach the DOM are wrapped in t() with natural-key style. Several\n  shadowed v-for `t` variables were renamed (tabItem, target, …) to\n  avoid clobbering the i18n function. Translations push now refetches\n  the sync-status cache so per-locale state stays fresh.\n\nValidator + housekeeping\n  - apps/bff/src/i18n/validate.ts: every source template must have a\n    sibling overlay file per OVERLAY_LOCALES; missing-file is a\n    finding.\n  - LiveDebuggerView tab buttons now push to /operate/live-debug/\u003ctab\u003e\n    instead of the non-existent /debug/\u003ctab\u003e.\n  - K8s instance Node Status: switched from a single-scalar card to a\n    table of currently-true Kubernetes conditions (matches the\n    cluster-scope sibling), and aligned rowSpan so widget heights\n    line up with neighbors.\n  - MonacoYaml gutter glyph (▶) on `- name:` lines is now visible; CSS\n    class was referenced but unstyled.\n  - ClusterStatusView fully i18n-wired; gate descriptions and\n    Zipkin/OTLP pane copy translate.\n  - Capture history row layout consolidated; LIVE chip moved inline\n    next to the timestamp.\n\nCatalog half-translation policy preserved\n  A missing key in any non-English catalog still falls back to English\n  at the leaf. This commit closes most gaps but does not change the\n  fallback semantics; future template additions remain valid even\n  before their translations land.\n\nUpstream API note (deferred)\n  skywalking#13884 made `id` REQUIRED on POST /ui-management/templates.\n  Horizon\u0027s create call still sends only `configuration` here; the\n  forward-compat change to send `id \u003d horizon.\u003ckind\u003e.\u003ckey\u003e` is held\n  back for a dedicated follow-up. Verified Horizon code treats r.id as\n  opaque (no UUID-format assumptions) and reconcileDuplicates handles\n  mixed-id legacy deployments, so the switch will be a small surgical\n  PR once OAP is on a release line that includes #13884.\n\n* fix(i18n + live-debug): re-apply admin polish + per-locale chips + live-debug bug fixes\n\nA grab-bag of small but visible fixes layered on top of the i18n branch,\nseveral of which had landed earlier in the cycle and were lost to\nmid-flight reverts.\n\nCluster Status — chrome i18n (Pane B columns + Pane C entirely)\n  - Pane B `Module / State / Env var / Gates` column headers, the\n    `enabled / missing` per-row badge, the `healthLabel` computed,\n    `adminBadgeLabel` (loading… / unreachable / all selectors on /\n    {n} selectors off), and the admin-host-unreachable hint all go\n    through t() now.\n  - Pane C `Zipkin / OTLP traces` lede, `Endpoint` card heading,\n    `zipkinBadgeLabel` (reachable / unreachable / loading…), and the\n    Zipkin-unreachable hint translated.\n  - 20 new keys seeded into all 8 locale catalogs (en + 7\n    translations).\n\nAdmin polish — hide BUNDLED when synced\n  - `OverviewTemplatesAdmin.vue` and `LayerDashboardsAdmin.vue` no\n    longer surface the `from bundled` source pill or the\n    `Reset to / Preview Bundled` dropdown items when the row\u0027s sync\n    badge is `synced`. With bundled \u003d\u003d remote, the distinction is just\n    visual noise. LOCAL drafts and DIVERGED rows still show the full\n    chrome.\n\nTranslations picker — per-locale status chips\n  - `TemplateSyncRow` now carries the existing wire-level `locale?`\n    field; `useTemplateSources.overlayStatus(name, locale)` looks up the\n    per-locale overlay row\u0027s status from the same sync-status payload\n    (zero extra round-trips). `TemplatePickerEntry.localeBadges` is\n    rendered as a strip of small color-coded chips after the source\n    badge in both the collapsed button and the dropdown rows\n    (synced / diverged / remote-only / bundled-fallback / local /\n    empty), and `TranslationsView.metaFor` wires it up.\n\nLive debugger — fixes for history → debug deep-link\n  - MAL view dead-ended on `?historyId\u003d…` because `loadHistorical`\n    reset `selectedRow / expandedEntities / foldedRecords` before\n    those refs were declared (TDZ ReferenceError swallowed by the\n    setup boundary, no console trace). LAL had the same shape with\n    `selectedCell`. Hoisted the resettable refs above their consumer.\n  - Per-DSL view lookups (`history.entries.value.find`) changed to\n    `history.all.value.find`. The route had already pinned us to the\n    right widget; double-filtering by widget was a silent way for a\n    stale `widget` field to drop the entry on the floor.\n  - `useDebugHistory.save()` now refuses to downgrade a session\n    snapshot: if the existing entry already has records and the new\n    input has fewer, only metadata (retentionDeadline, retentionMillis)\n    updates. Defeats the post-stop wipe — `stop()` triggers one final\n    `session()` refresh against an already-cleaned-up OAP, which\n    returns nodes-only-with-empty-records and used to overwrite the\n    rich live snapshot with an empty one.\n  - Stable node ordering in `DebugMal / DebugLal / DebugOal` —\n    `nodeViews` sorts by `nodeId ?? peer` before rendering so cards\n    don\u0027t reshuffle between polls.\n  - `DebugView` shows an explicit \"This capture has no records\"\n    placeholder when an effective session has zero node-views, so an\n    honest empty capture is visibly different from a bug.\n  - LiveDebuggerView tab buttons push `/operate/live-debug/\u003ctab\u003e` (the\n    actual mounted path) instead of the non-existent `/debug/\u003ctab\u003e`.\n    Same one-line fix in the surrounding doc-comments.\n\nOther small fixes\n  - MonacoYaml `.rr-debug-glyph` CSS restored — the green ▶ in the MAL\n    / LAL editor gutter was referencing an unstyled class, so the\n    live-debug entrance was invisible.\n  - RuleCard duplicate BUNDLED pill removed — the status pill in the\n    header row already conveys it; the second pill below was just\n    duplication.\n  - `i18n:validate` tightened: empty `{}` overlays are now a finding\n    rather than a free pass. The missing-overlay finding\u0027s secondary\n    \"(an empty {} is acceptable)\" hint goes away.\n\nCLAUDE.md\n  Adds a sixth failure mode under \"Common AI failure modes\":\n  audit `router.push({ path })` and `\u003crouter-link to\u003e` against the\n  router on every route refactor, and hoist refs that param-driven\n  immediate watches reset above the watch itself (Vue\u0027s compiler does\n  not hoist `const` — a TDZ crash silently aborts setup with no\n  console error in some browser builds).\n\n* feat(ui-template): send id\u003d\u003cenvelope name\u003e on POST /ui-management/templates\n\nUpstream skywalking#13884 (\"remove auto generate id for UI templates\")\nmade `id` required on POST /ui-management/templates — OAP no longer\nauto-generates one. Pass the envelope name (`horizon.\u003ckind\u003e.\u003ckey\u003e` for\nsource rows, `horizon.\u003ckind\u003e.\u003ckey\u003e.i18n.\u003clocale\u003e` for translation\noverlays) as the requested id; the row lands at the predictable id on\ncurrent OAP and `ack.id` echoes it back.\n\nForward- and backward-compatible:\n  - Current OAP (post-#13884): row created at the requested id; Horizon\n    treats `ack.id` as the wire handle (no change in downstream code,\n    which already operates on `r.id` as opaque).\n  - Legacy OAP (pre-#13884): the extra `id` body field is ignored, the\n    server still auto-assigns a UUID and echoes it back in `ack.id`.\n    Behavior identical to before.\n\n`createAndConfirm` parses the envelope to extract `name`, so all\nexisting call-sites (sync.ts, template-sync.ts admin routes) keep their\nsignatures.\n\nMixed-id deployments (UUID-keyed legacy rows + name-keyed new rows under\nthe same envelope name) self-heal via `reconcileDuplicates` on next\nboot.\n\n* docs(changelog): 0.6.0 — i18n / Cluster Status / live-debug / id-required\n\nAdds the 0.6.0 release-notes block for everything in this branch:\neight-locale parity, Cluster Status + admin polish, live-debugger\nfixes, the gutter-glyph / duplicate-BUNDLED / k8s-Node-Status cleanups,\nand the upstream `id`-required ui-management alignment."
    },
    {
      "commit": "4dee09616725ecebdc2bb4376d87e945d689ad85",
      "tree": "5c362aac7a57b2aed6842990e3cd2d9e15e89c26",
      "parents": [
        "78cb9841bf588c7608f10a89375d12f1e239086e"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Mon May 25 12:15:40 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Mon May 25 12:15:40 2026 +0800"
      },
      "message": "feat(service-map): Smartscape cross-layer hierarchy overlay (#14)"
    },
    {
      "commit": "78cb9841bf588c7608f10a89375d12f1e239086e",
      "tree": "479965028de666568488c778e348823fa54b72b3",
      "parents": [
        "b8650c9c3902c59da094c1bc1f474a430eeff368"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sun May 24 21:28:23 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 24 21:28:23 2026 +0800"
      },
      "message": "feat: BanyanDB cold-stage query + lifecycle TTL page + picker fixes (#13)\n\nCold-stage query\n- Topbar \u0027Cold\u0027 pill (BanyanDB-only) swaps every page to read from\n  the cold lifecycle stage. Sticky per browser; flipping invalidates\n  the visible queries so subscribers refetch immediately.\n- Yellow trap banner under the topbar when the pill is on AND the\n  current time range is within hot+warm (cold returns nothing there).\n- Trace lookup from a log row carries the row\u0027s timestamp through\n  the popout so OAP widens its trace search past the default 1-day\n  window — pairs with the Cold pill so cold-tier trace IDs resolve\n  from cold-era log rows.\n\nData retention page (/operate/ttl)\n- Per-data-class lifecycle bar: one row per class (Normal, Trace,\n  Log, Metadata, Minute metric, …) with Hot+Warm and (when present)\n  Cold segments. Widths proportional to total retention across all\n  rows. Rows collapse to \u0027All records (5)\u0027 / \u0027All metrics (4)\u0027 when\n  every class in a category shares the same TTL pair.\n- Page branches sharply by backend: BanyanDB shows the full lifecycle\n  bar + stage vocabulary; non-BanyanDB renders a single Retention\n  pane with per-class values and skips the stage vocabulary.\n- Footer note names the wire-level truth (hot+warm is collapsed on\n  the wire, segment-migration overlaps near boundaries, property\n  omitted).\n\nOverview widgets follow the global time picker\n- The Services Dashboard (overview metric / KPI / table widgets)\n  now honors the topbar time picker — previously landing + topology\n  were hardcoded to the last 60 minutes regardless of picker.\n- Layer dashboard header KPIs follow the picker too (was showing\n  recent numbers while body honored the picker).\n- Active alarms widget title shows the actual window (· last 10m)\n  instead of a fixed \u0027last 60m\u0027; empty-state uses the same value.\n\nTime picker\n- Custom range seeds from the last applied range instead of resetting\n  to half-max-ending-now. Re-opening the picker on a custom range\n  auto-expands the Custom form on the matching precision tab.\n- Locale-bleed fixed on the alarms-page custom-range stamp and the\n  log row date column (was \u00275月08日\u0027 on zh-CN browsers; now uniform\n  MM-DD).\n\nBFF\n- New apps/bff/src/util/window.ts (Window/TimeStep/fmtForStep/\n  windowFromRange/defaultMinuteWindow) — shared by dashboard /\n  landing / topology / trace-by-id so per-route format drift can\u0027t\n  happen.\n- New apps/bff/src/util/duration.ts — onRequest hook reads the\n  cold-stage header, withColdStage() splices coldStage:true into\n  Duration variables.\n- New apps/bff/src/logic/oap/backend.ts — classifies the connected\n  OAP from its TTL response (banyandb / other / unknown), exposed\n  on /api/oap/info.\n- /api/oap/ttl now returns backend + stages.{hot,cold} derived view.\n- POST /api/layer/:key/landing + GET /api/layer/:key/topology now\n  accept step/startMs/endMs.\n- GET /api/trace/:traceId accepts startMs/endMs/step and forwards\n  queryTrace(duration:…) so older trace IDs resolve.\n\nPolish\n- Sentence-case fixes (\u0027DSL management\u0027, \u0027Metrics inspect\u0027) across\n  sidebar, roles, cluster status, DSL warning, catalog kicker.\n- Public-demo references removed from setup docs."
    },
    {
      "commit": "b8650c9c3902c59da094c1bc1f474a430eeff368",
      "tree": "36e9c68ad2bafefd84feb5984b31da2ef757f25a",
      "parents": [
        "3ec51dd44617af02349cb958dedcbbd151b31476"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sun May 24 09:39:25 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 24 09:39:25 2026 +0800"
      },
      "message": "ci: mirror multi-arch image to Docker Hub on tag-push + manual dispatch (#12)"
    },
    {
      "commit": "3ec51dd44617af02349cb958dedcbbd151b31476",
      "tree": "1ab73e6bc7eb234330b5cc3d33cf1e2d82074e16",
      "parents": [
        "eafb2a9921fc110c752204337848f9ab7a66a402"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sun May 24 08:04:14 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sun May 24 08:04:14 2026 +0800"
      },
      "message": "fix(admin): unpublished-edits prompt re-shows after login + align verb gates (#11)\n\nTwo small, related fixes for the \"You have unpublished local edits\"\nprompt that appears for editors with browser-local template drafts.\n\n- auth.login() now clears the prompt\u0027s `sessionStorage` dismissal key\n  on every successful login. Previously the per-tab dismissal carried\n  across the logout / login boundary in the same browser tab, so an\n  operator who had dismissed the modal earlier would log back in and\n  not see the reminder for drafts they hadn\u0027t pushed. Since /login\n  lives outside AppShell, the next AppShell mount re-reads the\n  (now-clean) sessionStorage and the modal opens reliably whenever\n  drafts exist.\n- TemplateConflictPrompt\u0027s gate for layer drafts is now `dashboard:write`\n  (was `dashboard:read`), matching the symmetric `overview:write` check\n  used for overview drafts. The comment above the component already\n  said \"kinds the user can edit\"; the prior read-verb worked in the\n  default role policy only because dashboard:read and dashboard:write\n  resolve to the same roles (operator + admin). A future role that\n  grants read-only dashboard access would have seen the prompt\n  without being able to act on it.\n\nNet effect: viewers + maintainers see no prompt (unchanged — they\ncan\u0027t push drafts anyway); operator + admin see the prompt reliably\non every fresh login, whatever they did in a prior session."
    },
    {
      "commit": "eafb2a9921fc110c752204337848f9ab7a66a402",
      "tree": "253d1a978c71023414e52193b6f44aa81ee755ad",
      "parents": [
        "4f5c1648d41ca49cebd454676820c95bc20a61a3"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sat May 23 23:01:43 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 23 23:01:43 2026 +0800"
      },
      "message": "feat: overview-by-layer visibility, server-global service catalog, landing cascade (#10)\n\nThree connected improvements driven by \"show what\u0027s relevant, never a\nblank page\" + reduce duplicate OAP fan-out.\n\n- Overview dashboards appear in the sidebar only when at least one of\n  their layers is reporting services. Visibility is derived from each\n  dashboard\u0027s widgets (their `layer` field) unioned with its explicit\n  `layers[]` list, gated against the live `availableLayers`. User-\n  created dashboards from \"+ New\" inherit this without needing a\n  hand-maintained `layers[]`. Updates on the existing 60s menu cadence.\n- New process-global ServiceLayerCatalog (apps/bff/src/logic/services/)\n  owns the `listLayers` + aliased `listServices(layer)` fan-out with a\n  60s TTL + single-flight dedup. The sidebar menu\u0027s per-layer counts\n  and the alarms layer-tagger share this one cache instead of each\n  running their own poll. OAP sees at most one fan-out per minute\n  regardless of how many routes are polling, and the two views can no\n  longer drift relative to each other. Replaces alarms-only\n  ServiceLayerMap (deleted).\n- Root `/` lander now cascades to a real destination so the user never\n  sees a blank page: first available public overview, else first\n  layer-with-services, else first bundled layer template, else the new\n  `/landing-empty` route. The empty landing has two distinct copies —\n  \"No data is flowing yet\" (no agents reporting) vs \"No dashboard\n  configured yet\" (services exist but no overview) — each with the\n  right operations-team handoff and no action buttons (avoids sending\n  a viewer to a 403).\n- Debug events panel default flipped to OFF uniformly (was on for\n  localhost). Same baseline for operators and developers; sticky\n  preference unchanged."
    },
    {
      "commit": "4f5c1648d41ca49cebd454676820c95bc20a61a3",
      "tree": "48684a6b782d41ed28e718cc8484f19ad6915b47",
      "parents": [
        "a9ece2b00d4f53b52aee5dc0cbb9e30c8f53515e"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Sat May 23 07:46:24 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Sat May 23 07:46:24 2026 +0800"
      },
      "message": "feat(admin): browser-local template authoring — create, preview, delete/disable (#9)\n\nDashboard authoring (layer + overview admin) now shares one model: edits\nlive as a browser-local draft, the live page stays on the published OAP\nversion, and \"Check diff \u0026 push\" publishes (create-or-update on OAP).\n\n- Create mirrors edit: \"+ New dashboard\" writes a local draft (id \u003d name,\n  checked unique), preview, then push. Remote-only dashboards (pushed, no\n  bundled default) now render on the live page + sidebar, not just the\n  editor.\n- Delete \u003d soft-disable (OAP has no hard DELETE): local-only drafts are\n  removed from the browser; on-OAP templates are disabled and drop from\n  the picker live-state, sidebar, and live page. Layer disable hides the\n  layer (menu route honors disabled templates); Reactivate re-enables from\n  the bundled default via the OAP update path. Styled in-app confirm\n  dialogs replace the native confirm box.\n- Overview editor rebuilt as a layer-style 12-col canvas (drag-reorder,\n  corner-resize, click-to-edit drawer, selectable section-breaks/title,\n  dashed selectable hints) mirroring the live grid; composite-metrics KPI\n  rows are stacked cards so they read in the narrow drawer.\n- Layer editor: Diverged/Local filters moved inside the picker dropdown,\n  one-line \"Layer: \u003cname\u003e \u003ckey\u003e \u003cstatus\u003e\" header showing the same sync\n  status the picker does.\n- Reliability: clear \"Cannot reach the server\" message when the BFF is\n  down (API client no longer double-reads the response body); fixed the\n  sidebar scrolling to the bottom on every navigation (the Debug-events\n  toggle\u0027s active state was the scroll target).\n- Docs: rewrote the layer + overview template admin sections for the\n  draft -\u003e preview -\u003e push model, create, and delete/disable."
    },
    {
      "commit": "a9ece2b00d4f53b52aee5dc0cbb9e30c8f53515e",
      "tree": "c9fa1943297dd75abfcdeedf565e9c1f31bec0a1",
      "parents": [
        "c20c8f79c4458d4b182785d4fcf02e9130475624"
      ],
      "author": {
        "name": "吴晟 Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Fri May 22 07:43:05 2026 +0800"
      },
      "committer": {
        "name": "GitHub",
        "email": "noreply@github.com",
        "time": "Fri May 22 07:43:05 2026 +0800"
      },
      "message": "Release 0.5.0, and prepare 0.6.0 (#8)"
    },
    {
      "commit": "c20c8f79c4458d4b182785d4fcf02e9130475624",
      "tree": "72d2884019ccac4ba22df81438f5261bb176fb57",
      "parents": [
        "ce33f3e2cfe1a0f7f8fe057117ac05090a3e02e4"
      ],
      "author": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 23:26:15 2026 +0800"
      },
      "committer": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 23:26:15 2026 +0800"
      },
      "message": "infra: enable branch protection on main\n\nRequire the rolled-up \"Required\" CI status check (strict — branch must be\nup to date before merge) and one approving PR review before merging to main.\n"
    },
    {
      "commit": "ce33f3e2cfe1a0f7f8fe057117ac05090a3e02e4",
      "tree": "ce494eca8ab1f149af00ed5be8b87e494016e2b9",
      "parents": [
        "0ba945c8443df09217a768bcf5be5bbf35227d41"
      ],
      "author": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 23:23:39 2026 +0800"
      },
      "committer": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 23:23:39 2026 +0800"
      },
      "message": "release: force English month in the vote email date\n\ndate\u0027s %B is locale-dependent, so on a non-English OS the vote email\nprinted e.g. \"5月\" instead of \"May\". Pin LC_ALL\u003dC for that date — the\nemail targets an English mailing list regardless of the runner\u0027s locale.\n"
    },
    {
      "commit": "0ba945c8443df09217a768bcf5be5bbf35227d41",
      "tree": "987e7af5cd4e5d42909c72240466ecedebb75b58",
      "parents": [
        "b14ee409744d4e7cb0b4b51f4884c55ea8e7876d"
      ],
      "author": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 23:12:56 2026 +0800"
      },
      "committer": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 23:12:56 2026 +0800"
      },
      "message": "license: ignore release scratch dirs in header check\n\nrelease.sh / release-finalize.sh clone the repo into scripts/.release-work\n(and .finalize-work). license-eye recursed into that nested copy and failed\non files whose repo-root-relative ignores (.claude/**, *.tpl) no longer\nmatched under the nested path. Exclude **/.release-work/** + **/.finalize-work/**.\n"
    },
    {
      "commit": "b14ee409744d4e7cb0b4b51f4884c55ea8e7876d",
      "tree": "3a4ec0da8ba732b32522e5cc7eff37a2755d3d6f",
      "parents": [
        "7761edb96082357e9807ba5421364efba027c2ea"
      ],
      "author": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 23:09:31 2026 +0800"
      },
      "committer": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 23:09:31 2026 +0800"
      },
      "message": "release: portable source tarball — git archive + plain tar, no GNU --transform\n\nStep 9 used GNU tar\u0027s --transform to rename the tarball root, which bsdtar\n(macOS default) rejects. Build the source tarball with `git archive`\n(--prefix sets the root dir) piped through plain `tar -xf`/`-czf`, so it\nworks the same on macOS and Linux. As a bonus the tarball now contains\nexactly the committed files at the tag (git archive emits only tracked\nfiles), still dropping the ASF-infra CI publish workflow.\n"
    },
    {
      "commit": "7761edb96082357e9807ba5421364efba027c2ea",
      "tree": "9c1e64bf3b9bbc36c5c52400561d6988295cb244",
      "parents": [
        "a205aad98f2a34b71c3c060b4a38649f9a8b7fcb"
      ],
      "author": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 23:03:01 2026 +0800"
      },
      "committer": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 23:03:01 2026 +0800"
      },
      "message": "config: don\u0027t ship explicit relative state-file paths in the example\n\nhorizon.example.yaml hard-coded ./horizon-*.jsonl for audit/setup/alarms/\ndebugLog. In the container those resolve under /app, which the non-root\nhorizon user cannot write — EACCES on boot. Leaving the file paths unset\nlets the right location win automatically: /data in the published image\n(via the HORIZON_*_FILE env defaults), ./horizon-* for a local binary run.\n"
    },
    {
      "commit": "a205aad98f2a34b71c3c060b4a38649f9a8b7fcb",
      "tree": "6870515ca325a178174da6f6b3e1006427c8e59e",
      "parents": [
        "ea29dc2d1d9aee47ba1dec68df534db410dc5ed2"
      ],
      "author": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 22:53:05 2026 +0800"
      },
      "committer": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 22:53:05 2026 +0800"
      },
      "message": "release: Horizon owns :latest on Docker Hub; drop the overview file\n\nTag scheme on the shared apache/skywalking-ui repo is now horizon-\u003cver\u003e +\nlatest (latest serves the newest Horizon release). Drop the horizon-latest\ntag and the optional-latest prompt. Remove the committed Docker Hub overview\nREADME now that it\u0027s maintained directly in the repo\u0027s \"Repository overview\".\n"
    },
    {
      "commit": "ea29dc2d1d9aee47ba1dec68df534db410dc5ed2",
      "tree": "79a5a01c14beafff6a15345da2f7c31d33373073",
      "parents": [
        "7ff0ffab7777695cf589423c1683eff648ffc937"
      ],
      "author": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 22:15:32 2026 +0800"
      },
      "committer": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 22:15:32 2026 +0800"
      },
      "message": "rbac+docs: grant overview/ttl/config reads to maintainer; doc refresh\n\nAdd overview:read to viewer + maintainer defaults, and ttl:read / config:read\nto the maintainer + operator baselines, so the read-only Data Retention and\nOAP Configuration pages are reachable by monitoring roles. Document those two\noperate pages and refresh the docs tree (menu, customization, components,\naccess-control, setup) for accuracy.\n"
    },
    {
      "commit": "7ff0ffab7777695cf589423c1683eff648ffc937",
      "tree": "cad45c82e1217937520db8f6d4de211f44b6e7e8",
      "parents": [
        "dbfa4d0d872e4df8e1e440e6d1955dbd52d169dd"
      ],
      "author": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 22:15:21 2026 +0800"
      },
      "committer": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 22:15:21 2026 +0800"
      },
      "message": "release: post-vote finalize script + single from-source Dockerfile\n\nAdd scripts/release-finalize.sh for the post-[VOTE] half of the release:\nSVN move dev-\u003erelease (auto-creating the release/ path, pruning the prior\nrelease), GitHub release attaching the voted artifacts, and a multi-arch\nDocker Hub push to apache/skywalking-ui under horizon-\u003cver\u003e/horizon-latest\n(+ optional latest). Harden release.sh to create the dev/ SVN folder too.\n\nCollapse the two Dockerfiles into one from-source multi-stage build (the\ncopy-in image cannot go multi-arch from a single host: dist/node_modules\ncarries an arch-specific argon2 binding). CI builds each arch natively, so\nthe runner-side pnpm package step is dropped. Flip .dockerignore from a\ncopy-in allowlist to a from-source denylist. Final shipped image is\nunchanged (~201MB; the build stage is discarded).\n\nBump Node base + CI to 24, engines floor to \u003e\u003d22. Add a Docker Hub README\ndocumenting the shared-repo tag split (booster \u003d 9.x/10.x/latest, Horizon\n\u003d horizon-*).\n"
    },
    {
      "commit": "dbfa4d0d872e4df8e1e440e6d1955dbd52d169dd",
      "tree": "a77fff75e6d43a6f8e21fb1ac6998fc76b61e617",
      "parents": [
        "c262b2c9cf743248df9ccc95389e11065eda9e6c"
      ],
      "author": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 21:46:55 2026 +0800"
      },
      "committer": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 21:46:55 2026 +0800"
      },
      "message": "admin(conflict): reframe the login prompt as a two-way version difference\n\n\"Local template changes not published\" implied the local side was always\nthe edit. Divergence is symmetric (no timestamp comparison), so the title\nis now \"Template versions differ from OAP\" and the body notes OAP may have\nnewer changes OR you have unpublished local edits — leading with the\nremote/live version. Drop the unverifiable \"(newer)\" claim on the confirm.\n"
    },
    {
      "commit": "c262b2c9cf743248df9ccc95389e11065eda9e6c",
      "tree": "7d27345f13c21fb7b81b33a57d19339dd164fb1c",
      "parents": [
        "e95a60c87a8c111b399ffbd61411ed2884929d38"
      ],
      "author": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 21:44:51 2026 +0800"
      },
      "committer": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 21:44:51 2026 +0800"
      },
      "message": "docs+changelog: table widget, edit-locally→publish flow, route verb gate\n\nCHANGELOG (0.5.0): Dashboards \u0026 templates section (time picker drives\ndashboards, the table widget + K8S/upstream alignment, edit-locally →\nSync all publish, local/remote conflict prompt), a Traces section (v1/v2\nauto-select + banner, span-kind coloring), plus RBAC route-gating, LDAP\nservice-bind, TTL-on-non-BanyanDB, and the flame-graph single-tooltip fix.\n\nDocs: dashboard-widgets gains the `table` type (fields, example, MQE\nmapping); layer-templates \"Admin editor\" rewritten for the edit-locally\n→ publish + local/remote-conflict workflow (the old removed save route is\ngone); rbac notes whole-page verb gating.\n"
    },
    {
      "commit": "e95a60c87a8c111b399ffbd61411ed2884929d38",
      "tree": "d6c0cbcf48c979685b84c992d7bb0ce43cdc2743",
      "parents": [
        "c7aef3a35db4d41420c12dba2828a7e370fe5f6d"
      ],
      "author": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 21:33:23 2026 +0800"
      },
      "committer": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 21:33:23 2026 +0800"
      },
      "message": "rbac: verb-guard gated routes; OAP chip only links Cluster for cluster:read\n\nA viewer could reach the maintainer-only Cluster Status page via URL or\nthe topbar OAP chip — its data comes from the shared `auth`-gated\n/api/oap/info + /api/preflight (used by the topbar/shell for every user,\nso the BFF can\u0027t gate them per-page), so the page rendered fully.\n\nAdd a router verb guard: routes declare `meta.verb`; an authenticated\nuser lacking it is bounced home. Annotate the operate/admin routes\n(cluster, alerting-rules, dsl, inspect, ttl, config, live-debug, and the\nadmin pages) with their verbs, mirroring the sidebar gates and the BFF\nroute policy. The topbar OAP chip now links to /operate/cluster only for\ncluster:read users; others get a static (non-navigational) health chip.\n"
    },
    {
      "commit": "c7aef3a35db4d41420c12dba2828a7e370fe5f6d",
      "tree": "075ae9946c76017593ecfe4418eae6d7e988ec27",
      "parents": [
        "1230152ea8b313be6b1bc8a32bf2dbdcda364728"
      ],
      "author": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 21:25:38 2026 +0800"
      },
      "committer": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 21:25:38 2026 +0800"
      },
      "message": "profiling(flame): drop the duplicate native tooltip\n\nWith the d3-tip tooltip disabled, d3-flame-graph still appends an\n\u003csvg:title\u003e per cell (filled from the label handler → \"name (pct%, N\nsamples)\"), which rendered a browser-native tooltip on top of our\nricher cursor-following .fg-tip card. Set an empty label handler so the\ntitle text is blank — only the custom card shows now.\n"
    },
    {
      "commit": "1230152ea8b313be6b1bc8a32bf2dbdcda364728",
      "tree": "edb35e94cfdd842ba7a0fb4c54ffa9c6062aa8d8",
      "parents": [
        "6a775e107be13ab6b6451077ce775858021c24a4"
      ],
      "author": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 21:15:00 2026 +0800"
      },
      "committer": {
        "name": "Wu Sheng",
        "email": "wu.sheng@foxmail.com",
        "time": "Thu May 21 21:15:00 2026 +0800"
      },
      "message": "sidebar: yellow warning icon on layers with unpublished local changes\n\nEach per-layer sidebar row (Kubernetes, Redis, …) now shows a yellow\nalert icon when that layer\u0027s template is diverged — local edits not yet\npublished to OAP — not just the \"Layer dashboards\" admin row.\n"
    }
  ],
  "next": "6a775e107be13ab6b6451077ce775858021c24a4"
}
