From fb0b0e3c559cc02edd9751c3f769ab9f4f6450ea Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Wed, 2 Sep 2026 16:19:57 +0300 Subject: [PATCH] feat(privacy): surface the customer-data scan above the hardware report - Move the "Customer data" panel to the top of the data section (above the chart iframe); header + customer guess always visible, findings table collapsed by default and expandable. - Add a chart top-notice (above Board/CPUs) summarizing the scan via the viewer's standard NoticeTitle/NoticeBody - interim until chart custom panels. - Allowlist ieisystem.com (IEI = Inspur brand infrastructure). - Add chart-custom-panels-spec.md: a reusable, versioned contract proposal for host-supplied panels across every app embedding reanimator/chart. Co-Authored-By: Claude Sonnet 5 --- bible-local/docs/chart-custom-panels-spec.md | 246 +++++++++++++++++++ bible-local/docs/privacy-scan.md | 10 +- internal/privacy/allowlist.go | 2 +- internal/server/handlers.go | 70 +++++- web/static/js/app.js | 30 ++- web/templates/index.html | 44 ++-- 6 files changed, 369 insertions(+), 33 deletions(-) create mode 100644 bible-local/docs/chart-custom-panels-spec.md diff --git a/bible-local/docs/chart-custom-panels-spec.md b/bible-local/docs/chart-custom-panels-spec.md new file mode 100644 index 0000000..3d274ed --- /dev/null +++ b/bible-local/docs/chart-custom-panels-spec.md @@ -0,0 +1,246 @@ +# ТЗ: Custom Panels for Reanimator Chart + +**Status:** proposal / draft for review +**Requesting app:** LOGPile (customer-data / privacy scan panel) +**Applies to:** `reanimator/chart` viewer module, all embedding apps +**Note:** authored in the LOGPile repo; to be moved into the `reanimator/chart` +repo when accepted. Paths below (`docs/`, `bible-local/`) are chart-repo-relative. + +## 1. Problem + +`chart` renders one Reanimator JSON snapshot as a fixed set of hardware sections +(`board`, `firmware`, `cpus`, ...). Embedding apps increasingly need to show their +own presentational blocks alongside that data - LOGPile wants a "Customer data" +panel above `cpus`; other apps have asked for build/QA notes, RMA context, +collection warnings, links back to a ticket. + +Today the only lever is `RenderOptions.NoticeTitle` / `NoticeBody` - a single +top-of-page title + one paragraph. It cannot carry a table, cannot be positioned, +and there is only one of it. Apps work around this by post-processing the chart +HTML or forking the templates, which breaks on every `chart` update. + +`chart` is embedded in several apps, so the mechanism must be **standardized and +reusable**, not LOGPile-specific. + +## 2. Goals + +- A stable, versioned data contract for host-supplied panels. +- Panels can be placed relative to the built-in sections (e.g. before `cpus`). +- Reuse the existing section-card / kv-table / data-table presentation and CSS - + no new heavy UI, no client framework. +- Works through every integration path: in-process `RenderHTML*`, embedded + `POST /render`, and standalone file upload. +- Preserves the "read-only, schema-preserving, does not compute" contract + (see `bible-local/decisions/2026-03-15-read-only-schema-preserving-viewer.md`). +- Backward compatible: no field / key present -> byte-identical output. + +## 3. Non-goals + +- Interactivity beyond what built-in sections already have (severity filter). +- Panels influencing or overriding built-in section data. +- Arbitrary HTML/CSS/JS injection from the host. +- Per-viewer persistence, collapse state sync, theming knobs. +- Replacing `platform_config` or the hardware sections. + +## 4. Data contract + +### 4.1 Envelope + +```jsonc +{ + "version": "1.0", + "panels": [ Panel, ... ] +} +``` + +`version` is the panels contract version (`chart` exposes +`viewer.PanelsContractVersion`). `chart` renders a known major version; an +unknown major version renders each panel's `title` plus a muted +"panel format vX not supported by this viewer" line (never a silent drop). + +### 4.2 Panel + +| Field | Type | Required | Notes | +|-------|------|----------|-------| +| `id` | string | yes | Stable slug, `[a-z0-9-]+`, unique within the set. Rendered as the card's DOM id, prefixed `ext-` to avoid collision with built-in section ids. | +| `title` | string | yes | Card header text. | +| `placement` | string | no | `top` (default), `bottom`, `before:`, `after:`. `` is a built-in section id (`board`, `firmware`, `cpus`, `memory`, `storage`, `pcie_devices`, `power_supplies`, `licenses`, `sensors`, `event_logs`, `platform_config`) or another panel's rendered id. | +| `severity` | string | no | `""` \| `info` \| `warning` \| `critical`. Drives the card's left-accent color only, reusing existing status colors. | +| `blocks` | Block[] | yes | 1..N content blocks, rendered top to bottom. | +| `collapsed` | bool | no | Initial collapsed state (default expanded). Uses the same collapse affordance as built-in cards if/when present; otherwise ignored. | + +### 4.3 Block + +Tagged union on `kind`: + +```jsonc +{ "kind": "text", "text": "one or more paragraphs, \n splits paragraphs" } + +{ "kind": "keyvalue", "rows": [ { "key": "Customer", "value": "acme.ru" }, ... ] } + +{ "kind": "list", "items": [ "line 1", "line 2", ... ] } + +{ "kind": "links", "links": [ { "label": "Ticket OPS-649", "href": "https://..." } ] } + +{ "kind": "table", + "columns": [ "severity", "category", "location", "match" ], + "rows": [ + { "cells": { "category": "domain", "location": "resolv.conf:4", "match": "acme.ru" }, + "severity": "warning" } + ] +} +``` + +Table block details: +- `columns` is the display order. A `severity` column renders as the existing + icon-only status badge; all other cells render as text (multi-line on `\n`). +- `rows[].severity` (optional) feeds row coloring and the severity filter, exactly + like built-in table sections. +- No sorting/pagination in v1. The existing per-table severity filter applies. + +Unknown `kind` -> a muted "unsupported block kind: X" line. Never dropped. + +### 4.4 Escaping / safety + +- Every string is HTML-escaped by the template (`html/template`), as today. +- `links[].href`: only `http`, `https`, or root-relative (`/...`) accepted; + anything else is rendered as inert text. `rel="noopener noreferrer"`, + `target="_top"`. +- Total injected payload is size-capped (proposal: 256 KiB after JSON decode); + over the cap -> panels ignored + one top notice "custom panels omitted (too large)". +- Panel count cap (proposal: 32). + +## 5. Delivery paths + +The same envelope reaches `chart` three ways; all are merged (options win over +snapshot key on `id` conflict): + +1. **In-process** - `viewer.RenderOptions.Panels []viewer.Panel` + (and a matching `viewer.RenderHTMLWithOptions` already exists). Type-safe, + preferred for embedders that server-render (LOGPile does). + +2. **Embedded HTTP** - `POST /render` accepts an optional part: + - multipart field `panels` (JSON), or + - request header `X-Chart-Panels: base64(json)` for the raw-JSON body case. + +3. **Snapshot-embedded** - reserved top-level key `"_chart_panels"` in the + snapshot JSON (underscore prefix = explicitly not Reanimator payload). + `chart` already tolerates unknown top-level keys; this one is consumed and + not shown in Snapshot Metadata. + +`NoticeTitle` / `NoticeBody` stay as a convenience shorthand, internally +converted to a single `{id:"notice", placement:"top", severity:"info", +blocks:[{kind:"text",...}]}` panel. Documented as legacy-but-supported. + +## 6. Rendering rules + +- Panels render as `section-card section-card-full` with an optional + `section-card--sev-{severity}` accent class (new CSS, ~6 lines). +- Placement resolution order: build the ordered list of built-in sections, then + for each panel insert at its anchor. `before:/after:` an unknown id -> fall + back to `top` and emit nothing visible (no error). `top` = after Snapshot + Metadata, before the first section. `bottom` = after the last section, before + the error/empty panels. +- Panel-to-panel anchors are resolved in input order (a panel can anchor to an + earlier panel's id). +- Section nav (if present) lists panels alongside built-in sections using + `title` + `id`. + +## 7. Public API additions (`viewer` package) + +```go +const PanelsContractVersion = "1.0" + +type Panel struct { + ID string + Title string + Placement string // "", "top", "bottom", "before:", "after:" + Severity string // "", "info", "warning", "critical" + Collapsed bool + Blocks []Block +} + +type Block struct { + Kind string // "text" | "keyvalue" | "list" | "links" | "table" + Text string `json:",omitempty"` + Rows []KeyValue `json:",omitempty"` // keyvalue + Items []string `json:",omitempty"` // list + Links []Link `json:",omitempty"` + Table *TableBlock `json:",omitempty"` +} + +type KeyValue struct{ Key, Value string } +type Link struct{ Label, Href string } +type TableBlock struct { + Columns []string + Rows []TableBlockRow +} +type TableBlockRow struct { + Cells map[string]string + Severity string +} + +// RenderOptions gains: +type RenderOptions struct { + // ... existing fields ... + Panels []Panel +} +``` + +JSON tags on these types are the wire contract for paths 2 and 3. + +## 8. Backward compatibility & versioning + +- No panels supplied anywhere -> output unchanged (golden test). +- `chart` module version: minor bump (feature, no breaking change). +- Contract version `1.0`; `chart` accepts `1.x`, rejects `2.x` gracefully. +- Existing `NoticeTitle/Body` behavior unchanged. + +## 9. Testing requirements + +- Placement: `top`, `bottom`, `before:cpus`, `after:storage`, unknown target, + panel-anchored-to-panel, multiple panels same anchor (stable order). +- Each block kind renders; `keyvalue` and `table` reuse existing table CSS classes. +- `table` severity column -> icon badge; row severity -> filter + row class. +- Escaping: `