Add print badges, pagination, route splitting, JWT auth fixes, and list page alignment

- 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>
This commit is contained in:
cproudlock
2026-02-04 07:32:44 -05:00
parent fe7a00b260
commit 83caaa7b7c
89 changed files with 3951 additions and 1138 deletions

View File

@@ -20,22 +20,22 @@
</div>
<div class="hero-meta">
<span class="badge badge-lg badge-info">Computer</span>
<span class="badge badge-lg" :class="getStatusClass(computer.status_name)">
{{ computer.status_name || 'Unknown' }}
<span class="badge badge-lg" :class="getStatusClass(computer.statusname)">
{{ computer.statusname || 'Unknown' }}
</span>
</div>
<div class="hero-details">
<div class="hero-detail" v-if="computer.computer?.computertype_name">
<div class="hero-detail" v-if="computer.computer?.computertypename">
<span class="hero-detail-label">Type</span>
<span class="hero-detail-value">{{ computer.computer.computertype_name }}</span>
<span class="hero-detail-value">{{ computer.computer.computertypename }}</span>
</div>
<div class="hero-detail" v-if="computer.computer?.os_name">
<div class="hero-detail" v-if="computer.computer?.osname">
<span class="hero-detail-label">OS</span>
<span class="hero-detail-value">{{ computer.computer.os_name }}</span>
<span class="hero-detail-value">{{ computer.computer.osname }}</span>
</div>
<div class="hero-detail" v-if="computer.location_name">
<div class="hero-detail" v-if="computer.locationname">
<span class="hero-detail-label">Location</span>
<span class="hero-detail-value">{{ computer.location_name }}</span>
<span class="hero-detail-value">{{ computer.locationname }}</span>
</div>
</div>
</div>
@@ -74,11 +74,11 @@
<div class="info-list">
<div class="info-row">
<span class="info-label">Computer Type</span>
<span class="info-value">{{ computer.computer?.computertype_name || '-' }}</span>
<span class="info-value">{{ computer.computer?.computertypename || '-' }}</span>
</div>
<div class="info-row">
<span class="info-label">Operating System</span>
<span class="info-value">{{ computer.computer?.os_name || '-' }}</span>
<span class="info-value">{{ computer.computer?.osname || '-' }}</span>
</div>
</div>
</div>
@@ -126,14 +126,14 @@
:top="computer.maptop"
:machineName="computer.assetnumber"
>
<span class="location-link">{{ computer.location_name || 'On Map' }}</span>
<span class="location-link">{{ computer.locationname || 'On Map' }}</span>
</LocationMapTooltip>
<span v-else>{{ computer.location_name || '-' }}</span>
<span v-else>{{ computer.locationname || '-' }}</span>
</span>
</div>
<div class="info-row">
<span class="info-label">Business Unit</span>
<span class="info-value">{{ computer.businessunit_name || '-' }}</span>
<span class="info-value">{{ computer.businessunitname || '-' }}</span>
</div>
</div>
</div>
@@ -145,7 +145,7 @@
<router-link
v-for="item in controlledEquipment"
:key="item.relationshipid"
:to="`/machines/${item.plugin_id || item.assetid}`"
:to="`/machines/${item.pluginid || item.assetid}`"
class="equipment-item"
>
<div class="equipment-info">
@@ -153,7 +153,7 @@
<span class="equipment-alias" v-if="item.name">{{ item.name }}</span>
</div>
<div class="equipment-meta">
<span class="category-tag">{{ item.assettype_name }}</span>
<span class="category-tag">{{ item.assettypename }}</span>
<span class="connection-tag">{{ item.relationshipType }}</span>
</div>
</router-link>
@@ -221,22 +221,22 @@ const controlledEquipment = computed(() => {
// Check outgoing - computer controls equipment
for (const rel of relationships.value.outgoing || []) {
if (rel.target_asset?.assettype_name === 'equipment' && rel.relationship_type_name === 'Controls') {
if (rel.targetasset?.assettypename === 'equipment' && rel.relationshiptypename === 'Controls') {
items.push({
...rel.target_asset,
...rel.targetasset,
relationshipid: rel.relationshipid,
relationshipType: rel.relationship_type_name
relationshipType: rel.relationshiptypename
})
}
}
// Also check incoming - legacy data may have equipment -> computer Controls relationships
for (const rel of relationships.value.incoming || []) {
if (rel.source_asset?.assettype_name === 'equipment' && rel.relationship_type_name === 'Controls') {
if (rel.sourceasset?.assettypename === 'equipment' && rel.relationshiptypename === 'Controls') {
items.push({
...rel.source_asset,
...rel.sourceasset,
relationshipid: rel.relationshipid,
relationshipType: rel.relationship_type_name
relationshipType: rel.relationshiptypename
})
}
}

View File

@@ -30,6 +30,7 @@
<th>Type</th>
<th>Features</th>
<th>Status</th>
<th>Location</th>
<th>Actions</th>
</tr>
</thead>
@@ -38,17 +39,18 @@
<td>{{ item.assetnumber }}</td>
<td>{{ item.computer?.hostname || '-' }}</td>
<td class="mono">{{ item.serialnumber || '-' }}</td>
<td>{{ item.computer?.computertype_name || '-' }}</td>
<td>{{ item.computer?.computertypename || '-' }}</td>
<td class="features">
<span v-if="item.computer?.isvnc" class="feature-tag active">VNC</span>
<span v-if="item.computer?.iswinrm" class="feature-tag active">WinRM</span>
<span v-if="!item.computer?.isvnc && !item.computer?.iswinrm">-</span>
</td>
<td>
<span class="badge" :class="getStatusClass(item.status_name)">
{{ item.status_name || 'Unknown' }}
<span class="badge" :class="getStatusClass(item.statusname)">
{{ item.statusname || 'Unknown' }}
</span>
</td>
<td>{{ item.locationname || '-' }}</td>
<td class="actions">
<router-link
:to="`/pcs/${item.computer?.computerid || item.assetid}`"
@@ -59,7 +61,7 @@
</td>
</tr>
<tr v-if="computers.length === 0">
<td colspan="7" style="text-align: center; color: var(--text-light);">
<td colspan="8" style="text-align: center; color: var(--text-light);">
No computers found
</td>
</tr>
@@ -68,16 +70,13 @@
</div>
<!-- Pagination -->
<div class="pagination" v-if="totalPages > 1">
<button
v-for="p in totalPages"
:key="p"
:class="{ active: p === page }"
@click="goToPage(p)"
>
{{ p }}
</button>
</div>
<PaginationBar
:page="page"
:totalPages="totalPages"
:perPage="perPage"
@update:page="goToPage"
@update:perPage="changePerPage"
/>
</template>
</div>
</div>
@@ -86,12 +85,14 @@
<script setup>
import { ref, onMounted } from 'vue'
import { computersApi } from '../../api'
import PaginationBar from '../../components/PaginationBar.vue'
const computers = ref([])
const loading = ref(true)
const search = ref('')
const page = ref(1)
const totalPages = ref(1)
const perPage = ref(20)
let searchTimeout = null
@@ -104,13 +105,13 @@ async function loadComputers() {
try {
const params = {
page: page.value,
per_page: 20
perpage: perPage.value
}
if (search.value) params.search = search.value
const response = await computersApi.list(params)
computers.value = response.data.data || []
totalPages.value = response.data.meta?.pagination?.total_pages || 1
totalPages.value = response.data.meta?.pagination?.totalpages || response.data.meta?.pagination?.total_pages || 1
} catch (error) {
console.error('Error loading computers:', error)
} finally {
@@ -131,6 +132,12 @@ function goToPage(p) {
loadComputers()
}
function changePerPage(newPerPage) {
perPage.value = newPerPage
page.value = 1
loadComputers()
}
function getStatusClass(status) {
if (!status) return 'badge-info'
const s = status.toLowerCase()