# Vela Platform v2 Single FastAPI application that powers the Vela business — a home server installation service in Casablanca, Morocco. **Built:** June 26, 2026 **Deployed:** Proxmox CT 124 @ 192.168.1.244 --- ## Architecture ``` ┌─────────────────────────────────────────┐ │ nginx :80 │ │ ├── / → Marketing site │ │ ├── /admin → Admin P&L dashboard │ │ ├── /backup → Backup explainer │ │ ├── /static/* → Logos, assets │ │ └── /api/* → JSON API │ │ │ │ │ uvicorn :8000 (systemd: vela.service) │ │ ├── main.py — FastAPI app │ │ ├── models.py — SQLAlchemy models │ │ ├── database.py — SQLite WAL mode │ │ └── auth.py — JWT auth │ │ │ │ vela.db (SQLite) │ └─────────────────────────────────────────┘ ``` One box. One app. One database. No microservices. --- ## What It Does ### Public Marketing Site (`/`) - Bilingual FR/EN with URL-based language toggle (`?lang=fr` / `?lang=en`) - Dark/light mode toggle (persisted in localStorage) - Hero section with value proposition - Problem/solution section - Live pricing cards pulled from API - Immich & Nextcloud sections with real SVG logos - "How it works" — 3-step visual - WhatsApp CTA buttons (all pointing to +212 725-569519) - Footer with barely-visible "Admin" link (30% opacity) ### Backup Explainer (`/backup`) - Explains backups to non-technical users - Simple vs redundant backup comparison - "The golden rule: if it only exists in one place, it doesn't really exist" - Bilingual ### Admin Panel (`/admin`) - JWT login (username: `oimwiodev`, password: `kxdr781020`) - Token stored in localStorage, verified via `/api/verify` - 4 tabs: Dashboard, Tiers, Transactions, Expenses #### Dashboard - Real-time P&L cards: Revenue, COGS, Gross Profit, Margin %, Expenses, Net Profit - Monthly breakdown table with month filter - Formula: **Revenue − COGS − Expenses = Net Profit** (not the fake v1 math) #### Tiers - 4 tiers: Rif, Rif+, Atlas, Atlas+ - Component breakdown with unit costs - Inline edit: tier name, sell price, component costs - Toggle active/inactive - Cost price auto-calculated from components - Margin % shown per tier #### Transactions - Add manual sale (select tier → snapshots sell price + cost at time of sale) - Filter by month - Delete with confirm + undo timer: - 1st click: "Sure?" (4s timeout) - 2nd click: deletes, row fades, 8s undo window #### Expenses - Add/delete expenses with month and category - Monthly expense templates (auto-generate from templates) - Soft delete (deleted_at timestamp) --- ## API Endpoints ### Public (no auth) | Method | Path | Description | |--------|------|-------------| | GET | `/api/public/pricing` | Active tiers: name, description, sell_price | | GET | `/api/public/services/{id}` | Single tier details + components | ### Auth | Method | Path | Description | |--------|------|-------------| | POST | `/api/auth/login` | Login (username + password) → JWT | | POST | `/api/auth/logout` | Clear cookie | | GET | `/api/verify` | Verify JWT, returns email | ### Admin (JWT required) | Method | Path | Description | |--------|------|-------------| | GET | `/api/pnl` | P&L summary + monthly breakdown | | GET | `/api/services` | All tiers with components | | PUT | `/api/services/{id}` | Update tier (name, price, active, etc.) | | PUT | `/api/services/{id}/toggle` | Toggle active/inactive | | PUT | `/api/services/{id}/components/{cid}` | Update component cost/qty | | GET | `/api/transactions` | List transactions (optional `?month=`) | | POST | `/api/transactions` | Create sale (snapshots prices) | | DELETE | `/api/transactions/{id}` | Delete transaction | | GET | `/api/expenses` | List expenses | | POST | `/api/expenses` | Add expense | | DELETE | `/api/expenses/{id}` | Soft-delete expense | | GET | `/api/expense-templates` | List recurring templates | | POST | `/api/expenses/generate-month` | Generate from templates | --- ## Database Schema 6 tables in SQLite (`vela.db`): | Table | Purpose | |-------|---------| | `users` | Single admin (email, bcrypt password) | | `services` | 4 tiers (name, sell_price, cost_price, active, sort_order) | | `service_components` | Per-tier parts (name, unit_cost, quantity) | | `transactions` | Frozen sales with price/cost snapshots, unique (source_type, source_id) | | `expenses` | Monthly costs with soft delete | | `expense_templates` | Recurring expense patterns | Key design: **transactions freeze sell_price and cost_price at time of sale.** If you change an SSD cost later, old sales don't change. Real P&L math. SQLite pragmas: `journal_mode=WAL`, `foreign_keys=ON`, `busy_timeout=5000` --- ## Deployment **Server:** Proxmox CT 124 (Debian 12) **IP:** 192.168.1.244 **Service:** `systemctl restart vela` **Files:** `/opt/vela/` **DB:** `/opt/vela/vela.db` ### Update workflow ```bash # 1. Edit files locally # 2. Package and deploy cd ~/src/vela-platform tar czf /tmp/vela-update.tar.gz main.py templates/ sshpass -p 'PW' scp /tmp/vela-update.tar.gz root@192.168.1.109:/tmp/ sshpass -p 'PW' ssh root@192.168.1.109 " pct push 124 /tmp/vela-update.tar.gz /tmp/vela-update.tar.gz && pct exec 124 -- bash -c 'cd /opt/vela && tar xzf /tmp/vela-update.tar.gz && systemctl restart vela' " ``` ### Public access Point Nginx Proxy Manager at `192.168.1.244:80` for your domain. --- ## Design Decisions - **No Jinja2** — hit a cache bug (`unhashable type: 'dict'`). Switched to `FileResponse` serving raw HTML. All dynamic content via `fetch()` to JSON APIs. - **No framework** — vanilla HTML/CSS/JS. No React, no Vue, no build step. - **CSS variables** — dark/light mode is a single `data-theme` attribute. Zero hardcoded hex colors in JS. - **Single admin** — no user management. One account. Simple. - **Public API is read-only** — pricing only. No costs, no margins, no internals leak. --- ## WhatsApp Sales Bot A separate Hermes agent with a custom SOUL.md handles WhatsApp conversations. The bot: - Answers questions in French/Darija/English - Fetches live pricing via `curl http://192.168.1.244/api/public/pricing` - Follows a 4-step sales process: understand → recommend → handle objections → close - Never hardcodes prices — always checks the API - Personality: calm, direct, short messages, no corporate tone SOUL.md at `/tmp/vela-sales-bot-soul.md` --- ## What Was Removed (v1) The old Vela had **two separate LXCs** (CT125 + CT126) with **three Python services** (two FastAPI apps + nginx, two auth systems, two databases). All nuked. Replaced with this single monolith.