Phase 0: lock platform contract, naming convention, and style enforcement

Establishes the framework's foundation as a multi-site adoptable platform.

ADRs (migrations/adr/):
- ADR-001 (ACCEPTED): Asset is the platform contract; Machine retires.
  Three relationship types (partof, controls, connectedto) with free-text
  label, position-resolution chain (asset > related > location),
  hierarchical locations, sibling-bay propagation.
- ADR-002 (ACCEPTED): Plugin contract semver via __contract_version__.
- ADR-003 (ACCEPTED): Hybrid plugin distribution (in-tree bundled +
  filesystem-based external).
- ADR-004 (ACCEPTED): Per-site instances, not multi-tenant.
- ADR-005 (ACCEPTED): Equipment plugin (manufacturing) split from
  measuringtools plugin (metrology). Subtype-table pattern for protocol
  data (FOCAS, CLM, MTConnect).
- ADR-006 (ACCEPTED): Plugin collector contract via get_collector_schema
  hook with API-key auth and identity-based upsert.

Naming convention v1 (CONTRIBUTING.md):
- DB tables/columns: lowercase concatenated, no underscores or dashes
- DB-mirrored Python/JS variables match column names exactly; pure code
  follows host-language convention (PEP 8 / camelCase)
- Closed acronym allowlist (universal + shop-floor domain), banned
  shorthand list with suffix exception (printers_bp etc allowed)
- Plain ASCII everywhere: chat, docs, comments, string literals

Style enforcement (scripts/check-naming-and-style.sh):
- Pre-commit-runnable check script: non-ASCII, banned shorthand,
  snake_case DB names, snake_case API params in frontend
- Fixes 14 violations across 11 files (Unicode arrows, snake_case
  params, ctx -> canvasContext, res -> response, req -> request_obj)

Project state (CLAUDE.md, README.md, frontend/CLAUDE.md):
- De-staled CLAUDE.md to reflect actual current state
- README unifies DB story (MySQL canonical, SQLite test-only)
- frontend/CLAUDE.md points at root convention

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-05-08 14:47:30 -04:00
parent ca22d62a2a
commit d6725c08e0
22 changed files with 1058 additions and 175 deletions

166
CLAUDE.md
View File

@@ -1,108 +1,124 @@
# ShopDB Flask Project
## Database Configuration
Modern rewrite of the classic-ASP shopdb. Built as a framework so sister GE Aerospace sites can adopt it. Plugin system is the product.
- **Development database:** `shopdb_flask` (new schema with asset abstraction)
- **Legacy database:** `shopdb` (Classic ASP schema - used for data migration reference only)
- **Connection:** Configured in `.env` file
## Database
## Current Tasks
- **Active database:** `shopdb_flask` (MySQL, asset-based schema)
- **Legacy database:** `shopdb` (Classic ASP schema, used only for one-time data import via `scripts/import_from_mysql.py`)
- **Connection:** `.env` file. See `.env.example`.
### Data Migration from Legacy Database
Architecture decisions live in `migrations/adr/`. Read those before making schema or contract changes.
Migrate data from `shopdb` to `shopdb_flask` using the new asset-based schema.
- ADR-001: Asset model is the platform contract (Machine retires) - ACCEPTED
- ADR-002: Plugin contract versioning (semver) - ACCEPTED
- ADR-003: Plugin distribution model (in-tree bundled + filesystem-based external) - ACCEPTED
- ADR-004: Deployment topology (per-site instances, not multi-tenant) - ACCEPTED
- ADR-005: Equipment vs measuringtools plugin scope - ACCEPTED
- ADR-006: Plugin collector contract pattern - ACCEPTED
**Status:** Pending
## Coding convention
**Migration guide:** `migrations/DATA_MIGRATION_GUIDE.md`
`CONTRIBUTING.md` defines naming rules (DB tables, columns, Python, JS, Vue, API). Pre-commit hook at `scripts/check-naming-and-style.sh` enforces them. Read `CONTRIBUTING.md` before naming any new identifier.
**Tables to migrate:**
- [ ] Reference data (vendors, models, locations, business units, subnets)
- [ ] Equipment (machines with category='Equipment')
- [ ] Computers/PCs (machines with pctypeid IS NOT NULL)
- [ ] Network devices (machines with category='Network')
- [ ] Printers (separate printers table)
- [ ] Communications/IP addresses
- [ ] Notifications
- [ ] Machine relationships
## Current state (as of 2026-05-08)
### Plugin Architecture Verification
### Wired in
Verify all plugins follow plug-and-play architecture - can be added/removed without impacting core site or other plugins.
- App factory pattern, Flask 3 + SQLAlchemy + Flask-Migrate + JWT + Marshmallow + CORS + Caching
- 6 plugins: computers, equipment, network, notifications, printers, usb
- Plugin contract: `BasePlugin` ABC, `PluginMeta`, registry, dependency-aware loader
- JWT auth with refresh tokens, audit logging, system settings, user management
- Frontend: Vue 3 + Pinia + Vite, dynamic plugin routing, dark mode default
- Search across all plugins via `get_searchable_fields` hook
- Asset relationships (cross-plugin)
**Plugins to verify:**
- [ ] Equipment (`plugins/equipment/`) - frontend: `views/machines/`
- [ ] Computers (`plugins/computers/`) - frontend: `views/pcs/`
- [ ] Printers (`plugins/printers/`) - frontend: `views/printers/`
- [ ] Network (`plugins/network/`) - frontend: `views/network/`
- [ ] USB (`plugins/usb/`) - frontend: `views/usb/`
- [ ] Notifications (`plugins/notifications/`) - frontend: `views/notifications/`
### In progress / partial
**Architecture checks:**
- [ ] Core API uses try/except ImportError for plugin imports (see `shopdb/core/api/assets.py` for pattern)
- [ ] Frontend router can dynamically add/remove routes per plugin
- [ ] Navigation menu reads from plugin `get_navigation_items()`
- [ ] Document plugin enable/disable mechanism in app factory
- **Dual model coexistence**: legacy `Machine` and new `Asset` both live. ADR-001 settles direction (Asset wins). Migration plan is the next concrete deliverable.
- **Plugin verification**: 6 plugins follow `BasePlugin` interface but no contract test asserts compliance. Skill `enforcing-plugin-contract` and the contract test suite are pending.
- **Tests**: hollow. `tests/conftest.py` exists but uses Flask-SQLAlchemy 2.x patterns that error on 3.x. No actual test files. Skill `pinning-flask-behavior` covers the rebuild.
**Each plugin must have:**
- `models/__init__.py` - exports all models
- `api/routes.py` - Flask Blueprint with endpoints
- `plugin.py` - implements BasePlugin class
- `manifest.json` - plugin metadata (name, version, dependencies)
- No direct imports from core code (use optional imports)
- Modular frontend components
### Pending
## Plugin Structure
- Alembic versions directory exists (`migrations/versions/`) but is empty. No migrations have been generated yet. Run `flask db init` is partial; need `flask db migrate` to capture current schema.
- Plugin scaffold CLI (`flask plugin new <name>`)
- Plugin author docs (`docs/PLUGIN-QUICKSTART.md`, `docs/PLUGIN-REFERENCE.md`)
- Per-site deploy story (`Dockerfile`, `docs/DEPLOY.md`)
- Frontend hook contract (asset-detail, map markers, search results)
```
plugins/
{plugin_name}/
__init__.py
plugin.py # BasePlugin implementation
manifest.json # Plugin metadata
models/
__init__.py # Export all models
{model}.py # SQLAlchemy models
api/
__init__.py
routes.py # Flask Blueprint
```
## Key Files
- `shopdb/plugins/base.py` - BasePlugin class and PluginMeta
- `shopdb/core/api/assets.py` - Example of optional plugin imports with try/except
- `frontend/src/router/index.js` - Frontend routing
- `frontend/src/components/AppSidebar.vue` - Navigation menu
## Migration Notes
See `migrations/` folder for:
- `DATA_MIGRATION_GUIDE.md` - Complete guide for migrating from legacy shopdb to new schema
- `MIGRATE_USB_DEVICES_FROM_EQUIPMENT.md` - USB device migration from equipment table
- `FIX_LOCATIONONLY_EQUIPMENT_TYPES.md` - Fix for LocationOnly equipment types
- `PRODUCTION_MIGRATION_GUIDE.md` - Production data import methods
## Quick Start
## Quick start
```bash
# Start dev environment
~/start-dev-env.sh
# Create/update database tables
# Activate venv and install deps
cd /home/camp/projects/shopdb-flask
source venv/bin/activate
pip install -r requirements.txt
# Configure environment
cp .env.example .env
# Edit .env with DB credentials, JWT secrets
# Create / update database tables
flask db-utils create-all
# Seed reference data
flask seed reference-data
# Restart services after changes
# Restart services
pm2 restart shopdb-flask-api shopdb-flask-ui
```
## Service URLs
- **Flask API:** http://localhost:5001
- **Flask UI:** http://localhost:5173
- **Legacy ASP:** http://192.168.122.151:8080
- Flask API: http://localhost:5001
- Flask UI: http://localhost:5173
- Legacy ASP (data source for one-time import): http://192.168.122.151:8080
## Plugin structure
```
plugins/
<plugin_name>/
__init__.py
plugin.py # BasePlugin implementation
manifest.json # Plugin metadata (name, version, dependencies, api_prefix)
models/
__init__.py # Export all models
<model>.py # SQLAlchemy models
api/
__init__.py
routes.py # Flask Blueprint
services/ # Optional, business logic
schemas/ # Optional, marshmallow schemas
migrations/ # Optional, plugin-specific Alembic migrations
```
Each plugin must have:
- `models/__init__.py` exports all models
- `plugin.py` extends `BasePlugin`
- `manifest.json` with metadata (single source of truth per ADR-002)
- No direct imports from core code (use the contract surface defined in ADR-001)
## Key files
- `shopdb/__init__.py` - app factory, blueprint registration
- `shopdb/plugins/base.py` - BasePlugin ABC, PluginMeta dataclass
- `shopdb/plugins/loader.py` - filesystem discovery, dependency-aware loading
- `shopdb/core/api/assets.py` - example of optional plugin imports
- `frontend/src/router/index.js` - frontend routing
- `frontend/src/components/AppSidebar.vue` - navigation menu
- `migrations/adr/` - architecture decision records
## Migration notes
- `migrations/DATA_MIGRATION_GUIDE.md` - one-time import from legacy ASP shopdb
- `migrations/MIGRATE_USB_DEVICES_FROM_EQUIPMENT.md` - USB device migration from equipment table
- `migrations/FIX_LOCATIONONLY_EQUIPMENT_TYPES.md` - LocationOnly equipment type fix
- `migrations/PRODUCTION_MIGRATION_GUIDE.md` - production import methods
- `migrations/rename_underscore_columns.sql` - one-time rename of snake_case columns to lowercase concatenated (per CONTRIBUTING.md)
- `migrations/versions/` - Alembic versions (currently empty)

View File

@@ -2,93 +2,141 @@
## Coding Standards
### Database Naming Convention
### Naming principles
**IMPORTANT: No underscores in database identifiers**
1. Spell it out. If a non-technical user looking at a report would not understand a column at a glance, rename it.
2. No project-specific shorthand. A name either uses a word in full, or uses an acronym from the allowed list below.
3. Consistency beats cleverness. Once a name is committed and used in code, do not rename it for marginal improvement.
4. When unsure, ask: "would a new shop-floor IT person understand this?"
All database table names and column names must use lowercase concatenated words (no underscores).
### Database
| Pattern | Good | Bad |
|---------|------|-----|
| Table names | `printerdata` | `printer_data` |
| Column names | `passwordhash` | `password_hash` |
| Foreign keys | `machinetypeid` | `machine_type_id` |
| Index names | `idx_printer_zabbix` | Allowed for indexes |
- **Table names**: lowercase, concatenated, plural. No dashes, no underscores.
- Good: `machines`, `pctypes`, `networkdevices`, `businessunits`, `auditlogs`
- Bad: `machine_types`, `network-devices`, `BusinessUnits`
- **Column names**: lowercase, concatenated, singular. No dashes, no underscores.
- Good: `machineid`, `machinenumber`, `lastzabbixsync`, `isactive`
- Bad: `machine_id`, `last_zabbix_sync`, `IsActive`
- **Foreign keys**: referenced table name (singular) + `id`.
- Good: `locationid` references `locations`, `vendorid` references `vendors`
- **Booleans**: prefix with `is` or `has`.
- Good: `isactive`, `isshopfloor`, `hasprinter`
- **Index names**: `idx_<table>_<column>`. Underscores allowed here only because indexes are infrastructure, not data.
- Good: `idx_machines_locationid`
### Intuitive Naming for Non-Technical Users
### Allowed acronyms (closed list, expand by PR)
Database tables and columns should use **simple, intuitive names** that non-technical users can understand when viewing data or reports.
- **Universal**: `id`, `url`, `api`, `http`, `https`, `json`, `jwt`, `sql`, `os`, `ip`, `dns`, `csv`, `pdf`, `cors`, `ttl`, `uuid`, `html`, `css`, `orm`
- **Domain (shop floor / IT infra)**: `cmm`, `cnc`, `pc`, `usb`, `vnc`, `winrm`, `ssh`, `ssl`, `tls`, `tcp`, `udp`, `smtp`, `ldap`, `vlan`, `sso`, `dnc`, `focas`, `clm`, `mtconnect`
| Avoid | Prefer | Why |
|-------|--------|-----|
| `printerextensions` | `printerdata` | "data" is clearer than "extensions" |
| `machinerelationships` | `machinelinks` | "links" is simpler (consider) |
| `comtypeid` | `connectiontypeid` | Spell it out when unclear |
Any acronym not on these lists must be spelled out. Proposing a new acronym is a separate PR that updates this list.
**Guiding principle:** If someone unfamiliar with the system looked at a table name, would they understand what's in it?
**Third-party imports are exempt.** Library and stdlib module paths cannot be renamed. `from sqlalchemy.orm import ...`, `import importlib.util`, `from werkzeug.utils import ...` are all fine even if they contain words that would otherwise be banned.
Examples:
### Banned shorthand
`cfg`, `ctx`, `mgr`, `req`, `res`, `env`, `util`, `helper`, and any other domain-specific shortening. Spell it out: `config`, `context`, `manager`, `request`, `response`, `environment`, `utilities`. Note: `db` as a standalone variable name is banned, but `db` as a prefix in `database` or as part of project name `shopdb` is fine.
**Suffix exception.** Banned shorthand applies to standalone identifiers. As a suffix where the prefix establishes meaning, it is acceptable:
- `printers_bp`, `auth_bp`, `network_bp` are fine. The prefix names what blueprint, the `_bp` suffix is conventional for Flask blueprints.
- `request_obj` is fine when disambiguating from imported `request`.
- `bp` alone as a variable name is not fine. Always pair with a meaningful prefix.
### Code
#### Python
- **Variables and functions holding a database value**: match the column name exactly. No conversion to snake_case.
- DB column `machineid` -> Python variable `machineid`, attribute `Machine.machineid`, dict key `{"machineid": 1}`
- DB column `lastzabbixsync` -> Python variable `lastzabbixsync`
- **Pure code variables and functions** (loop counters, helpers, anything that does not mirror a DB field): snake_case per PEP 8.
- Good: `loop_count`, `current_user`, `validate_input()`
- **Classes**: PascalCase. Spell out fully.
- Good: `PrinterData`, `AssetType`, `NetworkDevice`
- **Modules / file names**: lowercase. Underscores allowed where it improves readability (PEP 8 permits this for modules).
- Good: `employee_service.py`, `asset_routes.py`
#### JavaScript / Vue
- **Variables**: camelCase per JS convention.
- Good: `currentUser`, `isLoading`
- **Variables holding API field values**: match the API field name exactly. Do NOT convert to camelCase.
- API returns `{"machineid": 1}` -> JS `response.machineid`, NOT `response.machineId`
- API returns `{"lastzabbixsync": "..."}` -> JS `device.lastzabbixsync`
- **Components**: PascalCase. Spell out fully.
- Good: `AssetDetail.vue`, `MachineForm.vue`, `PrinterList.vue`
- **CSS classes**: lowercase with dashes.
- Good: `asset-detail`, `machine-form`
#### API
- **Endpoints**: lowercase, plural nouns. No underscores or dashes.
- Good: `/api/machines`, `/api/networkdevices`, `/api/businessunits`
- **Query parameters**: lowercase, concatenated. Match column names where applicable.
- Good: `?locationid=5`, `?isactive=true`
- **Response keys**: match database column names exactly.
- Good: `{"machineid": 1, "lastzabbixsync": "2026-01-12T10:00:00Z"}`
### Examples
```python
# Good
class PrinterExtension(db.Model):
__tablename__ = 'printerextensions'
# Good - DB-mirrored fields keep column names; pure code uses snake_case
class PrinterData(db.Model):
__tablename__ = 'printerdata'
machineid = db.Column(db.Integer, db.ForeignKey('machines.machineid'))
lastzabbixsync = db.Column(db.DateTime)
isnetworkprinter = db.Column(db.Boolean)
# Bad
class PrinterExtension(db.Model):
__tablename__ = 'printer_extensions'
machine_id = db.Column(db.Integer, db.ForeignKey('machines.machine_id'))
last_zabbix_sync = db.Column(db.DateTime)
is_network_printer = db.Column(db.Boolean)
def list_printers_by_location(locationid: int) -> list[dict]:
printers = PrinterData.query.filter_by(locationid=locationid).all()
result_count = len(printers)
return [{"machineid": p.machineid, "isnetworkprinter": p.isnetworkprinter} for p in printers]
```
### API Response Keys
API JSON responses should also use lowercase concatenated keys to match database columns:
```json
{
"machineid": 1,
"machinenumber": "M001",
"lastzabbixsync": "2026-01-12T10:00:00Z",
"isnetworkprinter": true
}
```
### Python Code
Python variable and function names follow standard Python conventions (snake_case for variables/functions, PascalCase for classes):
```python
# Variables and functions use snake_case
machine_type = get_machine_type()
is_valid = validate_input(data)
# Bad - PEP 8-style underscores on DB fields, banned shorthand, project acronym not on allowed list
class PrinterData(db.Model):
__tablename__ = 'printer_data'
# Classes use PascalCase
class PrinterExtension:
pass
machine_id = db.Column(db.Integer)
last_zabbix_sync = db.Column(db.DateTime)
is_net_prn = db.Column(db.Boolean)
def list_prns_by_loc(loc_id):
bp_ctx = get_context()
cfg = load_cfg()
return PrinterData.query.filter_by(loc_id=loc_id).all()
```
### File Structure
### Style policy (non-naming)
- Models: `shopdb/core/models/` or `plugins/<plugin>/models/`
- API routes: `shopdb/core/api/` or `plugins/<plugin>/api/`
- Services: `shopdb/core/services/` or `plugins/<plugin>/services/`
- No emojis in code, comments, documentation, string literals, or UI.
- No em-dashes (U+2014), en-dashes (U+2013), Unicode arrows, or smart quotes anywhere. Plain ASCII only.
- Comments default to none. Add one only when the WHY is non-obvious. Keep comments terse (lite caveman style is fine for inline `#` and `//` comments). Docstrings stay normal English.
- Dark theme is default. Keep UI functional and professional.
### Plugin Development
### File structure
- Core models: `shopdb/core/models/`
- Core API routes: `shopdb/core/api/`
- Core services: `shopdb/core/services/`
- Plugin code: `plugins/<plugin>/{models,api,services,schemas}/`
### Plugin development
When creating a new plugin:
1. Create directory structure in `plugins/<name>/`
2. Include `manifest.json` with metadata
3. Extend `BasePlugin` class
4. Follow naming conventions above
5. Run `flask plugin install <name>` to install
1. Create directory `plugins/<name>/`. Use the scaffold (`flask plugin new <name>`) when available.
2. Include `manifest.json` with metadata. This is the single source of truth for plugin name, version, dependencies, api_prefix.
3. Extend `BasePlugin` class.
4. Follow naming conventions above.
5. Run `flask plugin install <name>` to install.
See `docs/PLUGIN-QUICKSTART.md` (when published) for the 30-minute walkthrough.
## Testing
@@ -98,9 +146,17 @@ Run tests with:
pytest tests/
```
## Code Review Checklist
Tests are required for:
- New routes (smoke + happy path + 1-2 edge cases)
- New plugin hooks (contract test)
- Schema migrations (before/after data integrity)
- [ ] No underscores in table/column names
- [ ] API responses use consistent key naming
- [ ] Plugin follows BasePlugin interface
## Code review checklist
- [ ] No underscores in table or column names
- [ ] No banned shorthand or unapproved acronyms in any new identifier
- [ ] DB-mirrored Python and JS variables match column names exactly
- [ ] API response keys match column names exactly
- [ ] No em-dashes, en-dashes, smart quotes, Unicode arrows, or emojis (run `grep -rP '[^\x00-\x7F]' --include='*.py' --include='*.vue' --include='*.js' .`)
- [ ] Plugin follows `BasePlugin` interface and contract version
- [ ] Tests included for new functionality

View File

@@ -131,13 +131,15 @@ npm run build
### Database
ShopDB Flask uses MySQL 5.6+ as the canonical database. SQLite is used only for the test suite (`TestingConfig` in `shopdb/config.py` points at an in-memory SQLite). Do not run dev or production against SQLite.
The database schema is exported in `database/schema.sql`. To initialize:
```bash
mysql -u root -p shopdb_flask < database/schema.sql
```
To import data from the legacy ShopDB MySQL database:
To import data from the legacy ShopDB MySQL database (one-time, see `migrations/DATA_MIGRATION_GUIDE.md`):
```bash
python scripts/import_from_mysql.py

View File

@@ -1,5 +1,21 @@
# Frontend Development Standards
## Naming Convention (LOCKED)
The shopdb-flask naming convention lives in the root `CONTRIBUTING.md`. Read it before naming any variable, component, API param, or CSS class.
Frontend-specific reminders pulled from the convention:
- Variables holding API field values: match the API field name exactly. Do NOT convert to camelCase. (`response.machineid`, NOT `response.machineId`)
- Pure JS variables: camelCase (`currentUser`, `isLoading`)
- Vue components: PascalCase, spelled out (`AssetDetail.vue`, `MachineForm.vue`)
- CSS classes: lowercase with dashes (`asset-detail`, `machine-form`)
- API params sent to backend: match DB column names without underscores (`params.locationid = 5`, NOT `params.location_id`)
- No emojis, em-dashes, smart quotes, or Unicode arrows anywhere. Plain ASCII only.
- Banned shorthand as standalone variables: `cfg`, `ctx`, `mgr`, `req`, `res`, `env`, `util`, `helper`. Spell them out (`canvasContext`, `response`, `manager`, etc.). Suffix usage like `printers_bp` is allowed.
Pre-commit hook at `scripts/check-naming-and-style.sh` enforces these rules.
## CSS Styling Standards
### Use CSS Variables for ALL Colors

View File

@@ -95,8 +95,8 @@
<div class="form-group">
<label>Direction</label>
<select v-model="newRel.direction" class="form-control">
<option value="outgoing">This asset Target asset</option>
<option value="incoming">Source asset This asset</option>
<option value="outgoing">This asset -> Target asset</option>
<option value="incoming">Source asset -> This asset</option>
</select>
</div>

View File

@@ -86,8 +86,8 @@ async function onSearch() {
searchTimeout = setTimeout(async () => {
try {
const res = await employeesApi.search(query)
results.value = res.data.data || []
const response = await employeesApi.search(query)
results.value = response.data.data || []
} catch (err) {
console.error('Employee search error:', err)
results.value = []

View File

@@ -170,8 +170,8 @@ onMounted(async () => {
// Load business units
try {
const res = await businessUnitsApi.list()
businessUnits.value = res.data.data || []
const response = await businessUnitsApi.list()
businessUnits.value = response.data.data || []
} catch (err) {
console.error('Error loading business units:', err)
}
@@ -200,8 +200,8 @@ async function loadData() {
if (businessUnit.value) {
params.businessunit = businessUnit.value
}
const res = await notificationsApi.getShopfloor(params)
notifications.value = res.data.data || { current: [], upcoming: [] }
const response = await notificationsApi.getShopfloor(params)
notifications.value = response.data.data || { current: [], upcoming: [] }
} catch (err) {
console.error('Error loading shopfloor data:', err)
} finally {

View File

@@ -199,9 +199,9 @@ async function loadDevices() {
perpage: perPage.value
}
if (search.value) params.search = search.value
if (selectedType.value) params.type_id = selectedType.value
if (vendorFilter.value) params.vendor_id = vendorFilter.value
if (locationFilter.value) params.location_id = locationFilter.value
if (selectedType.value) params.typeid = selectedType.value
if (vendorFilter.value) params.vendorid = vendorFilter.value
if (locationFilter.value) params.locationid = locationFilter.value
const response = await networkApi.list(params)
devices.value = response.data.data || []

View File

@@ -376,8 +376,8 @@ async function searchEmployees() {
searchTimeout = setTimeout(async () => {
try {
const res = await employeesApi.search(query)
employeeResults.value = res.data.data || []
const response = await employeesApi.search(query)
employeeResults.value = response.data.data || []
} catch (err) {
console.error('Employee search error:', err)
employeeResults.value = []

View File

@@ -127,7 +127,7 @@ async function loadNotifications() {
params.search = searchQuery.value
}
if (selectedType.value) {
params.type_id = selectedType.value
params.typeid = selectedType.value
}
if (currentFilter.value === 'current') {
params.current = 'true'

View File

@@ -129,22 +129,22 @@ function generateQRCodes() {
}
function drawLogoOverlay(canvas) {
const ctx = canvas.getContext('2d')
const canvasContext = canvas.getContext('2d')
const size = canvas.width
const logoSize = Math.round(size * 0.22)
const x = (size - logoSize) / 2
const y = (size - logoSize) / 2
// White circle background
ctx.beginPath()
ctx.arc(size / 2, size / 2, logoSize / 2 + 4, 0, Math.PI * 2)
ctx.fillStyle = '#fff'
ctx.fill()
canvasContext.beginPath()
canvasContext.arc(size / 2, size / 2, logoSize / 2 + 4, 0, Math.PI * 2)
canvasContext.fillStyle = '#fff'
canvasContext.fill()
// Load and draw the GE monogram
const img = new Image()
img.onload = () => {
ctx.drawImage(img, 0, 0, 32.5, 32, x, y, logoSize, logoSize)
canvasContext.drawImage(img, 0, 0, 32.5, 32, x, y, logoSize, logoSize)
}
const svgStr = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32.5 32"><path d="M19.8915 11.8362C19.8915 10.0196 21.1404 8.25119 21.826 8.5888C22.6014 8.97061 21.2424 10.6868 19.8915 11.8362ZM11.3823 12.4994C11.3823 11.0364 12.8475 8.25521 13.7453 8.54861C14.8023 8.89425 12.8679 11.6996 11.3823 12.4994ZM9.89679 22.9611C9.2234 22.9932 8.77447 22.5672 8.77447 21.8558C8.77447 19.9508 11.4558 18.1301 13.4841 17.1535C13.125 19.8141 12.2108 22.8525 9.90087 22.957M22.279 16.7516C20.7486 16.7516 19.5773 17.8608 19.5773 19.1912C19.5773 20.3004 20.2507 21.1846 21.1526 21.1846C21.4668 21.1846 21.7811 21.0078 21.7811 20.6099C21.7811 20.0352 21.0057 19.8945 21.0669 19.0304C21.1036 18.4637 21.6505 18.0819 22.1892 18.0819C23.2707 18.0819 23.7768 19.1148 23.7768 20.1758C23.7319 21.8156 22.5075 22.957 21.0669 22.957C19.1773 22.957 17.9611 21.1846 17.9611 19.2756C17.9611 16.4381 19.8507 15.3328 20.8424 15.0676C20.8547 15.0676 23.4299 15.5217 23.3483 14.4004C23.3156 13.9101 22.5688 13.7212 22.03 13.6971C21.4301 13.6729 20.8302 13.886 20.8302 13.886C20.5159 13.7292 20.2996 13.4238 20.165 13.0701C22.0096 11.6956 23.3156 10.3652 23.3156 8.85808C23.3156 8.0623 22.7769 7.35092 21.7403 7.35092C19.8956 7.35092 18.4998 9.65386 18.4998 11.7398C18.4998 12.0934 18.4998 12.4511 18.5896 12.7606C17.4183 13.6046 16.5491 14.1271 14.9737 15.0555C14.9737 14.8626 15.0145 14.3602 15.1492 13.7131C15.6879 13.1384 16.4307 12.2743 16.4307 11.6112C16.4307 11.3017 16.2511 11.0364 15.892 11.0364C14.9941 11.0364 14.3167 12.3667 14.1371 13.2952C13.7331 13.7815 12.9209 14.4044 12.2475 14.4044C11.7088 14.4044 11.5292 13.9141 11.4803 13.7413C13.1903 13.1625 15.3084 10.8596 15.3084 8.77769C15.3084 8.33559 15.1288 7.35895 13.778 7.35895C11.7537 7.35895 10.0437 10.3291 10.0437 12.632C9.32134 12.632 9.05607 11.8764 9.05607 11.3017C9.05607 10.727 9.28053 10.1482 9.28053 9.97136C9.28053 9.79452 9.19075 9.57347 8.92139 9.57347C8.248 9.57347 7.83989 10.4617 7.83989 11.4785C7.88478 12.8973 8.83161 13.7855 10.0886 13.8739C10.2682 14.7179 11.0354 15.5137 11.9782 15.5137C12.5659 15.5137 13.2841 15.3369 13.778 14.8947C13.7331 15.2042 13.6882 15.4695 13.6433 15.7388C11.6639 16.7596 10.2233 17.467 8.91731 18.6204C7.88478 19.5529 7.29709 20.7908 7.29709 21.7674C7.29709 23.0977 8.15005 24.3356 9.90903 24.3356C11.9782 24.3356 13.5535 22.6958 14.3208 20.4371C14.6799 19.372 14.8268 17.8247 14.9166 16.4059C16.9857 15.2565 17.9693 14.5853 19.0467 13.8337C19.1814 14.0548 19.3202 14.2316 19.4956 14.3642C18.5529 14.8505 16.3001 16.2251 16.3001 19.4604C16.3001 21.7674 17.8754 24.3356 20.9812 24.3356C23.5482 24.3356 25.3031 22.2537 25.3031 20.2602C25.3031 18.4436 24.2665 16.7596 22.2872 16.7596M30.025 20.5657C30.025 20.5657 29.9924 20.6019 29.9434 20.5818C29.9067 20.5697 29.8944 20.5496 29.8944 20.5255C29.8944 20.4974 30.4372 18.9219 30.4331 17.1133C30.429 15.164 29.621 13.9663 28.5884 13.9663C27.96 13.9663 27.5069 14.4084 27.5069 15.0756C27.5069 16.2733 28.9925 16.3617 28.9925 18.9781C28.9925 20.0432 28.768 21.06 28.4089 22.1693C26.7438 27.7076 21.4301 30.2798 16.2593 30.2798C13.8718 30.2798 12.1781 29.7975 11.6721 29.5765C11.6517 29.5684 11.6354 29.5283 11.6517 29.4881C11.6639 29.4559 11.6966 29.4358 11.717 29.4439C11.921 29.5242 13.378 29.9744 15.1778 29.9744C17.1571 29.9744 18.3284 29.1786 18.3284 28.202C18.3284 27.583 17.8346 27.0967 17.202 27.0967C15.9859 27.0967 15.8961 28.6039 13.2882 28.6039C12.1618 28.6039 11.1742 28.3828 10.0029 28.0291C4.41988 26.3451 1.76306 21.1605 1.76714 16.0161C1.76714 13.5122 2.48134 11.5187 2.49358 11.4986C2.50174 11.4866 2.53439 11.4705 2.5752 11.4866C2.61602 11.4986 2.62418 11.5348 2.62418 11.5428C2.55888 11.7518 2.08547 13.1786 2.08547 14.951C2.08547 16.9003 2.89353 18.0538 3.93015 18.0538C4.51783 18.0538 5.01165 17.6117 5.01165 16.9887C5.01165 15.791 3.52611 15.6584 3.52611 13.0862C3.52611 11.9769 3.75058 11.0043 4.10972 9.85079C5.80747 4.34464 11.0722 1.7684 16.2471 1.72821C18.6509 1.70811 20.7567 2.41949 20.8383 2.47978C20.8506 2.49184 20.8669 2.52399 20.8506 2.56016C20.8343 2.60035 20.8057 2.60839 20.7935 2.60437C20.769 2.60437 19.3977 2.03768 17.3286 2.03768C15.3941 2.03768 14.1779 2.83346 14.1779 3.85431C14.1779 4.42904 14.6268 4.91535 15.3043 4.91535C16.5205 4.91535 16.6103 3.4524 19.2181 3.4524C20.3445 3.4524 21.3322 3.67345 22.5035 4.02713C28.1314 5.71113 30.6902 10.94 30.7392 15.996C30.7637 18.5843 30.025 20.5456 30.0169 20.5576M16.2471 0.75157C7.69705 0.75157 0.763175 7.58001 0.763175 16C0.763175 24.42 7.69705 31.2444 16.2471 31.2444C24.7971 31.2444 31.7269 24.42 31.7269 16C31.7269 7.58001 24.7971 0.75157 16.2471 0.75157ZM16.2471 32C7.28893 32 0 24.8661 0 16C0 7.13389 7.28893 0 16.2471 0C25.2052 0 32.4941 7.18212 32.4941 16C32.4941 24.8179 25.2011 32 16.2471 32Z" fill="black"/></svg>`
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgStr)

View File

@@ -97,23 +97,23 @@ function generateQR() {
}
function drawLogoOverlay(canvas) {
const ctx = canvas.getContext('2d')
const canvasContext = canvas.getContext('2d')
const size = canvas.width
const logoSize = Math.round(size * 0.22)
const x = (size - logoSize) / 2
const y = (size - logoSize) / 2
// White circle background
ctx.beginPath()
ctx.arc(size / 2, size / 2, logoSize / 2 + 4, 0, Math.PI * 2)
ctx.fillStyle = '#fff'
ctx.fill()
canvasContext.beginPath()
canvasContext.arc(size / 2, size / 2, logoSize / 2 + 4, 0, Math.PI * 2)
canvasContext.fillStyle = '#fff'
canvasContext.fill()
// Load and draw the GE monogram (the circular part of the SVG)
const img = new Image()
img.onload = () => {
// Draw only the GE monogram portion (left 32x32 of the 138x32 SVG)
ctx.drawImage(img, 0, 0, 32.5, 32, x, y, logoSize, logoSize)
canvasContext.drawImage(img, 0, 0, 32.5, 32, x, y, logoSize, logoSize)
}
// Use a data URI with black fill for the monogram
const svgStr = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32.5 32"><path d="M19.8915 11.8362C19.8915 10.0196 21.1404 8.25119 21.826 8.5888C22.6014 8.97061 21.2424 10.6868 19.8915 11.8362ZM11.3823 12.4994C11.3823 11.0364 12.8475 8.25521 13.7453 8.54861C14.8023 8.89425 12.8679 11.6996 11.3823 12.4994ZM9.89679 22.9611C9.2234 22.9932 8.77447 22.5672 8.77447 21.8558C8.77447 19.9508 11.4558 18.1301 13.4841 17.1535C13.125 19.8141 12.2108 22.8525 9.90087 22.957M22.279 16.7516C20.7486 16.7516 19.5773 17.8608 19.5773 19.1912C19.5773 20.3004 20.2507 21.1846 21.1526 21.1846C21.4668 21.1846 21.7811 21.0078 21.7811 20.6099C21.7811 20.0352 21.0057 19.8945 21.0669 19.0304C21.1036 18.4637 21.6505 18.0819 22.1892 18.0819C23.2707 18.0819 23.7768 19.1148 23.7768 20.1758C23.7319 21.8156 22.5075 22.957 21.0669 22.957C19.1773 22.957 17.9611 21.1846 17.9611 19.2756C17.9611 16.4381 19.8507 15.3328 20.8424 15.0676C20.8547 15.0676 23.4299 15.5217 23.3483 14.4004C23.3156 13.9101 22.5688 13.7212 22.03 13.6971C21.4301 13.6729 20.8302 13.886 20.8302 13.886C20.5159 13.7292 20.2996 13.4238 20.165 13.0701C22.0096 11.6956 23.3156 10.3652 23.3156 8.85808C23.3156 8.0623 22.7769 7.35092 21.7403 7.35092C19.8956 7.35092 18.4998 9.65386 18.4998 11.7398C18.4998 12.0934 18.4998 12.4511 18.5896 12.7606C17.4183 13.6046 16.5491 14.1271 14.9737 15.0555C14.9737 14.8626 15.0145 14.3602 15.1492 13.7131C15.6879 13.1384 16.4307 12.2743 16.4307 11.6112C16.4307 11.3017 16.2511 11.0364 15.892 11.0364C14.9941 11.0364 14.3167 12.3667 14.1371 13.2952C13.7331 13.7815 12.9209 14.4044 12.2475 14.4044C11.7088 14.4044 11.5292 13.9141 11.4803 13.7413C13.1903 13.1625 15.3084 10.8596 15.3084 8.77769C15.3084 8.33559 15.1288 7.35895 13.778 7.35895C11.7537 7.35895 10.0437 10.3291 10.0437 12.632C9.32134 12.632 9.05607 11.8764 9.05607 11.3017C9.05607 10.727 9.28053 10.1482 9.28053 9.97136C9.28053 9.79452 9.19075 9.57347 8.92139 9.57347C8.248 9.57347 7.83989 10.4617 7.83989 11.4785C7.88478 12.8973 8.83161 13.7855 10.0886 13.8739C10.2682 14.7179 11.0354 15.5137 11.9782 15.5137C12.5659 15.5137 13.2841 15.3369 13.778 14.8947C13.7331 15.2042 13.6882 15.4695 13.6433 15.7388C11.6639 16.7596 10.2233 17.467 8.91731 18.6204C7.88478 19.5529 7.29709 20.7908 7.29709 21.7674C7.29709 23.0977 8.15005 24.3356 9.90903 24.3356C11.9782 24.3356 13.5535 22.6958 14.3208 20.4371C14.6799 19.372 14.8268 17.8247 14.9166 16.4059C16.9857 15.2565 17.9693 14.5853 19.0467 13.8337C19.1814 14.0548 19.3202 14.2316 19.4956 14.3642C18.5529 14.8505 16.3001 16.2251 16.3001 19.4604C16.3001 21.7674 17.8754 24.3356 20.9812 24.3356C23.5482 24.3356 25.3031 22.2537 25.3031 20.2602C25.3031 18.4436 24.2665 16.7596 22.2872 16.7596M30.025 20.5657C30.025 20.5657 29.9924 20.6019 29.9434 20.5818C29.9067 20.5697 29.8944 20.5496 29.8944 20.5255C29.8944 20.4974 30.4372 18.9219 30.4331 17.1133C30.429 15.164 29.621 13.9663 28.5884 13.9663C27.96 13.9663 27.5069 14.4084 27.5069 15.0756C27.5069 16.2733 28.9925 16.3617 28.9925 18.9781C28.9925 20.0432 28.768 21.06 28.4089 22.1693C26.7438 27.7076 21.4301 30.2798 16.2593 30.2798C13.8718 30.2798 12.1781 29.7975 11.6721 29.5765C11.6517 29.5684 11.6354 29.5283 11.6517 29.4881C11.6639 29.4559 11.6966 29.4358 11.717 29.4439C11.921 29.5242 13.378 29.9744 15.1778 29.9744C17.1571 29.9744 18.3284 29.1786 18.3284 28.202C18.3284 27.583 17.8346 27.0967 17.202 27.0967C15.9859 27.0967 15.8961 28.6039 13.2882 28.6039C12.1618 28.6039 11.1742 28.3828 10.0029 28.0291C4.41988 26.3451 1.76306 21.1605 1.76714 16.0161C1.76714 13.5122 2.48134 11.5187 2.49358 11.4986C2.50174 11.4866 2.53439 11.4705 2.5752 11.4866C2.61602 11.4986 2.62418 11.5348 2.62418 11.5428C2.55888 11.7518 2.08547 13.1786 2.08547 14.951C2.08547 16.9003 2.89353 18.0538 3.93015 18.0538C4.51783 18.0538 5.01165 17.6117 5.01165 16.9887C5.01165 15.791 3.52611 15.6584 3.52611 13.0862C3.52611 11.9769 3.75058 11.0043 4.10972 9.85079C5.80747 4.34464 11.0722 1.7684 16.2471 1.72821C18.6509 1.70811 20.7567 2.41949 20.8383 2.47978C20.8506 2.49184 20.8669 2.52399 20.8506 2.56016C20.8343 2.60035 20.8057 2.60839 20.7935 2.60437C20.769 2.60437 19.3977 2.03768 17.3286 2.03768C15.3941 2.03768 14.1779 2.83346 14.1779 3.85431C14.1779 4.42904 14.6268 4.91535 15.3043 4.91535C16.5205 4.91535 16.6103 3.4524 19.2181 3.4524C20.3445 3.4524 21.3322 3.67345 22.5035 4.02713C28.1314 5.71113 30.6902 10.94 30.7392 15.996C30.7637 18.5843 30.025 20.5456 30.0169 20.5576M16.2471 0.75157C7.69705 0.75157 0.763175 7.58001 0.763175 16C0.763175 24.42 7.69705 31.2444 16.2471 31.2444C24.7971 31.2444 31.7269 24.42 31.7269 16C31.7269 7.58001 24.7971 0.75157 16.2471 0.75157ZM16.2471 32C7.28893 32 0 24.8661 0 16C0 7.13389 7.28893 0 16.2471 0C25.2052 0 32.4941 7.18212 32.4941 16C32.4941 24.8179 25.2011 32 16.2471 32Z" fill="black"/></svg>`

View File

@@ -0,0 +1,173 @@
# ADR-001: Asset model is the platform contract
- **Status:** ACCEPTED
- **Date:** 2026-05-08
- **Deciders:** cproudlock
- **Supersedes:** none
## Context
shopdb-flask is being shaped as a framework that sister GE Aerospace facilities can adopt. The framework defines a stable core; sites install plugins for the asset classes they care about (equipment, computers, printers, measuring tools, network gear, etc.).
The codebase ran two parallel object models:
1. **Legacy `Machine` model** in `shopdb/core/models/machine.py`. Original schema inherited from the classic-ASP shopdb. Tables: `machines`, `pctypes`, `machinetypes`. Plugins like printers stored extension data via `PrinterData` keyed by `machineid`.
2. **New `Asset` model** in `shopdb/core/models/asset.py`. Generic asset abstraction with `AssetType`, `AssetStatus`, `AssetRelationship`. Plugins also exposed asset-based tables keyed by `assetid`.
For the framework to be adoptable, the platform contract has to be one model, documented, versioned, and stable.
## Decision
**`Asset` is the platform contract.** Plugin authors target the asset-based API. The `Machine` model and its dependents (`PCType`, `MachineType`, `PrinterData` keyed by `machineid`) are legacy and will be retired through a tracked migration.
### Platform contract surface
The following are the public, versioned surface. Plugin authors may depend on them. Breaking changes require a major version bump per ADR-002.
#### Models
- `Asset` (core entity)
- `AssetType` (asset classification, registered by plugins)
- `AssetStatus` (active / inactive / decommissioned / retired)
- `AssetRelationship` + `RelationshipType` (cross-plugin links)
- `Vendor`, `Location`, `LocationType`, `BusinessUnit`, `Model`, `OperatingSystem` (shared reference data)
#### Helpers
- `AuditLog` API: `audit_log(action, entitytype, entityid, ...)` for plugins to record audit entries with consistent schema
- `Setting` API: `plugin.get_setting(key)` and `plugin.set_setting(key, value)` for plugin-scoped config persisted via the core `Setting` model
#### Plugin contract
- `BasePlugin` ABC and its hooks (search, navigation, dashboard, relationships, collector schema)
#### Excluded from the contract for v1
- Event bus (`get_event_handlers` removed from `BasePlugin`). Add later if a real use case appears.
### Relationship types (seeded core values)
`RelationshipType` is seeded with three rows. Plugin authors do not add new types in v1.
| Type | Meaning | Boundary rule |
|------|---------|---------------|
| `partof` | Composition, siblings, sub-assemblies | Parent dies without it |
| `controls` | One asset has operational authority over another | PC commands a machine, measuring tool, etc. |
| `connectedto` | Network or data link without operational authority | Cable, switch port, NAS mount |
### AssetRelationship columns
```
AssetRelationship
- relationshipid PK
- sourceassetid FK to assets
- targetassetid FK to assets
- relationshiptypeid FK to relationshiptypes (one of partof, controls, connectedto)
- label text, free description (e.g., "ethernet PoE", "DNC feed")
- inheritsposition bool, true means resolved-position walk follows this edge
- propagatesthroughid FK to relationshiptypes, nullable, see propagation below
- notes text
- isactive bool
- createddate, modifieddate
```
Free-text `label` carries the domain nuance (`controls` with label "DNC feed" vs `controls` with label "operator workstation"). Avoids inflating the type list.
### Sibling propagation
When a relationship is created or deleted, the framework checks `RelationshipType.propagatesthroughid`. If set, it finds all assets related to the source via the propagation type and applies the same change.
Default seeding:
| RelationshipType | propagatesthroughid |
|------------------|---------------------|
| `partof` | null (the propagation rail itself) |
| `controls` | `partof` (controls relationships propagate across siblings) |
| `connectedto` | null (network paths don't propagate) |
Cycle protection: max walk depth 3, visited-set during traversal.
### Position resolution
Resolved map position for any asset follows this priority chain:
1. Asset-specific override (`assets.mapx`, `assets.mapy` if non-null)
2. Walk relationships where `inheritsposition=true`, ordered by relationship type priority (`partof` first, then `controls`), recursively resolve the related asset's position with cycle detection
3. Fall back to the asset's location coords (`locations.mapx`, `locations.mapy` via `assets.locationid`)
4. If none of the above, asset is "unplaced" and rendered in a tray, not on the map
API exposes resolved position with a source indicator: `{"mapx": 234, "mapy": 567, "positionsource": "self|related|location|none"}`.
### Hierarchical locations
`locations` is extended with `parentlocationid` (self-FK, nullable). Sites define their own location tree (cells, sub-cells, network closets, meeting rooms, labs).
`locationtypes` lookup is added with seeded values: `section`, `cell`, `subcell`, `meetingroom`, `lab`, `office`, `storage`, `hallway`, `networkcloset`, `building`. Sites can extend.
Asset-to-location is the existing `assets.locationid` FK only (single primary location for v1). Multi-location and transient placement are deferred.
## Migration scope (narrowed)
Only one class of legacy data migrates: physical manufacturing equipment with a `machinenumber` (5-axis mills, lathes, broachers, heat treatment, CMMs, etc.).
Migration filter:
```sql
SELECT * FROM legacy.machines
WHERE category = 'Equipment'
AND machinenumber IS NOT NULL
```
Rows without `machinenumber`: **skipped.** Add manually post-migration if any matter.
Migrated rows become `assets` with `assettype='Equipment'` plus a row in the equipment plugin's `equipment` table. Legacy `machinetypeid` is preserved on the equipment row to enable later reclassification (some "Equipment" rows are actually measuring tools, see ADR-005).
Skipped from migration:
- PCs (rebuilt via PXE collector pipeline, see ADR-006)
- Printers, USB devices, network gear, notifications, KB articles (operational or re-collectable, decision per class deferred until needed)
## Consequences
### Positive
- Plugin authors at sister sites have a single, documented contract to target
- Cross-plugin features (relationships, search, map) work uniformly
- Three relationship types are easy to learn; free-text label captures nuance
- Sibling propagation handles dual-path machines without code changes per use case
- Position resolution gives users one map without per-plugin map code
### Negative
- Migration of equipment data must run cleanly; `migrating-asset-schema` skill owns the procedure
- Frontend `views/machines/` will be repointed or replaced; effort tracked in Phase 5
- Legacy `core/api/machines.py` (641 LOC) becomes deprecated; deletion or shim deferred until equipment is migrated
### Neutral
- `Machine`, `PCType`, `MachineType`, `PrinterData` remain in the schema until the migration completes, behind a deprecation notice. New code does not touch them.
## Alternatives considered
1. **Keep both models indefinitely.** Doubles test surface, confuses contract docs, breaks cross-plugin features. Rejected.
2. **Make `Machine` the contract; retire `Asset`.** `Machine` has shop-floor-specific assumptions that don't generalize. Rejected.
3. **Define a higher abstraction above both.** Yet another layer. `Asset` is already the abstraction. Rejected.
4. **More relationship types** (`controls`, `operates`, `monitors`, `dncfeeds`, `mountedon`, `connectedvia`, `inspects`, `siblingbay`). Eight types proved unwieldy. Collapsed to three with free-text labels and per-row inheritance/propagation flags.
## Open questions deferred
- Lifecycle relationship type (`replaces`, `replacedby`). Add when first needed.
- Multi-location asset placement, transient placement (calibration trip, off-site repair). Add when first needed.
- Map editing UI (drag locations onto floor plan). Phase 6 polish.
- Plugin-extensible relationship types beyond the three seeded. Add when a real cross-plugin case can't be expressed via labels.
## References
- `shopdb/core/models/asset.py`
- `shopdb/core/models/machine.py` (legacy, deprecated)
- `shopdb/plugins/base.py`
- ADR-002 (versioning of the surface)
- ADR-003 (plugin distribution)
- ADR-004 (deployment topology)
- ADR-005 (equipment vs measuring tools split)
- ADR-006 (collector contract pattern)

View File

@@ -0,0 +1,60 @@
# ADR-002: Plugin contract versioning
- **Status:** ACCEPTED
- **Date:** 2026-05-08
- **Deciders:** cproudlock
- **Supersedes:** none
## Context
Once sister sites start writing their own plugins (or pulling community plugins), the framework's plugin contract becomes a public API. Without a versioning story, any change to `BasePlugin` or the core platform models can silently break installed plugins at remote sites.
The existing `BasePlugin` and `PluginMeta` already declare a `core_version` field (default `">=1.0.0"`), but it is not enforced anywhere. The plugin loader does not check it before instantiation.
## Decision
The framework adopts **semantic versioning for the plugin contract**, declared in two places:
1. **Framework version** (`shopdb/__init__.py`): a single `__contract_version__` constant. This is the version of the platform contract as defined in ADR-001. Bumped according to semver:
- **Major**: breaking change to `BasePlugin` ABC, `PluginMeta` schema, or any model in the platform contract (`Asset`, `AssetType`, `AssetStatus`, `AssetRelationship`, `Vendor`, `Location`, `BusinessUnit`, `Model`, `OperatingSystem`).
- **Minor**: additive change (new optional hook, new field on a contract model with default).
- **Patch**: bug fix, no contract surface change.
2. **Plugin requirement** (`plugins/<name>/manifest.json`): the existing `core_version` field, expressed as a semver range (e.g., `">=1.0.0,<2.0.0"`).
The plugin loader (`shopdb/plugins/loader.py`) checks `core_version` against `__contract_version__` at load time. Mismatch in dev = re-raise (fail loud). Mismatch in prod = log error, mark plugin as incompatible, exclude from registration.
The `__contract_version__` starts at **`1.0.0`** when ADR-001 is accepted and the `Machine` retirement migration is complete (whichever comes later). Until then, the framework is pre-1.0; plugins should declare `core_version: ">=0.1.0,<1.0.0"`.
## Consequences
### Positive
- Sister sites can pin a known-good framework version. They will not be silently broken when the framework is upgraded.
- Plugin authors know what counts as a breaking change because the contract surface is enumerated in ADR-001.
- The loader fails predictably: a mismatched plugin is reported, not silently disabled.
### Negative / cost
- Discipline required: every change to the contract surface must be classified (major / minor / patch). Adding a `version-bump` skill (or a check in code review) reduces the chance of mis-classification.
- `__contract_version__` becomes a coupling point. Forgetting to bump it after a breaking change means downstream plugins crash silently at runtime instead of failing at install.
### Neutral
- Existing plugins (`plugins/printers/`, etc.) ship as part of the framework, so their `core_version` is always the current `__contract_version__`. The discipline matters mostly for external / sister-site plugins.
## Alternatives considered
1. **No versioning, just trust.** Works for an in-tree-only world. Fails the moment a sister site ships its own plugin. Rejected.
2. **Calendar versioning** (e.g., `2026.05.0`). Easier to bump, harder to communicate breaking changes. Rejected; semver is the industry standard for library-like contracts.
3. **Per-hook versioning.** Each hook has its own version. Too granular; plugins still couple to multiple hooks. Rejected.
## Open questions
- When does the framework declare `1.0.0`? Tied to ADR-001 (Asset retirement of Machine) and the framework being deemed "ready for sister sites". Best-effort target: end of Phase 5 in the refactor plan.
- Should `core_version` accept commercial-grade ranges (`^1.0.0`) or stick to PEP 440 / npm-style ranges? Recommend pip-style (`>=,<`) to match Python ecosystem.
## References
- `shopdb/plugins/base.py` (PluginMeta declaration)
- `shopdb/plugins/loader.py` (where the version check belongs)
- ADR-001 (defines what is in the contract)

View File

@@ -0,0 +1,68 @@
# ADR-003: Plugin distribution model
- **Status:** ACCEPTED
- **Date:** 2026-05-08
- **Deciders:** cproudlock
## Context
Sister sites adopting shopdb-flask need a way to:
1. Install the framework
2. Pick which plugins they want
3. Build their own plugins for site-specific equipment
4. Receive updates to both framework and plugins
Today, every plugin lives in the `plugins/` directory of the framework repo. There is no separation between framework code and plugin code, no install / uninstall, and no way for a site to develop a plugin without forking the whole repo.
Three viable distribution models:
| Model | How a site installs a plugin |
|---|---|
| **In-tree only** | Fork the framework repo, add plugin under `plugins/`, run their own deploy. No separation. |
| **Pip-installable plugins** | Each plugin published as a Python package. Site does `pip install shopdb-printers shopdb-network` etc. Discovery via Python entry points. Framework loads any installed plugin that registers itself. |
| **Git-based plugins** | Each plugin lives in its own git repo. Site clones / submodules into `plugins/<name>/`. Loader picks them up from the directory. |
## Decision
**PROPOSED:** Use a **hybrid model** with two clearly-labeled paths.
1. **Bundled plugins**: a small set of plugins ships with the framework, in-tree at `plugins/`. These are the reference implementations and the default install (printers, computers, network, equipment, usb, notifications). A site that wants only what's bundled needs no extra work.
2. **External plugins**: sister sites or third parties build plugins in their own git repos. The site running the framework drops the plugin into `plugins/<name>/` (clone, submodule, or symlink) and runs `flask plugin install <name>`. No pip packaging required for v1.
Pip-installable plugins (Python entry-point discovery) are deferred to v2. The complexity is not justified until at least two sites are running their own plugins.
## Consequences
### Positive
- v1 is simple: filesystem-based discovery (already implemented in `shopdb/plugins/loader.py`), works for both bundled and external plugins.
- Sites can develop plugins without changing the framework repo.
- The `plugins/` directory is already the canonical location, so no architectural change is needed.
### Negative / cost
- No automatic update path for external plugins. Sites must `git pull` in each plugin directory manually. Acceptable for v1; revisit when plugin count grows.
- Multiple plugin authors writing in parallel can collide on namespace (e.g., two plugins both registering an `AssetType` named "Equipment"). Need a naming policy: plugin names and asset-type names should be prefixed with the site or org if not in the bundled set.
### Neutral
- The existing in-tree pattern keeps working. This decision just formalizes it and clarifies the path for outside-the-tree plugins.
## Alternatives considered
1. **Pip-installable from day one.** Cleaner for the long term but adds packaging, entry-point registration, and CI steps. Premature for current scale (one site running, no sister-site plugins yet).
2. **In-tree only forever.** Forces every site to fork. Doesn't scale beyond two or three sites.
3. **Submodules only.** Forces git-submodule discipline on every adopting site. Submodules are notoriously fiddly. Rejected.
## Open questions
- For external plugins, should there be a manifest field (`source_url`) declaring where the plugin can be cloned from, so `flask plugin install` could pull it for the site? Defer; manual clone is fine for v1.
- Naming convention for non-bundled plugin directory names: prefix with site? (`gea-wjsf-shipping`)? Adopt if and when we hit a name collision.
## References
- `shopdb/plugins/loader.py` (filesystem discovery)
- `shopdb/plugins/cli.py` (plugin install / uninstall command)
- ADR-001 (defines what plugins target)
- ADR-002 (defines plugin version compatibility)

View File

@@ -0,0 +1,69 @@
# ADR-004: Deployment topology (per-site instances)
- **Status:** ACCEPTED
- **Date:** 2026-05-08
- **Deciders:** cproudlock
## Context
shopdb-flask manages shop-floor inventory. If multiple GE Aerospace sites adopt it, the deployment can take one of two shapes:
| Model | How it works |
|---|---|
| **Per-site instances** | Each site runs its own Flask + MySQL + Vue stack. Each site has its own DB, its own users, its own enabled-plugin list, its own deploy. Sites are isolated. |
| **Multi-tenant single instance** | One central Flask + MySQL + Vue stack serves all sites. A `siteid` foreign key on every asset partitions data. Auth distinguishes which site a user belongs to. |
The codebase today is single-tenant per deployment. There is no `siteid` column, no tenant filter, no cross-site auth model. Plugins can be enabled / disabled but only globally for the running instance.
## Decision
**PROPOSED:** **Per-site instances.** Each adopting site runs its own dedicated stack. The framework does not support multi-tenancy.
Each site:
- Owns its database (own credentials, own backup policy, own retention)
- Picks its own enabled plugins
- Configures its own JWT secret, CORS allowlist, Zabbix integration, Active Directory binding
- Deploys at its own cadence
The framework provides:
- A `Dockerfile` and `docker-compose.yml` template suitable for a single-site deploy
- A `.env.example` listing all required environment variables
- A `docs/DEPLOY.md` walking through a fresh-site install
## Consequences
### Positive
- Simpler code: no tenant filter on every query, no cross-tenant auth, no shared-state partitioning bugs.
- Sites are independent. A schema change at one site does not affect another. A plugin crash at one site does not blast radius to other sites.
- Clear ownership: each site's IT team owns their own stack and data. Compliance and audit boundaries match operational boundaries.
- Aligns with how GE Aerospace sites already operate (independent IT, independent shop floors).
### Negative / cost
- No cross-site reporting out of the box. If GE corporate ever wants a fleet-wide view, it has to be built on top (e.g., a roll-up dashboard that queries each site's API). That layer is out of scope for the framework.
- Each site administers its own stack. Higher operational overhead than a single central instance, but each site already runs its own infrastructure.
- Updates require visiting each site's deploy. Fine for the current adoption model; revisit if dozens of sites adopt.
### Neutral
- No `siteid` column needed. The existence of one DB per site is the partition.
## Alternatives considered
1. **Multi-tenant single instance.** Lower operational overhead at scale, easier cross-site reporting, but adds significant code complexity and risk: every query needs a tenant filter, auth gets complex, schema migrations affect every site at once, and a bug at one site can leak data across sites. Rejected for v1; revisit if and only if more than five sites adopt and operational overhead becomes painful.
2. **Hybrid: per-site DB but central app server.** Adds the operational complexity of multi-tenancy without isolating the failure domain (one app crash = all sites down). Rejected.
## Open questions
- Should the framework provide an optional **read-only fleet roll-up** mode where a "central" instance can pull aggregate metrics from each site's API? Defer. Out of scope for v1.
- Backup strategy per site: framework recommendation, or each site decides? Framework should publish a recommended backup runbook (mysqldump + offsite copy) but not enforce.
- Auth federation: each site has its own user table, or sites can share an LDAP / SSO? Recommend documenting the LDAP config knob in `.env.example` so sites can plug in their own auth without code change.
## References
- `shopdb/config.py` (currently single-tenant, no `siteid`)
- ADR-001 (asset model is per-site, not cross-site)
- ADR-003 (plugin distribution per site)

View File

@@ -0,0 +1,149 @@
# ADR-005: Equipment plugin scope vs measuringtools plugin
- **Status:** ACCEPTED
- **Date:** 2026-05-08
- **Deciders:** cproudlock
- **Supersedes:** none
## Context
ADR-001 narrowed the migration to physical manufacturing equipment with a `machinenumber`. In practice, the legacy `category='Equipment'` rows contain two distinct asset classes:
1. **Manufacturing machinery** (5-axis mills, lathes, broachers, heat treatment ovens). These produce parts.
2. **Metrology and inspection instruments** (CMMs, Keyence vision systems, wax-and-trace surface profilometers, GenSpec instruments). These measure parts.
Both share `Asset` properties (vendor, model, location, controller). They differ in domain fields (axes vs measurement accuracy, cycle time vs calibration interval, controller protocol vs measurement software).
Mixing them under one plugin pollutes the schema and confuses cross-plugin queries ("show me all measuring tools" requires an enumeration of measuring-instrument equipmenttype values, which scales badly).
## Decision
Two plugins, separate concerns, shared platform contract.
### `equipment` plugin
Tracks manufacturing machinery. Bundled, in-tree.
Schema (per ADR-001 contract):
```
equipment
- assetid FK to assets, PK
- equipmenttypeid FK to equipmenttypes (5-axis mill, lathe, broacher, heat treat, ...)
- vendorid FK to vendors (platform)
- modelid FK to models (platform)
- controllertypeid FK to controllertypes (equipment plugin)
- controllerosid FK to controlleros (equipment plugin)
- (other shared fields: spindle count, axes, max workpiece size, ...)
equipmenttypes (lookup, equipment plugin)
- equipmenttypeid, name (5-axis mill, lathe, broacher, heat treat, ...)
controllertypes (lookup, equipment plugin)
- controllertypeid, name (Fanuc 31i, Siemens 840D, Mitsubishi M70, Heidenhain TNC640, ...)
- vendorid (FK to vendors)
controlleros (lookup, equipment plugin - separate from PC OS)
- controllerosid, name (FAPT, VxWorks, embedded Windows, Linux RT, ...)
equipmentfocas (subtype, optional, present only when FOCAS-equipped)
- assetid PK, FK to equipment
- focasipaddress text
- focasport integer
- focasversion text
- focasmachinenumber text
equipmentclm (subtype, optional, present only when CLM-equipped)
- assetid PK, FK to equipment
- (CLM-specific: address, port, station ID - finalize when plugin is built)
equipmentmtconnect (subtype, optional, present only when MTConnect-equipped)
- assetid PK, FK to equipment
- mtconnectagenturl text
- mtconnectdevicename text
```
The `equipment.protocol` enum field is deliberately **not** included. Presence or absence of a subtype row indicates which protocol applies. Avoids a denormalized field that can drift out of sync.
### `measuringtools` plugin
Tracks metrology and inspection instruments. Bundled, in-tree (built in Phase 3-4 of the refactor as the first new plugin built using the framework scaffold).
Schema (initial draft, refined when plugin is built):
```
measuringtools
- assetid FK to assets, PK
- measuringtooltypeid FK to measuringtooltypes (CMM, vision system, profilometer, surface tester, ...)
- vendorid FK to vendors (platform)
- modelid FK to models (platform)
- measurementaxes integer (e.g., 3 for a 3-axis CMM)
- accuracyspec text (e.g., "+/-0.5um")
- calibrationintervaldays integer
- lastcalibrationdate date
- nextcalibrationdate date (computed)
- (other domain fields as needed)
measuringtooltypes (lookup, measuringtools plugin)
- measuringtooltypeid, name (CMM, vision system, surface profilometer, gage block, ...)
```
Future extension: subtype tables for measurement-software integrations (PC-DMIS, Keyence, GenSpec). Same pattern as equipment subtype tables.
### Subtype-table pattern (general)
Both plugins use the same pattern for protocol- or software-specific fields:
- Core plugin table carries shared, common fields
- Optional subtype tables (one per protocol or software) hold extension fields
- Each subtype table is keyed by `assetid` (PK), one-to-one with the parent
- Subtype row exists if and only if the asset uses that protocol or software
- Sister sites add new subtype tables for their own integrations without touching core
## Reclassification of legacy data
ADR-001's migration moves all legacy `category='Equipment' AND machinenumber IS NOT NULL` rows to `assets` with `assettype='Equipment'` and into the equipment plugin's `equipment` table. This includes both manufacturing machinery and measuring tools.
After the equipment migration, when the measuringtools plugin is built:
1. Build a mapping table: legacy `machinetypeid` values that are measuring tools (CMM type, Keyence type, etc.)
2. Run a reclassification script:
- For each `assets` row where the original `machinetypeid` is in the measuring-tool mapping
- Change `assets.assettype` to `'MeasuringTool'`
- Move the row from `equipment` to `measuringtools`
- Map domain fields where they differ (e.g., legacy `axes` field maps to `measurementaxes`)
3. Verify counts pre- and post-reclassification
4. Audit log entry per reclassified row
Reclassification is one-shot, run once, archived. Like the original migration script.
## Consequences
### Positive
- Manufacturing machinery and measuring tools are first-class plugins, each with appropriate domain fields
- Sister sites can install one or both depending on what they track
- Subtype-table pattern is the canonical example for protocol-specific data and extends naturally to other plugins
- Building `measuringtools` mid-refactor validates the plugin scaffold tooling against a real new plugin
### Negative
- Reclassification is a second migration step. Lower risk than the initial migration because it is data-only (no schema change beyond moving rows between two tables that share the same `assetid` link).
- Sites that adopt the framework before `measuringtools` ships need to either keep measuring tools in `equipment` (workable but suboptimal) or wait for the plugin
### Neutral
- Legacy `machinetypeid` is preserved on the equipment row during migration to enable reclassification
## Alternatives considered
1. **Single equipment plugin with sub-typed assets.** Use `equipment.equipmenttypeid` to discriminate manufacturing vs metrology. Rejected: domain fields differ enough that a single table is wide and full of NULLs.
2. **Migrate split (build mapping before initial migration).** Cleaner end state but requires the `measuringtools` plugin to exist before the migration runs, which delays Phase 5. Rejected.
3. **JSON blob for protocol data instead of subtype tables.** Considered for both plugins. Rejected: weak typing, awkward queries, no schema validation.
## References
- ADR-001 (Asset is platform contract)
- ADR-002 (versioning of the surface)
- `plugins/equipment/` (current placeholder)
- `plugins/computers/` (existing example of plugin pattern)

View File

@@ -0,0 +1,131 @@
# ADR-006: Plugin collector contract pattern
- **Status:** ACCEPTED
- **Date:** 2026-05-08
- **Deciders:** cproudlock
- **Supersedes:** none
## Context
PC inventory data was collected by PowerShell scripts pushing to `/api/collector/pc` (`shopdb/core/api/collector.py`, ~374 LOC). The endpoint is hardcoded for PCs: it accepts a fixed schema and writes to the legacy `Machine` model.
Per ADR-001, `Machine` is being retired in favor of `Asset`. Per the project shift to PXE-driven imaging, PC inventory is moving to a new collection pipeline (PXE / GE-Enforce / manifest engine produces JSON about each PC). Other asset classes may want similar collector pipelines (printers via Zabbix, network gear via SNMP scan).
This calls for a generalizable contract: any plugin that wants to accept external collector input declares a JSON schema, and the framework wires the endpoint, auth, and idempotency.
## Decision
`BasePlugin` gets one new optional hook:
```python
def get_collector_schema(self) -> Optional[dict]:
"""Return JSON Schema describing the collector payload for this plugin.
Return None if the plugin does not accept collector input.
The schema must include:
- 'identityfield': name of the field that uniquely identifies an asset
across submissions (e.g., 'hostname' for PCs, 'macaddress' for network
devices). Used for idempotent upsert.
- 'fields': JSON Schema definitions for the rest of the payload.
"""
return None
```
Plugin loader auto-registers an endpoint at `/api/collector/<pluginname>` for each plugin returning a schema. Auth is API-key, separate from JWT. Per-plugin keys via env vars:
- `COLLECTOR_API_KEY_<PLUGINNAME>` (preferred, plugin-specific)
- `COLLECTOR_API_KEY` (fallback, shared)
### Idempotent upsert
The endpoint uses the `identityfield` to find an existing `Asset` for the same identity. Found = update. Not found = insert. Existing relationships are preserved on update.
### Response contract
```json
{
"status": "ok",
"action": "created" | "updated" | "noop",
"assetid": 12345,
"identityvalue": "PC-1234",
"warnings": []
}
```
### Audit logging
Every collector submission produces an audit log entry: `{action, plugin, identityvalue, before/after diff}`. Audit retention per site policy.
### Schema discovery
The framework exposes the registered schemas at `/api/collector/_schemas` (read-only, JWT-protected) so external collector authors can introspect what payloads are accepted by which plugins.
## Concrete first user: computers plugin
The `computers` plugin is the first to implement `get_collector_schema`. The PXE pipeline conforms.
Initial computers collector schema (sketch, finalized when plugin is built):
```json
{
"identityfield": "hostname",
"fields": {
"hostname": "string, required",
"macaddress": "string, optional, secondary identity",
"osname": "string",
"osversion": "string",
"lastboottime": "datetime",
"currentuser": "string",
"ipaddress": "string",
"memorygb": "number",
"cputype": "string",
"imagename": "string (PXE image deployed)",
"imageappliedat": "datetime",
"installedsoftware": "array of {name, version}"
}
}
```
The PC re-image case is handled by the identity field: a freshly imaged PC keeps its hostname, so the existing `Asset` row is updated rather than duplicated. Existing `AssetRelationship` rows pointing at that PC (e.g., `controls` to a machine) are preserved across re-images.
## Migration of the existing endpoint
`shopdb/core/api/collector.py` (`/api/collector/pc`) is **deprecated** in v1 and **removed** before v1.0.
Migration path:
1. Implement `get_collector_schema` on the `computers` plugin. New endpoint `/api/collector/computers` is auto-registered.
2. Run both endpoints in parallel for one cycle of PXE imaging across the floor. PXE pipeline switches to `/api/collector/computers`.
3. Remove `shopdb/core/api/collector.py` and the legacy blueprint registration.
## Consequences
### Positive
- Generalizable across plugins. Sister sites adopting `printers`, `network`, etc. can wire their own collectors with no core change.
- Identity-based idempotency makes PC re-imaging safe by default.
- Audit logging is uniform across plugins.
- Schema discovery enables external tools to validate before submission.
### Negative
- Plugin authors must write a JSON schema. Slight learning curve, but JSON Schema is widely understood and the framework can ship a few examples.
- The `/api/collector/_schemas` endpoint plus per-plugin endpoints expand the public API surface; minor maintenance cost.
### Neutral
- API-key auth pattern stays as it is today (separate from JWT). Sites manage their own collector keys per plugin via env vars.
## Alternatives considered
1. **Keep `/api/collector/pc` and add new plugin-specific endpoints alongside.** Two ways to send PC data, plugin authors confused. Rejected.
2. **Use JWT for collectors instead of API key.** Collectors are headless processes (PXE pipeline, scripts), not interactive users. JWT lifecycle (refresh tokens, expiry) is the wrong tool. API key is simpler. Rejected.
3. **Plugins write directly to the database, no collector endpoint.** Skips audit logging and schema validation. Rejected.
## References
- `shopdb/core/api/collector.py` (legacy endpoint to be removed)
- `shopdb/plugins/base.py` (`get_collector_schema` hook to be added)
- ADR-001 (asset model the collectors target)
- ADR-002 (collector schema is part of plugin contract; changes to the hook signature are major bumps)
- The PXE project (`/home/camp/projects/pxe/`) which feeds the computers collector

25
migrations/adr/README.md Normal file
View File

@@ -0,0 +1,25 @@
# Architecture Decision Records
Each ADR captures a single architectural decision: the context, the decision itself, the consequences, and the alternatives considered. ADRs are immutable once accepted. Superseded ADRs stay in this folder with a pointer to the newer ADR.
## Status definitions
- **PROPOSED**: drafted, awaiting decision
- **ACCEPTED**: decision is in effect
- **SUPERSEDED**: replaced by a later ADR (link forward)
- **DEPRECATED**: no longer in effect, no replacement
## Index
| ADR | Title | Status |
|-----|-------|--------|
| [001](ADR-001-asset-as-platform-contract.md) | Asset model is the platform contract | ACCEPTED |
| [002](ADR-002-plugin-versioning.md) | Plugin contract versioning (semver) | ACCEPTED |
| [003](ADR-003-plugin-distribution.md) | Plugin distribution model | ACCEPTED |
| [004](ADR-004-deployment-topology.md) | Deployment topology (per-site instances) | ACCEPTED |
| [005](ADR-005-equipment-vs-measuringtools.md) | Equipment vs measuringtools plugin scope | ACCEPTED |
| [006](ADR-006-collector-contract.md) | Plugin collector contract pattern | ACCEPTED |
## Authoring
When proposing a new decision, copy the most recent ADR as a template, increment the number, and update this index. Do not edit accepted ADRs in place; supersede them with a new one.

118
scripts/check-naming-and-style.sh Executable file
View File

@@ -0,0 +1,118 @@
#!/usr/bin/env bash
#
# Pre-commit naming + style check for shopdb-flask.
# Enforces CONTRIBUTING.md rules:
# 1. No non-ASCII chars in source (em-dashes, smart quotes, arrows, emojis)
# 2. No banned shorthand identifiers (cfg, ctx, mgr, req, res, env, util, helper)
# as standalone names (suffix usage like printers_bp, request_obj is allowed)
# 3. No snake_case DB column names in __tablename__ or db.Column attrs
# 4. No snake_case API params in frontend that should match DB column names
#
# Exits non-zero if any violation found.
# Skips: venv/, node_modules/, __pycache__/, frontend/dist/, migrations/versions/
set -e
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
cd "$REPO_ROOT"
VIOLATIONS=0
EXCLUDES=(
--exclude-dir=venv
--exclude-dir=node_modules
--exclude-dir=__pycache__
--exclude-dir=dist
--exclude-dir=.git
--exclude-dir=versions
)
INCLUDES_CODE=(
--include='*.py'
--include='*.vue'
--include='*.js'
--include='*.ts'
)
INCLUDES_ALL=(
--include='*.py'
--include='*.vue'
--include='*.js'
--include='*.ts'
--include='*.json'
--include='*.md'
--include='*.yaml'
--include='*.yml'
)
echo "==> Checking for non-ASCII characters..."
NON_ASCII=$(grep -rPn '[^\x00-\x7F]' "${EXCLUDES[@]}" "${INCLUDES_CODE[@]}" . 2>/dev/null || true)
if [ -n "$NON_ASCII" ]; then
echo "FAIL: non-ASCII characters found (em-dashes, smart quotes, arrows, emojis):"
echo "$NON_ASCII"
echo
VIOLATIONS=$((VIOLATIONS + 1))
fi
echo "==> Checking for banned shorthand (standalone)..."
# Match the word as a standalone identifier: not preceded or followed by underscore/word char
# Word boundary in grep is \b but we want to exclude suffix usage like printers_bp
# So: match (^|[^a-zA-Z0-9_])(banned)([^a-zA-Z0-9_]|$)
for word in cfg ctx mgr req res; do
HITS=$(grep -rPn "(^|[^a-zA-Z0-9_])${word}([^a-zA-Z0-9_]|\$)" "${EXCLUDES[@]}" --include='*.py' --include='*.vue' --include='*.js' --include='*.ts' . 2>/dev/null \
| grep -vP "(^|[^a-zA-Z0-9_])(request_obj|response_obj)" \
|| true)
if [ -n "$HITS" ]; then
echo "FAIL: banned shorthand '$word' (standalone) found:"
echo "$HITS"
echo
VIOLATIONS=$((VIOLATIONS + 1))
fi
done
echo "==> Checking for snake_case DB tablenames..."
SNAKE_TABLES=$(grep -rPn "__tablename__\s*=\s*['\"][^'\"]*_" "${EXCLUDES[@]}" --include='*.py' . 2>/dev/null || true)
if [ -n "$SNAKE_TABLES" ]; then
echo "FAIL: snake_case __tablename__ found (must be lowercase concatenated):"
echo "$SNAKE_TABLES"
echo
VIOLATIONS=$((VIOLATIONS + 1))
fi
echo "==> Checking for snake_case DB column attrs..."
SNAKE_COLS=$(grep -rPn "^\s+[a-z]+_[a-z_]+\s*=\s*db\.Column" "${EXCLUDES[@]}" --include='*.py' . 2>/dev/null || true)
if [ -n "$SNAKE_COLS" ]; then
echo "FAIL: snake_case db.Column attribute found (must match column name, no underscores):"
echo "$SNAKE_COLS"
echo
VIOLATIONS=$((VIOLATIONS + 1))
fi
echo "==> Checking for snake_case ForeignKey targets..."
SNAKE_FK=$(grep -rPn "ForeignKey\(['\"][^'\"]*_[^'\"]*['\"]" "${EXCLUDES[@]}" --include='*.py' . 2>/dev/null || true)
if [ -n "$SNAKE_FK" ]; then
echo "FAIL: snake_case ForeignKey target found:"
echo "$SNAKE_FK"
echo
VIOLATIONS=$((VIOLATIONS + 1))
fi
echo "==> Checking for snake_case API params in frontend (DB-mirrored fields)..."
SNAKE_FE=$(grep -rPn "params\.(machine_id|location_id|vendor_id|type_id|business_unit_id|model_id|status_id|operating_system_id|asset_id|user_id|is_active|is_shopfloor)" "${EXCLUDES[@]}" --include='*.vue' --include='*.js' --include='*.ts' . 2>/dev/null || true)
if [ -n "$SNAKE_FE" ]; then
echo "FAIL: snake_case API params in frontend (must match DB column names without underscores):"
echo "$SNAKE_FE"
echo
VIOLATIONS=$((VIOLATIONS + 1))
fi
if [ "$VIOLATIONS" -gt 0 ]; then
echo "=================================================="
echo "$VIOLATIONS naming/style violation(s) found."
echo "See CONTRIBUTING.md for the full convention."
echo "=================================================="
exit 1
fi
echo "==> All naming/style checks passed."
exit 0

View File

@@ -108,16 +108,16 @@ class AuditLog(db.Model):
username = user.username if user else None
# Get request info
req = request or flask_request
request_obj = request or flask_request
ipaddress = None
useragent = None
if req:
if request_obj:
# Handle proxy forwarding
ipaddress = req.headers.get('X-Forwarded-For', req.remote_addr)
ipaddress = request_obj.headers.get('X-Forwarded-For', request_obj.remote_addr)
if ipaddress and ',' in ipaddress:
ipaddress = ipaddress.split(',')[0].strip()
useragent = req.headers.get('User-Agent', '')[:255]
useragent = request_obj.headers.get('User-Agent', '')[:255]
entry = cls(
userid=userid,

View File

@@ -4,26 +4,26 @@ from flask import request, current_app
from typing import Tuple
def get_pagination_params(req=None) -> Tuple[int, int]:
def get_pagination_params(request_obj=None) -> Tuple[int, int]:
"""
Extract pagination parameters from request.
Returns:
Tuple of (page, per_page)
"""
if req is None:
req = request
if request_obj is None:
request_obj = request
default_size = current_app.config.get('DEFAULT_PAGE_SIZE', 20)
max_size = current_app.config.get('MAX_PAGE_SIZE', 100)
try:
page = max(1, int(req.args.get('page', 1)))
page = max(1, int(request_obj.args.get('page', 1)))
except (TypeError, ValueError):
page = 1
try:
per_page = int(req.args.get('perpage', req.args.get('per_page', default_size)))
per_page = int(request_obj.args.get('perpage', request_obj.args.get('per_page', default_size)))
per_page = max(1, min(per_page, max_size))
except (TypeError, ValueError):
per_page = default_size