v0.27.0:0 - in-app settings gear + swap-lock route fix

Move the ~20 optional cluster knobs out of the StartOS "Configure Sparks"
action (now just the 4 required fields) and into a dashboard ⚙ Settings gear,
backed by a /data/app_settings.json overlay keyed by env-var names. One shared
mutable Settings instance + Settings.reload() applies edits live without a
restart; existing installs' values migrate automatically on first boot.

Also: support-service ports (parakeet/kokoro/embed/qdrant + vllm) are now
configurable, and GET /api/swap/lock no longer 404s (it was shadowed by the
/api/swap/{job_id} catch-all). WebhookNotifier is re-pointed on save so its
url/secret reload live too.
This commit is contained in:
Keysat
2026-06-18 13:41:28 -05:00
parent b67e001642
commit 7e0759846f
15 changed files with 797 additions and 268 deletions
+21 -154
View File
@@ -3,6 +3,15 @@ import { sparkConfigYaml } from '../fileModels/sparkConfig.yaml'
const { InputSpec, Value } = sdk
// This action is intentionally minimal: just the required wiring needed before
// Spark Control can do anything — the two Spark node addresses and SSH users.
// Every other knob (vLLM/service ports, container names, support-service hosts,
// integrations, webhooks) now lives behind the ⚙ Settings gear in the dashboard
// itself, which is where StartOS 0.4 expects routine config to live (and most
// operators never open StartOS actions). The optional keys still exist in the
// config.yaml schema (set by older versions); they're read into env at launch
// and migrated into the in-app settings overlay on first boot, so nothing is
// lost on upgrade — they're simply edited in the dashboard from now on.
const inputSpec = InputSpec.of({
spark1_host: Value.text({
name: 'Spark 1 hostname or IP',
@@ -40,164 +49,14 @@ const inputSpec = InputSpec.of({
placeholder: 'your SSH username',
masked: false,
}),
vllm_port: Value.text({
name: 'vLLM port (optional)',
description:
"The port your vLLM server listens on, on Spark 1 — used by the health check and the chat proxy. Leave blank to use 8888, which is what the bundled launch-cluster.sh wrapper uses. Set this to 8000 (vLLM's own default) or another port if your vLLM listens elsewhere.",
required: false,
default: null,
placeholder: 'leave blank for 8888',
masked: false,
}),
vllm_container: Value.text({
name: 'vLLM container name (optional)',
description:
'Docker container name for the swappable vLLM on Spark 1. Defaults to "vllm_node" (what the bundled launch-cluster.sh creates). Change this only if you run your vLLM under a different container name — the model-swap log view and the pre-flight validator exec into it by name.',
required: false,
default: null,
placeholder: 'leave blank for vllm_node',
masked: false,
}),
disabled_services: Value.text({
name: 'Services to hide (optional)',
description:
"Comma-separated list of built-in services your cluster doesn't run, so Spark Control hides their tiles and stops probing them. Valid names: parakeet, kokoro, embeddings, qdrant. Example: if you only run vLLM, set this to 'parakeet,kokoro,embeddings,qdrant'. Leave blank to monitor all of them. (Useful when, say, your vLLM shares port 8000 with Parakeet's default — hide Parakeet so its probe doesn't hit vLLM.)",
required: false,
default: null,
placeholder: 'e.g. parakeet,kokoro',
masked: false,
}),
parakeet_host: Value.text({
name: 'Parakeet host (optional)',
description:
"Override the host running the Parakeet STT container. Leave blank if Parakeet runs on Spark 2 — that's the default. Set this if you run Parakeet on Spark 1 or a different machine.",
required: false,
default: null,
placeholder: 'leave blank to use Spark 2',
masked: false,
}),
parakeet_container: Value.text({
name: 'Parakeet container name (optional)',
description:
'Docker container name for Parakeet. Defaults to "parakeet-asr" — change only if you named yours something else.',
required: false,
default: null,
placeholder: 'parakeet-asr',
masked: false,
}),
kokoro_host: Value.text({
name: 'Kokoro host (optional)',
description:
'Override the host running the Kokoro TTS container. Leave blank if Kokoro runs on Spark 2.',
required: false,
default: null,
placeholder: 'leave blank to use Spark 2',
masked: false,
}),
kokoro_container: Value.text({
name: 'Kokoro container name (optional)',
description: 'Docker container name for Kokoro. Defaults to "kokoro-tts".',
required: false,
default: null,
placeholder: 'kokoro-tts',
masked: false,
}),
embed_host: Value.text({
name: 'Embedding server host (optional)',
description:
'Override the host running the spark-embed container (bge-m3 dense embeddings + reranker). Leave blank if it runs on Spark 2.',
required: false,
default: null,
placeholder: 'leave blank to use Spark 2',
masked: false,
}),
embed_container: Value.text({
name: 'Embedding container name (optional)',
description:
'Docker container name for the embedding server. Defaults to "spark-embed".',
required: false,
default: null,
placeholder: 'spark-embed',
masked: false,
}),
qdrant_host: Value.text({
name: 'Qdrant host (optional)',
description:
'Override the host running the Qdrant vector database. Leave blank if it runs on Spark 2.',
required: false,
default: null,
placeholder: 'leave blank to use Spark 2',
masked: false,
}),
qdrant_container: Value.text({
name: 'Qdrant container name (optional)',
description: 'Docker container name for Qdrant. Defaults to "qdrant".',
required: false,
default: null,
placeholder: 'qdrant',
masked: false,
}),
qdrant_collection: Value.text({
name: 'Default Qdrant collection (optional)',
description:
'Default collection name used by /api/search when a request does not specify one. Leave blank to require callers to pass a collection.',
required: false,
default: null,
placeholder: 'e.g. crm_chunks',
masked: false,
}),
matrix_bridge_user: Value.text({
name: 'matrix-bridge bot SSH user (optional)',
description:
"If you run the matrix-bridge Matrix bot on Spark 2, enter the SSH user that owns its ~/matrix-bridge folder (e.g. 'modelo'). Spark Control then shows a tile to update, restart, and view logs for the bot. Leave blank if you don't run the bot — the tile stays hidden. Note: this package's SSH public key must be authorized for that user (Show Public Key action) unless it's the same as your Spark 2 user.",
required: false,
default: null,
placeholder: 'e.g. modelo',
masked: false,
}),
open_webui_url: Value.text({
name: 'Open WebUI URL (optional)',
description:
'If you also run Open WebUI on your LAN, paste its URL here. Spark Control will then show a one-click "Open chat" button next to the current model so you can jump straight to it.',
required: false,
default: null,
placeholder: 'e.g. https://open-webui.yourserver.local',
masked: false,
}),
ngc_api_key: Value.text({
name: 'NGC API key (optional)',
description:
'NVIDIA NGC personal API key — needed to install NIM containers (Parakeet, etc.) from nvcr.io. Get one free at https://ngc.nvidia.com/setup/personal-key. Stored only on this Start9 server; passed to docker as the NGC_API_KEY env var when installing NIM services. (Kokoro TTS is Apache 2.0 and does not need an NGC key.)',
required: false,
default: null,
placeholder: 'starts with "nvapi-..."',
masked: true,
}),
swap_webhook_url: Value.text({
name: 'Swap webhook URL (optional)',
description:
'If you run automation that needs to know when the loaded model changes, paste a URL here. Spark Control POSTs a small JSON event (swap_complete / swap_failed) to it after every model swap, so the consumer can re-point its config to the new model. Leave blank to disable. Only needed if something other than this dashboard cares about swaps.',
required: false,
default: null,
placeholder: 'e.g. https://my-service.local/spark-swap',
masked: false,
}),
swap_webhook_secret: Value.text({
name: 'Swap webhook secret (optional)',
description:
'Optional shared secret. If set, each webhook is signed with an "X-Spark-Signature: sha256=…" header (HMAC of the body) so the receiver can verify it really came from Spark Control. Leave blank to send the webhook unsigned.',
required: false,
default: null,
placeholder: 'a random string the receiver also knows',
masked: true,
}),
})
export const configureSparks = sdk.Action.withInput(
'configure-sparks',
async () => ({
name: 'Configure Sparks',
description: 'Set the hostnames and SSH users for your two Spark nodes.',
description:
'Set your two Spark node addresses and SSH users — the required wiring. Everything else (ports, container names, support services, integrations) is configured under ⚙ Settings in the Spark Control dashboard.',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
@@ -205,11 +64,19 @@ export const configureSparks = sdk.Action.withInput(
}),
async () => inputSpec,
async ({ effects }) => {
// Prefill from the saved config, but only the keys this (trimmed) form owns.
const cfg = await sparkConfigYaml.read().once()
return cfg ?? null
if (!cfg) return null
return {
spark1_host: cfg.spark1_host,
spark1_user: cfg.spark1_user,
spark2_host: cfg.spark2_host,
spark2_user: cfg.spark2_user,
}
},
async ({ effects, input }) => {
// Optional fields come through as `null`; coerce to empty string for the schema.
// merge() only touches the four keys we submit, leaving any legacy optional
// values already in config.yaml intact.
const normalized = Object.fromEntries(
Object.entries(input).map(([k, v]) => [k, v ?? '']),
) as Record<string, string>
+2 -2
View File
@@ -1,10 +1,10 @@
import { VersionInfo, IMPOSSIBLE } from '@start9labs/start-sdk'
export const v0_1_0 = VersionInfo.of({
version: '0.26.0:0',
version: '0.27.0:0',
releaseNotes: {
en_US:
"v0.26.0:0 — the model menu is now what's actually on your Sparks. The dashboard scans both Sparks for downloaded models and shows exactly those — no more hard-coded list. (1) Delete means delete: removing a model frees its weights AND takes the card off the menu (re-download later to bring it back, with its saved settings). (2) Download a new model and it appears on the menu by itself when it finishes. (3) Models Spark Control doesn't recognize show a \"needs setup\" card — the first time you switch to one, it reads the model's own files, guesses how to launch it (which family, solo vs both Sparks, the right vLLM flags), and asks you to confirm once; after that it's a normal card. (4) The download box now autocompletes known-good models. (5) Each install shows its own Sparks' models, so a shared copy no longer displays someone else's list. Removed the two legacy Qwen entries (235B FP8, 2.5 72B) — they'll still appear if you actually have them downloaded. No consumer-API changes; the /v1 proxy and swap API are unchanged.",
'v0.27.0:0 — settings move into the dashboard, plus two bug fixes. (1) New ⚙ Settings gear in the dashboard: all the optional cluster knobs — vLLM and support-service ports, container names, Parakeet/Kokoro/embeddings/Qdrant hosts, Open WebUI link, NGC key, swap webhook — are now edited here, in plain English, and apply immediately without a restart. The StartOS "Configure Sparks" action is now just the four required fields (two Spark IPs + SSH users); your existing optional values migrate into the gear automatically on first launch, and the settings are stored on the server and included in StartOS backups. (2) NEW: support-service ports are now configurable. If your vLLM runs on 8000 (vLLM\'s own default) and you moved Parakeet to another port, set them under ⚙ Settings → that fixes the false "vLLM down" and the Parakeet 404 some setups saw. (3) Bug fix: GET /api/swap/lock returned 404 (a routing bug where it was shadowed by the swap-job lookup); the swap reservation status now reads correctly. No breaking consumer-API changes; the /v1 proxy and swap API are unchanged.',
},
migrations: {
up: async ({ effects }) => {},