Lab systems: improvement report

2026-10-02 · repository lab-systems at 24f6a7a · gateway, Krea, Qwen Image and H3 · tested against GPU-free mocks

1. Summary

The backend is careful work. The GPU lease, the durable submission journal, the fsync discipline on the Qwen path and the strict input checks are better than in most hobby projects, and also than in many paid ones. Most of the problems are on the other side: what the user sees while a job runs, and what they can do with a result after it exists.

The five changes with the most value for the effort:

  1. Keep the result on screen. On a 1440×900 screen, the output panel is not visible when you press Generate in Krea or Qwen. The image appears in the middle of a 3,400 px page. A sticky output column fixes this in a few lines of CSS (details).
  2. Queue instead of HTTP 409. If another tab holds the GPU, Qwen and H3 fail immediately with "lab coordinator is busy". There is no owner name, no wait and no retry. A small wait-and-retry in the frontend helps. A real queue in the gateway is better (details).
  3. Progress and finish signals. No app shows step progress or an ETA. Nothing tells you that a 5-minute video is done when you are in another tab. ComfyUI already sends step progress on its websocket, and the gateway already sees every phase change.
  4. Gallery actions. No app can delete an item, mark a favorite, filter, or send a result to another app (for example, a Qwen image as the H3 start frame). The Qwen gallery loads full-size PNGs and has no paging or retention.
  5. Fix the H3 frontend state bugs and move H3 to the shared studio UI. H3 forgets a running job on reload, stops polling on one network error, and has its own look and CSS. Moving it to image-studio removes these bugs and the inconsistency together.

On the backend, the most important bug is a timeout mismatch: the gateway waits 30 s for an H3/Qwen unload, but the backend can take up to 75 s. A slow unload then shows as a false Krea 502 (BUG-1). I verified this and three other findings myself in the code. The rest of the bug list comes from code review without a GPU, so treat "medium" confidence items as leads, not facts.

Safety note from this session. This Mac runs a real Lemonade server on 127.0.0.1:13305. Krea had that address hard-coded, so my first mock run pointed the gateway at the real server. I stopped it within seconds, before any generation or unload request. I only sent GET requests, and the model stayed loaded. The mock stack now uses its own ports, refuses to start if any port is in use, and Krea has an optional KREA_LEMONADE_URL override (default unchanged).

2. Suggested roadmap

Phase 1: quick wins (1–2 days)

  1. Sticky output column, batch result strip (UX-1, UX-2)
  2. Ctrl/Cmd+Enter, block Enter-to-submit in number fields (UX-6)
  3. H3: resume job after reload, retry polling, fix Stop (BUG-6…8)
  4. Gateway unload timeout, idle "dirty" flag (BUG-1, BUG-2)
  5. Wait-and-retry on 409 with owner name (FEAT-1a)
  6. Tab title / favicon badge when a job finishes (FEAT-3)
  7. scripts/test-all.sh and a pre-push hook (OPS-1)

Phase 2: daily-use features (1–2 weeks)

  1. Step progress and ETA via ComfyUI websocket (FEAT-2)
  2. Thumbnails, gallery paging, delete, favorites (FEAT-4, FEAT-5, PERF-1)
  3. Send image to H3 start frame / Qwen edit (FEAT-6)
  4. Drag-and-drop and paste for images (UX-7)
  5. Persist form state per app (UX-5)
  6. Gateway queue (FEAT-1b)
  7. Deploy script with drain mode (OPS-2)

Phase 3: structure (ongoing)

  1. Move H3 UI to image-studio (ARCH-5)
  2. Share job poller / batch loop in image-studio (ARCH-6)
  3. Merge H3 and Qwen submit/reconcile pipelines (ARCH-2)
  4. Split h3-lab/server.py and Qwen app.js (ARCH-1, ARCH-7)
  5. Model/LoRA manifest as data (ARCH-4, FEAT-11)
  6. Move dated validation notes out of the runbooks (OPS-5)

3. How I tested

I added a mock stack under dev/mock/. It replaces only the two GPU services, so the real gateway, Krea server and H3/Qwen server run unchanged:

  • mock_backends.py has a fake ComfyUI (/prompt, /queue, /history, /free, /interrupt, /object_info, /system_stats) and a fake Lemonade (generation, load/unload, health, SDA capability). Outputs are placeholder PNGs and MP4s with audio. They show the prompt, seed and node list, so you can see what the backend really sent.
  • run.sh creates sparse model files with the exact sizes the readiness checks need, then starts everything. The UI is at http://127.0.0.1:7861/.
  • A prompt with MOCK_ERROR fails the job. A prompt with MOCK_SLOW makes it 5× longer, which is useful to test contention and reloads.
  • Not mocked: local Qwen PE (needs the isolated runtime) and the remote LLM enhancer (rejects loopback targets and uses Linux getifaddrs).

I then drove all three tabs with a headless Chromium at 1440×900 and 390×844: single images, 4-image batches, cross-tab contention, an H3 reload during a job, the lightbox and the galleries. Four review agents read the gateway, the H3/Qwen backend, the Krea backend with repo tooling, and all frontend code. I checked their main claims against the source.

4. UI walkthrough

4.1 The result is never on screen

Krea and Qwen use a two-column layout. The output panel stretches to the height of the form, and the image is centered vertically in it. The Qwen form is about 2,400 px tall. So the result appears around y=1,400: below the first screen and above the Generate button. At no scroll position can you see the Generate button and the result together. During a batch you see a dark empty panel and one status line.

Qwen during generation
Qwen at the Generate button. The right column is empty, but the job runs. The result is about 1,000 px above this point.
Qwen full page with result in the middle
Full page after a 4-image batch. The result is in the vertical middle of the panel. Only the last image of the batch shows.

Fix: position: sticky; top: 16px; align-self: start; max-height: calc(100vh - 32px) on the output panel (in image-studio.css, so both apps get it). Show batch results as a strip of thumbnails under the main image. Keep the previous image under a busy overlay, do not hide it.

4.2 "Lab coordinator is busy"

I started a slow 4-image Krea batch, switched to the Qwen tab and pressed Generate. Qwen got HTTP 409 and stopped. The error panel says only lab coordinator is busy. The status line under the button still said "Submitting…" (a stale status). The Krea batch finished 50 s later, and nothing retried.

Qwen busy error
The only feedback after a 409. It does not say who holds the GPU, for how long, or what to do.

Qwen already has a retry loop for 409, but only for image 2 and later of its own batch (static-qwen/app.js:1363). Krea batches release the lease between images, so a well-timed submit from another tab can also take the GPU in the gap and make the Krea batch fail on its next image.

Fix, small: in all three apps, on 409, read /api/coordination/status, show "Waiting for Krea (2 m 10 s)…", and retry until it is free or the user cancels. Fix, complete: a FIFO queue in the gateway (FEAT-1b). The coordinator already serializes all work, so a queue is a natural next step. It also fixes batch interleaving.

4.3 H3 forgets a running job on reload

I started a slow H3 video and reloaded the page. The form came back empty: no prompt, no status, Generate enabled. The gateway still showed generating h3. The video later appeared in the gallery, but the UI did not say anything. Qwen solves this with sessionStorage (static-qwen/app.js:1251-1273). H3 keeps the job ID only in a closure.

H3 after reload
H3 after reload during a job: looks idle, prompt lost.
H3 gallery card
The finished video in the H3 gallery: no poster frame, so every card is a grey box until you press play.

4.4 Noise and density

The remote LLM enhancer panel shows four paragraphs of warnings even when the gateway has no enhancer configured, and then says "Remote enhancement is not configured". In Krea, this pushes the LoRA stack and the Generate button down to y≈1,770. In Qwen, a "Model availability" box with red operator diagnostics ("Pinned llama.cpp executable is unavailable") is the fourth element on the page.

Krea full page
Krea full page. The enhancer panel takes ~380 px with nothing usable inside it.
H3 full page
H3 has a different header, button size, label style and CSS from Krea and Qwen. The Turbo label is cramped. LoRA strength looks enabled but is disabled.

Fix: hide the enhancer panel completely when it is not configured, and collapse the warnings to one line with a "details" link when it is. Move "Model availability" into a collapsed <details>, and show it open only when the selected checkpoint or profile is unavailable. Put only the controls you change often above the fold: prompt, size, count, Generate.

4.5 Other observations

  • The lightbox is good. It has navigation, zoom, metadata, Reuse settings and "Use output as reference". It needs Delete, Favorite, "Same settings, new seed" and "Send to H3".
  • Mobile works without horizontal scroll, but the shell header and the app header use ~40% of the first screen, and the page scrolls inside the iframe. On mobile the red "Unload all models" button is the largest element. Make it smaller or put it in a menu.
  • The shell status pill is useful. It could also show the elapsed time, a dot on the tab that owns the GPU, and a Stop button for H3/Qwen jobs.
  • Krea "Lemonade ready" is static text. It does not come from a health check.
Qwen lightbox
Qwen lightbox.
Mobile views
390 px wide: H3 and Qwen tabs.
Shell
The shell at 1440×900. The first screen of Krea shows no Generate button and an empty output panel.

5. All ideas

Effort: S hours, M days, L a week or more. Value is my estimate for a single-user lab. seen means I reproduced it in the mock UI. verified means I checked it in the code myself. Other items come from the review agents and have a confidence note where it matters. File references are relative to apps/ unless they start with another top folder.

5.1 Features

IDIdeaWhat and whyEffortValueRefs
FEAT-1aWait and retry on 409On a 409, show the owner and elapsed time from /api/coordination/status and retry until the lease is free or the user cancels. Today the first image fails at once. seenShighh3-lab/static-qwen/app.js:1363; h3-lab/static/app.js; krea-ui/static/app.js
FEAT-1bGateway job queueFIFO of stored request bodies with gateway job IDs and a queued phase. Submit four Qwen prompts and a video, then walk away. Also stops batches from different apps from interleaving.Lhighlab-ui/server.py (Coordinator)
FEAT-2Step progress, ETA, latent previewConnect to ComfyUI /ws with the existing client IDs, relay progress (step/max) and optional preview frames through /api/jobs/<id> or SSE. For Krea, show an ETA from a rolling median of past durations. No app shows progress today. verifiedMhighh3-lab/server.py:3157, 3437 (client_id); start.sh (--preview-method)
FEAT-3"Done" signalChange the tab title and favicon when the shell sees generating → idle, plus an optional sound. Browser notifications need HTTPS, so for the LAN the option is an ntfy/webhook URL that the gateway calls on lease release.Shighlab-ui/static/app.js:69-79; lab-ui/server.py:316
FEAT-4Gallery delete, favorites, filterNo app has a delete endpoint. Krea's ring buffer removes old images with no choice. Add DELETE /api/gallery/<id> (refuse non-terminal jobs), a favorite flag that retention keeps, and a text/mode/seed filter.Mhighkrea-ui/gallery.py; h3-lab/server.py:2507, 2782
FEAT-5A/B compare in the lightboxTwo-up view of two results (same seed, different profile or checkpoint). This is the core workflow of a lab with several checkpoints and Turbo profiles.Mmedpackages/image-studio/image-studio.mjs
FEAT-6Cross-app hand-off"Send to H3 as start frame" and "Use as Qwen edit input" from any lightbox. All apps are same-origin, so a BroadcastChannel("lab") message plus a tab switch is enough. The lightbox already has an extraActions hook.Mhighimage-studio.mjs:58; h3-lab/static/app.js (image.onchange)
FEAT-7Video continuation"Use last frame as start frame" on an H3 result: seek to the end, draw to canvas, feed the crop editor. With FEAT-6 this gives chained clips.Mmedh3-lab/static/app.js
FEAT-8Seed variationsOne click: same settings, seed+1 or random. Today this needs Reuse settings, then a seed edit, then Generate.Smedlightbox actions
FEAT-9Preview the local Qwen PE rewriteLocal PE runs only inside generation, so you cannot read or edit the rewrite before a 2K render. Add POST /qwen/api/rewrite and let generate reuse the stored rewrite. A content-addressed cache would also skip the unload/reload cycle on repeats.Mmedh3-lab/server.py:3168-3211, 102
FEAT-10Settings inside the filesWrite a compact lab iTXt chunk (prompt, effective prompt, seed, model, profile) into PNGs, and metadata tags into MP4s, so downloaded files keep their settings.Smedh3-lab/server.py:2373 (finalize); krea-ui/gallery.py
FEAT-11LoRA registryKrea LoRAs and H3 EXTRA_LORAS are hard-coded, and Qwen has none. A loras.json (file, AIR, family, default strength, size/sha) read at startup makes adding one a data change.Mmedkrea-ui/server.py:58; h3-lab/server.py:291
FEAT-12Upscale / hires pass"Upscale this" from the gallery: a model upscaler or a draft→2K img2img pass as a Qwen profile. Generate drafts at 1 MP, then upscale only the good ones.Mmedh3-lab/server.py (build_qwen_workflow)
FEAT-13Unified historyA "Recent" view in the shell that merges the three galleries by time, with deep links into each tab.Mmedlab-ui/static
FEAT-14Show what is loadedCombine Lemonade loaded models and ComfyUI /system_stats into the status: "Loaded: Qwen 2.1 · VRAM 21.3 GB". The Unload button can then say what it frees.Mmedlab-ui/server.py (status)
FEAT-15Stop button in the shellWhen the owner is h3, show Stop in the shell. H3 already has /api/interrupt. Make it job-scoped first (BUG-11).Smedh3-lab/server.py:3488

5.2 UX

IDIdeaWhat and whyEffortValueRefs
UX-1Sticky output columnSee 4.1. One CSS rule in image-studio. seenShighpackages/image-studio/image-studio.css
UX-2Batch strip, keep previous imageShow all images of a batch as thumbnails. Keep the last image under a busy overlay during generation. Today Krea hides it and both apps show only the last image. seenSmedkrea-ui/static/app.js:345; static-qwen/app.js
UX-3Hide the unconfigured enhancerDo not render the enhancer panel when the gateway has no enhancer. When configured, collapse the warnings to one line. seenSmedlab-ui/static/prompt-enhancer.js
UX-4Collapse "Model availability"Show it only when the selected option is unavailable. Put operator diagnostics in the API docs or a status page. seenSmedh3-lab/static-qwen/index.html
UX-5Persist form stateSave prompt, mode, size, steps, seed and LoRAs per app in localStorage, with "Reset to defaults". The existing reuse-settings code can do the restore.Mhighall app.js
UX-6Keyboard: Ctrl/Cmd+Enter, no Enter-submitEnter in Steps, Seed, Width or Height submits the form and can start an 8-image 2K batch. Block implicit submit. Add Ctrl/Cmd+Enter as one shared helper.Shighkrea-ui/static/index.html; static-qwen/index.html
UX-7Drag-and-drop and pasteFor Qwen references, H3 start frame and references, and enhancer attachments. All paths already end in a File[] function.Mhighstatic-qwen/index.html:52; h3-lab/static/index.html:34, 45
UX-8GPU owner on the tabsA dot on the tab that owns the lease and a "busy" hint on the others before the user submits.Smedlab-ui/static/app.js:53-68
UX-9H3 gallery: poster, lightbox, pagingGrey cards, no lightbox, all details on the card, and preload=metadata for every video. Use the shared lightbox with a video slot and a poster frame. seenMmedh3-lab/static/app.js:520-608
UX-10Mobile headerCombine the shell and app headers on narrow screens and make Unload smaller. seenSlowlab-ui/static/app.css; h3-lab/static/app.css
UX-11AccessibilityKrea announces its 100 ms timer through aria-live. The shell announces the countdown every 2 s. The H3 crop canvas has no keyboard control. The Krea LoRA card is one big label. Polling continues in hidden tabs.Smedkrea-ui/static/index.html:95; lab-ui/static/index.html:21; h3-lab/static/app.js:123-157
UX-12Live Krea status pill"Lemonade ready" is static text. Use the coordinator or Lemonade health. verifiedSlowkrea-ui/static/index.html:19

5.3 Bugs

IDBugWhat happensEffortValueRefs
BUG-1Unload timeout 30 s vs 75 sThe gateway coordinator HTTP client has a 30 s timeout. The H3 control unload waits up to 75 s for ComfyUI /free. A slow unload gives a false Krea 502 and a failed explicit/idle unload, while the unload still finishes. verifiedShighlab-ui/server.py:1359; h3-lab/server.py:696
BUG-2Idle unload repeats foreverAfter a successful unload, the next deadline is set again, so both backends get an unload call every 30 min (~48 per day) with nothing loaded, and the UI always shows a countdown. Add a "dirty" flag. verifiedSmedlab-ui/server.py:512-513, 547
BUG-3Idle worker can die silently_idle_loop has no try/except. If it dies, idle unload and H3 monitor recovery stop with no sign in the status. Confidence: medium.Smedlab-ui/server.py:534-554
BUG-4Krea lease released while Krea may still runOn an upstream timeout or read error, the Krea lease is released although Lemonade may still be generating. A following H3 claim then unloads Lemonade under it. Krea leases are also memory-only. Confidence: medium.Mmedlab-ui/server.py:907-926
BUG-5H3 gallery metadata not fsyncedH3 writes {id}.json with write_text and replace and no fsync. The runbook says it fsyncs. Qwen uses atomic_json. verifiedSmedh3-lab/server.py:3428-3432, 617
BUG-6H3 reload loses the jobSee 4.3. Copy the Qwen sessionStorage resume. seenShighh3-lab/static/app.js:338-391
BUG-7H3 stops polling on one errorAny fetch or JSON error ends polling and the UI shows a failure while the job runs on. Qwen retries with backoff.Shighh3-lab/static/app.js:380-390
BUG-8H3 Stop and Reuse racesStop does not check the response, and a late poll can undo the "interrupted" state. "Reuse settings" in Ref2VA mode re-enables Generate during a job.Smedh3-lab/static/app.js:507-510, 613-616
BUG-9Qwen stale "Submitting…" statusAfter a 409, the error panel shows the error but the status under the button still says "Submitting…". seenSlowstatic-qwen/app.js (jobStatus)
BUG-10INPUT_DIR grows without limitUploads up to 256 MiB, restored references and start images are never deleted. A start image is also leaked when later validation fails.Shighh3-lab/server.py:1355-1375, 1617
BUG-11/api/interrupt is not job-scopedIt interrupts whatever ComfyUI runs, and the H3 UI reports "interrupted" even on 502. Accept a job ID: remove it if pending, interrupt only if running.Smedh3-lab/server.py:3488-3495
BUG-12Journal rewrites on each pollPolling a completed job rewrites the journal with four fsyncs every time. H3 status also globs every gallery JSON per poll. Skip unchanged writes and read {gallery_id}.json directly.Smedh3-lab/server.py:2479, 545-550, 2726, 2759
BUG-13Non-atomic timing updatepersist_generation_timing uses a fixed .json.tmp name from concurrent poll threads with no fsync. Readers can see a partial file. Confidence: medium.Smedh3-lab/server.py:2759-2779
BUG-14Krea gallery resets after each imageAfter each image, Krea reloads page 1 of the gallery, so older pages the user loaded disappear and the grid flickers during a batch.Smedkrea-ui/static/app.js:379, 280
BUG-15Krea tests run three timesfrom test_gallery import HTTPTests makes discovery collect the HTTP tests again, and a subclass inherits them a third time. ~14 of 40 tests are duplicates and most of the 20 s runtime. verifiedSmedkrea-ui/test_sda.py:9, 61
BUG-16Gateway test fails on macOSOne enhancer test does not patch _local_addresses, so it gets 503 instead of 504 on non-Linux.Slowlab-ui/tests/test_prompt_enhancer.py:462
BUG-17Small Krea validation holesAn integer with 400 digits raises OverflowError, which kills the handler with no 400. A non-dict Lemonade health reply raises AttributeError. Floats and booleans are truncated to ints.Slowkrea-ui/server.py:99-106, 35, 43
BUG-18Safe default bindsKrea defaults to 0.0.0.0:7861 (the gateway port!) and H3 to 0.0.0.0:7862. A manual start bypasses the coordinator on the LAN. Default to 127.0.0.1.Smedkrea-ui/server.py:350; h3-lab/server.py:3500
BUG-19Small gateway edge cases--idle-timeout 0 gives a tight unload loop. /api/generate/ takes the lease and unloads H3, then Krea returns 404. Streaming reads use 128 KiB blocks (would stall SSE).Slowlab-ui/server.py:169, 835, 1105
BUG-20H3 crop re-encodes on each keystrokeThe enhancer listens to every input event and snapshots the H3 crop as a 1280 px JPEG data URL each time.Slowlab-ui/static/prompt-enhancer.js:205; h3-lab/static/app.js:624

5.4 Performance and storage

IDIdeaWhat and whyEffortValueRefs
PERF-1Thumbnails and cachingGallery cards load full 2–4 MP PNGs with no-store. Write a 512 px WebP at finalize (and an H3 poster frame) and serve it as immutable. Krea image IDs are never reused, so they can be cached too.Mhighh3-lab/server.py:2373, 3290; krea-ui/server.py:227
PERF-2Gallery paging and indexThe Qwen and H3 galleries stat and parse every item on each request, with no paging and no retention. H3 also reloads every 30 s. Add ?before=&limit= and a small SQLite index.Mmedh3-lab/server.py:2507, 2782; h3-lab/static/app.js:619
PERF-3Cache enhancer status probes/qwen/api/config runs the enhancer status three times, including nvidia-smi and a subprocess probe. The UI polls this endpoint.Smedh3-lab/server.py:2904-2906, 932-947
PERF-4Fewer image decodesReferences are fully decoded several times per request (validate, build, persist, fingerprint). Read sizes without load() and memoize per request.Slowh3-lab/server.py:1767-1780, 1966
PERF-5Stream uploads to diskVideo uploads up to 256 MiB are read fully into RAM, then copied into BytesIO.Slowh3-lab/server.py:3404, 1342
PERF-6Disk report and retentionA script that shows sizes per category and finds orphans (journals without gallery, gallery without journal, old inputs), with an opt-in prune.Smedscripts/

5.5 Architecture and maintainability

IDIdeaWhat and whyEffortValueRefs
ARCH-1Split h3-lab/server.py3,504 lines in one file. Seams: config, storage/journal, comfy client, enhancer, h3, qwen, http. Keep server.py as the entry point. Do it after ARCH-2, with tests split to match.Lhighh3-lab/server.py
ARCH-2One submit and reconcile pipelineH3 and Qwen have the same seven-way exception ladder for submit, and the same reconcile algorithm in qwen_job/h3_job. One parametrized function removes ~150 lines and fixes BUG-5 and BUG-10 for H3 as a side effect.Mhighh3-lab/server.py:3083-3254, 3410-3477, 2994-3082
ARCH-3One range-serving function, request validatorsHTTP Range handling is copied three times. Seed, int and prompt checks are repeated. Small helpers make the workflow builders ~30% shorter.Smedh3-lab/server.py:2855, 3301-3357
ARCH-4Model pins as dataSizes and hashes are in server.py, download scripts, MODELS.md and README. One models.json read by all of them stops drift.Mmedh3-lab/server.py:25-70, 120-266; scripts/; models/MODELS.md
ARCH-5H3 on image-studioH3 has its own minified CSS, header, gallery and no lightbox. Moving it to the shared package gives one look, a lightbox, mobile breakpoints, and fewer bugs. seenMhighh3-lab/static/; packages/image-studio/sync-assets.sh:34
ARCH-6More shared frontend codeTwo or three copies each of: requestJSON, job poller with resume, batch loop with "stop after current", setBusy, formatTime, gallery card builder. Put them in image-studio with tests.Mhighstatic-qwen/app.js:1239-1330; krea-ui/static/app.js:116-135, 300-396
ARCH-7Split Qwen app.js2,161 lines and ~30 module-level variables. Start with the guide editor (~400 lines, own state). Replace the regex/vm test harness with real module tests.Lmedstatic-qwen/app.js:852-1237; h3-lab/test_qwen_presets.mjs:79-85
ARCH-8Serve shared assets from the gatewayThe gateway already serves /prompt-enhancer.js to all apps. It could serve image-studio too, which removes the copy-and-check sync. Keep the copies only if standalone deploys matter. If you keep them, enforce sync-assets.sh --check in a hook or test.Smedpackages/image-studio/sync-assets.sh
ARCH-9Split gateway server.py1,472 lines: coordinator, proxy, http utils, cli. Coordinator tests then do not need the HTTP layer. Body-length parsing is duplicated three times.Mmedlab-ui/server.py:150-581, 766-817, 1019-1032
ARCH-10Workflow graph builderNode IDs are hand-numbered strings, and some branches overwrite node "6". A tiny Graph.add() builder removes collision risk. The tests assert exact graphs, so plan the test updates.Mlowh3-lab/server.py:1995, 2031
ARCH-11Krea: constants and pure functionsSDA limits are written in four places, and the capability check compares an ordered list. Pull out gallery_settings() and one header helper.Smedkrea-ui/server.py:44-48, 128, 176, 319-329

5.6 Operations and tooling

IDIdeaWhat and whyEffortValueRefs
OPS-1One test commandscripts/test-all.sh: sync check, three Python suites, node tests, node --check, OpenAPI JSON, bash -n, mock tests. Run it in a pre-push hook, or in Gitea Actions if a runner exists. Then the runbooks can drop hard-coded test counts.Shighkrea-lab.md:131; lab-ui.md:117; h3-video-lab.md:385
OPS-2Deploy scripts with drainAdd POST /api/coordination/drain (new claims get 409 "draining", running jobs continue). A deploy script then: runs tests, drains, waits for idle, installs from a manifest, compares hashes, restarts, smoke-tests. Today this is a manual runbook sequence with a race between "check idle" and "restart".Mhighlab-ui.md (safe deployment); krea-lab.md:204
OPS-3Log coordinator transitionsClaims, releases, unload results and journal restores only change in-memory fields. One log line per transition makes incidents traceable. Also drop the status-poll access logs (one line per browser every 2 s).Shighlab-ui/server.py:1218
OPS-4/healthzBackend reachability, idle worker alive, journal state, source hash. One curl replaces seven in the runbook.Smedlab-ui/server.py
OPS-5Slim the runbooksh3-video-lab.md is 76 KB with lines of 2,500 characters. Dated validation stories are history, not operations. Move them to docs/validation/ and keep the runbook to current state and commands.Mmedh3-video-lab.md:400-460; README.md (Qwen row)
OPS-6Container buildThe image clones and installs ComfyUI twice. The BF16 PE runtime (a second PyTorch stack, "rollback only") could be a build target. Prefix ComfyUI and server logs, keep stderr from the CDI step, add a HEALTHCHECK.Smedh3-lab/Containerfile; Containerfile.base; start.sh; run.sh
OPS-7Unit dependenciesRequires=krea-ui.service restarts the gateway when Krea restarts, which kills H3 proxy threads. Wants= plus After= is enough because the gateway fails closed.Smedlab-ui/deploy/lab-ui.service:5
OPS-8Lint baselineruff --select F,E9 is already clean, so it costs nothing to add. Do not adopt the formatter (17 of 24 files would change).Slowrepo root
OPS-9Mock stack improvementsMock the remote enhancer (needs a dev flag for the loopback check and a non-Linux address lookup) and the local Qwen PE. Add a ComfyUI websocket to the mock when FEAT-2 starts.Smeddev/mock/; lab-ui/prompt_enhancer.py:146

5.7 Security (only what matters on a trusted LAN)

IDIdeaWhat and whyEffortValueRefs
SEC-1Host header allowlistWithout --public-origin, the expected origin comes from Host, so a DNS-rebinding page passes the same-origin check. It could then read galleries, start jobs, unload models and use the enhancer. Check Host against a short allowlist.Smedlab-ui/server.py:1307-1308
SEC-2Network limits in the unitIPAddressDeny=any plus IPAddressAllow= localhost, LAN and the enhancer host.Slowlab-ui/deploy/lab-ui.service
SEC-3Limit concurrent enhancer callsEach call can hold a thread for 30 min with up to 24 MiB of body. A semaphore of about 4 is enough.Slowlab-ui/prompt_enhancer.py:26, 34

6. Things I would not do

  • Do not merge the gallery storage backends. Krea's SQLite blobs and Qwen's per-item directories with references serve different needs. Align the API (paging, delete, favorites), not the storage.
  • Do not add a web framework or a frontend build step. The stdlib servers and vanilla JS are small enough. The problems are file size and duplication, not tooling.
  • Do not adopt a code formatter now. The churn would hide real changes in review.
  • Do not create a shared Python HTTP package yet. It would need the same copy-sync as image-studio for little gain. Split h3-lab/server.py first.

7. Appendix

Changes made in this session

  • dev/mock/mock_backends.py, dev/mock/run.sh, dev/mock/test_mock_backends.py: GPU-free mock stack and its tests.
  • apps/krea-ui/server.py: optional KREA_LEMONADE_URL (loopback only, default unchanged) and test_lemonade_url.py. This was needed so the mock stack never talks to a real Lemonade.
  • README.md: short section on the mock stack. .gitignore: dev/mock/.state/.

Run it

dev/mock/run.sh, then open http://127.0.0.1:7861/. Put MOCK_SLOW or MOCK_ERROR in a prompt to simulate long or failing jobs.

Qwen gallery with mock outputs
Mock outputs in the Qwen gallery. Each placeholder shows the prompt, seed, steps, checkpoint and node list that the backend sent to ComfyUI.