# Polleo Demand — Handoff to Server Admin

**Audience:** server administrator deploying Polleo Demand to a Linux box on
the company intranet.

**Contact for issues:** Lovro Ljutić (lljutic@polleosport.com)

---

## Quick start (10 minutes)

If you've deployed FastAPI + Postgres + nginx apps before, the entire
process is in [DEPLOY.md](DEPLOY.md). This file is a higher-level summary
with everything you need to plan the deploy.

## Architecture

```
                     ┌──────────────────────┐
   Browser ───TLS──▶ │  nginx (port 443)    │
                     │  ├ /         → React │  (static, /var/www/html/nabava.polleosport.com/frontend/dist)
                     │  └ /api/*   → FastAPI│  (proxied to 127.0.0.1:8000)
                     └──────────────────────┘
                                │
                                ▼
                     ┌──────────────────────┐
                     │  uvicorn (port 8000) │
                     │  Polleo FastAPI app  │  (managed by systemd)
                     └──────────────────────┘
                                │
                                ▼
                     ┌──────────────────────┐
                     │  PostgreSQL 16       │
                     │  database polleo_demand
                     └──────────────────────┘
```

Single host. ~6 concurrent users. Internal intranet only (no public exposure).

## Prereqs

| | Version | Notes |
|---|---|---|
| OS | Ubuntu 22.04+ / RHEL 9+ | systemd-based Linux |
| Python | **3.12 exact** | scipy wheels NOT available for 3.14 |
| PostgreSQL | 16+ | |
| nginx | 1.22+ | |
| certbot | latest | For TLS — Let's Encrypt or internal CA |
| Node.js | 20+ | Only for the frontend `npm run build` step |

## What's in this repo

| Path | What |
|---|---|
| `backend/` | FastAPI app (Python) |
| `frontend/` | React + TypeScript + Vite |
| `db/` | DB connection helpers + `init_db.py` to create schema |
| `data/` | CSV / XLSX inputs (gitignored in prod — pulled from ERP) |
| `deploy/` | **This folder — all deploy artefacts** |
| `scripts/` | Operational scripts (init_passwords, etc.) |
| `requirements.txt` | Python deps |
| `.env.example` | Env var template |

## Files in `deploy/` (this folder)

| File | Purpose |
|---|---|
| **`HANDOFF.md`** | **This file — start here** |
| **`CI_CD.md`** | **GitHub Actions CI/CD upute (nabava.polleosport.com)** |
| **`VPS_DAY1_CHECKLIST.md`** | **Printable Day 1 checklist** |
| `DEPLOY.md` | Step-by-step deploy guide (13 sections) |
| `nginx.conf.example` | nginx site config (drop-in) |
| `nabava-api.service` | systemd unit for the API (drop-in) |
| `smoke_test.sh` | Post-deploy verification script |
| `deploy.sh` | Automated deploy script (git pull + build + restart) |
| `polleo-deploy.sudoers.example` | Passwordless `systemctl restart` for CI/CD |

Also in repo root:

| File | Purpose |
|---|---|
| `.env.example` | Production env var template — **copy + edit** |

## Critical operational scripts

| Script | When to run |
|---|---|
| `python db/init_db.py` | First deploy only — creates schema |
| `python scripts/init_passwords.py` | First deploy — generates user passwords |
| `python update_sales.py` | Daily — refresh ERP data into DB |
| `python recalc_uplift_erp.py` | After `update_sales.py` finishes |

## Production checklist

Order matters. After each step verify it worked before moving on.

- [ ] **Linux host provisioned** (Ubuntu 22.04 or similar)
- [ ] **`polleo` system user created** (`useradd -m -s /bin/bash polleo`)
- [ ] **Python 3.12 installed** — `python3.12 --version` returns 3.12.x
- [ ] **PostgreSQL 16 installed** + database `polleo_demand` + user `polleo` with strong password
- [ ] **Clone repo to `/var/www/html/nabava.polleosport.com`** owned by polleo user
- [ ] **Python venv at `/var/www/html/nabava.polleosport.com/.venv`** with `pip install -r requirements.txt`
- [ ] **Schema initialised:** `python db/init_db.py`
- [ ] **Initial data loaded:** `python update_sales.py` (pulls ERP CSVs from `data/`)
- [ ] **`.env` filled in** — copy from `.env.example`, set:
    - `APP_ENV=production`
    - `DATABASE_URL=postgresql://polleo:STRONG_PASSWORD@localhost:5432/polleo_demand`
    - `JWT_SECRET=` (generate with `python -c "import secrets; print(secrets.token_urlsafe(64))"`)
    - `CORS_ORIGINS=https://nabava.polleosport.com` (real frontend URL)
- [ ] **`chmod 600 .env`**
- [ ] **User passwords bootstrapped:** `python scripts/init_passwords.py`
    - **Save the printed passwords** — they're bcrypt-hashed in DB, not recoverable
    - Distribute via secure channel (Slack DM, password manager, in-person)
- [ ] **Frontend built:** `cd frontend && npm ci && npm run build`
    - Output goes to `frontend/dist/`
- [ ] **systemd unit installed:**
    - `sudo cp deploy/nabava-api.service /etc/systemd/system/`
    - `sudo systemctl daemon-reload && sudo systemctl enable --now nabava-api`
- [ ] **nginx config installed:**
    - `sudo cp deploy/nginx.conf.example /etc/nginx/sites-available/polleo-demand`
    - Edit hostnames / cert paths to match your env
    - `sudo ln -s ... /etc/nginx/sites-enabled/ && sudo nginx -t && sudo systemctl reload nginx`
- [ ] **TLS cert issued:** `sudo certbot --nginx -d demand.polleo.intra`
- [ ] **Smoke test passed:** `bash deploy/smoke_test.sh https://nabava.polleosport.com`
- [ ] **Daily backup cron set:** `pg_dump | gzip` to `/var/backups/polleo/`
- [ ] **Logrotate config** (optional — journalctl handles this by default)

## What the app does at a glance

- **6 user roles:** Admin / Veleprodaja / Maloprodaja / Nabava / Marketing / Uprava
- **Permission matrix:** `backend/services/role_permissions.yaml` (single source of truth)
- **JWT auth:** 7-day expiry, sessionStorage in browser
- **Login rate-limit:** 5 fails / 60s per IP → 60s lockout
- **Health check:** `GET /api/health` (no auth, returns DB connection status)

## Initial user list (handed off separately)

The bootstrap script generates passwords on first run. Sample table:

```
USERNAME       ROLE          DISPLAY NAME
─────────────────────────────────────────
lovro          Admin         Lovro Ljutić            password set: Lovro2575
mgelencir      Admin         Marko Gelenčir          via init_passwords.py
monika         Admin         Monika                  via init_passwords.py
Selma          Veleprodaja   Selma                   via init_passwords.py
Patrik_VP      Veleprodaja   Patrik                  via init_passwords.py
Ivan           Maloprodaja   Ivan                    via init_passwords.py
Patrik_MP      Maloprodaja   Patrik                  via init_passwords.py
```

To rotate one user's password later:
```bash
python scripts/init_passwords.py --user USERNAME
```

To reset everyone (security incident):
```bash
python scripts/init_passwords.py --all
```

## Common operations cheat-sheet

| Task | Command |
|---|---|
| Restart API | `sudo systemctl restart nabava-api` |
| Tail API logs | `journalctl -u nabava-api -f` |
| Reload nginx | `sudo systemctl reload nginx` |
| DB shell | `sudo -iu postgres psql polleo_demand` |
| Refresh sales data | `cd /var/www/html/nabava.polleosport.com && source .venv/bin/activate && python update_sales.py` |
| Health check | `curl https://nabava.polleosport.com/api/health` |
| Smoke test | `bash deploy/smoke_test.sh https://nabava.polleosport.com` |

## Updating the app (git pull deploy)

Manual:

```bash
sudo -iu polleo
bash /var/www/html/nabava.polleosport.com/deploy/deploy.sh
```

Automated (GitHub Actions on push to `master`):

See **`deploy/CI_CD.md`** for full steps. Summary:

1. Copy `deploy/polleo-deploy.sudoers.example` → `/etc/sudoers.d/polleo-deploy`
2. Add GitHub Actions secrets: `VPS_HOST`, `VPS_USER` (`polleo`), `VPS_SSH_KEY`
3. Optional post-deploy smoke test: `SMOKE_URL=https://nabava.polleosport.com`, `SMOKE_USER`, `SMOKE_PW`
4. Push to `master` → workflow `.github/workflows/deploy.yml` runs tests then SSH deploy

## Known things that aren't done (and why)

| Item | Why skipped | Workaround |
|---|---|---|
| Automated tests in CI | Out of scope for v1 | Manual regression + smoke_test.sh after deploys |
| Staging env | Single-server internal app | Test locally before pushing to prod |
| 2FA | Internal-only, intranet | If exposed externally later, add |
| Centralised logging | journald is enough | `journalctl -u nabava-api` |
| Metrics dashboard | Low-volume app | Add Netdata if needed |
| Audit log of logins | Not yet wired | Table exists (`audit_log`), can be added |
| Multi-host / HA | Not needed for ~6 users | Single-instance simplicity wins |

## Performance expectations

- Login: < 200ms
- Most analytical endpoints: 200-800ms (DB queries against ~50k SKUs)
- Forecast endpoints: 5-30 seconds (heavy compute)
- Frontend bundle: ~302 KB gzipped
- Concurrent users: tested with 6, comfortable up to 20

## Escalation path

| Issue | Owner |
|---|---|
| App crashes / API errors | Lovro (lljutic@polleosport.com) — code-side |
| Linux / OS / nginx | You (server admin) |
| Database backups / restores | You + Lovro |
| Network / firewall | You + IT |

## Security model — explicit assumptions

- App is **internal-only** — exposed only on the company intranet / VPN.
- No public internet exposure. If that changes, add WAF + DDoS protection.
- All users have a real bcrypt password (no dev-mode bypass — the legacy
  "any password works" backdoor has been removed).
- Login rate-limited (5 fails/60s → 60s lockout per IP).
- JWT tokens expire after 7 days; no server-side revoke (stateless).
- HTTPS enforced by nginx (HSTS header set, HTTP → 301 redirect).
- Admin role required for `/api/admin/*` endpoints (user management).
- Token stored in browser sessionStorage — clears when tab closes.

## Sign-off

Once you've completed the production checklist above and `smoke_test.sh`
passes, the app is ready for users. Hand the printed passwords back to
Lovro who will distribute them.
