Skip to content

feat(admin): Settings → Lemonade admin panel (PR-13)#183

Merged
thinmintdev merged 1 commit into
mainfrom
feat/lemonade-admin-panel-pr13
May 23, 2026
Merged

feat(admin): Settings → Lemonade admin panel (PR-13)#183
thinmintdev merged 1 commit into
mainfrom
feat/lemonade-admin-panel-pr13

Conversation

@thinmintdev
Copy link
Copy Markdown
Contributor

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/config on 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 — proxies lemond /internal/config verbatim and attaches a _hal0 block 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_auth gate; POST additionally declares require_writer so cookie sessions ride the CSRF tripwire (same pattern as /api/settings PUT).

Validation guardrails (all refuse 400 lemonade.config_invalid)

Key Rule Source
any must be in IMMEDIATE_KEYS ∪ DEFERRED_KEYS plan §2.2
llamacpp_args must contain --threads N with N >= 2 memory hal0_lemonade_threads_deadlock
flm_args must contain BOTH --asr 1 AND --embed 1 plan §5 + ADR-0009 §1
extra_models_dir must equal /var/lib/hal0/models plan §3 + §6.1

Multiple failed keys aggregate into a single details map (one round-trip per submit, not three). extra_models_dir is refused outright per ADR-0008 §7 (no extra.* 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/set applies different keys with different semantics:

  • Immediate (port, host, log_level, global_timeout, no_broadcast, extra_models_dir) — lemond updates its in-memory state on POST.
  • Deferred (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.effects block 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 applied echo) so a future protocol bump where lemond starts returning rejected keys here doesn't break the UI toast.

Frontend

ui/src/views/Settings/LemonadeAdmin.vue is a new view at /settings/lemonade. The main Settings.vue picks up a small link card pointing to it (not a section migration — anti-scope).

  • Flat-key reactive form bound to all 13 admin keys.
  • Six grouped sections (Service / Concurrency+serving / llama.cpp / FLM (NPU) / whisper.cpp / Stable Diffusion).
  • Per-field "Immediate" / "Deferred (next load)" badge sourced from the backend's _hal0.effects.
  • Inline locked-invariant hints rendered up front on llamacpp_args + flm_args (so the operator sees the rule before typing, not just after submitting).
  • extra_models_dir is read-only on the form (backend refuses divergence anyway).
  • Save sends ONLY the diff; success toast cites the per-request effect split; 400s land as per-key inline field-err lines.

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 /settings deep-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 onto feat/dash-v2-rework, not main. This PR therefore uses the existing toasts.js store + useApi composable. When feat/dash-v2-rework lands, the admin panel migrates onto the new lemonade + toast stores in a follow-up.

Test plan

  • Backend: pytest tests/ -q — 1571 passed, 8 skipped (+25 vs PR-12 baseline).
  • Backend: ruff check src tests + ruff format --check src tests clean.
  • Frontend: npm run build clean (LemonadeAdmin chunk 9.13kB / 3.62kB gzip).
  • Frontend: npx playwright test — 42 passed (+4 new).
  • Live verification on hal0 LXC after merge — load Settings → Lemonade admin, edit log_level, save, observe the "1 immediate" toast.
  • Live verification on hal0 LXC — edit llamacpp_args to drop --threads, save, observe the inline 400 (no lemond /internal/set call).

Anti-scope honoured

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.

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>
@thinmintdev thinmintdev merged commit 7a41f05 into main May 23, 2026
4 checks passed
@thinmintdev thinmintdev deleted the feat/lemonade-admin-panel-pr13 branch May 23, 2026 05:21
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.

1 participant