kirche-einfach/PLAN_NEXT.md

15 KiB
Raw Blame History

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.

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.