Skip to content

Commit 7f8112b

Browse files
committed
Refactor repo remote handling and enhance error messaging for better clarity
1 parent 542c639 commit 7f8112b

File tree

1 file changed

+253
-0
lines changed

1 file changed

+253
-0
lines changed

tools/start_dashboard_widget.ps1

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
$ErrorActionPreference = "Stop"
2+
3+
# Starts the Local Nexus Controller server and opens a small dashboard window
4+
# in the bottom-left corner of the primary monitor.
5+
#
6+
# Intended to be used at user logon (Startup shortcut / HKCU Run).
7+
8+
function Write-WidgetLog([string]$projectRoot, [string]$message) {
9+
try {
10+
$logDir = Join-Path $projectRoot "data\logs"
11+
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
12+
$logPath = Join-Path $logDir "dashboard-widget.log"
13+
$ts = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss.fff")
14+
Add-Content -Path $logPath -Value "[$ts] $message"
15+
} catch {
16+
# Best-effort logging only.
17+
}
18+
}
19+
20+
function Enter-WidgetLock {
21+
if ($env:LOCAL_NEXUS_WIDGET_FORCE -eq "1") { return $true }
22+
23+
$lockDir = Join-Path $env:LocalAppData "LocalNexusController"
24+
New-Item -ItemType Directory -Force -Path $lockDir | Out-Null
25+
$lockPath = Join-Path $lockDir "dashboard-widget.lock"
26+
27+
# If we ran very recently (e.g., both Startup shortcut and HKCU Run), exit.
28+
if (Test-Path $lockPath) {
29+
$age = (Get-Date) - (Get-Item $lockPath).LastWriteTime
30+
if ($age.TotalSeconds -lt 45) { return $false }
31+
}
32+
33+
Set-Content -Path $lockPath -Value (Get-Date).ToString("o")
34+
return $true
35+
}
36+
37+
function Resolve-ProjectRoot {
38+
$here = Split-Path -Parent $PSCommandPath
39+
return (Resolve-Path (Join-Path $here "..")).Path
40+
}
41+
42+
function Get-PythonExe([string]$projectRoot) {
43+
$venvPython = Join-Path $projectRoot ".venv\Scripts\python.exe"
44+
if (Test-Path $venvPython) { return $venvPython }
45+
$py = (Get-Command py -ErrorAction SilentlyContinue)
46+
if ($py) { return $py.Source }
47+
$python = (Get-Command python -ErrorAction SilentlyContinue)
48+
if ($python) { return $python.Source }
49+
throw "Python not found. Run .\run.ps1 once to create the venv."
50+
}
51+
52+
function Get-BrowserCmd {
53+
foreach ($name in @("msedge", "chrome", "brave")) {
54+
$cmd = Get-Command $name -ErrorAction SilentlyContinue
55+
if ($cmd) { return $cmd.Source }
56+
}
57+
58+
$candidates = @(
59+
(Join-Path $env:ProgramFiles "Microsoft\Edge\Application\msedge.exe"),
60+
(Join-Path ${env:ProgramFiles(x86)} "Microsoft\Edge\Application\msedge.exe"),
61+
(Join-Path $env:LocalAppData "Microsoft\Edge\Application\msedge.exe"),
62+
(Join-Path $env:ProgramFiles "Google\Chrome\Application\chrome.exe"),
63+
(Join-Path ${env:ProgramFiles(x86)} "Google\Chrome\Application\chrome.exe"),
64+
(Join-Path $env:LocalAppData "Google\Chrome\Application\chrome.exe"),
65+
(Join-Path $env:ProgramFiles "BraveSoftware\Brave-Browser\Application\brave.exe"),
66+
(Join-Path ${env:ProgramFiles(x86)} "BraveSoftware\Brave-Browser\Application\brave.exe"),
67+
(Join-Path $env:LocalAppData "BraveSoftware\Brave-Browser\Application\brave.exe")
68+
)
69+
70+
foreach ($p in $candidates) {
71+
if ($p -and (Test-Path $p)) { return $p }
72+
}
73+
74+
throw "No supported browser found (Edge/Chrome/Brave). Install one, or add it to PATH."
75+
}
76+
77+
function Get-WorkingArea {
78+
Add-Type -AssemblyName System.Windows.Forms | Out-Null
79+
return [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea
80+
}
81+
82+
function Start-Server([string]$pythonExe, [string]$projectRoot) {
83+
$logDir = Join-Path $projectRoot "data\logs"
84+
New-Item -ItemType Directory -Force -Path $logDir | Out-Null
85+
$outLogPath = Join-Path $logDir "local-nexus-startup.stdout.log"
86+
$errLogPath = Join-Path $logDir "local-nexus-startup.stderr.log"
87+
88+
# We open the dashboard window ourselves (in a specific size/position), so disable
89+
# the built-in "open default browser tab" behavior for this process.
90+
$env:LOCAL_NEXUS_OPEN_BROWSER = "false"
91+
92+
Start-Process `
93+
-FilePath $pythonExe `
94+
-ArgumentList @("-m", "local_nexus_controller") `
95+
-WorkingDirectory $projectRoot `
96+
-WindowStyle Hidden `
97+
-RedirectStandardOutput $outLogPath `
98+
-RedirectStandardError $errLogPath | Out-Null
99+
}
100+
101+
function Wait-For-Dashboard([string]$url, [int]$timeoutSeconds = 25) {
102+
$deadline = (Get-Date).AddSeconds($timeoutSeconds)
103+
while ((Get-Date) -lt $deadline) {
104+
try {
105+
$res = Invoke-WebRequest -UseBasicParsing -Uri $url -TimeoutSec 2
106+
if ($res.StatusCode -ge 200 -and $res.StatusCode -lt 500) { return }
107+
} catch {
108+
Start-Sleep -Milliseconds 500
109+
}
110+
}
111+
}
112+
113+
function Test-DashboardUp([string]$url) {
114+
try {
115+
$res = Invoke-WebRequest -UseBasicParsing -Uri $url -TimeoutSec 1
116+
return ($res.StatusCode -ge 200 -and $res.StatusCode -lt 500)
117+
} catch {
118+
return $false
119+
}
120+
}
121+
122+
function Open-WidgetWindow([string]$browserExe, [string]$url) {
123+
$wa = Get-WorkingArea
124+
125+
# Tweak these to taste.
126+
# Smaller footprint, but a bit wider for readability.
127+
$w = $widgetWidth
128+
$h = $widgetHeight
129+
$margin = $widgetMargin
130+
$x = $margin
131+
$y = [Math]::Max(0, $wa.Bottom - $h - $margin)
132+
133+
$browserArgs = @(
134+
"--app=$url",
135+
"--window-size=$w,$h",
136+
"--window-position=$x,$y"
137+
)
138+
139+
Start-Process -FilePath $browserExe -ArgumentList $browserArgs | Out-Null
140+
}
141+
142+
function ConvertTo-Bool([string]$value, [bool]$default) {
143+
if ($null -eq $value -or $value.Trim() -eq "") { return $default }
144+
$v = $value.Trim().ToLowerInvariant()
145+
return @("1", "true", "yes", "on").Contains($v)
146+
}
147+
148+
function ConvertTo-Int([string]$value, [int]$default) {
149+
try {
150+
if ($null -eq $value -or $value.Trim() -eq "") { return $default }
151+
return [int]$value.Trim()
152+
} catch {
153+
return $default
154+
}
155+
}
156+
157+
function Test-InternetUp {
158+
try {
159+
# DNS resolution check (fast).
160+
[void][System.Net.Dns]::GetHostEntry("example.com")
161+
162+
# HTTP connectivity check (short timeout).
163+
$res = Invoke-WebRequest -UseBasicParsing -Uri "http://www.msftconnecttest.com/connecttest.txt" -TimeoutSec 3
164+
return ($res.StatusCode -ge 200 -and $res.StatusCode -lt 400)
165+
} catch {
166+
return $false
167+
}
168+
}
169+
170+
function Wait-For-Internet([int]$timeoutSeconds) {
171+
$deadline = (Get-Date).AddSeconds($timeoutSeconds)
172+
$lastLog = Get-Date "2000-01-01"
173+
174+
while ((Get-Date) -lt $deadline) {
175+
if (Test-InternetUp) { return $true }
176+
177+
$now = Get-Date
178+
if (($now - $lastLog).TotalSeconds -ge 10) {
179+
Write-WidgetLog $projectRoot "Waiting for internet connectivity..."
180+
$lastLog = $now
181+
}
182+
Start-Sleep -Seconds 2
183+
}
184+
return $false
185+
}
186+
187+
$projectRoot = Resolve-ProjectRoot
188+
$pythonExe = Get-PythonExe $projectRoot
189+
190+
$port = 5010
191+
$waitForInternet = $true
192+
$internetTimeoutSeconds = 90
193+
$startupDelaySeconds = 12
194+
$widgetWidth = 780
195+
$widgetHeight = 240
196+
$widgetMargin = 8
197+
198+
$envPath = Join-Path $projectRoot ".env"
199+
if (Test-Path $envPath) {
200+
foreach ($line in (Get-Content $envPath)) {
201+
if ($line -match '^\s*LOCAL_NEXUS_PORT\s*=\s*(\d+)\s*$') { $port = [int]$Matches[1] }
202+
if ($line -match '^\s*LOCAL_NEXUS_WAIT_FOR_INTERNET\s*=\s*(.+?)\s*$') { $waitForInternet = ConvertTo-Bool $Matches[1] $waitForInternet }
203+
if ($line -match '^\s*LOCAL_NEXUS_INTERNET_TIMEOUT_SECONDS\s*=\s*(.+?)\s*$') { $internetTimeoutSeconds = ConvertTo-Int $Matches[1] $internetTimeoutSeconds }
204+
if ($line -match '^\s*LOCAL_NEXUS_WIDGET_START_DELAY_SECONDS\s*=\s*(.+?)\s*$') { $startupDelaySeconds = ConvertTo-Int $Matches[1] $startupDelaySeconds }
205+
if ($line -match '^\s*LOCAL_NEXUS_WIDGET_WIDTH\s*=\s*(.+?)\s*$') { $widgetWidth = ConvertTo-Int $Matches[1] $widgetWidth }
206+
if ($line -match '^\s*LOCAL_NEXUS_WIDGET_HEIGHT\s*=\s*(.+?)\s*$') { $widgetHeight = ConvertTo-Int $Matches[1] $widgetHeight }
207+
if ($line -match '^\s*LOCAL_NEXUS_WIDGET_MARGIN\s*=\s*(.+?)\s*$') { $widgetMargin = ConvertTo-Int $Matches[1] $widgetMargin }
208+
}
209+
}
210+
211+
# Give Explorer/network stack a moment to finish loading so the app window is visible,
212+
# and Wi-Fi/Ethernet has time to connect.
213+
Start-Sleep -Seconds $startupDelaySeconds
214+
215+
if (!(Enter-WidgetLock)) {
216+
Write-WidgetLog $projectRoot "Lock indicates recent run; exiting."
217+
exit 0
218+
}
219+
220+
# Dashboard URL: prefer loopback for local desktop widget.
221+
$lncHost = "127.0.0.1"
222+
$url = "http://$lncHost`:$port/?widget=1"
223+
224+
Write-WidgetLog $projectRoot "Starting widget. pythonExe=$pythonExe url=$url"
225+
226+
try {
227+
if (!(Test-DashboardUp $url)) {
228+
Write-WidgetLog $projectRoot "Dashboard not up; starting server."
229+
Start-Server $pythonExe $projectRoot
230+
Wait-For-Dashboard $url 25
231+
} else {
232+
Write-WidgetLog $projectRoot "Dashboard already up; not starting server."
233+
}
234+
235+
if ($waitForInternet) {
236+
Write-WidgetLog $projectRoot "Internet wait enabled (timeout=${internetTimeoutSeconds}s)."
237+
$ok = Wait-For-Internet $internetTimeoutSeconds
238+
if ($ok) {
239+
Write-WidgetLog $projectRoot "Internet connectivity detected."
240+
} else {
241+
Write-WidgetLog $projectRoot "WARNING: Internet not detected within timeout; opening widget anyway."
242+
}
243+
}
244+
245+
$browserExe = Get-BrowserCmd
246+
Write-WidgetLog $projectRoot "Using browser: $browserExe"
247+
Open-WidgetWindow $browserExe $url
248+
Write-WidgetLog $projectRoot "Widget window opened."
249+
} catch {
250+
Write-WidgetLog $projectRoot "ERROR: $($_.Exception.Message)"
251+
throw
252+
}
253+

0 commit comments

Comments
 (0)