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,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)