Scaffold project, data model and artwork

Establish the LLeMbas foundation: FastAPI/Jinja/SQLite layout, the ORM
schema, and the original SVG identity.

Notable decisions, all recorded in comments at the point they matter:

- No Alembic. SQLite only, schema created at startup, so models carry a
  few columns nothing reads yet (Message.parent_id for branching,
  content_parts_json for multimodal turns). Adding them later to a live
  database without migrations is the painful path.
- Sessions are server-side rows keyed by a SHA-256 of the cookie value,
  not JWTs, so logout and bans revoke access immediately.
- Upstream API keys are Fernet-encrypted with a key derived from
  LEMBAS_SECRET_KEY. decrypt() fails soft to "" so rotating the secret
  degrades to re-entering keys rather than crashing the admin UI.
- Artwork is generated by scripts/build_artwork.py rather than hand-drawn
  per file: the mallorn leaf appears in the icon, favicon, lockup and
  banner, and one source is the only way those stay in sync. The wordmark
  is Source Serif 4 (OFL) converted to outlines, because a README banner
  cannot load a webfont and <text> would render in whatever serif the
  viewer happens to have.
- Icons live in a template partial, not assets/, because same-document
  <use href="#id"> is universally supported and the cross-document form
  is not.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-07-21 10:34:48 +02:00
parent 0665027bc6
commit 5ef2af6a9f
32 changed files with 2041 additions and 0 deletions
View File
@@ -0,0 +1,125 @@
{#
Icon sprite, inlined once at the top of <body>.
It lives here rather than in assets/ and is deliberately NOT loaded as an
external file: cross-document <use href="sprite.svg#id"> has patchy browser
support, while same-document <use href="#id"> is universal. Inlining also
costs zero extra requests.
Icons are 24x24, stroked (never filled), and inherit currentColor, so they
take the surrounding text colour in every theme automatically.
Use via the macro in _macros.html: {{ icon("send") }}
#}
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
<defs>
<g id="icon-defaults"></g>
</defs>
<symbol id="i-send" viewBox="0 0 24 24"><path d="M12 20V5M5 12l7-7 7 7"/></symbol>
<symbol id="i-plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></symbol>
<symbol id="i-minus" viewBox="0 0 24 24"><path d="M5 12h14"/></symbol>
<symbol id="i-check" viewBox="0 0 24 24"><path d="M4.5 12.5 9 17 19.5 6.5"/></symbol>
<symbol id="i-x" viewBox="0 0 24 24"><path d="M6 6l12 12M18 6 6 18"/></symbol>
<symbol id="i-menu" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h16"/></symbol>
<symbol id="i-dots" viewBox="0 0 24 24">
<circle cx="12" cy="5" r="1.4"/><circle cx="12" cy="12" r="1.4"/><circle cx="12" cy="19" r="1.4"/>
</symbol>
<symbol id="i-chevron-right" viewBox="0 0 24 24"><path d="m9.5 5.5 7 6.5-7 6.5"/></symbol>
<symbol id="i-chevron-down" viewBox="0 0 24 24"><path d="m5.5 9.5 6.5 7 6.5-7"/></symbol>
<symbol id="i-chat" viewBox="0 0 24 24">
<path d="M20.5 12.2a7.7 7.7 0 0 1-8.3 7.7l-5 2.4.9-3.7a7.7 7.7 0 1 1 12.4-6.4Z"/>
</symbol>
<symbol id="i-folder" viewBox="0 0 24 24">
<path d="M3 7.5A2 2 0 0 1 5 5.5h3.6a1 1 0 0 1 .8.4l1.2 1.6H19a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2Z"/>
</symbol>
<symbol id="i-folder-open" viewBox="0 0 24 24">
<path d="M3 18.5V7.5a2 2 0 0 1 2-2h3.6a1 1 0 0 1 .8.4l1.2 1.6H18a2 2 0 0 1 2 2v1"/>
<path d="M3 18.5 5.6 11a1 1 0 0 1 .95-.7H21a1 1 0 0 1 .95 1.3l-2.1 6.2a2 2 0 0 1-1.9 1.4H5a2 2 0 0 1-2-2Z"/>
</symbol>
<symbol id="i-trash" viewBox="0 0 24 24">
<path d="M4 7h16M9.5 7V5.4A1.4 1.4 0 0 1 10.9 4h2.2a1.4 1.4 0 0 1 1.4 1.4V7"/>
<path d="m6.5 7 .9 11.6A1.5 1.5 0 0 0 8.9 20h6.2a1.5 1.5 0 0 0 1.5-1.4L17.5 7"/>
</symbol>
<symbol id="i-pencil" viewBox="0 0 24 24">
<path d="M4 20h4L19.3 8.7a2.4 2.4 0 0 0-3.4-3.4L4.6 16.6 4 20Z"/>
<path d="m14.8 6.4 2.8 2.8"/>
</symbol>
<symbol id="i-copy" viewBox="0 0 24 24">
<rect x="9" y="9" width="11" height="11" rx="2"/>
<path d="M15 6.5V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h.5"/>
</symbol>
<symbol id="i-refresh" viewBox="0 0 24 24">
<path d="M20 12a8 8 0 1 1-2.6-5.9"/>
<path d="M20 4v4.5h-4.5"/>
</symbol>
<symbol id="i-pin" viewBox="0 0 24 24">
<path d="M9 3.5h6l-.8 5.2 3 3.1-.7 2.2H7.5l-.7-2.2 3-3.1Z"/>
<path d="M12 14v6.5"/>
</symbol>
<symbol id="i-search" viewBox="0 0 24 24">
<circle cx="11" cy="11" r="6.5"/><path d="m16 16 4 4"/>
</symbol>
<symbol id="i-attach" viewBox="0 0 24 24">
<path d="M17.5 8.5v7.8a5 5 0 0 1-10 0V7a3.2 3.2 0 0 1 6.4 0v9.1a1.5 1.5 0 0 1-3 0V8.5"/>
</symbol>
<symbol id="i-user" viewBox="0 0 24 24">
<circle cx="12" cy="8.3" r="3.8"/>
<path d="M4.5 20a7.5 7.5 0 0 1 15 0"/>
</symbol>
<symbol id="i-users" viewBox="0 0 24 24">
<circle cx="9.5" cy="8.5" r="3.4"/>
<path d="M3 19.5a6.5 6.5 0 0 1 13 0"/>
<path d="M16 5.6a3.4 3.4 0 0 1 0 5.9M17.5 14.4a5.6 5.6 0 0 1 3.5 5.1"/>
</symbol>
<symbol id="i-shield" viewBox="0 0 24 24">
<path d="M12 3.2 19.5 6v5.6c0 4.6-3.1 7.6-7.5 9.2-4.4-1.6-7.5-4.6-7.5-9.2V6Z"/>
</symbol>
<symbol id="i-logout" viewBox="0 0 24 24">
<path d="M14.5 4.5H18a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2h-3.5"/>
<path d="M9.5 8 5.5 12l4 4M5.5 12H15"/>
</symbol>
<symbol id="i-server" viewBox="0 0 24 24">
<rect x="3.5" y="4" width="17" height="6" rx="1.8"/>
<rect x="3.5" y="14" width="17" height="6" rx="1.8"/>
<path d="M7 7h.01M7 17h.01"/>
</symbol>
<symbol id="i-sliders" viewBox="0 0 24 24">
<path d="M4 8h10M18 8h2M4 16h4M12 16h8"/>
<circle cx="16" cy="8" r="2"/><circle cx="10" cy="16" r="2"/>
</symbol>
<symbol id="i-gear" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="3.2"/>
<path d="M12 3v2.2M12 18.8V21M3 12h2.2M18.8 12H21M5.6 5.6l1.6 1.6M16.8 16.8l1.6 1.6M18.4 5.6l-1.6 1.6M7.2 16.8l-1.6 1.6"/>
</symbol>
<symbol id="i-sun" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="4"/>
<path d="M12 2.5v2M12 19.5v2M2.5 12h2M19.5 12h2M5.3 5.3l1.4 1.4M17.3 17.3l1.4 1.4M18.7 5.3l-1.4 1.4M6.7 17.3l-1.4 1.4"/>
</symbol>
<symbol id="i-moon" viewBox="0 0 24 24">
<path d="M20.5 13.8A8.5 8.5 0 0 1 10.2 3.5a8.5 8.5 0 1 0 10.3 10.3Z"/>
</symbol>
<symbol id="i-sidebar" viewBox="0 0 24 24">
<rect x="3.5" y="4.5" width="17" height="15" rx="2"/>
<path d="M10 4.5v15"/>
</symbol>
<symbol id="i-archive" viewBox="0 0 24 24">
<rect x="3.5" y="4.5" width="17" height="4" rx="1.2"/>
<path d="M5 8.5v9a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-9M10 12.5h4"/>
</symbol>
<symbol id="i-warning" viewBox="0 0 24 24">
<path d="M12 4.2 21 19.5H3Z"/>
<path d="M12 10v4M12 16.8h.01"/>
</symbol>
<symbol id="i-leaf" viewBox="0 0 64 64">
<path d="M20.5 45.5C13.8 31.7 23.8 20.9 45.5 18.5 49.8 35 39.8 45.8 20.5 45.5Z"/>
<path d="M20.5 45.5C28 38 36 29 45.5 18.5"/>
</symbol>
</svg>