19 KiB
Design Document: VCL Multi-Vertical Upload
Overview
This feature adds a multi-file upload flow to the VCL reporting page that accepts per-vertical compliance xlsx files, stores them with vertical-scoped resolution logic, and generates cross-organizational executive reports with drill-down capability by vertical and metric. It is designed as a POC for the compliance team to evaluate before eventual CyberMetrics API integration.
The feature is architecturally separate from the existing single-file AEO compliance upload. It reuses the same Python parser and database schema (with additions), but introduces vertical-scoped commit logic and a new set of API endpoints prefixed with /api/compliance/vcl-multi/.
Architecture
sequenceDiagram
participant U as Compliance Analyst
participant FE as React Frontend
participant BE as Express Backend
participant PY as Python Parser
participant DB as PostgreSQL
Note over U,DB: Multi-File Upload Flow
U->>FE: Drop/select 1–14 xlsx files
FE->>FE: Extract vertical + date from each filename
FE->>BE: POST /api/compliance/vcl-multi/preview (multipart, multiple files)
loop For each file
BE->>PY: parse_compliance_xlsx.py <file>
PY-->>BE: { items, summary, report_date, total }
BE->>DB: Query active items WHERE vertical = X
BE->>BE: Compute scoped diff (new/recurring/resolved within vertical)
end
BE-->>FE: { files: [{ vertical, date, diff, itemCount, tempFile }] }
FE->>FE: Display batch preview table
U->>FE: Confirm batch
FE->>BE: POST /api/compliance/vcl-multi/commit { files: [...] }
loop For each file (single transaction)
BE->>DB: Upsert items for vertical X
BE->>DB: Resolve missing items WHERE vertical = X only
BE->>DB: Update/create snapshot for vertical X
end
BE-->>FE: { committed: [...] }
Note over FE,DB: VCL Multi-Vertical Report Load
FE->>BE: GET /api/compliance/vcl-multi/stats
BE->>DB: Aggregate across all verticals
BE-->>FE: { stats, verticalBreakdown, donut }
FE->>BE: GET /api/compliance/vcl-multi/vertical/:code/metrics
BE->>DB: Per-metric breakdown for vertical
BE-->>FE: { metrics: [...] }
FE->>BE: GET /api/compliance/vcl-multi/vertical/:code/metric/:metricId/devices
BE->>DB: Device list for vertical + metric
BE-->>FE: { devices: [...] }
Data Flow Summary
- Upload — Multiple files uploaded simultaneously. Each file is parsed independently. Vertical identity comes from the filename, not from inside the xlsx.
- Scoped resolution — Each file's commit only resolves items within its own vertical. Other verticals are untouched.
- Aggregation — VCL stats endpoints aggregate across all verticals for the executive view.
- Drill-down — Vertical → Metric → Device hierarchy for investigation.
- Burndown — Computed from
resolution_datevalues on non-compliant devices, bucketed by month per vertical.
Data Model
Schema Changes
New column on compliance_items
ALTER TABLE compliance_items ADD COLUMN IF NOT EXISTS vertical TEXT DEFAULT NULL;
CREATE INDEX IF NOT EXISTS idx_compliance_items_vertical ON compliance_items(vertical);
CREATE INDEX IF NOT EXISTS idx_compliance_items_vertical_status ON compliance_items(vertical, status);
The vertical column stores the organizational vertical code (NTS_AEO, SDIT_CISO, etc.) extracted from the filename at upload time. Existing items (from the old single-file flow) will have vertical = NULL — they continue to work with the existing AEO compliance page unchanged.
New column on compliance_uploads
ALTER TABLE compliance_uploads ADD COLUMN IF NOT EXISTS vertical TEXT DEFAULT NULL;
Tags each upload record with its vertical so we can query upload history per vertical.
New table: vcl_multi_vertical_summary
Stores the parsed Summary sheet data per vertical per upload for metric-level reporting.
CREATE TABLE IF NOT EXISTS vcl_multi_vertical_summary (
id SERIAL PRIMARY KEY,
upload_id INTEGER NOT NULL REFERENCES compliance_uploads(id) ON DELETE CASCADE,
vertical TEXT NOT NULL,
metric_id TEXT NOT NULL,
metric_desc TEXT DEFAULT '',
category TEXT DEFAULT 'Other',
team TEXT DEFAULT '',
priority TEXT DEFAULT '',
non_compliant INTEGER DEFAULT 0,
compliant INTEGER DEFAULT 0,
total INTEGER DEFAULT 0,
compliance_pct NUMERIC(5,2) DEFAULT 0,
target NUMERIC(5,2) DEFAULT 0,
status TEXT DEFAULT '',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_vcl_multi_summary_vertical
ON vcl_multi_vertical_summary(vertical);
CREATE INDEX IF NOT EXISTS idx_vcl_multi_summary_upload
ON vcl_multi_vertical_summary(upload_id);
Updated compliance_snapshots
The existing snapshots table already has a vertical column. Multi-vertical uploads will create snapshots keyed on the vertical code (NTS_AEO, SDIT_CISO) rather than the team name (STEAM, ACCESS-ENG). An additional _ALL aggregate snapshot is created for the trend chart.
Entity Relationships
compliance_uploads (1) ──── (N) compliance_items
│ │
│ vertical │ vertical
│ │
└──── (N) vcl_multi_vertical_summary
│
compliance_snapshots ─────────────────┘ (keyed on vertical + month)
Vertical Identification Logic
/**
* Extracts vertical code and report date from a filename.
* Pattern: <VERTICAL>_YYYY_MM_DD.xlsx
* Examples:
* NTS_AEO_2026_05_11.xlsx → { vertical: 'NTS_AEO', date: '2026-05-11' }
* SDIT_CISO_2026_05_11.xlsx → { vertical: 'SDIT_CISO', date: '2026-05-11' }
* SR_2026_05_11.xlsx → { vertical: 'SR', date: '2026-05-11' }
* AllOthers_2026_05_11.xlsx → { vertical: 'AllOthers', date: '2026-05-11' }
*/
function parseVerticalFilename(filename) {
const stem = filename.replace(/\.xlsx$/i, '');
const match = stem.match(/^(.+?)_(\d{4})_(\d{2})_(\d{2})$/);
if (!match) return null;
return {
vertical: match[1],
date: `${match[2]}-${match[3]}-${match[4]}`,
};
}
API Endpoints
Upload Flow
POST /api/compliance/vcl-multi/preview
Accepts multiple xlsx files via multipart form data. Parses each, computes per-vertical scoped diffs.
- Auth:
requireAuth(),requireGroup('Admin', 'Standard_User') - Body: multipart/form-data with field
files(array of xlsx files) - Response:
{
"files": [
{
"filename": "NTS_AEO_2026_05_11.xlsx",
"vertical": "NTS_AEO",
"report_date": "2026-05-11",
"total_items": 342,
"diff": { "new_count": 12, "recurring_count": 320, "resolved_count": 8 },
"summary_entries": 24,
"tempFile": "/path/to/temp.json"
}
],
"unrecognized": ["weird_file.xlsx"]
}
POST /api/compliance/vcl-multi/commit
Commits all previewed files in a single transaction.
- Auth:
requireAuth(),requireGroup('Admin', 'Standard_User') - Body:
{ files: [{ tempFile, vertical, report_date, filename }] } - Response:
{
"committed": [
{ "vertical": "NTS_AEO", "upload_id": 45, "new_count": 12, "recurring_count": 320, "resolved_count": 8 }
],
"total_new": 85,
"total_resolved": 42
}
Reporting
GET /api/compliance/vcl-multi/stats
Aggregated cross-vertical executive summary.
- Response:
{
"stats": {
"total_devices": 4200,
"compliant": 3800,
"non_compliant": 400,
"compliance_pct": 90,
"target_pct": 95
},
"donut": {
"blocked": { "count": 120, "pct": 30 },
"in_progress": { "count": 280, "pct": 70 }
},
"vertical_breakdown": [
{
"vertical": "NTS_AEO",
"total_devices": 800,
"compliant": 720,
"non_compliant": 80,
"compliance_pct": 90,
"blockers": 25,
"forecast_burndown": { "2026-06": 20, "2026-07": 35, "2026-08": 15 },
"last_upload": "2026-05-11"
}
],
"last_upload_date": "2026-05-11"
}
GET /api/compliance/vcl-multi/trend
Monthly trend data for the overview chart.
- Response:
{
"months": [
{
"month": "2026-03",
"compliance_pct": 85,
"compliant": 3400,
"non_compliant": 600,
"forecast_pct": null,
"target_pct": 95
}
]
}
GET /api/compliance/vcl-multi/vertical/:code/metrics
Per-metric breakdown for a specific vertical.
- Response:
{
"vertical": "NTS_AEO",
"metrics": [
{
"metric_id": "5.2.4",
"metric_desc": "MFA enforcement on privileged accounts",
"category": "Access & MFA",
"non_compliant": 15,
"compliant": 785,
"total": 800,
"compliance_pct": 98.1,
"target": 100
}
],
"categories": [
{ "category": "Access & MFA", "non_compliant": 45, "compliance_pct": 94.4 }
]
}
GET /api/compliance/vcl-multi/vertical/:code/metric/:metricId/devices
Device list for a specific vertical + metric combination.
- Response:
{
"devices": [
{
"hostname": "srv-nts-001",
"ip_address": "10.1.2.3",
"device_type": "Router",
"team": "STEAM",
"seen_count": 4,
"first_seen": "2026-03-15",
"last_seen": "2026-05-11",
"resolution_date": "2026-07-01",
"remediation_plan": "Scheduled for next maintenance window"
}
]
}
GET /api/compliance/vcl-multi/vertical/:code/burndown
Burndown forecast for a specific vertical.
- Response:
{
"vertical": "NTS_AEO",
"total_non_compliant": 80,
"blockers": 25,
"with_dates": 55,
"monthly_forecast": { "2026-06": 20, "2026-07": 35 },
"projected_clear_date": "2026-08"
}
Frontend Components
New Page: VCLMultiVerticalPage.js
Top-level page accessible from the nav drawer. Contains:
| Component | Purpose |
|---|---|
MultiVerticalUploadModal |
Multi-file drag-drop, filename parsing, batch preview, commit |
VCLMultiStatsBar |
Aggregated stats across all verticals |
VCLMultiVerticalTable |
Breakdown table with one row per vertical, clickable for drill-down |
VCLMultiTrendChart |
Monthly compliance trend with forecast line |
VCLMultiDonutChart |
Blocked vs In-Progress donut |
VerticalDetailView |
Per-metric breakdown when a vertical is selected |
MetricDeviceList |
Device list when a metric is selected within a vertical |
VerticalBurndownChart |
Per-vertical burndown projection |
Navigation
- New entry in
NavDrawer.js: "VCL Multi-Vertical" (or "CCP Metrics") - Separate from existing "Compliance" and "VCL Report" entries
- Icon:
BarChart3orBuilding2from lucide-react
Drill-down UX Flow
VCL Multi-Vertical Overview
├── Stats Bar (aggregated)
├── Trend Chart (aggregated)
├── Donut Chart (aggregated)
└── Vertical Breakdown Table
├── NTS_AEO (90%) → click
│ ├── Metric Breakdown
│ │ ├── 5.2.4 — Access & MFA (98.1%) → click
│ │ │ └── Device List (15 devices)
│ │ ├── 1.1.1 — Logging & Monitoring (85%) → click
│ │ │ └── Device List (120 devices)
│ │ └── ...
│ └── Burndown Chart (vertical-specific)
├── SDIT_CISO (92%) → click
│ └── ...
└── ...
Scoped Resolution Logic
This is the core architectural change from the existing upload flow.
Current behavior (single-file)
// Resolves ALL active items not in the upload — global scope
for (const [key, row] of Object.entries(activeMap)) {
if (!newKeys.has(key)) {
await client.query(`UPDATE compliance_items SET status = 'resolved' WHERE id = $1`, [row.id]);
}
}
New behavior (multi-vertical)
// Resolves only items within the same vertical — scoped
const { rows: activeRows } = await client.query(
`SELECT id, hostname, metric_id, seen_count FROM compliance_items
WHERE status = 'active' AND vertical = $1`,
[vertical]
);
for (const [key, row] of Object.entries(activeMap)) {
if (!newKeys.has(key)) {
await client.query(`UPDATE compliance_items SET status = 'resolved', resolved_upload_id = $1 WHERE id = $2`, [uploadId, row.id]);
}
}
The only difference is the AND vertical = $1 filter on the active items query. This ensures uploading NTS_AEO data never touches SDIT_CISO items.
Burndown Forecast Computation
Per-vertical burndown
For each vertical, the burndown is computed from resolution_date values on active non-compliant items:
function computeVerticalBurndown(items) {
const total = items.length;
const withDates = items.filter(i => i.resolution_date != null);
const blockers = items.filter(i => i.resolution_date == null);
// Bucket by month
const monthly = {};
for (const item of withDates) {
const month = item.resolution_date.slice(0, 7); // YYYY-MM
monthly[month] = (monthly[month] || 0) + 1;
}
// Cumulative projection
let remaining = total;
const projection = {};
for (const month of Object.keys(monthly).sort()) {
remaining -= monthly[month];
projection[month] = { remediated: monthly[month], remaining };
}
return { total, blockers: blockers.length, with_dates: withDates.length, monthly, projection };
}
Aggregated trend forecast
The trend chart forecast uses linear regression on the last 3+ monthly snapshots to project forward. This reuses the same approach as the existing VCL trend endpoint.
Migration Script
// backend/migrations/add_vcl_multi_vertical.js
const pool = require('../db');
async function run() {
console.log('Starting VCL multi-vertical migration...');
try {
// Add vertical column to compliance_items
await pool.query(`ALTER TABLE compliance_items ADD COLUMN IF NOT EXISTS vertical TEXT DEFAULT NULL`);
console.log('✓ vertical column added to compliance_items');
await pool.query(`CREATE INDEX IF NOT EXISTS idx_compliance_items_vertical ON compliance_items(vertical)`);
console.log('✓ idx_compliance_items_vertical index created');
await pool.query(`CREATE INDEX IF NOT EXISTS idx_compliance_items_vertical_status ON compliance_items(vertical, status)`);
console.log('✓ idx_compliance_items_vertical_status index created');
// Add vertical column to compliance_uploads
await pool.query(`ALTER TABLE compliance_uploads ADD COLUMN IF NOT EXISTS vertical TEXT DEFAULT NULL`);
console.log('✓ vertical column added to compliance_uploads');
// Create summary table for per-vertical metric data
await pool.query(`
CREATE TABLE IF NOT EXISTS vcl_multi_vertical_summary (
id SERIAL PRIMARY KEY,
upload_id INTEGER NOT NULL REFERENCES compliance_uploads(id) ON DELETE CASCADE,
vertical TEXT NOT NULL,
metric_id TEXT NOT NULL,
metric_desc TEXT DEFAULT '',
category TEXT DEFAULT 'Other',
team TEXT DEFAULT '',
priority TEXT DEFAULT '',
non_compliant INTEGER DEFAULT 0,
compliant INTEGER DEFAULT 0,
total INTEGER DEFAULT 0,
compliance_pct NUMERIC(5,2) DEFAULT 0,
target NUMERIC(5,2) DEFAULT 0,
status TEXT DEFAULT '',
created_at TIMESTAMPTZ DEFAULT NOW()
)
`);
console.log('✓ vcl_multi_vertical_summary table created');
await pool.query(`CREATE INDEX IF NOT EXISTS idx_vcl_multi_summary_vertical ON vcl_multi_vertical_summary(vertical)`);
console.log('✓ idx_vcl_multi_summary_vertical index created');
await pool.query(`CREATE INDEX IF NOT EXISTS idx_vcl_multi_summary_upload ON vcl_multi_vertical_summary(upload_id)`);
console.log('✓ idx_vcl_multi_summary_upload index created');
} catch (err) {
console.error('Migration error:', err.message);
process.exit(1);
}
console.log('Migration complete.');
process.exit(0);
}
run();
Correctness Properties
Property 1: Vertical-Scoped Resolution Isolation
For any set of active compliance items across N verticals, committing an upload for vertical X must only resolve items where vertical = X. The count of active items for all other verticals must remain unchanged before and after the commit.
Property 2: Filename Parsing Completeness
For any filename matching the pattern <VERTICAL>_YYYY_MM_DD.xlsx where VERTICAL contains only alphanumeric characters and underscores, parseVerticalFilename must return a non-null result with the correct vertical code and ISO date string.
Property 3: Aggregated Stats Consistency
For any set of per-vertical stats, the aggregated total_devices must equal the sum of all vertical total_devices, compliant must equal the sum of all vertical compliant, and compliance_pct must equal Math.round((sum_compliant / sum_total) * 100).
Property 4: Burndown Forecast Conservation
For any set of non-compliant items with resolution dates, the sum of all monthly burndown bucket counts must equal the count of items with non-null resolution dates. No item is double-counted or lost.
Property 5: Idempotent Re-upload
For any vertical, uploading the same file twice on the same day must produce the same final state as uploading it once. Specifically: same active item set, same seen_counts, same resolved set.
Error Handling
| Condition | Behavior |
|---|---|
| Filename doesn't match pattern | File flagged as "unrecognized" in preview; user can assign vertical manually |
| Duplicate vertical in batch | Reject — only one file per vertical per batch |
| Parser failure on one file | That file is marked as errored; other files in batch can still proceed |
| Transaction failure during commit | Full rollback of entire batch — no partial commits |
| File exceeds 10MB | Rejected by multer before parsing |
| No items parsed from file | Warning in preview; user can still commit (creates upload record with 0 items) |
Deployment Considerations
- The feature is self-contained behind
/api/compliance/vcl-multi/endpoints - Can be deployed on a separate instance with its own database
- No changes to existing AEO compliance upload flow
- Feature flag not needed — the nav entry and endpoints simply exist or don't
- Environment variable
VCL_TARGET_PCT(default 95) applies to multi-vertical reporting as well