a02f4db850
wol.py:
- build_magic_packet(): standard 6x0xFF + 16x MAC layout
- send_local_broadcast(): direct from container (ports 9 + 7 for safety)
- send_via_peer(): preferred path; SSHes to the OTHER Spark and runs a Python one-liner there so the packet originates on the target's LAN segment (most reliable)
- MAC validation + normalization
connectivity.py:
- /data/connectivity.json persistence (thread-safe, atomic rename)
- Stores per-Spark current state + last_change timestamp + rolling 200-event log
- Records up/down transitions; computes down_seconds / up_seconds durations
- MAC cache populated lazily during hardware probes
hardware.py:
- Probe now reads MAC via /sys/class/net/<default-route-iface>/address
- After each probe, record_state() emits a transition event if state changed
- record_mac() caches the address so WoL works when the Spark next goes down
Endpoints:
- GET /api/connectivity: macs, current state, last_change, events[]
- POST /api/spark/{name}/wake: tries via-peer first, falls back to direct broadcast
UI:
- Unreachable hardware card shows the cached MAC + 'Wake (WoL)' button (only if MAC known)
- New 'Connectivity log' button opens a modal with per-Spark transition history (last 25 each), including duration of each prior up/down period
- pollHardware also pulls /api/connectivity so WoL buttons appear without an extra fetch
Package: bump 0.5.0:0; main.ts sets CONNECTIVITY_LOG=/data/connectivity.json
70 lines
2.1 KiB
TypeScript
70 lines
2.1 KiB
TypeScript
import { i18n } from './i18n'
|
|
import { sdk } from './sdk'
|
|
import { uiPort } from './utils'
|
|
import { sparkConfigYaml } from './fileModels/sparkConfig.yaml'
|
|
|
|
export const main = sdk.setupMain(async ({ effects }) => {
|
|
console.info(i18n('Starting Spark Control…'))
|
|
|
|
// Reactively read SSH targets from the user-configured yaml file.
|
|
// Changing this file via the "Configure Sparks" action restarts the daemon.
|
|
const cfg = (await sparkConfigYaml.read().const(effects)) ?? {
|
|
spark1_host: '',
|
|
spark1_user: '',
|
|
spark2_host: '',
|
|
spark2_user: '',
|
|
parakeet_host: '',
|
|
parakeet_user: '',
|
|
parakeet_container: '',
|
|
magpie_host: '',
|
|
magpie_user: '',
|
|
magpie_container: '',
|
|
open_webui_url: '',
|
|
ngc_api_key: '',
|
|
}
|
|
|
|
return sdk.Daemons.of(effects).addDaemon('primary', {
|
|
subcontainer: await sdk.SubContainer.of(
|
|
effects,
|
|
{ imageId: 'spark-control' },
|
|
sdk.Mounts.of().mountVolume({
|
|
volumeId: 'main',
|
|
subpath: null,
|
|
mountpoint: '/data',
|
|
readonly: false,
|
|
}),
|
|
'spark-control-sub',
|
|
),
|
|
exec: {
|
|
command: ['/app/entrypoint.sh'],
|
|
env: {
|
|
SPARK1_HOST: cfg.spark1_host,
|
|
SPARK1_USER: cfg.spark1_user,
|
|
SPARK2_HOST: cfg.spark2_host,
|
|
SPARK2_USER: cfg.spark2_user,
|
|
PARAKEET_HOST: cfg.parakeet_host,
|
|
PARAKEET_USER: cfg.parakeet_user,
|
|
PARAKEET_CONTAINER: cfg.parakeet_container,
|
|
MAGPIE_HOST: cfg.magpie_host,
|
|
MAGPIE_USER: cfg.magpie_user,
|
|
MAGPIE_CONTAINER: cfg.magpie_container,
|
|
MODELS_OVERRIDES: '/data/models-overrides.yaml',
|
|
SERVICES_OVERRIDES: '/data/services-overrides.yaml',
|
|
CONNECTIVITY_LOG: '/data/connectivity.json',
|
|
OPEN_WEBUI_URL: cfg.open_webui_url,
|
|
NGC_API_KEY: cfg.ngc_api_key,
|
|
BIND_PORT: String(uiPort),
|
|
},
|
|
},
|
|
ready: {
|
|
display: i18n('Web Interface'),
|
|
fn: () =>
|
|
sdk.healthCheck.checkPortListening(effects, uiPort, {
|
|
successMessage: i18n('The web interface is ready'),
|
|
errorMessage: i18n('The web interface is not ready'),
|
|
}),
|
|
},
|
|
requires: [],
|
|
})
|
|
})
|