Compare commits
2 Commits
b8ae693a41
...
ab7adc441c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab7adc441c | ||
|
|
13f23316a4 |
29
.dockerignore
Normal file
29
.dockerignore
Normal file
@ -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
|
||||
47
.env.example
Normal file
47
.env.example
Normal file
@ -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
|
||||
59
.env.prod.example
Normal file
59
.env.prod.example
Normal file
@ -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
|
||||
206
.github/copilot-instructions.md
vendored
Normal file
206
.github/copilot-instructions.md
vendored
Normal file
@ -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.
|
||||
176
.gitignore
vendored
176
.gitignore
vendored
@ -1,162 +1,30 @@
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
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/
|
||||
.venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
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/
|
||||
.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/
|
||||
|
||||
|
||||
259
DEVELOPMENT.md
Normal file
259
DEVELOPMENT.md
Normal file
@ -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 <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
|
||||
45
Dockerfile
Normal file
45
Dockerfile
Normal file
@ -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"]
|
||||
146
README.md
146
README.md
@ -1,2 +1,146 @@
|
||||
# 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**
|
||||
|
||||
1
app/__init__.py
Normal file
1
app/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""BMC Hub Application Package"""
|
||||
1
app/core/__init__.py
Normal file
1
app/core/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Core package"""
|
||||
41
app/core/config.py
Normal file
41
app/core/config.py
Normal file
@ -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()
|
||||
73
app/core/database.py
Normal file
73
app/core/database.py
Normal file
@ -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)
|
||||
1
app/models/__init__.py
Normal file
1
app/models/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Models package"""
|
||||
51
app/models/schemas.py
Normal file
51
app/models/schemas.py
Normal file
@ -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
|
||||
1
app/routers/__init__.py
Normal file
1
app/routers/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Routers package"""
|
||||
20
app/routers/billing.py
Normal file
20
app/routers/billing.py
Normal file
@ -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"}
|
||||
47
app/routers/customers.py
Normal file
47
app/routers/customers.py
Normal file
@ -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]
|
||||
47
app/routers/hardware.py
Normal file
47
app/routers/hardware.py
Normal file
@ -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]
|
||||
46
app/routers/system.py
Normal file
46
app/routers/system.py
Normal file
@ -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
|
||||
}
|
||||
1
data/.gitkeep
Normal file
1
data/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
# Data directory
|
||||
72
docker-compose.prod.yml
Normal file
72
docker-compose.prod.yml
Normal file
@ -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
|
||||
67
docker-compose.yml
Normal file
67
docker-compose.yml
Normal file
@ -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
|
||||
1
logs/.gitkeep
Normal file
1
logs/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
# Logs directory
|
||||
122
main.py
Normal file
122
main.py
Normal file
@ -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
|
||||
)
|
||||
35
migrations/init.sql
Normal file
35
migrations/init.sql
Normal file
@ -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;
|
||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -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
|
||||
70
static/index.html
Normal file
70
static/index.html
Normal file
@ -0,0 +1,70 @@
|
||||
<!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>
|
||||
1
uploads/.gitkeep
Normal file
1
uploads/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
# Uploads directory
|
||||
Loading…
Reference in New Issue
Block a user