Files that outlived the chats that held them, and a page that led with its footnotes
The second audit pass. Four things, and the first two were reported. The Prompts page put a screen of variables and a screen of preview above the editor, so the tabs began two screens down and switching one had to drag the whole page to be any use -- and on a short tab it could not drag far enough, leaving the panel stranded above a screenful of nothing. Editor first, reference after, bar sticky. Custom themes were three fixed slots: fifty-seven empty colour boxes on a fresh instance and no way to make a fourth theme. One block per theme plus a blank one, colours behind a disclosure. Both measured rather than argued about -- rendered through TestClient and driven under headless Chromium, where the tab bar moved 385->642px before and does not move now, and the themes page went from 5495px to 2820px. Asking where generated images go found the other two. Deleting a chat cascades to the attachment rows and leaves every file on disk; the helper written for exactly that was called from one place, and it was not the delete button, a schedule's chat, a helper's chat or deleting an account. Underneath it, `claim` bound message_id and never chat_id, so anything picked before a chat existed kept an empty chat_id forever -- which six readers filter on, so those files were also unnamed in the prompt, unopenable in the canvas, and invisible to the one caller the cleanup had. And folders nest now. The route has handled parent_id since folders existed, with a cycle guard and a depth cap the move path never applied; the sidebar has always drawn a tree. Nothing could ask for one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -99,18 +99,49 @@ runtime. Clone it, `pip install -e .`, run it.
|
||||
- **Model settings** — searchable, filterable list with a page per model:
|
||||
ordering, pinned models, an instance default and a per-user default, custom
|
||||
names, descriptions and images. Scales to hundreds of models
|
||||
- **Things that happen because time passed** — say "every Monday at nine" and a
|
||||
model can set it up itself, against the same recurrence rule the manual form
|
||||
uses. A run can file a **report** you read later, send you a message, or work
|
||||
on in a chat of its own. The reply says the timing back in words, which is the
|
||||
one moment anybody can check that Monday was understood as Monday
|
||||
- **News that finds you** — a dot in the sidebar, a count in the tab title while
|
||||
you are looking elsewhere, and **web push** so a schedule firing at seven in
|
||||
the morning reaches a browser that is shut. Opt-in per device
|
||||
- **Helpers** — a reply can hand a self-contained piece of work to another model
|
||||
that runs on its own and reports back, several at once, so research fans out
|
||||
instead of queueing. A helper cannot ask questions, cannot send helpers of its
|
||||
own, and on a machine runs only a fixed list of read-only commands
|
||||
- **Drawing** — point it at a ComfyUI and a model can make images, against
|
||||
workflow templates and defaults you set: size, steps, sampler, scheduler,
|
||||
checkpoint. It reviews its own result and can try again
|
||||
- **Semantic search** — pick an embedding model and library search fuses keyword
|
||||
and meaning, so *"how do I get paid"* finds a document that says *"invoicing"*.
|
||||
Choosing none is not a degraded mode: it is byte-for-byte the keyword search
|
||||
that was always there, with nothing written and no requests made
|
||||
- **Users, groups & permissions** — per-group grants that union rather than
|
||||
override, and model access restricted to chosen groups
|
||||
override, model access restricted to chosen groups, read and write split for
|
||||
notes, memory and skills, and a screen that answers *"what can this account
|
||||
actually do?"* by naming where each permission came from
|
||||
- **Quotas** — monthly tokens, concurrent replies, agent wall clock, images a
|
||||
day, helpers a reply. Resolved by maximum across a person's groups, with zero
|
||||
meaning *no limit*
|
||||
- **Sharing** — hand a document, a note, a skill or a report to a group or a
|
||||
person, read-only, with a *Shared with me* filter in every listing
|
||||
- **Make it yours** — name, tagline, logo, favicon and launcher icons; the
|
||||
Middle-earth wording is editable data; custom **themes** defined as a set of
|
||||
colours rather than a stylesheet, and global CSS overrides
|
||||
- **Accounts** — first account becomes the administrator, argon2 password
|
||||
hashing, revocable server-side sessions, self-service password change,
|
||||
admin-managed accounts
|
||||
- **Admin settings** — open or close registration from the UI, stored in the
|
||||
database and effective immediately
|
||||
- **Two themes** — *Moria* (dark) and *Shire* (light), switchable per user
|
||||
- **Admin settings** — registration, upload and extraction limits, prompt
|
||||
fragments, and an **Updates** page showing what is running, what is available
|
||||
and what changed between
|
||||
- **Two themes and your own** — *Moria* (dark), *Shire* (light), and as many
|
||||
more as you care to define
|
||||
|
||||
**Planned**
|
||||
|
||||
Image generation · OCR for scanned PDFs · semantic search in the library.
|
||||
OCR for scanned PDFs · conversation branching · chat export · archived chats.
|
||||
|
||||
See [PLAN.md](PLAN.md) for what is built, what is not, and why.
|
||||
|
||||
@@ -322,6 +353,80 @@ lembas secret-key # generate a value for LEMBAS_SECRET_KEY
|
||||
lembas create-admin # create or promote an administrator
|
||||
```
|
||||
|
||||
## Running it somewhere
|
||||
|
||||
Three ways, all in this repository.
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
export LEMBAS_SECRET_KEY="$(lembas secret-key)" # required; there is no default
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
One stage, no build step, non-root. The image bakes **no secret key, no data and
|
||||
no `.git`** — a key inside an image is one every copy shares, and rotating it
|
||||
makes stored API keys unreadable. Data lives in a named volume on `/data`.
|
||||
|
||||
`docker-compose.yml` publishes on `127.0.0.1` and expects a TLS proxy in front:
|
||||
the service worker and the microphone both require HTTPS or localhost, so plain
|
||||
http on a LAN address is a constraint rather than a preference. One replica, and
|
||||
that is deliberate — the generation registry, the terminal sessions and the
|
||||
schedule ticker are all in-process, so two would mean every schedule firing
|
||||
twice.
|
||||
|
||||
**Updating a container is pulling a new image**, and `/admin/updates` says so
|
||||
rather than offering a button:
|
||||
|
||||
```bash
|
||||
docker compose pull && docker compose up -d
|
||||
```
|
||||
|
||||
There is deliberately no in-container update helper. The one the other install
|
||||
paths use restarts a systemd service; the equivalent here would be a process
|
||||
inside the container reaching the Docker socket to replace the container it is
|
||||
running in — which is root on the host, granted to anybody who can administer
|
||||
the web interface. The image is the unit of deployment, and that is the whole
|
||||
point of it.
|
||||
|
||||
### A machine of its own
|
||||
|
||||
`deploy/` holds a systemd unit, an nginx vhost, and install/update scripts. Every
|
||||
template is parameterised and substituted at install time, so nothing
|
||||
host-specific is committed here. See [deploy/README.md](deploy/README.md).
|
||||
|
||||
`deploy/lxc-install.sh` creates an unprivileged Proxmox container and runs that
|
||||
same installer inside it — a wrapper around what already works rather than a
|
||||
second install path:
|
||||
|
||||
```bash
|
||||
CTID=140 SITE_HOST=chat.example ./deploy/lxc-install.sh
|
||||
```
|
||||
|
||||
The container gets the **update helper by default**, unlike a bare
|
||||
`install.sh`. The installer defaults it off because it cannot know what it is
|
||||
installing onto; a container this script made thirty seconds ago to run one
|
||||
thing, on a hypervisor you own, is not that host — and an appliance you cannot
|
||||
update without a shell is one nobody updates. `INSTALL_UPDATE_HELPER=0` opts
|
||||
out.
|
||||
|
||||
### Updating
|
||||
|
||||
**Admin → Updates** shows the version running, what is available on the channel
|
||||
this host follows, and the commits between. `stable` is the newest `vX.Y.Z` tag;
|
||||
`edge` is the branch tip, which is whatever was pushed most recently.
|
||||
|
||||
The button that applies an update is **opt-in**, and that is the design: the
|
||||
service runs unprivileged and cannot restart itself, so the request is a file
|
||||
that a systemd `.path` unit picks up and runs as root. It carries no ref and no
|
||||
channel — pressing it is always "deploy the channel this host was configured
|
||||
with", never "deploy something else". Install it with
|
||||
`INSTALL_UPDATE_HELPER=1`; without it the page says so and prints the command to
|
||||
run by hand.
|
||||
|
||||
Release notes come out of the annotated tag itself, so no forge API is involved
|
||||
anywhere.
|
||||
|
||||
## How it fits together
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user