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
+64
View File
@@ -0,0 +1,64 @@
"""Application configuration, loaded from the environment and/or a .env file."""
from __future__ import annotations
import secrets
from functools import lru_cache
from pathlib import Path
from typing import Literal
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""Runtime configuration. Every variable is prefixed ``LEMBAS_``."""
model_config = SettingsConfigDict(
env_prefix="LEMBAS_",
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
secret_key: str = Field(default="")
data_dir: Path = Path("./data")
host: str = "127.0.0.1"
port: int = 8080
reload: bool = False
log_level: Literal["debug", "info", "warning", "error"] = "info"
allow_signup: bool = True
default_theme: Literal["moria", "shire"] = "moria"
session_ttl: int = 60 * 60 * 24 * 30
request_timeout: float = 300.0
@field_validator("secret_key")
@classmethod
def _generate_secret_if_absent(cls, v: str) -> str:
# A generated key lets `lembas serve` work out of the box, but it changes
# on every restart: sessions drop and stored API keys become unreadable.
# main.py warns loudly about this. Never rely on it in production.
return v or secrets.token_urlsafe(48)
@property
def db_path(self) -> Path:
return self.data_dir / "lembas.db"
@property
def uploads_dir(self) -> Path:
return self.data_dir / "uploads"
def ensure_dirs(self) -> None:
self.data_dir.mkdir(parents=True, exist_ok=True)
self.uploads_dir.mkdir(parents=True, exist_ok=True)
@lru_cache
def get_settings() -> Settings:
"""Cached singleton so config is parsed once per process."""
return Settings()
settings = get_settings()