2026-08-14 12:43:11 +02:00
import 'dotenv/config'
import cors from 'cors'
import express from 'express'
import fs from 'node:fs'
import { createHash , randomUUID } from 'node:crypto'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import multer from 'multer'
import pg , { type PoolClient } from 'pg'
import type { CaseDocument , CaseState , Connection , Evidence , WidgetRelation } from '../src/types.js'
const { Pool } = pg
const databaseUrl = process . env . DATABASE_URL
if ( ! databaseUrl ) {
console . error ( 'DATABASE_URL is required. Run PostgreSQL and execute npm run migrate:up first.' )
process . exit ( 1 )
}
2026-08-14 12:57:06 +02:00
export const pool = new Pool ({ connectionString : databaseUrl })
2026-08-14 12:43:11 +02:00
const editingEnabled = process . env . LEVEL_EDITING_ENABLED === 'true'
type WidgetRow = {
id : string ; widget_type : 'document' | Evidence [ 'type' ]; title : string ; content : string
config : { kind? : string ; date? : string ; body? : string []; fileType? : CaseDocument [ 'fileType' ]; metadata? : Record < string , string >; [ key : string ] : unknown }; source_widget_id? : string ; source_region_key? : string
event_date? : string ; published_at? : string ; x? : number ; y? : number ; width? : number ; sort_order : number ; asset_id? : string
original_name? : string ; mime_type? : string ; byte_size? : number
}
function wantsEdit ( req : express.Request ) {
return editingEnabled && req . query . edit === '1'
}
async function assembleLevel ( levelId : string , playthroughId = `default: ${ levelId } ` , authorMode = false ) : Promise < CaseState | null > {
const levelResult = await pool . query < { id : string ; title : string ; subtitle : string ; status : string } > (
'SELECT id, title, subtitle, status FROM osint.levels WHERE id = $1' , [ levelId ],
)
const level = levelResult . rows [ 0 ]
if ( ! level ) return null
await pool . query (
`INSERT INTO osint.playthroughs (id, level_id) VALUES ($1, $2) ON CONFLICT (id) DO NOTHING` ,
[ playthroughId , levelId ],
)
const [ widgetsResult , regionsResult , authoredConnections , authoredRelations , playthroughResult , generatedResult , playerConnections , playerRelations , stateResult , relationStateResult ] = await Promise . all ([
pool . query < WidgetRow >( `SELECT w.id, w.widget_type, w.title, w.content, w.config, w.source_widget_id, w.source_region_key,
w.event_date::text, w.published_at::text, w.x, w.y, w.width, w.sort_order, w.asset_id, a.original_name, a.mime_type, a.byte_size
FROM osint.widgets w LEFT JOIN osint.assets a ON a.id = w.asset_id
WHERE w.level_id = $1 ORDER BY w.sort_order, w.id` , [ levelId ]),
pool . query < { document_widget_id : string ; region_key : string ; label : string ; excerpt : string ; event_date? : string } > (
`SELECT r.document_widget_id, r.region_key, r.label, r.excerpt, r.event_date::text
FROM osint.widget_regions r JOIN osint.widgets w ON w.id = r.document_widget_id
WHERE w.level_id = $1 ORDER BY r.sort_order, r.id` , [ levelId ]),
pool . query < { id : string ; from_widget_id : string ; to_widget_id : string } > (
'SELECT id, from_widget_id, to_widget_id FROM osint.level_connections WHERE level_id = $1' , [ levelId ]),
pool . query < { id : string ; from_widget_id : string ; to_widget_id : string ; relation_type : string ; sort_order : number ; config : Record < string , unknown > } > (
`SELECT id, from_widget_id, to_widget_id, relation_type, sort_order, config FROM osint.widget_relations
WHERE level_id = $1 ORDER BY sort_order, id` , [ levelId ]),
pool . query < { viewport : CaseState [ 'viewport' ]; updated_at : Date } > (
'SELECT viewport, updated_at FROM osint.playthroughs WHERE id = $1' , [ playthroughId ]),
pool . query < WidgetRow >( `SELECT id, widget_type, title, content, config, source_widget_id,
source_region_key, event_date::text, x, y, width, 0 AS sort_order
FROM osint.playthrough_widgets WHERE playthrough_id = $1 ORDER BY created_at, id` , [ playthroughId ]),
pool . query < { id : string ; from_widget_id : string ; to_widget_id : string } > (
'SELECT id, from_widget_id, to_widget_id FROM osint.playthrough_connections WHERE playthrough_id = $1' , [ playthroughId ]),
pool . query < { id : string ; from_widget_id : string ; to_widget_id : string ; relation_type : string ; sort_order : number ; config : Record < string , unknown > } > (
`SELECT id, from_widget_id, to_widget_id, relation_type, sort_order, config FROM osint.playthrough_widget_relations
WHERE playthrough_id = $1 ORDER BY sort_order, id` , [ playthroughId ]),
pool . query < { widget_id : string ; x : number ; y : number ; width : number ; hidden : boolean ; config : Record < string , unknown > } > (
'SELECT widget_id, x, y, width, hidden, config FROM osint.playthrough_widget_state WHERE playthrough_id = $1' , [ playthroughId ]),
pool . query < { relation_id : string ; config : Record < string , unknown > } > (
'SELECT relation_id, config FROM osint.playthrough_widget_relation_state WHERE playthrough_id = $1' , [ playthroughId ]),
])
const stateByWidget = new Map ( authorMode ? [] : stateResult . rows . map ( row => [ row . widget_id , row ]))
const documents : CaseDocument [] = widgetsResult . rows . filter ( w => w . widget_type === 'document' ). map ( w => { const override = stateByWidget . get ( w . id ) ? . config || {}; const publishedAt = String ( override . publishedAt || w . published_at || '' ); return ({
id : w.id , title : String ( override . title || w . title ), kind : w.config.kind || 'DOCUMENT' , date : publishedAt.slice ( 0 , 10 ) || w . config . date || '' , publishedAt : publishedAt || undefined ,
body : w.config.body || [], fileType : ( override . fileType || w . config . fileType || ( w . mime_type ? . startsWith ( 'image/' ) ? 'image' : 'file' )) as CaseDocument [ 'fileType' ], metadata : ( override . metadata || w . config . metadata || {}) as Record < string , string >,
assetId : w.asset_id , fileName : w.original_name , mimeType : w.mime_type , fileSize : w.byte_size ,
regions : regionsResult.rows.filter ( r => r . document_widget_id === w . id ). map ( r => ({ id : r.region_key , label : r.label , excerpt : r.excerpt , date : r.event_date })),
}) })
const relationState = new Map ( authorMode ? [] : relationStateResult . rows . map ( row => [ row . relation_id , row . config ]))
const containedByFolder = new Map < string , string [] >()
for ( const relation of [... authoredRelations . rows , ...( authorMode ? [] : playerRelations . rows )]) {
if ( relation . relation_type !== 'contains' ) continue
containedByFolder . set ( relation . from_widget_id , [...( containedByFolder . get ( relation . from_widget_id ) || []), relation . to_widget_id ])
}
const toEvidence = ( w : WidgetRow ) : Evidence => {
const override = stateByWidget . get ( w . id )
const runtimeConfig = { ... w . config , ...( override ? . config || {}) }
return { id : w.id , type : w . widget_type as Evidence [ 'type' ], title : String ( override ? . config ? . title || w . title ), content : String ( override ? . config ? . content ?? w . content ), config : runtimeConfig ,
sourceDocumentId : w.source_widget_id , sourceRegionId : w.source_region_key , eventDate : w.widget_type === 'event' ? w.event_date : undefined ,
containedDocumentIds : containedByFolder.get ( w . id ) || ( w . source_widget_id ? [ w . source_widget_id ] : []),
x : override?.x ?? w . x ?? 100 , y : override?.y ?? w . y ?? 100 , width : override?.width ?? w . width ?? 240 }
}
const authoredEvidence = widgetsResult . rows . filter ( w => w . widget_type !== 'document' && ! stateByWidget . get ( w . id ) ? . hidden ). map ( toEvidence )
const evidence = [... authoredEvidence , ...( authorMode ? [] : generatedResult . rows . map ( toEvidence ))]
const connections : Connection [] = [... authoredConnections . rows , ...( authorMode ? [] : playerConnections . rows )]. map ( c => ({
id : c.id , fromEvidenceId : c.from_widget_id , toEvidenceId : c.to_widget_id ,
}))
const relations : WidgetRelation [] = [... authoredRelations . rows , ...( authorMode ? [] : playerRelations . rows )]. map ( relation => ({
id : relation.id , fromWidgetId : relation.from_widget_id , toWidgetId : relation.to_widget_id , type : relation . relation_type ,
sortOrder : relation.sort_order , config : { ... relation . config , ...( relationState . get ( relation . id ) || {}) },
}))
const playthrough = playthroughResult . rows [ 0 ]
return { id : level.id , title : level.title , subtitle : level.subtitle , documents , evidence , relations , connections ,
viewport : playthrough.viewport , updatedAt : playthrough.updated_at.toISOString (), levelStatus : level.status , editingAllowed : editingEnabled }
}
async function savePlaythrough ( client : PoolClient , state : CaseState ) {
const playthroughId = `default: ${ state . id } `
const authored = await client . query < { id : string } > ( 'SELECT id FROM osint.widgets WHERE level_id = $1' , [ state . id ])
const authoredIds = new Set ( authored . rows . map ( row => row . id ))
const authoredRelations = await client . query < { id : string } > ( 'SELECT id FROM osint.widget_relations WHERE level_id = $1' , [ state . id ])
const authoredRelationIds = new Set ( authoredRelations . rows . map ( row => row . id ))
await client . query ( 'UPDATE osint.playthroughs SET viewport = $2::jsonb, updated_at = NOW() WHERE id = $1' , [ playthroughId , JSON . stringify ( state . viewport )])
await client . query ( 'DELETE FROM osint.playthrough_widget_state WHERE playthrough_id = $1' , [ playthroughId ])
await client . query ( 'DELETE FROM osint.playthrough_widget_relation_state WHERE playthrough_id = $1' , [ playthroughId ])
await client . query ( 'DELETE FROM osint.playthrough_widget_relations WHERE playthrough_id = $1' , [ playthroughId ])
await client . query ( 'DELETE FROM osint.playthrough_widgets WHERE playthrough_id = $1' , [ playthroughId ])
for ( const document of state . documents ) {
await client . query ( `INSERT INTO osint.playthrough_widget_state (playthrough_id, widget_id, x, y, width, config)
VALUES ($1,$2,0,0,0,$3::jsonb)` , [ playthroughId , document . id , JSON . stringify ({ title : document.title , publishedAt : document.publishedAt || null , fileType : document.fileType , metadata : document.metadata })])
}
for ( const widget of state . evidence ) {
if ( authoredIds . has ( widget . id )) {
await client . query ( `INSERT INTO osint.playthrough_widget_state (playthrough_id, widget_id, x, y, width, config)
VALUES ($1, $2, $3, $4, $5, $6::jsonb)` , [ playthroughId , widget . id , widget . x , widget . y , widget . width , JSON . stringify ({ ...( widget . config || {}), title : widget.title , content : widget.content })])
} else {
await client . query ( `INSERT INTO osint.playthrough_widgets
(id, playthrough_id, widget_type, title, content, config, source_widget_id, source_region_key, event_date, x, y, width)
VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7,$8,$9,$10,$11,$12)` , [ widget . id , playthroughId , widget . type , widget . title , widget . content , JSON . stringify ( widget . config || {}),
widget . sourceDocumentId || null , widget . sourceRegionId || null , widget . eventDate || null , widget . x , widget . y , widget . width ])
}
}
const fallbackRelations : WidgetRelation [] = state . evidence . flatMap ( widget => ( widget . containedDocumentIds || []). map (( documentId , index ) => ({ id : `contains: ${ widget . id } : ${ documentId } ` , fromWidgetId : widget.id , toWidgetId : documentId , type : 'contains' , sortOrder : index })))
for ( const relation of ( state . relations || fallbackRelations ). filter ( relation => authoredRelationIds . has ( relation . id ))) {
await client . query ( `INSERT INTO osint.playthrough_widget_relation_state (playthrough_id, relation_id, config)
VALUES ($1,$2,$3::jsonb)` , [ playthroughId , relation . id , JSON . stringify ( relation . config || {})])
}
for ( const relation of ( state . relations || fallbackRelations ). filter ( relation => ! authoredRelationIds . has ( relation . id ))) {
await client . query ( `INSERT INTO osint.playthrough_widget_relations
(id, playthrough_id, from_widget_id, to_widget_id, relation_type, sort_order, config)
VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb)` ,
[ relation . id , playthroughId , relation . fromWidgetId , relation . toWidgetId , relation . type , relation . sortOrder || 0 , JSON . stringify ( relation . config || {})])
}
await client . query ( 'DELETE FROM osint.playthrough_connections WHERE playthrough_id = $1' , [ playthroughId ])
const authoredConnections = await client . query < { id : string } > ( 'SELECT id FROM osint.level_connections WHERE level_id = $1' , [ state . id ])
const authoredConnectionIds = new Set ( authoredConnections . rows . map ( row => row . id ))
for ( const connection of state . connections . filter ( c => ! authoredConnectionIds . has ( c . id ))) {
await client . query ( `INSERT INTO osint.playthrough_connections (id, playthrough_id, from_widget_id, to_widget_id)
VALUES ($1, $2, $3, $4)` , [ connection . id , playthroughId , connection . fromEvidenceId , connection . toEvidenceId ])
}
}
async function saveAuthoredLevel ( client : PoolClient , state : CaseState ) {
await client . query ( 'UPDATE osint.levels SET title = $2, subtitle = $3, updated_at = NOW() WHERE id = $1' , [ state . id , state . title , state . subtitle ])
2026-08-14 12:57:06 +02:00
await client . query ( `INSERT INTO osint.playthroughs (id, level_id, viewport, updated_at)
VALUES ($1, $2, $3::jsonb, NOW())
ON CONFLICT (id) DO UPDATE SET viewport = EXCLUDED.viewport, updated_at = NOW()` ,
[ `default: ${ state . id } ` , state . id , JSON . stringify ( state . viewport )])
2026-08-14 12:43:11 +02:00
await client . query ( 'DELETE FROM osint.level_connections WHERE level_id = $1' , [ state . id ])
await client . query ( 'DELETE FROM osint.widget_relations WHERE level_id = $1' , [ state . id ])
await client . query ( 'DELETE FROM osint.widgets WHERE level_id = $1' , [ state . id ])
for ( const [ index , doc ] of state . documents . entries ()) {
await client . query ( `INSERT INTO osint.widgets (id, level_id, widget_type, title, config, published_at, asset_id, sort_order)
VALUES ($1,$2,'document',$3,$4::jsonb,$5,$6,$7)` , [ doc . id , state . id , doc . title , JSON . stringify ({ kind : doc.kind , body : doc.body , fileType : doc.fileType , metadata : doc.metadata }), doc . publishedAt || doc . date || null , doc . assetId || null , index ])
for ( const [ regionIndex , region ] of doc . regions . entries ()) {
await client . query ( `INSERT INTO osint.widget_regions (id, document_widget_id, region_key, label, excerpt, event_date, sort_order)
VALUES ($1,$2,$3,$4,$5,$6,$7)` , [ ` ${ doc . id } : ${ region . id } ` , doc . id , region . id , region . label , region . excerpt , region . date || null , regionIndex ])
}
}
for ( const [ index , widget ] of state . evidence . entries ()) {
await client . query ( `INSERT INTO osint.widgets
(id, level_id, widget_type, title, content, config, source_widget_id, source_region_key, event_date, x, y, width, sort_order)
VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7,$8,$9,$10,$11,$12,$13)` , [ widget . id , state . id , widget . type , widget . title , widget . content , JSON . stringify ( widget . config || {}),
widget . sourceDocumentId || null , widget . sourceRegionId || null , widget . eventDate || null , widget . x , widget . y , widget . width , index ])
}
const fallbackRelations : WidgetRelation [] = state . evidence . flatMap ( widget => ( widget . containedDocumentIds || []). map (( documentId , index ) => ({ id : `contains: ${ widget . id } : ${ documentId } ` , fromWidgetId : widget.id , toWidgetId : documentId , type : 'contains' , sortOrder : index })))
for ( const relation of state . relations || fallbackRelations ) {
await client . query ( `INSERT INTO osint.widget_relations
(id, level_id, from_widget_id, to_widget_id, relation_type, sort_order, config)
VALUES ($1,$2,$3,$4,$5,$6,$7::jsonb)` ,
[ relation . id , state . id , relation . fromWidgetId , relation . toWidgetId , relation . type , relation . sortOrder || 0 , JSON . stringify ( relation . config || {})])
}
for ( const connection of state . connections ) {
await client . query ( `INSERT INTO osint.level_connections (id, level_id, from_widget_id, to_widget_id)
VALUES ($1,$2,$3,$4)` , [ connection . id , state . id , connection . fromEvidenceId , connection . toEvidenceId ])
}
}
2026-08-14 12:57:06 +02:00
export const app = express ()
2026-08-14 12:43:11 +02:00
app . disable ( 'x-powered-by' )
app . use ( cors ({ origin : process.env.CORS_ORIGIN || true }))
app . use ( express . json ({ limit : '2mb' }))
const upload = multer ({
storage : multer.memoryStorage (),
limits : { fileSize : Number ( process . env . MAX_DOCUMENT_BYTES || 25 * 1024 * 1024 ), files : 1 },
})
app . get ( '/api/health' , async ( _req , res ) => {
try { await pool . query ( 'SELECT 1' ); res . json ({ ok : true , database : 'connected' , schema : 'osint' , editingEnabled }) }
catch { res . status ( 503 ). json ({ ok : false , database : 'unavailable' }) }
})
app . get ( '/api/levels' , async ( _req , res , next ) => {
try { const result = await pool . query ( 'SELECT id, title, subtitle, status, updated_at AS "updatedAt" FROM osint.levels ORDER BY updated_at DESC' ); res . json ( result . rows ) }
catch ( error ) { next ( error ) }
})
app . post ( '/api/levels' , async ( req , res , next ) => {
try {
if ( ! editingEnabled ) return res . status ( 403 ). json ({ error : 'Level editing is disabled' })
const title = String ( req . body ? . title || 'Untitled Investigation' ). trim ()
const id = String ( req . body ? . id || `level- ${ Date . now () } ` ). trim (). toLowerCase (). replace ( /[^a-z0-9-]+/g , '-' )
await pool . query ( 'INSERT INTO osint.levels (id, title, subtitle) VALUES ($1, $2, $3)' , [ id , title , String ( req . body ? . subtitle || '' )])
const level = await assembleLevel ( id , `default: ${ id } ` , true )
res . status ( 201 ). json ( level )
} catch ( error ) { next ( error ) }
})
app . get ( '/api/assets/:id' , async ( req , res , next ) => {
try {
const result = await pool . query < { original_name : string ; mime_type : string ; byte_size : string ; content : Buffer } > (
'SELECT original_name, mime_type, byte_size, content FROM osint.assets WHERE id = $1' , [ req . params . id ],
)
const asset = result . rows [ 0 ]
if ( ! asset ) return res . status ( 404 ). json ({ error : 'Asset not found' })
const inline = asset . mime_type === 'application/pdf' || asset . mime_type . startsWith ( 'image/' ) || asset . mime_type . startsWith ( 'text/' )
res . setHeader ( 'Content-Type' , asset . mime_type || 'application/octet-stream' )
res . setHeader ( 'Content-Length' , asset . byte_size )
res . setHeader ( 'Content-Disposition' , ` ${ inline ? 'inline' : 'attachment' } ; filename*=UTF-8'' ${ encodeURIComponent ( asset . original_name ) } ` )
res . setHeader ( 'X-Content-Type-Options' , 'nosniff' )
res . send ( asset . content )
} catch ( error ) { next ( error ) }
})
app . post ( '/api/levels/:id/documents' , upload . single ( 'file' ), async ( req , res , next ) => {
if ( ! wantsEdit ( req )) return res . status ( 403 ). json ({ error : 'Level editing is disabled' })
if ( ! req . file ) return res . status ( 400 ). json ({ error : 'A file is required' })
const client = await pool . connect ()
try {
const level = await client . query ( 'SELECT id FROM osint.levels WHERE id = $1' , [ req . params . id ])
if ( ! level . rows [ 0 ]) return res . status ( 404 ). json ({ error : 'Level not found' })
const assetId = `asset- ${ randomUUID () } `
const widgetId = `document- ${ randomUUID () } `
const checksum = createHash ( 'sha256' ). update ( req . file . buffer ). digest ( 'hex' )
const kind = req . file . mimetype === 'application/pdf' ? 'PDF' : req . file . mimetype . startsWith ( 'image/' ) ? 'IMAGE' : 'FILE'
const fileType : CaseDocument [ 'fileType' ] = req . file . mimetype . startsWith ( 'image/' ) ? 'image' : req . file . mimetype === 'application/pdf' ? 'pdf' : 'file'
await client . query ( 'BEGIN' )
await client . query ( `INSERT INTO osint.assets (id, level_id, original_name, mime_type, byte_size, content, checksum_sha256)
VALUES ($1,$2,$3,$4,$5,$6,$7)` , [ assetId , req . params . id , req . file . originalname , req . file . mimetype || 'application/octet-stream' , req . file . size , req . file . buffer , checksum ])
await client . query ( `INSERT INTO osint.widgets (id, level_id, widget_type, title, config, asset_id, sort_order)
VALUES ($1,$2,'document',$3,$4::jsonb,$5,(SELECT COALESCE(MAX(sort_order),-1)+1 FROM osint.widgets WHERE level_id=$2 AND widget_type='document'))` ,
[ widgetId , req . params . id , req . file . originalname , JSON . stringify ({ kind , body : [], fileType , metadata : {} }), assetId ])
await client . query ( 'UPDATE osint.levels SET updated_at = NOW() WHERE id = $1' , [ req . params . id ])
await client . query ( 'COMMIT' )
res . status ( 201 ). json ({ id : widgetId , title : req.file.originalname , kind , fileType , metadata : {}, date : '' , body : [], regions : [], assetId ,
fileName : req.file.originalname , mimeType : req.file.mimetype , fileSize : req.file.size })
} catch ( error ) { await client . query ( 'ROLLBACK' ); next ( error ) } finally { client . release () }
})
app . get ( '/api/levels/:id' , async ( req , res , next ) => {
try { const level = await assembleLevel ( req . params . id , `default: ${ req . params . id } ` , wantsEdit ( req )); level ? res . json ( level ) : res . status ( 404 ). json ({ error : 'Level not found' }) }
catch ( error ) { next ( error ) }
})
app . put ( '/api/levels/:id' , async ( req , res , next ) => {
const state = req . body as CaseState
if ( ! state || state . id !== req . params . id || ! Array . isArray ( state . evidence ) || ! Array . isArray ( state . connections )) return res . status ( 400 ). json ({ error : 'Invalid level state' })
const client = await pool . connect ()
try {
await client . query ( 'BEGIN' )
if ( wantsEdit ( req )) await saveAuthoredLevel ( client , state )
else await savePlaythrough ( client , state )
await client . query ( 'COMMIT' ); res . json ({ ok : true , mode : wantsEdit ( req ) ? 'author' : 'play' })
} catch ( error ) { await client . query ( 'ROLLBACK' ); next ( error ) } finally { client . release () }
})
app . post ( '/api/levels/:id/reset' , async ( req , res , next ) => {
const client = await pool . connect ()
try {
const playthroughId = `default: ${ req . params . id } `
await client . query ( 'BEGIN' )
await client . query ( 'DELETE FROM osint.playthroughs WHERE id = $1' , [ playthroughId ])
await client . query ( 'COMMIT' )
const level = await assembleLevel ( req . params . id ); level ? res . json ( level ) : res . status ( 404 ). json ({ error : 'Level not found' })
} catch ( error ) { await client . query ( 'ROLLBACK' ); next ( error ) } finally { client . release () }
})
app . use (( error : unknown , _req : express.Request , res : express.Response , _next : express.NextFunction ) => {
if ( error instanceof multer . MulterError ) {
return res . status ( error . code === 'LIMIT_FILE_SIZE' ? 413 : 400 ). json ({ error : error.code === 'LIMIT_FILE_SIZE' ? 'Document exceeds the upload limit' : error . message })
}
console . error ( error ); res . status ( 500 ). json ({ error : 'Internal server error' })
})
const here = path . dirname ( fileURLToPath ( import . meta . url )); const dist = path . resolve ( here , '..' , 'dist' )
if ( fs . existsSync ( dist )) { app . use ( express . static ( dist )); app . get ( '*splat' , ( _req , res ) => res . sendFile ( path . join ( dist , 'index.html' ))) }
const port = Number ( process . env . PORT || 8787 )
2026-08-14 12:57:06 +02:00
export const server = app . listen ( port , '0.0.0.0' , () => console . log ( `GUPI OSINT Board listening on http://localhost: ${ port } ` ))
2026-08-14 12:43:11 +02:00
async function shutdown() { server . close (); await pool . end (); process . exit ( 0 ) }
2026-08-14 12:57:06 +02:00
if ( ! process . env . VITEST ) {
process . on ( 'SIGTERM' , shutdown )
process . on ( 'SIGINT' , shutdown )
}