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

28
temp/ad2-diag.ps1 Normal file
View File

@@ -0,0 +1,28 @@
# Diagnostic script for TestDataDB on AD2
Write-Output "=== Node Process ==="
Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Select-Object ProcessId, CommandLine | Format-List
Write-Output "=== HTTP Test ==="
try {
$r = Invoke-WebRequest -Uri "http://localhost:3000/" -UseBasicParsing -TimeoutSec 10
Write-Output "Root page status: $($r.StatusCode)"
Write-Output "Content length: $($r.Content.Length)"
Write-Output "First 200 chars: $($r.Content.Substring(0, [Math]::Min(200, $r.Content.Length)))"
} catch {
Write-Output "Root page ERROR: $($_.Exception.Message)"
}
Write-Output "`n=== API Test ==="
try {
$r = Invoke-WebRequest -Uri "http://localhost:3000/api/stats" -UseBasicParsing -TimeoutSec 10
Write-Output "API status: $($r.StatusCode)"
Write-Output "First 200 chars: $($r.Content.Substring(0, [Math]::Min(200, $r.Content.Length)))"
} catch {
Write-Output "API ERROR: $($_.Exception.Message)"
}
Write-Output "`n=== Service Log Files ==="
Get-ChildItem "C:\Shares\testdatadb\logs\" -ErrorAction SilentlyContinue | Format-Table Name, Length, LastWriteTime
Write-Output "`n=== Recent Event Log ==="
Get-EventLog -LogName Application -Newest 5 -Source "*node*" -ErrorAction SilentlyContinue | Format-List

51
temp/bgb-lesley-check.ps1 Normal file
View File

@@ -0,0 +1,51 @@
# Check Lesley's email activity since disable
$ErrorActionPreference = "Stop"
$lesleyUPN = "lesley@bgbuildersllc.com"
Import-Module ExchangeOnlineManagement
Connect-ExchangeOnline -UserPrincipalName "sysadmin@bgbuildersllc.com" -ShowBanner:$false
$startDate = (Get-Date).AddDays(-3)
$endDate = Get-Date
Write-Output "=== MAILBOX STATUS ==="
$mbx = Get-Mailbox -Identity $lesleyUPN
$stats = Get-MailboxStatistics -Identity $lesleyUPN
Write-Output "Type: $($mbx.RecipientTypeDetails)"
Write-Output "LitigationHold: $($mbx.LitigationHoldEnabled)"
Write-Output "ItemCount: $($stats.ItemCount)"
Write-Output "TotalSize: $($stats.TotalItemSize)"
Write-Output "`n=== SENT MESSAGES (last 3 days) ==="
$sent = Get-MessageTraceV2 -SenderAddress $lesleyUPN -StartDate $startDate -EndDate $endDate
if ($sent) {
$sent | Format-Table Received,RecipientAddress,Subject -AutoSize
} else {
Write-Output "None found"
}
Write-Output "`n=== RECEIVED MESSAGES (last 3 days) ==="
$recv = Get-MessageTraceV2 -RecipientAddress $lesleyUPN -StartDate $startDate -EndDate $endDate
if ($recv) {
$recv | Select-Object -First 20 | Format-Table Received,SenderAddress,Subject -AutoSize
} else {
Write-Output "None found"
}
Write-Output "`n=== INBOX RULES ==="
$rules = Get-InboxRule -Mailbox $lesleyUPN
if ($rules) {
$rules | Format-Table Name,Enabled,Description -AutoSize
} else {
Write-Output "No inbox rules"
}
Write-Output "`n=== FORWARDING CONFIG ==="
Write-Output "ForwardingAddress: $($mbx.ForwardingAddress)"
Write-Output "ForwardingSmtpAddress: $($mbx.ForwardingSmtpAddress)"
Write-Output "DeliverToMailboxAndForward: $($mbx.DeliverToMailboxAndForward)"
Write-Output "`n=== FOLDER ITEM COUNTS ==="
Get-MailboxFolderStatistics -Identity $lesleyUPN | Where-Object { $_.ItemsInFolder -gt 0 } | Sort-Object ItemsInFolder -Descending | Select-Object -First 15 | Format-Table Name,FolderType,ItemsInFolder,FolderSize -AutoSize
Disconnect-ExchangeOnline -Confirm:$false

View File

@@ -0,0 +1,52 @@
# Update MFA phone number for Lesley Roth @ BG Builders
$ErrorActionPreference = "Stop"
$lesleyUPN = "lesley@bgbuildersllc.com"
$newPhone = "+1 4804954511"
$tenantId = "ededa4fb-f6eb-4398-851d-5eb3e11fab27"
Import-Module Microsoft.Graph.Authentication
Import-Module Microsoft.Graph.Users
Connect-MgGraph -TenantId $tenantId -Scopes 'UserAuthenticationMethod.ReadWrite.All','User.ReadWrite.All' -NoWelcome
Write-Output "=== Current Auth Methods for Lesley ==="
$methods = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/users/$lesleyUPN/authentication/phoneMethods"
if ($methods.value.Count -gt 0) {
foreach ($m in $methods.value) {
Write-Output " ID: $($m.id) | Type: $($m.phoneType) | Number: $($m.phoneNumber)"
}
} else {
Write-Output " No phone methods registered"
}
Write-Output "`n=== Updating MFA Phone ==="
# Phone method ID for mobile is always "3179e48a-750b-4051-897c-87b9720928f7"
$mobileMethodId = "3179e48a-750b-4051-897c-87b9720928f7"
try {
# Try to update existing mobile phone method
Invoke-MgGraphRequest -Method PUT -Uri "https://graph.microsoft.com/v1.0/users/$lesleyUPN/authentication/phoneMethods/$mobileMethodId" -Body @{
phoneNumber = $newPhone
phoneType = "mobile"
}
Write-Output "[OK] Mobile phone updated to $newPhone"
} catch {
Write-Output "[INFO] PUT failed, trying POST to create new method..."
try {
Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/users/$lesleyUPN/authentication/phoneMethods" -Body @{
phoneNumber = $newPhone
phoneType = "mobile"
}
Write-Output "[OK] Mobile phone created: $newPhone"
} catch {
Write-Output "[ERROR] Failed: $_"
}
}
Write-Output "`n=== Verify Updated Methods ==="
$methods = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/users/$lesleyUPN/authentication/phoneMethods"
foreach ($m in $methods.value) {
Write-Output " ID: $($m.id) | Type: $($m.phoneType) | Number: $($m.phoneNumber)"
}
Disconnect-MgGraph

View File

@@ -0,0 +1,26 @@
# Update MFA phone number for Lesley Roth @ BG Builders
$ErrorActionPreference = "Stop"
$lesleyUPN = "lesley@bgbuildersllc.com"
$newPhone = "+1 4804954511"
$tenantId = "ededa4fb-f6eb-4398-851d-5eb3e11fab27"
Import-Module Microsoft.Graph.Authentication
Connect-MgGraph -TenantId $tenantId -Scopes 'UserAuthenticationMethod.ReadWrite.All' -NoWelcome
$mobileMethodId = "3179e48a-750b-4051-897c-87b9720928f7"
Write-Output "Current: +1 4802299138"
Write-Output "Changing to: $newPhone"
$body = @{ phoneNumber = $newPhone; phoneType = "mobile" } | ConvertTo-Json
Invoke-MgGraphRequest -Method PUT -Uri "https://graph.microsoft.com/v1.0/users/$lesleyUPN/authentication/phoneMethods/$mobileMethodId" -Body $body -ContentType "application/json"
Write-Output "[OK] Phone updated"
# Verify
$methods = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/users/$lesleyUPN/authentication/phoneMethods"
foreach ($m in $methods.value) {
Write-Output "Verified: $($m.phoneType) = $($m.phoneNumber)"
}
Disconnect-MgGraph

View File

@@ -0,0 +1,52 @@
"""Generate backup codes for office@lonestarelectrical.net so Kyla can bypass 2FA enrollment block"""
from google.oauth2 import service_account
from googleapiclient.discovery import build
SCOPES = [
'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.user.security',
]
creds = service_account.Credentials.from_service_account_file(
'temp/acg-msp-access-8f72339997e5.json', scopes=SCOPES
)
delegated = creds.with_subject('sysadmin@lonestarelectrical.net')
service = build('admin', 'directory_v1', credentials=delegated)
user_email = 'office@lonestarelectrical.net'
# Check current 2SV status
print(f"=== {user_email} 2SV Status ===")
user = service.users().get(userKey=user_email).execute()
print(f"2SV Enrolled: {user.get('isEnrolledIn2Sv', False)}")
print(f"2SV Enforced: {user.get('isEnforcedIn2Sv', False)}")
# Generate backup verification codes
print(f"\n=== Generating Backup Codes ===")
try:
codes = service.verificationCodes().generate(userKey=user_email).execute()
print("[OK] Backup codes generated")
except Exception as e:
print(f"[INFO] Generate returned: {e}")
# List the codes
try:
result = service.verificationCodes().list(userKey=user_email).execute()
backup_codes = result.get('items', [])
if backup_codes:
print(f"\nBackup codes for Kyla to use at login:")
for code in backup_codes:
status = code.get('etag', '')
print(f" {code.get('verificationCode', 'N/A')}")
print(f"\nInstructions for Kyla:")
print(f" 1. Go to https://accounts.google.com")
print(f" 2. Enter email: {user_email}")
print(f" 3. Enter the temp password we set")
print(f" 4. When prompted for 2FA, click 'Try another way'")
print(f" 5. Select 'Enter a backup code'")
print(f" 6. Use one of the codes above")
print(f" 7. Once logged in, go to Security > 2-Step Verification to set up her phone")
else:
print("[WARNING] No codes returned")
except Exception as e:
print(f"[ERROR] Could not list codes: {e}")

View File

@@ -0,0 +1,60 @@
"""Reset password for office@lonestarelectrical.net so Kyla can login and set up MFA"""
import secrets
import string
from google.oauth2 import service_account
from googleapiclient.discovery import build
SCOPES = [
'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.user.security',
]
creds = service_account.Credentials.from_service_account_file(
'temp/acg-msp-access-8f72339997e5.json', scopes=SCOPES
)
delegated = creds.with_subject('sysadmin@lonestarelectrical.net')
service = build('admin', 'directory_v1', credentials=delegated)
user_email = 'office@lonestarelectrical.net'
# Check current user status
print(f"=== Checking {user_email} ===")
try:
user = service.users().get(userKey=user_email).execute()
print(f"Name: {user.get('name', {}).get('fullName', 'N/A')}")
print(f"Suspended: {user.get('suspended', 'N/A')}")
print(f"Archived: {user.get('archived', 'N/A')}")
print(f"2FA Enrolled: {user.get('isEnrolledIn2Sv', 'N/A')}")
print(f"2FA Enforced: {user.get('isEnforcedIn2Sv', 'N/A')}")
print(f"Last Login: {user.get('lastLoginTime', 'N/A')}")
print(f"Creation: {user.get('creationTime', 'N/A')}")
except Exception as e:
print(f"[ERROR] Could not get user: {e}")
exit(1)
# Generate a temp password
alphabet = string.ascii_letters + string.digits + "!@#$"
temp_pass = ''.join(secrets.choice(alphabet) for _ in range(16))
# Reset password, require change on next login
print(f"\n=== Resetting password ===")
try:
service.users().update(
userKey=user_email,
body={
'password': temp_pass,
'changePasswordAtNextLogin': True,
'suspended': False,
}
).execute()
print(f"[OK] Password reset successful")
print(f"[OK] Account unsuspended (if it was)")
print(f"[OK] Must change password on first login")
print(f"\nTemporary password: {temp_pass}")
print(f"\nGive Kyla:")
print(f" Email: {user_email}")
print(f" Password: {temp_pass}")
print(f" URL: https://accounts.google.com")
print(f" She will be prompted to change password and set up MFA")
except Exception as e:
print(f"[ERROR] Password reset failed: {e}")

View File

@@ -0,0 +1,25 @@
"""Reset password for office@lonestarelectrical.net - attempt 2, no force change"""
from google.oauth2 import service_account
from googleapiclient.discovery import build
SCOPES = ['https://www.googleapis.com/auth/admin.directory.user']
creds = service_account.Credentials.from_service_account_file(
'temp/acg-msp-access-8f72339997e5.json', scopes=SCOPES
)
delegated = creds.with_subject('sysadmin@lonestarelectrical.net')
service = build('admin', 'directory_v1', credentials=delegated)
user_email = 'office@lonestarelectrical.net'
new_pass = 'LoneStar2026!!'
service.users().update(
userKey=user_email,
body={
'password': new_pass,
'changePasswordAtNextLogin': False,
}
).execute()
print(f"[OK] Password reset for {user_email}")
print(f"Password: {new_pass}")

View File

@@ -0,0 +1,41 @@
"""Reset password and generate backup codes for russ@lonestarelectrical.net"""
from google.oauth2 import service_account
from googleapiclient.discovery import build
SCOPES = [
'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.user.security',
]
creds = service_account.Credentials.from_service_account_file(
'temp/acg-msp-access-8f72339997e5.json', scopes=SCOPES
)
delegated = creds.with_subject('sysadmin@lonestarelectrical.net')
service = build('admin', 'directory_v1', credentials=delegated)
user_email = 'russ@lonestarelectrical.net'
# Check user
print(f"=== {user_email} ===")
user = service.users().get(userKey=user_email).execute()
print(f"Name: {user.get('name', {}).get('fullName', 'N/A')}")
print(f"2SV Enrolled: {user.get('isEnrolledIn2Sv', False)}")
print(f"2SV Enforced: {user.get('isEnforcedIn2Sv', False)}")
print(f"Last Login: {user.get('lastLoginTime', 'N/A')}")
# Reset password
new_pass = 'LoneStar2026!!'
service.users().update(
userKey=user_email,
body={'password': new_pass, 'changePasswordAtNextLogin': False, 'suspended': False}
).execute()
print(f"\n[OK] Password reset: {new_pass}")
# Generate backup codes
service.verificationCodes().generate(userKey=user_email).execute()
result = service.verificationCodes().list(userKey=user_email).execute()
codes = result.get('items', [])
if codes:
print(f"\nBackup codes:")
for c in codes:
print(f" {c.get('verificationCode')}")

19
temp/test-ad2-web.ps1 Normal file
View File

@@ -0,0 +1,19 @@
$SshExe = 'C:\Windows\System32\OpenSSH\ssh.exe'
$SshTarget = 'INTRANET\sysadmin@192.168.0.6'
# Create a test script on AD2
$testScript = @'
try {
$r = Invoke-WebRequest -Uri "http://localhost:3000/api/stats" -UseBasicParsing -TimeoutSec 5
Write-Output "STATUS: $($r.StatusCode)"
Write-Output "CONTENT: $($r.Content.Substring(0, [Math]::Min(200, $r.Content.Length)))"
} catch {
Write-Output "ERROR: $($_.Exception.Message)"
}
'@
# Write test script to AD2
$testScript | & $SshExe $SshTarget 'powershell -Command "Set-Content -Path C:\Shares\testdatadb\test-web.ps1 -Value (Get-Content -Raw -Path -)"'
# Actually, simpler - just run inline
& $SshExe $SshTarget 'powershell -NoProfile -ExecutionPolicy Bypass -Command "try { $r = Invoke-WebRequest -Uri http://localhost:3000/ -UseBasicParsing -TimeoutSec 5; Write-Output STATUS:$($r.StatusCode) } catch { Write-Output ERROR:$($_.Exception.Message) }"'

3
temp/test-minimal.ps1 Normal file
View File

@@ -0,0 +1,3 @@
# Minimal test - just echo to NAS
$r = & "C:\Program Files\OpenSSH\ssh.exe" -i C:\Users\sysadmin\.ssh\id_ed25519 -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new root@192.168.0.9 "echo MINIMAL_TEST_OK" 2>&1
Write-Host "Result: $r"

View File

@@ -0,0 +1,48 @@
# Test script to run ON AD2 - diagnoses NAS SSH hang issue
$SSH = "C:\Program Files\OpenSSH\ssh.exe"
$SSH_KEY = "C:\Users\sysadmin\.ssh\id_ed25519"
$NAS_USER = "root"
$NAS_IP = "192.168.0.9"
Write-Host "=== Step 1: Kill any hung SSH processes ==="
Get-Process ssh -ErrorAction SilentlyContinue | ForEach-Object {
Write-Host " Killing SSH PID $($_.Id)"
Stop-Process -Id $_.Id -Force
}
Get-Process powershell -ErrorAction SilentlyContinue | Where-Object { $_.Id -ne $PID } | ForEach-Object {
Write-Host " Other PowerShell PID $($_.Id) - CommandLine: $($_.CommandLine)"
}
Write-Host "`n=== Step 2: Basic SSH echo test ==="
$t1 = Get-Date
$r1 = & $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${NAS_USER}@${NAS_IP}" "echo NAS_OK" 2>&1
$d1 = (Get-Date) - $t1
Write-Host " Result: $r1 (took $($d1.TotalSeconds)s)"
Write-Host "`n=== Step 3: find with temp file redirect (the actual fix) ==="
$t2 = Get-Date
Write-Host " Running find with output to /tmp/test-list.txt..."
# This is exactly what Get-NASFileList does - output goes to file on NAS, stdout discarded
& $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${NAS_USER}@${NAS_IP}" "find /data/test/TS-*/LOGS -name '*.DAT' -type f -mmin -1440 > /tmp/test-list.txt 2>/dev/null" *> $null
$d2 = (Get-Date) - $t2
Write-Host " SSH returned in $($d2.TotalSeconds)s"
Write-Host "`n=== Step 4: Count files found ==="
$r3 = & $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 "${NAS_USER}@${NAS_IP}" "wc -l /tmp/test-list.txt; head -3 /tmp/test-list.txt" 2>&1
foreach ($line in $r3) { Write-Host " $line" }
Write-Host "`n=== Step 5: Pull file list via SCP ==="
$localTemp = "$env:TEMP\test-nas-filelist.txt"
& "C:\Program Files\OpenSSH\scp.exe" -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new "${NAS_USER}@${NAS_IP}:/tmp/test-list.txt" "$localTemp" *> $null
if (Test-Path $localTemp) {
$lines = Get-Content $localTemp | Where-Object { $_.Trim() -ne '' }
Write-Host " Downloaded $($lines.Count) file paths"
Remove-Item $localTemp -ErrorAction SilentlyContinue
} else {
Write-Host " ERROR: SCP failed to download file"
}
Write-Host "`n=== Step 6: Cleanup ==="
& $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 "${NAS_USER}@${NAS_IP}" "rm -f /tmp/test-list.txt" 2>&1 | Out-Null
Write-Host "`n=== DONE ==="

32
temp/test-nas-v2.ps1 Normal file
View File

@@ -0,0 +1,32 @@
# Run ON AD2: Tests NAS SSH operations
# Deploy: scp test-nas-v2.ps1 AD2:C:\Shares\testdatadb\
# Run: powershell -NoProfile -ExecutionPolicy Bypass -File C:\Shares\testdatadb\test-nas-v2.ps1
$SSH = "C:\Program Files\OpenSSH\ssh.exe"
$SSH_KEY = "C:\Users\sysadmin\.ssh\id_ed25519"
Write-Host "=== Test A: echo ==="
$t = Get-Date
$r = & $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new root@192.168.0.9 "echo NAS_OK" 2>&1
Write-Host " Result: $r ($(((Get-Date)-$t).TotalSeconds)s)"
Write-Host "=== Test B: ls data dir ==="
$t = Get-Date
$r = & $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 root@192.168.0.9 "ls /data/test/ | head -5" 2>&1
foreach ($l in $r) { Write-Host " $l" }
Write-Host " ($(((Get-Date)-$t).TotalSeconds)s)"
Write-Host "=== Test C: find with wc -l only ==="
$t = Get-Date
$r = & $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 root@192.168.0.9 "find /data/test/TS-*/LOGS -name '*.DAT' -type f -mmin -1440 2>/dev/null | wc -l" 2>&1
Write-Host " Count: $r ($(((Get-Date)-$t).TotalSeconds)s)"
Write-Host "=== Test D: find to temp file ==="
$t = Get-Date
& $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 root@192.168.0.9 "find /data/test/TS-*/LOGS -name '*.DAT' -type f -mmin -1440 > /tmp/test-list.txt 2>/dev/null" *> $null
Write-Host " ($(((Get-Date)-$t).TotalSeconds)s)"
Write-Host "=== Test E: check temp file ==="
$r = & $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 root@192.168.0.9 "wc -l /tmp/test-list.txt; rm -f /tmp/test-list.txt" 2>&1
Write-Host " $r"
Write-Host "=== ALL DONE ==="

15
temp/testdatadb.xml Normal file
View File

@@ -0,0 +1,15 @@
<service>
<id>testdatadb</id>
<name>TestDataDB</name>
<description>Dataforth Test Data Database Server</description>
<executable>C:\Program Files\nodejs\node.exe</executable>
<argument>C:\Shares\testdatadb\server.js</argument>
<logpath>C:\Shares\testdatadb\logs</logpath>
<logmode>rotate</logmode>
<stoptimeout>15sec</stoptimeout>
<workingdirectory>C:\Shares\testdatadb</workingdirectory>
<onfailure action="restart" delay="5 sec"/>
<onfailure action="restart" delay="10 sec"/>
<onfailure action="restart" delay="30 sec"/>
<resetfailure>1 hour</resetfailure>
</service>