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__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
.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/
|
||||||
|
.venv/
|
||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env/
|
||||||
venv.bak/
|
*.egg-info/
|
||||||
|
dist/
|
||||||
# Spyder project settings
|
build/
|
||||||
.spyderproject
|
.env
|
||||||
.spyproject
|
.env.local
|
||||||
|
logs/*.log
|
||||||
# Rope project settings
|
uploads/*
|
||||||
.ropeproject
|
!uploads/.gitkeep
|
||||||
|
data/*
|
||||||
# mkdocs documentation
|
!data/.gitkeep
|
||||||
/site
|
.DS_Store
|
||||||
|
.vscode/
|
||||||
# mypy
|
.idea/
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
htmlcov/
|
||||||
|
.coverage
|
||||||
|
.pytest_cache/
|
||||||
.mypy_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