Compare commits
No commits in common. "ab7adc441c96ed0fe06607af4148ad7895e9ba91" and "b8ae693a41ebb7a9424e332539513840c42a65b3" have entirely different histories.
ab7adc441c
...
b8ae693a41
@ -1,29 +0,0 @@
|
|||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
*.so
|
|
||||||
.git/
|
|
||||||
.gitignore
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
venv/
|
|
||||||
.venv/
|
|
||||||
ENV/
|
|
||||||
env/
|
|
||||||
logs/*.log
|
|
||||||
uploads/*
|
|
||||||
!uploads/.gitkeep
|
|
||||||
data/*
|
|
||||||
!data/.gitkeep
|
|
||||||
.DS_Store
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
*.db
|
|
||||||
*.db-journal
|
|
||||||
htmlcov/
|
|
||||||
.coverage
|
|
||||||
.pytest_cache/
|
|
||||||
README.md
|
|
||||||
docker-compose.yml
|
|
||||||
docker-compose.prod.yml
|
|
||||||
.dockerignore
|
|
||||||
47
.env.example
47
.env.example
@ -1,47 +0,0 @@
|
|||||||
# =====================================================
|
|
||||||
# POSTGRESQL DATABASE - Local Development
|
|
||||||
# =====================================================
|
|
||||||
DATABASE_URL=postgresql://bmc_hub:bmc_hub@postgres:5432/bmc_hub
|
|
||||||
|
|
||||||
# Database credentials (bruges af docker-compose)
|
|
||||||
POSTGRES_USER=bmc_hub
|
|
||||||
POSTGRES_PASSWORD=bmc_hub
|
|
||||||
POSTGRES_DB=bmc_hub
|
|
||||||
POSTGRES_PORT=5432
|
|
||||||
|
|
||||||
# =====================================================
|
|
||||||
# API CONFIGURATION
|
|
||||||
# =====================================================
|
|
||||||
API_HOST=0.0.0.0
|
|
||||||
API_PORT=8000
|
|
||||||
API_RELOAD=true
|
|
||||||
|
|
||||||
# =====================================================
|
|
||||||
# SECURITY
|
|
||||||
# =====================================================
|
|
||||||
SECRET_KEY=change-this-in-production-use-random-string
|
|
||||||
CORS_ORIGINS=http://localhost:8000,http://localhost:3000
|
|
||||||
|
|
||||||
# =====================================================
|
|
||||||
# LOGGING
|
|
||||||
# =====================================================
|
|
||||||
LOG_LEVEL=INFO
|
|
||||||
LOG_FILE=logs/app.log
|
|
||||||
|
|
||||||
# =====================================================
|
|
||||||
# GITHUB/GITEA REPOSITORY (Optional - for reference)
|
|
||||||
# =====================================================
|
|
||||||
# Repository: https://g.bmcnetworks.dk/ct/bmc_hub
|
|
||||||
GITHUB_REPO=ct/bmc_hub
|
|
||||||
|
|
||||||
# =====================================================
|
|
||||||
# e-conomic Integration (Optional)
|
|
||||||
# =====================================================
|
|
||||||
# Get credentials from e-conomic Settings -> Integrations -> API
|
|
||||||
ECONOMIC_API_URL=https://restapi.e-conomic.com
|
|
||||||
ECONOMIC_APP_SECRET_TOKEN=your_app_secret_token_here
|
|
||||||
ECONOMIC_AGREEMENT_GRANT_TOKEN=your_agreement_grant_token_here
|
|
||||||
|
|
||||||
# 🚨 SAFETY SWITCHES - Beskytter mod utilsigtede ændringer
|
|
||||||
ECONOMIC_READ_ONLY=true # Set to false ONLY after testing
|
|
||||||
ECONOMIC_DRY_RUN=true # Set to false ONLY when ready for production writes
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
# =====================================================
|
|
||||||
# POSTGRESQL DATABASE - Production
|
|
||||||
# =====================================================
|
|
||||||
DATABASE_URL=postgresql://bmc_hub:CHANGEME_STRONG_PASSWORD@postgres:5432/bmc_hub
|
|
||||||
|
|
||||||
# Database credentials (bruges af docker-compose)
|
|
||||||
POSTGRES_USER=bmc_hub
|
|
||||||
POSTGRES_PASSWORD=CHANGEME_STRONG_PASSWORD
|
|
||||||
POSTGRES_DB=bmc_hub
|
|
||||||
POSTGRES_PORT=5432
|
|
||||||
|
|
||||||
# =====================================================
|
|
||||||
# GITHUB DEPLOYMENT - Production Version Control
|
|
||||||
# =====================================================
|
|
||||||
# Git tag eller branch at deploye (f.eks. "v1.0.0", "v1.2.3")
|
|
||||||
# VIGTIGT: Brug ALTID tags til production (ikke "latest" eller "main")
|
|
||||||
RELEASE_VERSION=v1.0.0
|
|
||||||
|
|
||||||
# GitHub repository (format: owner/repo eller path på Gitea)
|
|
||||||
GITHUB_REPO=ct/bmc_hub
|
|
||||||
|
|
||||||
# GitHub/Gitea Personal Access Token (skal have læseadgang til repo)
|
|
||||||
# Opret token på: https://g.bmcnetworks.dk/user/settings/applications
|
|
||||||
GITHUB_TOKEN=your_gitea_token_here
|
|
||||||
|
|
||||||
# =====================================================
|
|
||||||
# API CONFIGURATION - Production
|
|
||||||
# =====================================================
|
|
||||||
API_HOST=0.0.0.0
|
|
||||||
API_PORT=8000
|
|
||||||
API_RELOAD=false
|
|
||||||
|
|
||||||
# =====================================================
|
|
||||||
# SECURITY - Production
|
|
||||||
# =====================================================
|
|
||||||
# VIGTIGT: Generer en stærk SECRET_KEY i production!
|
|
||||||
# Brug: python -c "import secrets; print(secrets.token_urlsafe(32))"
|
|
||||||
SECRET_KEY=CHANGEME_GENERATE_RANDOM_SECRET_KEY
|
|
||||||
|
|
||||||
# CORS origins - tilføj din domain
|
|
||||||
CORS_ORIGINS=https://hub.bmcnetworks.dk,https://api.bmcnetworks.dk
|
|
||||||
|
|
||||||
# =====================================================
|
|
||||||
# LOGGING - Production
|
|
||||||
# =====================================================
|
|
||||||
LOG_LEVEL=INFO
|
|
||||||
LOG_FILE=logs/app.log
|
|
||||||
|
|
||||||
# =====================================================
|
|
||||||
# e-conomic Integration - Production
|
|
||||||
# =====================================================
|
|
||||||
ECONOMIC_API_URL=https://restapi.e-conomic.com
|
|
||||||
ECONOMIC_APP_SECRET_TOKEN=your_production_token_here
|
|
||||||
ECONOMIC_AGREEMENT_GRANT_TOKEN=your_production_grant_here
|
|
||||||
|
|
||||||
# 🚨 SAFETY SWITCHES
|
|
||||||
# Start ALTID med begge sat til true i ny production deployment!
|
|
||||||
ECONOMIC_READ_ONLY=true # Set to false after thorough testing
|
|
||||||
ECONOMIC_DRY_RUN=true # Set to false when ready for live writes
|
|
||||||
206
.github/copilot-instructions.md
vendored
206
.github/copilot-instructions.md
vendored
@ -1,206 +0,0 @@
|
|||||||
# GitHub Copilot Instructions - BMC Hub
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
BMC Hub is a central management system for BMC Networks built with **FastAPI + PostgreSQL**. The architecture is inspired by the OmniSync project (`/Users/christianthomas/pakkemodtagelse`) but adapted for BMC's specific needs.
|
|
||||||
|
|
||||||
**Tech Stack**: Python 3.13, FastAPI, PostgreSQL 16, Docker, psycopg2
|
|
||||||
|
|
||||||
## Architecture Principles
|
|
||||||
|
|
||||||
### Database-First Design
|
|
||||||
- **PostgreSQL only** - no SQLite. Use `psycopg2` with connection pooling
|
|
||||||
- All DB operations via `app/core/database.py::execute_query()` helper
|
|
||||||
- Parameterized queries (`%s` placeholders) to prevent SQL injection
|
|
||||||
- Use `RealDictCursor` for dict-like row access
|
|
||||||
- Schema defined in `migrations/init.sql` with versioned migrations
|
|
||||||
|
|
||||||
### FastAPI Router Pattern
|
|
||||||
- Each domain gets a router in `app/routers/` (customers, hardware, billing, system)
|
|
||||||
- Routers imported and registered in `main.py` with prefix `/api/v1`
|
|
||||||
- Use Pydantic models from `app/models/schemas.py` for request/response validation
|
|
||||||
- Return model instances directly - FastAPI handles serialization
|
|
||||||
|
|
||||||
### Configuration Management
|
|
||||||
- All settings in `app/core/config.py` using `pydantic_settings.BaseSettings`
|
|
||||||
- Loads from `.env` file automatically via `Config.env_file = ".env"`
|
|
||||||
- Access via `from app.core.config import settings`
|
|
||||||
- **Never hardcode credentials** - always use environment variables
|
|
||||||
|
|
||||||
### Safety-First Integrations
|
|
||||||
- External API integrations (e-conomic) have **safety switches**: `ECONOMIC_READ_ONLY` and `ECONOMIC_DRY_RUN`
|
|
||||||
- Always default to `true` for both switches in new deployments
|
|
||||||
- Log all external API calls before execution
|
|
||||||
- Provide dry-run mode that logs without executing
|
|
||||||
|
|
||||||
## Development Workflows
|
|
||||||
|
|
||||||
### Local Development Setup
|
|
||||||
```bash
|
|
||||||
cp .env.example .env # Create local environment
|
|
||||||
docker-compose up -d # Start PostgreSQL + API
|
|
||||||
docker-compose logs -f api # Watch logs
|
|
||||||
```
|
|
||||||
|
|
||||||
The local `docker-compose.yml` mounts source code for live reload:
|
|
||||||
- `./app:/app/app:ro` - Python code hot-reload
|
|
||||||
- `ENABLE_RELOAD=true` - Uvicorn auto-restart
|
|
||||||
|
|
||||||
### Production Deployment
|
|
||||||
Production uses **version-tagged GitHub releases** via Gitea:
|
|
||||||
|
|
||||||
1. Tag release: `git tag v1.2.3 && git push --tags`
|
|
||||||
2. Update `.env` with `RELEASE_VERSION=v1.2.3`
|
|
||||||
3. Deploy: `docker-compose -f docker-compose.prod.yml up -d --build`
|
|
||||||
|
|
||||||
The production Dockerfile downloads code from Gitea if `RELEASE_VERSION != "latest"`:
|
|
||||||
```dockerfile
|
|
||||||
ARG RELEASE_VERSION=latest
|
|
||||||
ARG GITHUB_TOKEN
|
|
||||||
ARG GITHUB_REPO=ct/bmc_hub
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key difference from local**: Production does NOT mount source code - it's baked into the image from the Git tag.
|
|
||||||
|
|
||||||
### Database Migrations
|
|
||||||
- SQL migrations in `migrations/` numbered sequentially
|
|
||||||
- `init.sql` runs on first container startup via Docker entrypoint
|
|
||||||
- For schema changes: create `migrations/001_feature_name.sql` and run manually or via migration script
|
|
||||||
|
|
||||||
### Adding New Features
|
|
||||||
1. **Create Pydantic models** in `app/models/schemas.py` (Base, Create, Full schemas)
|
|
||||||
2. **Add database migration** in `migrations/XXX_feature.sql`
|
|
||||||
3. **Create router** in `app/routers/feature.py` with CRUD endpoints
|
|
||||||
4. **Register router** in `main.py`: `app.include_router(feature.router, prefix="/api/v1", tags=["Feature"])`
|
|
||||||
5. **Add tests** (when test framework exists)
|
|
||||||
|
|
||||||
## Code Patterns
|
|
||||||
|
|
||||||
### Database Query Pattern
|
|
||||||
```python
|
|
||||||
from app.core.database import execute_query
|
|
||||||
|
|
||||||
# Fetch rows
|
|
||||||
query = "SELECT * FROM customers WHERE id = %s"
|
|
||||||
result = execute_query(query, (customer_id,))
|
|
||||||
|
|
||||||
# Insert/Update (returns affected rows)
|
|
||||||
query = "INSERT INTO customers (name) VALUES (%s) RETURNING *"
|
|
||||||
result = execute_query(query, (name,))
|
|
||||||
```
|
|
||||||
|
|
||||||
### Router Endpoint Pattern
|
|
||||||
```python
|
|
||||||
from fastapi import APIRouter, HTTPException
|
|
||||||
from app.models.schemas import Customer, CustomerCreate
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
@router.get("/customers/{id}", response_model=Customer)
|
|
||||||
async def get_customer(id: int):
|
|
||||||
result = execute_query("SELECT * FROM customers WHERE id = %s", (id,))
|
|
||||||
if not result:
|
|
||||||
raise HTTPException(status_code=404, detail="Not found")
|
|
||||||
return result[0]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration Access Pattern
|
|
||||||
```python
|
|
||||||
from app.core.config import settings
|
|
||||||
|
|
||||||
# Check feature flags
|
|
||||||
if settings.ECONOMIC_READ_ONLY:
|
|
||||||
logger.warning("Read-only mode enabled")
|
|
||||||
return {"message": "Dry run only"}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project-Specific Conventions
|
|
||||||
|
|
||||||
### Logging
|
|
||||||
- Use Python's `logging` module, not `print()`
|
|
||||||
- Format: `logger.info("Message with %s", variable)`
|
|
||||||
- Emoji prefixes for visibility: `🚀` startup, `✅` success, `❌` error, `⚠️` warning
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
- Use `HTTPException` for API errors with appropriate status codes
|
|
||||||
- Log exceptions with `logger.error()` before raising
|
|
||||||
- Return user-friendly error messages, log technical details
|
|
||||||
|
|
||||||
### API Response Format
|
|
||||||
- Use Pydantic response models - let FastAPI handle serialization
|
|
||||||
- For lists: `response_model=List[ModelName]`
|
|
||||||
- For health checks: return dict with `status`, `service`, `version` keys
|
|
||||||
|
|
||||||
### File Organization
|
|
||||||
- **One router per domain** - don't create mega-files
|
|
||||||
- **Services in `app/services/`** for business logic (e.g., `economic.py` for API integration)
|
|
||||||
- **Jobs in `app/jobs/`** for scheduled tasks
|
|
||||||
- **Keep routers thin** - delegate complex logic to services
|
|
||||||
|
|
||||||
## Docker & Deployment
|
|
||||||
|
|
||||||
### Environment Files
|
|
||||||
- **`.env.example`** - Local development template (weak passwords OK)
|
|
||||||
- **`.env.prod.example`** - Production template with placeholders and security notes
|
|
||||||
- **Never commit `.env`** - it's in `.gitignore`
|
|
||||||
|
|
||||||
### Docker Compose Files
|
|
||||||
- **`docker-compose.yml`** - Local dev with code mounting and auto-reload
|
|
||||||
- **`docker-compose.prod.yml`** - Production with version tags, no code mounts, always restart
|
|
||||||
|
|
||||||
### Gitea Integration
|
|
||||||
The production deployment pulls code from the Gitea server at `g.bmcnetworks.dk`:
|
|
||||||
- Requires `GITHUB_TOKEN` environment variable (Gitea personal access token)
|
|
||||||
- Repo format: `ct/bmc_hub` (owner/repo)
|
|
||||||
- Release tags: `v1.0.0`, `v1.2.3` etc. (semantic versioning)
|
|
||||||
|
|
||||||
## Common Pitfalls to Avoid
|
|
||||||
|
|
||||||
1. **Don't use SQLite** - this is a PostgreSQL-only project
|
|
||||||
2. **Don't use ORMs** - use raw SQL via `execute_query()` for simplicity
|
|
||||||
3. **Don't hardcode database URLs** - always use `settings.DATABASE_URL`
|
|
||||||
4. **Don't skip parameterization** - SQL injection risk with string formatting
|
|
||||||
5. **Don't mount code in production** - use version tags and Docker builds
|
|
||||||
6. **Don't disable safety switches in .env.example** - always default to safe mode
|
|
||||||
|
|
||||||
## Integration with OmniSync
|
|
||||||
|
|
||||||
This project shares architectural patterns with `/Users/christianthomas/pakkemodtagelse`:
|
|
||||||
- Similar FastAPI structure with routers, services, jobs
|
|
||||||
- PostgreSQL with psycopg2 (no ORM)
|
|
||||||
- Docker Compose for both dev and production
|
|
||||||
- Environment-based configuration
|
|
||||||
- Migration-based schema management
|
|
||||||
|
|
||||||
**Key differences**:
|
|
||||||
- BMC Hub uses **version-tagged deployments** from Gitea
|
|
||||||
- Simpler domain model (customers, hardware, billing vs. full invoice management)
|
|
||||||
- No Ollama/LLM integration (not needed for this use case)
|
|
||||||
|
|
||||||
## Quick Reference
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Import patterns
|
|
||||||
from app.core.config import settings
|
|
||||||
from app.core.database import execute_query
|
|
||||||
from app.models.schemas import Customer, CustomerCreate
|
|
||||||
from fastapi import APIRouter, HTTPException
|
|
||||||
|
|
||||||
# Database query
|
|
||||||
result = execute_query("SELECT * FROM table WHERE id = %s", (id,))
|
|
||||||
|
|
||||||
# Configuration
|
|
||||||
settings.DATABASE_URL
|
|
||||||
settings.ECONOMIC_READ_ONLY
|
|
||||||
|
|
||||||
# Router registration (main.py)
|
|
||||||
app.include_router(router, prefix="/api/v1", tags=["Feature"])
|
|
||||||
```
|
|
||||||
|
|
||||||
## Health Check Endpoint
|
|
||||||
Always maintain `/health` and `/api/v1/system/health` endpoints:
|
|
||||||
- Test database connectivity
|
|
||||||
- Report configuration state
|
|
||||||
- Return service name and version
|
|
||||||
|
|
||||||
This ensures monitoring systems (Uptime Kuma) can track service health.
|
|
||||||
172
.gitignore
vendored
172
.gitignore
vendored
@ -1,30 +1,162 @@
|
|||||||
|
# ---> Python
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
.Python
|
.Python
|
||||||
venv/
|
|
||||||
.venv/
|
|
||||||
ENV/
|
|
||||||
env/
|
|
||||||
*.egg-info/
|
|
||||||
dist/
|
|
||||||
build/
|
build/
|
||||||
.env
|
develop-eggs/
|
||||||
.env.local
|
dist/
|
||||||
logs/*.log
|
downloads/
|
||||||
uploads/*
|
eggs/
|
||||||
!uploads/.gitkeep
|
.eggs/
|
||||||
data/*
|
lib/
|
||||||
!data/.gitkeep
|
lib64/
|
||||||
.DS_Store
|
parts/
|
||||||
.vscode/
|
sdist/
|
||||||
.idea/
|
var/
|
||||||
*.db
|
wheels/
|
||||||
*.db-journal
|
share/python-wheels/
|
||||||
*.sqlite
|
*.egg-info/
|
||||||
*.sqlite3
|
.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/
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
.coverage
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
.mypy_cache/
|
.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
259
DEVELOPMENT.md
@ -1,259 +0,0 @@
|
|||||||
# BMC Hub - Development Guide
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
|
||||||
|
|
||||||
### 1. Clone & Setup
|
|
||||||
```bash
|
|
||||||
cd /Users/christianthomas/DEV/bmc_hub_dev
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Start Development Server
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
docker-compose logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Verify Installation
|
|
||||||
```bash
|
|
||||||
# Health check
|
|
||||||
curl http://localhost:8000/health
|
|
||||||
|
|
||||||
# API docs
|
|
||||||
open http://localhost:8000/api/docs
|
|
||||||
|
|
||||||
# Dashboard
|
|
||||||
open http://localhost:8000
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📁 Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
bmc_hub/
|
|
||||||
├── app/
|
|
||||||
│ ├── core/ # Config & database
|
|
||||||
│ ├── models/ # Pydantic schemas
|
|
||||||
│ ├── routers/ # API endpoints
|
|
||||||
│ ├── services/ # Business logic
|
|
||||||
│ └── jobs/ # Scheduled tasks
|
|
||||||
├── migrations/ # Database migrations
|
|
||||||
├── static/ # Web UI
|
|
||||||
├── .env.example # Local dev template
|
|
||||||
└── .env.prod.example # Production template
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Development Workflow
|
|
||||||
|
|
||||||
### Adding a New Feature
|
|
||||||
|
|
||||||
1. **Database Migration**
|
|
||||||
```sql
|
|
||||||
-- migrations/002_add_services.sql
|
|
||||||
CREATE TABLE services (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
customer_id INTEGER REFERENCES customers(id)
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Pydantic Models**
|
|
||||||
```python
|
|
||||||
# app/models/schemas.py
|
|
||||||
class ServiceBase(BaseModel):
|
|
||||||
name: str
|
|
||||||
customer_id: int
|
|
||||||
|
|
||||||
class Service(ServiceBase):
|
|
||||||
id: int
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Router**
|
|
||||||
```python
|
|
||||||
# app/routers/services.py
|
|
||||||
from fastapi import APIRouter
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
@router.get("/services")
|
|
||||||
async def list_services():
|
|
||||||
return execute_query("SELECT * FROM services")
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Register Router**
|
|
||||||
```python
|
|
||||||
# main.py
|
|
||||||
from app.routers import services
|
|
||||||
app.include_router(services.router, prefix="/api/v1", tags=["Services"])
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Queries
|
|
||||||
```python
|
|
||||||
from app.core.database import execute_query
|
|
||||||
|
|
||||||
# Fetch
|
|
||||||
customers = execute_query("SELECT * FROM customers WHERE id = %s", (id,))
|
|
||||||
|
|
||||||
# Insert
|
|
||||||
result = execute_query(
|
|
||||||
"INSERT INTO customers (name) VALUES (%s) RETURNING *",
|
|
||||||
(name,)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
```python
|
|
||||||
from app.core.config import settings
|
|
||||||
|
|
||||||
if settings.ECONOMIC_READ_ONLY:
|
|
||||||
logger.warning("Read-only mode")
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🐳 Docker Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
docker-compose logs -f api
|
|
||||||
|
|
||||||
# Restart
|
|
||||||
docker-compose restart api
|
|
||||||
|
|
||||||
# Stop
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
# Rebuild
|
|
||||||
docker-compose up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🚢 Production Deployment
|
|
||||||
|
|
||||||
### On Live Server
|
|
||||||
|
|
||||||
1. **Clone & Setup**
|
|
||||||
```bash
|
|
||||||
cd /opt
|
|
||||||
git clone git@g.bmcnetworks.dk:ct/bmc_hub.git
|
|
||||||
cd bmc_hub
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Configure Environment**
|
|
||||||
```bash
|
|
||||||
cp .env.prod.example .env
|
|
||||||
nano .env # Set RELEASE_VERSION, credentials, etc.
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Deploy**
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Update to New Version**
|
|
||||||
```bash
|
|
||||||
# Update .env with new RELEASE_VERSION
|
|
||||||
nano .env # Change to v1.2.3
|
|
||||||
|
|
||||||
# Pull and restart
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 Monitoring
|
|
||||||
|
|
||||||
### Health Checks
|
|
||||||
```bash
|
|
||||||
# Simple
|
|
||||||
curl http://localhost:8000/health
|
|
||||||
|
|
||||||
# Detailed
|
|
||||||
curl http://localhost:8000/api/v1/system/health
|
|
||||||
|
|
||||||
# Config
|
|
||||||
curl http://localhost:8000/api/v1/system/config
|
|
||||||
```
|
|
||||||
|
|
||||||
### Logs
|
|
||||||
```bash
|
|
||||||
# Application logs
|
|
||||||
tail -f logs/app.log
|
|
||||||
|
|
||||||
# Docker logs
|
|
||||||
docker-compose logs -f api
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔐 Security Checklist
|
|
||||||
|
|
||||||
### Before Production
|
|
||||||
- [ ] Change `SECRET_KEY` to random value
|
|
||||||
- [ ] Set strong `POSTGRES_PASSWORD`
|
|
||||||
- [ ] Set `ECONOMIC_READ_ONLY=true`
|
|
||||||
- [ ] Set `ECONOMIC_DRY_RUN=true`
|
|
||||||
- [ ] Use tagged release version (not `latest`)
|
|
||||||
- [ ] Configure proper CORS origins
|
|
||||||
- [ ] Setup Nginx reverse proxy
|
|
||||||
- [ ] Enable SSL/TLS
|
|
||||||
|
|
||||||
## 🧪 Testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install test dependencies
|
|
||||||
pip install pytest pytest-cov
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
pytest
|
|
||||||
|
|
||||||
# With coverage
|
|
||||||
pytest --cov=app --cov-report=html
|
|
||||||
open htmlcov/index.html
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 Git Workflow
|
|
||||||
|
|
||||||
### Development
|
|
||||||
```bash
|
|
||||||
git checkout -b feature/new-feature
|
|
||||||
# Make changes
|
|
||||||
git add .
|
|
||||||
git commit -m "Add new feature"
|
|
||||||
git push origin feature/new-feature
|
|
||||||
```
|
|
||||||
|
|
||||||
### Release
|
|
||||||
```bash
|
|
||||||
# Tag release
|
|
||||||
git tag v1.2.3
|
|
||||||
git push --tags
|
|
||||||
|
|
||||||
# Update production .env with RELEASE_VERSION=v1.2.3
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔗 Links
|
|
||||||
|
|
||||||
- **API Docs**: http://localhost:8000/api/docs
|
|
||||||
- **ReDoc**: http://localhost:8000/api/redoc
|
|
||||||
- **Dashboard**: http://localhost:8000
|
|
||||||
- **Gitea**: https://g.bmcnetworks.dk/ct/bmc_hub
|
|
||||||
|
|
||||||
## 🆘 Troubleshooting
|
|
||||||
|
|
||||||
### Port Already in Use
|
|
||||||
```bash
|
|
||||||
lsof -i :8000
|
|
||||||
kill -9 <PID>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Connection Error
|
|
||||||
```bash
|
|
||||||
docker-compose logs postgres
|
|
||||||
docker-compose restart postgres
|
|
||||||
```
|
|
||||||
|
|
||||||
### Clear Everything
|
|
||||||
```bash
|
|
||||||
docker-compose down -v # WARNING: Deletes database!
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
- Issues: Gitea Issues
|
|
||||||
- Email: support@bmcnetworks.dk
|
|
||||||
45
Dockerfile
45
Dockerfile
@ -1,45 +0,0 @@
|
|||||||
FROM python:3.13-slim
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Install system dependencies
|
|
||||||
RUN apt-get update && apt-get install -y \
|
|
||||||
curl \
|
|
||||||
git \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Build arguments for GitHub release deployment
|
|
||||||
ARG RELEASE_VERSION=latest
|
|
||||||
ARG GITHUB_TOKEN
|
|
||||||
ARG GITHUB_REPO=ct/bmc_hub
|
|
||||||
|
|
||||||
# Copy requirements first for better caching
|
|
||||||
COPY requirements.txt .
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
# If RELEASE_VERSION is set and not "latest", pull from GitHub release
|
|
||||||
# Otherwise, copy local files
|
|
||||||
RUN if [ "$RELEASE_VERSION" != "latest" ] && [ -n "$GITHUB_TOKEN" ]; then \
|
|
||||||
echo "Downloading release ${RELEASE_VERSION} from GitHub..." && \
|
|
||||||
curl -H "Authorization: token ${GITHUB_TOKEN}" \
|
|
||||||
-L "https://g.bmcnetworks.dk/api/v1/repos/${GITHUB_REPO}/archive/${RELEASE_VERSION}.tar.gz" \
|
|
||||||
-o /tmp/release.tar.gz && \
|
|
||||||
tar -xzf /tmp/release.tar.gz --strip-components=1 -C /app && \
|
|
||||||
rm /tmp/release.tar.gz; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Copy application code (only used if not downloading from GitHub)
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Create necessary directories
|
|
||||||
RUN mkdir -p /app/logs /app/uploads /app/static /app/data
|
|
||||||
|
|
||||||
# Expose port
|
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
# Health check
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --retries=3 --start-period=40s \
|
|
||||||
CMD curl -f http://localhost:8000/health || exit 1
|
|
||||||
|
|
||||||
# Run application
|
|
||||||
CMD ["python", "main.py"]
|
|
||||||
146
README.md
146
README.md
@ -1,146 +1,2 @@
|
|||||||
# BMC Hub 🚀
|
# bmc_hub
|
||||||
|
|
||||||
Et centralt management system til BMC Networks - håndterer kunder, services, hardware og billing.
|
|
||||||
|
|
||||||
**Baseret på OmniSync arkitektur med Python + PostgreSQL**
|
|
||||||
|
|
||||||
## 🌟 Features
|
|
||||||
|
|
||||||
- **Customer Management**: Komplet kundedatabase med CRM integration
|
|
||||||
- **Hardware Tracking**: Registrering og sporing af kundeudstyr
|
|
||||||
- **Service Management**: Håndtering af services og abonnementer
|
|
||||||
- **Billing Integration**: Automatisk fakturering via e-conomic
|
|
||||||
- **REST API**: FastAPI med OpenAPI dokumentation
|
|
||||||
- **Web UI**: Responsive Bootstrap 5 interface
|
|
||||||
- **PostgreSQL**: Production-ready database
|
|
||||||
- **Docker**: Container deployment med version control
|
|
||||||
|
|
||||||
## 📚 Quick Start
|
|
||||||
|
|
||||||
### Lokal Udvikling
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Clone repository
|
|
||||||
git clone git@g.bmcnetworks.dk:ct/bmc_hub.git
|
|
||||||
cd bmc_hub
|
|
||||||
|
|
||||||
# 2. Kopier og rediger .env
|
|
||||||
cp .env.example .env
|
|
||||||
nano .env # Tilføj dine credentials
|
|
||||||
|
|
||||||
# 3. Start med Docker Compose
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# 4. Åbn browser
|
|
||||||
open http://localhost:8000/api/docs
|
|
||||||
```
|
|
||||||
|
|
||||||
### Live Deployment
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# På serveren
|
|
||||||
cd /opt
|
|
||||||
git clone git@g.bmcnetworks.dk:ct/bmc_hub.git
|
|
||||||
cd bmc_hub
|
|
||||||
|
|
||||||
# Setup environment
|
|
||||||
cp .env.prod.example .env
|
|
||||||
nano .env # Udfyld credentials og version tag
|
|
||||||
|
|
||||||
# Deploy
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🛠️ Deployment Commands
|
|
||||||
|
|
||||||
### Lokal Development
|
|
||||||
```bash
|
|
||||||
docker-compose up -d # Start systemet
|
|
||||||
docker-compose logs -f # Se logs
|
|
||||||
docker-compose down # Stop systemet
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.prod.yml up -d # Start
|
|
||||||
docker-compose -f docker-compose.prod.yml pull # Update til ny version
|
|
||||||
docker-compose -f docker-compose.prod.yml restart # Restart
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📋 Krav
|
|
||||||
|
|
||||||
### Development
|
|
||||||
- Docker Desktop eller Podman
|
|
||||||
- Git
|
|
||||||
|
|
||||||
### Production
|
|
||||||
- Docker eller Podman
|
|
||||||
- PostgreSQL (via container)
|
|
||||||
- Nginx reverse proxy
|
|
||||||
- SSL certifikat
|
|
||||||
|
|
||||||
## 🏗️ Projekt Struktur
|
|
||||||
|
|
||||||
```
|
|
||||||
bmc_hub/
|
|
||||||
├── app/
|
|
||||||
│ ├── core/
|
|
||||||
│ │ ├── config.py # Konfiguration
|
|
||||||
│ │ └── database.py # PostgreSQL helpers
|
|
||||||
│ ├── models/
|
|
||||||
│ │ └── schemas.py # Pydantic models
|
|
||||||
│ ├── routers/
|
|
||||||
│ │ ├── customers.py # Customer CRUD
|
|
||||||
│ │ ├── hardware.py # Hardware management
|
|
||||||
│ │ └── billing.py # Billing endpoints
|
|
||||||
│ ├── services/
|
|
||||||
│ │ └── economic.py # e-conomic integration
|
|
||||||
│ └── jobs/
|
|
||||||
│ └── sync_job.py # Scheduled jobs
|
|
||||||
├── static/
|
|
||||||
│ └── index.html # Dashboard UI
|
|
||||||
├── migrations/ # Database migrations
|
|
||||||
├── docker-compose.yml # Local development
|
|
||||||
├── docker-compose.prod.yml # Production deployment
|
|
||||||
├── Dockerfile # Docker image
|
|
||||||
├── requirements.txt # Python dependencies
|
|
||||||
├── .env.example # Environment template (local)
|
|
||||||
├── .env.prod.example # Environment template (production)
|
|
||||||
└── main.py # FastAPI application
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔌 API Endpoints
|
|
||||||
|
|
||||||
- `GET /api/v1/customers` - List customers
|
|
||||||
- `GET /api/v1/hardware` - List hardware
|
|
||||||
- `GET /api/v1/billing/invoices` - List invoices
|
|
||||||
- `GET /health` - Health check
|
|
||||||
|
|
||||||
Se fuld dokumentation: http://localhost:8000/api/docs
|
|
||||||
|
|
||||||
## 🧪 Testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install test dependencies
|
|
||||||
pip install pytest pytest-cov
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
pytest
|
|
||||||
|
|
||||||
# Run with coverage
|
|
||||||
pytest --cov=app
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
## 📞 Support
|
|
||||||
|
|
||||||
- **Issues**: Gitea Issues
|
|
||||||
- **Dokumentation**: `/api/docs`
|
|
||||||
- **Email**: support@bmcnetworks.dk
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Made with ❤️ by BMC Networks**
|
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
"""BMC Hub Application Package"""
|
|
||||||
@ -1 +0,0 @@
|
|||||||
"""Core package"""
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
"""
|
|
||||||
Configuration Module
|
|
||||||
Handles environment variables and application settings
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
from typing import List
|
|
||||||
from pydantic_settings import BaseSettings
|
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
|
||||||
"""Application settings loaded from environment variables"""
|
|
||||||
|
|
||||||
# Database
|
|
||||||
DATABASE_URL: str = "postgresql://bmc_hub:bmc_hub@localhost:5432/bmc_hub"
|
|
||||||
|
|
||||||
# API
|
|
||||||
API_HOST: str = "0.0.0.0"
|
|
||||||
API_PORT: int = 8000
|
|
||||||
|
|
||||||
# Security
|
|
||||||
SECRET_KEY: str = "dev-secret-key-change-in-production"
|
|
||||||
ALLOWED_ORIGINS: List[str] = ["http://localhost:8000", "http://localhost:3000"]
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
LOG_LEVEL: str = "INFO"
|
|
||||||
LOG_FILE: str = "logs/app.log"
|
|
||||||
|
|
||||||
# e-conomic Integration
|
|
||||||
ECONOMIC_API_URL: str = "https://restapi.e-conomic.com"
|
|
||||||
ECONOMIC_APP_SECRET_TOKEN: str = ""
|
|
||||||
ECONOMIC_AGREEMENT_GRANT_TOKEN: str = ""
|
|
||||||
ECONOMIC_READ_ONLY: bool = True
|
|
||||||
ECONOMIC_DRY_RUN: bool = True
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
env_file = ".env"
|
|
||||||
case_sensitive = True
|
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
|
||||||
@ -1,73 +0,0 @@
|
|||||||
"""
|
|
||||||
Database Module
|
|
||||||
PostgreSQL connection and helpers using psycopg2
|
|
||||||
"""
|
|
||||||
|
|
||||||
import psycopg2
|
|
||||||
from psycopg2.extras import RealDictCursor
|
|
||||||
from psycopg2.pool import SimpleConnectionPool
|
|
||||||
from typing import Optional
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from app.core.config import settings
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Connection pool
|
|
||||||
connection_pool: Optional[SimpleConnectionPool] = None
|
|
||||||
|
|
||||||
|
|
||||||
def init_db():
|
|
||||||
"""Initialize database connection pool"""
|
|
||||||
global connection_pool
|
|
||||||
|
|
||||||
try:
|
|
||||||
connection_pool = SimpleConnectionPool(
|
|
||||||
minconn=1,
|
|
||||||
maxconn=10,
|
|
||||||
dsn=settings.DATABASE_URL
|
|
||||||
)
|
|
||||||
logger.info("✅ Database connection pool initialized")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"❌ Failed to initialize database: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def get_db_connection():
|
|
||||||
"""Get a connection from the pool"""
|
|
||||||
if connection_pool:
|
|
||||||
return connection_pool.getconn()
|
|
||||||
raise Exception("Database pool not initialized")
|
|
||||||
|
|
||||||
|
|
||||||
def release_db_connection(conn):
|
|
||||||
"""Return a connection to the pool"""
|
|
||||||
if connection_pool:
|
|
||||||
connection_pool.putconn(conn)
|
|
||||||
|
|
||||||
|
|
||||||
def get_db():
|
|
||||||
"""Context manager for database connections"""
|
|
||||||
conn = get_db_connection()
|
|
||||||
try:
|
|
||||||
yield conn
|
|
||||||
finally:
|
|
||||||
release_db_connection(conn)
|
|
||||||
|
|
||||||
|
|
||||||
def execute_query(query: str, params: tuple = None, fetch: bool = True):
|
|
||||||
"""Execute a SQL query and return results"""
|
|
||||||
conn = get_db_connection()
|
|
||||||
try:
|
|
||||||
with conn.cursor(cursor_factory=RealDictCursor) as cursor:
|
|
||||||
cursor.execute(query, params)
|
|
||||||
if fetch:
|
|
||||||
return cursor.fetchall()
|
|
||||||
conn.commit()
|
|
||||||
return cursor.rowcount
|
|
||||||
except Exception as e:
|
|
||||||
conn.rollback()
|
|
||||||
logger.error(f"Query error: {e}")
|
|
||||||
raise
|
|
||||||
finally:
|
|
||||||
release_db_connection(conn)
|
|
||||||
@ -1 +0,0 @@
|
|||||||
"""Models package"""
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
"""
|
|
||||||
Pydantic Models and Schemas
|
|
||||||
"""
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from typing import Optional
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
class CustomerBase(BaseModel):
|
|
||||||
"""Base customer schema"""
|
|
||||||
name: str
|
|
||||||
email: Optional[str] = None
|
|
||||||
phone: Optional[str] = None
|
|
||||||
address: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
class CustomerCreate(CustomerBase):
|
|
||||||
"""Schema for creating a customer"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Customer(CustomerBase):
|
|
||||||
"""Full customer schema"""
|
|
||||||
id: int
|
|
||||||
created_at: datetime
|
|
||||||
updated_at: Optional[datetime] = None
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
|
|
||||||
|
|
||||||
class HardwareBase(BaseModel):
|
|
||||||
"""Base hardware schema"""
|
|
||||||
serial_number: str
|
|
||||||
model: str
|
|
||||||
customer_id: int
|
|
||||||
|
|
||||||
|
|
||||||
class HardwareCreate(HardwareBase):
|
|
||||||
"""Schema for creating hardware"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Hardware(HardwareBase):
|
|
||||||
"""Full hardware schema"""
|
|
||||||
id: int
|
|
||||||
created_at: datetime
|
|
||||||
|
|
||||||
class Config:
|
|
||||||
from_attributes = True
|
|
||||||
@ -1 +0,0 @@
|
|||||||
"""Routers package"""
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
"""
|
|
||||||
Billing Router
|
|
||||||
API endpoints for billing operations
|
|
||||||
"""
|
|
||||||
|
|
||||||
from fastapi import APIRouter
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/billing/invoices")
|
|
||||||
async def list_invoices():
|
|
||||||
"""List all invoices"""
|
|
||||||
return {"message": "Billing integration coming soon"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/billing/sync")
|
|
||||||
async def sync_to_economic():
|
|
||||||
"""Sync data to e-conomic"""
|
|
||||||
return {"message": "e-conomic sync coming soon"}
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
"""
|
|
||||||
Customers Router
|
|
||||||
API endpoints for customer management
|
|
||||||
"""
|
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from app.models.schemas import Customer, CustomerCreate
|
|
||||||
from app.core.database import execute_query
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/customers", response_model=List[Customer])
|
|
||||||
async def list_customers():
|
|
||||||
"""List all customers"""
|
|
||||||
query = "SELECT * FROM customers ORDER BY created_at DESC"
|
|
||||||
customers = execute_query(query)
|
|
||||||
return customers
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/customers/{customer_id}", response_model=Customer)
|
|
||||||
async def get_customer(customer_id: int):
|
|
||||||
"""Get a specific customer"""
|
|
||||||
query = "SELECT * FROM customers WHERE id = %s"
|
|
||||||
customers = execute_query(query, (customer_id,))
|
|
||||||
|
|
||||||
if not customers:
|
|
||||||
raise HTTPException(status_code=404, detail="Customer not found")
|
|
||||||
|
|
||||||
return customers[0]
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/customers", response_model=Customer)
|
|
||||||
async def create_customer(customer: CustomerCreate):
|
|
||||||
"""Create a new customer"""
|
|
||||||
query = """
|
|
||||||
INSERT INTO customers (name, email, phone, address)
|
|
||||||
VALUES (%s, %s, %s, %s)
|
|
||||||
RETURNING *
|
|
||||||
"""
|
|
||||||
result = execute_query(
|
|
||||||
query,
|
|
||||||
(customer.name, customer.email, customer.phone, customer.address)
|
|
||||||
)
|
|
||||||
return result[0]
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
"""
|
|
||||||
Hardware Router
|
|
||||||
API endpoints for hardware management
|
|
||||||
"""
|
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from app.models.schemas import Hardware, HardwareCreate
|
|
||||||
from app.core.database import execute_query
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/hardware", response_model=List[Hardware])
|
|
||||||
async def list_hardware():
|
|
||||||
"""List all hardware"""
|
|
||||||
query = "SELECT * FROM hardware ORDER BY created_at DESC"
|
|
||||||
hardware = execute_query(query)
|
|
||||||
return hardware
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/hardware/{hardware_id}", response_model=Hardware)
|
|
||||||
async def get_hardware(hardware_id: int):
|
|
||||||
"""Get specific hardware"""
|
|
||||||
query = "SELECT * FROM hardware WHERE id = %s"
|
|
||||||
hardware = execute_query(query, (hardware_id,))
|
|
||||||
|
|
||||||
if not hardware:
|
|
||||||
raise HTTPException(status_code=404, detail="Hardware not found")
|
|
||||||
|
|
||||||
return hardware[0]
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/hardware", response_model=Hardware)
|
|
||||||
async def create_hardware(hardware: HardwareCreate):
|
|
||||||
"""Create new hardware entry"""
|
|
||||||
query = """
|
|
||||||
INSERT INTO hardware (serial_number, model, customer_id)
|
|
||||||
VALUES (%s, %s, %s)
|
|
||||||
RETURNING *
|
|
||||||
"""
|
|
||||||
result = execute_query(
|
|
||||||
query,
|
|
||||||
(hardware.serial_number, hardware.model, hardware.customer_id)
|
|
||||||
)
|
|
||||||
return result[0]
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
"""
|
|
||||||
System Router
|
|
||||||
Health checks and system information
|
|
||||||
"""
|
|
||||||
|
|
||||||
from fastapi import APIRouter
|
|
||||||
from app.core.config import settings
|
|
||||||
from app.core.database import execute_query
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/system/health")
|
|
||||||
async def health_check():
|
|
||||||
"""Comprehensive health check"""
|
|
||||||
try:
|
|
||||||
# Test database connection
|
|
||||||
result = execute_query("SELECT 1 as test")
|
|
||||||
db_status = "healthy" if result else "unhealthy"
|
|
||||||
except Exception as e:
|
|
||||||
db_status = f"unhealthy: {str(e)}"
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "healthy",
|
|
||||||
"service": "BMC Hub",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"database": db_status,
|
|
||||||
"config": {
|
|
||||||
"environment": "production" if not settings.ECONOMIC_DRY_RUN else "development",
|
|
||||||
"economic_read_only": settings.ECONOMIC_READ_ONLY,
|
|
||||||
"economic_dry_run": settings.ECONOMIC_DRY_RUN
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/system/config")
|
|
||||||
async def get_config():
|
|
||||||
"""Get system configuration (non-sensitive)"""
|
|
||||||
return {
|
|
||||||
"api_host": settings.API_HOST,
|
|
||||||
"api_port": settings.API_PORT,
|
|
||||||
"log_level": settings.LOG_LEVEL,
|
|
||||||
"economic_enabled": bool(settings.ECONOMIC_APP_SECRET_TOKEN),
|
|
||||||
"economic_read_only": settings.ECONOMIC_READ_ONLY,
|
|
||||||
"economic_dry_run": settings.ECONOMIC_DRY_RUN
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
# Data directory
|
|
||||||
@ -1,72 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
# PostgreSQL Database
|
|
||||||
postgres:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
container_name: bmc-hub-postgres-prod
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: ${POSTGRES_USER}
|
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
||||||
POSTGRES_DB: ${POSTGRES_DB}
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
- ./migrations/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
|
||||||
ports:
|
|
||||||
- "${POSTGRES_PORT:-5432}:5432"
|
|
||||||
restart: always
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
networks:
|
|
||||||
- bmc-hub-network
|
|
||||||
|
|
||||||
# FastAPI Application - Production with GitHub Release Version
|
|
||||||
api:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
args:
|
|
||||||
RELEASE_VERSION: ${RELEASE_VERSION:-latest}
|
|
||||||
GITHUB_TOKEN: ${GITHUB_TOKEN}
|
|
||||||
GITHUB_REPO: ${GITHUB_REPO:-ct/bmc_hub}
|
|
||||||
image: bmc-hub:${RELEASE_VERSION:-latest}
|
|
||||||
container_name: bmc-hub-api-prod
|
|
||||||
depends_on:
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
ports:
|
|
||||||
- "${API_PORT:-8000}:8000"
|
|
||||||
volumes:
|
|
||||||
- ./logs:/app/logs
|
|
||||||
- ./uploads:/app/uploads
|
|
||||||
- ./data:/app/data
|
|
||||||
# NOTE: No source code mount in production - code comes from GitHub release
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
# Override database URL to point to postgres service
|
|
||||||
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
|
|
||||||
- ENABLE_RELOAD=false
|
|
||||||
restart: always
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
networks:
|
|
||||||
- bmc-hub-network
|
|
||||||
labels:
|
|
||||||
- "com.bmcnetworks.app=bmc-hub"
|
|
||||||
- "com.bmcnetworks.version=${RELEASE_VERSION:-latest}"
|
|
||||||
|
|
||||||
networks:
|
|
||||||
bmc-hub-network:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
driver: local
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
# PostgreSQL Database
|
|
||||||
postgres:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
container_name: bmc-hub-postgres
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-bmc_hub}
|
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-bmc_hub}
|
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-bmc_hub}
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
- ./migrations/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
|
|
||||||
ports:
|
|
||||||
- "${POSTGRES_PORT:-5432}:5432"
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-bmc_hub}"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
networks:
|
|
||||||
- bmc-hub-network
|
|
||||||
|
|
||||||
# FastAPI Application
|
|
||||||
api:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: bmc-hub-api
|
|
||||||
depends_on:
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
ports:
|
|
||||||
- "${API_PORT:-8000}:8000"
|
|
||||||
volumes:
|
|
||||||
- ./logs:/app/logs
|
|
||||||
- ./uploads:/app/uploads
|
|
||||||
- ./static:/app/static
|
|
||||||
- ./data:/app/data
|
|
||||||
# Mount for local development - live code reload
|
|
||||||
- ./app:/app/app:ro
|
|
||||||
- ./main.py:/app/main.py:ro
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
# Override database URL to point to postgres service
|
|
||||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-bmc_hub}:${POSTGRES_PASSWORD:-bmc_hub}@postgres:5432/${POSTGRES_DB:-bmc_hub}
|
|
||||||
- ENABLE_RELOAD=true
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
networks:
|
|
||||||
- bmc-hub-network
|
|
||||||
|
|
||||||
networks:
|
|
||||||
bmc-hub-network:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
driver: local
|
|
||||||
@ -1 +0,0 @@
|
|||||||
# Logs directory
|
|
||||||
122
main.py
122
main.py
@ -1,122 +0,0 @@
|
|||||||
"""
|
|
||||||
BMC Hub - FastAPI Application
|
|
||||||
Main application entry point
|
|
||||||
"""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from fastapi import FastAPI
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
from fastapi.responses import RedirectResponse
|
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
|
|
||||||
from app.core.config import settings
|
|
||||||
from app.core.database import init_db
|
|
||||||
from app.routers import (
|
|
||||||
customers,
|
|
||||||
hardware,
|
|
||||||
billing,
|
|
||||||
system,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Configure logging
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
||||||
handlers=[
|
|
||||||
logging.StreamHandler(),
|
|
||||||
logging.FileHandler('logs/app.log')
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
@asynccontextmanager
|
|
||||||
async def lifespan(app: FastAPI):
|
|
||||||
"""Lifecycle management - startup and shutdown"""
|
|
||||||
# Startup
|
|
||||||
logger.info("🚀 Starting BMC Hub...")
|
|
||||||
logger.info(f"Database: {settings.DATABASE_URL}")
|
|
||||||
|
|
||||||
init_db()
|
|
||||||
|
|
||||||
logger.info("✅ System initialized successfully")
|
|
||||||
yield
|
|
||||||
# Shutdown
|
|
||||||
logger.info("👋 Shutting down...")
|
|
||||||
|
|
||||||
# Create FastAPI app
|
|
||||||
app = FastAPI(
|
|
||||||
title="BMC Hub API",
|
|
||||||
description="""
|
|
||||||
Central management system for BMC Networks.
|
|
||||||
|
|
||||||
**Key Features:**
|
|
||||||
- Customer management
|
|
||||||
- Hardware tracking
|
|
||||||
- Service management
|
|
||||||
- Billing integration
|
|
||||||
""",
|
|
||||||
version="1.0.0",
|
|
||||||
lifespan=lifespan,
|
|
||||||
docs_url="/api/docs",
|
|
||||||
redoc_url="/api/redoc",
|
|
||||||
openapi_url="/api/openapi.json"
|
|
||||||
)
|
|
||||||
|
|
||||||
# CORS middleware
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=settings.ALLOWED_ORIGINS,
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Include routers
|
|
||||||
app.include_router(customers.router, prefix="/api/v1", tags=["Customers"])
|
|
||||||
app.include_router(hardware.router, prefix="/api/v1", tags=["Hardware"])
|
|
||||||
app.include_router(billing.router, prefix="/api/v1", tags=["Billing"])
|
|
||||||
app.include_router(system.router, prefix="/api/v1", tags=["System"])
|
|
||||||
|
|
||||||
# Serve static files (UI)
|
|
||||||
app.mount("/static", StaticFiles(directory="static", html=True), name="static")
|
|
||||||
|
|
||||||
@app.get("/")
|
|
||||||
async def root():
|
|
||||||
"""Redirect to dashboard"""
|
|
||||||
return RedirectResponse(url="/static/index.html")
|
|
||||||
|
|
||||||
@app.get("/health")
|
|
||||||
async def health_check():
|
|
||||||
"""Health check endpoint"""
|
|
||||||
return {
|
|
||||||
"status": "healthy",
|
|
||||||
"service": "BMC Hub",
|
|
||||||
"version": "1.0.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
import uvicorn
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Only enable reload in local development (not in Docker)
|
|
||||||
enable_reload = os.getenv("ENABLE_RELOAD", "false").lower() == "true"
|
|
||||||
|
|
||||||
if enable_reload:
|
|
||||||
uvicorn.run(
|
|
||||||
"main:app",
|
|
||||||
host="0.0.0.0",
|
|
||||||
port=8000,
|
|
||||||
reload=True,
|
|
||||||
reload_includes=["*.py"],
|
|
||||||
reload_dirs=["app"],
|
|
||||||
log_level="info"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
uvicorn.run(
|
|
||||||
"main:app",
|
|
||||||
host="0.0.0.0",
|
|
||||||
port=8000,
|
|
||||||
reload=False
|
|
||||||
)
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
-- BMC Hub Database Schema
|
|
||||||
-- PostgreSQL 16
|
|
||||||
|
|
||||||
-- Customers table
|
|
||||||
CREATE TABLE IF NOT EXISTS customers (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
email VARCHAR(255),
|
|
||||||
phone VARCHAR(50),
|
|
||||||
address TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP,
|
|
||||||
deleted_at TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Hardware table
|
|
||||||
CREATE TABLE IF NOT EXISTS hardware (
|
|
||||||
id SERIAL PRIMARY KEY,
|
|
||||||
serial_number VARCHAR(255) NOT NULL UNIQUE,
|
|
||||||
model VARCHAR(255) NOT NULL,
|
|
||||||
customer_id INTEGER REFERENCES customers(id),
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
deleted_at TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Create indexes
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_customers_email ON customers(email);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_hardware_customer ON hardware(customer_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_hardware_serial ON hardware(serial_number);
|
|
||||||
|
|
||||||
-- Insert sample data
|
|
||||||
INSERT INTO customers (name, email, phone, address) VALUES
|
|
||||||
('BMC Networks', 'info@bmcnetworks.dk', '+45 12345678', 'København'),
|
|
||||||
('Test Customer', 'test@example.com', '+45 87654321', 'Aarhus')
|
|
||||||
ON CONFLICT DO NOTHING;
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
fastapi==0.109.0
|
|
||||||
uvicorn[standard]==0.27.0
|
|
||||||
psycopg2-binary==2.9.9
|
|
||||||
pydantic==2.5.3
|
|
||||||
pydantic-settings==2.1.0
|
|
||||||
python-dotenv==1.0.0
|
|
||||||
python-multipart==0.0.6
|
|
||||||
@ -1,70 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="da">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>BMC Hub</title>
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<nav class="navbar navbar-dark bg-primary">
|
|
||||||
<div class="container-fluid">
|
|
||||||
<span class="navbar-brand mb-0 h1">🚀 BMC Hub</span>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="container mt-4">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<h1>Velkommen til BMC Hub</h1>
|
|
||||||
<p class="lead">Central management system for BMC Networks</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mt-4">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">👥 Customers</h5>
|
|
||||||
<p class="card-text">Manage customer database</p>
|
|
||||||
<a href="/api/v1/customers" class="btn btn-primary">View API</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">🖥️ Hardware</h5>
|
|
||||||
<p class="card-text">Track customer hardware</p>
|
|
||||||
<a href="/api/v1/hardware" class="btn btn-primary">View API</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">📊 System</h5>
|
|
||||||
<p class="card-text">Health and configuration</p>
|
|
||||||
<a href="/api/v1/system/health" class="btn btn-primary">Health Check</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mt-4">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">📖 API Documentation</h5>
|
|
||||||
<p class="card-text">Explore the complete API documentation</p>
|
|
||||||
<a href="/api/docs" class="btn btn-success">OpenAPI Docs</a>
|
|
||||||
<a href="/api/redoc" class="btn btn-secondary">ReDoc</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
# Uploads directory
|
|
||||||
Loading…
Reference in New Issue
Block a user