commit 60f8399f8520b63bc0c463064b903c800a238c67 Author: Chris Amow Date: Sun Aug 9 23:34:01 2026 +0000 Scaffold FastAPI + Vue3 placeholder app with local docker compose stack diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc4f72a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.pyc +.venv/ +.env diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..93a6e20 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,11 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..b98f12c --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: uvicorn main:app --host 0.0.0.0 --port 8000 diff --git a/README.md b/README.md new file mode 100644 index 0000000..ec5a44d --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# chart + +FastAPI backend + Vue 3 (from CDN, no build step) served at +. + +Currently a placeholder: the frontend calls `/api/hello` and prints the JSON. + +## Local development + +```bash +docker compose up --build +``` + +Then open . The source tree is bind-mounted and uvicorn +runs with `--reload`, so edits to `main.py` or `static/` take effect without a +rebuild. Rebuild only when `requirements.txt` changes. + +Without Docker: + +```bash +python3 -m venv .venv && . .venv/bin/activate +pip install -r requirements.txt +uvicorn main:app --reload +``` + +## Layout + +| Path | Purpose | +|---|---| +| `main.py` | FastAPI app — JSON under `/api`, serves the SPA at `/` | +| `static/` | `index.html`, `app.js`, `style.css` — Vue 3 loaded from unpkg | +| `requirements.txt` | Python deps | +| `Procfile` | Start command; **nixpacks needs this** or the deploy has nothing to run | +| `Dockerfile.dev`, `docker-compose.yml` | Local dev only — production does not use them | + +## Deployment + +Push to `main` → Forgejo webhook → Coolify rebuilds with nixpacks → live in +roughly 25 seconds. + +```bash +git push +``` + +Two things to know before changing the deploy config: + +- The Coolify domain is registered as `https://chart.amow.com:8000`. The port + suffix is how Coolify decides the Traefik target port; drop it and every + request 502s while the container looks perfectly healthy. It does not appear + in the public URL. +- Ignore a 502 in the first minute after a deploy — that's the rolling container + swap, and it clears on its own. + +Manual redeploy: + +```bash +curl -X POST -H "Authorization: Bearer $(cat ~/.coolify-token)" \ + "http://127.0.0.1:8000/api/v1/deploy?uuid=$COOLIFY_UUID&force=true" +``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c3e4fb9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +# Local development stack. Production (chart.amow.com) is built by Coolify +# with nixpacks straight from this repo — it does not use this file. +services: + api: + build: + context: . + dockerfile: Dockerfile.dev + ports: + - "8000:8000" + volumes: + # Bind-mount the source so --reload picks up edits without a rebuild. + - .:/app + environment: + - PYTHONDONTWRITEBYTECODE=1 + - PYTHONUNBUFFERED=1 diff --git a/main.py b/main.py new file mode 100644 index 0000000..1cca4ee --- /dev/null +++ b/main.py @@ -0,0 +1,33 @@ +"""chart.amow.com — FastAPI backend. + +Placeholder app: serves the Vue 3 single-page frontend from static/ and a +couple of JSON endpoints under /api. Replace the endpoints as the real app +takes shape; the serving/deploy wiring below does not need to change. +""" +from pathlib import Path + +from fastapi import FastAPI +from fastapi.responses import FileResponse +from fastapi.staticfiles import StaticFiles + +BASE_DIR = Path(__file__).parent +STATIC_DIR = BASE_DIR / "static" + +app = FastAPI(title="chart") + +app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") + + +@app.get("/api/health") +def health(): + return {"status": "ok", "service": "chart"} + + +@app.get("/api/hello") +def hello(): + return {"msg": "backend is live"} + + +@app.get("/") +def index(): + return FileResponse(STATIC_DIR / "index.html") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..364e2ee --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +fastapi +uvicorn[standard] diff --git a/static/app.js b/static/app.js new file mode 100644 index 0000000..29f9074 --- /dev/null +++ b/static/app.js @@ -0,0 +1,25 @@ +const { createApp, ref } = Vue; + +createApp({ + setup() { + const result = ref(''); + const error = ref(''); + const loading = ref(false); + + async function ping() { + loading.value = true; + error.value = ''; + try { + const res = await fetch('/api/hello'); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + result.value = JSON.stringify(await res.json(), null, 2); + } catch (e) { + error.value = String(e); + } finally { + loading.value = false; + } + } + + return { result, error, loading, ping }; + }, +}).mount('#app'); diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..a37380e --- /dev/null +++ b/static/index.html @@ -0,0 +1,26 @@ + + + + + + chart + + + + +
+

chart

+

FastAPI + Vue 3 — placeholder

+ +
+ +
{{ result }}
+

{{ error }}

+
+
+ + + + diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..c782436 --- /dev/null +++ b/static/style.css @@ -0,0 +1,62 @@ +:root { + color-scheme: light dark; + --bg: #ffffff; + --fg: #16181d; + --muted: #6b7280; + --line: #e4e6eb; + --accent: #2f6feb; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #14161a; + --fg: #e8eaed; + --muted: #9aa1ab; + --line: #2a2e35; + --accent: #6d9bf5; + } +} + +* { box-sizing: border-box; } + +body { + margin: 0; + padding: 3rem 1.5rem; + background: var(--bg); + color: var(--fg); + font: 16px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; +} + +#app { max-width: 40rem; margin: 0 auto; } + +h1 { margin: 0; font-size: 1.75rem; letter-spacing: -0.01em; } + +.sub { margin: 0.25rem 0 2rem; color: var(--muted); } + +.card { + border: 1px solid var(--line); + border-radius: 10px; + padding: 1.25rem; +} + +button { + font: inherit; + padding: 0.5rem 1rem; + border: 0; + border-radius: 6px; + background: var(--accent); + color: #fff; + cursor: pointer; +} + +button:disabled { opacity: 0.6; cursor: default; } + +pre { + margin: 1rem 0 0; + padding: 0.75rem; + overflow-x: auto; + border-radius: 6px; + background: color-mix(in srgb, var(--fg) 6%, transparent); +} + +.error { color: #d24b4b; }