6.7 KiB
Implementation Plan: Multi-Metric Notes for Compliance Detail Panel
Overview
Extend the compliance notes system so a single note can be associated with multiple metrics in one action. Changes span three layers: a new migration script adding group_id to compliance_notes, updates to the POST /notes endpoint in backend/routes/compliance.js to accept metric_ids (array) and insert rows transactionally, and frontend changes in ComplianceDetailPanel.js to replace the single-select dropdown with a multi-select chip selector and group notes by group_id in the display.
Tasks
-
1. Create database migration for
group_idcolumn- 1.1 Create
backend/migrations/add_compliance_notes_group_id.js- Add
group_id TEXTcolumn tocompliance_notestable viaALTER TABLE - Create index
idx_compliance_notes_grouponcompliance_notes(group_id) - Backfill existing rows:
UPDATE compliance_notes SET group_id = 'legacy-' || id WHERE group_id IS NULL - Follow the existing migration pattern (sqlite3, serialize, console logging)
- Requirements: 6.1, 6.2, 6.3
- Add
- 1.1 Create
-
2. Update
POST /notesendpoint to support multi-metric submissions-
2.1 Modify the
POST /noteshandler inbackend/routes/compliance.jsto acceptmetric_idsarray- Accept
metric_ids(array of strings) as an alternative tometric_id(single string) - When both are provided,
metric_idstakes precedence - When neither is provided, return 400 with
"metric_id or metric_ids is required" - When
metric_idsis provided but is not an array, return 400 with"metric_ids must be an array" - Normalize single
metric_idinto a one-element array internally so the rest of the logic is uniform - Requirements: 5.1, 5.4, 5.5
- Accept
-
2.2 Add validation for
metric_idsarray entries- Validate that
metric_idshas at least one entry; return 400 with"At least one metric ID is required"if empty - Validate each entry is a non-empty string of 50 characters or fewer; return 400 with
"Invalid metric_id at index N"on failure - Reject the entire request if any entry fails validation (atomic rejection, no partial inserts)
- Requirements: 5.2, 5.6
- Validate that
-
2.3 Implement transactional multi-row insert with
group_id- Generate a
group_idusingcrypto.randomUUID()for each submission (single or multi) - Wrap all inserts in
BEGIN TRANSACTION/COMMITwithROLLBACKon error - Insert one
compliance_notesrow per metric ID, all sharing the samenote,group_id,created_by, andcreated_at - Requirements: 3.1, 3.2, 5.3, 6.1, 6.2
- Generate a
-
2.4 Update the response to return all created note rows
- After commit, query all created rows (joined with
usersforusername) and return as{ notes: [...] } - Each row includes
id,hostname,metric_id,note,group_id,created_at,created_by - Return HTTP 201 status
- Requirements: 5.7
- After commit, query all created rows (joined with
-
-
3. Update
GET /items/:hostnameto includegroup_idin notes response- Add
cn.group_idto the SELECT in the notes query within theGET /items/:hostnamehandler- The existing query already fetches notes for the hostname; just add the column
- No other changes to this endpoint
- Requirements: 6.3, 6.4
- Add
-
4. Checkpoint — Verify backend changes
- Ensure all backend changes are syntactically correct, ask the user if questions arise.
-
5. Replace single-select dropdown with multi-select MetricChipSelector in
ComplianceDetailPanel.js-
5.1 Replace
noteMetric(string) state withselectedMetrics(array) state- Initialize
selectedMetricswith the first active metric's ID when detail loads (matching current default behavior) - When there is exactly one active metric, pre-select it as a non-removable selection
- Requirements: 1.3, 1.4
- Initialize
-
5.2 Build the multi-select chip-based metric selector UI
- Replace the existing
<select>dropdown with a row of clickableMetricChipcomponents - Each active metric renders as a chip; selected chips get a highlighted border/background
- Clicking an unselected chip adds it to
selectedMetrics - Clicking a selected chip removes it, unless it is the only selected chip (minimum 1 selection)
- Only show the chip selector when there are 2+ active metrics (single metric is auto-selected)
- Style chips using existing
MetricChipcomponent patterns and category colors - Requirements: 1.1, 1.2, 1.5
- Replace the existing
-
5.3 Add Select All / Deselect All toggle
- Show a text toggle above or beside the chip row when there are 2+ active metrics
- "Select All" selects all active metrics; label changes to "Deselect All"
- "Deselect All" deselects all except the first metric (minimum selection invariant)
- Toggle label is a pure function of whether all metrics are selected
- Hide the toggle when there is only one active metric
- Requirements: 2.1, 2.2, 2.3
-
-
6. Update note submission logic to send
metric_idsarray- Modify
handleAddNoteto send{ hostname, metric_ids: selectedMetrics, note }instead of{ hostname, metric_id: noteMetric, note }- Disable the submit button when
selectedMetricsis empty or note text is empty - On success, clear note text, refresh the detail panel, and retain the current metric selection
- Handle the new response shape (
{ notes: [...] }) from the updated API - Requirements: 3.1, 3.4, 3.5
- Disable the submit button when
- Modify
-
7. Update note display to group by
group_id-
7.1 Add note grouping logic
- Group the
detail.notesarray bygroup_idbefore rendering - Notes sharing a
group_idare displayed as a single card with multipleMetricChipbadges - Notes without a
group_id(pre-migration legacy, should not occur after backfill) render as individual entries - Maintain reverse chronological order (newest
created_atfirst) across groups - Requirements: 4.1, 4.2, 4.3, 6.4
- Group the
-
7.2 Update the note card rendering
- For grouped notes, display all associated
MetricChipcomponents in the card header - For single-metric notes, display one
MetricChip(matching current behavior) - Preserve existing note card styling (background, border, padding, typography)
- Requirements: 4.1, 4.2
- For grouped notes, display all associated
-
-
8. Final checkpoint — Verify full feature
- Ensure frontend compiles without errors, ask the user if questions arise.
Notes
- No automated tests — feature is validated manually per user preference
- No new components or route modules required; all changes are scoped to existing files plus one migration
- The
group_idbackfill ensures legacy notes render correctly without null checks - Each task references specific requirements for traceability