Compare commits

..

No commits in common. "ab7adc441c96ed0fe06607af4148ad7895e9ba91" and "b8ae693a41ebb7a9424e332539513840c42a65b3" have entirely different histories.

28 changed files with 153 additions and 1515 deletions

View File

@ -1,29 +0,0 @@
__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

View File

@ -1,47 +0,0 @@
# =====================================================
# 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

View File

@ -1,59 +0,0 @@
# =====================================================
# 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

View File

@ -1,206 +0,0 @@
# 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.

172
.gitignore vendored
View File

@ -1,30 +1,162 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.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
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

View File

@ -1,259 +0,0 @@
# 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 <PID>
```
### 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

View File

@ -1,45 +0,0 @@
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"]

146
README.md
View File

@ -1,146 +1,2 @@
# BMC Hub 🚀
# 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**

View File

@ -1 +0,0 @@
"""BMC Hub Application Package"""

View File

@ -1 +0,0 @@
"""Core package"""

View File

@ -1,41 +0,0 @@
"""
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()

View File

@ -1,73 +0,0 @@
"""
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)

View File

@ -1 +0,0 @@
"""Models package"""

View File

@ -1,51 +0,0 @@
"""
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

View File

@ -1 +0,0 @@
"""Routers package"""

View File

@ -1,20 +0,0 @@
"""
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"}

View File

@ -1,47 +0,0 @@
"""
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]

View File

@ -1,47 +0,0 @@
"""
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]

View File

@ -1,46 +0,0 @@
"""
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
}

View File

@ -1 +0,0 @@
# Data directory

View File

@ -1,72 +0,0 @@
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

View File

@ -1,67 +0,0 @@
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

View File

@ -1 +0,0 @@
# Logs directory

122
main.py
View File

@ -1,122 +0,0 @@
"""
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
)

View File

@ -1,35 +0,0 @@
-- 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;

View File

@ -1,7 +0,0 @@
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

View File

@ -1,70 +0,0 @@
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BMC Hub</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-dark bg-primary">
<div class="container-fluid">
<span class="navbar-brand mb-0 h1">🚀 BMC Hub</span>
</div>
</nav>
<div class="container mt-4">
<div class="row">
<div class="col-md-12">
<h1>Velkommen til BMC Hub</h1>
<p class="lead">Central management system for BMC Networks</p>
</div>
</div>
<div class="row mt-4">
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">👥 Customers</h5>
<p class="card-text">Manage customer database</p>
<a href="/api/v1/customers" class="btn btn-primary">View API</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">🖥️ Hardware</h5>
<p class="card-text">Track customer hardware</p>
<a href="/api/v1/hardware" class="btn btn-primary">View API</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-body">
<h5 class="card-title">📊 System</h5>
<p class="card-text">Health and configuration</p>
<a href="/api/v1/system/health" class="btn btn-primary">Health Check</a>
</div>
</div>
</div>
</div>
<div class="row mt-4">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">📖 API Documentation</h5>
<p class="card-text">Explore the complete API documentation</p>
<a href="/api/docs" class="btn btn-success">OpenAPI Docs</a>
<a href="/api/redoc" class="btn btn-secondary">ReDoc</a>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -1 +0,0 @@
# Uploads directory