Skip to content

Milestone 18e: Metrics + History + Exports + POAM tabs - #33

Merged
Exonical merged 2 commits into
mainfrom
devin/1779421664-milestone-18e-metrics-history-exports-poam
May 22, 2026
Merged

Exonical merged 2 commits into
mainfrom
devin/1779421664-milestone-18e-metrics-history-exports-poam

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented May 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Implements the four remaining data-visualization and extraction tabs in the Collection detail page: Metrics, History, Exports, and POAM. These replace the "Coming in milestone M18e" stubs.

Metrics tab (collection-tab-metrics)

  • Calls /metrics/summary/collection for headline KPIs (assets, stigs, checklists, assessments, assessed, findings by severity, results by result type, statuses).
  • Calls /metrics/summary/asset for a sortable per-asset table (name, STIGs count, assessed, findings H/M/L, pass, fail).
  • Calls /metrics/summary/stig for a per-benchmark table (benchmarkId + title, assets, rules, findings H/M/L).
  • Empty states rendered when no assets/STIGs are mapped yet.

History tab (collection-tab-history)

  • Filter bar: asset dropdown, ruleId text, status dropdown, start/end date pickers. Filters apply to both the stats panel and the entries list.
  • Stats card: total entries count, oldest entry date, per-asset breakdown (expandable <details>).
  • Entries table: flat chronological list of all review-history rows matching the filter.
  • Prune card (Manage+ only): retention-date picker + destructive delete button with a confirmation dialog. Scopes the delete to the currently-selected asset if one is picked in the filter bar.

Exports tab (collection-tab-exports)

  • Format toggle (CKL / CKLB / XCCDF), mode toggle (mono / multi, only for CKL/CKLB).
  • Asset multi-select (defaults to "all" when none checked). Lists each asset with its STIG count.
  • Download button POSTs the asset-stig selection to /archive/{format}, receives a Blob, and triggers a browser download via an anchor element + URL.createObjectURL.
  • Helper fetchBlob() in api/index.ts handles raw binary responses, bearer injection, error extraction, and Content-Disposition filename parsing (including RFC 6266 filename* UTF-8 support).

POAM tab (collection-tab-poam)

  • Aggregator (groupId / ruleId), template (EMASS / MCCAST), benchmark filter, asset filter, date, office, status, acceptedOnly toggle.
  • MCCAST-specific fields (Package ID, Auth Name) conditionally rendered.
  • GETs /collections/{cid}/poam as a binary stream and triggers a browser download.

Wiring

  • api/index.ts: 20+ new types and functions covering the metrics, history, blob-download, and POAM surfaces.
  • api/hooks.ts: useMetricsCollection, useMetricsByAsset, useMetricsByStig, useReviewHistory, useReviewHistoryStats, useDeleteReviewHistory.
  • detail.tsx: imports + conditional rendering for the four new tab components; stub filter now excludes the wired tabs.
  • 4 new Playwright tests (18/18 total): metrics empty state KPIs, history stats + empty entries, exports form visibility, POAM form visibility.

Review & Testing Checklist for Human

  • Sign in as admin → open a populated collection → Metrics tab → confirm KPI numbers are non-zero and match the API output from a raw curl.
  • Same collection → History tab → apply a filter (e.g., status=submitted) → confirm the entries table updates.
  • Same collection → Exports tab → select at least one asset, choose CKL, click Download archive → confirm the browser downloads a valid ZIP file containing .ckl files.
  • Same collection → POAM tab → click Download POAM → confirm the browser downloads a valid .xlsx that opens in Excel / LibreOffice Calc.
  • Sign in as evaluator (Restricted or Read grant) → confirm the Prune card in the History tab is hidden (Manage-gated).

Notes

  • The export/POAM download buttons currently trigger even when the Collection has zero assets — the API responds with an empty ZIP or a 400. Graceful client-side validation is tracked for a follow-up.
  • The downloadBlob() approach uses URL.createObjectURL + an ephemeral <a> element; this is the standard SPA download pattern. The URL is revoked after 30 seconds to free memory.
  • Only the stub for Labels (M18c) and Grants (M18f) remain as coming-soon cards now.

Link to Devin session: https://app.devin.ai/sessions/022810763c4643c0848ba894c1512b92
Requested by: @Exonical


Open in Devin Review

Co-Authored-By: Bryce Anglin <brycemanglin@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +348 to +352
rule.history.map((h) => ({
assetId: asset.assetId,
ruleId: rule.ruleId,
...h,
})),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Object spread of ...h overwrites canonical ruleId from parent rule group

In HistoryList, each flattened entry is built as { assetId, ruleId: rule.ruleId, ...h }. Because ReviewHistoryEntry (defined at web/src/lib/api/index.ts:610-621) includes an optional ruleId?: string field, the ...h spread comes after the explicit ruleId: rule.ruleId and will overwrite it whenever the server includes ruleId in the history entry — including when it is null or a different value. This causes the Rule column (web/src/app/pages/collections/history-tab.tsx:379) to render blank or incorrect data, and corrupts the React key at line 372 which relies on r.ruleId. The fix is to place the explicit properties after the spread: { ...h, assetId: asset.assetId, ruleId: rule.ruleId }.

Suggested change
rule.history.map((h) => ({
assetId: asset.assetId,
ruleId: rule.ruleId,
...h,
})),
rule.history.map((h) => ({
...h,
assetId: asset.assetId,
ruleId: rule.ruleId,
})),
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed by moving the spread before the explicit properties so assetId and ruleId from the parent groups always win. Pushed in the next commit.

Co-Authored-By: Bryce Anglin <brycemanglin@gmail.com>
@Exonical
Exonical merged commit f017c8e into main May 22, 2026
6 checks passed
@Exonical
Exonical deleted the devin/1779421664-milestone-18e-metrics-history-exports-poam branch May 22, 2026 04:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant