263 lines
15 KiB
Markdown
263 lines
15 KiB
Markdown
# Continuation plan — kirche-einfach next steps (written after Phase 0)
|
||
|
||
> For a future agent restoring this session (e.g. after a container rebuild).
|
||
> Read this file + `AGENTS.md` + `README.md` first. All code/comments in
|
||
> English; only user-visible website strings are German.
|
||
|
||
## 0. Where we are
|
||
|
||
**Phase 0 (this commit) is done** — Docker/devcontainer reproducibility:
|
||
|
||
* `langflow` image pinned via `${LANGFLOW_VERSION:-1.12.0}` (was `:latest`
|
||
with nightly Watchtower drift). `Dockerfile.lfx` pins `lfx~=<same>`
|
||
(`ARG LFX_VERSION=1.12.0`). Backend test dep pinned `lfx~=1.12.0`.
|
||
Reason: official LFX contract — `major.minor` of Langflow and LFX must
|
||
match, patches are independent.
|
||
* `langflow` mounts `backend/components` (`LANGFLOW_COMPONENTS_PATH`) and
|
||
`backend/flows` (`LANGFLOW_LOAD_FLOWS_PATH`), both `:ro`. Files stay
|
||
source of truth; the API watcher (`app/flows/sync.py`) remains the ONLY
|
||
sync path — never add a second watcher.
|
||
* Backend venv moved to `/opt/venv` (image-local, `UV_PROJECT_ENVIRONMENT`),
|
||
managed Python to `/opt/python` (`UV_PYTHON_INSTALL_DIR`), both `a+rX`.
|
||
Removed `kirche-venv` + `kirche-node-modules` named volumes (they shadowed
|
||
image content with empty volumes on first start = split-brain).
|
||
`backend/entrypoint.sh` uses `/opt/venv/bin/*`, ensures `data/` exists and
|
||
is `vscode`-owned. `frontend/entrypoint.sh` runs `npm ci` when
|
||
`node_modules/.bin/vite` is missing (covers fresh prod checkouts).
|
||
* `devcontainer.json`: opencode session persistence via named volumes
|
||
`kirche-opencode-share → ~/.local/share/opencode` (sessions DB) and
|
||
`kirche-opencode-config → ~/.config/opencode`; interpreter path is now
|
||
`/opt/venv/bin/python`. `setup.sh` chowns first, then runs `uv sync` as
|
||
`vscode` (was: root `uv sync` into `/root`-backed venv → `Permission
|
||
denied` for vscode; `backend/data` root-owned → DB unwritable).
|
||
* Stale local `backend/.venv/` (bind-mount leftover) can be deleted manually
|
||
after rebuild: `rm -rf backend/.venv`. It is gitignored; nothing references
|
||
it anymore.
|
||
|
||
**Verified in this session:** `uv run pytest` green (after local venv repair,
|
||
see §5). NOT verifiable here (no node/docker in this sandbox): `npm run
|
||
build`, `docker compose config`, container rebuild. CI covers all three
|
||
(`.forgejo/workflows/`). After rebuilding the devcontainer, re-run: `uv run
|
||
pytest` (attached api, from `backend/`), `docker compose exec frontend npm
|
||
run build`, `docker compose config --quiet`.
|
||
|
||
**Still open:** nothing from the original list — Phases 1–4 are implemented
|
||
(see §0.2). Remaining eyeball checks: `kirche` category in the Langflow
|
||
palette, RJSF form UX in a browser, magic-link mail with real SMTP.
|
||
|
||
## 0.1 Follow-up fixes (commits after Phase 0)
|
||
|
||
`fix: devcontainer build and setup on fresh clones` + `feat: magic-link
|
||
auth` + the LFX/stack repair commit:
|
||
|
||
* `docker-outside-of-docker` needs `"moby": false` (`python:3.12-slim` is
|
||
Debian trixie, no moby-cli in its repos). Removed the manual
|
||
`/var/run/docker.sock` bind (breaks Windows, feature handles it).
|
||
* `setup.sh` used `su vscode -c` (drops env incl. `UV_PROJECT_ENVIRONMENT`
|
||
→ stray `backend/.venv`, blind sync). Now `sudo -u vscode -E` helper.
|
||
Missing `.env` is provisioned from `.env.example`, not fatal.
|
||
* Compose `env_file` is `required: false` everywhere (fresh clones, CI).
|
||
Images install `curl git ca-certificates`; `.dockerignore` files keep
|
||
host `node_modules/.venv/data` out of build contexts.
|
||
* `settings.database_url` default is now a real `sqlite:///` URL (bare
|
||
path crashed `create_engine`/alembic; api could never boot uncconfigured).
|
||
* `Dockerfile.lfx` CMD dropped the positional `/flows`: `lfx>=1.12.1`
|
||
exits on an empty flow dir (restart-loop → whole stack `unhealthy`).
|
||
`lfx` service blanks `LANGFLOW_API_KEY` (server prefers it over
|
||
`LFX_API_KEY`, so backend deploys 401'd and `/flows` stayed empty).
|
||
* Both entrypoints run via explicit `sh` (git tracks them 100644; the
|
||
bind-mount shadows image perms, breaking Windows checkouts).
|
||
* Verified live: full `compose up` green (api/frontend/langflow/lfx),
|
||
startup sync deploys `backend/flows` to LFX, `uv run pytest` 60 green.
|
||
Before Reopen in Container: `docker compose down` any manual stack
|
||
(same project name → port clash).
|
||
## 0.2 Phases 1–4 (done, with deviations)
|
||
|
||
* Phase 1 auth as planned (`app/auth/*`, migration `a3f1c9e2b4d7`).
|
||
Additionally fixed: `settings.database_url` default is a real
|
||
`sqlite:///` URL (bare path crashed `create_engine`/alembic).
|
||
* Phase 2: `CustomInputForm` has the 10 planned inputs **plus one hidden
|
||
`form_values: MessageTextInput (show=False)` plumbing input**. Without
|
||
it, submitted web-form values never reached the run (LFX routes run
|
||
input to entry nodes; scalar fields are not connectable). Fixture flow
|
||
`intake_form.json`: `ChatInput → form.form_values`,
|
||
`form.form_data → ChatOutput`; `collect()` merges the envelope
|
||
(`{"payload": {...}}` unwrapped like the other components) over field
|
||
defaults. Verified live: run with values returns them in the output.
|
||
* Phase 2 generator caveat: lfx 1.12.x `to_frontend_node()` **cannot
|
||
re-instantiate `MultiselectInput`** (emits `list`, constructor forbids
|
||
extras — their own builtin `SearXNGToolComponent` fails identically).
|
||
`kirche_node()` therefore replicates the pre-validation dict build and
|
||
appends the `code` field by hand. If a future lfx fixes this, simplify
|
||
back to plain `to_frontend_node()`.
|
||
* Phase 3 as planned (`input_schema.py`, `FlowVersion.input_schema`,
|
||
migration `b7e2d4a1c9f3`, `GET input-schema`). Deviations: schema is
|
||
cached in `store.create_version` (single choke point: sync, import, API
|
||
all covered); parser skips `show: false` fields; enum-mismatched
|
||
defaults omitted; endpoint maps `InvalidError` → 404 too
|
||
(`get_latest_deployed_version` raises it, not `NotFoundError`).
|
||
* Phase 4 as planned (`react-router-dom@7`, `@rjsf/core` +
|
||
`@rjsf/validator-ajv8` (v6), English routes, German labels,
|
||
`credentials: include`, old tab shell dropped). `FlowsTab.tsx` deleted;
|
||
`ApprovalsTab`/`AdminTab` reused inside Tasks/Admin pages. Verified:
|
||
`npm run build` green, routes + `/api` proxy return 200, served schema
|
||
validates under JSON-Schema Draft 7.
|
||
|
||
## 0.3 Frontend design pass (Mantine)
|
||
|
||
Plain-HTML UI replaced with a coherent Mantine v9 design system:
|
||
`@mantine/core` + `@mantine/hooks` + `@rjsf/mantine` (v6, matches core)
|
||
+ `@tabler/icons-react`. `src/theme.ts` (kirchenblau primary, system
|
||
fonts only, serif display headings), `AppShell` header/nav, per-page
|
||
`Card`/`Table`/`Alert`/`Badge` via shared `StatusBadge`, dark mode via
|
||
localStorage manager + toggle + inline `ColorSchemeScript` equivalent.
|
||
RJSF swap was import-only; SSR smoke test proved all 10 intake labels +
|
||
textarea/password/checkbox/slider widgets render. `npm run build` green.
|
||
|
||
## 0.4 Landing page (conversion structure + screenshot automation)
|
||
|
||
StartPage rebuilt along the 8-block anatomy (hero with outcome headline,
|
||
fact bar, 3 benefits, real product screenshot, 3 steps, trust list, FAQ,
|
||
final CTA, footer mini) — one CTA everywhere (`/access`), honest copy
|
||
(no fake testimonials/logos/numbers; pricing section omitted, model open).
|
||
Centralized hosting in Germany per operator decision (not self-hosted).
|
||
`frontend/scripts/capture.mjs` + `npm run screenshots` (Playwright/Chromium
|
||
in the frontend container) regenerates `public/screenshots/*.png`; the
|
||
workflows shot auto-selects `intake_form` to show the generated RJSF form.
|
||
Vite dev `server.watch` polling enabled (Windows bind-mount events often
|
||
never reach the Linux watcher → stale modules otherwise).
|
||
|
||
## 0.5 Email-first login (`POST /api/auth/magic/request`)
|
||
|
||
`/login` without `?token=` shows an email form (+ "Hier anfragen" link).
|
||
Allowed = approved user row or `ADMIN_EMAILS` (admin auto-created on first
|
||
request); anything else → 403 and the frontend forwards to
|
||
`/access?email=…` (prefilled). Rate-limited (5/hour), token by mail only,
|
||
delivery failures never 500 (see §0.4 hardening).
|
||
|
||
## 0.6 Polish round (bell-tower favicon, lfx-clean flows, HTML mail)
|
||
|
||
* Favicon simplified to a stylized bell tower (spire + shaft + bell-arch
|
||
knockout); PNGs regenerated via `npm run icons`.
|
||
* `lfx validate --level 4` now green for ALL canonical flows + regression
|
||
test (`test_lfx_validate.py`). Root cause: required form fields have no
|
||
edge (values arrive via the form_values envelope). Fix: generator writes
|
||
a single-space sentinel value for required template fields; the form
|
||
parser treats whitespace-only values as "no default". L3
|
||
Message-vs-str warnings are known lfx-validator noise (MessageTextInput
|
||
accepts Message at runtime).
|
||
* New-input authoring docs: `backend/components/kirche/README.md`.
|
||
* Magic-link + access-notification mails are branded multipart
|
||
(text + HTML): kirchenblau header, serif headline, real button link,
|
||
German copy, email-safe tables/inline styles, no external assets.
|
||
|
||
## 1. Locked decisions (do not re-ask)
|
||
|
||
* Auth: SMTP magic-link, 15-min single-use tokens (`secrets.token_urlsafe`,
|
||
store `sha256`, `used_at` set before session creation), session = opaque id
|
||
in **DB table**, cookie `HttpOnly; Secure; SameSite=Lax; Path=/`.
|
||
SMTP creds already in `.env` (`SMTP_HOST/PORT/USERNAME/PASSWORD`,
|
||
`MAIL_FROM`, `CONTACT_TO`); dev fallback logs the link when SMTP unset.
|
||
* `FileInput` is a **stub** for now (string reference only); real upload later.
|
||
* Frontend: `react-router-dom` + RJSF (`@rjsf/core` + `@rjsf/validator-ajv8`).
|
||
RJSF renders backend-supplied `{json_schema, ui_schema}`; server
|
||
re-validates (client validation is UX only).
|
||
* **Routes are pure English** (user correction — no German slugs anywhere,
|
||
including URLs):
|
||
`/`, `/access` (was once sketched as `/zugang` — do NOT use),
|
||
`/login?token=`, `/app/workflows`, `/app/tasks`, `/app/admin`.
|
||
Visible labels/nav are German (`Start`, `Zugang`, `Workflows`, `Aufgaben`,
|
||
`Verwaltung`); code, props, comments, routes English.
|
||
* Langflow preload + mount (done in Phase 0). Custom-component lib in
|
||
`backend/components/kirche/` **must stay GUI-editable**: category folder +
|
||
`__init__.py` + `Component` subclasses using `lfx.io` inputs only
|
||
(stdlib + `lfx`, so the LFX runtime can execute them).
|
||
* Watchtower: `langflow` label removed (pinned images must not auto-update).
|
||
Version bumps = deliberate `LANGFLOW_VERSION` + `LFX_VERSION` change, then
|
||
`lfx upgrade --upgrade-flow=safe` check over `backend/flows/*.json`.
|
||
|
||
## 2. Phase 1 — Backend auth (magic-link + RBAC)
|
||
|
||
Files: `backend/app/models/user.py` (`User`, `AccessRequest`,
|
||
`MagicLinkToken`, `Session`), `backend/app/auth/{schemas,service,mail,api,
|
||
deps}.py`, wire router in `app/main.py`, settings in `app/settings.py`
|
||
(`frontend_url, admin_emails, contact_to, mail_from, smtp_*`), ONE new
|
||
Alembic revision (never edit applied migrations).
|
||
|
||
Endpoints:
|
||
|
||
* `POST /api/access/requests` public, rate-limited: `{name,email,reason}`
|
||
→ row `pending` + notify `CONTACT_TO`.
|
||
* `POST /api/access/requests/{id}/approve|reject` admin: approve →
|
||
upsert `User` + emit magic-link + SMTP send.
|
||
* `POST /api/auth/magic/consume {token}` → session cookie; `GET
|
||
/api/auth/me`; `POST /api/auth/logout` (delete session row).
|
||
* `POST /api/auth/admin-bootstrap`: only when user table empty AND email in
|
||
`ADMIN_EMAILS`.
|
||
* Tenant from session (`user.tenant_id`); keep old `tenant_id` body param as
|
||
deprecated fallback. `require_admin` dependency; keep API handlers thin
|
||
(DB only via `app/flows/store.py` + new `app/auth/*` service funcs).
|
||
|
||
Tests (all in `backend/tests/`): request→approve→consume→me→logout happy
|
||
path; token expiry boundary (15 min); token reuse → 400; admin-only → 403;
|
||
bootstrap-once; cookie flags (`HttpOnly`, `SameSite`, `Path`); assert no
|
||
token/secret in logs.
|
||
|
||
## 3. Phase 2 — `CustomInput*` library (GUI-editable)
|
||
|
||
* `backend/components/kirche/__init__.py` +
|
||
`backend/components/kirche/form_inputs.py`:
|
||
`class CustomInputForm(Component)` with inputs `StrInput`,
|
||
`MultilineInput`, `SecretStrInput`, `IntInput`, `FloatInput`, `BoolInput`,
|
||
`DropdownInput`, `MultiselectInput`, `SliderInput`, `FileInput` (stub) +
|
||
`Output`. Category dir `kirche/` = appears in Langflow GUI.
|
||
* Regenerate/add fixture flow using it via
|
||
`backend/scripts/make_flow_fixtures.py`; keep `simple_rule.json` as the
|
||
textarea-fallback case. Commit flow JSON (source of truth).
|
||
|
||
## 4. Phase 3 — Dynamic-form parsing (no second watcher)
|
||
|
||
* New `backend/app/flows/input_schema.py`:
|
||
`extract_form_schema(langflow_json) -> {json_schema, ui_schema}`.
|
||
Find nodes with `type`/`display_name` starting `CustomInput`; iterate
|
||
`template` minus `code`; map `_input_type` → JSON Schema type/widget
|
||
(`StrInput`/`MessageTextInput`→string, `MultilineInput`→string+textarea,
|
||
`SecretStrInput`→string+password, `IntInput`→integer,
|
||
`FloatInput`→number, `BoolInput`→boolean, `DropdownInput`→string+enum,
|
||
`MultiselectInput`→array+enum, `SliderInput`→number+range,
|
||
`FileInput`→string stub); `display_name/info/value/options/required/
|
||
advanced` → `title/description/default/enum/required/uiSchema`.
|
||
No such node → `{fallback: "textarea"}`.
|
||
* Called from existing `sync_from_directory` (cache per version in new
|
||
nullable `input_schema` JSON column via Alembic) + new `GET
|
||
/api/flows/{id}/input-schema → {flow_id, version_no, json_schema,
|
||
ui_schema}` (latest **deployed** version; 404 when none).
|
||
* Tests: per-type mapping table, fallback, sync persistence, endpoint 404.
|
||
|
||
## 5. Phase 4 — Frontend (router + RJSF, German UI, English routes)
|
||
|
||
Deps: `react-router-dom`, `@rjsf/core`, `@rjsf/validator-ajv8`.
|
||
Routes (English, see §1): `/`, `/access`, `/login`, `/app/workflows`
|
||
(flow select → `GET input-schema` → RJSF `<Form>` → `POST run`),
|
||
`/app/tasks` (own runs + approval states), `/app/admin` (admin-only:
|
||
access-request queue, ALL tasks incl. open human-intervention with
|
||
approve/reject + note, import/sync/deploy). `fetch` with
|
||
`credentials: "include"`. Refactor `FlowsTab/ApprovalsTab/AdminTab` into
|
||
route components; drop old tab shell. Verify with `npm run build`.
|
||
|
||
## 6. Operational notes for the next agent
|
||
|
||
* Commands: backend from `backend/`: `uv run pytest`,
|
||
`uv run alembic upgrade head`, `uv run python scripts/sync_flows.py
|
||
--sync`; frontend via `docker compose exec frontend npm run build`.
|
||
* Local venv repair (this sandbox had a root-backed broken `.venv`):
|
||
`rm -rf backend/.venv && uv sync` (network needed for `uv python
|
||
install`). In rebuilt containers this is unnecessary (`/opt/venv`).
|
||
* `.env` (gitignored) still lacks `FRONTEND_URL`/`ADMIN_EMAILS` from the
|
||
`.env.example` diff — copy them over locally when starting Phase 1. Never
|
||
commit `.env`.
|
||
* Flow JSON that fails LFX after a version bump: run
|
||
`lfx upgrade --upgrade-flow=safe` on the file, review diff, commit.
|
||
* Definition of done per `AGENTS.md`: new behavior has tests, `uv run
|
||
pytest` green, `npm run build` green when frontend changed, schema changes
|
||
as new Alembic revisions.
|