154 Commits
Author SHA1 Message Date
Hanzo AI b89cf707a2 studio v0.18.3: monochrome shell chrome — coherent with the Home + @hanzo/gui design system
shell.css (chat sidebar · Developers dock · GPUs panel) was still on the old
charcoal palette; snap it to the canonical monochrome tokens the Home already
uses: page #000 · surface #0a0a0a · raised #1a1a1a · border #1f1f1f · text
#ededed · dim #888 · brand/CTA #fff. Semantic green (online) / red (error) kept.
The shared shell now matches the create experience + the chat's own empty-state
and friendly-error work already on main.
2026-07-25 10:17:15 -07:00
zandClaude Opus 4.8 20d2f809af fix(studio/create): RED findings — dedup double-bill, dim type-confusion, alg over-match, sub-caption XSS (v0.18.2)
- F1 (MED): _dedup_key now keys on stable identity (org|kind|prompt|refs|uploads),
  not the random-tagged output_prefix, so two identical creates collapse to one
  dispatch (the anti-double-bill guard actually fires now).
- F2 (LOW): _image_graph/_video_graph raise DispatchError on non-numeric dims (clean 400, not 500).
- F3 (LOW): alg allowlist tightened to the exact JWS set in gateway_auth + documents.
- F4 (INFO): esc() the gallery sub-caption (crafted-upload filename_prefix XSS).
Red verified /v1/library/image cross-org/traversal/secret-exfil fail closed. +8 tests, 65 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 09:31:48 -07:00
zeekayandClaude Opus 4.8 cadc589875 studio v0.18.1: text-to-image default create + fail-closed gateway auth + UX fixes
P1 — the primary Create now produces something, and Chat reaches the gateway:
- middleware/gateway_auth.py: ONE fail-closed allowlist for the browser-edge
  gateway bearer — a server key first, else pk-/asymmetric-JWT only; the HS256
  hanzo.id session token and sk-/hk-/rk- secrets are NEVER forwarded (the old
  "unsupported signing method: HS256" 502). copilot._resolve_target adopts it.
- Z-Image-Turbo text-to-image lane: _image_graph + dispatch_image + POST
  /v1/library/image. The composer's default up-arrow (words, no photo) now
  generates an image instead of the old "attach a photo first" dead end.
- composer defaults to an IMAGE model (hz_create_model, decoupled from the chat
  model); routes by modality (assistant -> Chat via window.HanzoChat, else
  image/video/fix/compose); create() ALWAYS shows a generating state + friendly
  retry, never a silent no-op.
- shell.js: copilot errors render one friendly line (never raw JSON); chat empty
  state (assistant identity + 3 starters); one public HanzoChat.send hook.

P2 — friction on the common path:
- first-class Download on every tile AND in the viewer (PNG / JPG / WebP)
- opaque fixed header + sticky live-bar (content no longer scrolls under them)
- ARCHIVED badge only on truly deleted/flagged items (active work is unbadged)
- mobile: ~30 filter pills collapse to one scrollable row; tab-bar scroll fade

P3 — composer pill tooltips, per-model descriptions, code model hidden, and the
</> request-preview gated behind Developers mode.

Tests: gateway_auth_test (allowlist refuses HS256/sk-, accepts pk-/asymmetric),
copilot_test (never forwards the HS256 session token), create_image_test
(Z-Image graph shape + served-HTML invariants: default-model-is-image,
Create-always-shows-a-state, first-class Download). Green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 09:04:03 -07:00
zeekayandClaude Opus 4.8 267735d0bd studio: office documents — a non-GPU document pipeline (PDF/DOCX/XLSX/PPTX/MD)
Add a first-class office-document capability ALONGSIDE the ComfyUI/GPU creative
pipeline: structured content (typed template fields, filled by a person or the
AI) → a pure-Python renderer → a real .pdf/.docx/.xlsx/.pptx/.md. No graph, no
GPU. Orthogonal subsystem — not bolted onto graphs.

- middleware/doc_render.py: one neutral IR per archetype (doc/sheet/deck) and
  exactly one renderer per format (DRY). PDF via WeasyPrint (full-CSS monochrome
  print style) with a self-contained pure-Python PDF fallback (valid %PDF, zero
  native deps — so CI/model-less pods still render). DOCX/XLSX/PPTX via
  python-docx/openpyxl/python-pptx.
- middleware/documents.py: one declarative CATALOG of 22 common templates
  (Documents 15, Spreadsheets 4, Presentations 3). Each template's typed fields
  drive the form, the AI-fill JSON, and the renderer inputs (one schema). Per-org
  store (orgs/<org>/documents, atomic index.json + <id>/doc.json), rendered on
  demand. Endpoints (/v1 house style): GET /v1/documents, POST
  /v1/documents/generate, GET /v1/documents/library, GET|PATCH|DELETE
  /v1/documents/{id}, GET /v1/documents/{id}/download?format=. Registered via the
  same injected tenancy resolvers as stacks_store.
- AI-fill is a layered enhancement — the form path renders with NO LLM, so a
  broken/unauthorized gateway never blocks creation (422 needFields → prefilled
  form). Reuses the shipped Chat's working token: the browser sends window.HZ.pk
  (publishable pk- key) as the bearer; the backend forwards it fail-closed —
  server key, else a gateway-verifiable pk-/RS-JWT; an HS256 session token or
  sk-/hk- secret is NEVER forwarded. Model default enso (window.HZ.model).
- studio_home.html: a "Documents" Home tab (searchable catalog + your documents
  with Download PDF/Word/Excel/PowerPoint) + describe/fill dialog; replaced the
  dead doc/sheet/paper composer placeholders with one "Office documents" card.
- deps: weasyprint/python-docx/openpyxl/python-pptx (requirements.txt) + pango
  native libs (Dockerfile). CI builds the image; not built locally.
- tests-unit/documents_test: catalog invariants, full template×format render
  matrix asserted by magic numbers, AI-fill parse + fail-closed bearer allowlist,
  and the REST surface incl. per-org isolation (44 tests, green).
- version 0.17.24 → 0.18.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-25 08:14:55 -07:00
Hanzo AI 657e2b5fc0 release: studio v0.17.24 — monochrome create experience + dropdowns/switcher + Enso default live 2026-07-24 23:44:48 -07:00
ce491d2848 feat: per-GPU gpu-jobs queue + worker-mode gate (no hidden runs) (#30)
Make EVERY studio render flow through the org's visible gpu-jobs queue and
eliminate the direct-to-127.0.0.1:8188 bypass. Studio-side only (cloud CLI /
worker two-lane claim owned by the cloud agent).

- gpu_dispatch: per-GPU targeting. X-Target-GPU -> taskQueue "gpu:<identity>"
  (namespace stays gpu-jobs); an explicit target always enqueues. Untargeted =
  shared "gpu-jobs" lane.
- server.post_prompt: in --worker-mode refuse a direct /prompt POST without
  X-Worker-Token (403) via worker_client.reject_untrusted_worker_submit (one
  policy gate). The only accepted execution is a claimed job over
  /v1/worker/execute. Legacy prompt_router push is OFF unless
  STUDIO_LEGACY_PUSH_ROUTER=1 -> gpu-jobs enqueue is the ONE submit path.
- worker_client: /v1/worker/execute validates X-Worker-Token, runs the graph on
  the local prompt_queue, scopes outputs to the job's org, returns {prompt_id}.
- studio_home: /v1/render-queue + /v1/nodes are AUTHORITATIVE — read the cloud
  tasks engine's gpu-jobs activities (real claiming identity, status ->
  queued/running/done/failed, lastHeartbeatTime, per-node depth). render_jobs.json
  is only a ~2s pre-claim placeholder. X-Target-GPU forwarded through _queue_prompt.
- UI: "Run on <gpu>" chips (studio_home.html/shell.js) set X-Target-GPU; in-flight
  rows show the REAL worker + queued/running state.

Tests: gpu_dispatch_test (targeting), worker_seam_test (gate 403 + claim seam).
378 passed. Follow-up (cloud agent): content/studio_render.go:86 convergence.

Co-authored-by: hanzo-dev <dev@hanzo.ai>
2026-07-21 14:14:23 -07:00
Hanzo AI b6f6186a39 studio: update the middleware_test fix-graph tests to the low-denoise Edit design
The CI 'Test (per hanzo.yml)' step failed because tests-unit/middleware_test/
studio_home_fix_test.py still asserted the OLD fix behavior (denoise 1.0, the
'same model, pose, framing' prompt, and the removed size=→EmptySD3LatentImage repose
branch). Updated to the new invariants: denoise == EDIT_DENOISE_DEFAULT (0.25), the
new preservation prompt, and — replacing the repose-Empty test — a
test_fix_graph_never_uses_empty_latent that proves Edit NEVER falls back to an empty
latent for any instruction (repose is now Regenerate, not Edit). Full CI set: 375 pass.
2026-07-19 13:26:00 -07:00
Hanzo AI 3e50f9414e studio: generated-image Edit is a low-denoise edit of the SOURCE, never a regenerate
ROOT CAUSE: 'Fix' on a generated gallery image ran the Qwen edit at denoise 1.0 with a
RANDOM seed (full re-paint → color/composition/subject drift even for 'brighten very
slightly'), and a broad _REPOSE keyword regex silently swapped in EmptySD3LatentImage
(a literal text-to-image regenerate) on words like 'standing'/'looking'/'side view'.

THREE DISTINCT ACTIONS now, no ambiguity:
- Regenerate = Re-run (/v1/library/rerun: re-queue the recipe, new seed) — the only
  path that may use EmptyLatentImage + denoise 1.0.
- Edit = low-denoise Qwen edit of the SOURCE: LoadImage → FluxKontextImageScale →
  VAEEncode → KSampler(latent = the source, denoise from an edit-strength slider,
  default 0.25) → VAEDecode → Save. Reuses the source's ORIGINAL seed (read from the
  embedded graph via _provenance) so a locked seed is deterministic; a varied seed
  still edits the source. NO _REPOSE→Empty branch. Full-res source (never the thumb).
- Edit selected area = masked inpaint: SetLatentNoiseMask restricts sampling to the
  painted region + ImageCompositeMasked keeps pixels OUTSIDE the mask byte-identical.

INVARIANT (impossible-by-construction): _assert_edit_graph refuses any edit whose
KSampler.latent_image is an EmptyLatentImage or whose denoise leaves the edit band
[0.05,0.95] — an Edit can NEVER silently become a Regenerate; a bad edit surfaces an
inline error, never a fallback to random generation.

Frontend: Fix dialog → 'Edit image' with an edit-strength slider (10–95% → denoise,
default 25%) + a paint-the-area mask canvas (Edit area) + clear labels (gallery card:
Regenerate · Versions · Edit). sendFix passes {strength, seed?, mask?}. Output linked
as a child (parents=[source]); color-match post-process (v0.17.9) still runs.

Tests: 6 new edit-invariant tests (VAEEncode-not-Empty, denoise 0.25, regenerate
rejected, seed determinism, prompt-preservation-not-expansion, masked composite);
existing fix test updated 1.0→0.25. 42 pass. Model-compatible with the Qwen edit model
already on spark (kept per the user's choice); I did NOT submit any live render to
verify visuals (per the no-autonomous-image-creation rule) — the graph structure is
unit-proven and the user validates the visual output.
2026-07-19 08:10:36 -07:00
Hanzo AI c3ecffd4cd studio: delete queue/history items — remove render requests you didn't want
You can now delete any queue entry (a cancelled probe, a failed attempt, clutter) —
a 🗑 on every non-active queue row + a Delete button in the item detail dialog.
worklog.remove(org, ids) + POST /v1/worklog/delete drop ONLY the log rows; output
images live in the library and are archived/deleted there (unchanged). Optimistic +
idempotent. Pairs with the earlier Queue archive (output→Archive) and the retry-cap +
dispatch-dedup that stopped the workflow from re-running/duplicating a render in the
first place. Verified: remove by id + bulk + idempotent-missing.
2026-07-19 07:44:51 -07:00
Hanzo AI d278c03f80 studio: org-aware empty state — 'wrong workspace' not 'my work vanished'
Root-caused 'can't see any assets': it's org resolution, not lost data. The active
org lives in the studio_active_org cookie; a fresh re-login clears it, dropping the
session to the token's home org — and if that resolves to 'default' (doesn't exist)
or an empty org (maxpower=0), the gallery is blank. The user's work is intact in
karma (416 visible) + hanzo (130). Verified the render path is sound at full scale.

Fix the confusion at the source: the empty gallery now NAMES the current workspace
and points at the org switcher (account pill, top-right) — so landing on the wrong
org reads as 'switch workspace', never 'my assets disappeared'. Also distinguishes a
no-search-match empty from a truly-empty workspace.

Verified: empty maxpower org shows 'No assets in maxpower yet — switch from the
account menu'.
2026-07-18 23:14:09 -07:00
hanzo-devandz d45067db4d fix(studio): anon /studio nav → 302 /login, not a raw 401
The IAM auth gate refused any unauthenticated request whose Accept lacked
`text/html` with a 401, so a plain browser/curl navigation to a protected
page (e.g. /studio) hit a dead 401 instead of the login flow.

Invert the rule and decomplect it: a top-level browser navigation is now
redirected (302) to the one server-owned login entry point,
`/login?next=<path>` — which starts the OIDC code flow and returns the user
where they were. Only a request that self-identifies as API/XHR/JSON
(Sec-Fetch-Mode != navigate, X-Requested-With, or Accept: application/json
without HTML) still gets 401, so the SPA's own fetch calls handle re-auth
instead of following a redirect into HTML. An ambiguous client (plain curl,
Accept: */*) counts as a navigation and is redirected — a human never sees a
raw 401. handle_login honors a sanitized ?next (open-redirect/loop-safe).

Bump 0.17.18.
2026-07-18 17:31:25 -07:00
Hanzo AI 20a433678c studio: faster + lighter image loading — lazy thumbs, WebP, immutable cache
Four wins across gallery + queue + stacks:
1. LAZY: cards no longer eager-load every thumbnail (a 1000-image gallery fired 1000
   requests up front). Each .im carries its URL as data-bg; an IntersectionObserver
   (rootMargin 600px) paints it only as the card nears the viewport → a screenful of
   requests, not N. Falls back to eager where IO is unavailable.
2. WEBP: thumbnails are now WebP q80 instead of JPEG q82 — measured 45% smaller.
3. QUEUE was loading FULL-RES outputs (vURL) as row backgrounds — now uses the same
   downsized thumbnail as the gallery (huge cut on the History tab).
4. IMMUTABLE CACHE: thumbURL is versioned by content sha (?v=, from the indexer) or
   mtime, so a thumb is served Cache-Control: immutable, max-age=1yr — instant on
   repeat loads; a re-render gets a new sha → new URL → never stale.

Verified: lazy observer paints in-view cards + defers off-screen; WebP 23KB vs JPEG
41KB on a sample; ?v= present on thumb URLs.
2026-07-18 11:08:36 -07:00
Hanzo AI c474161fba studio: Queue view — fullscreen lightbox + ←/→ nav, 👍/👎 + reject on outputs
On Queue & History, per the ask:
- Click a photo → fullscreen (already opened zoom, now with navigation). Generalized
  the lightbox: zoomList(list,i) carries the queue's outputs so the on-screen ‹ › AND
  the keyboard ← / → step through them (wraps both ways); Esc closes. Stack view still
  uses svStep — zStep() routes to whichever context is open.
- 👍/👎 + 💬 on each output — the SAME rate endpoint + feedback.jsonl training log as
  the gallery, keyed by the output's library path (a queue vote == a gallery vote).
- 🗑 Reject → Archive — soft-deletes the output (⌘Z-undoable), dims the row + marks it
  ✓ Rejected. Reuses the one status/undo path; no new lifecycle.

Each queue output resolves to its library asset (path = subfolder/filename), so it
reuses the gallery's vote/archive machinery — one way to do everything, no duplication.
Verified headless: lightbox a→b→c + wrap via ←/→ and the arrows; 👍 logs training
feedback (stats up=1); reject archives + ⌘Z toast.
2026-07-18 10:47:53 -07:00
Hanzo AI 100ca8d8a2 studio: structural fix — duplicate renders impossible by construction
The retry-cap + dispatch-dedup patches stop the two known triggers; this makes the
whole class impossible at every layer, independent of any upstream bug:

COMPUTE — don't render a duplicate:
  * retry cap (maximumAttempts=1): a failed render never re-runs itself.
  * dispatch dedup: identical (org, prefix, instruction, refs) submits within 60s
    collapse to the in-flight render.

QUEUE — nothing lingers to replay:
  * scheduleToStartTimeout=1800s: a job no worker picks up in 30 min EXPIRES, so a
    worker outage can't leave a backlog that silently re-bills when a worker returns.

STORAGE — the gallery is CONTENT-ADDRESSED (the last line of defense):
  * library_manifest (the one indexer that builds every library.json, sweeping each
    org continuously) now enforces AT MOST ONE active asset per content hash. Any
    byte-identical duplicate is auto-soft-deleted, keeper = a curated asset if one
    exists else the earliest. Idempotent, recoverable, and O(new-files) per sweep
    (sha reused when mtime is stable). So even 873 identical re-renders can NEVER
    accumulate in the gallery — the invariant heals it within one sweep.

Verified: 3 identical + 1 distinct -> 2 active (one-per-hash), distinct untouched,
stable across sweeps.
2026-07-18 09:56:13 -07:00
Hanzo AI 9370bf82b2 studio: stop 'the same reject over and over' — dedup dispatch + cap retries
Two root causes of repeated/duplicate GPU renders (money + gallery clutter):

1. NO retry cap: the render activity had heartbeat/startToClose timeouts but no
   retryPolicy, so the tasks framework re-ran a failed render (e.g. spark OOM mid-
   sample) — the same reject re-rendered. Add retryPolicy maximumAttempts=1: one
   attempt, a failure surfaces to the user, no auto re-burn.

2. NO dispatch idempotency: a double-fired/unguarded submit queued a SECOND render
   of the same thing (logs showed one output dispatched 4x in a second). _dispatch
   is now idempotent — an identical (org, output-prefix, instruction, refs) dispatch
   within a 60s window returns the in-flight prompt_id instead of billing a new job.
   A genuinely different instruction on the same source still dispatches.

Server-side, so it covers every submit path (composer, fix dialog, compose, chat).
Verified: 4 identical submits → 1 render; different instruction → allowed; retry
cap present.
2026-07-18 01:07:36 -07:00
Hanzo AI 7d419c2101 studio: rebrand the Editor frontend → Hanzo Studio (no ComfyUI in the UI)
The Advanced-mode node editor is served from the upstream comfyui-frontend package,
whose index still shipped <title>ComfyUI</title> + 'Loading ComfyUI...' — the only
user-facing surface still showing the upstream name (the home + web/dist SPA are
already Hanzo Studio, and the running image is our fork ghcr.io/hanzoai/studio).

editor_index() already injects our shell into that index; add two idempotent swaps
there so the Editor tab title + loading status read 'Hanzo Studio'. The ComfyUI-format
compatibility stays internal, per rule — the name is out of the UI. (Deeper editor
menu/i18n strings live in the minified frontend bundle; a full frontend rebrand is a
separate build-side change.)
2026-07-17 22:55:35 -07:00
Hanzo AI 225658f60f studio: kill 'Prompt outputs failed validation' for real — per-graph model check
Reported again on compose (base + ref). Root cause the v0.17.5 guard missed: the
/prompt intercept fell back to LOCAL validate_prompt whenever dispatch didn't happen
AND has_local_models() was True — but has_local_models() is 'ANY model present', and
the coordinator pod carries a stray SD1.5 checkpoint. So a Qwen fix/compose graph the
pod CANNOT load (no qwen-image-edit unet) still validated locally → 'unet not in []'
→ 'Prompt outputs failed validation'. The stray checkpoint defeated the model-less
invariant.

Fix: replace has_local_models() with has_models_for(prompt) — a per-GRAPH check of
the UNET/checkpoint/CLIP/VAE loader inputs against folder_paths. A box renders locally
only a graph it can actually load; every other graph dispatches to the fleet (or a
retryable 503), regardless of unrelated models on disk. Used in BOTH the dispatch
decision (gpu_dispatch.dispatch_if_worker) and the intercept fallback (server.post_prompt).

Verified: stray-SD1.5 pod → False (enqueue, not local); real Qwen box → True (renders
locally, dev/worker unaffected); model-less pod → False.
2026-07-17 22:17:31 -07:00
Hanzo AI d561e35584 studio: fix the top composer — one create box, ↑ actually generates
Reported: adding a photo to the top 'What should we create?' box opened a SECOND
box at the bottom; only that bottom box worked; typing in the top box + clicking ↑
did nothing.

Root cause: the top ↑ (create()) was a STUB that only toast'd a HIP-0506 placeholder
and never generated, while the '+' funnelled photos into a separate fixed bottom
'Next generation' bar (#reftray/genFromTray) that was the only real generate path.
Two composers, only the second wired.

Fix — collapse to ONE composer (the top box):
- create() now really generates from the top prompt + the photos attached to it:
  1 photo → edit (/v1/library/fix), 2-5 → compose — the same proven graphs the
  gallery uses. Text-only (no photo) gives a clear 'attach a photo with +' hint.
- attached photos render INSIDE the top composer (a thumbnail strip with × to
  remove), fed by the same TRAY (+ / drag / paste / a gallery card's +).
- the bottom #reftray bar + genFromTray are removed (dead once create() is real).
- Enter submits (Shift+Enter = newline).

Verified headless: attach → thumb in the top box, NO bottom bar; type + ↑ → POST
/v1/library/fix {upload, instruction} → Queued.
2026-07-17 22:17:31 -07:00
Hanzo AI 701d166b3f studio: vote + text feedback on renders → training dataset (flywheel)
There was no way to turn user judgement into a training signal — ratings.json fed
only the runtime copilot, on the runs view, as stars. This adds a clear 👍/👎 vote
+ a written note on EVERY render (gallery card + a feedback dialog), and logs each
judgement, immutable, to the org's append-only training dataset.

- 👍/👎 on each image card (one click; click again to clear) + 💬 opens a dialog
  for the written critique ('what should the model do differently?').
- /v1/library/rate extended with a normalized vote (1|-1|0); every rate also
  appends {ts,user,org,key,output,vote,stars,text,prov} to orgs/<org>/output/
  feedback.jsonl — the flywheel the model trainer consumes. The output PNG each row
  anchors to carries its own prompt+refs in its embedded graph, so the row stays
  small and the dataset is training-ready (preference / reward / instruction-tuning).
- GET /v1/feedback/stats (👍/👎/with-text/total) + GET /v1/feedback/export (the JSONL,
  as a download). Toolbar shows '⬇ Feedback (N)'.

ratings.json stays the current-state store the UI reads; feedback.jsonl is the
append-only event log — orthogonal concerns, one rate call writes both. Verified
end-to-end headless: vote registers + stats increment, dialog captures vote+text,
export emits training-ready JSONL; backend unit + full app reboot clean.
2026-07-17 19:01:48 -07:00
Hanzo AI 0fd473923b studio: color-fidelity for Fix outputs — kill the fix-of-a-fix color drift
A Qwen-Image-Edit fix runs at denoise 1.0 → it re-paints EVERY pixel, so the
render's global white-balance/exposure/saturation drift off the source, and the
drift COMPOUNDS across fix-of-a-fix (each fix re-encodes the prior, already-shifted
output). That is why repeatedly-fixed images end up visibly mis-colored.

Fix: restore the render's global color statistics to its source (Reinhard mean/std
transfer, per RGB channel) — the local edit stays, the global cast goes; a render
that didn't drift matches to itself = untouched. Verified: 4 successive fixes hold
Δ→source ~1 with the match vs 6.5→16.5 (compounding) without.

Applied POD-SIDE at worker-render ingest (server.upload_output → colormatch_fix_ingest):
- needs NO node on the GPU worker and touches no render graph → can never make a
  render fail (avoids the 'failed validation' class entirely)
- scoped to fixes/… outputs only (never compose/normal renders), keyed to the
  staged source via the render-jobs record
- fail-safe throughout: any error / missing source / size mismatch leaves the raw
  bytes exactly as uploaded (proven: a save-format bug left files untouched, never
  corrupted)
- strength 0.85 default (STUDIO_COLORMATCH_STRENGTH), leaving headroom for an
  intentional global recolor in the instruction

New middleware/color_fidelity.py (pure algorithm + atomic in-place match);
studio_home.colormatch_fix_ingest + _fix_source_ref resolve org+source.
2026-07-17 18:11:56 -07:00
Hanzo AI 5016d2c5d8 studio: rename Deleted → Archive + permanent Delete forever
The soft-delete holding area is now an Archive (recover any item, or the
whole history stays visible). Inside it, permanent removal:

- 🗑 Delete forever per card + bulk on a selection, confirmed via the
  existing <dialog> ask() (irreversible — no ⌘Z can undo an unlinked file)
- 🗑 Empty Archive clears the lot in one confirmed step
- POST /v1/library/purge {paths[]|all}: unlink file + drop record + drop
  cached thumb. Archive-only by design — refuses any asset whose status is
  not 'deleted', so a gallery mis-click can only ever archive (recoverable),
  never destroy. Verified end-to-end headless: per-card/bulk/empty purge,
  the archive-only guard, and rename coverage (tab, count, chips, toasts).

Rename is display-only; the underlying status stays 'deleted', so the
1,259 already-archived items need no migration.
2026-07-17 17:10:21 -07:00
Hanzo AI 4ee5adcef9 studio: Dedupe — one-click removal of exact-duplicate images (recoverable, ⌘Z)
The catalog seeder emits byte-identical copies (found 94 dups in one org's 268
visible assets). POST /v1/library/dedup hashes visible assets and soft-deletes
exact-content duplicates — keeps one per cluster, distinct re-renders (different
seeds) untouched — and returns the moved paths. Frontend "Dedupe" button beside
Select; the whole sweep pushes ONE undo entry so ⌘Z restores it all. no-store on
the response. Verified in-browser: dedup hides dups, Deleted count updates, ⌘Z
restores. v0.17.7
2026-07-17 16:19:13 -07:00
Hanzo AI c86ded1bad studio delete UX: ⌘Z undo, findable Deleted tab, one-click bulk delete + no-store library
Deletes already persist server-side (verified live: soft-delete → status 'deleted'
holds, zero reverts); the "moved to deleted then restored" was the frontend serving
a cached /v1/library on refresh. Fixes:
- /v1/library → Cache-Control: no-store, so a delete is never masked by a stale copy.
- ⌘Z / Ctrl+Z undoes the last delete (and any status change) — soft-deletes are
  reversible, so undo is just a status write-back. Dropped the bulk confirm dialog.
- 🗑 Deleted is now a top-level tab with a live count — trash is easy to find;
  per-item Recover + bulk Recover (context-aware bulk bar).
- Bulk: Select → pick many → one-click "🗑 Delete selected". Discoverable.
Verified in-browser (Playwright): delete hides the card, ⌘Z restores it, Deleted tab
filters + Recover works. v0.17.6
2026-07-17 16:09:39 -07:00
Hanzo AI a363751352 studio: dispatch resilience — kill the intermittent "failed validation" on fix
The /prompt intercept sends every render to the org's GPU worker, but a
transient fleet-check fault (/v1/machines 502 / TLS reset / timeout) made
dispatch_if_worker return False, so the render fell through to LOCAL
validation on the model-less cloud pod → the cryptic "Prompt outputs failed
validation" (unet_name '…' not in []). That's why Fix failed only *sometimes*.

- _fleet_machines: retry 3x with backoff so a transient hiccup isn't read as
  "no GPU" and bounced onto the model-less pod.
- has_local_models() + intercept: on a model-less coordinator pod a failed
  dispatch now returns a clear retryable 503 "GPU render worker is momentarily
  unavailable — please retry", never the doomed local validation. A box WITH
  models still renders locally (dev). v0.17.5
2026-07-17 15:17:26 -07:00
Hanzo AI 86e301345f studio /studio: serve the rich home (gallery remove/edit/+ refs), not the minimal SPA
The web/dist chat SPA shadowed studio_home.html at /studio, dropping the
per-image affordances: delx (remove → POST /v1/library/status deleted),
the Fix dialog (type-and-edit → /v1/library/fix), and the + reference tray
(→ /upload/image → /v1/library/compose, up to 5 refs). Serve the rich home
explicitly; drop the dead /studio/assets route + the node web-build stage.
web/login.html still ships (server.py serves /login). v0.17.4
2026-07-17 13:06:01 -07:00
Hanzo AI b07530398d studio: /v1/mcp render tools (fix/compose/ask) for the cloud chat tool registry
MCP-over-HTTP (JSON-RPC 2.0) exposing the studio render pipeline as tools the
cloud /v1/chat 'create' capability drives via the tool registry. Reuses the ONE
dispatch path (studio_home.dispatch_fix/dispatch_compose, extracted from the
/v1/library/{fix,compose} routes) so an LLM-issued fix behaves identically to a
UI one. Registered per-org via cloud POST /v1/tools/servers. v0.17.3.
2026-07-16 09:01:36 -07:00
Hanzo AI 0d307dceb2 studio: responsive — home was desktop-only (0 media queries). Add tablet(≤900) + phone(≤600) breakpoints: fluid grid, wrapping controls, full-width chat sheet, bigger tap targets, always-show card tools on touch (no hover-reveal). Works mobile/tablet/laptop/desktop. v0.17.2 2026-07-16 00:21:55 -07:00
Hanzo AI 1b713f904c studio: fix queue jobs VANISHING after 30s — render-queue completion check did a fuzzy base-prefix match that collided with the SOURCE image (a fix's output prefix derives from the source name), so every job looked 'done' after 30s and disappeared. Now checks for the EXACT output file (<outPrefix>_NNNNN_.png). This is why 'the queue is not working'. v0.17.1 2026-07-15 23:19:39 -07:00
Hanzo AI c969641d7d studio: EXIT Advanced mode — always-visible '← Studio' button (fixed top-left) + 'Exit to Studio Home' in the user menu. Advanced mode (the full-screen node editor) had NO way back to Studio Home — users got stuck. v0.17.0 2026-07-15 23:11:34 -07:00
Hanzo AI b7dff20861 studio: fix 'connect a GPU' when a GPU IS connected — UI checked /v1/engines (worker-federation) but 'hanzo gpu connect' registers with the FLEET (gpu-jobs), which is what dispatch actually uses. New /v1/gpu-status reports the SAME source as dispatch (_has_online_gpu → /v1/machines); UI now shows spark connected. v0.16.9 2026-07-15 22:58:04 -07:00
Hanzo AI e696fb7480 studio: SHOW BYO-GPU renders in the queue — fix/compose dispatch to gpu-jobs (worker), NOT the pod's local /queue, so a queued fix was invisible ('didn't bring it into queue'). Now dispatch_if_worker tracks each job per-org; /v1/render-queue serves in-flight ones (drops on output-landed/age-out); live-bar + Queue tab show them with thumbnail + ETA. v0.16.8 2026-07-15 22:20:50 -07:00
Hanzo AI b3f20495f8 studio: /studio served no-store — browsers cached the SPA HTML and served a STALE page after deploys, making UI fixes (delete) look broken to users on yesterday's cached page. v0.16.7 2026-07-15 22:12:29 -07:00
Hanzo AI 179b577add studio SECURITY: org-scope the inherited core endpoints (multi-tenant release blockers from red audit) — /history + /history/{id} filtered by caller org (was cross-tenant BOLA, live in Queue&History UI); /queue GET filtered; /queue clear/delete scoped (was: any tenant's 'Clear all' wiped ALL orgs); /interrupt scoped to caller's running job; org_id path-traversal sanitization in get_org_base_path. Single-tenant (auth off) unchanged. v0.16.6 2026-07-15 21:37:33 -07:00
Hanzo AI c2336e0c1e studio: YouTube-style up-next queue rows — reference thumbnail + title + brief description (the prompt) + ETA/#-in-line; serves staged fix/compose refs via ?input=1; v0.16.5 2026-07-15 20:51:01 -07:00
Hanzo AI 0538abf199 studio: FAST thumbnails (?thumb=1 → cached 480px JPEG, fixes slow full-res grid loading) + always-visible instant 🗑 trash on every card (optimistic delete, feels instant) + tray/compose use thumbs; v0.16.4 2026-07-15 20:49:03 -07:00
Hanzo AI 7eefdcd206 studio: persistent live-queue bar (always visible, non-advanced) — shows what's Generating now + queued count + ETA, polls /queue every 5s; auto-hiding header + full-bleed immersive stage CSS + Hanzo AI chat widget scaffold; v0.16.3 2026-07-15 20:42:01 -07:00
Hanzo AI b238b8c568 studio: bump BYO-GPU render deadline 600s→1200s (heavy Qwen edit on cold GB10 exceeds 10min); v0.16.2 2026-07-15 20:18:30 -07:00
Hanzo AI e97194613c studio: '+' on each photo → reference tray for the next generation; 1 ref=fix/edit, 2-5=compose; describe + Generate from the floating tray; v0.16.1 2026-07-15 20:12:56 -07:00
Hanzo AI b31a1ea3e2 studio: YouTube-style Queue & History — Now-running + Up-next (live /queue, remove/clear) + History with 0-5 star ratings + notes-for-AI (real /v1/library/rate + ratings.json curation store) + favorite→save-as-template guided flow; v0.16.0 2026-07-15 19:58:21 -07:00
Hanzo AI dd21fd9f85 studio: multi-asset Compose (select N → describe how to combine → /v1/library/compose, multi-ref Qwen graph) + Fix/Compose now report GPU-online status clearly (Qwen models live on BYO-GPU); v0.15.9 2026-07-15 19:50:41 -07:00
Hanzo AI 109cd04a54 studio: soft-delete (recoverable, Deleted filter) + multi-select bulk (approve/publish/fork/delete) + hover-reveal tools (cleaner grid) + systematized fashion templates (CAD→ghost, outfit-switcher, product, lifestyle, CAD→3D grouped by category); v0.15.8 2026-07-15 19:22:32 -07:00
Hanzo AI 552f47b844 studio: chat-first Claude-Design home ('What should we create?' + full Hanzo template set: product/ghost/CAD-3D/video/music/voice/social/docs/paper) + org-scoped /v1/library/file (fixes blank thumbnails); v0.15.7 2026-07-15 18:33:59 -07:00
Hanzo AI b0d1b06a6f studio home: Hanzo logo top-left + clean inline org switcher/avatar (drop broken floating identity pill); v0.15.6 2026-07-15 18:30:09 -07:00
Hanzo AI 023367bfec studio home: monochrome brand (drop yellow --brand) + surface org-switcher/user/wallet identity pill; v0.15.5 2026-07-15 17:42:05 -07:00
Hanzo AI b2121abc71 chore: version 0.15.4 — rebuild Studio Home (0.15.3 image never reached GHCR) 2026-07-14 17:51:53 -07:00
hanzo-dev 8e55ecac9a refactor: karma_manifest.py -> library_manifest.py — org-generic name for the org-generic tool (unified multi-tenant SaaS; no tenant names in code)
Claude-Session: https://claude.ai/code/session_01Gq8suw7uuodAMPDRpo6iAB
2026-07-14 17:05:44 -07:00
hanzo-dev b6317999dd fix: dedupe version key from rebase resolution
Claude-Session: https://claude.ai/code/session_01Gq8suw7uuodAMPDRpo6iAB
2026-07-14 16:52:46 -07:00
hanzo-dev 5e9f72bc16 feat(studio): Studio Home — content-forward default view; node editor becomes Advanced mode
/studio: org-scoped asset gallery over library.json (status chips draft/
approved/queued/published), per-asset workflow provenance from the embedded
PNG graph, Re-run (exact graph, fresh seed) and chat-fix (instruction ->
Qwen-edit pass on that output) both through the ONE /prompt path with the
caller's own credentials. Pipelines tab reuses the 0.15.1 deep-link opener
for Edit/Run; STUDIO_HOME_DEFAULT=1 makes / land on /studio (SPA at
/?advanced=1). 5 unit tests.

Claude-Session: https://claude.ai/code/session_01Gq8suw7uuodAMPDRpo6iAB
2026-07-14 16:52:24 -07:00
Hanzo AI acd3bcc99c chore: version 0.15.1 — deep-link opener atop the live 0.15.0 line 2026-07-14 06:32:07 -07:00
Hanzo AI cc352ba658 chore: version 0.14.11 2026-07-12 22:50:42 -07:00