Skip to content

Commit db18a70

Browse files
cdburgess75claude
andcommitted
Add read-only fleet health-check script
Standalone diagnostic to run from Datto/GPO across the fleet. Reports, per box, whether the agentless loop is intact BEFORE the dashboard would show it drifting stale: scheduled task present+enabled, config.json valid, last run recency vs configured interval, installed version. Exit 0/1/2 = healthy/degraded/unhealthy so Datto can flag non-zero. Reads only; sends nothing. Motivated by the self-deleted-task drift that went unseen ~12d. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 300457d commit db18a70

1 file changed

Lines changed: 115 additions & 0 deletions

File tree

deploy/healthcheck.ps1

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# ==============================================================================
2+
# ShellKnight - Health Check (read-only)
3+
# ==============================================================================
4+
# Confirms a box is actually self-sufficient. Run it from Datto RMM (or a GPO /
5+
# any remote-exec) across the fleet and eyeball the output: it tells you, per
6+
# machine, whether the agentless loop is intact BEFORE the dashboard would
7+
# otherwise show it drifting stale days later.
8+
#
9+
# Checks: PASS/WARN/FAIL
10+
# * Scheduled task 'ShellKnight' present + enabled (+ next run time)
11+
# * config.json present + valid (has Battlefield URL + key; shows site/interval)
12+
# * Last run recency (newest JSON report vs the configured interval)
13+
# * Installed version (from the newest JSON report)
14+
#
15+
# Exit code: 0 = healthy 1 = warning(s) 2 = failure(s)
16+
# (so Datto can flag non-zero results automatically)
17+
#
18+
# This script only READS. It changes nothing and sends nothing.
19+
# ==============================================================================
20+
21+
$Root = 'C:\ProgramData\ShellKnight'
22+
$ConfigPath = Join-Path $Root 'config.json'
23+
$JsonDir = Join-Path $Root 'JSON'
24+
$TaskName = 'ShellKnight'
25+
26+
$fails = 0; $warns = 0
27+
$lines = @()
28+
function Add-Line([string]$state, [string]$label, [string]$detail) {
29+
$script:lines += (' [{0}] {1,-22}: {2}' -f $state, $label, $detail)
30+
if ($state -eq 'FAIL') { $script:fails++ }
31+
elseif ($state -eq 'WARN') { $script:warns++ }
32+
}
33+
34+
# --- config.json ---------------------------------------------------------------
35+
$cfg = $null
36+
$scheduleHours = 8
37+
if (Test-Path $ConfigPath) {
38+
try {
39+
$cfg = Get-Content -LiteralPath $ConfigPath -Raw | ConvertFrom-Json
40+
$hasUrl = [bool]$cfg.BattlefieldURL
41+
$hasKey = [bool]$cfg.BattlefieldApiKey
42+
if ($cfg.ScheduleHours) { $scheduleHours = [int]$cfg.ScheduleHours }
43+
if ($hasUrl -and $hasKey) {
44+
$site = if ($cfg.SiteName) { $cfg.SiteName } else { '(none - falls back to domain)' }
45+
Add-Line 'PASS' 'config.json' "valid | site '$site' | every ${scheduleHours}h"
46+
} else {
47+
Add-Line 'FAIL' 'config.json' 'present but missing Battlefield URL or key -> re-bootstrap'
48+
}
49+
} catch {
50+
Add-Line 'FAIL' 'config.json' "present but unreadable/corrupt -> re-bootstrap ($($_.Exception.Message))"
51+
}
52+
} else {
53+
Add-Line 'FAIL' 'config.json' 'MISSING -> never onboarded here; run the install command'
54+
}
55+
56+
# --- scheduled task ------------------------------------------------------------
57+
$taskState = $null; $nextRun = $null
58+
try {
59+
$t = Get-ScheduledTask -TaskName $TaskName -ErrorAction Stop
60+
$taskState = "$($t.State)"
61+
try { $nextRun = (Get-ScheduledTaskInfo -TaskName $TaskName -ErrorAction Stop).NextRunTime } catch {}
62+
} catch {
63+
# Fallback for older hosts without the ScheduledTasks module.
64+
$raw = & schtasks.exe /Query /TN $TaskName /FO LIST /V 2>$null
65+
if ($LASTEXITCODE -eq 0 -and $raw) {
66+
$taskState = 'Present'
67+
$nr = ($raw | Select-String 'Next Run Time:') -replace '.*Next Run Time:\s*', ''
68+
if ($nr) { $nextRun = "$nr".Trim() }
69+
}
70+
}
71+
if (-not $taskState) {
72+
Add-Line 'FAIL' "task '$TaskName'" 'MISSING -> not self-scheduling; re-bootstrap to restore it'
73+
} elseif ($taskState -eq 'Disabled') {
74+
Add-Line 'FAIL' "task '$TaskName'" 'present but DISABLED -> enable or re-bootstrap'
75+
} else {
76+
$nr = if ($nextRun) { "next run $nextRun" } else { 'next run unknown' }
77+
Add-Line 'PASS' "task '$TaskName'" "$taskState, $nr"
78+
}
79+
80+
# --- last run recency + version (from newest JSON report) ----------------------
81+
$newest = $null
82+
if (Test-Path $JsonDir) {
83+
$newest = Get-ChildItem -LiteralPath $JsonDir -Filter '*.json' -ErrorAction SilentlyContinue |
84+
Sort-Object LastWriteTime -Descending | Select-Object -First 1
85+
}
86+
if ($newest) {
87+
$ageH = [math]::Round(((Get-Date) - $newest.LastWriteTime).TotalHours, 1)
88+
$when = $newest.LastWriteTime.ToString('yyyy-MM-dd HH:mm')
89+
# Allow up to 2x the interval + a little slack before calling it late.
90+
$threshold = ($scheduleHours * 2) + 2
91+
if ($ageH -gt $threshold) {
92+
Add-Line 'WARN' 'last run' "$when (${ageH}h ago) -> exceeds ${threshold}h; box may be offline or task not firing"
93+
} else {
94+
Add-Line 'PASS' 'last run' "$when (${ageH}h ago)"
95+
}
96+
$ver = $null
97+
try { $ver = (Get-Content -LiteralPath $newest.FullName -Raw | ConvertFrom-Json).version } catch {}
98+
if ($ver) { Add-Line 'PASS' 'installed version' "$ver" }
99+
else { Add-Line 'WARN' 'installed version' 'unknown (report had no version field)' }
100+
} else {
101+
Add-Line 'WARN' 'last run' 'no run reports found yet (has it completed a run?)'
102+
}
103+
104+
# --- verdict -------------------------------------------------------------------
105+
$result = if ($fails) { 'UNHEALTHY' } elseif ($warns) { 'DEGRADED' } else { 'HEALTHY' }
106+
$stamp = (Get-Date).ToString('yyyy-MM-dd HH:mm')
107+
Write-Host ''
108+
Write-Host " ShellKnight Health Check | $env:COMPUTERNAME | $stamp"
109+
Write-Host ' ------------------------------------------------------------------------'
110+
$lines | ForEach-Object { Write-Host $_ }
111+
Write-Host ' ------------------------------------------------------------------------'
112+
Write-Host " RESULT: $result ($fails fail, $warns warn)"
113+
Write-Host ''
114+
115+
if ($fails) { exit 2 } elseif ($warns) { exit 1 } else { exit 0 }

0 commit comments

Comments
 (0)