From 13f23316a453236af3cb701f15904636c0dfce6c Mon Sep 17 00:00:00 2001 From: Christian Date: Fri, 5 Dec 2025 14:22:39 +0100 Subject: [PATCH] Initial BMC Hub setup --- .dockerignore | 29 ++++ .env.example | 47 ++++++ .env.prod.example | 59 ++++++++ .github/copilot-instructions.md | 206 +++++++++++++++++++++++++ .gitignore | 30 ++++ DEVELOPMENT.md | 259 ++++++++++++++++++++++++++++++++ Dockerfile | 45 ++++++ README.md | 146 ++++++++++++++++++ app/__init__.py | 1 + app/core/__init__.py | 1 + app/core/config.py | 41 +++++ app/core/database.py | 73 +++++++++ app/models/__init__.py | 1 + app/models/schemas.py | 51 +++++++ app/routers/__init__.py | 1 + app/routers/billing.py | 20 +++ app/routers/customers.py | 47 ++++++ app/routers/hardware.py | 47 ++++++ app/routers/system.py | 46 ++++++ data/.gitkeep | 1 + docker-compose.prod.yml | 72 +++++++++ docker-compose.yml | 67 +++++++++ logs/.gitkeep | 1 + main.py | 122 +++++++++++++++ migrations/init.sql | 35 +++++ requirements.txt | 7 + static/index.html | 70 +++++++++ uploads/.gitkeep | 1 + 28 files changed, 1526 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .env.prod.example create mode 100644 .github/copilot-instructions.md create mode 100644 .gitignore create mode 100644 DEVELOPMENT.md create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/core/__init__.py create mode 100644 app/core/config.py create mode 100644 app/core/database.py create mode 100644 app/models/__init__.py create mode 100644 app/models/schemas.py create mode 100644 app/routers/__init__.py create mode 100644 app/routers/billing.py create mode 100644 app/routers/customers.py create mode 100644 app/routers/hardware.py create mode 100644 app/routers/system.py create mode 100644 data/.gitkeep create mode 100644 docker-compose.prod.yml create mode 100644 docker-compose.yml create mode 100644 logs/.gitkeep create mode 100644 main.py create mode 100644 migrations/init.sql create mode 100644 requirements.txt create mode 100644 static/index.html create mode 100644 uploads/.gitkeep diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5340dd1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,29 @@ +__pycache__/ +*.py[cod] +*$py.class +*.so +.git/ +.gitignore +.env +.env.local +venv/ +.venv/ +ENV/ +env/ +logs/*.log +uploads/* +!uploads/.gitkeep +data/* +!data/.gitkeep +.DS_Store +.vscode/ +.idea/ +*.db +*.db-journal +htmlcov/ +.coverage +.pytest_cache/ +README.md +docker-compose.yml +docker-compose.prod.yml +.dockerignore diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..efc1246 --- /dev/null +++ b/.env.example @@ -0,0 +1,47 @@ +# ===================================================== +# POSTGRESQL DATABASE - Local Development +# ===================================================== +DATABASE_URL=postgresql://bmc_hub:bmc_hub@postgres:5432/bmc_hub + +# Database credentials (bruges af docker-compose) +POSTGRES_USER=bmc_hub +POSTGRES_PASSWORD=bmc_hub +POSTGRES_DB=bmc_hub +POSTGRES_PORT=5432 + +# ===================================================== +# API CONFIGURATION +# ===================================================== +API_HOST=0.0.0.0 +API_PORT=8000 +API_RELOAD=true + +# ===================================================== +# SECURITY +# ===================================================== +SECRET_KEY=change-this-in-production-use-random-string +CORS_ORIGINS=http://localhost:8000,http://localhost:3000 + +# ===================================================== +# LOGGING +# ===================================================== +LOG_LEVEL=INFO +LOG_FILE=logs/app.log + +# ===================================================== +# GITHUB/GITEA REPOSITORY (Optional - for reference) +# ===================================================== +# Repository: https://g.bmcnetworks.dk/ct/bmc_hub +GITHUB_REPO=ct/bmc_hub + +# ===================================================== +# e-conomic Integration (Optional) +# ===================================================== +# Get credentials from e-conomic Settings -> Integrations -> API +ECONOMIC_API_URL=https://restapi.e-conomic.com +ECONOMIC_APP_SECRET_TOKEN=your_app_secret_token_here +ECONOMIC_AGREEMENT_GRANT_TOKEN=your_agreement_grant_token_here + +# 🚨 SAFETY SWITCHES - Beskytter mod utilsigtede Γ¦ndringer +ECONOMIC_READ_ONLY=true # Set to false ONLY after testing +ECONOMIC_DRY_RUN=true # Set to false ONLY when ready for production writes diff --git a/.env.prod.example b/.env.prod.example new file mode 100644 index 0000000..2634303 --- /dev/null +++ b/.env.prod.example @@ -0,0 +1,59 @@ +# ===================================================== +# POSTGRESQL DATABASE - Production +# ===================================================== +DATABASE_URL=postgresql://bmc_hub:CHANGEME_STRONG_PASSWORD@postgres:5432/bmc_hub + +# Database credentials (bruges af docker-compose) +POSTGRES_USER=bmc_hub +POSTGRES_PASSWORD=CHANGEME_STRONG_PASSWORD +POSTGRES_DB=bmc_hub +POSTGRES_PORT=5432 + +# ===================================================== +# GITHUB DEPLOYMENT - Production Version Control +# ===================================================== +# Git tag eller branch at deploye (f.eks. "v1.0.0", "v1.2.3") +# VIGTIGT: Brug ALTID tags til production (ikke "latest" eller "main") +RELEASE_VERSION=v1.0.0 + +# GitHub repository (format: owner/repo eller path pΓ₯ Gitea) +GITHUB_REPO=ct/bmc_hub + +# GitHub/Gitea Personal Access Token (skal have lΓ¦seadgang til repo) +# Opret token pΓ₯: https://g.bmcnetworks.dk/user/settings/applications +GITHUB_TOKEN=your_gitea_token_here + +# ===================================================== +# API CONFIGURATION - Production +# ===================================================== +API_HOST=0.0.0.0 +API_PORT=8000 +API_RELOAD=false + +# ===================================================== +# SECURITY - Production +# ===================================================== +# VIGTIGT: Generer en stΓ¦rk SECRET_KEY i production! +# Brug: python -c "import secrets; print(secrets.token_urlsafe(32))" +SECRET_KEY=CHANGEME_GENERATE_RANDOM_SECRET_KEY + +# CORS origins - tilfΓΈj din domain +CORS_ORIGINS=https://hub.bmcnetworks.dk,https://api.bmcnetworks.dk + +# ===================================================== +# LOGGING - Production +# ===================================================== +LOG_LEVEL=INFO +LOG_FILE=logs/app.log + +# ===================================================== +# e-conomic Integration - Production +# ===================================================== +ECONOMIC_API_URL=https://restapi.e-conomic.com +ECONOMIC_APP_SECRET_TOKEN=your_production_token_here +ECONOMIC_AGREEMENT_GRANT_TOKEN=your_production_grant_here + +# 🚨 SAFETY SWITCHES +# Start ALTID med begge sat til true i ny production deployment! +ECONOMIC_READ_ONLY=true # Set to false after thorough testing +ECONOMIC_DRY_RUN=true # Set to false when ready for live writes diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..0461fe5 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,206 @@ +# GitHub Copilot Instructions - BMC Hub + +## Project Overview + +BMC Hub is a central management system for BMC Networks built with **FastAPI + PostgreSQL**. The architecture is inspired by the OmniSync project (`/Users/christianthomas/pakkemodtagelse`) but adapted for BMC's specific needs. + +**Tech Stack**: Python 3.13, FastAPI, PostgreSQL 16, Docker, psycopg2 + +## Architecture Principles + +### Database-First Design +- **PostgreSQL only** - no SQLite. Use `psycopg2` with connection pooling +- All DB operations via `app/core/database.py::execute_query()` helper +- Parameterized queries (`%s` placeholders) to prevent SQL injection +- Use `RealDictCursor` for dict-like row access +- Schema defined in `migrations/init.sql` with versioned migrations + +### FastAPI Router Pattern +- Each domain gets a router in `app/routers/` (customers, hardware, billing, system) +- Routers imported and registered in `main.py` with prefix `/api/v1` +- Use Pydantic models from `app/models/schemas.py` for request/response validation +- Return model instances directly - FastAPI handles serialization + +### Configuration Management +- All settings in `app/core/config.py` using `pydantic_settings.BaseSettings` +- Loads from `.env` file automatically via `Config.env_file = ".env"` +- Access via `from app.core.config import settings` +- **Never hardcode credentials** - always use environment variables + +### Safety-First Integrations +- External API integrations (e-conomic) have **safety switches**: `ECONOMIC_READ_ONLY` and `ECONOMIC_DRY_RUN` +- Always default to `true` for both switches in new deployments +- Log all external API calls before execution +- Provide dry-run mode that logs without executing + +## Development Workflows + +### Local Development Setup +```bash +cp .env.example .env # Create local environment +docker-compose up -d # Start PostgreSQL + API +docker-compose logs -f api # Watch logs +``` + +The local `docker-compose.yml` mounts source code for live reload: +- `./app:/app/app:ro` - Python code hot-reload +- `ENABLE_RELOAD=true` - Uvicorn auto-restart + +### Production Deployment +Production uses **version-tagged GitHub releases** via Gitea: + +1. Tag release: `git tag v1.2.3 && git push --tags` +2. Update `.env` with `RELEASE_VERSION=v1.2.3` +3. Deploy: `docker-compose -f docker-compose.prod.yml up -d --build` + +The production Dockerfile downloads code from Gitea if `RELEASE_VERSION != "latest"`: +```dockerfile +ARG RELEASE_VERSION=latest +ARG GITHUB_TOKEN +ARG GITHUB_REPO=ct/bmc_hub +``` + +**Key difference from local**: Production does NOT mount source code - it's baked into the image from the Git tag. + +### Database Migrations +- SQL migrations in `migrations/` numbered sequentially +- `init.sql` runs on first container startup via Docker entrypoint +- For schema changes: create `migrations/001_feature_name.sql` and run manually or via migration script + +### Adding New Features +1. **Create Pydantic models** in `app/models/schemas.py` (Base, Create, Full schemas) +2. **Add database migration** in `migrations/XXX_feature.sql` +3. **Create router** in `app/routers/feature.py` with CRUD endpoints +4. **Register router** in `main.py`: `app.include_router(feature.router, prefix="/api/v1", tags=["Feature"])` +5. **Add tests** (when test framework exists) + +## Code Patterns + +### Database Query Pattern +```python +from app.core.database import execute_query + +# Fetch rows +query = "SELECT * FROM customers WHERE id = %s" +result = execute_query(query, (customer_id,)) + +# Insert/Update (returns affected rows) +query = "INSERT INTO customers (name) VALUES (%s) RETURNING *" +result = execute_query(query, (name,)) +``` + +### Router Endpoint Pattern +```python +from fastapi import APIRouter, HTTPException +from app.models.schemas import Customer, CustomerCreate + +router = APIRouter() + +@router.get("/customers/{id}", response_model=Customer) +async def get_customer(id: int): + result = execute_query("SELECT * FROM customers WHERE id = %s", (id,)) + if not result: + raise HTTPException(status_code=404, detail="Not found") + return result[0] +``` + +### Configuration Access Pattern +```python +from app.core.config import settings + +# Check feature flags +if settings.ECONOMIC_READ_ONLY: + logger.warning("Read-only mode enabled") + return {"message": "Dry run only"} +``` + +## Project-Specific Conventions + +### Logging +- Use Python's `logging` module, not `print()` +- Format: `logger.info("Message with %s", variable)` +- Emoji prefixes for visibility: `πŸš€` startup, `βœ…` success, `❌` error, `⚠️` warning + +### Error Handling +- Use `HTTPException` for API errors with appropriate status codes +- Log exceptions with `logger.error()` before raising +- Return user-friendly error messages, log technical details + +### API Response Format +- Use Pydantic response models - let FastAPI handle serialization +- For lists: `response_model=List[ModelName]` +- For health checks: return dict with `status`, `service`, `version` keys + +### File Organization +- **One router per domain** - don't create mega-files +- **Services in `app/services/`** for business logic (e.g., `economic.py` for API integration) +- **Jobs in `app/jobs/`** for scheduled tasks +- **Keep routers thin** - delegate complex logic to services + +## Docker & Deployment + +### Environment Files +- **`.env.example`** - Local development template (weak passwords OK) +- **`.env.prod.example`** - Production template with placeholders and security notes +- **Never commit `.env`** - it's in `.gitignore` + +### Docker Compose Files +- **`docker-compose.yml`** - Local dev with code mounting and auto-reload +- **`docker-compose.prod.yml`** - Production with version tags, no code mounts, always restart + +### Gitea Integration +The production deployment pulls code from the Gitea server at `g.bmcnetworks.dk`: +- Requires `GITHUB_TOKEN` environment variable (Gitea personal access token) +- Repo format: `ct/bmc_hub` (owner/repo) +- Release tags: `v1.0.0`, `v1.2.3` etc. (semantic versioning) + +## Common Pitfalls to Avoid + +1. **Don't use SQLite** - this is a PostgreSQL-only project +2. **Don't use ORMs** - use raw SQL via `execute_query()` for simplicity +3. **Don't hardcode database URLs** - always use `settings.DATABASE_URL` +4. **Don't skip parameterization** - SQL injection risk with string formatting +5. **Don't mount code in production** - use version tags and Docker builds +6. **Don't disable safety switches in .env.example** - always default to safe mode + +## Integration with OmniSync + +This project shares architectural patterns with `/Users/christianthomas/pakkemodtagelse`: +- Similar FastAPI structure with routers, services, jobs +- PostgreSQL with psycopg2 (no ORM) +- Docker Compose for both dev and production +- Environment-based configuration +- Migration-based schema management + +**Key differences**: +- BMC Hub uses **version-tagged deployments** from Gitea +- Simpler domain model (customers, hardware, billing vs. full invoice management) +- No Ollama/LLM integration (not needed for this use case) + +## Quick Reference + +```python +# Import patterns +from app.core.config import settings +from app.core.database import execute_query +from app.models.schemas import Customer, CustomerCreate +from fastapi import APIRouter, HTTPException + +# Database query +result = execute_query("SELECT * FROM table WHERE id = %s", (id,)) + +# Configuration +settings.DATABASE_URL +settings.ECONOMIC_READ_ONLY + +# Router registration (main.py) +app.include_router(router, prefix="/api/v1", tags=["Feature"]) +``` + +## Health Check Endpoint +Always maintain `/health` and `/api/v1/system/health` endpoints: +- Test database connectivity +- Report configuration state +- Return service name and version + +This ensures monitoring systems (Uptime Kuma) can track service health. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4b54c7c --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +.venv/ +ENV/ +env/ +*.egg-info/ +dist/ +build/ +.env +.env.local +logs/*.log +uploads/* +!uploads/.gitkeep +data/* +!data/.gitkeep +.DS_Store +.vscode/ +.idea/ +*.db +*.db-journal +*.sqlite +*.sqlite3 +htmlcov/ +.coverage +.pytest_cache/ +.mypy_cache/ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..7f060f8 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,259 @@ +# BMC Hub - Development Guide + +## πŸš€ Quick Start + +### 1. Clone & Setup +```bash +cd /Users/christianthomas/DEV/bmc_hub_dev +cp .env.example .env +``` + +### 2. Start Development Server +```bash +docker-compose up -d +docker-compose logs -f +``` + +### 3. Verify Installation +```bash +# Health check +curl http://localhost:8000/health + +# API docs +open http://localhost:8000/api/docs + +# Dashboard +open http://localhost:8000 +``` + +## πŸ“ Project Structure + +``` +bmc_hub/ +β”œβ”€β”€ app/ +β”‚ β”œβ”€β”€ core/ # Config & database +β”‚ β”œβ”€β”€ models/ # Pydantic schemas +β”‚ β”œβ”€β”€ routers/ # API endpoints +β”‚ β”œβ”€β”€ services/ # Business logic +β”‚ └── jobs/ # Scheduled tasks +β”œβ”€β”€ migrations/ # Database migrations +β”œβ”€β”€ static/ # Web UI +β”œβ”€β”€ .env.example # Local dev template +└── .env.prod.example # Production template +``` + +## πŸ”§ Development Workflow + +### Adding a New Feature + +1. **Database Migration** +```sql +-- migrations/002_add_services.sql +CREATE TABLE services ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + customer_id INTEGER REFERENCES customers(id) +); +``` + +2. **Pydantic Models** +```python +# app/models/schemas.py +class ServiceBase(BaseModel): + name: str + customer_id: int + +class Service(ServiceBase): + id: int +``` + +3. **Router** +```python +# app/routers/services.py +from fastapi import APIRouter +router = APIRouter() + +@router.get("/services") +async def list_services(): + return execute_query("SELECT * FROM services") +``` + +4. **Register Router** +```python +# main.py +from app.routers import services +app.include_router(services.router, prefix="/api/v1", tags=["Services"]) +``` + +### Database Queries +```python +from app.core.database import execute_query + +# Fetch +customers = execute_query("SELECT * FROM customers WHERE id = %s", (id,)) + +# Insert +result = execute_query( + "INSERT INTO customers (name) VALUES (%s) RETURNING *", + (name,) +) +``` + +### Configuration +```python +from app.core.config import settings + +if settings.ECONOMIC_READ_ONLY: + logger.warning("Read-only mode") +``` + +## 🐳 Docker Commands + +```bash +# Start +docker-compose up -d + +# Logs +docker-compose logs -f api + +# Restart +docker-compose restart api + +# Stop +docker-compose down + +# Rebuild +docker-compose up -d --build +``` + +## 🚒 Production Deployment + +### On Live Server + +1. **Clone & Setup** +```bash +cd /opt +git clone git@g.bmcnetworks.dk:ct/bmc_hub.git +cd bmc_hub +``` + +2. **Configure Environment** +```bash +cp .env.prod.example .env +nano .env # Set RELEASE_VERSION, credentials, etc. +``` + +3. **Deploy** +```bash +docker-compose -f docker-compose.prod.yml up -d --build +``` + +4. **Update to New Version** +```bash +# Update .env with new RELEASE_VERSION +nano .env # Change to v1.2.3 + +# Pull and restart +docker-compose -f docker-compose.prod.yml up -d --build +``` + +## πŸ“Š Monitoring + +### Health Checks +```bash +# Simple +curl http://localhost:8000/health + +# Detailed +curl http://localhost:8000/api/v1/system/health + +# Config +curl http://localhost:8000/api/v1/system/config +``` + +### Logs +```bash +# Application logs +tail -f logs/app.log + +# Docker logs +docker-compose logs -f api +``` + +## πŸ” Security Checklist + +### Before Production +- [ ] Change `SECRET_KEY` to random value +- [ ] Set strong `POSTGRES_PASSWORD` +- [ ] Set `ECONOMIC_READ_ONLY=true` +- [ ] Set `ECONOMIC_DRY_RUN=true` +- [ ] Use tagged release version (not `latest`) +- [ ] Configure proper CORS origins +- [ ] Setup Nginx reverse proxy +- [ ] Enable SSL/TLS + +## πŸ§ͺ Testing + +```bash +# Install test dependencies +pip install pytest pytest-cov + +# Run tests +pytest + +# With coverage +pytest --cov=app --cov-report=html +open htmlcov/index.html +``` + +## πŸ“ Git Workflow + +### Development +```bash +git checkout -b feature/new-feature +# Make changes +git add . +git commit -m "Add new feature" +git push origin feature/new-feature +``` + +### Release +```bash +# Tag release +git tag v1.2.3 +git push --tags + +# Update production .env with RELEASE_VERSION=v1.2.3 +``` + +## πŸ”— Links + +- **API Docs**: http://localhost:8000/api/docs +- **ReDoc**: http://localhost:8000/api/redoc +- **Dashboard**: http://localhost:8000 +- **Gitea**: https://g.bmcnetworks.dk/ct/bmc_hub + +## πŸ†˜ Troubleshooting + +### Port Already in Use +```bash +lsof -i :8000 +kill -9 +``` + +### Database Connection Error +```bash +docker-compose logs postgres +docker-compose restart postgres +``` + +### Clear Everything +```bash +docker-compose down -v # WARNING: Deletes database! +docker-compose up -d +``` + +## πŸ“ž Support + +- Issues: Gitea Issues +- Email: support@bmcnetworks.dk diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b8067fc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,45 @@ +FROM python:3.13-slim + +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + curl \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Build arguments for GitHub release deployment +ARG RELEASE_VERSION=latest +ARG GITHUB_TOKEN +ARG GITHUB_REPO=ct/bmc_hub + +# Copy requirements first for better caching +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# If RELEASE_VERSION is set and not "latest", pull from GitHub release +# Otherwise, copy local files +RUN if [ "$RELEASE_VERSION" != "latest" ] && [ -n "$GITHUB_TOKEN" ]; then \ + echo "Downloading release ${RELEASE_VERSION} from GitHub..." && \ + curl -H "Authorization: token ${GITHUB_TOKEN}" \ + -L "https://g.bmcnetworks.dk/api/v1/repos/${GITHUB_REPO}/archive/${RELEASE_VERSION}.tar.gz" \ + -o /tmp/release.tar.gz && \ + tar -xzf /tmp/release.tar.gz --strip-components=1 -C /app && \ + rm /tmp/release.tar.gz; \ + fi + +# Copy application code (only used if not downloading from GitHub) +COPY . . + +# Create necessary directories +RUN mkdir -p /app/logs /app/uploads /app/static /app/data + +# Expose port +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=40s \ + CMD curl -f http://localhost:8000/health || exit 1 + +# Run application +CMD ["python", "main.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..2a071f0 --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +# BMC Hub πŸš€ + +Et centralt management system til BMC Networks - hΓ₯ndterer kunder, services, hardware og billing. + +**Baseret pΓ₯ OmniSync arkitektur med Python + PostgreSQL** + +## 🌟 Features + +- **Customer Management**: Komplet kundedatabase med CRM integration +- **Hardware Tracking**: Registrering og sporing af kundeudstyr +- **Service Management**: HΓ₯ndtering af services og abonnementer +- **Billing Integration**: Automatisk fakturering via e-conomic +- **REST API**: FastAPI med OpenAPI dokumentation +- **Web UI**: Responsive Bootstrap 5 interface +- **PostgreSQL**: Production-ready database +- **Docker**: Container deployment med version control + +## πŸ“š Quick Start + +### Lokal Udvikling + +```bash +# 1. Clone repository +git clone git@g.bmcnetworks.dk:ct/bmc_hub.git +cd bmc_hub + +# 2. Kopier og rediger .env +cp .env.example .env +nano .env # TilfΓΈj dine credentials + +# 3. Start med Docker Compose +docker-compose up -d + +# 4. Γ…bn browser +open http://localhost:8000/api/docs +``` + +### Live Deployment + +```bash +# PΓ₯ serveren +cd /opt +git clone git@g.bmcnetworks.dk:ct/bmc_hub.git +cd bmc_hub + +# Setup environment +cp .env.prod.example .env +nano .env # Udfyld credentials og version tag + +# Deploy +docker-compose -f docker-compose.prod.yml up -d +``` + +## πŸ› οΈ Deployment Commands + +### Lokal Development +```bash +docker-compose up -d # Start systemet +docker-compose logs -f # Se logs +docker-compose down # Stop systemet +``` + +### Production +```bash +docker-compose -f docker-compose.prod.yml up -d # Start +docker-compose -f docker-compose.prod.yml pull # Update til ny version +docker-compose -f docker-compose.prod.yml restart # Restart +``` + +## πŸ“‹ Krav + +### Development +- Docker Desktop eller Podman +- Git + +### Production +- Docker eller Podman +- PostgreSQL (via container) +- Nginx reverse proxy +- SSL certifikat + +## πŸ—οΈ Projekt Struktur + +``` +bmc_hub/ +β”œβ”€β”€ app/ +β”‚ β”œβ”€β”€ core/ +β”‚ β”‚ β”œβ”€β”€ config.py # Konfiguration +β”‚ β”‚ └── database.py # PostgreSQL helpers +β”‚ β”œβ”€β”€ models/ +β”‚ β”‚ └── schemas.py # Pydantic models +β”‚ β”œβ”€β”€ routers/ +β”‚ β”‚ β”œβ”€β”€ customers.py # Customer CRUD +β”‚ β”‚ β”œβ”€β”€ hardware.py # Hardware management +β”‚ β”‚ └── billing.py # Billing endpoints +β”‚ β”œβ”€β”€ services/ +β”‚ β”‚ └── economic.py # e-conomic integration +β”‚ └── jobs/ +β”‚ └── sync_job.py # Scheduled jobs +β”œβ”€β”€ static/ +β”‚ └── index.html # Dashboard UI +β”œβ”€β”€ migrations/ # Database migrations +β”œβ”€β”€ docker-compose.yml # Local development +β”œβ”€β”€ docker-compose.prod.yml # Production deployment +β”œβ”€β”€ Dockerfile # Docker image +β”œβ”€β”€ requirements.txt # Python dependencies +β”œβ”€β”€ .env.example # Environment template (local) +β”œβ”€β”€ .env.prod.example # Environment template (production) +└── main.py # FastAPI application +``` + +## πŸ”Œ API Endpoints + +- `GET /api/v1/customers` - List customers +- `GET /api/v1/hardware` - List hardware +- `GET /api/v1/billing/invoices` - List invoices +- `GET /health` - Health check + +Se fuld dokumentation: http://localhost:8000/api/docs + +## πŸ§ͺ Testing + +```bash +# Install test dependencies +pip install pytest pytest-cov + +# Run tests +pytest + +# Run with coverage +pytest --cov=app +``` + +## πŸ“„ License + +MIT License + +## πŸ“ž Support + +- **Issues**: Gitea Issues +- **Dokumentation**: `/api/docs` +- **Email**: support@bmcnetworks.dk + +--- + +**Made with ❀️ by BMC Networks** diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..4207132 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +"""BMC Hub Application Package""" diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..89a666b --- /dev/null +++ b/app/core/__init__.py @@ -0,0 +1 @@ +"""Core package""" diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..67fe13e --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,41 @@ +""" +Configuration Module +Handles environment variables and application settings +""" + +import os +from typing import List +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + """Application settings loaded from environment variables""" + + # Database + DATABASE_URL: str = "postgresql://bmc_hub:bmc_hub@localhost:5432/bmc_hub" + + # API + API_HOST: str = "0.0.0.0" + API_PORT: int = 8000 + + # Security + SECRET_KEY: str = "dev-secret-key-change-in-production" + ALLOWED_ORIGINS: List[str] = ["http://localhost:8000", "http://localhost:3000"] + + # Logging + LOG_LEVEL: str = "INFO" + LOG_FILE: str = "logs/app.log" + + # e-conomic Integration + ECONOMIC_API_URL: str = "https://restapi.e-conomic.com" + ECONOMIC_APP_SECRET_TOKEN: str = "" + ECONOMIC_AGREEMENT_GRANT_TOKEN: str = "" + ECONOMIC_READ_ONLY: bool = True + ECONOMIC_DRY_RUN: bool = True + + class Config: + env_file = ".env" + case_sensitive = True + + +settings = Settings() diff --git a/app/core/database.py b/app/core/database.py new file mode 100644 index 0000000..ba06d42 --- /dev/null +++ b/app/core/database.py @@ -0,0 +1,73 @@ +""" +Database Module +PostgreSQL connection and helpers using psycopg2 +""" + +import psycopg2 +from psycopg2.extras import RealDictCursor +from psycopg2.pool import SimpleConnectionPool +from typing import Optional +import logging + +from app.core.config import settings + +logger = logging.getLogger(__name__) + +# Connection pool +connection_pool: Optional[SimpleConnectionPool] = None + + +def init_db(): + """Initialize database connection pool""" + global connection_pool + + try: + connection_pool = SimpleConnectionPool( + minconn=1, + maxconn=10, + dsn=settings.DATABASE_URL + ) + logger.info("βœ… Database connection pool initialized") + except Exception as e: + logger.error(f"❌ Failed to initialize database: {e}") + raise + + +def get_db_connection(): + """Get a connection from the pool""" + if connection_pool: + return connection_pool.getconn() + raise Exception("Database pool not initialized") + + +def release_db_connection(conn): + """Return a connection to the pool""" + if connection_pool: + connection_pool.putconn(conn) + + +def get_db(): + """Context manager for database connections""" + conn = get_db_connection() + try: + yield conn + finally: + release_db_connection(conn) + + +def execute_query(query: str, params: tuple = None, fetch: bool = True): + """Execute a SQL query and return results""" + conn = get_db_connection() + try: + with conn.cursor(cursor_factory=RealDictCursor) as cursor: + cursor.execute(query, params) + if fetch: + return cursor.fetchall() + conn.commit() + return cursor.rowcount + except Exception as e: + conn.rollback() + logger.error(f"Query error: {e}") + raise + finally: + release_db_connection(conn) diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..e83e943 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1 @@ +"""Models package""" diff --git a/app/models/schemas.py b/app/models/schemas.py new file mode 100644 index 0000000..7ee6133 --- /dev/null +++ b/app/models/schemas.py @@ -0,0 +1,51 @@ +""" +Pydantic Models and Schemas +""" + +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + + +class CustomerBase(BaseModel): + """Base customer schema""" + name: str + email: Optional[str] = None + phone: Optional[str] = None + address: Optional[str] = None + + +class CustomerCreate(CustomerBase): + """Schema for creating a customer""" + pass + + +class Customer(CustomerBase): + """Full customer schema""" + id: int + created_at: datetime + updated_at: Optional[datetime] = None + + class Config: + from_attributes = True + + +class HardwareBase(BaseModel): + """Base hardware schema""" + serial_number: str + model: str + customer_id: int + + +class HardwareCreate(HardwareBase): + """Schema for creating hardware""" + pass + + +class Hardware(HardwareBase): + """Full hardware schema""" + id: int + created_at: datetime + + class Config: + from_attributes = True diff --git a/app/routers/__init__.py b/app/routers/__init__.py new file mode 100644 index 0000000..bf6ba02 --- /dev/null +++ b/app/routers/__init__.py @@ -0,0 +1 @@ +"""Routers package""" diff --git a/app/routers/billing.py b/app/routers/billing.py new file mode 100644 index 0000000..17e52d1 --- /dev/null +++ b/app/routers/billing.py @@ -0,0 +1,20 @@ +""" +Billing Router +API endpoints for billing operations +""" + +from fastapi import APIRouter + +router = APIRouter() + + +@router.get("/billing/invoices") +async def list_invoices(): + """List all invoices""" + return {"message": "Billing integration coming soon"} + + +@router.post("/billing/sync") +async def sync_to_economic(): + """Sync data to e-conomic""" + return {"message": "e-conomic sync coming soon"} diff --git a/app/routers/customers.py b/app/routers/customers.py new file mode 100644 index 0000000..28cc5f7 --- /dev/null +++ b/app/routers/customers.py @@ -0,0 +1,47 @@ +""" +Customers Router +API endpoints for customer management +""" + +from fastapi import APIRouter, HTTPException +from typing import List + +from app.models.schemas import Customer, CustomerCreate +from app.core.database import execute_query + +router = APIRouter() + + +@router.get("/customers", response_model=List[Customer]) +async def list_customers(): + """List all customers""" + query = "SELECT * FROM customers ORDER BY created_at DESC" + customers = execute_query(query) + return customers + + +@router.get("/customers/{customer_id}", response_model=Customer) +async def get_customer(customer_id: int): + """Get a specific customer""" + query = "SELECT * FROM customers WHERE id = %s" + customers = execute_query(query, (customer_id,)) + + if not customers: + raise HTTPException(status_code=404, detail="Customer not found") + + return customers[0] + + +@router.post("/customers", response_model=Customer) +async def create_customer(customer: CustomerCreate): + """Create a new customer""" + query = """ + INSERT INTO customers (name, email, phone, address) + VALUES (%s, %s, %s, %s) + RETURNING * + """ + result = execute_query( + query, + (customer.name, customer.email, customer.phone, customer.address) + ) + return result[0] diff --git a/app/routers/hardware.py b/app/routers/hardware.py new file mode 100644 index 0000000..a185fe4 --- /dev/null +++ b/app/routers/hardware.py @@ -0,0 +1,47 @@ +""" +Hardware Router +API endpoints for hardware management +""" + +from fastapi import APIRouter, HTTPException +from typing import List + +from app.models.schemas import Hardware, HardwareCreate +from app.core.database import execute_query + +router = APIRouter() + + +@router.get("/hardware", response_model=List[Hardware]) +async def list_hardware(): + """List all hardware""" + query = "SELECT * FROM hardware ORDER BY created_at DESC" + hardware = execute_query(query) + return hardware + + +@router.get("/hardware/{hardware_id}", response_model=Hardware) +async def get_hardware(hardware_id: int): + """Get specific hardware""" + query = "SELECT * FROM hardware WHERE id = %s" + hardware = execute_query(query, (hardware_id,)) + + if not hardware: + raise HTTPException(status_code=404, detail="Hardware not found") + + return hardware[0] + + +@router.post("/hardware", response_model=Hardware) +async def create_hardware(hardware: HardwareCreate): + """Create new hardware entry""" + query = """ + INSERT INTO hardware (serial_number, model, customer_id) + VALUES (%s, %s, %s) + RETURNING * + """ + result = execute_query( + query, + (hardware.serial_number, hardware.model, hardware.customer_id) + ) + return result[0] diff --git a/app/routers/system.py b/app/routers/system.py new file mode 100644 index 0000000..2cc927b --- /dev/null +++ b/app/routers/system.py @@ -0,0 +1,46 @@ +""" +System Router +Health checks and system information +""" + +from fastapi import APIRouter +from app.core.config import settings +from app.core.database import execute_query + +router = APIRouter() + + +@router.get("/system/health") +async def health_check(): + """Comprehensive health check""" + try: + # Test database connection + result = execute_query("SELECT 1 as test") + db_status = "healthy" if result else "unhealthy" + except Exception as e: + db_status = f"unhealthy: {str(e)}" + + return { + "status": "healthy", + "service": "BMC Hub", + "version": "1.0.0", + "database": db_status, + "config": { + "environment": "production" if not settings.ECONOMIC_DRY_RUN else "development", + "economic_read_only": settings.ECONOMIC_READ_ONLY, + "economic_dry_run": settings.ECONOMIC_DRY_RUN + } + } + + +@router.get("/system/config") +async def get_config(): + """Get system configuration (non-sensitive)""" + return { + "api_host": settings.API_HOST, + "api_port": settings.API_PORT, + "log_level": settings.LOG_LEVEL, + "economic_enabled": bool(settings.ECONOMIC_APP_SECRET_TOKEN), + "economic_read_only": settings.ECONOMIC_READ_ONLY, + "economic_dry_run": settings.ECONOMIC_DRY_RUN + } diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..861acda --- /dev/null +++ b/data/.gitkeep @@ -0,0 +1 @@ +# Data directory diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..6915728 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,72 @@ +version: '3.8' + +services: + # PostgreSQL Database + postgres: + image: postgres:16-alpine + container_name: bmc-hub-postgres-prod + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} + volumes: + - postgres_data:/var/lib/postgresql/data + - ./migrations/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + ports: + - "${POSTGRES_PORT:-5432}:5432" + restart: always + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - bmc-hub-network + + # FastAPI Application - Production with GitHub Release Version + api: + build: + context: . + dockerfile: Dockerfile + args: + RELEASE_VERSION: ${RELEASE_VERSION:-latest} + GITHUB_TOKEN: ${GITHUB_TOKEN} + GITHUB_REPO: ${GITHUB_REPO:-ct/bmc_hub} + image: bmc-hub:${RELEASE_VERSION:-latest} + container_name: bmc-hub-api-prod + depends_on: + postgres: + condition: service_healthy + ports: + - "${API_PORT:-8000}:8000" + volumes: + - ./logs:/app/logs + - ./uploads:/app/uploads + - ./data:/app/data + # NOTE: No source code mount in production - code comes from GitHub release + env_file: + - .env + environment: + # Override database URL to point to postgres service + - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} + - ENABLE_RELOAD=false + restart: always + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - bmc-hub-network + labels: + - "com.bmcnetworks.app=bmc-hub" + - "com.bmcnetworks.version=${RELEASE_VERSION:-latest}" + +networks: + bmc-hub-network: + driver: bridge + +volumes: + postgres_data: + driver: local diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5326f0e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,67 @@ +version: '3.8' + +services: + # PostgreSQL Database + postgres: + image: postgres:16-alpine + container_name: bmc-hub-postgres + environment: + POSTGRES_USER: ${POSTGRES_USER:-bmc_hub} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-bmc_hub} + POSTGRES_DB: ${POSTGRES_DB:-bmc_hub} + volumes: + - postgres_data:/var/lib/postgresql/data + - ./migrations/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + ports: + - "${POSTGRES_PORT:-5432}:5432" + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-bmc_hub}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - bmc-hub-network + + # FastAPI Application + api: + build: + context: . + dockerfile: Dockerfile + container_name: bmc-hub-api + depends_on: + postgres: + condition: service_healthy + ports: + - "${API_PORT:-8000}:8000" + volumes: + - ./logs:/app/logs + - ./uploads:/app/uploads + - ./static:/app/static + - ./data:/app/data + # Mount for local development - live code reload + - ./app:/app/app:ro + - ./main.py:/app/main.py:ro + env_file: + - .env + environment: + # Override database URL to point to postgres service + - DATABASE_URL=postgresql://${POSTGRES_USER:-bmc_hub}:${POSTGRES_PASSWORD:-bmc_hub}@postgres:5432/${POSTGRES_DB:-bmc_hub} + - ENABLE_RELOAD=true + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - bmc-hub-network + +networks: + bmc-hub-network: + driver: bridge + +volumes: + postgres_data: + driver: local diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..25a51f0 --- /dev/null +++ b/logs/.gitkeep @@ -0,0 +1 @@ +# Logs directory diff --git a/main.py b/main.py new file mode 100644 index 0000000..bc97ca8 --- /dev/null +++ b/main.py @@ -0,0 +1,122 @@ +""" +BMC Hub - FastAPI Application +Main application entry point +""" + +import logging +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import RedirectResponse +from contextlib import asynccontextmanager + +from app.core.config import settings +from app.core.database import init_db +from app.routers import ( + customers, + hardware, + billing, + system, +) + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(), + logging.FileHandler('logs/app.log') + ] +) + +logger = logging.getLogger(__name__) + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Lifecycle management - startup and shutdown""" + # Startup + logger.info("πŸš€ Starting BMC Hub...") + logger.info(f"Database: {settings.DATABASE_URL}") + + init_db() + + logger.info("βœ… System initialized successfully") + yield + # Shutdown + logger.info("πŸ‘‹ Shutting down...") + +# Create FastAPI app +app = FastAPI( + title="BMC Hub API", + description=""" + Central management system for BMC Networks. + + **Key Features:** + - Customer management + - Hardware tracking + - Service management + - Billing integration + """, + version="1.0.0", + lifespan=lifespan, + docs_url="/api/docs", + redoc_url="/api/redoc", + openapi_url="/api/openapi.json" +) + +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=settings.ALLOWED_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Include routers +app.include_router(customers.router, prefix="/api/v1", tags=["Customers"]) +app.include_router(hardware.router, prefix="/api/v1", tags=["Hardware"]) +app.include_router(billing.router, prefix="/api/v1", tags=["Billing"]) +app.include_router(system.router, prefix="/api/v1", tags=["System"]) + +# Serve static files (UI) +app.mount("/static", StaticFiles(directory="static", html=True), name="static") + +@app.get("/") +async def root(): + """Redirect to dashboard""" + return RedirectResponse(url="/static/index.html") + +@app.get("/health") +async def health_check(): + """Health check endpoint""" + return { + "status": "healthy", + "service": "BMC Hub", + "version": "1.0.0" + } + +if __name__ == "__main__": + import uvicorn + import os + + # Only enable reload in local development (not in Docker) + enable_reload = os.getenv("ENABLE_RELOAD", "false").lower() == "true" + + if enable_reload: + uvicorn.run( + "main:app", + host="0.0.0.0", + port=8000, + reload=True, + reload_includes=["*.py"], + reload_dirs=["app"], + log_level="info" + ) + else: + uvicorn.run( + "main:app", + host="0.0.0.0", + port=8000, + reload=False + ) diff --git a/migrations/init.sql b/migrations/init.sql new file mode 100644 index 0000000..00248bb --- /dev/null +++ b/migrations/init.sql @@ -0,0 +1,35 @@ +-- BMC Hub Database Schema +-- PostgreSQL 16 + +-- Customers table +CREATE TABLE IF NOT EXISTS customers ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255), + phone VARCHAR(50), + address TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP, + deleted_at TIMESTAMP +); + +-- Hardware table +CREATE TABLE IF NOT EXISTS hardware ( + id SERIAL PRIMARY KEY, + serial_number VARCHAR(255) NOT NULL UNIQUE, + model VARCHAR(255) NOT NULL, + customer_id INTEGER REFERENCES customers(id), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + deleted_at TIMESTAMP +); + +-- Create indexes +CREATE INDEX IF NOT EXISTS idx_customers_email ON customers(email); +CREATE INDEX IF NOT EXISTS idx_hardware_customer ON hardware(customer_id); +CREATE INDEX IF NOT EXISTS idx_hardware_serial ON hardware(serial_number); + +-- Insert sample data +INSERT INTO customers (name, email, phone, address) VALUES + ('BMC Networks', 'info@bmcnetworks.dk', '+45 12345678', 'KΓΈbenhavn'), + ('Test Customer', 'test@example.com', '+45 87654321', 'Aarhus') +ON CONFLICT DO NOTHING; diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..abfd76e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +fastapi==0.109.0 +uvicorn[standard]==0.27.0 +psycopg2-binary==2.9.9 +pydantic==2.5.3 +pydantic-settings==2.1.0 +python-dotenv==1.0.0 +python-multipart==0.0.6 diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..21fa493 --- /dev/null +++ b/static/index.html @@ -0,0 +1,70 @@ + + + + + + BMC Hub + + + + + +
+
+
+

Velkommen til BMC Hub

+

Central management system for BMC Networks

+
+
+ +
+
+
+
+
πŸ‘₯ Customers
+

Manage customer database

+ View API +
+
+
+
+
+
+
πŸ–₯️ Hardware
+

Track customer hardware

+ View API +
+
+
+
+
+
+
πŸ“Š System
+

Health and configuration

+ Health Check +
+
+
+
+ +
+
+
+
+
πŸ“– API Documentation
+

Explore the complete API documentation

+ OpenAPI Docs + ReDoc +
+
+
+
+
+ + + + diff --git a/uploads/.gitkeep b/uploads/.gitkeep new file mode 100644 index 0000000..f352f2f --- /dev/null +++ b/uploads/.gitkeep @@ -0,0 +1 @@ +# Uploads directory