Auto-sync .kiro/ from master (post-checkout hook)

This commit is contained in:
Jordan Ramos
2026-05-19 15:01:25 -06:00
parent ada9df26a8
commit 8ebd7e4d5e
23 changed files with 3485 additions and 19 deletions

View File

@@ -0,0 +1 @@
{"specId": "731f6cf9-7d25-41ea-a550-745d6a917b3f", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -0,0 +1,279 @@
# Design Document: Remediation Plan History
## Overview
Adds an append-only audit trail for resolution_date and remediation_plan changes on compliance items. The design preserves the existing compliance_items schema (current values remain directly queryable) and introduces a new `compliance_item_history` table for historical entries. The pattern mirrors how `compliance_notes` works — separate rows with timestamps and attribution.
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ ComplianceDetailPanel │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────┐ │
│ │ Resolution │ │ Remediation │ │ Change Reason (text) │ │
│ │ Date Input │ │ Plan Input │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └───────────┬───────────┘ │
│ │ │ │ │
│ └──────────────────┴──────────────────────┘ │
│ │ │
│ PATCH /metadata │
│ { resolution_date, remediation_plan, │
│ change_reason } │
│ │ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Change History Section │ │
│ │ [date] [field] [old→new] [by user] [reason] │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Backend (compliance.js) │
│ │
│ PATCH /items/:hostname/metadata │
│ 1. Validate inputs │
│ 2. SELECT current values from compliance_items │
│ 3. Compare old vs new — skip if identical │
│ 4. INSERT into compliance_item_history (per changed field) │
│ 5. UPDATE compliance_items with new values │
│ │
│ GET /items/:hostname │
│ (existing) + SELECT from compliance_item_history │
│ LIMIT 10 ORDER BY changed_at DESC │
│ │
│ POST /vcl/bulk-commit │
│ For each hostname: same compare-then-insert pattern │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ PostgreSQL │
│ │
│ compliance_items (unchanged) │
│ resolution_date DATE │
│ remediation_plan TEXT │
│ │
│ compliance_item_history (new) │
│ id, hostname, field_name, old_value, new_value, │
│ change_reason, changed_by, changed_at │
└─────────────────────────────────────────────────────────────────┘
```
## Data Model
### New Table: compliance_item_history
```sql
CREATE TABLE IF NOT EXISTS compliance_item_history (
id SERIAL PRIMARY KEY,
hostname TEXT NOT NULL,
field_name TEXT NOT NULL CHECK (field_name IN ('resolution_date', 'remediation_plan')),
old_value TEXT,
new_value TEXT,
change_reason TEXT,
changed_by TEXT NOT NULL,
changed_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_compliance_history_hostname_field
ON compliance_item_history(hostname, field_name);
CREATE INDEX IF NOT EXISTS idx_compliance_history_changed_at
ON compliance_item_history(changed_at);
```
### Existing Table: compliance_items (no changes)
The `resolution_date` and `remediation_plan` columns remain as-is. They continue to hold the current/latest value for direct querying by VCL reports.
## API Changes
### PATCH /api/compliance/items/:hostname/metadata
**Request body changes:**
```json
{
"resolution_date": "2026-03-15",
"remediation_plan": "Upgrade firmware to v4.2",
"change_reason": "Vendor pushed back delivery date"
}
```
New optional field: `change_reason` (string, max 500 characters, nullable).
**Behavior changes:**
1. Before updating compliance_items, query the current values:
```sql
SELECT DISTINCT ON (hostname) resolution_date, remediation_plan
FROM compliance_items
WHERE hostname = $1 AND status = 'active'
ORDER BY hostname, id DESC
LIMIT 1
```
2. For each field being updated, compare old vs new. If different, insert a history row:
```sql
INSERT INTO compliance_item_history (hostname, field_name, old_value, new_value, change_reason, changed_by)
VALUES ($1, $2, $3, $4, $5, $6)
```
3. Then proceed with the existing UPDATE as before.
**Response:** unchanged (`{ updated: number }`).
### GET /api/compliance/items/:hostname
**Response changes:**
Add a `history` array to the response:
```json
{
"hostname": "server01.example.com",
"ip_address": "10.0.1.5",
"device_type": "Server",
"team": "STEAM",
"resolution_date": "2026-03-15",
"remediation_plan": "Upgrade firmware to v4.2",
"metrics": [...],
"notes": [...],
"history": [
{
"id": 42,
"field_name": "resolution_date",
"old_value": "2026-02-01",
"new_value": "2026-03-15",
"change_reason": "Vendor pushed back delivery date",
"changed_by": "jsmith",
"changed_at": "2026-01-20T14:30:00.000Z"
},
{
"id": 41,
"field_name": "remediation_plan",
"old_value": null,
"new_value": "Upgrade firmware to v4.2",
"change_reason": null,
"changed_by": "jsmith",
"changed_at": "2026-01-15T09:00:00.000Z"
}
]
}
```
Query:
```sql
SELECT id, field_name, old_value, new_value, change_reason, changed_by, changed_at
FROM compliance_item_history
WHERE hostname = $1
ORDER BY changed_at DESC
LIMIT 10
```
### POST /api/compliance/vcl/bulk-commit
**Behavior changes:**
Within the transaction, before updating each hostname:
1. Query current values for that hostname
2. Compare each field (resolution_date, remediation_plan) against the incoming value
3. Insert history rows for changed fields with `changed_by` set to `req.user.username`
No request/response shape changes. The `change_reason` is not supported for bulk updates (would require per-row reasons which adds complexity without clear value for mass imports).
## Frontend Changes
### ComplianceDetailPanel.js
**New state:**
- `changeReason` (string) — text input value for the reason field
- `history` (array) — populated from the API response
**New UI elements:**
1. **Change Reason input** — a single-line text input placed between the remediation plan save button and the notes section. Cleared after each successful save.
2. **Change History section** — a new `<Section>` component placed between the Remediation Plan section and the Notes section. Displays up to 10 history entries in reverse chronological order.
**History entry display format:**
```
[field_name icon] old_value → new_value
username · 2026-01-20 [reason if present]
```
- Resolution date entries: show dates as YYYY-MM-DD, NULL shown as "—"
- Remediation plan entries: truncate old/new values to 60 characters with "…" suffix; full text shown on hover via title attribute
- Change reason: displayed in muted text below the change line when present
**Save flow update:**
- The `handleSaveMetadata` function passes `change_reason` alongside the field values
- After successful save, clear the `changeReason` input and re-fetch detail (which now includes updated history)
## Error Handling
- If the history INSERT fails, the entire PATCH operation should fail (history is part of the same logical operation). Use a transaction wrapping both the history insert and the compliance_items update.
- If the history SELECT for the GET endpoint fails, return the device detail without history (graceful degradation) and log the error.
- `change_reason` validation: max 500 characters, trimmed. If over 500, return 400 with descriptive error.
## Migration
File: `backend/migrations/add_compliance_item_history.js`
```javascript
const pool = require('../db');
async function run() {
console.log('Starting compliance_item_history migration...');
try {
await pool.query(`
CREATE TABLE IF NOT EXISTS compliance_item_history (
id SERIAL PRIMARY KEY,
hostname TEXT NOT NULL,
field_name TEXT NOT NULL CHECK (field_name IN ('resolution_date', 'remediation_plan')),
old_value TEXT,
new_value TEXT,
change_reason TEXT,
changed_by TEXT NOT NULL,
changed_at TIMESTAMPTZ DEFAULT NOW()
)
`);
console.log('✓ compliance_item_history table created (or already exists)');
await pool.query(`
CREATE INDEX IF NOT EXISTS idx_compliance_history_hostname_field
ON compliance_item_history(hostname, field_name)
`);
console.log('✓ hostname/field_name index created');
await pool.query(`
CREATE INDEX IF NOT EXISTS idx_compliance_history_changed_at
ON compliance_item_history(changed_at)
`);
console.log('✓ changed_at index created');
console.log('Migration complete.');
} catch (err) {
console.error('Migration failed:', err.message);
throw err;
}
}
module.exports = { run };
```
## Reporting Isolation
No changes to VCL reporting queries. The following queries continue to read directly from `compliance_items.resolution_date`:
- Donut chart: `SELECT hostname, MAX(resolution_date) FROM compliance_items WHERE status = 'active' GROUP BY hostname`
- Burndown forecast: `SELECT resolution_date FROM compliance_items WHERE status = 'active' AND resolution_date IS NOT NULL`
- Per-vertical burndown in vclMultiVertical.js
The `compliance_item_history` table is never referenced by any reporting query.
## Performance Considerations
- History inserts are lightweight (single row per field change). Even bulk updates with 500 hostnames produce at most 1000 history rows per commit — well within PostgreSQL's transaction capacity.
- The LIMIT 10 on history retrieval prevents unbounded result sets for devices with many changes.
- Indexes on (hostname, field_name) and (changed_at) ensure fast lookups without full table scans.
- No additional queries are added to the VCL reporting paths.

View File

@@ -0,0 +1,98 @@
# Requirements Document
## Introduction
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.
## Glossary
- **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.
## Requirements
### 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.

View File

@@ -0,0 +1,51 @@
# Tasks — Remediation Plan History
## Task 1: Create migration for compliance_item_history table [Requirement 2, 7]
- [x] Create `backend/migrations/add_compliance_item_history.js` with the schema from the design doc
- [~] Table: `compliance_item_history` (id, hostname, field_name, old_value, new_value, change_reason, changed_by, changed_at)
- [~] Add CHECK constraint on field_name: IN ('resolution_date', 'remediation_plan')
- [~] Add index on (hostname, field_name)
- [~] Add index on (changed_at)
- [~] Register in `migrations/run-all.js`
- [~] Run migration and verify table exists
## Task 2: Modify PATCH /items/:hostname/metadata to record history [Requirement 1, 6]
- [~] In `backend/routes/compliance.js`, locate the PATCH metadata handler
- [~] Accept new optional `change_reason` field (max 500 chars, validated)
- [~] Before updating compliance_items, SELECT current resolution_date and remediation_plan for the hostname
- [~] For each field where old !== new, INSERT into compliance_item_history (hostname, field_name, old_value, new_value, change_reason, changed_by)
- [~] Skip history insert if old === new (no-op changes)
- [~] Wrap history insert + item update in a transaction
- [~] Handle NULL → value and value → NULL transitions
- [~] Add audit log entry with old/new values
- [~] Verify existing response shape is preserved
## Task 3: Extend GET /items/:hostname to return history [Requirement 4]
- [~] In the existing `/items/:hostname` handler, add a query: `SELECT id, field_name, old_value, new_value, change_reason, changed_by, changed_at FROM compliance_item_history WHERE hostname = $1 ORDER BY changed_at DESC LIMIT 10`
- [~] Add `history` array to the response object
- [~] If query fails, return empty array (graceful degradation) and log error
- [~] Verify response includes history alongside existing metrics and notes
## Task 4: Modify bulk update commit to track history [Requirement 6]
- [~] In the bulk update flow (POST /vcl/bulk-commit), before updating each hostname's resolution_date or remediation_plan, query current values
- [~] For each changed field, INSERT into compliance_item_history with changed_by = req.user.username
- [~] Skip if value is unchanged
- [~] No change_reason for bulk updates (set to NULL)
## Task 5: Add change_reason input and history section to ComplianceDetailPanel [Requirement 5]
- [~] Add `changeReason` state and a single-line text input between the Save button and Notes section
- [~] Pass `change_reason` in the PATCH request body when saving
- [~] Clear changeReason after successful save
- [~] Add "Change History" section below the remediation plan area
- [~] Fetch history from the GET /items/:hostname response
- [~] Display entries: field icon, old → new, username, date, reason (if present)
- [~] Resolution dates formatted as YYYY-MM-DD, NULL shown as "—"
- [~] Remediation plan values truncated to 60 chars with title tooltip
- [~] Show "No changes recorded" when history is empty
- [~] Run `npm run build` after changes
## Task 6: Verify VCL burndown is unaffected [Requirement 3]
- [~] Confirm burndown query in vclMultiVertical.js reads from compliance_items.resolution_date only
- [~] Confirm donut query uses MAX(resolution_date) grouped by hostname
- [~] Set a resolution date, change it multiple times, verify device appears once in burndown
- [~] No code changes expected — verification only