sync: Dataforth sync fixes, TestDataDB stability, and client scripts

Dataforth DOS:
- TestDataDB: singleton DB connection fix (crash prevention), WAL mode,
  WinSW service config, backup script, uncaught exception handlers
- Sync-FromNAS.ps1: Get-NASFileList temp file approach to avoid SSH
  stdout deadlock, *> $null output suppression, 8.3 filename filter
  for PUSH phase, backslash-escaped SCP paths, rename-to-.synced
- import.js: INSERT OR REPLACE for re-tested devices
- Full import run: 1,028,275 -> 1,632,793 records, indexes added
- Deploy script for sync fixes to AD2

Client scripts (temp/):
- BG Builders: Lesley account check, MFA phone update
- Lonestar Electrical: Kyla/Russ Google Workspace setup, 2FA bypass
- AD2 diagnostics and NAS connectivity tests

PENDING: Investigate why newest test_date is Jan 19 despite daily tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 20:16:24 -07:00
parent 1a26eb051a
commit 470638ff86
24 changed files with 2498 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
# backup-db.ps1
# Backs up the TestDataDB SQLite database with 7-day retention.
# Intended to run as a scheduled task via install-backup-task.ps1.
$ErrorActionPreference = 'Stop'
$SourceDb = 'C:\Shares\testdatadb\database\testdata.db'
$BackupDir = 'C:\Shares\testdatadb\backups'
$LogFile = 'C:\Shares\testdatadb\logs\backup.log'
$Retention = 7
function Write-Log {
param([string]$Message)
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$entry = "[$timestamp] $Message"
Add-Content -Path $LogFile -Value $entry
Write-Host $entry
}
# Ensure directories exist
foreach ($dir in @($BackupDir, (Split-Path $LogFile -Parent))) {
if (-not (Test-Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
}
# Verify source database exists
if (-not (Test-Path $SourceDb)) {
Write-Log "[ERROR] Source database not found: $SourceDb"
exit 1
}
# Create dated backup
$datestamp = Get-Date -Format 'yyyy-MM-dd'
$backupFile = Join-Path $BackupDir "testdata-$datestamp.db"
try {
Copy-Item -Path $SourceDb -Destination $backupFile -Force
$sizeKb = [math]::Round((Get-Item $backupFile).Length / 1024, 1)
Write-Log "[OK] Backup created: $backupFile ($sizeKb KB)"
} catch {
Write-Log "[ERROR] Backup failed: $_"
exit 1
}
# Prune old backups beyond retention period
$cutoff = (Get-Date).AddDays(-$Retention)
$oldBackups = Get-ChildItem -Path $BackupDir -Filter 'testdata-*.db' |
Where-Object { $_.LastWriteTime -lt $cutoff }
foreach ($old in $oldBackups) {
try {
Remove-Item -Path $old.FullName -Force
Write-Log "[OK] Deleted old backup: $($old.Name)"
} catch {
Write-Log "[WARNING] Could not delete old backup: $($old.Name) - $_"
}
}
$remaining = (Get-ChildItem -Path $BackupDir -Filter 'testdata-*.db').Count
Write-Log "[INFO] Backup complete. $remaining backup(s) on disk."

View File

@@ -0,0 +1,134 @@
# deploy.ps1
# Deploys the fixed TestDataDB application to AD2 (192.168.0.6).
#
# Copies files via SCP, installs dependencies, sets up the Windows service,
# and installs the backup scheduled task.
#
# Must be run from the directory containing the fixed files.
$ErrorActionPreference = 'Stop'
$RemoteHost = '192.168.0.6'
$RemoteUser = 'INTRANET\sysadmin'
$RemotePass = 'Paper123!@#'
$RemotePath = 'C:\Shares\testdatadb'
$SshExe = 'C:\Windows\System32\OpenSSH\ssh.exe'
$ScpExe = 'C:\Windows\System32\OpenSSH\scp.exe'
$LocalDir = $PSScriptRoot
# Credentials for SSH - set up sshpass-equivalent via environment
# Note: For automated deployment, the SSH key should be pre-configured.
# This script uses password-based auth via the ssh client.
$SshTarget = "${RemoteUser}@${RemoteHost}"
function Invoke-RemoteCommand {
param([string]$Command)
Write-Host "[INFO] Remote: $Command"
& $SshExe $SshTarget $Command
if ($LASTEXITCODE -ne 0) {
Write-Host "[ERROR] Remote command failed with exit code $LASTEXITCODE"
exit 1
}
}
function Copy-ToRemote {
param([string]$LocalFile, [string]$RemoteDir)
$remoteDest = "${SshTarget}:`"${RemoteDir}`""
Write-Host "[INFO] Copying $LocalFile -> $RemoteDir"
& $ScpExe $LocalFile $remoteDest
if ($LASTEXITCODE -ne 0) {
Write-Host "[ERROR] SCP failed for $LocalFile"
exit 1
}
}
Write-Host '========================================'
Write-Host 'TestDataDB Deployment to AD2'
Write-Host '========================================'
Write-Host ''
# -----------------------------------------------------------------------
# Step 1: Ensure remote directories exist
# -----------------------------------------------------------------------
Write-Host '[STEP 1] Creating remote directories...'
Invoke-RemoteCommand "if not exist `"${RemotePath}\routes`" mkdir `"${RemotePath}\routes`""
Invoke-RemoteCommand "if not exist `"${RemotePath}\logs`" mkdir `"${RemotePath}\logs`""
Invoke-RemoteCommand "if not exist `"${RemotePath}\backups`" mkdir `"${RemotePath}\backups`""
# -----------------------------------------------------------------------
# Step 2: Stop existing node process / service
# -----------------------------------------------------------------------
Write-Host ''
Write-Host '[STEP 2] Stopping existing processes...'
# Try stopping the service first (may not exist yet)
& $SshExe $SshTarget "net stop TestDataDB 2>nul & echo Service stop attempted"
# Kill any lingering node processes running server.js
& $SshExe $SshTarget "taskkill /F /FI `"IMAGENAME eq node.exe`" 2>nul & echo Process kill attempted"
Write-Host '[OK] Existing processes stopped.'
# -----------------------------------------------------------------------
# Step 3: Copy files to remote
# -----------------------------------------------------------------------
Write-Host ''
Write-Host '[STEP 3] Copying files to AD2...'
$filesToCopy = @(
@{ Local = "$LocalDir\server.js"; Remote = $RemotePath },
@{ Local = "$LocalDir\package.json"; Remote = $RemotePath },
@{ Local = "$LocalDir\install-service.js"; Remote = $RemotePath },
@{ Local = "$LocalDir\uninstall-service.js"; Remote = $RemotePath },
@{ Local = "$LocalDir\backup-db.ps1"; Remote = $RemotePath },
@{ Local = "$LocalDir\install-backup-task.ps1"; Remote = $RemotePath },
@{ Local = "$LocalDir\routes\api.js"; Remote = "$RemotePath\routes" }
)
foreach ($file in $filesToCopy) {
Copy-ToRemote -LocalFile $file.Local -RemoteDir $file.Remote
}
Write-Host '[OK] All files copied.'
# -----------------------------------------------------------------------
# Step 4: Install npm dependencies
# -----------------------------------------------------------------------
Write-Host ''
Write-Host '[STEP 4] Installing npm dependencies...'
Invoke-RemoteCommand "cd /d `"${RemotePath}`" && npm install --production"
Write-Host '[OK] Dependencies installed.'
# -----------------------------------------------------------------------
# Step 5: Uninstall old service (if present) and install new one
# -----------------------------------------------------------------------
Write-Host ''
Write-Host '[STEP 5] Installing Windows service...'
# Uninstall first to ensure clean state
& $SshExe $SshTarget "cd /d `"${RemotePath}`" && node uninstall-service.js 2>nul"
Start-Sleep -Seconds 3
Invoke-RemoteCommand "cd /d `"${RemotePath}`" && node install-service.js"
Write-Host '[OK] Windows service installed.'
# -----------------------------------------------------------------------
# Step 6: Install backup scheduled task
# -----------------------------------------------------------------------
Write-Host ''
Write-Host '[STEP 6] Installing backup scheduled task...'
Invoke-RemoteCommand "powershell -NoProfile -ExecutionPolicy Bypass -File `"${RemotePath}\install-backup-task.ps1`""
Write-Host '[OK] Backup task installed.'
# -----------------------------------------------------------------------
# Step 7: Verify service is running
# -----------------------------------------------------------------------
Write-Host ''
Write-Host '[STEP 7] Verifying service status...'
Start-Sleep -Seconds 5
& $SshExe $SshTarget "sc query TestDataDB"
Write-Host ''
Write-Host '========================================'
Write-Host '[OK] Deployment complete.'
Write-Host "[INFO] Service: TestDataDB on $RemoteHost"
Write-Host "[INFO] URL: http://${RemoteHost}:3000"
Write-Host "[INFO] Logs: ${RemotePath}\logs\"
Write-Host "[INFO] Backups: ${RemotePath}\backups\ (daily at 2 AM)"
Write-Host '========================================'

View File

@@ -0,0 +1,55 @@
# install-backup-task.ps1
# Creates a Windows Scheduled Task to run backup-db.ps1 daily at 2:00 AM.
# Must be run as Administrator.
$ErrorActionPreference = 'Stop'
$TaskName = 'TestDataDB-Backup'
$ScriptPath = 'C:\Shares\testdatadb\backup-db.ps1'
# Check for admin privileges
$principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Host '[ERROR] This script must be run as Administrator.'
exit 1
}
# Remove existing task if present
$existing = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if ($existing) {
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
Write-Host "[INFO] Removed existing task: $TaskName"
}
# Build task components
$action = New-ScheduledTaskAction `
-Execute 'powershell.exe' `
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`""
$trigger = New-ScheduledTaskTrigger -Daily -At '2:00AM'
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-StartWhenAvailable `
-RunOnlyIfNetworkAvailable:$false `
-ExecutionTimeLimit (New-TimeSpan -Minutes 30)
$taskPrincipal = New-ScheduledTaskPrincipal `
-UserId 'SYSTEM' `
-LogonType ServiceAccount `
-RunLevel Highest
# Register task
Register-ScheduledTask `
-TaskName $TaskName `
-Action $action `
-Trigger $trigger `
-Settings $settings `
-Principal $taskPrincipal `
-Description 'Daily backup of TestDataDB SQLite database with 7-day retention' |
Out-Null
Write-Host "[OK] Scheduled task '$TaskName' created."
Write-Host "[INFO] Runs daily at 2:00 AM as SYSTEM."
Write-Host "[INFO] Script: $ScriptPath"

View File

@@ -0,0 +1,51 @@
/**
* Install TestDataDB as a Windows Service
*
* Uses node-windows to register the server as a persistent service
* with automatic restart on crash.
*
* Run: node install-service.js
*/
const path = require('path');
const Service = require('node-windows').Service;
const svc = new Service({
name: 'TestDataDB',
description: 'Dataforth Test Data Database Server',
script: path.join(__dirname, 'server.js'),
nodeOptions: [],
workingDirectory: __dirname,
allowServiceLogon: true,
// Restart configuration: max 3 restarts with 5-second delay
maxRestarts: 3,
maxRetries: 3,
wait: 5,
grow: 0.5
});
// Set log directory
svc.logpath = path.join('C:', 'Shares', 'testdatadb', 'logs');
svc.on('install', () => {
console.log('[OK] TestDataDB service installed successfully.');
console.log('[INFO] Starting service...');
svc.start();
});
svc.on('start', () => {
console.log('[OK] TestDataDB service started.');
});
svc.on('alreadyinstalled', () => {
console.log('[WARNING] TestDataDB service is already installed.');
console.log('[INFO] To reinstall, run uninstall-service.js first.');
});
svc.on('error', (err) => {
console.error('[ERROR] Service installation failed:', err);
});
console.log('[INFO] Installing TestDataDB as a Windows service...');
console.log('[INFO] Log directory: C:\\Shares\\testdatadb\\logs\\');
svc.install();

View File

@@ -0,0 +1,18 @@
{
"name": "testdatadb",
"version": "1.1.0",
"description": "Test data database and search interface",
"main": "server.js",
"scripts": {
"start": "node server.js",
"import": "node database/import.js",
"install-service": "node install-service.js",
"uninstall-service": "node uninstall-service.js"
},
"dependencies": {
"better-sqlite3": "^9.4.3",
"cors": "^2.8.5",
"express": "^4.18.2",
"node-windows": "^1.0.0-beta.8"
}
}

View File

@@ -0,0 +1,363 @@
/**
* API Routes for Test Data Database
*
* Fixed version - uses a single persistent database connection instead of
* opening and closing on every request. WAL journal mode enabled for
* concurrent read support. Limit parameter capped at 1000.
*/
const express = require('express');
const path = require('path');
const Database = require('better-sqlite3');
const { generateDatasheet } = require('../templates/datasheet');
const router = express.Router();
// ---------------------------------------------------------------------------
// Singleton database connection - opened once at module load
// ---------------------------------------------------------------------------
const DB_PATH = path.join(__dirname, '..', 'database', 'testdata.db');
const db = new Database(DB_PATH, { readonly: false });
db.pragma('journal_mode = WAL');
db.pragma('busy_timeout = 5000');
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const MAX_LIMIT = 1000;
function clampLimit(value) {
const parsed = parseInt(value, 10);
if (isNaN(parsed) || parsed < 1) return 100;
return Math.min(parsed, MAX_LIMIT);
}
function clampOffset(value) {
const parsed = parseInt(value, 10);
if (isNaN(parsed) || parsed < 0) return 0;
return parsed;
}
// ---------------------------------------------------------------------------
// GET /api/search
// Search test records
// Query params: serial, model, from, to, result, q, station, logtype, limit, offset
// ---------------------------------------------------------------------------
router.get('/search', (req, res) => {
try {
const { serial, model, from, to, result, q, station, logtype } = req.query;
const limit = clampLimit(req.query.limit || 100);
const offset = clampOffset(req.query.offset || 0);
let sql = 'SELECT * FROM test_records WHERE 1=1';
const params = [];
if (serial) {
sql += ' AND serial_number LIKE ?';
params.push(serial.includes('%') ? serial : `%${serial}%`);
}
if (model) {
sql += ' AND model_number LIKE ?';
params.push(model.includes('%') ? model : `%${model}%`);
}
if (from) {
sql += ' AND test_date >= ?';
params.push(from);
}
if (to) {
sql += ' AND test_date <= ?';
params.push(to);
}
if (result) {
sql += ' AND overall_result = ?';
params.push(result.toUpperCase());
}
if (station) {
sql += ' AND test_station = ?';
params.push(station);
}
if (logtype) {
sql += ' AND log_type = ?';
params.push(logtype);
}
if (q) {
// Full-text search - rebuild query with FTS
sql = `SELECT test_records.* FROM test_records
JOIN test_records_fts ON test_records.id = test_records_fts.rowid
WHERE test_records_fts MATCH ?`;
params.length = 0;
params.push(q);
if (serial) {
sql += ' AND serial_number LIKE ?';
params.push(serial.includes('%') ? serial : `%${serial}%`);
}
if (model) {
sql += ' AND model_number LIKE ?';
params.push(model.includes('%') ? model : `%${model}%`);
}
if (station) {
sql += ' AND test_station = ?';
params.push(station);
}
if (logtype) {
sql += ' AND log_type = ?';
params.push(logtype);
}
if (result) {
sql += ' AND overall_result = ?';
params.push(result.toUpperCase());
}
if (from) {
sql += ' AND test_date >= ?';
params.push(from);
}
if (to) {
sql += ' AND test_date <= ?';
params.push(to);
}
}
sql += ' ORDER BY test_date DESC, serial_number';
sql += ' LIMIT ? OFFSET ?';
params.push(limit, offset);
const records = db.prepare(sql).all(...params);
// Get total count
let countSql = sql.replace(/SELECT .* FROM/, 'SELECT COUNT(*) as count FROM')
.replace(/ORDER BY.*$/, '');
countSql = countSql.replace(/LIMIT \? OFFSET \?/, '');
const countParams = params.slice(0, -2);
const total = db.prepare(countSql).get(...countParams);
res.json({
records,
total: total?.count || records.length,
limit,
offset
});
} catch (err) {
console.error(`[${new Date().toISOString()}] [SEARCH ERROR] ${err.message}`);
res.status(500).json({ error: err.message });
}
});
// ---------------------------------------------------------------------------
// GET /api/record/:id
// Get single record by ID
// ---------------------------------------------------------------------------
router.get('/record/:id', (req, res) => {
try {
const record = db.prepare('SELECT * FROM test_records WHERE id = ?').get(req.params.id);
if (!record) {
return res.status(404).json({ error: 'Record not found' });
}
res.json(record);
} catch (err) {
console.error(`[${new Date().toISOString()}] [RECORD ERROR] ${err.message}`);
res.status(500).json({ error: err.message });
}
});
// ---------------------------------------------------------------------------
// GET /api/datasheet/:id
// Generate datasheet for a record
// Query params: format (html, txt)
// ---------------------------------------------------------------------------
router.get('/datasheet/:id', (req, res) => {
try {
const record = db.prepare('SELECT * FROM test_records WHERE id = ?').get(req.params.id);
if (!record) {
return res.status(404).json({ error: 'Record not found' });
}
const format = req.query.format || 'html';
const datasheet = generateDatasheet(record, format);
if (format === 'html') {
res.type('html').send(datasheet);
} else {
res.type('text/plain').send(datasheet);
}
} catch (err) {
console.error(`[${new Date().toISOString()}] [DATASHEET ERROR] ${err.message}`);
res.status(500).json({ error: err.message });
}
});
// ---------------------------------------------------------------------------
// GET /api/stats
// Get database statistics
// ---------------------------------------------------------------------------
router.get('/stats', (req, res) => {
try {
const stats = {
total_records: db.prepare('SELECT COUNT(*) as count FROM test_records').get().count,
by_log_type: db.prepare(`
SELECT log_type, COUNT(*) as count
FROM test_records
GROUP BY log_type
ORDER BY count DESC
`).all(),
by_result: db.prepare(`
SELECT overall_result, COUNT(*) as count
FROM test_records
GROUP BY overall_result
`).all(),
by_station: db.prepare(`
SELECT test_station, COUNT(*) as count
FROM test_records
WHERE test_station IS NOT NULL AND test_station != ''
GROUP BY test_station
ORDER BY test_station
`).all(),
date_range: db.prepare(`
SELECT MIN(test_date) as oldest, MAX(test_date) as newest
FROM test_records
`).get(),
recent_serials: db.prepare(`
SELECT DISTINCT serial_number, model_number, test_date
FROM test_records
ORDER BY test_date DESC
LIMIT 10
`).all()
};
res.json(stats);
} catch (err) {
console.error(`[${new Date().toISOString()}] [STATS ERROR] ${err.message}`);
res.status(500).json({ error: err.message });
}
});
// ---------------------------------------------------------------------------
// GET /api/filters
// Get available filter options (test stations, log types, models)
// ---------------------------------------------------------------------------
router.get('/filters', (req, res) => {
try {
const filters = {
stations: db.prepare(`
SELECT DISTINCT test_station
FROM test_records
WHERE test_station IS NOT NULL AND test_station != ''
ORDER BY test_station
`).all().map(r => r.test_station),
log_types: db.prepare(`
SELECT DISTINCT log_type
FROM test_records
ORDER BY log_type
`).all().map(r => r.log_type),
models: db.prepare(`
SELECT DISTINCT model_number, COUNT(*) as count
FROM test_records
GROUP BY model_number
ORDER BY count DESC
LIMIT 500
`).all()
};
res.json(filters);
} catch (err) {
console.error(`[${new Date().toISOString()}] [FILTERS ERROR] ${err.message}`);
res.status(500).json({ error: err.message });
}
});
// ---------------------------------------------------------------------------
// GET /api/export
// Export search results as CSV
// ---------------------------------------------------------------------------
router.get('/export', (req, res) => {
try {
const { serial, model, from, to, result, station, logtype } = req.query;
let sql = 'SELECT * FROM test_records WHERE 1=1';
const params = [];
if (serial) {
sql += ' AND serial_number LIKE ?';
params.push(serial.includes('%') ? serial : `%${serial}%`);
}
if (model) {
sql += ' AND model_number LIKE ?';
params.push(model.includes('%') ? model : `%${model}%`);
}
if (from) {
sql += ' AND test_date >= ?';
params.push(from);
}
if (to) {
sql += ' AND test_date <= ?';
params.push(to);
}
if (result) {
sql += ' AND overall_result = ?';
params.push(result.toUpperCase());
}
if (station) {
sql += ' AND test_station = ?';
params.push(station);
}
if (logtype) {
sql += ' AND log_type = ?';
params.push(logtype);
}
sql += ' ORDER BY test_date DESC, serial_number LIMIT 10000';
const records = db.prepare(sql).all(...params);
// Generate CSV
const headers = ['id', 'log_type', 'model_number', 'serial_number', 'test_date', 'test_station', 'overall_result', 'source_file'];
let csv = headers.join(',') + '\n';
for (const record of records) {
const row = headers.map(h => {
const val = record[h] || '';
return `"${String(val).replace(/"/g, '""')}"`;
});
csv += row.join(',') + '\n';
}
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename=test_records.csv');
res.send(csv);
} catch (err) {
console.error(`[${new Date().toISOString()}] [EXPORT ERROR] ${err.message}`);
res.status(500).json({ error: err.message });
}
});
// ---------------------------------------------------------------------------
// Cleanup function for graceful shutdown
// ---------------------------------------------------------------------------
function cleanup() {
try {
db.close();
} catch (err) {
console.error(`[${new Date().toISOString()}] [CLEANUP ERROR] ${err.message}`);
}
}
module.exports = router;
module.exports.cleanup = cleanup;

View File

@@ -0,0 +1,103 @@
/**
* Test Data Database Server
* Express.js server with search API and web interface
*
* Fixed version - singleton DB connection, crash resilience,
* graceful shutdown, request logging.
*/
const express = require('express');
const cors = require('cors');
const path = require('path');
const apiRoutes = require('./routes/api');
const { cleanup } = require('./routes/api');
const app = express();
const PORT = process.env.PORT || 3000;
const HOST = '0.0.0.0';
// ---------------------------------------------------------------------------
// Crash resilience - log and continue rather than dying
// ---------------------------------------------------------------------------
process.on('uncaughtException', (err) => {
console.error(`[${new Date().toISOString()}] [UNCAUGHT EXCEPTION] ${err.stack || err.message}`);
});
process.on('unhandledRejection', (reason) => {
console.error(`[${new Date().toISOString()}] [UNHANDLED REJECTION] ${reason}`);
});
// ---------------------------------------------------------------------------
// Middleware
// ---------------------------------------------------------------------------
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
// Request logging
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(
`[${new Date().toISOString()}] ${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`
);
});
next();
});
// ---------------------------------------------------------------------------
// Routes
// ---------------------------------------------------------------------------
app.use('/api', apiRoutes);
// Serve index.html for root
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// ---------------------------------------------------------------------------
// Start server
// ---------------------------------------------------------------------------
const server = app.listen(PORT, HOST, () => {
console.log(`\n========================================`);
console.log(`Test Data Database Server`);
console.log(`========================================`);
console.log(`Server running on all interfaces (${HOST}:${PORT})`);
console.log(`Local: http://localhost:${PORT}`);
console.log(`LAN: http://192.168.0.6:${PORT}`);
console.log(`API endpoints:`);
console.log(` GET /api/search?serial=...&model=...`);
console.log(` GET /api/record/:id`);
console.log(` GET /api/datasheet/:id`);
console.log(` GET /api/stats`);
console.log(` GET /api/filters`);
console.log(` GET /api/export?format=csv&...`);
console.log(`========================================\n`);
});
// ---------------------------------------------------------------------------
// Graceful shutdown
// ---------------------------------------------------------------------------
function shutdown(signal) {
console.log(`\n[${new Date().toISOString()}] Received ${signal}. Shutting down gracefully...`);
server.close(() => {
console.log(`[${new Date().toISOString()}] HTTP server closed.`);
cleanup();
console.log(`[${new Date().toISOString()}] Database connection closed. Goodbye.`);
process.exit(0);
});
// Force exit after 10 seconds if graceful shutdown stalls
setTimeout(() => {
console.error(`[${new Date().toISOString()}] Forced shutdown after timeout.`);
cleanup();
process.exit(1);
}, 10000);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
module.exports = app;

View File

@@ -0,0 +1,24 @@
/**
* Uninstall TestDataDB Windows Service
*
* Run: node uninstall-service.js
*/
const path = require('path');
const Service = require('node-windows').Service;
const svc = new Service({
name: 'TestDataDB',
script: path.join(__dirname, 'server.js')
});
svc.on('uninstall', () => {
console.log('[OK] TestDataDB service uninstalled successfully.');
});
svc.on('error', (err) => {
console.error('[ERROR] Service uninstall failed:', err);
});
console.log('[INFO] Uninstalling TestDataDB Windows service...');
svc.uninstall();