From d187a2c535d85b2c4630998443596b5d2e8ff00a Mon Sep 17 00:00:00 2001 From: cproudlock Date: Sun, 12 Jul 2026 13:37:58 -0400 Subject: [PATCH] Fix PC installed-applications rendering; minor UI cleanups The PC detail Installed Applications section 500d and vanished on any real PC: ComputerInstalledApp had no to_dict, so the endpoint errored and the v-if hid the section. Added the serializer (curated version wins over the raw collected string, app name + description included) and aligned PCDetail to the flat payload; regression test added. Also: employee detail skips its USB panels when the usb plugin is disabled (was firing 404s), and the shopfloor kiosk header is now light-on-dark for readability. 810 tests pass; PC 259 installed apps verified live. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 10 ++++ frontend/src/views/ShopfloorDashboard.vue | 3 +- .../src/views/employees/EmployeeDetail.vue | 14 ++++- frontend/src/views/pcs/PCDetail.vue | 10 ++-- plugins/computers/models/computer.py | 19 +++++++ tests/test_plugins/test_installed_apps.py | 57 +++++++++++++++++++ 6 files changed, 104 insertions(+), 9 deletions(-) create mode 100644 tests/test_plugins/test_installed_apps.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b0a9ce2..5fc7f33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,16 @@ ADR-007 and ADR-002. ## [Unreleased] +### Fixed + +- PC detail Installed Applications no longer 500s and silently disappears on + real PCs (ComputerInstalledApp had no to_dict); the section renders app + name, version, and description again. +- Employee detail skips its USB panels when the usb plugin is disabled (no + more 404 console noise). +- Shopfloor kiosk header text is readable (light on the dark navy header). + + ### Added - Single-label sheet-position printing. The single asset-label page diff --git a/frontend/src/views/ShopfloorDashboard.vue b/frontend/src/views/ShopfloorDashboard.vue index 8e462d9..5faa96a 100644 --- a/frontend/src/views/ShopfloorDashboard.vue +++ b/frontend/src/views/ShopfloorDashboard.vue @@ -444,7 +444,7 @@ function handlePhotoError(e) { .location-title { font-size: 18px; font-weight: 600; - color: #888; + color: #b8c4d8; text-transform: uppercase; letter-spacing: 2px; } @@ -452,6 +452,7 @@ function handlePhotoError(e) { .header-center h1 { font-size: 28px; font-weight: 700; + color: #ffffff; text-transform: uppercase; letter-spacing: 2px; margin: 0; diff --git a/frontend/src/views/employees/EmployeeDetail.vue b/frontend/src/views/employees/EmployeeDetail.vue index 7d7b260..ed769bb 100644 --- a/frontend/src/views/employees/EmployeeDetail.vue +++ b/frontend/src/views/employees/EmployeeDetail.vue @@ -64,7 +64,7 @@ -
+

Checked Out USB Devices

Loading...
@@ -99,7 +99,7 @@
-
+

USB Checkout History

Loading...
@@ -136,6 +136,7 @@ import { ref, computed, onMounted } from 'vue' import { useRoute } from 'vue-router' import { employeesApi, usbApi, notificationsApi } from '@/api' +import { isPluginEnabled, loadEnabledPlugins } from '@/composables/enabledPlugins' import { useToast } from '../../composables/toast' const toast = useToast() @@ -145,6 +146,7 @@ const employee = ref(null) const recognitions = ref([]) const usbDevices = ref([]) const checkoutHistory = ref([]) +const usbEnabled = ref(true) const loading = ref(true) const recognitionsLoading = ref(true) const usbLoading = ref(true) @@ -175,7 +177,13 @@ const initials = computed(() => { onMounted(async () => { await loadEmployee() - await Promise.all([loadRecognitions(), loadUSBDevices(), loadCheckoutHistory()]) + // Skip the USB panels entirely when the usb plugin is disabled - its + // /api/usb routes 404 otherwise and spam the console. + await loadEnabledPlugins() + usbEnabled.value = isPluginEnabled('usb') + const tasks = [loadRecognitions()] + if (usbEnabled.value) tasks.push(loadUSBDevices(), loadCheckoutHistory()) + await Promise.all(tasks) }) async function loadEmployee() { diff --git a/frontend/src/views/pcs/PCDetail.vue b/frontend/src/views/pcs/PCDetail.vue index 1800cc2..30e9575 100644 --- a/frontend/src/views/pcs/PCDetail.vue +++ b/frontend/src/views/pcs/PCDetail.vue @@ -191,15 +191,15 @@
- {{ app.application?.appname }} - v{{ app.version }} + {{ app.appname }} + v{{ app.installedversion }}
-
- {{ app.application.appdescription }} +
+ {{ app.appdescription }}
diff --git a/plugins/computers/models/computer.py b/plugins/computers/models/computer.py index 06ba0ce..81abcd7 100644 --- a/plugins/computers/models/computer.py +++ b/plugins/computers/models/computer.py @@ -183,6 +183,25 @@ class ComputerInstalledApp(db.Model): db.Index('idx_compapp_app', 'appid'), ) + def to_dict(self): + # Curated AppVersion wins; else the raw collected version string. + version = None + if self.appversion is not None: + version = self.appversion.version + elif self.installedversion: + version = self.installedversion + return { + 'id': self.id, + 'computerid': self.computerid, + 'appid': self.appid, + 'appname': self.application.appname if self.application else None, + 'appdescription': self.application.appdescription if self.application else None, + 'appversionid': self.appversionid, + 'installedversion': version, + 'installeddate': self.installeddate.isoformat() + 'Z' if self.installeddate else None, + 'isactive': self.isactive, + } + class AccessProtocol(db.Model): """ diff --git a/tests/test_plugins/test_installed_apps.py b/tests/test_plugins/test_installed_apps.py new file mode 100644 index 0000000..094a1de --- /dev/null +++ b/tests/test_plugins/test_installed_apps.py @@ -0,0 +1,57 @@ +"""Installed-applications detail endpoint serializes correctly. + +Regression guard: GET /api/applications/machines/ serializes each +ComputerInstalledApp via its to_dict(). A missing to_dict raised a 500 that the +PCDetail page swallowed (v-if on a non-empty list), so real PCs silently showed +no installed software. This exercises the with-data path that the empty-list +case short-circuits past. +""" + +from shopdb.extensions import db as _db +from shopdb.core.models import Asset, AssetType, Application + +from plugins.computers.models import Computer, ComputerInstalledApp + + +def _seed_pc_with_app(client_db): + atype = AssetType.query.filter_by(assettype='computer').first() + if not atype: + atype = AssetType(assettype='computer') + _db.session.add(atype) + _db.session.flush() + asset = Asset(assetnumber='PC-APPTEST', assettypeid=atype.assettypeid) + _db.session.add(asset) + _db.session.flush() + comp = Computer(assetid=asset.assetid, hostname='PC-APPTEST') + _db.session.add(comp) + _db.session.flush() + app = Application(appname='Test App') + _db.session.add(app) + _db.session.flush() + link = ComputerInstalledApp(computerid=comp.computerid, appid=app.appid, + installedversion='1.2.3') + _db.session.add(link) + _db.session.commit() + return comp + + +def test_installed_apps_endpoint_returns_data(client, db, auth_headers): + comp = _seed_pc_with_app(db) + resp = client.get(f'/api/applications/machines/{comp.computerid}', + headers=auth_headers) + assert resp.status_code == 200 + rows = resp.get_json()['data'] + assert len(rows) == 1 + row = rows[0] + assert row['appname'] == 'Test App' + assert row['installedversion'] == '1.2.3' + assert row['computerid'] == comp.computerid + + +def test_installed_app_to_dict_prefers_curated_version(db): + comp = _seed_pc_with_app(db) + link = ComputerInstalledApp.query.filter_by(computerid=comp.computerid).first() + data = link.to_dict() + assert data['appname'] == 'Test App' + assert data['installedversion'] == '1.2.3' + assert data['isactive'] is True