Files
cve-dashboard/.kiro/specs/fp-submissions-cleanup/design.md

10 KiB

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

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:

{ "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:

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