Skip to content

Add optional unified Strata Manager - #434

Closed
Ayshinko wants to merge 8 commits into
Niko1221:mainfrom
Ayshinko:feature/gui-manager
Closed

Ayshinko wants to merge 8 commits into
Niko1221:mainfrom
Ayshinko:feature/gui-manager

Conversation

@Ayshinko

@Ayshinko Ayshinko commented Oct 1, 2026 •

Copy link
Copy Markdown

Summary

Adds an optional Strata Manager that turns the existing Strata web UI into a unified local control surface:

Manager · Chat · Monitor · About

The Manager runs as a persistent local control server on 127.0.0.1:8275, while the normal Strata model server remains on its
existing port (usually 8080).

This keeps model configuration and lifecycle controls available even while Strata itself is stopped or restarting, without changing
the existing START-HERE / SETUP workflows.

What it adds

The new Manager tab supports:

  • Discovering and selecting installed Strata models
  • Context size presets and custom context values
  • Vision: Off / GPU / CPU
  • Low-RAM mode
  • KV cache settings
  • GPU selection
  • Host / port / API key settings
  • Existing custom GGUF selection
  • Start / Stop / Restart
  • Force Stop fallback where appropriate
  • Installed-model overview
  • Server logs
  • Local CPU / RAM / GPU information

Configuration continues to use the existing setup.py semantics and the same strata-*.json files. The Manager does not introduce a
second configuration system.

Unified web UI

The existing Strata frontend is reused rather than embedded as a separate site.

The result is one native interface:

  Strata

  [ Manager ] [ Chat ] [ Monitor ] [ About ]

@Ayshinko
Ayshinko force-pushed the feature/gui-manager branch 2 times, most recently from 804e7ac to 98a4074 Compare October 2, 2026 12:22
@Ayshinko Ayshinko changed the title Add optional Strata GUI Manager Add optional unified Strata Manager Oct 2, 2026
…ision, Low RAM, custom GGUF, Start/Stop/Restart)

A thin management GUI on top of setup.py and the existing strata-*.json configs: model/config
discovery uses setup.choices_from_config, edits write the same config format setup writes (atomic
writes + .bak backups), and Start launches the same serve/server.py command the generated run-*.bat /
run-*.sh scripts use. No engine code, no setup logic, and no downloads are duplicated.

  gui/manager.py      - HTTP server (127.0.0.1), config editor backend, folder picker
  gui/launcher.py     - platform-neutral supervisor (state, port checks, status, log tail)
  gui/platforms/      - the only OS-specific code: spawn/terminate adapters (windows.py, linux.py)
  gui/web/*           - dependency-free dark UI using the Strata design tokens
  START-MANAGER.bat, start-manager.sh - launchers (same code)
  docs/GUI_MANAGER.md - what it is, launch, relation to START-HERE/SETUP, config storage,
                        Windows and Linux usage, limitations
  gui/test_manager.py - 33 unit + HTTP round-trip tests (temp folders only; fake platform adapter
                        so the suite runs identically on Windows and Linux)

Vision/context/low-ram reuse setup.py's exact semantics and functions (derived_factor/resolve_rope,
low_ram_resident, VISION). Custom GGUF preparation delegates to the existing setup.py --gguf-dir
pipeline. Stop closes the process tree on Windows and SIGTERMs the server's group on Linux.
…e Stop

The extra blank terminal was the engine: on Windows a console-subsystem child
(strata.exe) spawned by a process with no console gets a brand-new visible console
window.  The Windows launcher now spawns the server with CREATE_NEW_CONSOLE and a
console hidden from birth (STARTUPINFO SW_HIDE), so the engine inherits the hidden
console instead of opening its own - verified on the real install (window snapshot:
no new console window).  The Manager's own console, and Linux behavior, are unchanged.

Stop/Restart no longer look frozen:
- backend lifecycle states stopped/starting/running/stopping/restarting/error,
  carried in logs/manager.json while an operation runs; /api/status reports the real
  phase (graceful/forcing/... ) and measured elapsed seconds
- UI switches to Stopping/Restarting immediately on click (never waits for the API),
  shows a spinner + 'Gracefully shutting down the engine 4.2s' ticking locally, and
  confirms 'Stopped in Xs'
- Restart walks Stopping -> Starting -> Running visibly
- new Force Stop button (POST /api/force-stop) terminates the process tree
  immediately; normal Stop still tries graceful shutdown first
- fixed the Windows PID-alive check (os.kill(pid,0) kept reporting dead processes as
  alive, which made every stop burn its full grace wait): OpenProcess +
  GetExitCodeProcess now answers immediately - a normal stop went from ~30 s to ~3 s
- measured: normal stop 3.26 s (forced fallback after ~3 s), restart 4.7 s to
  running, force stop 0.1 s; port 8080 released and VRAM back to 1.2-1.4 GiB after
  each stop
…y proxy, external detection, console-free launcher

The Manager becomes a first-class page of ONE Strata web app (Manager / Chat /
Monitor / About) served by the persistent control server on 127.0.0.1:8275 -
no iframe, no second tab, no redirect to 8080, no page reload on Start/Restart.

- gui/web/index.html + app.css: the Manager view rebuilt native in the Strata UI
  kit (tokens.css/components.css), as a fourth tab next to Chat/Monitor/About.
- gui/web/app.js: a thin shell that loads serve/web/app.js unchanged and adds the
  Manager tab, hash navigation (default #manager, remembered), offline awareness
  (clean 'Strata is not running' states; Chat/Monitor/About resume automatically)
  and the lifecycle/external pills.  serve/web/app.js is reused verbatim under
  web/serve-app.js - no copied, diverging frontend.
- gui/manager.py: serves the shared UI kit + fonts and proxies Strata's own
  endpoints (/health /metrics /mcp /settings /v1/* ...) on the SAME origin, with
  the config's API key injected and the browser Origin kept off the wire (no CORS
  hacks); streams Chat SSE; answers a clean 'offline' 503 while Strata is down.
- gui/launcher.py: 'external' state - a Strata started outside the Manager is
  detected by its configured port answering the real /v1/models endpoint (with
  the config's key), never by a random process on the port; Start/Stop/Restart
  refuse to touch it, and the status returns to Stopped automatically.
- START-MANAGER.bat: launches with pythonw.exe through 'start' - no console
  window at all (the browser page is the control center); output/logs to
  logs/manager.log; gui/manager.py prints tolerate pythonw (stdout is None).
- serve/web/sprite.svg: one new i-manager glyph (additive).
- gui/test_manager.py: 7 new tests - external detection (incl. key-protected and
  not-strata cases), the gateway (proxied metrics/models/chat-stream, offline 503,
  Manager UI alive), unified page file serving.

59->43 manager tests: 43 pass; serve server/detok/lifecycle/winjob/monitor/
structured/mcp: 143 pass (7 skipped: need engine/GPU).
…nal detection

Two Windows/Linux issues found while validating the unified Manager:

1. START-MANAGER.bat: replaced the parenthesized IF block (whose ECHO of a
   ')' string once produced the real ' . was unexpected at this time.' cmd
   failure) with a goto structure, and dropped the 'start ... >> log 2>&1'
   redirect.  Under pythonw (sys.stdout is None) the redirected handle only
   LOCKED logs/manager.log for the Manager's whole lifetime without ever
   writing it; the Manager now writes its own startup/error lines there
   (gui/manager.py _out() falls back to the log file when there is no
   console).  Verified from cmd.exe and PowerShell, and from a working
   directory outside the repo.

2. External Strata detection is now Strata-specific: a process on the
   configured port is only classified as external Strata when
   GET /api/health answers 200 JSON with service == 'strata' (the config's
   API key is sent when set).  An OpenAI-compatible server (LM Studio,
   plain llama.cpp) that answers /v1/models with object/data has no Strata
   identity and is never claimed; /v1/models remains a readiness check
   only.  A dead Manager PID with a non-Strata process on the port now
   falls through to the crash report instead of being mislabeled external.

tests: +2 bat-structure checks, +4 detection checks (OpenAI-compatible
server not detected, key-protected Strata detected with the configured key,
Manager-owned server precedence, external disappearance -> Stopped).
gui 50/50, serve 143 (7 engine/GPU-skipped), setup rope 19/19.

Validated on the real machine with the installed Q2_0 model: start via
Manager (Starting -> Running in ~18 s), Monitor metrics through the 8275
gateway (real GPU/VRAM/RAM), a real question streamed back through 8275,
Restart (restarting -> running in 6 s, page stays alive), Stop (page stays
alive with clean offline states), external instance via run-q2_0.bat-style
launch detected as Running · External (~1 s) with Chat/Monitor working and
Stop/Start refused, and the external instance's closing returning the
status to Stopped.
…lished model of the same size

The Manager identified a custom GGUF folder only by family+size (its tag), so a different
build of the same size (Qwen3.8-Flash-Next Q2_0 Abliterated next to the original Q2_0) was
"already installed" the moment strata-q2_0.json existed - and Prepare only flashed and
returned.  Fixes the identity, the naming, the setup.py integration and the UX:

- identity is the GGUF files themselves: prepare first compares the chosen shards against
  every strata-*.json's --native/--ple-gguf; an existing config with the exact same files
  is selected, nothing runs
- setup.py --variant LABEL: the existing pipeline, one extra flag.  tag = size-variant, so
  the variant gets its OWN config (strata-q2_0-abliterated.json), its own pack (the fused
  Q2_0 experts are rebuilt from THIS build's shards, never the original's), its own log and
  run script - the canonical strata-q2_0.json is never written or touched
- choices_from_config reads the variant from the config file name (split_size_variant), so
  an installed variant config loads with its family/size/variant and a human title
- the Manager derives the variant label deterministically from the GGUF file/folder names,
  slugified filesystem-safe; a same-named but different build gets a short sha256 suffix
  IN the --variant, so setup.py writes exactly the collision-free name
- UX: Prepare disables itself with "Preparing…", a card shows setup.py's live log, on
  success the model list refreshes, the NEW config is selected and a success toast names it;
  a failed run shows setup.py's error and the log path instead of silently flashing

tests: original+same files -> already; original+different Q2_0 -> second unique config;
two custom variants coexist; the original config is byte-identical after every prepare;
names deterministic/safe; setup.py writes the variant config/pack/script and never the
canonical config; prepare-status exposes failed/ready for the UI settle.
@Ayshinko
Ayshinko force-pushed the feature/gui-manager branch from 98a4074 to b6a87f4 Compare October 3, 2026 09:58
…prepare a stale detection

The real import bug (E:\Model\Q2_0-Abliterated next to the original Q2_0):
  - typing/pasting into the Custom GGUF field ran no detection at all, and the autofill on
    model-select filled the field without one - so the visible path could be A while the
    committed detection (and therefore Prepare) referred to folder B.  Prepare then ran B
    and answered "already installed as strata-q2_0.json", the original's config.
  - detection kept its own shard regex and a MODELS substring scan instead of setup.py's
    naming rules.

Now:
  - the text field is the ONLY path ever detected or prepared.  Every change (typed, pasted,
    autofilled, Browse's "Use this folder") invalidates the previous detection immediately
    and hides Prepare; the new value is detected through one code path, with a generation
    token that drops late responses for an older value, and a successful detection is
    committed only when its directory is exactly the current field value (ggufDet.accept).
    Browse's preview panel never touches the committed state - "Use this folder" writes the
    choice into the field and runs the same change handler, so browse and manual input can
    not diverge.  Prepare refuses (and re-detects) unless ggufDet.prepareAllowed - visible
    path A + internal detection B is impossible.
  - setup.py's own naming rules are the single source of truth: the shard set is its
    SHARD_NAME pattern and the size is its GGUF_QUANT first, with a boundary-safe MODELS
    scan as fallback (IQ2_0 inside a name can no longer match Q2_0); the detected directory
    is resolved so "already installed" compares the canonical files (E:\Model\Q2_0-Abliterated
    stays a different model from E:\Model\Q2_0 and never reuses the original's config).

  Tests: node tests for the field state machine (stale invalidation, late response dropped,
  Prepare refuses a mismatched detection, browse == manual input; gui/web/test_gguf_state.mjs,
  run by the unittest suite when node is present), plus backend tests for the real abliterated
  pattern, the quant boundary rules and canonical dirs.  Verified end-to-end on the real
  folder: detect returns family qwen / Q2_0 / abliterated, prepare builds its own pack and
  writes strata-q2_0-abliterated.json (original strata-q2_0.json byte-identical), and both
  models are listed with the variant selected.
…shes from the Manager

Variant API identity: setup.py wrote model_name without the --variant label, so
strata-q2_0-abliterated.json advertised qwen3.8-flash-next-q2_0 even though the
Abliterated build was what the engine ran.  A variant config now advertises
qwen3.8-flash-next-q2_0-abliterated (api_identity: canonical name + '-' + the
normalized label) and keeps the canonical name as an alias, so clients that
still send the published id are answered while /v1/models, /v1/status and
/api/health truthfully report what is loaded.  Aliases the owner edited by hand
survive a setup run again (saved_aliases).  Canonical naming is unchanged.

Black console flashes: the Manager runs under pythonw (no console), so its
short utility subprocesses - nvidia-smi (manager system_info + setup.gpus and
setup.cpu_info's powershell, all through setup.out/run), and taskkill
(platforms/windows terminate/terminate_force) - each spawned a transient
visible console.  setup.child_flags() now returns CREATE_NO_WINDOW exactly when
the current process has no console (the pythonw context); interactive terminal
runs and Manager-spawned setup children (which have a hidden console to
inherit) keep their exact behavior; Linux is untouched.  The Manager's own
nvidia-smi carries the same flags; taskkill always uses CREATE_NO_WINDOW; the
long-lived server/engine hidden inherited console is preserved.

Tests: tools/test_setup_identity (canonical/variant identity, saved_aliases,
child_flags policy incl. a no-console simulation and the Linux zero),
serve/test_identity (variant identity over real HTTP /v1/models + /v1/status
from a config written as setup writes it), gui.test_manager NoWindowPolicy
(system_info flags, taskkill CREATE_NO_WINDOW, engine spawn keeps its hidden
console).
@Niko1221

Niko1221 commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Thanks for the work and the clear write-up. We are not merging it as it stands, because the Manager opens three ways in that Strata's own server closes on purpose.

  1. Cross-site requests. The POST endpoints (/api/save, /api/start, /api/stop, /api/restart, /api/browse, /api/prepare-gguf) check neither Content-Type nor Origin. Any web page open in the user's browser can send a text/plain POST to http://127.0.0.1:8275/ (a CORS simple request, no preflight) and change the run config (remove the api_key, set host to 0.0.0.0, change the port, context, vision or GPU) or start and stop the server. The page cannot read the answer, but the action happens.
  2. DNS rebinding. There is no Host check, so a page on a hostname rebound to 127.0.0.1 becomes same-origin with the Manager and can read the answers too (the config, the folder listing).
  3. A proxy that adds the key. The proxy puts the config's Authorization header on every forwarded request and drops the browser's Origin, so Strata sees a trusted local client. That makes the Manager a key-injecting door to /settings, /v1/load, /v1/unload and the MCP tools (which run with the user's rights) for any page that can reach port 8275. Strata's own-page guard and API key exist to stop exactly that.

A version we could take needs:

  1. Reject any request whose Origin is not the Manager's own address, and require Content-Type application/json on every POST.
  2. Allow only the Host values 127.0.0.1: and localhost:.
  3. A random per-start token, given to the page when it loads and required on every state-changing call.
  4. No key injection: the proxy forwards the browser's own Authorization and Origin unchanged (or the Manager does not proxy at all).
  5. Loopback only, with no option to bind elsewhere, and a config save limited to the few keys it edits today.

With those, and a test for each (a foreign Origin, a text/plain POST, a wrong Host, a missing token), we would look again. The CREATE_NO_WINDOW part for the Windows launch is welcome as its own small PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants