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 <noreply@anthropic.com>
This commit is contained in:
10
CHANGELOG.md
10
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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Currently Checked Out USB Devices -->
|
||||
<div class="section-card">
|
||||
<div v-if="usbEnabled" class="section-card">
|
||||
<h2 class="section-title">Checked Out USB Devices</h2>
|
||||
<div v-if="usbLoading" class="loading">Loading...</div>
|
||||
<div v-else-if="usbDevices.length === 0" class="empty">
|
||||
@@ -99,7 +99,7 @@
|
||||
</div>
|
||||
|
||||
<!-- USB Checkout History -->
|
||||
<div class="section-card">
|
||||
<div v-if="usbEnabled" class="section-card">
|
||||
<h2 class="section-title">USB Checkout History</h2>
|
||||
<div v-if="historyLoading" class="loading">Loading...</div>
|
||||
<div v-else-if="checkoutHistory.length === 0" class="empty">
|
||||
@@ -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() {
|
||||
|
||||
@@ -191,15 +191,15 @@
|
||||
<router-link
|
||||
v-for="app in installedApps"
|
||||
:key="app.id"
|
||||
:to="`/applications/${app.application?.appid}`"
|
||||
:to="`/applications/${app.appid}`"
|
||||
class="app-item"
|
||||
>
|
||||
<div class="app-info">
|
||||
<span class="app-name">{{ app.application?.appname }}</span>
|
||||
<span class="app-version" v-if="app.version">v{{ app.version }}</span>
|
||||
<span class="app-name">{{ app.appname }}</span>
|
||||
<span class="app-version" v-if="app.installedversion">v{{ app.installedversion }}</span>
|
||||
</div>
|
||||
<div class="app-desc" v-if="app.application?.appdescription">
|
||||
{{ app.application.appdescription }}
|
||||
<div class="app-desc" v-if="app.appdescription">
|
||||
{{ app.appdescription }}
|
||||
</div>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
57
tests/test_plugins/test_installed_apps.py
Normal file
57
tests/test_plugins/test_installed_apps.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""Installed-applications detail endpoint serializes correctly.
|
||||
|
||||
Regression guard: GET /api/applications/machines/<computerid> 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
|
||||
Reference in New Issue
Block a user