Changelog ========= %%version%% (unreleased) ------------------------ Changes ~~~~~~~ - [dashboard] AttackWidget: count techniques via Event ACL find, drop restSearch. [Claude Opus 4.8 (1M context), iglocska] The ATT&CK heatmap widget proxied Event::restSearch(..., 'attack', ...), which hydrated every matching event with all of its attributes just to tally event-level ATT&CK galaxy tags (~83s on the dev corpus of 6k events / 1.9M attributes). It only needs per-technique event counts, so resolve matching events under the standard event ACL + filters via Event::filterEventIds() (createEventConditions: org/role + distribution + sharing_group_id, plus the full event filter set) with no event/attribute hydration, then group-count the event-level cluster tags over those events (COUNT(DISTINCT event_id)). The score universe is the matrix cell tags, and the returned payload is identical to the attack export footer shape, so the renderer (Attack.ctp) is unchanged. Counting is now event-level only. ~83s -> ~0.7s; per-technique counts and event ACL verified against SQL. v2.5.39 (2026-06-03) -------------------- New ~~~ - [dashboard] DD-52: "Save as template" opens in the slide-in panel. [iglocska] The ⋯ More → "Save as template…" item no longer navigates to a full-page form; it opens the template form in the dashboard's shared slide-in panel, saves via AJAX, and stays on the current board with an inline confirmation. - New save-template.module.mjs (mirrors config-io.module.mjs): opens the shared panel in a new `save-template` mode, fetches the server-rendered form (XHR GET, incl. its CSRF token) and injects it, then POSTs it with Accept: application/json so saveTemplate's existing REST branch returns a clean saved/failed JSON. Success → inline "✓ Saved" + Browse-templates / Close, no reload. Failure → re-fetch the form (single-use CSRF token is spent) and surface the error. ESC / ✕ / backdrop close via the shared chain + a mode-gated ESC handler + MutationObserver cleanup. - board.module.mjs: import openSaveTemplate + a `save-as-template` board action case next to export/import-config. - index.ctp: trigger gains data-misp-board-action="save-as-template" (href kept as a no-JS fallback); board root gains data-misp-board-savetemplate-url. - dashboard.default.css: `save-template` mode hides the preview pane + footer and uses the full-width body (joins the gallery/export/import rules); inline success-confirmation styling. Verified: node --check clean (2 modules). JS mirrors the proven config-io pattern; the save POST reuses the identical shipping form + endpoint, so the CSRF round-trip is already exercised by the full-page flow. Open (blocked): the live in-browser round-trip — same session-cookie CSRF blocker as the analyst track's live screenshot. - [analyst-dashboard] B15 AD-24: W12 optional galaxy_type filter. [iglocska] RecentGalaxyClustersWidget gains an optional typed `string` schema knob, `galaxy_type`, to scope the local-cluster feed to a single galaxy. When set, matches the typed value case-insensitively against the galaxy `type` OR `name` (so "threat-actor" or "Threat Actor" both work). Galaxy ids are resolved in PHP off the small (~140-row) galaxies table -- collation-independent and dodging SQL-function quoting -- then AND'd as GalaxyCluster.galaxy_id IN (). A value matching no galaxy yields an empty feed (honest); blank = all galaxies, so existing instances are unchanged. Added galaxy_type to $params + $schema (default '') + the placeholder; the resolution block sits after the existing default/deleted/window conditions, before find(). Pure additive widget change. Verified on the real render path: REST renderWidget (time_window=-1) -- threat-actor -> only the 3 threat-actor local clusters; Threat Actor / THREAT-ACTOR / threat actor (name + case variants) -> same 3; bogus value -> empty feed; blank -> all 50 galaxies (unchanged). Configure-form text input traced in source: WidgetSchema::getSchema serves $schema verbatim -> configure.module.mjs buildScalarField renders type:'string' as a text input (generic control proven in B9). Closes the PRD AD-W12 deferred filter item; flips Phase B15 to COMPLETE. - [analyst-dashboard] B15 AD-23: W11 per-target-type drilldowns (all 11 targets) [iglocska] RecentAnalystDataWidget linked only Event-targeted notes/opinions; every other target type rendered a bare chip. Re-verification on build (user confirmed "link all 11") corrected the spec's undercount of seven: both exclusion premises were false. - EventReport is NOT numeric-id-only: EventReport::simpleFetchById() resolves Validation::uuid (Model/EventReport.php:424) -> /eventReports/view/. - Note/Opinion/Relationship DO have a standalone view: AnalystDataController::view($type,$id) resolves the uuid via getIDFromUUID() (__valid_types = Opinion/Note/Relationship) -> /analystData/view//. So all eleven AnalystData::valid_targets resolve a uuid in their view(). Added a const VIEW_PATHS (objType -> view-path prefix) used in mapRow(): $drilldown = (uuid && isset(VIEW_PATHS[objType])) ? prefix.uuid : null. The analyst-data type rides the path, so the two-segment form still fits the prefix map. All links relative -> DD-03-admitted, no validator change (FeedList.ctp routes drilldown through DashboardURLValidator::validate). Verified on the real render path: REST renderWidget (time_window=-1) emits the right view/ for the six corpus target types (Attribute, Event, Object, GalaxyCluster, Note, Opinion -- incl. both new two-segment analyst-data links); each real link REST-GETs HTTP 200 + uuid body; bogus uuid fails closed. Five absent-from-corpus types asserted by the map + per-controller code-read. Closes the PRD AD-W11 open drilldown item. - [analyst-dashboard] B15: N/A unset-meta tooltip on the targeting card. [Claude Opus 4.8 (1M context), iglocska] Closes the second Phase B15 task (AD-22) in analyst-dashboard-progress.md. The "Targeting similar orgs" card reads "N/A" when neither the org's country nor sector resolves (AD-07) -- opaque on its own. Surface *why* via a hover tooltip: the org's country/sector are unset, and how to enable the metric. Delivered with one additive opt-in key on the shared StatGrid renderer (signed off, same posture as AD-21's statGridLabels / icon_class): - StatGrid.ctp: an optional `tooltip` row key (documented in the data contract) sets the card `title` attribute, overriding the default field-name fallback when present. No behaviour change for any other consumer -- a row without `tooltip` keeps the field-name title. - NewDataStatsWidget::targetingMetric(): sets `tooltip` on the N/A early-return. Verified two layers: (1) the real StatGrid.ctp render harness -- 5/5 PASS (N/A card title = the escaped tooltip, tooltip overrides the field-name title, a no-tooltip row falls back, labeled variant intact); (2) the real REST renderWidget pipeline -- temporarily blanked org-1 nationality/sector so the handler hit the N/A branch, confirmed value:"N/A" + the full tooltip string in the payload, then restored org-1 meta exactly (Luxembourg / Government). php -l clean. - [analyst-dashboard] B14: misp-iconify glyphs for New-data StatGrid. [Claude Opus 4.8 (1M context), iglocska] Final task of the AD-W7 rework (AD-21): swap the interim StatGlyph icons for the official MISP/misp-iconify set, now published. - Add MISP/misp-iconify as a submodule at app/files/misp-iconify (.gitignore whitelisted like the sibling app/files submodules). - Copy its generated exports/css/icons.css to app/webroot/css/misp-iconify.css (self-contained masked-SVG classes, background-color: currentColor) and load it in both dashboard layouts (default + Overmind; Cake's theme resolver falls back to the main webroot, same as dashboard.default.css). - StatGrid.ctp: support a row `icon_class` (a misp-icon name) rendered as . The StatGlyph `icon` key stays the path for the admin UsageDataWidget (unchanged). + .misp-stat-glyph .misp-icon { font-size: 22px } to match the glyph size. - NewDataStatsWidget: all 9 cards switch to icon_class — event/attribute/object/report/galaxy/analyst-note/analyst-opinion/ organisation (targeting) / sharing-group (published). The masked SVGs inherit currentColor, so they pick up the card's muted / hover-accent colour and theme for free in midnight. Verified: /css/misp-iconify.css serves 200; the REST renderWidget payload carries icon_class for all 9 metrics; the real misp-iconify.css + StatGrid.ctp paint the icons in both light and midnight. Webroot CSS is a copy of the submodule export — re-copy on submodule bump (Makefile target = follow-up). Closes the "glyph swap" task; completes Phase B14. - [analyst-dashboard] B14: NewDataStatsWidget +5 metrics, labels, 9 cards. [Claude Opus 4.8 (1M context), iglocska] Second task of the AD-W7 rework (AD-21). Grows the New-data pulse from 4 to 9 KPI cards and turns on the StatGrid opt-in labels from the previous commit. Five new GLOBAL scale-counts (AD-06), each windowed + prior-window delta + per-metric Redis cache like the original four: - Objects — Object.timestamp, deleted=0 - Event reports — EventReport.timestamp, deleted=0 - Galaxy clusters— GalaxyCluster.version, default=0 (LOCAL only, like W12), deleted=0 - Notes — Note.modified (UTC datetime; new timeConditionsDatetime gmdate helper — notes/opinions have no epoch ts and no deleted column) - Opinions — Opinion.modified Also: dropped the redundant "New " title prefix (the widget is "New data"), set $statGridLabels = true (opt into glyph+label cards), bumped the default tile 3x4 -> 4x6 for nine labelled cards. Icons are interim StatGlyph names pending the misp-iconify swap (the remaining B14 task, gated on the icon-set CSS classes). Verified at runtime through the real REST renderWidget pipeline (time_window=-1): all nine metrics match the DB exactly — objects 48,697, reports 435, local clusters 441, notes 53, opinions 27 (+ events 6088, attributes 1,919,285, targeting N/A, published 52), renderer=StatGrid. Closes the "+5 metrics" task under Phase B14. - [analyst-dashboard] B14: StatGrid opt-in labels (glyph + label) [Claude Opus 4.8 (1M context), iglocska] First task of the AD-W7 New-data rework (AD-21). StatGrid hid the text label behind a hover tooltip whenever a row had a glyph (DD-32, tuned for the dense admin UsageDataWidget), so the analyst New-data cards were glyph-only. A widget can now set `public $statGridLabels = true`; StatGrid.ctp then renders the glyph AND the label together — a `.misp-stat-head` row with a wider `.misp-stat-grid-labeled` column track and a 2-line label — instead of the glyph-only default. The admin UsageDataWidget leaves the flag unset and is unchanged (verified). Touching the shared StatGrid renderer is signed off; the opt-in keeps it additive for every other consumer. Verified via the real StatGrid.ctp (real StatGlyph) rendered with the flag on and off, light + midnight: labeled cards show glyph + full label + value + delta with clean 2-line wrap; the no-flag control stays glyph-only. Closes the "StatGrid opt-in labels" task under Phase B14. - [analyst-dashboard] B13: RecentGalaxyClustersWidget (W12) [Claude Opus 4.8 (1M context), iglocska] Closes Phase B13 (AD-W12 Recently Added Galaxy Clusters) as it appears in analyst-dashboard-progress.md. A 'what's new' feed of the newest LOCAL galaxy clusters (default=0; render=FeedList, AD-20) — shipped default=1 clusters excluded (batch import dates = noise). Recency = version (the only timestamp; add-or-update). Direct find() reusing GalaxyCluster::buildConditions ACL + contain ['Galaxy','Orgc'] + order version DESC + limit (fetchGalaxyClusters dropped the aliased order and didn't hydrate Galaxy/Orgc; both are standard-FK so a plain find hydrates them; ACL-identical, additive). Rows: Galaxy.icon, cluster value, description snippet, Orgc, relative version, galaxy-name chip, /galaxy_clusters/view/ drilldown. limit int (10, 1-50) + time_window (default -1). No cache. Verified: php -l clean; REST renderer=FeedList, 8 rows in correct version DESC order (newest hackathon2026 2026-04-14), org/icon/chips/drilldown resolve; stored-XSS test clusters (icon = HTML payload) confirmed NEUTRALIZED in the rendered HTML (0 raw onerror; payload only escaped inside the FA class attr via getClass h() + the quoted attribute); web-UI HTML http 200, 0 errors; screenshot. Appended to user 1's board as w_18 (14 tiles; all 3 feed widgets). - [analyst-dashboard] B12: RecentAnalystDataWidget (W11) [Claude Opus 4.8 (1M context), iglocska] Closes Phase B12 (AD-W11 Recent Analyst Data) as it appears in analyst-dashboard-progress.md. A 'what's new' feed of the newest visible analyst Notes + Opinions (render=FeedList, AD-19, re-scoped off the my-org-events brief to drop the child-UUID IN-list risk). Two ACL'd finds via AnalystData::buildConditions (distribution ACL; viewing is NOT perm_analyst_data-gated, matching AnalystDataController::index), merged + top-N by modified DESC. Rows: type glyph (sticky-note/balance-scale), note text / opinion comment as title, type + humanized target-object-type chips (the user's 'show target type' requirement), author org, relative modified, Opinion score subtitle. Event targets drill down to /events/view/ (Events::view resolves uuid, click-tested 200); other types show the type chip without a link. No cache. Build note: the model's uuid-keyed foreignKey=false Org/Orgc association does not hydrate via Containable here, so author org is resolved by an explicit bulk Organisation.find('list', uuid=>name). Verified: php -l clean; REST renderer=FeedList, 8 rows, org resolved, merge correct, Opinion score subtitle; web-UI HTML http 200, 0 errors, correct chips/icons/drilldown; Event uuid drilldown http 200; screenshot. Appended to user 1's board as w_17. - [analyst-dashboard] B11: RecentEventReportsWidget (W10) [Claude Opus 4.8 (1M context), iglocska] Closes Phase B11 (AD-W10 Recent Event Reports) as it appears in analyst-dashboard-progress.md. A 'what's new' feed of the N newest visible Event Reports (render=FeedList, AD-18). Direct ACL-correct find() reusing EventReport::buildACLConditions + DEFAULT_CONTAIN with order EventReport.timestamp DESC + limit (fetchReports ignores order/limit, so its ACL builder is reused in an orderable find; additive, no model code touched). deleted=0; optional time_window filter (default -1 = N-newest, no time bound); limit int (default 10, 1-50). Rows: file-alt glyph, report name, cleaned content snippet (strips ##/**/code/MISP @[..](uuid) embed markup), Orgc, relative time, 'Event #' context, Report chip, /eventReports/view/ drilldown. No cache (per-user ACL'd live fetch). Verified: php -l clean; REST renderWidget renderer=FeedList, 6 rows, DB cross-check exact; web-UI HTML render http 200, live fas fa-file-alt icon, 0 errors; screenshot on real CSS. Appended to user 1's board as w_16. - [analyst-dashboard] B10: FeedList render kind (template + CSS + glyph) [Claude Opus 4.8 (1M context), iglocska] Closes Phase B10 (FeedList render kind) as it appears in analyst-dashboard-progress.md. New shared read-only render kind for the W10-W12 feed widgets (AD-17): a reverse-chron feed of recently-added items — icon · title · meta line (org · relative-time · context) · chips · snippet · whole-row DD-03-gated drilldown. Bare flat row-list contract (like Trending/StatGrid). - app/View/Elements/dashboard/Widgets/FeedList.ctp (new) - .misp-feedlist-* token-driven block appended to dashboard.default.css (reuses semantic tokens -> midnight overlay retones for free) - thumbFeedList() glyph + REGISTRY entry in render-thumbs.mjs (CLAUDE.md new-render-kind rule) Verified: php -l clean; CSS braces 530/530; node --check clean; a focused harness over the REAL FeedList.ctp + real DashboardURLValidator passes 21/21 contract checks (DD-03 admit/drop, FA-class parity, all optional-key branches, empty state); light+midnight screenshots on the real CSS confirm the visual and token-only midnight retone. Pure addition - no existing widget/handler touched. Full renderWidget pipeline exercised with real data at B11. - [analyst-dashboard] promote EventStreamWidget used filters to typed schema; close Phase B9. [Claude Opus 4.8 (1M context), iglocska] Closes the Phase B9 verification + posture tasks AND adds a 4th promotion (EventStreamWidget tags/published/limit) in analyst-dashboard-progress.md; refreshes the session handoff (Phase B9 COMPLETE). EventStreamWidget fix (user-requested, sign-off granted): re-verified that the inherited handler() genuinely consumes tags / published / limit on W6 (EventStreamCardsWidget) — tags -> fetchEvent filter, published/limit via !empty()/$rawLimit — so they were real functional knobs only rendering as raw JSON. Promoted them on the PARENT EventStreamWidget::$schema (tags->string, published->bool default false, limit->int default 5) so both the original stream and the W6 cards gain typed controls via the verbatim inheritance (no subclass divergence). handler() UNCHANGED — already reads those keys; a default-injected config reproduces the current behaviour ({limit:5}, no tag/published filter). fields stays $params-only (array of column names, no scalar type fits; the cards renderer ignores it). tags->tag_filter chip picker NOT taken (needs a handler change -> not additive). Resolves the Discovered-work item. B9 verification (3 core promotions): verified each through the REAL configure.module.mjs (served the real webroot over http so the module + imports resolve; fed each widget its real data-widget-schema JSON; config={}). --dump-dom asserts: exclude_own_org -> checkbox (checked); dimension -> instead of a dot-notation row. Pure additive $schema edit — no platform/JS change: configure.module.mjs already builds enum->; falls back to the raw value when unmapped, so existing enum fields are unchanged. Documented in WidgetSchema.php. - AttackFlowMapWidget: mode enum_labels {2d:'2D map', 3d-globe:'Globe'} (stored value stays '3d-globe' for schema stability, DD-46). Refresh the now-stale echarts-gl references in / / help / the class doc-comment to the orthographic-globe reality. - [dashboard-v2] DD-45 D3 — orthographic globe branch in pew-pew builder (DD-46) [Claude Opus 4.8 (1M context), iglocska] Add the '3d-globe' render path to the pew-pew builder as a projection swap, not a new chart engine (DD-46). New orthographicProjection(rotate) helper wraps d3-geo geoOrthographic with hemisphere culling: returns [NaN, NaN] for back-hemisphere points (great-circle cosine < 0), which ECharts' geo coordinate system tolerates (NaN-safe bbox fit + canvas path skip) so the limb renders clean with no folding. When payload.mode === '3d-globe' the geo block gets geo.projection set to this wrapper at a North-Atlantic default rotate [10,-30]; '2d' keeps the native flat lon/lat. Same flows[], same three z-stacked arc layers, same tokenOn colours in both modes. Dispatch stays SYNC (no lazy import / no echarts-gl), voiding the DD-45 async-restructure gotcha. Renamed buildPewPewOption2D -> buildPewPewOption (it now draws both modes); updated the builders registry + the PewPewMap.ctp doc-comment. Verified: both modes render correctly through the real initChartsIn dispatch path (synthetic 6-arc payload) — flat map + clean orthographic globe with culled back face, arcs, glows, animated arrowheads intact. - [dashboard-v2] DD-45 D1 — geoOrthographic in d3-geo bundle (DD-46 globe path) [Claude Opus 4.8 (1M context), iglocska] Add geoOrthographic (d3-geo core) to d3-geo.bundle.mjs's export barrel so the AttackFlowMap 'globe' mode (DD-46) can swap the geo projection to an orthographic from-space view, reusing the Phase C arc engine. No new package — geoOrthographic ships in d3-geo core, so the rebuild adds only +0.1 KB gzipped (17.4 -> 18.1 KB raw / 7.4 -> 7.5 KB gz). VENDORING.md size row + exports list + recipe entry.mjs updated. - [dashboard-v2] DD-45 C4 — thumbPewPewMap gallery glyph. [Claude Opus 4.8 (1M context), iglocska] Phase C task C4. Adds the render-kind glyph for the PewPepMap render kind so the Add Widget gallery card for AttackFlowMapWidget gets a shape that evokes its output instead of falling through to thumbGeneric (CLAUDE.md render-kind glyph rule). - thumbPewPewMap(): soft continent blobs (low-opacity ellipses) + two arcs converging on a destination drawn as a filled core + a ripple ring — mirrors the 2D map's arcs and effectScatter glow, domain-independent per the file's guidance. - Registered in REGISTRY under the exact `$render` string 'PewPewMap'. - 80x45 viewBox, currentColor, content within the x=18..62 / y=10..35 safe area. node --check clean. Progress tracker: Post-5.5 New features, DD-45 Phase C task C4. - [dashboard-v2] DD-45 C3 — buildPewPewOption2D + pewpew dispatch. [Claude Opus 4.8 (1M context), iglocska] Phase C task C3. Adds the 2D pew-pew chart builder to charts.module.mjs and registers the `pewpew` render-kind dispatch. buildPewPewOption2D(payload, hostEl) — three z-stacked layers over a static `geo` world map (coords are raw lon/lat from the server-resolved centroids; native equirectangular, no custom projection): 1. `lines` static arc bodies — per-arc width log(value)-scaled (0.6..3.2px), opacity normalised (0.25..0.85); danger token. 2. `lines` animated trail — moving arrowhead per arc (effect.show, period 6, trailLength 0.3), zero-width base so layer 1 owns the body; danger token. 3. `effectScatter` destination glow — pulsing ripple at each victim centroid, sized by total incoming value; warning token. Dispatch wiring: - `pewpew: buildPewPewOption2D` added to the builders registry. - `kind === 'pewpew'` joins the `await ensureWorldMap()` branch in initChart (the geo coordinate system needs the 'world' map registered, same as the geo render kind). - payload.mode === '3d-globe' degrades to the 2D builder until the Phase D echarts-gl bundle ships (documented in the builder comment), so a 3D config opt-in never renders blank. Colours resolve via tokenOn() (theme-aware per PRD §8.1). Aggregate-only (DD-11/DD-45): no drilldown, no per-arc click. node --check clean; functional render verified in C5. Progress tracker: Post-5.5 New features, DD-45 Phase C task C3. - [dashboard-v2] DD-45 C3a — add EffectScatterChart to echarts bundle. [Claude Opus 4.8 (1M context), iglocska] Prereq for buildPewPewOption2D (C3): the DD-45 spec's destination glow (a pulsing ripple at victim centroids in the --misp-dash-warning token) is idiomatically an ECharts `effectScatter` series, which is a distinct series type not covered by C1's LinesChart-only rebuild. Without echarts.use([..., EffectScatterChart]) a type:'effectScatter' series silently renders nothing (same tree-shaking trap as Lines/Pie/Graph). - entry.mjs: EffectScatterChart added to the import + use() call. - Rebuilt via esbuild per VENDORING.md recipe. Verified `"effectScatter"` registration is new (old bundle 0 -> new 2). - Size delta over the C1 bundle: +4 KB raw / +1.2 KB gzipped (721 KB / 245 KB total; +20 KB raw / +5 KB gz over the DD-33 base). - VENDORING.md manifest row, entry.mjs example, and bundled-features list updated. Decision: user opted for the spec-faithful destination glow over a lines-only arrival cue (DD-45 fork resolved this session). Progress tracker: Post-5.5 New features, DD-45 Phase C (C3 prereq). - [dashboard-v2] DD-45 C2 — PewPewMap.ctp render-kind shim. [Claude Opus 4.8 (1M context), iglocska] Phase C task C2. Adds the PewPewMap render-kind template — a dumb host shim consistent with the other ECharts render kinds (WorldMap, BarChart, PieChart, NetworkGraph): it emits a `data-misp-chart="pewpew"` div carrying the {mode, flows[]} payload from AttackFlowMapWidget::handler(); charts.module.mjs (C3) dispatches on payload.mode and builds the option client-side. Refinement vs the Phase A plan: the planning note had the .ctp call buildPewPewOption2D directly, but the chart builders aren't exported and every other render kind dispatches via initChart on the data-misp-chart attribute. Keeping PewPewMap.ctp a dumb shim matches that established pattern; the mode-dispatch + "3d-globe degrades to 2D until Phase D" logic lives in the JS module (C3), not the template. - Empty flows → "No data." placeholder (mirrors WorldMap/BarChart). - Aggregate-only (DD-11/DD-45): no drilldown map, no per-arc click. - Body-filling: `.misp-chart` fills the body, `.misp-widget-body` owns scrolling. - Colour resolution deferred to client-side tokenOn (theme-aware). - php -l clean. Progress tracker: Post-5.5 New features, DD-45 Phase C task C2. - [dashboard-v2] DD-45 C1 — rebuild echarts bundle with LinesChart. [Claude Opus 4.8 (1M context), iglocska] Phase C (front-end 2D) of the pew-pew attack flow map. Adds the `lines` series type to the tree-shaken ECharts vendor bundle so buildPewPewOption2D (C3) can render great-circle arcs; without the echarts.use([..., LinesChart]) registration a type:'lines' series renders nothing (same tree-shaking trap as PieChart/GraphChart). - entry.mjs: LinesChart added to both the echarts/charts import and the echarts.use([...]) call. - Rebuilt via esbuild per VENDORING.md recipe. Verified the `"lines"` series-type registration is new (old bundle: 0 occurrences, new: 3). - Size delta: +15 KB raw / +4 KB gzipped (717 KB / 243 KB total) — LinesChart is a small series type, under the 15-20 KB estimate. - VENDORING.md: manifest row, entry.mjs example, and bundled-features list updated (also corrected the stale features list to include the already-present Pie/Graph). Progress tracker: Post-5.5 New features, DD-45 Phase C task C1. - [dashboard-v2] DD-45 B3 — PHPUnit coverage for AttackFlowMapWidget. [iglocska] Phase B3 of DD-45. Closes Phase B (backend) with 15 tests / 30 assertions / all green / ~52 ms. `app/Test/AttackFlowMapWidgetTest.php` — pure PHPUnit per the MISP test convention (no Cake bootstrap, no DB). Framework deps stubbed at the top of the file: - `ClassRegistry::init('EventTag')` returns a fake model with a controllable response queue; each `find('all', ...)` call pops the next queued response. Lets each test specify victim + attacker row sets independently without touching MySQL. - `WWW_ROOT` is defined to a per-test temp dir (`sys_get_temp_dir() . '/afm-test-/'`); `setUp()` drops a small iso-centroids.json fixture into the expected sub-path covering US/RU/IR/GB/DE/FR/CN/ JP/KR (the ISOs used across the suite). Coverage: * Empty paths — empty victims short-circuits; victims present but no attackers returns empty flows. * Single-event single-arc — IR → US, centroids resolved. * Self-loop skip — RU → RU drops; self-loop on one event doesn't poison sibling arcs on another event. * Multi-event aggregation — two events with the same pair → value=2. * Cross-product within event — 2 actors × 2 victims = 4 arcs. * Within-event ISO dedupe — duplicate tag rows pointing at the same cluster count once. * `max_arcs` truncation — value-desc sort + slice; highest-value arc always survives. `max_arcs=0` falls back to default 500. * Invalid ISO drop — `XYZ` (not [A-Z]{2}) dropped; lowercase `gb` upper-cased and kept; empty string dropped. * Missing centroid drop — ISO present in galaxy element but not in centroids JSON → arc silently dropped (matches widget's "ISO codes without a centroid are silently dropped" contract). * Mode normalisation — default = 2d; bogus value → 2d; `'3d-globe'` preserved. Implementation note carried in the file: assertions use `assertEquals` (==) not `assertSame` (===) for centroid arrays — `json_encode([54.0])` produces `"[54]"` (PHP drops `.0`), and `json_decode` returns int 54 not float 54.0. Mathematically equal but type-different. `dashboard-progress.md` Phase B header ticked (B1+B2+B3 all done). Next: Phase C — Front-end 2D (rebuild ECharts bundle with LinesChart, PewPewMap.ctp render kind shim, buildPewPewOption2D in charts.module.mjs, thumbPewPewMap glyph in render-thumbs.mjs, visual verification). - [dashboard-v2] DD-45 B2 — AttackFlowMapWidget: galaxy-derived attacker→victim resolution. [iglocska] Phase B2 of DD-45. Adds `app/Lib/Dashboard/AttackFlowMapWidget.php` implementing the spec's resolution path: per-event JOIN against event_tags → tags → galaxy_clusters → galaxies (filter type='country' for victims, 'threat-actor' for attackers) → galaxy_elements (filter key='ISO' for victims, 'country' for attackers). Victim ISO list is collected first; attacker query is event-id-restricted to the victim event set (cheap skip on events that can't yield arcs anyway). Per-event cross-product, self-loops skipped, aggregate by (src_iso, dst_iso) pair → value count; arsort + slice at max_arcs. Centroids loaded once per request from js/dashboard/charts/vendor/iso-centroids.json (DD-45 B1 output). ISO codes without a centroid (de-facto entities, unmappable sources) silently dropped from the rendered flows[]. Two spec deviations resolved during implementation, both with DD-45 + PRD §15 + progress.md updated to match reality: 1. `$category='events'` (spec draft said 'system'). Matches AttributeGeoMapWidget + ThreatActorCountryMapWidget; this is event-derived data, not server-internal. 2. Params simplified to `time_window` + `mode` + `max_arcs`. The Phase A spec draft mentioned org-meta `filter` + start_date/ end_date/days; B2 uses `time_window` for parity with AttributeGeoMapWidget (which doesn't carry an org filter either). Toolbar-reachable via the canonical-type machinery; if a per-org or absolute-date cut becomes load-bearing, extend via canonical-type, not new params. Verified: - php -l clean - Live REST renders for 5 configs: all-time / 1-day window / invalid mode / 3d-globe mode / max_arcs=0 - Cache hit on second identical request (Redis key `misp:attack_flow_map_cache:` present) - Empty-state path returns {mode:'2d', flows:[]} - Invalid mode falls back to 2d Dev-DB arc inventory (sub-noted in DD-45 + carried for next session): only 1 visible arc on the dev box (IR → US, from event 1421's Charming Kitten / APT33 / APT35 actor cluster + the US country galaxy tag). The other 3 dual-tagged events on the dev DB resolve to self-loops (Russia → Russia from Sofacy, Iran → Iran ×2 from MuddyWater) and are correctly skipped per spec. The Phase A scope-decision conversation estimated "~35 arcs" which was wrong — the cluster.country attacker path turns out to overlap heavily with same-country victim tags on this particular dev seed. Production instances are expected richer. Phase B3 (PHPUnit coverage) is the next commit. - [dashboard-v2] DD-45 B1 — iso-centroids.json from world-110m.geojson + build script. [iglocska] Phase B1 of the Pew-pew attack flow map (DD-45). Adds the country-centroid lookup table the widget needs to resolve attacker/victim ISO alpha-2 codes to [lon, lat] arc endpoints. Build script `app/files/scripts/build_iso_centroids.py`: - Reads the vendored `world-110m.geojson` (already antimeridian-split at vendor time, per vendor/VENDORING.md). - Cartesian shoelace centroid per polygon ring, area-weighted across MultiPolygon parts. Outer rings only — Lesotho-style enclaves shift the country centroid by 10-30 km at this scale, invisible at arc-endpoint resolution. - Antimeridian-spanning features (Fiji, Russia's Chukotka, US Aleutians) get their *western* polygons shifted +360° into continuous longitude space before the per-polygon centroid is computed; final result wrapped back into [-180, 180]. Without this, Fiji's two half-polygons average to a centroid in the Atlantic. - Name → ISO resolution via `pycountry`: exact lookup first (catches Russia → Russian Federation, Brunei → Brunei Darussalam), then fuzzy search. A small NAME_OVERRIDES table handles the ~12 Natural Earth abbreviations that don't match (W. Sahara → EH, Bosnia and Herz. → BA, etc.); de-facto entities without ISO codes (Somaliland, N. Cyprus) map to None and are silently dropped. - Build-time dependency only — `pycountry` is not vendored; script runs locally, output is committed. Output `app/webroot/js/dashboard/charts/vendor/iso-centroids.json`: - 175 entries, 4 KB raw / 2 KB gzipped. - Sorted-key JSON, format `{"ISO_A2": [lon, lat], ...}`. - Sanity-checked: US/RU/TR/BN/FJ/FR/GB/DE/IN/CN/ZA/AU/JP/BR/CA/ IL/NZ centroids all land inside the right country shape (a few overseas-territory countries drift ~5° west due to French Polynesia / Hawaii area pull — acceptable for arc endpoints). - Read server-side by the widget at render time; never exposed to the browser directly (the rendered flows[] payload carries pre-resolved centroids). `VENDORING.md` updated: new row in the files table; new "Reproducing `iso-centroids.json`" section with the recipe and implementation notes. `dashboard-progress.md` Phase B1 ticked. Phase B2 (AttackFlowMapWidget.php) is the next commit. - [dashboard-v2] DD-45 — pew pew attack flow map: planning docs (Phase A) [iglocska] Phase A of DD-45 — planning docs only, no code touched. `AttackFlowMapWidget` + `PewPewMap` render kind specced: animated attacker→victim arcs in 2D (lines-airline) and 3D (echarts-gl globe, lazy-loaded). Data source = threat-actor galaxy cluster's `country` element → country-galaxy tag on the same event (forks resolved against IP-src/ip-dst pairs, which conflate indicator location with attribution). Default mode 2D; widget cache_duration 3600s + cache_scope global; open to all users (matches AttributeGeoMapWidget posture DD-11). Files in this commit: - dashboard-design-decisions.md: DD-45 entry with all forks resolved, data shape contract, render-kind details, vendoring plan, theming, perf/safety, verification plan, reversibility. - dashboard-prd.md: DD-45 row in §15 binding-decisions table (status: "binding (spec)"). - dashboard-progress.md: new entry under "Post-5.5 — New features" with the full sequential phase plan (A done; B backend; C 2D front-end; D 3D lazy-load front-end; E polish). One commit per Phase B-E sub-task. - dashboard-handoff.md: refreshed to point next session at DD-45 Phase B (B1: build_iso_centroids.py + iso-centroids.json from world-110m.geojson). Carried prior-session detail trimmed to keep the active pointer prominent. UsageDataWidget audit + dark-theme readiness audit from this session captured in the "What landed" section. Refs DD-45. - [dashboard-v2] DD-43 — MailLogTool rotated-file traversal. [iglocska] Closes the bounded-scan caveat carried in the DD-41 search-filter sub-note: when `\$search !== ''` and the live-tail returns fewer than `\$limit` matching rows, MailLogTool now fills remaining slots from rotated companions (`.1` plain + `.N.gz`) in age order. Without `\$search`, behaviour is byte-identical to DD-41 — search-gated trigger IS the opt-in (no value in plowing through rotated history just for "latest N events"). Implementation factored into three new private methods: - _tailPlainFseek() — DD-41 fseek body extracted so the live file and uncompressed rotated `.1` share the same bounded- tail path. - _scanForward(\$path, \$isGzip, \$limit, \$search) — streaming gzopen+gzgets (or fopen+fgets) chronological scan with per- file array_reverse to newest-first; memory bound = matches × ~200B/row + 10M-line hard iteration cap. - _findRotated() — glob('.*') filtered to numeric-or- numeric+.gz suffix only, sorted by rank ASC. Bogus siblings like `.foo` / `.bak` never enter the candidate list. Per-rotated-file safety bundle `_isReadableAllowedFile()` re-runs the full DD-41 three-layer check (allow-list regex + is_file + realpath re-validation + is_readable) on every companion. A `.99` symlink resolving to /etc/passwd IS discovered as a rotation candidate (the symlink itself sits under the allow-list prefix) but dropped before any content is opened. Age-ordered concatenation preserves global newest-first across files without needing a final sort; the cap to \$limit at the very end catches any per-file overshoot. Empty-state header adapts: when zero matches AND rotated companions exist, "No matches for '' across N log files" replaces the DD-41 phrasing "in the last X of log" (which would understate the scan and mislead operators into bumping lookback_bytes). Requires new public helper MailLogTool::countLogFiles(\$path) — cheap, stats only. PHPUnit coverage backfilled — new app/Test/MailLogToolTest.php (24 tests, 54 assertions): path safety (5), DD-41 live-tail baseline (6), DD-43 rotated traversal incl. symlink rejection + bogus suffix filter (7), countLogFiles helper (4). No PHPUnit existed for MailLogTool before this DD — the refactor's blast radius warranted the coverage backfill. Verified: php -l clean ×2; PHPUnit 24/24; live REST renders against synthetic live+`.1`+`.2.gz` fixture across 8 scenarios (no-search, search-fills-live-only, search-spills-rotated, search-only-in-gz, zero-matches new across-N-files empty-state, etc.); reflection-driven safety check confirms `.99` and `.98` symlinks pointing at /etc/passwd are discovered as candidates by _findRotated() but rejected by _isReadableAllowedFile(). Backward- compat: omit \$search or pass empty → byte-identical to DD-41 / DD-41 sub-note. Pure addition. Reverse = revert MailLogTool.php + MispMailLogWidget.php + delete the new PHPUnit; behaviour falls back to DD-41 / DD-41 sub-note exactly. No view / CSS / JS / model / controller touched. Refs progress-tracker task: "MispMailLogWidget rotated-file traversal (DD-43)". - [dashboard-v2] DD-41 — server-side search filter on MispMailLogWidget. [iglocska] User-requested follow-up to DD-41. Tier 2 implementation (server- side filter via config param, no protocol-plumbing changes), chosen over Tier 1 (client-side-only, can't reach backward in the log) and Tier 3 (transient-search-param protocol extension, larger blast radius). MailLogTool::tail() gains an optional 4th arg $search (default ''). When non-empty, each parsed row is filtered by case-insensitive substring match against recipient + relay + queue_id + message BEFORE the limit cap — so $limit reflects matching rows, not all rows in the window. Substring not regex (good enough for "find all entries for alice@..."; avoids the false-positive surface of a regex over user-controlled input). MispMailLogWidget exposes `search` as a config param + $schema + $placeholder slot. When set, the default lookback bumps from 64 KB to 1 MB so the filter has range to scan (the operator can still override `lookback_bytes` either way; the 4 MB hard-cap is preserved). Header text adapts: - filter active + matches: "N match(es) for '' · " - filter active + no match: "No matches for '' in the last of log" - filter empty: same per-status tally as before Caching: WidgetCache keys by sha256(config) (DD-20), so each distinct search term naturally gets its own cache entry — no invalidation plumbing needed. Bounded-scan caveat documented in the DD-41 sub-note: even at the 4 MB hard-cap, the filter doesn't open rotated files (mail.log.1, .gz companions). Search-deep-history is a deferred follow-up, not v1 scope. Verified: php -l clean x2; synthetic-fixture matrix covering unfiltered / substring-recipient / substring-message / no-match / case-insensitive / limit-clamps-to-newest-match all pass. Live REST renders against /var/log/mail.log confirm the bumped lookback ("in the last 1.0 MB of log" in the empty-state) and the filter-active header text. Backward-compat: omit `search` -> same behaviour as before the sub-note. Refs progress-tracker tasks: "MailLogTool — add `$search` filter parameter", "MispMailLogWidget — wire `search` config param", "Verify + DD-41 sub-note + handoff + commit". - [dashboard-v2] MispMailLogWidget — outgoing-mail status tail (DD-41) [iglocska] Site-admin widget reading the last N postfix delivery records from the OS mail log via MailLogTool, rendered as UserList rows with the DD-41 glyph slot — status → glyph mapping: sent → check (success / green) deferred → warn (warning / amber) bounced → danger (danger / red) expired → danger undeliverable → danger Header row tallies per-status counts ("5 events · 1 Sent · 1 Deferred · 1 Bounced · 1 Expired · 1 Undeliverable"); per-row meta carries status label + humanised age + relay + truncated MTA message. Humanisation shape lifted from DD-40 / IndexTable/Fields/caching.ctp so the "ago" reads identically across the operational widget family. Two empty-state paths: - InvalidArgumentException (path not in allow-list) → message row + recipe explaining the /var/log|/tmp/ allow-list. - RuntimeException (file missing / unreadable — the expected default-install state, since www-data is not in adm on standard distros) → message row + recipe covering the three operator strategies: distro adm membership, dedicated rsyslog tee under /var/log/misp/, or a POSIX ACL. Lets operators pick their preferred trade-off rather than silently expanding www-data's /var/log/* read scope. Configurable params: log_path (default /var/log/mail.log), limit (default 20, hard-cap 200), lookback_bytes (default 65536, hard-cap 4 MB). $cache_duration = 30 (anti-thundering-herd only — tail-read is cheap). $autoRefreshDelay = 60. Site-admin gated via checkPermissions(). Verified: php -l clean; live REST render against /tmp synthetic fixture → HTTP 200, 5 rows (one per status branch) + correct header tally + correct glyph tokens; live REST render against /var/log/mail.log (unreadable for www-data) → HTTP 200, message-row with title + path + 5-line recipe; headless-Chrome screenshot against the full CSS stack (bootstrap5 + mainOvermind + fontawesome7 + dashboard.default + midnight + Overmind theme) shows distinct green/amber/red glyphs and a properly-expanded
recipe block side-by-side; LoggedInUsersWidget (the existing UserList consumer) class histogram unchanged → backward-compat held. Refs progress-tracker task: "Write MispMailLogWidget" + "Verify widget — REST + headless screenshot". - [dashboard-v2] UserList — optional `recipe` slot on message rows. [iglocska] Adds an optional `recipe` array (list of strings) to UserList message rows. When set, the renderer wraps the lines in an inline `
How to enable this widget
    ...
` block beneath the title + value text — pure HTML, no JS, accessible by default. Each recipe line h()'d individually (DD-34). Lets an empty-state row carry operator setup help inline without needing a heavier slide-in side panel. Backward-compat verified: LoggedInUsersWidget (which emits no recipe field) renders byte-identical class histogram — no `misp-user-help` class appears in its HTML. Sets up the empty-state shape MispMailLogWidget (DD-41) uses when /var/log/mail.log is unreadable on standard distros — the recipe lists the operator's access-strategy options (adm membership, dedicated rsyslog tee, POSIX ACL). Refs progress-tracker task: "Extend UserList message-row with recipe slot". - [dashboard-v2] MailLogTool — postfix mail-log tail reader (DD-41) [iglocska] Bounded tail-read of a postfix-format mail log: fseek from end-of-file (default 64KB lookback) so the parser stays O(lookback) and doesn't load multi-MB rotated logs into memory. Parses both RFC3339 (modern rsyslog default) and legacy syslog timestamp formats, scoped to postfix delivery processes (smtp / lmtp / local / virtual / error / bounce) that emit `status=`. Filters status to the recognised set {sent, deferred, bounced, expired, undeliverable}; everything else is silently dropped. Returns up to N normalised rows newest-first: {ts, recipient, status, message, relay, queue_id}. Three-layer path hardening: (1) reject `..` / NUL byte upfront; (2) regex match against `^/(var/log|tmp)/...` allow-list; (3) post- existence realpath() re-check so a symlink under /tmp pointing at /etc/shadow can't slip through the prefix. Verified: symlink /tmp/x.log → /etc/passwd is blocked at the realpath layer; the traversal forms `/var/log/../etc/passwd` and `/tmp/../etc/passwd` are blocked at the `..` layer; NUL injection blocked. Verified against a synthetic fixture exercising all 5 status branches (`sent`, `deferred`, `bounced`, `expired`, `undeliverable`) across both timestamp formats + a malformed line + queue-lifecycle records (qmgr / pickup / cleanup / master) that should NOT match — parser returns exactly the 5 expected delivery rows, newest-first. Refs progress-tracker task: "Write MailLogTool parser". - [dashboard-v2] UserList — optional `glyph` slot (token allow-list) [iglocska] Adds an optional `glyph` field to the UserList row contract — token from `{check, warn, danger, info}` selects an inline-SVG status chip that overrides the org-logo / initials-chip avatar. Tokens drive a matching `.misp-user-glyph-{token}` CSS class that pulls the corresponding `--misp-dash-{success,warning,danger,info}` token pair, so themes that only retone the tokens get status colours for free. DD-34 escaping invariant preserved: the widget passes the TOKEN, not raw SVG. Token list is hard-coded in the renderer; anything outside the four-entry allow-list falls through to the legacy avatar path. Backward-compat verified: LoggedInUsersWidget (the existing DD-35 consumer) emits no `glyph` field and renders byte-identically — no `misp-user-glyph*` classes appear in its HTML; the 2 initials chips + 1 org-logo histograms are unchanged. Refs progress-tracker task: "Extend UserList with glyph slot". - [dashboard-v2] MispCacheStatusWidget — server & feed cache freshness (DD-40) [Claude Opus 4.7 (1M context), iglocska] Site-admin-only NetworkGraph widget surfacing cache age across sync servers and feeds with caching enabled. Same hub-and-spoke front end as MispAdminSyncTestWidget (DD-33), different dimension. Pure consumer of existing model helpers — no Redis key read directly: - Server::find on caching_enabled=1 + attachServerCacheTimestamps() - Feed::find on caching_enabled=1 + attachFeedCacheTimestamps() Per-row classification (user-spec thresholds): - cache_timestamp null/empty → status 'error', label "never" - age >= 86400 s (>= 1 day) → status 'warn', label humanised age - age < 86400 s → status 'info', label humanised age Humanisation shape lifted from IndexTable/Fields/caching.ctp for visual consistency with the existing Server / Feed list views ("85d 3h", "5h 30m", "45m 12s"). Age embedded in the visible node name ("#7 5007 · 85d 3h") because age IS the load-bearing signal for this widget — staying in tooltip would hide it. Tooltip carries URL + a status sentence ("Cached 85d 3h ago — stale. Server cache."). Empty state mirrors the sync widget pattern (data.error message + the hub-only nodes list). $autoRefreshDelay=false (manual refresh, cache state moves slowly); $cacheLifetime=1 (anti-spam tick-level cache); default size 4×5; site-admin gated via checkPermissions(). Verified: php -l clean. Live REST render → HTTP 200, 5 spokes on the dev box (3 servers: 1 stale 85d 3h, 2 never-cached; 2 feeds: both stale 65d) with correct kind + status mapping. - [dashboard-v2] NetworkGraph — per-node kind, info status, feedSymbol (DD-40) [Claude Opus 4.7 (1M context), iglocska] Surgical extension to the NetworkGraph render kind, additive and backward-compatible — MispAdminSyncTestWidget renders byte-identically after the change (defaults preserve the old code path). - charts.module.mjs: * new feedSymbol(colour) builder — RSS-waves glyph (two concentric arcs + a filled dot in the lower-left, user-chosen via fork in DD-40 vs stacked-chevrons / document-with-arrow); same theme-aware `image://data:image/svg+xml;base64,...` pipeline as serverSymbol * statusColour gains `info: --misp-dash-info` (fifth tier; resolves to the cyan token already present in dashboard.default.css) * symbolFor restructured as a nested map symbolFor[kind][status] (2 kinds × 5 statuses = 10 cached symbols), built once per render * node-mapping reads optional `kind` field (default 'server'); hub overrides to 'server' unconditionally — the diagram centre is always a MISP instance regardless of what its spokes are - NetworkGraph.ctp PHPDoc updated to document the new fields Regression verified: live JSON render of MispAdminSyncTestWidget is byte-identical to pre-change (no `kind` emitted; old status set unchanged). - [dashboard-v2] MispAdminHealthWidget — application-layer health rollup (DD-39) [Claude Opus 4.7 (1M context), iglocska] Site-admin-only widget that surfaces only the diagnostic checks that are not green. A healthy MISP shows just the "All checks passing" header row — the absence-of-rows is the good-news signal. Pure consumer of existing Server::*Diagnostics() methods — no diagnostic logic is re-implemented: - getCurrentGitStatus(true): upToDate==='older' → warn - getIniSetting()/dbConfiguration(): each-under-recommended → warn - writeable{Dirs,Files}Diagnostics() + readableFilesDiagnostics(): rolled into one row, value=2/readable=1 fail, value=1 warn - moduleDiagnostics() per type (Enrichment/Import/Export): 2=warn, error=fail, disabled=skip (user-intentional) - gpgDiagnostics(): status 2-4=fail, 1=skip (could be intentional) - stixDiagnostics(): operational!=1=fail, invalid_version=warn - sessionDiagnostics(): handler !== 'php_redis' → warn - dbSchemaDiagnostic(): version mismatch + update_locked=warn, update_fail_number_reached=fail Header severity reflects the worst row's severity; row count drives the header label ("All checks passing" / "N issues found"). $render='HealthList' (DD-39 renderer); $category='system'; default size 3×4; $autoRefreshDelay=60; $cache_duration=300 (DD-20) — five of the diagnostics do real work (Python subprocess for stix, HTTP pings for modules, SHOW VARIABLES for MySQL config). Site-admin gated via checkPermissions(). Pure addition; no existing widget / model / controller touched. - [dashboard-v2] HealthList render kind (DD-39) [Claude Opus 4.7 (1M context), iglocska] New typed-row renderer for issue-only health rollups. Row shape: [severity glyph] check_name [detail] [severity chip]. Only non-OK rows ever reach the renderer — the consumer widget filters info- tier results out before emitting. The chip + severity glyph carry the colour signal together, so the glyph set is intentionally narrow (two: warn-triangle and danger-circle, plus a success check-mark for the "all checks passing" header). - app/View/Elements/dashboard/Widgets/HealthList.ctp — typed rows (header/check/message); severity_class allow-list = warning, danger; drilldown gated via DashboardURLValidator (DD-03); renderer owns escaping (DD-34) — handler() emits raw strings, every scalar h()'d exactly once. - app/webroot/css/dashboard/dashboard.default.css — .misp-health-* token-only block (3-px coloured left border per severity; muted-pill chips matching .misp-queue-chip-*). Themes that only redefine the --misp-dash-* tokens retone for free. - app/webroot/js/dashboard/gallery/render-thumbs.mjs — thumbHealthList glyph (3 stacked rows: triangle/circle severity mark + name bar + chip) registered in REGISTRY (CLAUDE.md rule). Pure addition — no existing renderer / widget / CSS rule touched. No ECharts series; no bundle rebuild needed. - [dashboard-v2] QueueList render kind + MispAdminWorkerWidget rework (DD-38) [Claude Opus 4.7 (1M context), iglocska] Reworks MispAdminWorkerWidget from a 3-rows-per-queue SimpleList list into a single row per background-queue: [glyph] queue_name [alive/total] [pending_jobs] Workers and jobs chips carry independent semantic colours so "workers alive but stuck" reads at a glance: workers `0/0` warning (precedence), `x=100` danger. Scheduler queue has no jobCount, so its jobs chip is omitted (zero would falsely read as "0 pending"). New render kind QueueList (template + .misp-queue-* token-only CSS + thumbQueueList registered per CLAUDE.md). New QueueGlyph tool with six 24x24 currentColor SVG glyphs keyed by BackgroundJobsTool::VALID_QUEUES (default boxes, email envelope, cache lightning, prio flame, update sync-arrows, scheduler clock). FontAwesome rejected because the dashboard layouts load different FA majors per theme (DD-32 lesson). Folded-in bug fix: workerDiagnostics() mixes per-queue arrays with top-level scalar/bool summary keys (controls, proc_accessible, supervisord_status) at the same dict level. The old widget skipped two by name and would crash on supervisord_status under the new array_key_exists(jobCount) call. Iteration now constrains to BackgroundJobsTool::VALID_QUEUES instead of skipping by name, so any future summary key the diagnostics function adds can't render as a "queue". Widget defaults: $render SimpleList -> QueueList; size 2x2 -> 3x4; autoRefreshDelay=5 kept (worker freshness is the value here); no cache (diagnostics is cheap). Renderer h()s every interpolated scalar (DD-34); class names allow-listed; drilldown DD-03 validated to /servers/serverSettings/workers. Closes Post-5.5 task: MispAdminWorkerWidget rework — QueueList render kind, one row per background-queue with two coloured chips, in docs/dev/dashboard-progress.md. Verified: php -l clean x3 (widget, glyph, renderer); node --check clean on render-thumbs.mjs; live REST render HTTP 200 (6 queues + header "6 queues · 21 workers alive"); HTML render class-histogram (10 info chips + 1 warning chip on the dev box's empty scheduler queue); 10/10 threshold unit checks pass across all 4 chip states; headless-Chrome screenshot against the full CSS stack (bootstrap5 + mainOvermind + fontawesome7 + dashboard.default + midnight + overmind theme override) exercising all 4 chip colours and all 6 glyphs renders as expected (prio glyph iterated once from an initial teardrop into a clearer curling flame). Temp eye-check file deleted post-screenshot. - [dashboard-v2] user-list.module.mjs — search filter + invalidate- sessions panel flow (DD-36) [Claude Opus 4.7 (1M context), iglocska] Wires the UserList render kind's two affordances, theme-independently (only the board events + the dashboard's own side panel — no theme modal): - Client-side search: [data-misp-user-search] filters rows by name/meta; the term is kept per widget instance and re-applied after each render (the board replaces the body wholesale on auto-refresh). Idempotent re-wiring via a per-element flag. - Per-row action: [data-misp-user-action-url] opens the confirm form in the side panel (new 'confirm' mode) via GET, then POSTs the form's FormData (carries the fresh _Token) back to the same endpoint. Both requests use Accept: text/html so MISP keeps them non-REST and enforces CSRF. On {saved:true} it closes the panel and asks the board to re-render the source widget — repaint from server truth, no optimistic DOM surgery. board.module.mjs: import + init the module after the board is built, and add a misp-board:render-widget listener (re-render a widget by instance id) — event-driven like add-widget-pending, no module forking. Panel 'confirm' mode brings its own ESC handler + a hidden-attribute MutationObserver for cleanup; the shared ✕/backdrop close chain is reused. Verified: node --check; a hermetic headless-Chrome harness (stubbed fetch) asserts all 7 behaviours green — search hide/show, panel opens in confirm mode, GET form injected, submit dispatches render-widget(t1), panel closes, mode cleared on close. - [dashboard-v2] UserList search box + per-row action affordances (DD-36) [Claude Opus 4.7 (1M context), iglocska] UserList gains two opt-in affordances driven by the data contract: - header 'search' => true renders a client-side filter input; - user-row 'action' => {url,label} renders a sibling icon button carrying the URL (inline-SVG logout glyph, not FA). Each user row is restructured from a single into a
wrapper holding an inner .misp-user-main drilldown link (avatar+name+badge) plus the action