Files
LLeMbas/deploy/nginx-vhost.conf
T
Jaroslav Beneš 436226370a PWA, one send/stop button, audio in and out, web search as a tool
Four pieces of work.

**Installable.** A manifest carrying the instance name, PWA icons rasterised
from the existing mark at design time, a service worker and a themed offline
page. The worker caches the shell only and bails out on /api/, /auth/, /admin/
and anything accepting text/event-stream -- passing a reply stream through a
worker turns it into one delivery at the end, or nothing. It is served from
GET /sw.js rather than the static mount because a worker's scope is the path it
came from.

**Send and Stop are one button.** They were two, and the hidden one was never
hidden: `.btn` is display: inline-flex, which outranks the browser's own
`[hidden] { display: none }`, so Stop sat permanently beside Send. app.css now
forces the attribute to win -- every control toggled with `hidden` depended on
that -- and the composer renders one button carrying both icons, with ui.js
flipping data-composer-action and the type with it.

**Audio.** Speech to text and text to speech against any OpenAI-shaped
/v1/audio/* endpoint: dictate into the composer, have a reply read out.
Instance settings in Admin, per-reader overrides in Settings, with the voice
list discovered from the server where it offers one. Recorded audio is capped
and never written to disk -- it is not an attachment, it has no owner, and
nothing would ever sweep it.

**Web search, as a tool.** This is the tool loop PLAN.md described as the real
work: one reply is now a bounded sequence of requests rather than one. The model
asks, the tool runs, the result goes back and it is asked again, up to three
rounds. Providers are DuckDuckGo (no setup), SearXNG and Firecrawl.

Two decisions worth stating. Tools are only offered to models flagged `tools`,
because an endpoint without support rejects the whole request rather than
ignoring the array -- the same reason images only reach models flagged
`vision`. And tool results are not replayed as context on the next turn, for the
same reasons reasoning is not: the answer already contains what the model made
of them, and replaying stale results into every later request wastes the window
and reliably sends a small model into a search loop. The sources stay visible in
the transcript instead.

Search results are untrusted third-party text and are treated as such: escaped,
and only http/https URLs rendered as links.

338 tests, ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 17:56:50 +02:00

72 lines
2.6 KiB
Plaintext

# nginx vhost template for LLeMbas.
#
# install.sh substitutes __SITE_HOST__ and __APP_PORT__ and writes the result to
# /etc/nginx/conf.d/<host>.conf. Edit this file, not the installed copy.
#
# Assumes a self-signed certificate at /etc/nginx/ssl/<host>.{crt,key}, which
# install.sh generates. To use a real certificate, point ssl_certificate at it;
# nothing else here needs to change.
server {
listen 80;
listen [::]:80;
server_name __SITE_HOST__;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name __SITE_HOST__;
ssl_certificate /etc/nginx/ssl/__SITE_HOST__.crt;
ssl_certificate_key /etc/nginx/ssl/__SITE_HOST__.key;
ssl_protocols TLSv1.2 TLSv1.3;
# File uploads land here once that feature exists; 0 = no limit.
client_max_body_size 0;
location / {
proxy_pass http://127.0.0.1:__APP_PORT__;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Streamed replies are server-sent events. Every one of these matters:
# with buffering on (the default) nginx holds the whole reply and
# delivers it in one lump at the end, which is indistinguishable from
# streaming being broken.
proxy_buffering off;
proxy_request_buffering off;
proxy_cache off;
# SSE is plain HTTP/1.1 chunked, not a websocket upgrade, so the
# connection header must simply be left to keep-alive.
proxy_set_header Connection "";
# A model can think for minutes before the first token. The default
# 60s read timeout would cut long generations off mid-sentence.
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
location /static/ {
proxy_pass http://127.0.0.1:__APP_PORT__;
proxy_set_header Host $host;
expires 1h;
add_header Cache-Control "public";
}
# The service worker must never be cached. A stale worker keeps serving a
# stale cache to every tab, and there is no way to tell it to stop. The
# application already sends no-store; this stops the proxy overriding it.
# /manifest.webmanifest needs nothing special and comes through location /.
location = /sw.js {
proxy_pass http://127.0.0.1:__APP_PORT__;
proxy_set_header Host $host;
add_header Cache-Control "no-store";
}
}