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
+49
View File
@@ -0,0 +1,49 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64"
role="img" aria-label="LLeMbas">
<title>LLeMbas</title>
<desc>A silver mallorn leaf laid across a scored golden lembas wafer.</desc>
<defs>
<linearGradient id="m-wafer" x1="0" y1="0" x2="0.3" y2="1">
<stop offset="0" stop-color="#EACB74"/>
<stop offset="0.5" stop-color="#C9A227"/>
<stop offset="1" stop-color="#916F13"/>
</linearGradient>
<linearGradient id="m-leaf" x1="0.1" y1="1" x2="0.9" y2="0">
<stop offset="0" stop-color="#93A5B6"/>
<stop offset="0.4" stop-color="#F1F6FA"/>
<stop offset="1" stop-color="#B8C7D5"/>
</linearGradient>
<clipPath id="m-clip">
<rect x="6" y="6" width="52" height="52" rx="13"/>
</clipPath>
</defs>
<rect x="6" y="6" width="52" height="52" rx="13" fill="url(#m-wafer)"/>
<g clip-path="url(#m-clip)" fill="none" stroke-linecap="round">
<g stroke="#7A5C10" stroke-opacity="0.38" stroke-width="2">
<path d="M32 6 V58"/>
<path d="M6 32 H58"/>
</g>
<g stroke="#F6E3A8" stroke-opacity="0.3" stroke-width="1">
<path d="M33.2 6 V58"/>
<path d="M6 33.2 H58"/>
</g>
</g>
<rect x="7.1" y="7.1" width="49.8" height="49.8" rx="11.9"
fill="none" stroke="#7A5C10" stroke-opacity="0.3" stroke-width="1.2"/>
<g>
<path d="M21.2 44.8 L17 49.4" stroke="#8A9AA8" stroke-width="3"
stroke-linecap="round" fill="none"/>
<path d="M20.5 45.5 C13.8 31.7 23.8 20.9 45.5 18.5 C49.8 35 39.8 45.8 20.5 45.5 Z" fill="url(#m-leaf)"/>
<path d="M20.5 45.5 C28 38 36 29 45.5 18.5" fill="none" stroke="#61758A" stroke-opacity="0.5"
stroke-width="1.5" stroke-linecap="round"/>
<g fill="none" stroke="#61758A" stroke-opacity="0.32"
stroke-width="1" stroke-linecap="round">
<path d="M26.9 38.8 Q25.2 35.8 24.9 32.1"/>
<path d="M32.3 33.1 Q30.9 30.3 30.4 26.9"/>
<path d="M37.8 27.0 Q36.6 24.6 36.3 21.7"/>
<path d="M26.9 38.8 Q30.5 40.1 33.7 40.3"/>
<path d="M32.3 33.1 Q35.8 34.3 38.6 34.5"/>
<path d="M37.8 27.0 Q40.8 27.9 43.2 28.1"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB