Historical tracking for resolution dates and remediation plans on compliance items. When a user changes the resolution_date or remediation_plan for a device, the previous value is preserved as an audit trail entry with a timestamp and the identity of the user who made the change. The most recent values remain directly queryable on the compliance_items table so existing VCL reporting queries continue to work without modification.
This spec also covers per-metric scoping of resolution_date and remediation_plan (GitLab issue #19). Previously these fields were edited at the hostname level (one value applied uniformly to all active metrics for a device). The per-metric extension allows analysts to set different resolution dates and remediation plans for individual metrics within the same device, matching the pattern already established by compliance notes.
- **Compliance_Item**: A row in the `compliance_items` table representing a single non-compliant device/metric pair.
- **Resolution_Date**: A DATE field on a compliance item indicating when remediation is expected to complete.
- **Remediation_Plan**: A TEXT field (max 2000 characters) describing the planned remediation approach.
- **History_Entry**: A row in the `compliance_item_history` table capturing a previous value of resolution_date or remediation_plan before it was overwritten.
- **Change_Reason**: An optional text field on a History_Entry describing why the change was made.
- **Detail_Panel**: The ComplianceDetailPanel UI component that displays device-level compliance information and allows editing of metadata fields.
- **VCL_Report**: The multi-vertical compliance reporting system that uses resolution_date for burndown forecasts and blocked/in-progress donut charts.
- **Current_Value**: The value stored directly on the compliance_items row, representing the most recent resolution_date or remediation_plan.
- **Metric_Selector**: The UI control in the Detail_Panel that allows the user to choose which metric(s) a remediation plan update applies to. Displays active metrics as selectable options with category-colored chips.
- **Active_Metric**: A compliance item with `status = 'active'` for the selected hostname — a metric currently failing for that device.
- **Metric_Chip**: A small colored badge displaying a metric ID, used throughout the compliance UI to visually identify metrics by category color.
- **Metadata_API**: The `PATCH /api/compliance/items/:hostname/metadata` endpoint that updates resolution_date and remediation_plan on compliance items.
### Requirement 1: Persist History on Field Change
**User Story:** As a compliance analyst, I want previous resolution dates and remediation plans to be preserved when I make changes, so that I have an audit trail of what was planned and when plans changed.
#### Acceptance Criteria
1. WHEN a user updates the resolution_date for a hostname via the metadata PATCH endpoint, THE History_Service SHALL insert a History_Entry containing the previous resolution_date value, the field name, the hostname, the timestamp of the change, the username of the user who made the change, and the change_reason if provided.
2. WHEN a user updates the remediation_plan for a hostname via the metadata PATCH endpoint, THE History_Service SHALL insert a History_Entry containing the previous remediation_plan value, the field name, the hostname, the timestamp of the change, the username of the user who made the change, and the change_reason if provided.
3. WHEN the previous value is NULL and the user sets a new value, THE History_Service SHALL insert a History_Entry with the old_value recorded as NULL.
4. WHEN the new value is identical to the current value, THE History_Service SHALL NOT create a History_Entry.
5. WHEN a bulk update changes resolution_date or remediation_plan for multiple hostnames, THE History_Service SHALL insert one History_Entry per hostname per changed field.
6. THE History_Service SHALL accept an optional change_reason field in the metadata PATCH request body.
### Requirement 2: History Storage Schema
**User Story:** As a system administrator, I want history entries stored in a dedicated table with proper indexing, so that history queries are fast and do not impact existing compliance_items queries.
#### Acceptance Criteria
1. THE Database SHALL store history entries in a `compliance_item_history` table with columns: id (serial primary key), hostname (text, not null), field_name (text, not null), old_value (text), new_value (text), change_reason (text), changed_by (text, not null), changed_at (timestamptz, default NOW()).
2. THE Database SHALL index the `compliance_item_history` table on (hostname, field_name) for efficient per-device history lookups.
3. THE Database SHALL index the `compliance_item_history` table on (changed_at) for chronological queries.
4. THE compliance_items table SHALL continue to store the current resolution_date and remediation_plan directly as columns, unchanged from the existing schema.
### Requirement 3: Reporting Isolation
**User Story:** As a VCL report consumer, I want burndown forecasts and donut charts to use only the current resolution_date, so that historical changes do not cause double-counting or incorrect projections.
#### Acceptance Criteria
1. THE VCL_Report SHALL read resolution_date exclusively from the compliance_items table for burndown and donut calculations.
2. THE VCL_Report SHALL NOT join or reference the compliance_item_history table for any reporting query.
3. WHEN multiple History_Entries exist for a hostname, THE VCL_Report SHALL use only the Current_Value from compliance_items.resolution_date for forecasting.
### Requirement 4: History Retrieval API
**User Story:** As a compliance analyst, I want to retrieve the change history for a device's resolution date and remediation plan, so that I can see who changed what and when.
#### Acceptance Criteria
1. WHEN a client requests the detail for a hostname, THE Compliance_API SHALL return the history of resolution_date and remediation_plan changes alongside the current values and notes.
2. THE Compliance_API SHALL return history entries sorted by changed_at in descending order (most recent first).
3. THE Compliance_API SHALL return a maximum of 10 history entries per hostname.
4. THE Compliance_API SHALL include the fields: field_name, old_value, new_value, change_reason, changed_by, and changed_at for each History_Entry.
5. IF no history entries exist for a hostname, THE Compliance_API SHALL return an empty array for the history field.
### Requirement 5: History Display in Detail Panel
**User Story:** As a compliance analyst, I want to see the history of changes to resolution date and remediation plan in the device detail panel, so that I can understand how plans have evolved over time.
#### Acceptance Criteria
1. THE Detail_Panel SHALL display a "Change History" section showing all History_Entries for the selected hostname.
2. WHEN a History_Entry exists, THE Detail_Panel SHALL display the field that changed, the old value, the new value, who made the change, when the change occurred, and the change reason if one was provided.
3. THE Detail_Panel SHALL display history entries in reverse chronological order (most recent change at the top).
4. WHEN no history entries exist, THE Detail_Panel SHALL display a message indicating no changes have been recorded.
5. THE Detail_Panel SHALL format resolution_date values as YYYY-MM-DD and remediation_plan values as truncated text with a tooltip or expandable view for long entries.
6. THE Detail_Panel SHALL include a text input for change_reason when saving resolution_date or remediation_plan changes.
### Requirement 6: Bulk Update History Tracking
**User Story:** As a compliance analyst, I want bulk xlsx updates to also track history, so that mass changes to resolution dates and remediation plans are auditable.
#### Acceptance Criteria
1. WHEN the bulk update commit endpoint applies changes to resolution_date or remediation_plan, THE History_Service SHALL create History_Entries for each hostname where the value changed.
2. THE History_Service SHALL record the changed_by as the username of the user who initiated the bulk update.
3. WHEN a bulk update row contains the same value as the current value for a hostname, THE History_Service SHALL NOT create a History_Entry for that field.
### Requirement 7: Database Migration
**User Story:** As a developer, I want the history table created via a standard migration script, so that it can be applied to existing deployments without manual intervention.
#### Acceptance Criteria
1. THE Migration SHALL create the `compliance_item_history` table if it does not already exist.
2. THE Migration SHALL create the required indexes on the `compliance_item_history` table.
3. THE Migration SHALL be idempotent and safe to run multiple times without error.
4. THE Migration SHALL NOT modify the existing compliance_items table structure.
**User Story:** As a compliance analyst, I want to set resolution dates and remediation plans for specific metrics within a device, so that I can track different remediation timelines for different compliance failures on the same hostname.
#### Acceptance Criteria
1. THE Metadata_API SHALL accept an optional `metric_id` field (string) in the request body to scope the update to a single metric for the given hostname.
2. THE Metadata_API SHALL accept an optional `metric_ids` field (array of strings) in the request body to scope the update to multiple specific metrics for the given hostname.
3. WHEN `metric_ids` is provided, THE Metadata_API SHALL update only the compliance_items rows matching the specified hostname AND metric_id values.
4. WHEN `metric_id` is provided (single string), THE Metadata_API SHALL update only the compliance_items row matching the specified hostname AND metric_id.
5. IF both `metric_id` and `metric_ids` are provided, THEN THE Metadata_API SHALL use `metric_ids` and ignore `metric_id`.
6. WHEN neither `metric_id` nor `metric_ids` is provided, THE Metadata_API SHALL update all active compliance_items for the hostname, preserving backward compatibility with the existing hostname-level behavior.
7. IF a provided metric_id does not correspond to an active compliance_item for the hostname, THEN THE Metadata_API SHALL return a 400 error identifying the invalid metric_id.
8. WHEN `metric_ids` is provided, THE Metadata_API SHALL validate that each entry is a non-empty string of 100 characters or fewer.
### Requirement 9: Per-Metric Metric Selector UI
**User Story:** As a compliance analyst, I want a metric selector in the detail panel when editing remediation plans, so that I can choose which metrics a resolution date or remediation plan applies to — matching the pattern used for notes.
#### Acceptance Criteria
1. WHEN the Detail_Panel is open for a hostname with more than one Active_Metric, THE Detail_Panel SHALL display a Metric_Selector above the resolution date and remediation plan inputs.
2. WHEN the Detail_Panel is open for a hostname with exactly one Active_Metric, THE Detail_Panel SHALL pre-select that metric and display the Metric_Selector as a single non-removable selection.
3. THE Metric_Selector SHALL allow the user to select one or more Active_Metrics simultaneously.
4. THE Metric_Selector SHALL display each option using the Metric_Chip component with the metric's category color, so that metrics are visually distinguishable.
5. WHEN the hostname has more than one Active_Metric, THE Metric_Selector SHALL display a "Select All" toggle that selects all Active_Metrics when activated.
6. WHEN all Active_Metrics are already selected, THE "Select All" toggle SHALL change to "Deselect All" and deselect all Active_Metrics when activated.
7. WHEN the Detail_Panel first opens for a hostname with multiple Active_Metrics, THE Metric_Selector SHALL pre-select all Active_Metrics by default, preserving the existing hostname-level editing experience.
### Requirement 10: Per-Metric Field Display
**User Story:** As a compliance analyst, I want to see the current resolution date and remediation plan for the selected metric(s), so that I know what values are already set before making changes.
#### Acceptance Criteria
1. WHEN a single metric is selected in the Metric_Selector, THE Detail_Panel SHALL populate the resolution date and remediation plan inputs with the current values from that specific compliance_item row.
2. WHEN multiple metrics are selected and all share the same resolution_date value, THE Detail_Panel SHALL display that shared value in the resolution date input.
3. WHEN multiple metrics are selected and they have different resolution_date values, THE Detail_Panel SHALL display the resolution date input as empty with placeholder text indicating "Multiple values".
4. WHEN multiple metrics are selected and all share the same remediation_plan value, THE Detail_Panel SHALL display that shared value in the remediation plan input.
5. WHEN multiple metrics are selected and they have different remediation_plan values, THE Detail_Panel SHALL display the remediation plan input as empty with placeholder text indicating "Multiple values".
6. WHEN the user saves with "Multiple values" placeholder visible and the input left empty, THE Detail_Panel SHALL NOT send that field in the PATCH request, preserving existing per-metric values.
### Requirement 11: Per-Metric History Tracking
**User Story:** As a compliance analyst, I want the change history to record which specific metric was changed, so that the audit trail reflects per-metric remediation plan changes accurately.
#### Acceptance Criteria
1. THE compliance_item_history table SHALL include a `metric_id` column (text, nullable) to record which metric the change applies to.
2. WHEN a per-metric update changes a field value, THE History_Service SHALL record the metric_id in the History_Entry.
3. WHEN a hostname-level update (no metric_id specified) changes a field value, THE History_Service SHALL record the metric_id as NULL in the History_Entry, indicating the change applied to all metrics.
4. THE History_Entry SHALL record the old_value and new_value per metric when a per-metric update is performed, reflecting the actual previous value of that specific compliance_item row.
5. WHEN a per-metric update targets multiple metrics with different current values, THE History_Service SHALL insert one History_Entry per metric that actually changed, each with its own old_value.
6. THE Compliance_API SHALL include the metric_id field in history entries returned to the client.
### Requirement 12: Per-Metric History Display
**User Story:** As a compliance analyst, I want the change history section to show which metric each change applied to, so that I can distinguish between hostname-wide changes and metric-specific changes.
#### Acceptance Criteria
1. WHEN a History_Entry has a non-null metric_id, THE Detail_Panel SHALL display the associated Metric_Chip next to the history entry.
2. WHEN a History_Entry has a null metric_id, THE Detail_Panel SHALL display "All metrics" label next to the history entry to indicate a hostname-level change.
3. THE Detail_Panel SHALL continue to display all history entries for the hostname in reverse chronological order, regardless of metric_id.
### Requirement 13: Per-Metric Reporting
**User Story:** As a VCL report consumer, I want burndown forecasts to use per-metric resolution dates, so that the forecast accurately reflects when each individual compliance failure is expected to be resolved.
#### Acceptance Criteria
1. THE VCL_Report SHALL read resolution_date from each individual compliance_items row (per hostname+metric_id pair) for burndown calculations, rather than using a single hostname-level value.
2. THE VCL_Report SHALL count each compliance_item row with a null resolution_date as a separate blocker in the donut chart.
3. THE VCL_Report SHALL bucket each compliance_item row by its own resolution_date month for the burndown forecast chart.
4. WHEN deduplicating by hostname for aggregated views, THE VCL_Report SHALL use the latest (MAX) resolution_date among all active metrics for that hostname.
### Requirement 14: Per-Metric History Migration
**User Story:** As a developer, I want the history table extended with a metric_id column via a migration script, so that per-metric history tracking can be deployed to existing environments.
#### Acceptance Criteria
1. THE Migration SHALL add a nullable `metric_id` column (text) to the `compliance_item_history` table.
2. THE Migration SHALL create an index on (hostname, metric_id) for efficient per-metric history lookups.
3. THE Migration SHALL be idempotent and safe to run multiple times without error.
4. THE Migration SHALL NOT alter or backfill existing history rows — pre-existing entries retain a NULL metric_id indicating they were hostname-level changes.
### Requirement 15: Backward Compatibility
**User Story:** As an existing user of the bulk upload and API workflows, I want hostname-level updates to continue working without modification, so that existing integrations and scripts are not broken by the per-metric change.
#### Acceptance Criteria
1. WHEN the Metadata_API receives a request without `metric_id` or `metric_ids`, THE Metadata_API SHALL update all active compliance_items for the hostname, matching the pre-existing behavior.
2. WHEN the bulk update commit endpoint processes a row, THE Metadata_API SHALL continue to apply resolution_date and remediation_plan to all active metrics for that hostname.
3. THE Detail_Panel SHALL default to "Select All" metrics when first opened, so that saving without changing the metric selection produces the same hostname-level update behavior as before.
4. WHEN all metrics are selected and the user saves, THE Metadata_API SHALL NOT include metric_ids in the request body, triggering the hostname-level update path for backward compatibility.