diff --git a/README.md b/README.md new file mode 100644 index 0000000..8045fe5 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Apple Form + +Apple-style 3-field step-by-step customer form with FastAPI backend. + +## Frontend + +Single-file HTML (`index.html`) — zero dependencies, native system fonts. + +- 3-step wizard: Email → Name → Domain +- Apple design system: `#f5f5f7` backgrounds, Apple Blue `#0071e3` accent, SF Pro typography +- Slide animations between steps, step-dot progress indicator +- SVG checkmark draw animation on success +- POSTs to the backend at `http://192.168.1.121:8080/api/submit` + +### Open directly + +```bash +firefox index.html +``` + +Or serve from an HTTP server: + +```bash +python3 -m http.server 8000 +``` + +## Backend + +FastAPI server in a Proxmox LXC CT: + +| Detail | Value | +|---|---| +| CT ID | 121 | +| Hostname | form-login | +| IP | 192.168.1.121 | +| Port | 8080 | +| Database | SQLite (`/srv/form-backend/submissions.db`) | +| Service | `form-backend.service` (enabled, auto-start) | + +### Endpoints + +| Method | Path | Description | +|---|---|---| +| POST | `/api/submit` | Submit form data `{email, name, domain}` → `{ok, id}` | +| GET | `/api/submissions` | List all submissions (JSON) | +| GET | `/health` | Health check | + +### Test + +```bash +curl http://192.168.1.121:8080/health +curl -X POST http://192.168.1.121:8080/api/submit \ + -H 'Content-Type: application/json' \ + -d '{"email":"jane@example.com","name":"Jane","domain":"example.io"}' +``` + +### Server code + +`server.py` lives at `/srv/form-backend/server.py` on the CT. + +```bash +ssh root@192.168.1.109 'pct exec 121 -- systemctl status form-backend' +``` diff --git a/server.py b/server.py new file mode 100644 index 0000000..08d9cdf --- /dev/null +++ b/server.py @@ -0,0 +1,75 @@ +import sqlite3 +import os +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel + +DB_PATH = os.environ.get("DB_PATH", "/srv/form-backend/submissions.db") + +def get_db(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA journal_mode=WAL") + return conn + +@asynccontextmanager +async def lifespan(app: FastAPI): + db = get_db() + db.execute(""" + CREATE TABLE IF NOT EXISTS submissions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL, + name TEXT NOT NULL, + domain TEXT NOT NULL, + created_at TEXT NOT NULL + ) + """) + db.commit() + db.close() + yield + +app = FastAPI(title="Form Backend", lifespan=lifespan) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + +class Submission(BaseModel): + email: str + name: str + domain: str + +@app.post("/api/submit", status_code=201) +def submit(data: Submission): + if not data.email or "@" not in data.email: + return {"error": "Invalid email"}, 422 + if not data.name.strip(): + return {"error": "Name required"}, 422 + if not data.domain.strip(): + return {"error": "Domain required"}, 422 + + db = get_db() + cur = db.execute( + "INSERT INTO submissions (email, name, domain, created_at) VALUES (?, ?, ?, datetime(\"now\"))", + (data.email.strip(), data.name.strip(), data.domain.strip()) + ) + db.commit() + row_id = cur.lastrowid + db.close() + return {"ok": True, "id": row_id} + +@app.get("/api/submissions") +def list_submissions(): + db = get_db() + rows = db.execute("SELECT id, email, name, domain, created_at FROM submissions ORDER BY id DESC LIMIT 100").fetchall() + db.close() + return [dict(r) for r in rows] + +@app.get("/health") +def health(): + return {"status": "ok"}