- 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 <noreply@anthropic.com>
9.8 KiB
ТЗ: 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*, embeddedPOST /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_configor the hardware sections.
4. Data contract
4.1 Envelope
{
"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:<section-id>, after:<section-id>. <section-id> 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:
{ "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:
columnsis the display order. Aseveritycolumn 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: onlyhttp,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):
-
In-process -
viewer.RenderOptions.Panels []viewer.Panel(and a matchingviewer.RenderHTMLWithOptionsalready exists). Type-safe, preferred for embedders that server-render (LOGPile does). -
Embedded HTTP -
POST /renderaccepts an optional part:- multipart field
panels(JSON), or - request header
X-Chart-Panels: base64(json)for the raw-JSON body case.
- multipart field
-
Snapshot-embedded - reserved top-level key
"_chart_panels"in the snapshot JSON (underscore prefix = explicitly not Reanimator payload).chartalready 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-fullwith an optionalsection-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 totopand 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)
const PanelsContractVersion = "1.0"
type Panel struct {
ID string
Title string
Placement string // "", "top", "bottom", "before:<id>", "after:<id>"
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).
chartmodule version: minor bump (feature, no breaking change).- Contract version
1.0;chartaccepts1.x, rejects2.xgracefully. - Existing
NoticeTitle/Bodybehavior 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;
keyvalueandtablereuse existing table CSS classes. tableseverity column -> icon badge; row severity -> filter + row class.- Escaping:
<script>, quotes, and ajavascript:href are inert. - Unknown block kind and unknown contract major -> visible muted note, not dropped.
idcollision withcpus-> rendered id isext-cpus, anchor still works.- Size cap and count cap -> panels omitted + top notice.
- Merge precedence across the three delivery paths.
- Golden HTML for a representative multi-block panel.
10. Docs to update
docs/embedding.md- new "Custom panels" section with a full example.bible-local/decisions/- new decision record (custom panels, why a bounded block vocabulary instead of raw HTML).bible-local/architecture/ui-information-architecture.md- panels in the page structure and section order.bible-local/architecture/data-model.md- the envelope and Block union.
11. LOGPile migration (after this ships)
Replace the interim privacyNotice -> NoticeTitle/Body shim in
internal/server/handlers.go with:
opts.Panels = []viewer.Panel{{
ID: "privacy",
Title: "Customer data",
Placement: "before:cpus",
Severity: privacySeverity(scan), // warning if any high, else info
Blocks: []viewer.Block{
{Kind: "keyvalue", Rows: customerRows(scan)},
{Kind: "table", Table: findingsTable(scan)},
},
}}
The separate below-iframe "Customer data" panel in the LOGPile UI can then be retired, since the same content renders inside the chart.
12. Estimated effort
chart: types + JSON contract, placement resolver, template blocks, CSS accent, merge logic, tests, docs - ~2 focused days.- LOGPile migration - ~half a day.