Update .kiro: remove SQLite hooks, add PostgreSQL migration hook, add workflow steering, sync specs

This commit is contained in:
Jordan Ramos
2026-05-12 14:45:58 -06:00
parent 3ee8487286
commit 1bb8ec1658
35 changed files with 4645 additions and 48 deletions

View File

@@ -0,0 +1 @@
{"specId": "f0fd28b9-12b8-48bb-b0b2-3a0e5872a3e4", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -0,0 +1,222 @@
# Design Document: FP Submissions Cleanup
## Overview
This feature addresses three usability improvements to the FP Submissions section in the Queue Panel:
1. **Auto-clear approved submissions** — filter out approved FP submissions from the visible list so finalized items do not clutter the panel.
2. **Dismiss rejected submissions** — add an X button on rejected submissions allowing users to permanently hide them from view, persisted in the database.
3. **Collapsible Submissions section** — add a collapse/expand toggle on the SUBMISSIONS header, with state persisted in localStorage.
The design builds on the existing `ivantiFpWorkflow.js` route, the `QueuePanel` component's submissions section in `ReportingPage.js`, and the `ivanti_fp_submissions` table. Changes are minimal: one new database column (`dismissed_at`), one new API endpoint (`PATCH /submissions/:id/dismiss`), and frontend filtering/UI logic.
## Architecture
```mermaid
sequenceDiagram
participant U as User
participant FE as React Frontend
participant BE as Express Backend
participant DB as PostgreSQL
Note over FE,DB: Auto-Clear Approved (frontend filter only)
FE->>BE: GET /api/ivanti/fp-workflow/submissions
BE->>DB: SELECT * FROM ivanti_fp_submissions WHERE user_id = $1
DB-->>BE: All submissions (including approved)
BE-->>FE: JSON array of submissions
FE->>FE: Filter out approved + dismissed submissions for display
Note over U,DB: Dismiss Rejected Submission
U->>FE: Click X button on rejected submission
FE->>BE: PATCH /api/ivanti/fp-workflow/submissions/:id/dismiss
BE->>DB: UPDATE ivanti_fp_submissions SET dismissed_at = NOW() WHERE id = $1
BE->>DB: INSERT audit_logs (action: ivanti_fp_submission_dismissed)
BE-->>FE: 200 OK
FE->>FE: Remove submission from visible list
Note over U,FE: Collapse/Expand Toggle
U->>FE: Click collapse toggle on SUBMISSIONS header
FE->>FE: Toggle collapsed state
FE->>FE: Persist to localStorage (key: steam_submissions_collapsed)
```
## Components and Interfaces
### Backend
#### Extended Route: `backend/routes/ivantiFpWorkflow.js`
One new endpoint added to the existing `createIvantiFpWorkflowRouter`.
**Endpoint: `PATCH /api/ivanti/fp-workflow/submissions/:id/dismiss`**
Marks a rejected submission as dismissed.
- Auth: `requireAuth()`, `requireGroup('Admin', 'Standard_User')`
- Ownership: verified via `user_id` match (returns 403 otherwise)
- Lifecycle guard: only submissions with `lifecycle_status === 'rejected'` can be dismissed (returns 400 otherwise)
- On success: sets `dismissed_at = NOW()` on the submission record, logs audit entry
- Response: `{ success: true }`
Request: no body required.
Response:
```json
{ "success": true }
```
Error responses:
- 404: `{ "error": "Submission not found." }`
- 403: `{ "error": "You can only dismiss your own submissions." }`
- 400: `{ "error": "Only rejected submissions can be dismissed." }`
#### Modified Endpoint: `GET /api/ivanti/fp-workflow/submissions`
No changes to the backend query — it continues to return all submissions for the user. The frontend handles filtering. This preserves the ability to open approved submissions from workflow badges in read-only mode.
#### Pure Helper Function (exported for testing)
- `filterVisibleSubmissions(submissions)` — new, filters an array of submission objects to exclude those with `lifecycle_status === 'approved'` or `dismissed_at !== null`. Returns the filtered array.
### Frontend
#### QueuePanel Submissions Section Modifications
**Filtering logic:**
The `fpSubmissions` array passed to `QueuePanel` is filtered before rendering:
- Exclude submissions where `lifecycle_status === 'approved'`
- Exclude submissions where `dismissed_at` is not null
This filtering is applied in the `useMemo` that already enriches `fpSubmissionsRaw` with Ivanti workflow state.
**Dismiss button (X icon):**
For submissions with `lifecycle_status === 'rejected'`, render an X button (lucide `X` icon, 12px) on the right side of the submission row. On click:
1. Call `PATCH /api/ivanti/fp-workflow/submissions/:id/dismiss`
2. On success, re-fetch submissions (or optimistically remove from local state)
The X button uses `e.stopPropagation()` to prevent triggering the row's click-to-edit handler.
**Collapse/expand toggle:**
The SUBMISSIONS header row gets a clickable chevron icon:
- Expanded state: `ChevronDown` icon (already imported from lucide-react)
- Collapsed state: `ChevronUp` icon
State management:
- `const [submissionsCollapsed, setSubmissionsCollapsed] = useState(() => localStorage.getItem('steam_submissions_collapsed') === 'true')`
- On toggle: flip state and persist to localStorage
- When collapsed: hide the submission list, keep the header visible with the count badge
- Default: expanded (when localStorage has no stored value)
localStorage key: `steam_submissions_collapsed`
#### FpEditModal — No Changes
The `FpEditModal` already handles approved submissions in read-only mode. Since the modal can be opened from workflow badges in the Reporting Table (which bypass the Queue Panel filter), no changes are needed.
## Data Models
### Schema Change to `ivanti_fp_submissions`
One new column:
```sql
ALTER TABLE ivanti_fp_submissions ADD COLUMN dismissed_at TIMESTAMPTZ DEFAULT NULL;
```
- `NULL` means the submission is not dismissed (visible, subject to other filters)
- A timestamp value means the submission was dismissed at that time (hidden from the Submissions section)
### Migration Script: `backend/migrations/add_fp_submissions_dismissed.js`
Applies the schema change idempotently. Uses the same pattern as `add_fp_submission_editing.js`:
- PostgreSQL: `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`
- Wrapped in try/catch for safety
```javascript
const pool = require('../db');
async function run() {
console.log('Starting FP submissions dismissed migration...');
try {
await pool.query(`ALTER TABLE ivanti_fp_submissions ADD COLUMN IF NOT EXISTS dismissed_at TIMESTAMPTZ DEFAULT NULL`);
console.log('✓ dismissed_at column added (or already exists)');
} catch (err) {
console.error('Error adding dismissed_at column:', err.message);
process.exit(1);
}
console.log('Migration complete.');
process.exit(0);
}
run();
```
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property 1: Submission Visibility Filter
*For any* array of FP submission objects with arbitrary `lifecycle_status` values (from the set {submitted, approved, rejected, rework, resubmitted}) and arbitrary `dismissed_at` values (null or a timestamp string), `filterVisibleSubmissions(submissions)` should return only submissions where `lifecycle_status` is NOT "approved" AND `dismissed_at` is null. Additionally, every submission in the input that satisfies both conditions must appear in the output, and the output length must be less than or equal to the input length.
**Validates: Requirements 1.1, 2.2, 2.3**
### Property 2: Dismiss Button Visibility Predicate
*For any* FP submission object with a `lifecycle_status` value drawn from {submitted, approved, rejected, rework, resubmitted} and a `dismissed_at` value (null or timestamp), the dismiss button should be rendered if and only if `lifecycle_status === 'rejected'` AND `dismissed_at` is null.
**Validates: Requirements 2.1**
## Error Handling
### Dismiss Endpoint Errors
| Condition | HTTP Status | Response | Behavior |
|-----------|-------------|----------|----------|
| Submission not found | 404 | `{ "error": "Submission not found." }` | No state change |
| User does not own submission | 403 | `{ "error": "You can only dismiss your own submissions." }` | No state change |
| Submission not in "rejected" status | 400 | `{ "error": "Only rejected submissions can be dismissed." }` | No state change |
| Database error | 500 | `{ "error": "Internal server error." }` | Log error, no state change |
### Frontend Error Handling
- If the dismiss API call fails, show a brief error toast/message and leave the submission in the list
- If `localStorage` is unavailable (private browsing), the collapse state defaults to expanded and is not persisted (graceful degradation)
## Testing Strategy
### Property-Based Testing
Use `fast-check` as the property-based testing library. Each correctness property maps to a single property-based test with a minimum of 100 iterations.
Property tests focus on the pure filtering function:
- `filterVisibleSubmissions(submissions)` — Property 1
- Dismiss button visibility predicate — Property 2
Tag format: **Feature: fp-submissions-cleanup, Property {number}: {title}**
Test file: `backend/__tests__/fp-submissions-cleanup.property.test.js`
### Unit Testing
Unit tests cover specific examples, edge cases, and integration points:
- **Filter with all approved**: verify empty result when all submissions are approved
- **Filter with all dismissed**: verify empty result when all submissions are dismissed
- **Filter with mixed statuses**: verify correct subset returned
- **Dismiss endpoint — happy path**: verify `dismissed_at` is set and audit log created
- **Dismiss endpoint — wrong status**: verify 400 when attempting to dismiss a non-rejected submission
- **Dismiss endpoint — ownership check**: verify 403 when non-owner attempts dismiss
- **Collapse state persistence**: verify localStorage read/write with the correct key
- **Collapse default**: verify expanded state when localStorage has no value
Test file: `backend/__tests__/fp-submissions-cleanup.test.js`
### Integration Testing
- Dismiss a rejected submission via API, re-fetch submissions, verify it's excluded from the response when filtered
- Verify audit log entry is created with correct action and entity_id
- Verify approved submissions are still accessible via direct ID lookup (for the edit modal)

View File

@@ -0,0 +1,51 @@
# Requirements Document
## Introduction
This feature addresses three usability issues in the FP Submissions section of the Queue Panel. Approved submissions currently persist indefinitely in the list, rejected submissions cannot be dismissed, and the entire Submissions section cannot be collapsed. These enhancements improve the signal-to-noise ratio of the Queue Panel by giving users control over which submissions remain visible and allowing the section to be hidden when not needed.
## Glossary
- **Dashboard**: The STEAM Security Dashboard application
- **Queue_Panel**: The slide-out panel in the Reporting Page that displays the user's Ivanti todo queue items, FP workflow submissions, and action buttons
- **Submissions_Section**: The "SUBMISSIONS" area within the Queue_Panel that lists the user's FP workflow submissions with their lifecycle statuses
- **FP_Submission**: A local database record in the `ivanti_fp_submissions` table tracking a False Positive workflow submission, including its lifecycle status
- **Lifecycle_Status**: The current state of an FP submission: submitted, approved, rejected, rework, or resubmitted
- **Dismissed_Submission**: An FP_Submission that the user has explicitly removed from the Submissions_Section view without deleting the underlying database record
## Requirements
### Requirement 1: Auto-Clear Approved Submissions
**User Story:** As an editor or admin, I want approved FP submissions to be automatically removed from the Submissions list, so that finalized items do not clutter the Queue Panel indefinitely.
#### Acceptance Criteria
1. WHEN the Dashboard fetches FP_Submissions for display in the Submissions_Section, THE Dashboard SHALL exclude submissions with Lifecycle_Status "approved" from the visible list
2. WHEN an FP_Submission transitions to Lifecycle_Status "approved" while the Queue_Panel is open, THE Dashboard SHALL remove the submission from the Submissions_Section without requiring a page refresh
3. THE Dashboard SHALL retain approved FP_Submission records in the database for audit and history purposes
4. WHEN the user opens the FpEditModal from a Reporting Table workflow badge for an approved submission, THE Dashboard SHALL still display the submission data in read-only mode regardless of its visibility in the Submissions_Section
### Requirement 2: Dismiss Rejected Submissions
**User Story:** As an editor or admin, I want to dismiss rejected FP submissions from the Submissions list, so that I can remove items I no longer intend to rework.
#### Acceptance Criteria
1. WHEN an FP_Submission has Lifecycle_Status "rejected", THE Dashboard SHALL display a dismiss button (X icon) on the submission row in the Submissions_Section
2. WHEN the user clicks the dismiss button on a rejected submission, THE Dashboard SHALL mark the FP_Submission as dismissed and remove it from the Submissions_Section
3. THE Dashboard SHALL persist the dismissed state so that dismissed submissions remain hidden across page reloads and session changes
4. THE Dashboard SHALL retain dismissed FP_Submission records in the database for audit and history purposes
5. IF the user dismisses a rejected submission, THEN THE Dashboard SHALL log an audit entry with action "ivanti_fp_submission_dismissed" and the submission's workflow batch ID as entity ID
### Requirement 3: Collapsible Submissions Section
**User Story:** As a user, I want to collapse the Submissions section in the Queue Panel, so that I can hide it when I only need to work with queue items.
#### Acceptance Criteria
1. THE Dashboard SHALL display a collapse/expand toggle control on the Submissions_Section header row
2. WHEN the user clicks the collapse toggle, THE Dashboard SHALL hide the list of FP_Submissions while keeping the section header visible with the submission count
3. WHEN the user clicks the expand toggle, THE Dashboard SHALL reveal the full list of FP_Submissions
4. THE Dashboard SHALL persist the collapsed/expanded state in browser local storage so that the preference survives page reloads
5. THE Dashboard SHALL default the Submissions_Section to the expanded state when no stored preference exists

View File

@@ -0,0 +1,50 @@
# Tasks
## Task 1: Database Migration — Add `dismissed_at` Column
- [x] 1.1 Create migration file `backend/migrations/add_fp_submissions_dismissed.js` that adds a `dismissed_at TIMESTAMPTZ DEFAULT NULL` column to the `ivanti_fp_submissions` table using `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`
- [x] 1.2 Run the migration and verify the column exists in the database schema
## Task 2: Backend — Dismiss Endpoint
- [x] 2.1 Add `PATCH /submissions/:id/dismiss` endpoint to `backend/routes/ivantiFpWorkflow.js` with `requireAuth()` and `requireGroup('Admin', 'Standard_User')` middleware
- [x] 2.2 Implement ownership verification (user_id match, return 403 if mismatch)
- [x] 2.3 Implement lifecycle guard (only `lifecycle_status === 'rejected'` can be dismissed, return 400 otherwise)
- [x] 2.4 Set `dismissed_at = NOW()` on the submission record on success
- [x] 2.5 Log audit entry with action `ivanti_fp_submission_dismissed`, entity_type `ivanti_workflow`, and the workflow batch ID as entity_id
## Task 3: Backend — Pure Filter Function
- [x] 3.1 Create and export `filterVisibleSubmissions(submissions)` function in `backend/routes/ivantiFpWorkflow.js` that excludes submissions with `lifecycle_status === 'approved'` or `dismissed_at !== null`
- [x] 3.2 Create and export `shouldShowDismissButton(submission)` predicate that returns true only when `lifecycle_status === 'rejected'` and `dismissed_at` is null
## Task 4: Frontend — Filter Approved and Dismissed Submissions
- [x] 4.1 Modify the `fpSubmissions` useMemo in `ReportingPage.js` to filter out submissions where `lifecycle_status === 'approved'` or `dismissed_at` is not null before passing to the QueuePanel
- [x] 4.2 Verify that the FpEditModal can still be opened for approved submissions from Reporting Table workflow badges (no filtering on that path)
## Task 5: Frontend — Dismiss Button on Rejected Submissions
- [x] 5.1 Add an X button (lucide `X` icon, 12px) to the right side of submission rows where `lifecycle_status === 'rejected'` in the QueuePanel submissions section
- [x] 5.2 Implement click handler that calls `PATCH /api/ivanti/fp-workflow/submissions/:id/dismiss` and removes the submission from the visible list on success
- [x] 5.3 Use `e.stopPropagation()` on the X button to prevent triggering the row's click-to-edit handler
- [x] 5.4 Show error feedback if the dismiss API call fails
## Task 6: Frontend — Collapsible Submissions Section
- [x] 6.1 Add `submissionsCollapsed` state initialized from `localStorage.getItem('steam_submissions_collapsed')`, defaulting to `false` (expanded)
- [x] 6.2 Add a clickable chevron icon (ChevronDown when expanded, ChevronUp when collapsed) to the SUBMISSIONS header row
- [x] 6.3 Conditionally render the submissions list based on collapsed state — when collapsed, hide the list but keep the header with the count badge visible
- [x] 6.4 Persist collapsed/expanded state to localStorage on toggle using key `steam_submissions_collapsed`
## Task 7: Property-Based Tests
- [x] 7.1 Create `backend/__tests__/fp-submissions-cleanup.property.test.js` with fast-check
- [x] 7.2 Implement Property 1 test: for any array of submissions with random lifecycle_status and dismissed_at values, `filterVisibleSubmissions` returns only non-approved and non-dismissed submissions (min 100 iterations)
- [x] 7.3 Implement Property 2 test: for any submission with random lifecycle_status and dismissed_at, `shouldShowDismissButton` returns true iff status is 'rejected' and dismissed_at is null (min 100 iterations)
## Task 8: Unit and Integration Tests
- [x] 8.1 Write unit tests for the dismiss endpoint (happy path, wrong status, ownership check, not found)
- [x] 8.2 Write unit tests for filter edge cases (all approved, all dismissed, mixed, empty array)
- [x] 8.3 Write integration test verifying dismissed submissions remain in the database but are excluded from the filtered list