Files

191 lines
6.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.