main was 16 days and 259 commits behind the line the product is actually built on, at contract 0.30.0 against 0.35.0, with no per-PC credential code at all. It was neither current nor curated - just stale. This brings it forward and, at the same time, makes it mean something: main is what a receiving site installs, and nothing else. WHAT IT CARRIES. Twelve plugins - backups, computers, endpoints, guides, knowledgebase, machines, network, notifications, printers, slides, tools, warranty - plus applications, which is manifest-less core and always ships. The only manifest dependency inside that set is endpoints on computers, and both are in it, so the closure is self-contained. WHAT IT DOES NOT. employees, geenforce, measuringtools, printedparts and usb stay on the site line. So do the Windows installer tree, the VM rehearsal helpers, the agent tooling config, one site's data loader, the internal audit and findings records, and the planner spreadsheet. 1,135 tracked files against 1,438. A REMOVAL THAT BREAKS THE APP IS NOT A REMOVAL, and two nearly shipped: deploy/site-profile-universal.json still named all seventeen plugins. It is the file docs/DEPLOY.md tells a site to provision from, so a fresh install would have failed at `flask plugin apply-profile` with PluginError - the first thing a receiving site does, broken by the branch meant to serve them. It now names the twelve that exist, and its comment says the two must agree. agent/ was never refreshed. Whoever populated this branch copied shopdb/, plugins/, frontend/ and tests/ from the site line and left the agent tree at its September state: 22 C# and PowerShell sources missing, including AgentCredentials, EnrollmentService, AgentHttpFactory and the fleet-alert client, plus one file the site line had deleted still present. The old tree is internally consistent, so it compiles, and nothing in CI builds it - every gate was green over a server that speaks enrollment and per-PC credentials to a bundled agent source that cannot. Resynced whole. DECISIONS THIS ENCODES, rather than defers: - The plugin-authoring lab builds printedparts, whose manifest depends on employees, so it could not stay. The lab remains on the site line and main ships no build-along tutorial. That is a real loss for an adopting developer and is recorded here rather than discovered later. - Recognition, Training and Recertification notifications are no longer seeded. Each splits one notification into a card per employee and renders a photo, which needs a plugin this branch does not bundle. Seeding them would offer an author a type that cannot resolve anybody. A site that installs employees gets them from that plugin's own seed. - Windows/IIS is documented as the legacy path throughout, and the README leads with Docker, because that is what a receiving site gets. The API description is regenerated for THIS branch - 347 paths and 483 operations, down from 415 and 573 - so the published contract describes what is here rather than what the site line serves. docs/CONFIG.md lost the two measuring-tool label keys the code no longer declares, which two lanes had decided opposite ways. Suite: 3124 passed, 6 skipped. Naming and style gates pass. The docs gates run rather than skip, and every index line count is derived from the file it names.
201 lines
9.8 KiB
Markdown
201 lines
9.8 KiB
Markdown
<!-- docmeta
|
|
purpose: explanation
|
|
audience: developer
|
|
status: current
|
|
summary: Why Asset, not the legacy Machine model, is the surface plugin authors may depend on.
|
|
loadwith: none
|
|
-->
|
|
|
|
# ADR-001: Asset model is the platform contract
|
|
|
|
- **Status:** ACCEPTED
|
|
- **Date:** 2026-05-08
|
|
- **Deciders:** ShopDB maintainers
|
|
- **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
|
|
- `resolve_asset_position(asset)` for the documented position-resolution algorithm
|
|
|
|
#### Import surface (`shopdb.api`) - expanded in __contract_version__ 0.3.0
|
|
|
|
`shopdb.api` is the ONLY core module plugins may import (plus `shopdb.plugins.base`
|
|
for the ABC). Deep imports of `shopdb.core.*`, `shopdb.extensions`, or
|
|
`shopdb.utils.*` are contract violations enforced by the
|
|
`test_plugins_only_import_contract_surface` test. The surface re-exports:
|
|
|
|
- Infrastructure: `db`, `cache`
|
|
- Model bases: `BaseModel`, `AuditMixin`
|
|
- Core models: `Asset`, `AssetType`, `AssetStatus`, `Vendor`, `Model`,
|
|
`Communication`, `CommunicationType`, `Location`, `Setting`, `AuditLog`,
|
|
`Application`, `AppVersion`, `OperatingSystem`
|
|
- Responses: `success_response`, `error_response`, `paginated_response`, `ErrorCodes`
|
|
- Pagination: `get_pagination_params`, `paginate_query`
|
|
- Helpers: `audit_log`, `resolve_asset_position`; legacy `employee_connection`
|
|
|
|
Adding a name here is a minor (additive) contract change; removing one is major.
|
|
|
|
#### Plugin contract
|
|
|
|
- `BasePlugin` ABC and its hooks (navigation, dashboard widgets, collector schema + `apply_collector_payload`). Note: `get_searchable_fields` was removed in contract 0.4.0 - global search is a core concern over the asset model, not a per-plugin hook.
|
|
|
|
#### 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`
|
|
- (core `Machine` model retired per this decision; machine data now owned by the machines plugin at `plugins/machines/models/machine.py`)
|
|
- `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)
|