Two-phase streaming architecture:
Phase 1 (sync thread): IngredientClassifier builds element profiles +
gap list from SQLite — thread-safe, no async context needed
Phase 2 (async): LLMRecipeGenerator.stream_generate() yields tokens via
cf-orch warm vllm (existing /stream-token path) or AsyncOpenAI against
Ollama if the coordinator is unavailable
Backend (app/services/recipe/llm_recipe.py):
- stream_generate() async generator; _try_alloc_for_stream() sync helper
- _stream_openai_compat() static method handles __auto__ model resolution
- LLMRecipeGenerator(None) is safe for streaming (store not used)
Endpoint (app/api/endpoints/recipes.py):
- ?stream=true on POST /recipes/suggest returns StreamingResponse
- X-Accel-Buffering: no prevents nginx buffering without nginx.conf edits
Frontend (api.ts, recipes.ts, RecipesView.vue):
- suggestRecipeStream() uses fetch + ReadableStream (POST; EventSource
only supports GET)
- streamSuggest() action in recipes store builds request internally
- RecipesView.streamRecipe() silently falls back to native SSE when
cf-orch token fetch fails rather than surfacing an error
- App.vue: lazy-mount pattern (v-if + v-show) so non-active tabs only mount on
first visit, eliminating concurrent onMounted calls across all components (#98)
- nginx.cloud.conf: add /kiwi/api/ location to proxy API calls on direct-port
access (localhost:8515); was serving SPA HTML → causing M.map/filter/find
TypeError chain on load (#98)
- nginx.cloud.conf: $host → $http_host so 307 redirects preserve port number (#107)
- RecipeBrowserPanel: show friendly "corpus not loaded" notice and skip auto-select
when all category counts are 0, instead of rendering confusing empty buttons (#106)
- Defensive Array.isArray guards in inventory store, mealPlan store, ReceiptsView
Vite builds with VITE_BASE_URL=/kiwi so assets are referenced as
/kiwi/assets/... in index.html. When accessed via Caddy at the /kiwi
path, Caddy strips the prefix and nginx gets /assets/... correctly.
When accessed directly at localhost:8515, nginx had no /kiwi/ route
so the JS/CSS 404'd and the SPA never booted (blank page on hard refresh).
Add location ^~ /kiwi/ { alias ...; } — ^~ prevents the regex
\.(js|css|...)$ location from intercepting /kiwi/ paths first.