Vela Platform v2: single monolith, real P&L, bilingual marketing site
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user