This commit is contained in:
root
2026-05-26 07:05:54 +00:00
parent 619bac34cb
commit 8fa8f9fb27
9 changed files with 117 additions and 28 deletions

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
**/node_modules
**/__pycache__
**/*.pyc
frontend/dist
storage
guides.db
.git
.claude-data

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ node_modules/
frontend/dist/ frontend/dist/
__pycache__/ __pycache__/
*.pyc *.pyc
.claude-data/

43
Dockerfile Normal file
View File

@@ -0,0 +1,43 @@
# Stage 1: Frontend bauen
FROM node:20-alpine AS frontend
WORKDIR /build
COPY frontend/package.json frontend/package-lock.json ./
RUN npm install
COPY frontend/ ./
RUN npm run build
# Stage 2: Runtime
FROM python:3.12-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
poppler-utils \
libpango-1.0-0 \
libpangoft2-1.0-0 \
libharfbuzz0b \
libcairo2 \
libgdk-pixbuf-2.0-0 \
libffi-dev \
fonts-dejavu \
curl \
ca-certificates \
gnupg \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs \
&& npm install -g @anthropic-ai/claude-code \
&& rm -rf /var/lib/apt/lists/*
RUN useradd -m -u 1000 app
COPY backend/requirements.txt /app/backend/requirements.txt
RUN pip install --no-cache-dir -r /app/backend/requirements.txt
COPY --chown=app:app backend/ /app/backend/
COPY --chown=app:app templates/ /app/templates/
COPY --chown=app:app --from=frontend /build/dist /app/frontend/dist
RUN chown app:app /app
USER app
WORKDIR /app/backend
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -1,4 +1,12 @@
.PHONY: install dev prod stop remove .PHONY: install dev prod stop logs remove auth
COMPOSE = docker compose
auth:
@mkdir -p .claude-data storage
@cp -aT /root/.claude .claude-data
@chown -R 1000:1000 .claude-data storage
@echo "Claude-Auth nach .claude-data synct, storage chowned auf uid 1000."
install: install:
sudo apt install -y poppler-utils libpango-1.0-0 libcairo2 libgdk-pixbuf-2.0-0 libffi-dev sudo apt install -y poppler-utils libpango-1.0-0 libcairo2 libgdk-pixbuf-2.0-0 libffi-dev
@@ -11,18 +19,19 @@ dev:
@cd backend && uvicorn main:app --reload --port 8000 & @cd backend && uvicorn main:app --reload --port 8000 &
@cd frontend && npx vite --port 5173 @cd frontend && npx vite --port 5173
prod: prod: auth
@echo "Backend: http://localhost:8000" $(COMPOSE) up -d --build
@cd frontend && npx vite build
@cd backend && uvicorn main:app --host 0.0.0.0 --port 8000
stop: stop:
-@pkill -f "uvicorn main:app" 2>/dev/null -@pkill -f "uvicorn main:app" 2>/dev/null
-@pkill -f "vite --port 5173" 2>/dev/null -@pkill -f "vite --port 5173" 2>/dev/null
$(COMPOSE) down
@echo "Server gestoppt." @echo "Server gestoppt."
logs:
$(COMPOSE) logs -f
remove: stop remove: stop
@echo "Lösche Datenbank und generierte Dateien..." @echo "Lösche Datenbank und generierte Dateien..."
rm -f guides.db rm -rf storage/*
rm -rf storage/html/* storage/pdf/*
@echo "Fertig." @echo "Fertig."

View File

@@ -3,7 +3,8 @@ from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent PROJECT_ROOT = Path(__file__).resolve().parent.parent
TEMPLATES_DIR = PROJECT_ROOT / "templates" TEMPLATES_DIR = PROJECT_ROOT / "templates"
STORAGE_DIR = PROJECT_ROOT / "storage" STORAGE_DIR = PROJECT_ROOT / "storage"
DB_PATH = PROJECT_ROOT / "guides.db" FRONTEND_DIST = PROJECT_ROOT / "frontend" / "dist"
DB_PATH = STORAGE_DIR / "guides.db"
ALLOWED_FORMATS = [ ALLOWED_FORMATS = [
"OnePager", "OnePager",
@@ -24,15 +25,15 @@ FORMAT_META = {
} }
GENERATION_TIMEOUTS = { GENERATION_TIMEOUTS = {
"OnePager": 600, "OnePager": 900,
"Cheatsheet": 600, "Cheatsheet": 900,
"MiniGuide": 600, "MiniGuide": 1200,
"BeginnerGuide": 900, "BeginnerGuide": 1800,
"IntermediateGuide": 1200, "IntermediateGuide": 2400,
"ExtendedGuide": 1500, "ExtendedGuide": 3000,
} }
MAX_CONCURRENT_GENERATIONS = 10 MAX_CONCURRENT_GENERATIONS = 6
MAX_ITERATIONS = { MAX_ITERATIONS = {
"OnePager": 3, "OnePager": 3,
"Cheatsheet": 3, "Cheatsheet": 3,

View File

@@ -45,11 +45,19 @@ async def _run_claude(guide_id: str, prompt: str, timeout: int) -> tuple[int, st
stderr=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
) )
_active_processes[guide_id] = process _active_processes[guide_id] = process
try:
try: try:
stdout, stderr = await asyncio.wait_for( stdout, stderr = await asyncio.wait_for(
process.communicate(input=prompt.encode("utf-8")), process.communicate(input=prompt.encode("utf-8")),
timeout=timeout, timeout=timeout,
) )
except asyncio.TimeoutError:
process.kill()
try:
await asyncio.wait_for(process.wait(), timeout=5)
except asyncio.TimeoutError:
pass
raise
return process.returncode, stdout.decode("utf-8", errors="replace"), stderr.decode("utf-8", errors="replace") return process.returncode, stdout.decode("utf-8", errors="replace"), stderr.decode("utf-8", errors="replace")
finally: finally:
_active_processes.pop(guide_id, None) _active_processes.pop(guide_id, None)

View File

@@ -1,9 +1,9 @@
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles
from config import STORAGE_DIR from config import FRONTEND_DIST, STORAGE_DIR
from database import init_db, close_db from database import init_db, close_db
from routes import router from routes import router
@@ -20,11 +20,7 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="Guides Generator", lifespan=lifespan) app = FastAPI(title="Guides Generator", lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(router) app.include_router(router)
if FRONTEND_DIST.exists():
app.mount("/", StaticFiles(directory=FRONTEND_DIST, html=True), name="frontend")

View File

@@ -1,3 +1,5 @@
fastapi fastapi
uvicorn[standard] uvicorn[standard]
aiosqlite aiosqlite
weasyprint
pdf2image

21
docker-compose.yml Normal file
View File

@@ -0,0 +1,21 @@
services:
guides:
build:
context: .
container_name: guides
restart: unless-stopped
networks:
- web
volumes:
- ./storage:/app/storage
- ./.claude-data:/home/app/.claude
labels:
- "traefik.enable=true"
- "traefik.http.routers.guidesapp.rule=Host(`guides.marha.de`)"
- "traefik.http.routers.guidesapp.entrypoints=websecure"
- "traefik.http.routers.guidesapp.tls.certresolver=letsencrypt"
- "traefik.http.services.guidesapp.loadbalancer.server.port=8000"
networks:
web:
external: true