Auto-sync .kiro/ from master (post-checkout hook)
This commit is contained in:
1
.kiro/specs/remediation-plan-history/.config.kiro
Normal file
1
.kiro/specs/remediation-plan-history/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"specId": "731f6cf9-7d25-41ea-a550-745d6a917b3f", "workflowType": "requirements-first", "specType": "feature"}
|
||||
279
.kiro/specs/remediation-plan-history/design.md
Normal file
279
.kiro/specs/remediation-plan-history/design.md
Normal 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.
|
||||
98
.kiro/specs/remediation-plan-history/requirements.md
Normal file
98
.kiro/specs/remediation-plan-history/requirements.md
Normal 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.
|
||||
51
.kiro/specs/remediation-plan-history/tasks.md
Normal file
51
.kiro/specs/remediation-plan-history/tasks.md
Normal 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
|
||||
Reference in New Issue
Block a user