feat: self-host Hobbs Family Organizer (Shield TV dashboard) on hf-pdocker-02:8340
- tv app/Hobbs-Family-Organizer-Self-Hosted-v1.0.0: release bundle (Next.js app + Shield APK v1.4 + source) - compose/hf-pdocker-02/hobbs-organizer: override (local-disk secrets, watchtower off), .env (port 8340), Shield provisioning script - CLAUDE.md: service tree + internal ports Trusted-LAN mode. Secrets live only on .100 /var/lib/hobbs-organizer/secrets (not in git). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E9NnVEico3WvFxLyjy1iW3
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Device,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$BaseUrl,
|
||||
|
||||
[string]$AdbPath = "adb",
|
||||
|
||||
[switch]$AllowInsecureHttp,
|
||||
|
||||
[switch]$PromptForToken,
|
||||
|
||||
[string]$TokenFile,
|
||||
|
||||
[switch]$NoLaunch
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if ($PromptForToken -and $PSBoundParameters.ContainsKey("TokenFile")) {
|
||||
throw "Use either -PromptForToken or -TokenFile, not both."
|
||||
}
|
||||
|
||||
function Resolve-AdbExecutable {
|
||||
param([string]$RequestedPath)
|
||||
|
||||
if (Test-Path -LiteralPath $RequestedPath -PathType Leaf) {
|
||||
return (Resolve-Path -LiteralPath $RequestedPath).Path
|
||||
}
|
||||
|
||||
$command = Get-Command -Name $RequestedPath -CommandType Application -ErrorAction SilentlyContinue
|
||||
if ($null -eq $command) {
|
||||
throw "ADB was not found. Install Android platform-tools or pass -AdbPath with the full path to adb.exe."
|
||||
}
|
||||
return $command.Source
|
||||
}
|
||||
|
||||
function Invoke-Adb {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$Arguments,
|
||||
|
||||
[AllowNull()]
|
||||
[string]$StandardInput = $null
|
||||
)
|
||||
|
||||
$startInfo = [System.Diagnostics.ProcessStartInfo]::new()
|
||||
$startInfo.FileName = $script:ResolvedAdbPath
|
||||
$startInfo.UseShellExecute = $false
|
||||
$startInfo.CreateNoWindow = $true
|
||||
$startInfo.RedirectStandardInput = $true
|
||||
$startInfo.RedirectStandardOutput = $true
|
||||
$startInfo.RedirectStandardError = $true
|
||||
foreach ($argument in $Arguments) {
|
||||
[void]$startInfo.ArgumentList.Add($argument)
|
||||
}
|
||||
|
||||
$process = [System.Diagnostics.Process]::new()
|
||||
$process.StartInfo = $startInfo
|
||||
if (-not $process.Start()) {
|
||||
throw "ADB could not be started."
|
||||
}
|
||||
|
||||
$stdoutTask = $process.StandardOutput.ReadToEndAsync()
|
||||
$stderrTask = $process.StandardError.ReadToEndAsync()
|
||||
if ($null -ne $StandardInput) {
|
||||
$process.StandardInput.Write($StandardInput)
|
||||
}
|
||||
$process.StandardInput.Close()
|
||||
$process.WaitForExit()
|
||||
|
||||
$stdout = $stdoutTask.GetAwaiter().GetResult()
|
||||
$stderr = $stderrTask.GetAwaiter().GetResult()
|
||||
if ($process.ExitCode -ne 0) {
|
||||
$detail = $stderr.Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($detail)) {
|
||||
$detail = $stdout.Trim()
|
||||
}
|
||||
throw "ADB failed with exit code $($process.ExitCode): $detail"
|
||||
}
|
||||
return $stdout.Trim()
|
||||
}
|
||||
|
||||
function Set-AppPrivateFile {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet("base_url", "access_token", "allow_insecure_http")]
|
||||
[string]$Name,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
$remoteCommand = "run-as com.dayline.tv sh -c 'umask 077; cat > files/.$Name.new && chmod 600 files/.$Name.new && mv files/.$Name.new files/$Name'"
|
||||
[void](Invoke-Adb -Arguments @("-s", $Device, "shell", $remoteCommand) -StandardInput $Value)
|
||||
}
|
||||
|
||||
function Remove-AppPrivateFile {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet("access_token", "allow_insecure_http")]
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
$remoteCommand = "run-as com.dayline.tv sh -c 'rm -f files/$Name files/.$Name.new'"
|
||||
[void](Invoke-Adb -Arguments @("-s", $Device, "shell", $remoteCommand))
|
||||
}
|
||||
|
||||
function Test-PrivateLanHost {
|
||||
param([string]$HostName)
|
||||
|
||||
$normalized = $HostName.Trim().Trim("[", "]").ToLowerInvariant()
|
||||
if ($normalized -eq "localhost" -or
|
||||
$normalized.EndsWith(".local") -or
|
||||
$normalized.EndsWith(".lan") -or
|
||||
$normalized.EndsWith(".home.arpa")) {
|
||||
return $true
|
||||
}
|
||||
|
||||
$address = $null
|
||||
if (-not [System.Net.IPAddress]::TryParse($normalized, [ref]$address)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$bytes = $address.GetAddressBytes()
|
||||
if ($address.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) {
|
||||
return $bytes[0] -eq 10 -or
|
||||
$bytes[0] -eq 127 -or
|
||||
($bytes[0] -eq 172 -and $bytes[1] -ge 16 -and $bytes[1] -le 31) -or
|
||||
($bytes[0] -eq 192 -and $bytes[1] -eq 168) -or
|
||||
($bytes[0] -eq 169 -and $bytes[1] -eq 254)
|
||||
}
|
||||
|
||||
if ($address.Equals([System.Net.IPAddress]::IPv6Loopback)) {
|
||||
return $true
|
||||
}
|
||||
return (($bytes[0] -band 0xFE) -eq 0xFC) -or
|
||||
($bytes[0] -eq 0xFE -and ($bytes[1] -band 0xC0) -eq 0x80)
|
||||
}
|
||||
|
||||
$script:ResolvedAdbPath = Resolve-AdbExecutable -RequestedPath $AdbPath
|
||||
|
||||
$dashboardUri = $null
|
||||
if (-not [Uri]::TryCreate($BaseUrl.Trim(), [UriKind]::Absolute, [ref]$dashboardUri)) {
|
||||
throw "BaseUrl is not a valid absolute URL."
|
||||
}
|
||||
if ($dashboardUri.Scheme -notin @("http", "https")) {
|
||||
throw "BaseUrl must use http or https."
|
||||
}
|
||||
if (-not [string]::IsNullOrEmpty($dashboardUri.UserInfo) -or
|
||||
-not [string]::IsNullOrEmpty($dashboardUri.Query) -or
|
||||
-not [string]::IsNullOrEmpty($dashboardUri.Fragment)) {
|
||||
throw "BaseUrl cannot contain credentials, a query, or a fragment."
|
||||
}
|
||||
if ($dashboardUri.AbsolutePath -notin @("", "/")) {
|
||||
throw "The organizer must be served from the URL root path."
|
||||
}
|
||||
if ($dashboardUri.Scheme -eq "http") {
|
||||
if (-not $AllowInsecureHttp) {
|
||||
throw "HTTP requires the explicit -AllowInsecureHttp switch. HTTPS is recommended."
|
||||
}
|
||||
if (-not (Test-PrivateLanHost -HostName $dashboardUri.Host)) {
|
||||
throw "HTTP is restricted to private LAN IP addresses or .local, .lan, and .home.arpa hosts."
|
||||
}
|
||||
}
|
||||
|
||||
$normalizedBaseUrl = $dashboardUri.GetLeftPart([UriPartial]::Authority) + "/"
|
||||
$plainToken = $null
|
||||
try {
|
||||
if ($PSBoundParameters.ContainsKey("TokenFile")) {
|
||||
if (-not (Test-Path -LiteralPath $TokenFile -PathType Leaf)) {
|
||||
throw "TokenFile was not found."
|
||||
}
|
||||
$plainToken = (Get-Content -LiteralPath $TokenFile -Raw).Trim()
|
||||
} elseif ($PromptForToken) {
|
||||
$secureToken = Read-Host "Local bearer token" -AsSecureString
|
||||
$pointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureToken)
|
||||
try {
|
||||
$plainToken = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($pointer)
|
||||
} finally {
|
||||
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($pointer)
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -ne $plainToken -and $plainToken -notmatch "^[A-Za-z0-9._~-]{32,512}$") {
|
||||
throw "The bearer token must contain 32 to 512 URL-safe characters."
|
||||
}
|
||||
|
||||
[void](Invoke-Adb -Arguments @("connect", $Device))
|
||||
$state = Invoke-Adb -Arguments @("-s", $Device, "get-state")
|
||||
if ($state -ne "device") {
|
||||
throw "The Shield is not connected and authorized."
|
||||
}
|
||||
$packagePath = Invoke-Adb -Arguments @("-s", $Device, "shell", "pm path com.dayline.tv")
|
||||
if (-not $packagePath.StartsWith("package:")) {
|
||||
throw "Hobbs' Family Organizer App is not installed on the selected Shield."
|
||||
}
|
||||
|
||||
Set-AppPrivateFile -Name "base_url" -Value $normalizedBaseUrl
|
||||
if ($null -ne $plainToken) {
|
||||
Set-AppPrivateFile -Name "access_token" -Value $plainToken
|
||||
} else {
|
||||
Remove-AppPrivateFile -Name "access_token"
|
||||
}
|
||||
|
||||
if ($dashboardUri.Scheme -eq "http") {
|
||||
Set-AppPrivateFile -Name "allow_insecure_http" -Value "enabled"
|
||||
} else {
|
||||
Remove-AppPrivateFile -Name "allow_insecure_http"
|
||||
}
|
||||
|
||||
if (-not $NoLaunch) {
|
||||
[void](Invoke-Adb -Arguments @("-s", $Device, "shell", "am force-stop com.dayline.tv"))
|
||||
[void](Invoke-Adb -Arguments @("-s", $Device, "shell", "am start -W -n com.dayline.tv/.MainActivity"))
|
||||
}
|
||||
|
||||
Write-Host "Configured Hobbs' Family Organizer App on $Device"
|
||||
Write-Host "Dashboard: $normalizedBaseUrl"
|
||||
Write-Host "Bearer token: $(if ($null -eq $plainToken) { 'not configured' } else { 'configured' })"
|
||||
Write-Host "Insecure HTTP: $(if ($dashboardUri.Scheme -eq 'http') { 'enabled for this private LAN origin' } else { 'disabled' })"
|
||||
Write-Host "The legacy hosted service_token was preserved for rollback."
|
||||
} finally {
|
||||
$plainToken = $null
|
||||
}
|
||||
Reference in New Issue
Block a user