Files
Ondřej Glaser 48cef99257 Initial portal commit: landing + 9 AI-powered apps
Apps:
- dwg-rooms: extract room numbers from DWG/DXF
- dwg-counting: count symbols in PDF drawings (OpenCV template matching)
- contract-check: review PDF contracts against a checklist (Claude vision + Tesseract OCR fallback)
- email-drafter: bullet notes → polished Czech/English business emails
- invoice-extractor: PDF/image invoice → structured data → Excel
- translator: Czech-first translator across 19 languages with tone control
- vv-check: find inconsistent unit prices across VV sheets in one workbook
- vv-compare: diff original vs new VV files (changes / added / removed)
- feature-request: portal users submit ideas + sample files

Infrastructure:
- LiteLLM gateway with per-app virtual keys + budgets
- Langfuse observability
- Geist font, shared theme, cross-subdomain back link + theme sync via cookie/URL
- Caddy reverse proxy on *.klas.chat
2026-05-13 15:25:04 +02:00

95 lines
2.9 KiB
Python

"""FastAPI: collect feature requests from portal users.
Stores each request to a timestamped folder on disk so the team can review
later. No LLM, no external calls — pure form handler.
"""
import datetime
import json
import logging
import os
import re
import uuid
from pathlib import Path
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI(title="Feature Request")
app.add_middleware(CORSMiddleware, allow_origins=["*"],
allow_methods=["*"], allow_headers=["*"])
# Volume-mounted; survives container rebuilds.
STORAGE = Path(os.getenv("STORAGE_DIR", "/data/requests"))
STORAGE.mkdir(parents=True, exist_ok=True)
MAX_FILE_BYTES = 25 * 1024 * 1024 # 25 MB cap
def _slug(text: str) -> str:
text = re.sub(r"[^\w\s-]", "", text, flags=re.UNICODE).strip().lower()
text = re.sub(r"[\s_-]+", "-", text)
return text[:40] or "request"
@app.get("/")
async def root():
return FileResponse("static/index.html")
@app.post("/api/submit")
async def submit(
title: str = Form(..., min_length=3, max_length=120),
description: str = Form(..., min_length=10, max_length=8000),
name: str = Form("", max_length=120),
email: str = Form("", max_length=200),
file: UploadFile | None = File(None),
):
title = title.strip()
description = description.strip()
if not title or not description:
raise HTTPException(400, "Vyplňte název a popis")
now = datetime.datetime.now()
folder_name = f"{now.strftime('%Y%m%d_%H%M%S')}_{_slug(title)}_{uuid.uuid4().hex[:8]}"
folder = STORAGE / folder_name
folder.mkdir(parents=True, exist_ok=True)
record = {
"submitted_at": now.isoformat(timespec="seconds"),
"title": title,
"description": description,
"name": name.strip(),
"email": email.strip(),
"attachment": None,
}
if file is not None and file.filename:
raw = await file.read()
if len(raw) > MAX_FILE_BYTES:
raise HTTPException(400, f"Soubor je příliš velký (max {MAX_FILE_BYTES // (1024*1024)} MB)")
safe_name = re.sub(r"[^\w\.\-]", "_", file.filename)[:200]
attachment_path = folder / safe_name
attachment_path.write_bytes(raw)
record["attachment"] = safe_name
record["attachment_bytes"] = len(raw)
record["attachment_content_type"] = file.content_type or ""
(folder / "request.json").write_text(
json.dumps(record, ensure_ascii=False, indent=2), encoding="utf-8"
)
logger.info("Stored feature request → %s (%s)", folder_name, title)
return {"ok": True, "id": folder_name}
@app.get("/health")
async def health():
return {"status": "ok"}
app.mount("/static", StaticFiles(directory="static"), name="static")