Self-hosted P&L tracking app with component-level pricing. Offers: Atlas/Atlas+/Rif/Rif+ with granular cost breakdown. API + MCP + multi-user auth.
4.2 KiB
Vela Platform 🏔️
A lightweight self-hosted P&L tracking app for a C2C server/storage business.
Stack
- Python 3.11+ with FastAPI + uvicorn
- SQLite via SQLAlchemy (single file, zero setup)
- Jinja2 templates + Bootstrap 5 dark frontend
- Chart.js for trend charts
- JWT auth (cookie + Bearer header)
- stdio MCP server for Hermes integration
Data Model
User
- id, username, password_hash, display_name, role (admin|viewer)
Service (Offer)
- id, name (Atlas/Atlas+/Rif/Rif+), description
- sell_price (computed from components), cost_price (computed from components)
- active
ServiceComponent
- id, service_id (FK), name (RAM/HDD/SSD/etc.), unit_cost, unit_sell, quantity, notes
recompute_prices()method sums cost/sell from components
Transaction
- id, service_id (FK), quantity, revenue (calculated: qty * service.sell_price), cost (calculated: qty * service.cost_price), month (YYYY-MM), notes, created_by (FK user), created_at
Monthly P&L (computed)
- month, total_revenue, total_cost, gross_profit, gross_margin%, opex (800 MAD), net_profit, net_margin%
Preseed Data
- Users: admin (password: admin123), viewer (password: viewer123)
- Services with components (see COMPONENT_TEMPLATES in main.py)
- Opex: 800 MAD/month hardcoded
Key Files
| Path | What |
|---|---|
backend/main.py |
FastAPI app (API + frontend routes) |
backend/models.py |
SQLAlchemy models |
backend/auth.py |
JWT auth, cookie support |
backend/database.py |
SQLite setup |
backend/templates/ |
Jinja2 templates |
mcp/server.py |
MCP stdio server |
CLAUDE.md |
This file |
API Endpoints
Auth
- POST /api/auth/login -> {token, user} + set cookie
- GET /api/auth/me -> current user
Services
- GET /api/services -> list with components
- GET /api/services/{id} -> single with components
- POST /api/services -> create (admin)
- PUT /api/services/{id} -> update (admin)
- DELETE /api/services/{id} -> deactivate (admin)
Components
- GET /api/services/{id}/components -> list
- POST /api/services/{id}/components -> create (admin, JSON body)
- PUT /api/components/{id} -> update (admin, JSON body) — auto-recomputes prices
- DELETE /api/components/{id} -> delete (admin) — auto-recomputes prices
Transactions
- GET /api/transactions?month=YYYY-MM -> list
- POST /api/transactions -> create {service_id, quantity, month, notes}
- DELETE /api/transactions/{id} -> (admin)
P&L
- GET /api/pnl?month=YYYY-MM -> computed P&L
- GET /api/dashboard -> current + 12-month trend
MCP Server (stdio)
Located at mcp/server.py. Run as a stdio MCP server via python3 mcp/server.py.
Tools: get_pnl, get_services, get_service, add_component, update_component, delete_component, add_transaction, get_dashboard Resources: vela://pnl/current, vela://services, vela://services/{id}
URLs
- Web app: http://192.168.1.30:8788
- MCP: registered as
vela-platformin Hermes config
Component Pricing Templates
Each offer has its own set of components. The total sell/cost is auto-computed:
Atlas (1TB Storage)
HDD(1TB) 600/1000, RAM(16GB) 300/500, CPU alloc 200/400, Casing 200/300, Bandwidth 200/500, Setup 300/600, Support 200/700 → Total: cost 2000, sell 4000
Atlas+ (1TB + Backup)
HDD(1TB) 600/1000, SSD(256GB) 400/700, RAM(32GB) 600/1000, CPU alloc 300/600, Casing 200/300, Bandwidth 200/500, Sauvegarde 300/500, Setup 200/400, Support 200/700 → Total: cost 2800, sell 5000
Rif (500GB Server)
SSD(500GB) 300/600, RAM(16GB) 300/500, CPU alloc 200/400, Casing 200/300, Bandwidth 200/500, Setup 300/600, Support 200/700 → Total: cost 1700, sell 3500
Rif+ (500GB + Backup)
SSD(500GB) 300/600, HDD(1TB) 600/1000, RAM(32GB) 600/1000, CPU alloc 300/600, Casing 200/300, Bandwidth 200/500, Sauvegarde 300/500, Setup 200/400, Support 200/700 → Total: cost 2200, sell 4500
Deployment
cd ~/bestof-manager/backend && uvicorn main:app --host 0.0.0.0 --port 8788
# Or via systemd:
systemctl --user restart bestof-manager.service
Auth Flow
- Page routes use cookie auth (httpOnly 'token' cookie set on login)
- API routes use Bearer token from Authorization header
- Login sets both cookie AND returns JSON token
- Token expires after 24h
- Pages without valid cookie redirect to /login