Files
logpile/bible-local/docs/chart-custom-panels-spec.md
T
Mikhail ChusavitinandClaude Sonnet 5 fb0b0e3c55 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 <noreply@anthropic.com>
2026-09-02 16:19:57 +03:00

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*, 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

{
  "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:

  • 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)

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).
  • 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: <script>, quotes, and a javascript: href are inert.
  • Unknown block kind and unknown contract major -> visible muted note, not dropped.
  • id collision with cpus -> rendered id is ext-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.