An asset that carries a model but no vendor was showing a blank the database
could already answer: the model records its vendor, and both sides reference the
same vendors table. Machines, PCs, printers and network devices now fall back to
it.
The fallback is FLAGGED, not merged silently. to_dict sets vendorfrommodel and
the detail pages render "(from model)" beside the value, because the record
itself is still empty: the edit form shows an empty vendor box, and a page
implying the vendor is stored would be lying about where it came from.
The model's type is exposed under its own name, modeltypename, and shown as a
separate "Model type" row. It is deliberately NOT used to fill in the asset's
own type. modeltypes is the catalog-wide list covering every kind of asset - it
holds "Access Point", "Camera" and "Desktop PC" alongside the machine entries -
so it is a different taxonomy from machinetypes. Only about two thirds of the
names overlap, and mapping one onto the other would mistype the remainder, with
the failure mode being a machine labelled "Desktop PC".
scripts/backfill_vendor_from_model.py writes the derived vendor down for real,
since the display fallback leaves reports that read vendorid still seeing
nothing. It is a dry run unless given --commit, fills only rows where the
asset's vendor is NULL and the model names one, and never overwrites a vendor
somebody chose. It skips a table lacking either column, so it runs against a
server whose network migration has not been applied yet.
Verified against the development database by nulling one machine's vendor inside
a transaction: it was detected as fillable, restored to exactly its original
value, and the rollback left the row untouched.
FLASK_ENV is not forced by the script. The app already reads it from .env, and
overriding it demanded a SECRET_KEY the environment had no reason to supply.
Three faults around vendor-model photos, found while looking at why an uploaded
image did not appear.
Saving a model was blocked after uploading a photo. The Image URL field was
type="url", and an upload sets it to an application path such as
/api/models/image/model-120.png. Native url validation demands an absolute URL
with a scheme, so the browser refused to submit the form with "Please enter a
URL" for a value the page had just written itself. The field is now type="text",
which is what it always needed to be: it holds either a full web address or a
path on this server. documentationurl stays type="url".
The upload button did not appear when adding a model, only when editing one.
That was deliberate - the photo is stored as model-<id>.<ext>, so it cannot be
sent before the record has an id - but it reads as a missing feature, and the
hint explaining it was easy to miss. A photo chosen while creating is now held
and uploaded as soon as the model is saved, and it is dropped if the dialog is
cancelled, so it cannot land on the next model created in the same session.
Network devices could never show a photo. NetworkDeviceDetail.vue binds its hero
image to networkdevice.imageurl, but networkdevices carried only vendorid, with
no link to a catalog model, so nothing could populate it - a feature that looked
present and could not work. Machines, PCs and printers have carried
modelnumberid since July. This adds the same column and relationship, the
to_dict branch that exposes modelname and imageurl, the field on the API, and a
Model selector on the form so the link can actually be set.
The migration is guarded the same way employees0002photo is: on a fresh database
the tables come from the SQLAlchemy models, which already declare the column, so
an unconditional add fails with "duplicate column name". The foreign key is
created only on databases that can add one by ALTER; routing it through
batch_alter_table made Alembic's column sort raise "Circular dependency
detected" on the fresh-database test.
Deploying this needs `flask db upgrade` and `flask plugin upgrade-all` on the
server, not just a file copy.
From the database review (verdict: sound-with-minor-issues). Applies the
actionable findings.
Redundant indexes: five non-unique secondary indexes duplicated a named idx_*
or a unique index on the same column - ix_communications_assetid,
ix_computers_hostname, ix_networkdevices_hostname, ix_printers_hostname (each
shadowing an idx_*), and idx_usb_serial (shadowing the serialnumber unique
index). Removed the redundant index source from the models (column index=True /
the extra db.Index) and added core migration 7d25 dropping the live duplicates.
The unique ix_*_assetid indexes are kept (they enforce assetid uniqueness).
Dead column: usbcheckouts.machineid was a NOT NULL soft-ref to the retired
machines table storing sentinel 0 (ADR-001). Dropped from the model + the
machineid=0 literal in selfhosted checkout; usb plugin migration 0002 drops it
live (downgrade restores it default 0).
Index: notifications.businessunitid (filtered by the shopfloor feed) was
unindexed; added index=True + notifications migration 0002.
CI: new migrations-mysql job proves the real multi-site deploy path - fresh
`flask db upgrade` + per-plugin install on utf8mb4 MySQL from empty, asserting
table count + charset and a clean second-run no-op. The pytest suite only
exercises SQLite create_all(), so a regression in the Alembic chain on MySQL
would otherwise ship undetected.
Verified: fresh core upgrade on a scratch utf8mb4 MySQL builds clean + no-op on
rerun (redundant indexes absent, unique assetid kept); plugin migrations applied
+ verified on the dev DB (machineid gone, bu index present). 953 backend tests
pass; naming + pyflakes green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Feature work from the 2026-07 session:
Settings IA
- Replace the flat 27-card settings hub with a persistent two-pane shell
(SettingsLayout.vue): grouped, searchable left rail + content pane.
- Nest all settings/* routes under the shell via router post-processing;
shared nav catalog in settingsNav.js. Group by asset class (PCs, Printers,
Equipment, Network) so per-type settings stop scattering.
Custom fields (core)
- customfields + customfieldvalues tables (migration 7d14), CRUD API at
/api/customfields, per-asset value get/save.
- Settings management page + reusable CustomFieldsSection (detail) and
CustomFieldsInputs (form) wired into all four asset types.
Warranty (new plugin)
- plugins/warranty: warranties + warrantyassets (migration 7d15), derived
coverage status, provider abstraction (manual now; Dell/Lenovo/HP stubs).
- API CRUD + per-asset panel + report buckets; WarrantyPanel on all four
detail pages; Warranties management page; Warranty report + Reports card.
- Seed warranty.* permissions.
Printer drivers
- printerdrivers table (migration 7d13) linked to printer models; drivers now
surface on the matching printer's detail page.
Other
- PCDetail rebalanced (Network + Status + Warranty + custom fields on the right).
- Rename PCs list "Features" column to "Remote Access"; fix badge hover underline.
- Drop equipment islocationonly field.
- Centralize asset-type label/route maps into utils/assetTypes.js.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plugins were reaching into internal core paths (shopdb.core.models.*,
shopdb.extensions, shopdb.utils.*), coupling them to core's file layout and
violating the ADR-001 contract. Consolidate onto one versioned surface.
- shopdb.api: expand from 2 helpers to the full plugin import surface -
db, cache; BaseModel, AuditMixin; core models (Asset, AssetType,
AssetStatus, Vendor, Model, Communication, CommunicationType, Location,
Setting, AuditLog, Application, AppVersion, OperatingSystem); response +
pagination helpers; employee_connection. Documented in PLUGIN-HOOKS.md.
- Migrate all 22 plugin source files to import only from shopdb.api (plus
shopdb.plugins.base for the ABC).
- Drop the printers plugin's legacy MachineType dependency: remove
_ensure_legacy_machine_types and the seed_supplies machinetypeid lookup
(Model.machinetypeid is nullable; printers carry type via PrinterType).
- Guard test test_plugins_only_import_contract_surface scans plugin source
and fails on any core import outside shopdb.api / shopdb.plugins.base.
- Scaffold templates updated so generated plugins are contract-pure.
- Bump __contract_version__ 0.2.0 -> 0.3.0 (additive surface expansion;
manifests pin <1.0.0 so they still satisfy).
145 tests pass, naming/style green, app factory boots all 6 plugins.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Fix equipment badge barcode not rendering (loading race condition)
- Fix printer QR code not rendering on initial load (same race condition)
- Add model image to equipment badge via imageurl from Model table
- Fix white-on-white machine number text on badge, tighten barcode spacing
- Add PaginationBar component used across all list pages
- Split monolithic router into per-plugin route modules
- Fix 25 GET API endpoints returning 401 (jwt_required -> optional=True)
- Align list page columns across Equipment, PCs, and Network pages
- Add print views: EquipmentBadge, PrinterQRSingle, PrinterQRBatch, USBLabelBatch
- Add PC Relationships report, migration docs, and CLAUDE.md project guide
- Various plugin model, API, and frontend refinements
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
New Plugins:
- USB plugin: Device checkout/checkin with employee lookup, checkout history
- Notifications plugin: Announcements with types, scheduling, shopfloor display
- Network plugin: Network device management with subnets and VLANs
- Equipment and Computers plugins: Asset type separation
Frontend:
- EmployeeSearch component: Reusable employee lookup with autocomplete
- USB views: List, detail, checkout/checkin modals
- Notifications views: List, form with recognition mode
- Network views: Device list, detail, form
- Calendar view with FullCalendar integration
- Shopfloor and TV dashboard views
- Reports index page
- Map editor for asset positioning
- Light/dark mode fixes for map tooltips
Backend:
- Employee search API with external lookup service
- Collector API for PowerShell data collection
- Reports API endpoints
- Slides API for TV dashboard
- Fixed AppVersion model (removed BaseModel inheritance)
- Added checkout_name column to usbcheckouts table
Styling:
- Unified detail page styles
- Improved pagination (page numbers instead of prev/next)
- Dark/light mode theme improvements
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>