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