feat(admin): Settings → Lemonade admin panel (PR-13)#183
Merged
Conversation
Wires the GET/POST /api/lemonade/config surface that the Settings →
Lemonade admin panel needs, plus the Vue view that consumes it.
Backend (src/hal0/api/routes/lemonade_admin.py):
- GET /api/lemonade/config → lemond /internal/config snapshot +
_hal0.{effects, locked} metadata (plan §2.2 partition + the locked
extra_models_dir invariant). UI uses these to render badges + hints
without re-encoding the lists in the frontend.
- POST /api/lemonade/config → forwards a flat-key patch to
/internal/set after validation. Response echoes
{applied, effects:{immediate,deferred}} for the toast copy.
Validation guardrails (refuse 400 lemonade.config_invalid):
- Unknown key (not in plan §2.2 immediate∪deferred sets).
- llamacpp_args missing --threads N or N<2 — per the
hal0_lemonade_threads_deadlock memory, omitting --threads
trips a Vulkan dispatch deadlock under concurrent load.
- flm_args missing --asr 1 or --embed 1 — the FLM NPU trio is
mandatory in v0.2 (plan §5, ADR-0009 §1).
- extra_models_dir diverging from /var/lib/hal0/models — would
silently desync the dashboard catalog (plan §3 + §6.1).
Routes mount under the parent _admin_auth gate in hal0.api; POST
additionally declares require_writer for the CSRF tripwire on
cookie sessions (matches /api/settings PUT).
Frontend (ui/src/views/Settings/LemonadeAdmin.vue):
- Flat-key form bound to all 13 admin-editable keys, grouped into
Service / Concurrency+serving / llama.cpp / FLM (NPU) /
whisper.cpp / Stable Diffusion sections.
- Per-field Immediate / Deferred (next load) badge sourced from
the backend's _hal0.effects (no frontend constant to keep in
sync with plan §2.2).
- Inline locked-invariant hints rendered up front for
llamacpp_args + flm_args; extra_models_dir is read-only on the
form (the backend would refuse a diverging value anyway).
- Save sends ONLY the diff; success toast cites the
"N immediate, M deferred until next load" split lifted from the
response. 400s land as per-key inline errors via fieldErrors.
- Mounted at /settings/lemonade (separate route from the main
Settings page so unsaved-changes-on-leave stays isolated). The
main Settings view picks up a small link card pointing to it.
Tests:
- tests/api/test_lemonade_admin_route.py (25 tests) — covers GET
pass-through + metadata attachment, POST happy path with all
three effect splits, every validator branch (unknown key,
llamacpp_args without --threads / with --threads 0 / with
--threads 1 / accepting --threads=8 equals form, flm_args
missing each trio flag / disable form, extra_models_dir
divergence + canonical), and the empty/non-object/non-JSON
body cases.
- ui/tests/e2e/specs/lemonade-admin.spec.ts (4 tests) — renders
all sections + effect badges, save POSTs only the diff with
the correct toast copy, validation surfaces inline per-field
errors, link from /settings deep-links into the panel.
Note: #177 + #178 (design v2 tokens + new Pinia stores) merged
onto feat/dash-v2-rework, NOT main. This PR uses the existing
toasts.js store + useApi composable. When dash-v2-rework lands the
admin panel migrates onto the new lemonade + toast stores.
Suite: 1571 passed, 8 skipped (+25 from PR-12 baseline). γ-suite
42 passed (+4 new).
Refs: plan §11 PR-13 + §2.2; ADR-0008 §1 + §7; memories
hal0_lemonade_threads_deadlock + hal0_lemonade_v1_load_schema +
hal0_lemonade_flm_npu_install.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR-13 of the v0.2 Lemonade migration (plan §11 + §2.2, ADR-0008 §1 + §7). Adds the Settings → Lemonade admin surface — GET/POST
/api/lemonade/configon the backend plus a new Vue panel at/settings/lemonade.The endpoints wrap
LemonadeClient.internal_config()+internal_set()(PR-3 / #156). No client changes here — this is the UI + API-route surface that consumes them.GET /api/lemonade/config— proxieslemond /internal/configverbatim and attaches a_hal0block carrying the plan §2.2 immediate-vs-deferred key partition plus the locked-invariant pointers (extra_models_dir = /var/lib/hal0/models). The UI sources its effect badges + inline hints from this block so the partition stays in one place.POST /api/lemonade/config— accepts a flat{key: value, ...}patch, validates against the admin allowlist + the locked invariants, forwards to/internal/set, and echoes{applied, effects:{immediate,deferred}}so the toast can cite "N immediate, M deferred until next load" precisely.Both routes mount under the parent
_admin_authgate; POST additionally declaresrequire_writerso cookie sessions ride the CSRF tripwire (same pattern as/api/settingsPUT).Validation guardrails (all refuse
400 lemonade.config_invalid)IMMEDIATE_KEYS ∪ DEFERRED_KEYSllamacpp_args--threads NwithN >= 2hal0_lemonade_threads_deadlockflm_args--asr 1AND--embed 1extra_models_dir/var/lib/hal0/modelsMultiple failed keys aggregate into a single
detailsmap (one round-trip per submit, not three).extra_models_diris refused outright per ADR-0008 §7 (noextra.*repurposing) rather than warned — flipping it leaves the dashboard rendering models lemond can't load.Immediate vs deferred — a note on the chosen wire shape
Lemond's
/internal/setapplies different keys with different semantics:port,host,log_level,global_timeout,no_broadcast,extra_models_dir) — lemond updates its in-memory state on POST.max_loaded_models,ctx_size,llamacpp_backend,llamacpp_args,sdcpp_backend,whispercpp_backend,steps,cfg_scale,width,height,flm_args) — value persists immediately but observed behaviour doesn't change until the next/v1/load.The partition is enforced at the backend (the
_hal0.effectsblock in GET, plus the per-request classification echoed by POST) rather than encoded in the frontend. That keeps two surfaces from drifting when plan §2.2 evolves: the operator's UI label and the place where validation rejection messages reference "next-load" semantics will always agree, because they read from the same Python constants.The split is also classified on the request body's keys (not on lemond's
appliedecho) so a future protocol bump where lemond starts returning rejected keys here doesn't break the UI toast.Frontend
ui/src/views/Settings/LemonadeAdmin.vueis a new view at/settings/lemonade. The mainSettings.vuepicks up a small link card pointing to it (not a section migration — anti-scope)._hal0.effects.llamacpp_args+flm_args(so the operator sees the rule before typing, not just after submitting).extra_models_diris read-only on the form (backend refuses divergence anyway).field-errlines.Tests
tests/api/test_lemonade_admin_route.py— 25 tests covering GET pass-through + metadata attachment, POST happy path with all three effect-split shapes, every validator branch (unknown / no-threads / threads=0 / threads=1 / threads=8 equals-form / asr-missing / embed-missing / asr=0 disable / extra_models_dir divergence + canonical), and empty/non-object/non-JSON body cases.ui/tests/e2e/specs/lemonade-admin.spec.ts— 4 γ-suite tests covering section rendering + badges, save-posts-only-diff with toast assertion, inline validation error on bad llamacpp_args, link from/settingsdeep-link.Heads-up on parallel work
The brief mentioned PRs #177 + #178 (design v2 tokens + Pinia stores:
lemonade,backends,banner,toast,tweaks) being on main. Verified at start of work — they're merged ontofeat/dash-v2-rework, not main. This PR therefore uses the existingtoasts.jsstore +useApicomposable. Whenfeat/dash-v2-reworklands, the admin panel migrates onto the newlemonade+toaststores in a follow-up.Test plan
pytest tests/ -q— 1571 passed, 8 skipped (+25 vs PR-12 baseline).ruff check src tests+ruff format --check src testsclean.npm run buildclean (LemonadeAdmin chunk 9.13kB / 3.62kB gzip).npx playwright test— 42 passed (+4 new).log_level, save, observe the "1 immediate" toast.llamacpp_argsto drop--threads, save, observe the inline 400 (nolemond /internal/setcall).Anti-scope honoured
[CPU]chip (PR-15).LemonadeClient.internal_setorinternal_config(PR-3 territory)./api/lemonade/shutdownor/api/lemonade/cleanup-cacheaction buttons.Settings.vuekeeps its sections, just gains one link card.extra.*namespace (ADR-0008 §7).docs/internal/.Refs: plan §11 PR-13 + §2.2; ADR-0008 §1 + §7; memories
hal0_lemonade_threads_deadlock,hal0_lemonade_v1_load_schema,hal0_lemonade_flm_npu_install.