feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
// Compliance Routes — AEO metric tracking
// Handles xlsx upload/parse, non-compliant item history, and notes.
const express = require ( 'express' ) ;
const path = require ( 'path' ) ;
const fs = require ( 'fs' ) ;
2026-04-16 14:28:44 -06:00
const crypto = require ( 'crypto' ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
const { spawn } = require ( 'child_process' ) ;
2026-05-06 11:44:17 -06:00
const pool = require ( '../db' ) ;
const { requireAuth , requireGroup } = require ( '../middleware/auth' ) ;
2026-04-20 20:12:12 +00:00
const { loadConfig , compareSchemaToDrift , reconcileConfig } = require ( '../helpers/driftChecker' ) ;
const logAudit = require ( '../helpers/auditLog' ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
2026-04-20 20:12:12 +00:00
const PARSER _SCRIPT = path . join ( _ _dirname , '../scripts/parse_compliance_xlsx.py' ) ;
const SCHEMA _SCRIPT = path . join ( _ _dirname , '../scripts/extract_xlsx_schema.py' ) ;
const CONFIG _PATH = path . join ( _ _dirname , '..' , 'scripts' , 'compliance_config.json' ) ;
const PYTHON _BIN = process . env . PYTHON _BIN || 'python3' ;
const TEMP _DIR = path . join ( process . cwd ( ) , 'uploads' , 'temp' ) ;
const ALLOWED _TEAMS = new Set ( [ 'STEAM' , 'ACCESS-ENG' , 'ACCESS-OPS' , 'INTELDEV' ] ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
// ---------------------------------------------------------------------------
// Run Python parser, return parsed object
// ---------------------------------------------------------------------------
function parseXlsx ( filePath ) {
return new Promise ( ( resolve , reject ) => {
2026-04-01 12:47:50 -06:00
const py = spawn ( PYTHON _BIN , [ PARSER _SCRIPT , filePath ] ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
let out = '' ;
let err = '' ;
py . stdout . on ( 'data' , d => { out += d ; } ) ;
py . stderr . on ( 'data' , d => { err += d ; } ) ;
py . on ( 'close' , code => {
if ( code !== 0 ) return reject ( new Error ( err || ` Parser exited with code ${ code } ` ) ) ;
try { resolve ( JSON . parse ( out ) ) ; }
catch ( e ) { reject ( new Error ( 'Parser returned invalid JSON' ) ) ; }
} ) ;
py . on ( 'error' , reject ) ;
} ) ;
}
2026-04-20 20:12:12 +00:00
function extractXlsxSchema ( filePath ) {
return new Promise ( ( resolve , reject ) => {
const py = spawn ( PYTHON _BIN , [ SCHEMA _SCRIPT , filePath ] ) ;
let out = '' ;
let err = '' ;
py . stdout . on ( 'data' , d => { out += d ; } ) ;
py . stderr . on ( 'data' , d => { err += d ; } ) ;
py . on ( 'close' , code => {
if ( code !== 0 ) return reject ( new Error ( err || ` Schema extractor exited with code ${ code } ` ) ) ;
try { resolve ( JSON . parse ( out ) ) ; }
catch ( e ) { reject ( new Error ( 'Schema extractor returned invalid JSON' ) ) ; }
} ) ;
py . on ( 'error' , reject ) ;
} ) ;
}
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
function isSafeTempPath ( filePath ) {
const resolved = path . resolve ( filePath ) ;
return resolved . startsWith ( TEMP _DIR + path . sep ) && path . extname ( resolved ) === '.json' ;
}
// ---------------------------------------------------------------------------
// Compute diff: new / recurring / resolved
// ---------------------------------------------------------------------------
2026-05-06 11:44:17 -06:00
async function computeDiff ( incomingItems ) {
const { rows : activeRows } = await pool . query (
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
` SELECT hostname, metric_id FROM compliance_items WHERE status = 'active' `
) ;
const activeKeys = new Set ( activeRows . map ( r => ` ${ r . hostname } ||| ${ r . metric _id } ` ) ) ;
const newKeys = new Set ( incomingItems . map ( i => ` ${ i . hostname } ||| ${ i . metric _id } ` ) ) ;
let newCount = 0 , recurringCount = 0 , resolvedCount = 0 ;
for ( const k of newKeys ) { if ( activeKeys . has ( k ) ) recurringCount ++ ; else newCount ++ ; }
for ( const k of activeKeys ) { if ( ! newKeys . has ( k ) ) resolvedCount ++ ; }
return { newCount , recurringCount , resolvedCount } ;
}
// ---------------------------------------------------------------------------
// Write a parsed upload to the DB (within a transaction)
// ---------------------------------------------------------------------------
2026-05-06 11:44:17 -06:00
async function persistUpload ( { items , summary , reportDate , filename , userId } ) {
const { rows : activeRows } = await pool . query (
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
` SELECT id, hostname, metric_id, seen_count, first_seen_upload_id FROM compliance_items WHERE status = 'active' `
) ;
const activeMap = { } ;
activeRows . forEach ( r => { activeMap [ ` ${ r . hostname } ||| ${ r . metric _id } ` ] = r ; } ) ;
const newKeys = new Set ( items . map ( i => ` ${ i . hostname } ||| ${ i . metric _id } ` ) ) ;
2026-05-06 11:44:17 -06:00
const client = await pool . connect ( ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
try {
2026-05-06 11:44:17 -06:00
await client . query ( 'BEGIN' ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
// 1. Insert the upload record
2026-05-06 11:44:17 -06:00
const uploadResult = await client . query (
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
` INSERT INTO compliance_uploads (filename, report_date, uploaded_by, uploaded_at, summary_json)
2026-05-06 11:44:17 -06:00
VALUES ( $1 , $2 , $3 , NOW ( ) , $4 )
RETURNING id ` ,
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
[ filename , reportDate || null , userId || null , JSON . stringify ( summary ) ]
) ;
2026-05-06 11:44:17 -06:00
const uploadId = uploadResult . rows [ 0 ] . id ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
let newCount = 0 , recurringCount = 0 , resolvedCount = 0 ;
// 2. Upsert each incoming non-compliant item
for ( const item of items ) {
const key = ` ${ item . hostname } ||| ${ item . metric _id } ` ;
const existing = activeMap [ key ] ;
const extraStr = JSON . stringify ( item . extra _json || { } ) ;
if ( existing ) {
2026-05-06 11:44:17 -06:00
await client . query (
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
` UPDATE compliance_items
2026-05-06 11:44:17 -06:00
SET upload _id = $1 , seen _count = $2 , ip _address = $3 , device _type = $4 , extra _json = $5
WHERE id = $6 ` ,
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
[ uploadId , existing . seen _count + 1 , item . ip _address , item . device _type , extraStr , existing . id ]
) ;
recurringCount ++ ;
} else {
2026-05-06 11:44:17 -06:00
await client . query (
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
` INSERT INTO compliance_items
( upload _id , hostname , ip _address , device _type , team , metric _id , metric _desc ,
category , extra _json , status , first _seen _upload _id , seen _count )
2026-05-06 11:44:17 -06:00
VALUES ( $1 , $2 , $3 , $4 , $5 , $6 , $7 , $8 , $9 , 'active' , $10 , 1 ) ` ,
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
[ uploadId , item . hostname , item . ip _address , item . device _type , item . team ,
item . metric _id , item . metric _desc , item . category , extraStr , uploadId ]
) ;
newCount ++ ;
}
}
// 3. Mark items not present in this upload as resolved
for ( const [ key , row ] of Object . entries ( activeMap ) ) {
if ( ! newKeys . has ( key ) ) {
2026-05-06 11:44:17 -06:00
await client . query (
` UPDATE compliance_items SET status = 'resolved', resolved_upload_id = $ 1 WHERE id = $ 2 ` ,
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
[ uploadId , row . id ]
) ;
resolvedCount ++ ;
}
}
// 4. Update upload with final counts
2026-05-06 11:44:17 -06:00
await client . query (
` UPDATE compliance_uploads SET new_count = $ 1, resolved_count = $ 2, recurring_count = $ 3 WHERE id = $ 4 ` ,
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
[ newCount , resolvedCount , recurringCount , uploadId ]
) ;
2026-05-06 11:44:17 -06:00
await client . query ( 'COMMIT' ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
return { uploadId , newCount , recurringCount , resolvedCount } ;
} catch ( err ) {
2026-05-06 11:44:17 -06:00
await client . query ( 'ROLLBACK' ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
throw err ;
2026-05-06 11:44:17 -06:00
} finally {
client . release ( ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
}
}
// ---------------------------------------------------------------------------
// Group flat compliance_items rows into per-device objects
// ---------------------------------------------------------------------------
function groupByHostname ( rows , noteHostnames ) {
const deviceMap = { } ;
for ( const row of rows ) {
if ( ! deviceMap [ row . hostname ] ) {
deviceMap [ row . hostname ] = {
2026-05-06 11:44:17 -06:00
hostname : row . hostname , ip _address : row . ip _address || '' , device _type : row . device _type || '' ,
team : row . team || '' , status : row . status , failing _metrics : [ ] ,
seen _count : row . seen _count || 1 , first _seen : row . first _seen || null ,
last _seen : row . last _seen || null , resolved _on : row . resolved _on || null ,
has _notes : noteHostnames . has ( row . hostname ) ,
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
} ;
}
const dev = deviceMap [ row . hostname ] ;
2026-05-06 11:44:17 -06:00
dev . failing _metrics . push ( { metric _id : row . metric _id , metric _desc : row . metric _desc || '' , category : row . category || '' } ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
if ( ( row . seen _count || 1 ) > dev . seen _count ) dev . seen _count = row . seen _count ;
2026-05-06 11:44:17 -06:00
if ( row . first _seen && ( ! dev . first _seen || row . first _seen < dev . first _seen ) ) dev . first _seen = row . first _seen ;
if ( row . last _seen && ( ! dev . last _seen || row . last _seen > dev . last _seen ) ) dev . last _seen = row . last _seen ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
}
return Object . values ( deviceMap ) ;
}
2026-05-01 17:15:41 +00:00
// ---------------------------------------------------------------------------
2026-05-06 11:44:17 -06:00
// Pure helpers
2026-05-01 17:15:41 +00:00
// ---------------------------------------------------------------------------
const BUCKET _ORDER = [ '1 cycle' , '2– 3 cycles' , '4– 6 cycles' , '7+ cycles' ] ;
function bucketAgingItems ( items ) {
const teams = [ 'STEAM' , 'ACCESS-ENG' , 'ACCESS-OPS' , 'INTELDEV' ] ;
const buckets = { } ;
for ( const b of BUCKET _ORDER ) {
buckets [ b ] = { bucket : b , total : 0 } ;
for ( const t of teams ) buckets [ b ] [ t ] = 0 ;
}
for ( const item of items ) {
const sc = item . seen _count ;
let label ;
2026-05-06 11:44:17 -06:00
if ( sc === 1 ) label = '1 cycle' ;
else if ( sc >= 2 && sc <= 3 ) label = '2– 3 cycles' ;
else if ( sc >= 4 && sc <= 6 ) label = '4– 6 cycles' ;
else label = '7+ cycles' ;
2026-05-01 17:15:41 +00:00
buckets [ label ] . total += 1 ;
2026-05-06 11:44:17 -06:00
if ( item . team in buckets [ label ] ) buckets [ label ] [ item . team ] += 1 ;
2026-05-01 17:15:41 +00:00
}
return BUCKET _ORDER . map ( b => buckets [ b ] ) ;
}
function computeWaterfall ( uploads ) {
let start = 0 ;
return uploads . map ( ( row ) => {
const end = start + row . new _count + row . recurring _count - row . resolved _count ;
2026-05-06 11:44:17 -06:00
const entry = { date : row . report _date , start , new _count : row . new _count , recurring _count : row . recurring _count , resolved _count : row . resolved _count , end } ;
2026-05-01 17:15:41 +00:00
start = end ;
return entry ;
} ) ;
}
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
// ---------------------------------------------------------------------------
// Router factory
// ---------------------------------------------------------------------------
2026-05-06 11:44:17 -06:00
function createComplianceRouter ( upload ) {
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
const router = express . Router ( ) ;
// All compliance routes require authentication
2026-05-06 11:44:17 -06:00
router . use ( requireAuth ( ) ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
// POST /preview
2026-04-06 16:18:07 -06:00
router . post ( '/preview' , requireGroup ( 'Admin' , 'Standard_User' ) , ( req , res ) => {
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
upload . single ( 'file' ) ( req , res , async ( uploadErr ) => {
2026-05-06 11:44:17 -06:00
if ( uploadErr ) return res . status ( 400 ) . json ( { error : uploadErr . message } ) ;
if ( ! req . file ) return res . status ( 400 ) . json ( { error : 'No file uploaded' } ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
if ( path . extname ( req . file . originalname ) . toLowerCase ( ) !== '.xlsx' ) {
fs . unlink ( req . file . path , ( ) => { } ) ;
return res . status ( 400 ) . json ( { error : 'File must be an .xlsx spreadsheet' } ) ;
}
try {
2026-05-06 11:44:17 -06:00
let drift = null , drift _error = null ;
2026-04-20 20:12:12 +00:00
let config ;
2026-05-06 11:44:17 -06:00
try { config = loadConfig ( CONFIG _PATH ) ; } catch ( configErr ) {
2026-04-20 20:12:12 +00:00
fs . unlink ( req . file . path , ( ) => { } ) ;
return res . status ( 500 ) . json ( { error : 'Configuration file could not be loaded: ' + configErr . message } ) ;
}
let xlsxSchema = null ;
try {
xlsxSchema = await extractXlsxSchema ( req . file . path ) ;
2026-05-06 11:44:17 -06:00
if ( xlsxSchema . error ) throw new Error ( xlsxSchema . error ) ;
2026-04-20 20:12:12 +00:00
drift = compareSchemaToDrift ( xlsxSchema , config ) ;
} catch ( driftErr ) {
drift = null ;
drift _error = driftErr . message || 'Drift check failed' ;
}
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
const parsed = await parseXlsx ( req . file . path ) ;
if ( parsed . error ) {
fs . unlink ( req . file . path , ( ) => { } ) ;
return res . status ( 422 ) . json ( { error : parsed . error } ) ;
}
2026-05-06 11:44:17 -06:00
const diff = await computeDiff ( parsed . items ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
if ( ! fs . existsSync ( TEMP _DIR ) ) fs . mkdirSync ( TEMP _DIR , { recursive : true } ) ;
const tempFilename = ` compliance_preview_ ${ Date . now ( ) } _ ${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } .json ` ;
const tempFilePath = path . join ( TEMP _DIR , tempFilename ) ;
fs . writeFileSync ( tempFilePath , JSON . stringify ( {
2026-05-06 11:44:17 -06:00
items : parsed . items , summary : parsed . summary ,
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
report _date : parsed . report _date ,
2026-05-06 11:44:17 -06:00
filename : req . file . originalname . replace ( /[^\w.\-() ]/g , '_' ) ,
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
} ) ) ;
fs . unlink ( req . file . path , ( ) => { } ) ;
res . json ( {
2026-05-06 11:44:17 -06:00
drift , drift _error , schema : xlsxSchema ,
diff : { new _count : diff . newCount , recurring _count : diff . recurringCount , resolved _count : diff . resolvedCount } ,
tempFile : tempFilePath , filename : req . file . originalname ,
report _date : parsed . report _date , total _items : parsed . total ,
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
} ) ;
} catch ( err ) {
fs . unlink ( req . file . path , ( ) => { } ) ;
console . error ( '[Compliance] Preview error:' , err . message ) ;
res . status ( 500 ) . json ( { error : 'Failed to parse file: ' + err . message } ) ;
}
} ) ;
} ) ;
2026-04-20 20:12:12 +00:00
// POST /reconcile-config
router . post ( '/reconcile-config' , requireGroup ( 'Admin' ) , async ( req , res ) => {
const { drift , schema } = req . body ;
2026-05-06 11:44:17 -06:00
if ( ! drift || typeof drift !== 'object' ) return res . status ( 400 ) . json ( { error : 'drift report is required in request body' } ) ;
const hasFindings = ( drift . breaking && drift . breaking . length > 0 ) || ( drift . silent _miss && drift . silent _miss . length > 0 ) ;
if ( ! hasFindings ) return res . status ( 400 ) . json ( { error : 'No breaking or silent-miss findings to reconcile' } ) ;
2026-04-20 20:12:12 +00:00
try {
const { changes } = reconcileConfig ( CONFIG _PATH , drift , schema || null ) ;
2026-05-06 11:44:17 -06:00
if ( changes . length === 0 ) return res . json ( { changes : [ ] , message : 'No changes needed' } ) ;
2026-04-20 20:12:12 +00:00
for ( const change of changes ) {
2026-05-06 11:44:17 -06:00
logAudit ( { userId : req . user . id , username : req . user . username , action : 'compliance_config_reconcile' , entityType : 'compliance_config' , entityId : change . value , details : { action : change . action , key : change . key , detail : change . detail } , ipAddress : req . ip } ) ;
2026-04-20 20:12:12 +00:00
}
res . json ( { changes , message : ` Reconciled ${ changes . length } config change(s) ` } ) ;
} catch ( err ) {
console . error ( '[Compliance] Reconcile config error:' , err . message ) ;
res . status ( 500 ) . json ( { error : 'Failed to reconcile config: ' + err . message } ) ;
}
} ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
// POST /commit
2026-04-06 16:18:07 -06:00
router . post ( '/commit' , requireGroup ( 'Admin' , 'Standard_User' ) , async ( req , res ) => {
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
const { tempFile , filename , report _date } = req . body ;
2026-05-06 11:44:17 -06:00
if ( ! tempFile || typeof tempFile !== 'string' ) return res . status ( 400 ) . json ( { error : 'tempFile is required' } ) ;
if ( ! isSafeTempPath ( tempFile ) ) return res . status ( 400 ) . json ( { error : 'Invalid tempFile path' } ) ;
if ( ! fs . existsSync ( tempFile ) ) return res . status ( 400 ) . json ( { error : 'Preview session expired — please upload again' } ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
let parsed ;
2026-05-06 11:44:17 -06:00
try { parsed = JSON . parse ( fs . readFileSync ( tempFile , 'utf8' ) ) ; }
catch { return res . status ( 400 ) . json ( { error : 'Could not read preview data — please upload again' } ) ; }
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
try {
2026-05-06 11:44:17 -06:00
const result = await persistUpload ( {
items : parsed . items , summary : parsed . summary ,
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
reportDate : report _date || parsed . report _date ,
2026-05-06 11:44:17 -06:00
filename : filename || parsed . filename ,
userId : req . user ? . id || null ,
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
} ) ;
fs . unlink ( tempFile , ( ) => { } ) ;
2026-05-06 11:44:17 -06:00
const { rows } = await pool . query (
` SELECT id, filename, report_date, uploaded_at, new_count, resolved_count, recurring_count
FROM compliance _uploads WHERE id = $1 ` , [result.uploadId]
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
) ;
2026-05-06 11:44:17 -06:00
res . json ( { upload : rows [ 0 ] } ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
} catch ( err ) {
console . error ( '[Compliance] Commit error:' , err . message ) ;
res . status ( 500 ) . json ( { error : 'Failed to commit upload: ' + err . message } ) ;
}
} ) ;
// GET /uploads
router . get ( '/uploads' , async ( req , res ) => {
try {
2026-05-06 11:44:17 -06:00
const { rows } = await pool . query (
` SELECT id, filename, report_date, uploaded_at, new_count, resolved_count, recurring_count
FROM compliance _uploads ORDER BY id DESC `
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
) ;
res . json ( { uploads : rows } ) ;
} catch ( err ) {
console . error ( '[Compliance] GET /uploads error:' , err . message ) ;
res . status ( 500 ) . json ( { error : 'Database error' } ) ;
}
} ) ;
2026-04-20 20:12:12 +00:00
// POST /rollback/:uploadId
router . post ( '/rollback/:uploadId' , requireGroup ( 'Admin' ) , async ( req , res ) => {
const uploadId = parseInt ( req . params . uploadId , 10 ) ;
2026-05-06 11:44:17 -06:00
if ( isNaN ( uploadId ) ) return res . status ( 400 ) . json ( { error : 'Invalid upload ID' } ) ;
2026-04-20 20:12:12 +00:00
try {
2026-05-06 11:44:17 -06:00
const { rows : uploadRows } = await pool . query (
` SELECT id, filename, report_date, new_count, resolved_count, recurring_count FROM compliance_uploads WHERE id = $ 1 ` , [ uploadId ]
2026-04-20 20:12:12 +00:00
) ;
2026-05-06 11:44:17 -06:00
const upload = uploadRows [ 0 ] ;
if ( ! upload ) return res . status ( 404 ) . json ( { error : 'Upload not found' } ) ;
2026-04-20 20:12:12 +00:00
2026-05-06 11:44:17 -06:00
const { rows : latestRows } = await pool . query ( ` SELECT id FROM compliance_uploads ORDER BY id DESC LIMIT 1 ` ) ;
if ( latestRows [ 0 ] . id !== uploadId ) {
return res . status ( 400 ) . json ( { error : 'Only the most recent upload can be rolled back' , latest _upload _id : latestRows [ 0 ] . id } ) ;
2026-04-20 20:12:12 +00:00
}
2026-05-06 11:44:17 -06:00
const { rows : prevRows } = await pool . query ( ` SELECT id FROM compliance_uploads WHERE id < $ 1 ORDER BY id DESC LIMIT 1 ` , [ uploadId ] ) ;
const previousUpload = prevRows [ 0 ] ;
2026-04-20 20:12:12 +00:00
2026-05-06 11:44:17 -06:00
const client = await pool . connect ( ) ;
2026-04-20 20:12:12 +00:00
try {
2026-05-06 11:44:17 -06:00
await client . query ( 'BEGIN' ) ;
2026-04-20 20:12:12 +00:00
2026-05-06 11:44:17 -06:00
const deleteNew = await client . query (
` DELETE FROM compliance_items WHERE first_seen_upload_id = $ 1 AND upload_id = $ 1 ` , [ uploadId ]
) ;
const reactivate = await client . query (
` UPDATE compliance_items SET status = 'active', resolved_upload_id = NULL WHERE resolved_upload_id = $ 1 ` , [ uploadId ]
2026-04-20 20:12:12 +00:00
) ;
if ( previousUpload ) {
2026-05-06 11:44:17 -06:00
await client . query (
` UPDATE compliance_items SET upload_id = $ 1, seen_count = GREATEST(seen_count - 1, 1) WHERE upload_id = $ 2 AND first_seen_upload_id != $ 2 ` ,
[ previousUpload . id , uploadId ]
2026-04-20 20:12:12 +00:00
) ;
}
2026-05-06 11:44:17 -06:00
await client . query ( ` DELETE FROM compliance_uploads WHERE id = $ 1 ` , [ uploadId ] ) ;
await client . query ( 'COMMIT' ) ;
2026-04-20 20:12:12 +00:00
2026-05-06 11:44:17 -06:00
logAudit ( { userId : req . user . id , username : req . user . username , action : 'compliance_upload_rollback' , entityType : 'compliance_upload' , entityId : String ( uploadId ) , details : { filename : upload . filename , report _date : upload . report _date , items _deleted : deleteNew . rowCount , items _reactivated : reactivate . rowCount } , ipAddress : req . ip } ) ;
2026-04-20 20:12:12 +00:00
2026-05-06 11:44:17 -06:00
res . json ( { message : ` Rolled back upload " ${ upload . filename } " ` , rolled _back : { upload _id : uploadId , filename : upload . filename , report _date : upload . report _date , items _deleted : deleteNew . rowCount , items _reactivated : reactivate . rowCount } } ) ;
2026-04-20 20:12:12 +00:00
} catch ( err ) {
2026-05-06 11:44:17 -06:00
await client . query ( 'ROLLBACK' ) ;
2026-04-20 20:12:12 +00:00
throw err ;
2026-05-06 11:44:17 -06:00
} finally {
client . release ( ) ;
2026-04-20 20:12:12 +00:00
}
} catch ( err ) {
console . error ( '[Compliance] Rollback error:' , err . message ) ;
res . status ( 500 ) . json ( { error : 'Failed to rollback upload: ' + err . message } ) ;
}
} ) ;
2026-05-06 11:44:17 -06:00
// GET /summary
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
router . get ( '/summary' , async ( req , res ) => {
const team = req . query . team ;
2026-05-06 11:44:17 -06:00
if ( team && ! ALLOWED _TEAMS . has ( team ) ) return res . status ( 400 ) . json ( { error : 'Invalid team' } ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
try {
2026-05-06 11:44:17 -06:00
const { rows : latestRows } = await pool . query (
` SELECT id, summary_json, report_date, uploaded_at FROM compliance_uploads ORDER BY id DESC LIMIT 1 `
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
) ;
2026-05-06 11:44:17 -06:00
const latestUpload = latestRows [ 0 ] ;
if ( ! latestUpload || ! latestUpload . summary _json ) return res . json ( { entries : [ ] , overall _scores : { } , upload : null } ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
let summary ;
2026-05-06 11:44:17 -06:00
try { summary = JSON . parse ( latestUpload . summary _json ) ; } catch { return res . json ( { entries : [ ] , overall _scores : { } , upload : null } ) ; }
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
let entries = summary . entries || [ ] ;
2026-05-06 11:44:17 -06:00
if ( team ) entries = entries . filter ( e => e . team === team ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
2026-05-06 11:44:17 -06:00
res . json ( { entries , overall _scores : summary . overall _scores || { } , upload : { id : latestUpload . id , report _date : latestUpload . report _date , uploaded _at : latestUpload . uploaded _at } } ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
} catch ( err ) {
console . error ( '[Compliance] GET /summary error:' , err . message ) ;
res . status ( 500 ) . json ( { error : 'Database error' } ) ;
}
} ) ;
2026-05-06 11:44:17 -06:00
// GET /items
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
router . get ( '/items' , async ( req , res ) => {
const { team , status = 'active' } = req . query ;
if ( ! team ) return res . status ( 400 ) . json ( { error : 'team is required' } ) ;
if ( ! ALLOWED _TEAMS . has ( team ) ) return res . status ( 400 ) . json ( { error : 'Invalid team' } ) ;
if ( ! [ 'active' , 'resolved' ] . includes ( status ) ) return res . status ( 400 ) . json ( { error : 'Invalid status' } ) ;
try {
2026-05-06 11:44:17 -06:00
const { rows } = await pool . query (
` SELECT ci.hostname, ci.ip_address, ci.device_type, ci.team, ci.metric_id, ci.metric_desc, ci.category, ci.status, ci.seen_count,
fu . report _date AS first _seen , lu . report _date AS last _seen , ru . report _date AS resolved _on
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
FROM compliance _items ci
LEFT JOIN compliance _uploads fu ON ci . first _seen _upload _id = fu . id
2026-05-06 11:44:17 -06:00
LEFT JOIN compliance _uploads lu ON ci . upload _id = lu . id
LEFT JOIN compliance _uploads ru ON ci . resolved _upload _id = ru . id
WHERE ci . team = $1 AND ci . status = $2
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
ORDER BY ci . hostname , ci . metric _id ` ,
[ team , status ]
) ;
2026-05-06 11:44:17 -06:00
const { rows : noteRows } = await pool . query ( ` SELECT DISTINCT hostname FROM compliance_notes ` ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
const noteHostnames = new Set ( noteRows . map ( r => r . hostname ) ) ;
const devices = groupByHostname ( rows , noteHostnames ) ;
res . json ( { devices , team , status } ) ;
} catch ( err ) {
console . error ( '[Compliance] GET /items error:' , err . message ) ;
res . status ( 500 ) . json ( { error : 'Database error' } ) ;
}
} ) ;
// GET /items/:hostname
router . get ( '/items/:hostname' , async ( req , res ) => {
const hostname = req . params . hostname ;
2026-05-06 11:44:17 -06:00
if ( ! hostname || hostname . length > 300 ) return res . status ( 400 ) . json ( { error : 'Invalid hostname' } ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
try {
2026-05-06 11:44:17 -06:00
const { rows : metricRows } = await pool . query (
` SELECT ci.metric_id, ci.metric_desc, ci.category, ci.status, ci.ip_address, ci.device_type, ci.team, ci.seen_count, ci.extra_json,
fu . report _date AS first _seen , fu . uploaded _at AS first _seen _at , lu . report _date AS last _seen , lu . uploaded _at AS last _seen _at , ru . report _date AS resolved _on
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
FROM compliance _items ci
LEFT JOIN compliance _uploads fu ON ci . first _seen _upload _id = fu . id
2026-05-06 11:44:17 -06:00
LEFT JOIN compliance _uploads lu ON ci . upload _id = lu . id
LEFT JOIN compliance _uploads ru ON ci . resolved _upload _id = ru . id
WHERE ci . hostname = $1
ORDER BY ci . status DESC , ci . metric _id ` , [hostname]
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
) ;
2026-05-06 11:44:17 -06:00
if ( metricRows . length === 0 ) return res . status ( 404 ) . json ( { error : 'Device not found' } ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
2026-05-06 11:44:17 -06:00
const metrics = metricRows . map ( r => ( { ... r , extra : ( ( ) => { try { return JSON . parse ( r . extra _json || '{}' ) ; } catch { return { } ; } } ) ( ) , extra _json : undefined } ) ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
2026-05-06 11:44:17 -06:00
const { rows : notes } = await pool . query (
` SELECT cn.id, cn.metric_id, cn.note, cn.group_id, cn.created_at, u.username AS created_by
FROM compliance _notes cn LEFT JOIN users u ON cn . created _by = u . id
WHERE cn . hostname = $1 ORDER BY cn . created _at DESC ` , [hostname]
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
) ;
const identity = metricRows . find ( r => r . status === 'active' ) || metricRows [ 0 ] ;
2026-05-06 11:44:17 -06:00
res . json ( { hostname , ip _address : identity . ip _address || '' , device _type : identity . device _type || '' , team : identity . team || '' , metrics , notes } ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
} catch ( err ) {
console . error ( '[Compliance] GET /items/:hostname error:' , err . message ) ;
res . status ( 500 ) . json ( { error : 'Database error' } ) ;
}
} ) ;
// POST /notes
2026-04-07 09:52:26 -06:00
router . post ( '/notes' , requireGroup ( 'Admin' , 'Standard_User' ) , async ( req , res ) => {
2026-04-16 14:28:44 -06:00
const { hostname , metric _id , metric _ids , note } = req . body ;
2026-05-06 11:44:17 -06:00
if ( ! hostname || typeof hostname !== 'string' || hostname . length > 300 || ! /^[a-zA-Z0-9._-]+$/ . test ( hostname ) ) return res . status ( 400 ) . json ( { error : 'Invalid hostname format' } ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
2026-04-16 14:28:44 -06:00
let resolvedIds ;
if ( metric _ids !== undefined ) {
2026-05-06 11:44:17 -06:00
if ( ! Array . isArray ( metric _ids ) ) return res . status ( 400 ) . json ( { error : 'metric_ids must be an array' } ) ;
2026-04-16 14:28:44 -06:00
resolvedIds = metric _ids ;
} else if ( metric _id !== undefined && metric _id !== null && metric _id !== '' ) {
2026-05-06 11:44:17 -06:00
if ( typeof metric _id !== 'string' || metric _id . length > 50 ) return res . status ( 400 ) . json ( { error : 'Invalid metric_id' } ) ;
2026-04-16 14:28:44 -06:00
resolvedIds = [ metric _id ] ;
} else {
return res . status ( 400 ) . json ( { error : 'metric_id or metric_ids is required' } ) ;
}
2026-05-06 11:44:17 -06:00
if ( resolvedIds . length === 0 ) return res . status ( 400 ) . json ( { error : 'At least one metric ID is required' } ) ;
2026-04-16 14:28:44 -06:00
for ( let i = 0 ; i < resolvedIds . length ; i ++ ) {
const mid = resolvedIds [ i ] ;
2026-05-06 11:44:17 -06:00
if ( ! mid || typeof mid !== 'string' || mid . length === 0 || mid . length > 50 ) return res . status ( 400 ) . json ( { error : ` Invalid metric_id at index ${ i } ` } ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
}
2026-04-16 14:28:44 -06:00
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
const noteText = String ( note || '' ) . trim ( ) . slice ( 0 , 1000 ) ;
2026-05-06 11:44:17 -06:00
if ( ! noteText ) return res . status ( 400 ) . json ( { error : 'Note cannot be empty' } ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
2026-04-16 14:28:44 -06:00
const groupId = crypto . randomUUID ( ) ;
const userId = req . user ? . id || null ;
2026-05-06 11:44:17 -06:00
const client = await pool . connect ( ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
try {
2026-05-06 11:44:17 -06:00
await client . query ( 'BEGIN' ) ;
2026-04-16 14:28:44 -06:00
const insertedIds = [ ] ;
for ( const mid of resolvedIds ) {
2026-05-06 11:44:17 -06:00
const { rows } = await client . query (
` INSERT INTO compliance_notes (hostname, metric_id, note, group_id, created_by, created_at) VALUES ( $ 1, $ 2, $ 3, $ 4, $ 5, NOW()) RETURNING id ` ,
2026-04-16 14:28:44 -06:00
[ hostname , mid , noteText , groupId , userId ]
) ;
2026-05-06 11:44:17 -06:00
insertedIds . push ( rows [ 0 ] . id ) ;
2026-04-16 14:28:44 -06:00
}
2026-05-06 11:44:17 -06:00
await client . query ( 'COMMIT' ) ;
2026-04-16 14:28:44 -06:00
2026-05-06 11:44:17 -06:00
const { rows : notes } = await pool . query (
` SELECT cn.id, cn.hostname, cn.metric_id, cn.note, cn.group_id, cn.created_at, u.username AS created_by
FROM compliance _notes cn LEFT JOIN users u ON cn . created _by = u . id
WHERE cn . id = ANY ( $1 ) ORDER BY cn . id ASC ` , [insertedIds]
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
) ;
2026-04-16 14:28:44 -06:00
res . status ( 201 ) . json ( { notes } ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
} catch ( err ) {
2026-05-06 11:44:17 -06:00
await client . query ( 'ROLLBACK' ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
console . error ( '[Compliance] POST /notes error:' , err . message ) ;
res . status ( 500 ) . json ( { error : 'Failed to save note' } ) ;
2026-05-06 11:44:17 -06:00
} finally {
client . release ( ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
}
} ) ;
// GET /notes/:hostname/:metricId
router . get ( '/notes/:hostname/:metricId' , async ( req , res ) => {
const { hostname , metricId } = req . params ;
if ( ! hostname || hostname . length > 300 ) return res . status ( 400 ) . json ( { error : 'Invalid hostname' } ) ;
2026-05-06 11:44:17 -06:00
if ( ! metricId || metricId . length > 50 ) return res . status ( 400 ) . json ( { error : 'Invalid metricId' } ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
try {
2026-05-06 11:44:17 -06:00
const { rows : notes } = await pool . query (
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
` SELECT cn.id, cn.note, cn.created_at, u.username AS created_by
2026-05-06 11:44:17 -06:00
FROM compliance _notes cn LEFT JOIN users u ON cn . created _by = u . id
WHERE cn . hostname = $1 AND cn . metric _id = $2 ORDER BY cn . created _at DESC ` , [hostname, metricId]
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
) ;
res . json ( { notes } ) ;
} catch ( err ) {
console . error ( '[Compliance] GET /notes error:' , err . message ) ;
res . status ( 500 ) . json ( { error : 'Database error' } ) ;
}
} ) ;
2026-04-20 21:39:43 +00:00
// DELETE /notes/:id
router . delete ( '/notes/:id' , requireGroup ( 'Admin' , 'Standard_User' ) , async ( req , res ) => {
const noteId = parseInt ( req . params . id , 10 ) ;
if ( isNaN ( noteId ) ) return res . status ( 400 ) . json ( { error : 'Invalid note ID' } ) ;
const deleteGroup = req . query . group === 'true' ;
try {
2026-05-06 11:44:17 -06:00
const { rows } = await pool . query ( ` SELECT id, hostname, metric_id, note, group_id, created_by FROM compliance_notes WHERE id = $ 1 ` , [ noteId ] ) ;
const noteRow = rows [ 0 ] ;
if ( ! noteRow ) return res . status ( 404 ) . json ( { error : 'Note not found' } ) ;
2026-04-20 21:39:43 +00:00
2026-05-06 11:44:17 -06:00
const isAuthor = req . user && String ( req . user . id ) === String ( noteRow . created _by ) ;
2026-04-20 21:39:43 +00:00
const isAdminUser = req . user && req . user . group === 'Admin' ;
2026-05-06 11:44:17 -06:00
if ( ! isAuthor && ! isAdminUser ) return res . status ( 403 ) . json ( { error : 'You can only delete your own notes' } ) ;
2026-04-20 21:39:43 +00:00
let deleted = 0 ;
2026-05-06 11:44:17 -06:00
if ( deleteGroup && noteRow . group _id ) {
const result = await pool . query ( ` DELETE FROM compliance_notes WHERE group_id = $ 1 ` , [ noteRow . group _id ] ) ;
deleted = result . rowCount ;
2026-04-20 21:39:43 +00:00
} else {
2026-05-06 11:44:17 -06:00
const result = await pool . query ( ` DELETE FROM compliance_notes WHERE id = $ 1 ` , [ noteId ] ) ;
deleted = result . rowCount ;
2026-04-20 21:39:43 +00:00
}
2026-05-06 11:44:17 -06:00
logAudit ( { userId : req . user . id , username : req . user . username , action : 'compliance_note_delete' , entityType : 'compliance_note' , entityId : String ( noteId ) , details : JSON . stringify ( { hostname : noteRow . hostname , group _id : noteRow . group _id , deleted _count : deleted } ) , ipAddress : req . ip } ) ;
2026-04-20 21:39:43 +00:00
res . json ( { deleted } ) ;
} catch ( err ) {
console . error ( '[Compliance] DELETE /notes error:' , err . message ) ;
res . status ( 500 ) . json ( { error : 'Failed to delete note' } ) ;
}
} ) ;
2026-04-02 09:49:32 -06:00
// GET /trends
router . get ( '/trends' , async ( req , res ) => {
try {
2026-05-06 11:44:17 -06:00
const { rows : uploads } = await pool . query (
` SELECT id, report_date, COALESCE(new_count, 0) AS new_count, COALESCE(recurring_count, 0) AS recurring_count, COALESCE(resolved_count, 0) AS resolved_count, COALESCE(new_count, 0) + COALESCE(recurring_count, 0) AS total_active FROM compliance_uploads ORDER BY report_date ASC `
2026-04-02 09:49:32 -06:00
) ;
if ( uploads . length === 0 ) return res . json ( { trends : [ ] } ) ;
2026-05-06 11:44:17 -06:00
const { rows : teamRows } = await pool . query (
` SELECT ci.upload_id, ci.team, COUNT(ci.id)::int AS count FROM compliance_items ci WHERE ci.team IS NOT NULL GROUP BY ci.upload_id, ci.team `
2026-04-02 09:49:32 -06:00
) ;
const teamMap = { } ;
2026-05-06 11:44:17 -06:00
teamRows . forEach ( r => { if ( ! teamMap [ r . upload _id ] ) teamMap [ r . upload _id ] = { } ; teamMap [ r . upload _id ] [ r . team ] = r . count ; } ) ;
2026-04-02 09:49:32 -06:00
const trends = uploads . map ( u => ( {
2026-05-06 11:44:17 -06:00
report _date : u . report _date , new _count : u . new _count , recurring _count : u . recurring _count , resolved _count : u . resolved _count , total _active : u . total _active ,
STEAM : teamMap [ u . id ] ? . STEAM || 0 , 'ACCESS-ENG' : teamMap [ u . id ] ? . [ 'ACCESS-ENG' ] || 0 , 'ACCESS-OPS' : teamMap [ u . id ] ? . [ 'ACCESS-OPS' ] || 0 , INTELDEV : teamMap [ u . id ] ? . INTELDEV || 0 ,
2026-04-02 09:49:32 -06:00
} ) ) ;
res . json ( { trends } ) ;
} catch ( err ) {
console . error ( '[Compliance] GET /trends error:' , err . message ) ;
res . status ( 500 ) . json ( { error : 'Database error' } ) ;
}
} ) ;
// GET /mttr
router . get ( '/mttr' , async ( req , res ) => {
try {
2026-05-06 11:44:17 -06:00
const { rows } = await pool . query ( ` SELECT COALESCE(seen_count, 1) AS seen_count, team FROM compliance_items WHERE status = 'active' ` ) ;
if ( rows . length === 0 ) return res . json ( { aging : [ ] } ) ;
2026-05-01 17:15:41 +00:00
const aging = bucketAgingItems ( rows ) ;
res . json ( { aging } ) ;
2026-04-02 09:49:32 -06:00
} catch ( err ) {
console . error ( '[Compliance] GET /mttr error:' , err . message ) ;
res . status ( 500 ) . json ( { error : 'Database error' } ) ;
}
} ) ;
// GET /top-recurring
router . get ( '/top-recurring' , async ( req , res ) => {
try {
2026-05-06 11:44:17 -06:00
const { rows } = await pool . query (
` SELECT id, report_date, COALESCE(new_count, 0) AS new_count, COALESCE(recurring_count, 0) AS recurring_count, COALESCE(resolved_count, 0) AS resolved_count FROM compliance_uploads ORDER BY report_date ASC `
2026-04-02 09:49:32 -06:00
) ;
2026-05-01 17:15:41 +00:00
const waterfall = computeWaterfall ( rows ) ;
res . json ( { waterfall } ) ;
2026-04-02 09:49:32 -06:00
} catch ( err ) {
console . error ( '[Compliance] GET /top-recurring error:' , err . message ) ;
res . status ( 500 ) . json ( { error : 'Database error' } ) ;
}
} ) ;
// GET /category-trend
router . get ( '/category-trend' , async ( req , res ) => {
try {
2026-05-06 11:44:17 -06:00
const { rows } = await pool . query (
` SELECT cu.report_date, COALESCE(ci.category, 'Unknown') AS category, COUNT(ci.id)::int AS count
FROM compliance _uploads cu JOIN compliance _items ci ON ci . upload _id = cu . id
GROUP BY cu . id , cu . report _date , category ORDER BY cu . report _date ASC `
2026-04-02 09:49:32 -06:00
) ;
res . json ( { categoryTrend : rows } ) ;
} catch ( err ) {
console . error ( '[Compliance] GET /category-trend error:' , err . message ) ;
res . status ( 500 ) . json ( { error : 'Database error' } ) ;
}
} ) ;
feat(compliance): add AEO compliance tracking backend
- Migration: compliance_uploads, compliance_items, compliance_notes tables
with indexes on (hostname, metric_id) identity key and team/status
- Python parser (parse_compliance_xlsx.py): reads NTS_AEO xlsx, extracts
non-compliant assets from all detail sheets, parses Summary sheet for
metric health data and overall scores, outputs JSON to stdout
- Route (/api/compliance): preview/commit upload flow with diff summary,
items endpoint grouped by hostname with seen_count tracking, metric
summary endpoint for health cards, notes endpoints keyed on
(hostname, metric_id) persisting across uploads
- server.js: register compliance router at /api/compliance
- .gitignore: exclude planning docs and xlsx source files
2026-03-31 15:06:59 -06:00
return router ;
}
2026-05-01 17:15:41 +00:00
module . exports = { createComplianceRouter , bucketAgingItems , computeWaterfall } ;