MechBay is a Flask web application for managing BattleTech miniature inventories and organizing forces. It provides tools to track individual miniatures, organize them into forces and lances, and use reusable lance templates for quick force building. Focus is on physical miniature management rather than spicific mech versions that would be used in gameplay.
- Miniatures: Individual models with properties like chassis, series ID, paint status, and storage location
- Forces: Collections of lances representing your army composition
- Lance Templates: Reusable configurations of 4 mechs that can auto-match miniatures from inventory
- Lances: Groups of miniatures within a force, with drag-and-drop ordering
- Backend: Flask 3.1+ with SQLAlchemy 2.0+ ORM
- Database: SQLite (default location:
%APPDATA%\MechBay\mechbay.db) - Server: Waitress WSGI server (production)
- Frontend: Bootstrap 5, FontAwesome, SortableJS for drag-and-drop
- Package Manager: UV (modern Python dependency manager)
- Python: 3.13+ required
Current: Windows-only (database defaults to %APPDATA%\MechBay)
Future: Cross-platform support would require using platformdirs library or conditional logic for database paths:
- Linux:
~/.local/share/mechbay/ - macOS:
~/Library/Application Support/MechBay/
- Python 3.13 or higher
- UV package manager (install guide)
- Git
# Clone repository
git clone https://github.com/cantis/MechBay.git
cd MechBay
# Install dependencies
uv sync
# Create database and tables
uv run python -m app.migrations
# (Optional) Load demo data
uv run python -m app.seed
# Run development server
uv run python main.pyThe server will start on http://127.0.0.1:5001 and automatically open in your browser.
Create a .env file in the project root to customize settings:
SECRET_KEY=your-secret-key-here
DATABASE_URL=sqlite:///custom/path/to/database.db
DEBUG=trueSee .env.example for all available options.
Critical Pattern: Always use the session_scope() context manager from app/extensions.py for database operations.
from ..extensions import session_scope
def get_force_by_id(force_id: int) -> Force | None:
with session_scope() as session:
force = session.get(Force, force_id)
if force:
# Eager load relationships within session
for lance in force.lances:
_ = lance.miniatures
session.expunge(force) # Critical: make accessible outside session
return forceKey Rules:
- Always
session.expunge()objects before returning from service functions to preventDetachedInstanceError - Service layer (
app/services/) handles all business logic and DB transactions - Blueprints (
app/blueprints/) are thin controllers that call services
Routes support both JSON (AJAX) and traditional form submissions:
@bp.route("/<int:id>/add-miniature", methods=["POST"])
def add_miniature(id: int):
is_json = request.is_json
data = request.get_json(silent=True) or request.form
# ... business logic ...
if is_json:
return jsonify({"success": True}), 200
else:
flash("Miniature added to lance", "success")
return redirect(url_for("miniatures.list_miniatures"))Important: Set flash messages BEFORE the if is_json check when using AJAX + page reload pattern. JavaScript uses setTimeout(() => location.reload(), 100) to allow flash messages to persist.
Cannot call .delete() on queries with joins. Use this pattern:
# ❌ Fails with joins
session.query(ForceMiniature).join(Lance).filter(...).delete()
# ✅ Correct pattern
records = session.query(ForceMiniature).join(Lance).filter(...).all()
for record in records:
session.delete(record)MechBay/
├── app/
│ ├── __init__.py # Flask app factory, routes, session restore
│ ├── config.py # Configuration classes
│ ├── extensions.py # SQLAlchemy setup and session_scope()
│ ├── migrations.py # Database schema creation
│ ├── seed.py # Demo data population
│ ├── blueprints/ # Route controllers
│ │ ├── forces.py
│ │ ├── lance_templates.py
│ │ └── miniatures.py
│ ├── models/ # SQLAlchemy models
│ │ ├── force.py
│ │ ├── lance.py
│ │ ├── miniature.py
│ │ ├── force_miniature.py
│ │ ├── lance_template.py
│ │ └── lance_template_miniature.py
│ ├── services/ # Business logic layer
│ │ ├── force_service.py
│ │ ├── lance_template_service.py
│ │ └── miniature_service.py
│ ├── templates/ # Jinja2 HTML templates
│ │ ├── base.html
│ │ ├── navbar.html
│ │ ├── about.html
│ │ ├── forces/
│ │ ├── lance_templates/
│ │ └── miniatures/
│ └── static/ # CSS and JavaScript (mostly CDN-based)
├── tests/ # Pytest test suite
│ ├── conftest.py # Test fixtures (in-memory DB)
│ └── test_miniatures.py
├── Docs/ # Documentation
├── main.py # Application entry point
├── mechbay.spec # PyInstaller configuration
├── build.ps1 # Build and packaging script
├── pyproject.toml # Project metadata and dependencies
└── .env.example # Environment variable template
uv run python -m app.migrationsCreates all tables using SQLAlchemy Base.metadata.create_all(). Also runs automatically on first app startup via init_db() in app/__init__.py.
Note: No Alembic migrations - uses simple "create if not exists" pattern. Schema changes require manual ALTER statements or DB recreation.
uv run python -m app.seedPopulates database with:
- 2 sample miniatures (Warhammer WHM and Banshee BNC)
- 6 lance templates (Command, Assault, Heavy, Fire Support, Recon, Battle)
Use File → Load sample data… in the app, or uv run python -m app.seed from the command line. The app does not auto-seed on startup; an empty first launch shows the inventory empty-state prompt instead.
Force→ manyLance(cascade delete)Lance→ manyForceMiniature(join table with position ordering)ForceMiniature→ oneMiniature(reference, not cascade)LanceTemplate→ manyLanceTemplateMiniature(chassis patterns)
Active Force: Only one force can be is_active=True at a time. Used for quick miniature assignment from inventory screen.
Document files replace the early JSON export pages:
.mechbay— full inventory project (miniatures, lance templates, settings).mbforce— force document (schema v2)- Jeff's BT Tools — external export format only
Open inventory accepts legacy miniature-only or template-only JSON exports.
Service-layer _upgrade_miniature_schema in miniature_service supports legacy JSON normalization for inventory open.
# Run all tests
uv run pytest
# Run with verbose output
uv run pytest -v
# Run specific test file
uv run pytest tests/test_miniatures.py
# Run with coverage
uv run pytest --cov=appTests use in-memory SQLite database via conftest.py fixture:
@pytest.fixture(scope="function")
def app():
"""Create test app with in-memory database."""
return create_app({"DATABASE_URL": "sqlite+pysqlite:///:memory:"})Each test gets a fresh database, ensuring test isolation.
# Install/update dependencies
uv sync
# Run development server
uv run python main.py# Run automated build script (bumps version, builds, packages)
.\build.ps1Build script automates:
- Bump patch version in
pyproject.tomlviauv version --bump patch - Extract new version number
- Sync dependencies with
uv sync - Build executable with PyInstaller using
mechbay.spec - Create distribution folder
dist/MechBay_vX.Y.Z/with:- Executable folder
.env.examplefileREADME.txtwith user instructions
- Create ZIP archive
dist/MechBay_vX.Y.Z.zipfor distribution
The mechbay.spec file configures:
- Entry point:
main.py - Data files:
app/templates/directory bundled - Hidden imports: SQLAlchemy, Waitress, Flask, dotenv
- Build mode: Single-folder distribution (COLLECT)
- Console: Enabled (shows startup messages and logs)
- Compression: UPX enabled
# Bump version manually
uv version --bump patch # or 'minor' or 'major'
# Build executable
uv run pyinstaller mechbay.spec --clean
# Executable located in: dist/MechBay/MechBay.exeMechBay uses GitHub Actions for automated testing, building, and releases.
Triggers: Push to main or PackageForDistro branches, or pull requests to main
Actions:
- ✅ Runs all pytest tests
- ✅ Runs Ruff linter
- ✅ Verifies app initialization
- ❌ Fails build if any checks fail
This ensures code quality and prevents regressions from being merged.
Triggers: Manually creating a GitHub release
Actions:
- Sets up Python 3.13 and UV environment
- Installs dependencies and PyInstaller
- Extracts version from git tag
- Builds Windows executable with PyInstaller
- Creates distribution folder with executable,
.env.example, and user README - Creates ZIP archive
MechBay_vX.Y.Z-windows.zip - Uploads ZIP to GitHub release as downloadable asset
- Stores build artifacts for 30 days
Recommended workflow:
# Bump version locally
uv version --bump patch # or 'minor' or 'major'
# Commit version change
git add pyproject.toml
git commit -m "Bump version to $(Select-String -Path pyproject.toml -Pattern 'version = ""(.+)""' | ForEach-Object { $_.Matches.Groups[1].Value })"
git push
# Create release on GitHub.com:
# 1. Go to Releases → Draft a new release
# 2. Create tag: v0.1.1 (matching pyproject.toml version)
# 3. Set title: Release v0.1.1
# 4. Add release notes describing changes
# 5. Click "Publish release"
# GitHub Actions automatically builds and attaches Windows ZIPAfter release is published with attached ZIP:
- Share GitHub release URL with users (e.g., your brothers)
- They download
MechBay_vX.Y.Z-windows.zipfrom release page - Extract and run
MechBay.exe - Application runs with no additional setup required
- Actions Tab: View workflow runs and build logs
- Releases Page: See all published releases with downloadable assets
- Build Artifacts: Access 30-day stored builds even without creating releases
Tests fail on push:
- Check Actions tab for detailed error logs
- Run
uv run pytest -vlocally to reproduce - Fix issues before merging to main
Release build fails:
- Verify PyInstaller spec is correct
- Check that all dependencies are in
pyproject.toml - Test local build with
uv run pyinstaller mechbay.spec
# Check code style
uv run ruff check .
# Auto-fix issues
uv run ruff check --fix .- Follow PEP 8 conventions
- Use type hints for function signatures
- Prefer f-strings over
.format()or%formatting - Use
from __future__ import annotationsfor forward references - Service functions should be pure and stateless
- Always use
session_scope()context manager for DB access
Forces detail page uses SortableJS for miniature reordering between lances:
- User drags miniature to new position
- UI updates immediately (optimistic)
- POST request to backend with new positions
- Backend validates and saves
- On error, page reloads to show correct state
Double-click elements with .editable-lance-name class to edit inline:
- Uses JavaScript
prompt()for input - Fetch API to save changes
- Updates DOM on success without reload
Flash messages auto-dismiss after 3 seconds using Bootstrap Alert component (see base.html). Includes close button with alert-dismissible fade show classes.
GET /health
Returns JSON with app status and version:
{
"status": "ok",
"version": "0.1.0"
}/- Redirects to miniatures list/about- About page with version info/miniatures- List all miniatures (search, sort, filter)/miniatures/add- Add new miniature/miniatures/<id>/edit- Edit miniature/miniatures/<id>/delete- Delete miniature/forces- List all forces/forces/<id>- Force detail with drag-drop lance management/lance_templates- List all lance templates/lance_templates/<id>- Template detail/files/inventory/save- Save linked inventory project/files/inventory/save-as- Save inventory as.mechbay/files/inventory/open- Open inventory project/files/inventory/sample-data- Load sample miniatures and templates/files/force/<id>/save- Save linked force file/files/force/<id>/save-as- Save force as.mbforce/files/force/open- Open force file (adds to library)/forces/<id>/export/jeff- Jeff's BT Tools export (zip)/forces/<id>/lances/<lance_id>/export/jeff- Single-lance Jeff export
Current limitation: Database path uses Windows-specific AppData/Roaming.
Solution options:
-
Use
platformdirslibrary (recommended):from platformdirs import user_data_dir db_dir = Path(user_data_dir("MechBay", "YourOrg"))
-
Manual OS detection:
import sys if sys.platform == "win32": db_dir = Path.home() / "AppData" / "Roaming" / "MechBay" elif sys.platform == "darwin": db_dir = Path.home() / "Library" / "Application Support" / "MechBay" else: # Linux db_dir = Path.home() / ".local" / "share" / "mechbay"
Current: Waitress WSGI server (Windows-focused)
Alternatives:
- Gunicorn: Industry standard, but Linux/macOS only
- Docker: Containerized deployment, cross-platform
- Cloud: Deploy to Heroku, Railway, Render, etc.
Current: Simple create_all() approach, no versioned migrations.
Future: Consider Alembic for:
- Schema versioning
- Automated migration generation
- Safe schema upgrades without data loss
Current focus: Physical miniature inventory management only.
Out of scope:
- Pilot skills and experience
- Special abilities or equipment loadouts
- Damage tracking
- Campaign management
- Force balancing by BV (Battle Value)
This is intentional - MechBay focuses on the "what do I own and where is it?" problem, not recreating MegaMek or other game management tools.
- Batch operations: Edit/delete multiple miniatures at once
- Advanced search: Filter by multiple criteria simultaneously
- Photos: Upload images of painted miniatures
- Tags: Custom labels/categories beyond series and status
- Reports: Inventory statistics, paint progress tracking
- Backup automation: Scheduled database backups
- Multi-user: User accounts and shared inventories (requires authentication)
This is currently a personal project, but contributions are welcome! When contributing:
- Follow existing code patterns (especially session management)
- Add tests for new features
- Run linter before committing:
uv run ruff check . - Update this documentation for architecture changes
- Test both JSON API and form submission modes for new routes
- Repository: https://github.com/cantis/MechBay
- Issues: Use GitHub Issues for bug reports and feature requests
- Documentation: Check
Docs/folder and inline code comments
MechBay is open source software licensed under the MIT License. See the LICENSE file for details.
Last updated: November 2025