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:
61
projects/dataforth-dos/testdatadb-fix/backup-db.ps1
Normal file
61
projects/dataforth-dos/testdatadb-fix/backup-db.ps1
Normal 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."
|
||||
134
projects/dataforth-dos/testdatadb-fix/deploy.ps1
Normal file
134
projects/dataforth-dos/testdatadb-fix/deploy.ps1
Normal 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 '========================================'
|
||||
@@ -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"
|
||||
51
projects/dataforth-dos/testdatadb-fix/install-service.js
Normal file
51
projects/dataforth-dos/testdatadb-fix/install-service.js
Normal 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();
|
||||
18
projects/dataforth-dos/testdatadb-fix/package.json
Normal file
18
projects/dataforth-dos/testdatadb-fix/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
363
projects/dataforth-dos/testdatadb-fix/routes/api.js
Normal file
363
projects/dataforth-dos/testdatadb-fix/routes/api.js
Normal 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;
|
||||
103
projects/dataforth-dos/testdatadb-fix/server.js
Normal file
103
projects/dataforth-dos/testdatadb-fix/server.js
Normal 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;
|
||||
24
projects/dataforth-dos/testdatadb-fix/uninstall-service.js
Normal file
24
projects/dataforth-dos/testdatadb-fix/uninstall-service.js
Normal 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();
|
||||
Reference in New Issue
Block a user