|
1 | 1 | # MechBay Copilot Instructions |
2 | 2 |
|
3 | | -## Project Overview |
| 3 | +This repository uses scoped instruction files under `.github/instructions/`. |
| 4 | +Follow those files when their `applyTo` patterns match the files being changed. |
| 5 | + |
| 6 | +## Important project references |
| 7 | + |
| 8 | +- Read `docs/ARCHITECTURE.md` before changing service boundaries, session handling, route patterns, or model relationships. |
| 9 | +- Read `docs/TESTING.md` before adding or changing tests. |
| 10 | +- Read `docs/SECURITY.md` before touching CSRF configuration, file uploads, input validation, logging, or secret key handling. |
| 11 | +- Read `docs/DEVELOPMENT.md` for local setup, migration commands, Docker workflow, and environment variables. |
| 12 | +- Read `docs/DESIGN.md` before changing colors, typography, spacing, or component visual styling (machine-readable design token spec). |
| 13 | +- Read `docs/UI-CONVENTIONS.md` before changing templates, Jinja structure, flash messages, icons, or JavaScript interaction patterns. |
| 14 | + |
| 15 | +## Overview |
| 16 | + |
4 | 17 | Flask web app for managing BattleTech miniature inventories and organizing forces. Three main entities: **Miniatures** (individual models), **Forces** (collections of lances), and **Lance Templates** (reusable lance configurations). |
5 | 18 |
|
| 19 | +- **Backend**: Flask 3.1+, SQLAlchemy 2.0+, SQLite, Waitress (production) |
| 20 | +- **Frontend**: Bootstrap 5.3, Font Awesome 6.4, SortableJS — all via CDN; server-rendered Jinja2 |
| 21 | +- **Package manager**: `uv` — use PowerShell for all commands |
| 22 | +- **Python**: 3.13+ required |
| 23 | + |
| 24 | +## Directory Layout |
| 25 | + |
| 26 | +``` |
| 27 | +app/ |
| 28 | + __init__.py # Application factory: create_app() |
| 29 | + config.py # Config class; reads from environment / .env |
| 30 | + extensions.py # SQLAlchemy setup and session_scope() |
| 31 | + blueprints/ # Thin route controllers (one file per area) |
| 32 | + models/ # SQLAlchemy models (one file per model) |
| 33 | + services/ # Business logic (one file per domain) |
| 34 | + templates/ # Jinja2 templates; base.html + per-area folders |
| 35 | + static/ # Custom CSS and JS; libraries via CDN |
| 36 | +tests/ # pytest; conftest.py has all shared fixtures |
| 37 | +docs/ # Architecture, development, design, security docs |
| 38 | +main.py # Dev entry point (debug=True, port 5001) |
| 39 | +server.py # Production entry point (Waitress) |
| 40 | +Dockerfile # Docker image |
| 41 | +``` |
| 42 | + |
| 43 | +## Blueprint / Route Structure |
| 44 | + |
| 45 | +| Blueprint | Prefix | File | |
| 46 | +|-------------------|--------------------|-------------------------------------| |
| 47 | +| `miniatures` | `/miniatures` | `app/blueprints/miniatures.py` | |
| 48 | +| `forces` | `/forces` | `app/blueprints/forces.py` | |
| 49 | +| `lance_templates` | `/lance-templates` | `app/blueprints/lance_templates.py` | |
| 50 | +| *(root)* | `/` | registered in `app/__init__.py` | |
| 51 | + |
6 | 52 | ## Architecture Patterns |
7 | 53 |
|
8 | 54 | ### Session Management |
9 | | -- Use `session_scope()` context manager from `app/extensions.py` for all DB operations |
10 | | -- Always `session.expunge()` objects before returning from service functions to prevent detached instance errors |
11 | | -- Service layer (`app/services/`) handles all business logic and DB transactions |
12 | | -- Blueprints (`app/blueprints/`) are thin controllers that call services |
13 | 55 |
|
14 | | -Example: |
| 56 | +Always use `session_scope()` from `app/extensions.py`. Always `session.expunge()` objects before returning from service functions. |
| 57 | + |
15 | 58 | ```python |
16 | 59 | from ..extensions import session_scope |
17 | 60 |
|
18 | 61 | def get_force_by_id(force_id: int) -> Force | None: |
19 | 62 | with session_scope() as session: |
20 | 63 | force = session.get(Force, force_id) |
21 | 64 | if force: |
22 | | - # Eager load relationships within session |
23 | 65 | for lance in force.lances: |
24 | 66 | _ = lance.miniatures |
25 | | - session.expunge(force) # Critical: make accessible outside session |
| 67 | + session.expunge(force) # Critical: prevents DetachedInstanceError |
26 | 68 | return force |
27 | 69 | ``` |
28 | 70 |
|
29 | 71 | ### Dual-Mode Routes (JSON + Form) |
30 | | -Routes support both JSON (AJAX) and form submissions. Detect request type and respond appropriately: |
| 72 | + |
| 73 | +Routes support both JSON (AJAX) and form submissions. Standard JSON envelope: |
31 | 74 |
|
32 | 75 | ```python |
33 | | -@bp.route("/<int:id>/add-miniature", methods=["POST"]) |
34 | | -def add_miniature(id: int): |
35 | | - is_json = request.is_json |
36 | | - data = request.get_json(silent=True) or request.form # silent=True prevents Content-Type errors |
37 | | - |
38 | | - # ... business logic ... |
39 | | - |
40 | | - if is_json: |
41 | | - return jsonify({"success": True}), 200 |
42 | | - else: |
43 | | - flash("Miniature added to lance", "success") |
44 | | - return redirect(url_for("miniatures.list_miniatures")) |
| 76 | +{"success": bool, "error": str | None, "data": dict | None} |
45 | 77 | ``` |
46 | 78 |
|
47 | | -**Key**: Set flash messages BEFORE the `if is_json` check when using AJAX + page reload (see `forces.remove_miniature`). JavaScript uses `setTimeout(() => location.reload(), 100)` to allow flash message to persist. |
| 79 | +HTTP status codes: `200` success · `400` bad input · `404` not found · `409` conflict |
| 80 | + |
| 81 | +Rules: |
| 82 | +1. Validate `int()` conversions in `try/except` at route level — return 400 on failure |
| 83 | +2. Set flash messages **before** the `if is_json` check — JS uses `setTimeout(() => location.reload(), 100)` |
| 84 | +3. Use `request.get_json(silent=True) or request.form` — `silent=True` prevents Content-Type errors |
| 85 | +4. Always include `"success"` key in every JSON response |
48 | 86 |
|
49 | 87 | ### SQLAlchemy Delete Pattern |
50 | | -Cannot call `.delete()` on queries with joins. Use this pattern instead: |
51 | 88 |
|
52 | 89 | ```python |
53 | 90 | # ❌ Fails with joins |
54 | 91 | session.query(ForceMiniature).join(Lance).filter(...).delete() |
55 | 92 |
|
56 | | -# ✅ Correct pattern |
| 93 | +# ✅ Correct |
57 | 94 | records = session.query(ForceMiniature).join(Lance).filter(...).all() |
58 | 95 | for record in records: |
59 | 96 | session.delete(record) |
60 | 97 | ``` |
61 | 98 |
|
62 | | -## File Naming Conventions |
63 | | -- Export files use timestamp format: `{EntityType}_YYYYMMDD_HHMMSS.json` |
64 | | -- Example: `Miniature_Inventory_20251120_143025.json` |
65 | | -- Generate with: `datetime.now().strftime("%Y%m%d_%H%M%S")` |
66 | | - |
67 | | -## Frontend Patterns |
| 99 | +## Key Model Relationships |
68 | 100 |
|
69 | | -### SortableJS Drag-and-Drop |
70 | | -Forces page uses SortableJS for miniature reordering between lances. On drop: |
71 | | -1. Update UI immediately (optimistic) |
72 | | -2. POST new positions to backend |
73 | | -3. Backend validates and saves |
| 101 | +- `Force` → many `Lance` (cascade delete) |
| 102 | +- `Lance` → many `ForceMiniature` (join table with position ordering) |
| 103 | +- `ForceMiniature` → one `Miniature` (reference, not cascade) |
| 104 | +- `LanceTemplate` → many `LanceTemplateMiniature` (chassis patterns for auto-matching) |
74 | 105 |
|
75 | | -### Auto-Fading Flash Messages |
76 | | -Flash messages auto-dismiss after 3 seconds using Bootstrap Alert component (see `app/templates/base.html`). Include close button with `alert-dismissible fade show` classes. |
| 106 | +**Active Force**: Only one force can be `is_active=True` at a time. |
77 | 107 |
|
78 | | -### Editable Elements |
79 | | -Double-click elements with `.editable-lance-name` class to edit inline. Use `prompt()` for input, fetch API to save, update DOM on success. |
| 108 | +**Miniature naming**: Chassis (`"Warhammer"`), Prefix (`"WHM"`), Type (`"WHM-6R"`). Template matching uses chassis name substring. |
80 | 109 |
|
81 | 110 | ## Development Workflow |
82 | 111 |
|
83 | | -### Running the App |
84 | | -```powershell |
85 | | -uv sync # Install dependencies |
86 | | -uv run python .\main.py # Start dev server (debug=True) |
87 | | -``` |
88 | | - |
89 | | -### Database Operations |
90 | | -```powershell |
91 | | -uv run python -m app.migrations # Create/update schema |
92 | | -uv run python -m app.seed # Populate sample data (6 lance templates) |
93 | | -``` |
94 | | - |
95 | | -### Testing |
96 | 112 | ```powershell |
97 | | -uv run pytest -q # Run tests (uses in-memory SQLite) |
98 | | -uv run ruff check . # Lint |
| 113 | +uv sync # Install dependencies |
| 114 | +uv run python .\main.py # Start dev server (port 5001) |
| 115 | +uv run python -m app.migrations # Create/update schema |
| 116 | +uv run python -m app.seed # Load demo data |
| 117 | +uv run pytest -q # Run tests |
| 118 | +uv run ruff check . # Lint |
99 | 119 | ``` |
100 | 120 |
|
101 | | -Tests use `conftest.py` fixture that creates fresh in-memory DB per test via `create_app({"DATABASE_URL": "sqlite+pysqlite:///:memory:"})`. |
102 | | - |
103 | | -## Key Relationships |
104 | | -- `Force` → many `Lance` (cascade delete) |
105 | | -- `Lance` → many `ForceMiniature` (join table with position ordering) |
106 | | -- `ForceMiniature` → one `Miniature` (reference, not cascade) |
107 | | -- `LanceTemplate` → many `LanceTemplateMiniature` (chassis patterns for auto-matching) |
| 121 | +Tests use in-memory SQLite via `create_app({"DATABASE_URL": "sqlite+pysqlite:///:memory:"})`. |
108 | 122 |
|
109 | | -**Active Force**: Only one force can be `is_active=True` at a time. Used for quick miniature assignment from inventory screen. |
| 123 | +## Key Environment Variables |
110 | 124 |
|
111 | | -### Miniature Naming Convention |
112 | | -- **Chassis**: Base model name (e.g., "Warhammer") |
113 | | -- **Prefix**: Chassis designator/acronym (e.g., "WHM") |
114 | | -- **Type**: Variant code (e.g., "WHM-6R", "WHM-7M") |
| 125 | +See `docs/DEVELOPMENT.md` for the full table. |
115 | 126 |
|
116 | | -Lance template matching uses chassis name - "Warhammer" pattern matches both "Warhammer WHM-6R" and "Warhammer WHM-7M" variants. |
| 127 | +- `SECRET_KEY` — Flask session/CSRF signing key (random ephemeral default; **set in production**) |
| 128 | +- `DATABASE_URL` — defaults to `%APPDATA%\MechBay\mechbay.db` (Windows) or `/data/mechbay.db` (Docker) |
| 129 | +- `DEBUG` — enables debug mode and colourized logs |
| 130 | +- `TRUST_PROXY_HEADERS` — set `true` only behind a trusted reverse proxy |
| 131 | +- `APPLICATION_ROOT` — path prefix for reverse-proxy deployments |
117 | 132 |
|
118 | | -## Import/Export |
119 | | -All import functions support merge mode (match on key field) or overwrite mode. Export generates timestamped filenames. See `app/services/*_service.py` for implementations. |
| 133 | +## File Naming Conventions |
120 | 134 |
|
121 | | -## Future Considerations |
122 | | -- **Deployment**: Plans to migrate to Gunicorn + Docker for production |
123 | | -- **Scope**: Currently focused on physical miniature inventory management, not game mechanics (pilot skills, special abilities, detailed loadouts) |
| 135 | +Export files: `{EntityType}_YYYYMMDD_HHMMSS.json` |
| 136 | +Generate with: `datetime.now().strftime("%Y%m%d_%H%M%S")` |
0 commit comments