This commit is contained in:
Team3
2026-06-05 19:54:34 +02:00
commit 3ed5f7c3e5
18 changed files with 2670 additions and 0 deletions

7
.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
**/node_modules
**/__pycache__
**/*.pyc
frontend/dist
creator.db
.git
.env

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
creator.db
node_modules/
frontend/dist/
__pycache__/
*.pyc
.env

25
Dockerfile Normal file
View File

@@ -0,0 +1,25 @@
# 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 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 --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"]

26
Makefile Normal file
View File

@@ -0,0 +1,26 @@
.PHONY: install dev prod stop logs
COMPOSE = docker compose
install:
pip install --break-system-packages -r backend/requirements.txt
cd frontend && npm install
dev:
@echo "Backend: http://localhost:8000"
@echo "Frontend: http://localhost:5173"
@cd backend && uvicorn main:app --reload --port 8000 &
@cd frontend && npx vite --port 5173
prod:
@touch creator.db
$(COMPOSE) up -d --build
stop:
-@pkill -f "uvicorn main:app" 2>/dev/null
-@pkill -f "vite --port 5173" 2>/dev/null
$(COMPOSE) down
@echo "Server gestoppt."
logs:
$(COMPOSE) logs -f

5
backend/config.py Normal file
View File

@@ -0,0 +1,5 @@
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
FRONTEND_DIST = PROJECT_ROOT / "frontend" / "dist"
DB_PATH = PROJECT_ROOT / "creator.db"

30
backend/database.py Normal file
View File

@@ -0,0 +1,30 @@
import aiosqlite
from config import DB_PATH
_db: aiosqlite.Connection | None = None
async def get_db() -> aiosqlite.Connection:
global _db
if _db is None:
_db = await aiosqlite.connect(DB_PATH)
return _db
async def init_db():
db = await get_db()
# Tabellen folgen, sobald die ersten Features stehen.
await db.commit()
async def close_db():
global _db
if _db is not None:
await _db.close()
_db = None
def _row_to_dict(row, cursor):
columns = [d[0] for d in cursor.description]
return dict(zip(columns, row))

23
backend/main.py Normal file
View File

@@ -0,0 +1,23 @@
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from config import FRONTEND_DIST
from database import init_db, close_db
from routes import router
@asynccontextmanager
async def lifespan(app: FastAPI):
await init_db()
yield
await close_db()
app = FastAPI(title="Creator", lifespan=lifespan)
app.include_router(router)
if FRONTEND_DIST.exists():
app.mount("/", StaticFiles(directory=FRONTEND_DIST, html=True), name="frontend")

3
backend/requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
fastapi
uvicorn[standard]
aiosqlite

8
backend/routes.py Normal file
View File

@@ -0,0 +1,8 @@
from fastapi import APIRouter
router = APIRouter(prefix="/api")
@router.get("/health")
async def health():
return {"ok": True}

20
docker-compose.yml Normal file
View File

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

22
frontend/index.html Normal file
View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Creator</title>
<script>
(function () {
var stored = localStorage.getItem('darkMode')
var dark = stored === null
? window.matchMedia('(prefers-color-scheme: dark)').matches
: stored === 'true'
if (dark) document.documentElement.classList.add('dark')
})()
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

8
frontend/jsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

2305
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
frontend/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "frontend",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.32"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.6",
"vite": "^8.0.8",
"vite-plugin-vue-devtools": "^8.1.1"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}

127
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,127 @@
<script setup>
import { ref, onMounted } from 'vue'
import { fetchHealth } from './api.js'
const backendOk = ref(false)
const darkMode = ref(
localStorage.getItem('darkMode') === null
? window.matchMedia('(prefers-color-scheme: dark)').matches
: localStorage.getItem('darkMode') === 'true',
)
function applyTheme() {
document.documentElement.classList.toggle('dark', darkMode.value)
}
function toggleDark() {
darkMode.value = !darkMode.value
localStorage.setItem('darkMode', darkMode.value ? 'true' : 'false')
applyTheme()
}
applyTheme()
onMounted(async () => {
try {
const res = await fetchHealth()
backendOk.value = res.ok === true
} catch (e) {
console.error('Backend nicht erreichbar:', e)
}
})
</script>
<template>
<div class="layout">
<header class="topbar">
<h1>Creator</h1>
<button class="theme-toggle" @click="toggleDark">{{ darkMode ? '' : '' }}</button>
</header>
<main class="main">
<p>Backend: <span :class="backendOk ? 'ok' : 'err'">{{ backendOk ? 'verbunden' : 'nicht erreichbar' }}</span></p>
</main>
</div>
</template>
<style>
:root {
--bg: #f8f9fb;
--panel: #ffffff;
--border: #e2e5e9;
--text: #1a1a1a;
--text-muted: #4b5563;
--accent: #6366f1;
--success: #065f46;
--danger: #991b1b;
}
html.dark {
--bg: #15171c;
--panel: #1c1f26;
--border: #2c3038;
--text: #e6e8ee;
--text-muted: #9aa3b2;
--accent: #6366f1;
--success: #34d399;
--danger: #f87171;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
}
.layout {
display: flex;
flex-direction: column;
height: 100vh;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1.25rem;
background: var(--panel);
border-bottom: 1px solid var(--border);
h1 {
font-size: 1.1rem;
}
.theme-toggle {
background: none;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
cursor: pointer;
font-size: 1rem;
padding: 0.25rem 0.6rem;
&:hover {
border-color: var(--accent);
}
}
}
.main {
flex: 1;
padding: 1.25rem;
color: var(--text-muted);
.ok {
color: var(--success);
}
.err {
color: var(--danger);
}
}
</style>

6
frontend/src/api.js Normal file
View File

@@ -0,0 +1,6 @@
const BASE = '/api'
export async function fetchHealth() {
const res = await fetch(`${BASE}/health`)
return res.json()
}

4
frontend/src/main.js Normal file
View File

@@ -0,0 +1,4 @@
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

23
frontend/vite.config.js Normal file
View File

@@ -0,0 +1,23 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
server: {
proxy: {
'/api': 'http://localhost:8000',
},
},
})