Vela Platform v2: single monolith, real P&L, bilingual marketing site

This commit is contained in:
2026-06-26 19:51:06 +01:00
commit 918c48ffca
12 changed files with 2093 additions and 0 deletions
+190
View File
@@ -0,0 +1,190 @@
# 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.