Retire the legacy Machine model (ADR-001 cutover)

The asset/computer model is now the single source of truth. Remove the Machine
instance layer end to end:

- Delete models Machine, MachineStatus, PCType, MachineRelationship,
  InstalledApp, PrinterData; keep MachineType (models.machinetypeid still
  references it).
- Delete the /api/machines, /api/statuses, /api/pctypes blueprints and the
  legacy /api/printers/legacy (PrinterData) blueprint.
- Drop the deprecated communications.machineid column and its FK.
- Migration 7c01 drops tables machines, machinestatuses, pctypes,
  machinerelationships, installedapps, printerdata (idempotent).
- Fix remaining readers (applications install counts) to ComputerInstalledApp.
- Frontend: remove dead machinesApi/statusesApi/pctypesApi wrappers; repoint
  the PC Types settings page at computer types.

143 tests pass; all asset/computer/dashboard/report/collector endpoints 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-26 09:59:17 -04:00
parent cb47b9087e
commit b0a0670fcd
23 changed files with 344 additions and 2047 deletions

View File

@@ -1,177 +1,126 @@
<template>
<div>
<div class="page-header">
<h2>PC Types</h2>
<button class="btn btn-primary" @click="openModal()">+ Add PC Type</button>
</div>
<div class="card">
<div v-if="loading" class="loading">Loading...</div>
<template v-else>
<div class="table-container">
<table>
<thead>
<tr>
<th>PC Type</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="pt in pcTypes" :key="pt.pctypeid">
<td>{{ pt.pctype }}</td>
<td class="cell-truncate" :title="pt.description">{{ pt.description || '-' }}</td>
<td class="actions">
<button class="btn btn-secondary btn-sm" @click="openModal(pt)">Edit</button>
<button class="btn btn-danger btn-sm" @click="confirmDelete(pt)">Delete</button>
</td>
</tr>
<tr v-if="pcTypes.length === 0">
<td colspan="3" style="text-align: center; color: var(--text-light);">
No PC types found
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<PaginationBar
:page="page"
:totalPages="totalPages"
:perPage="perPage"
@update:page="goToPage"
@update:perPage="changePerPage"
/>
</template>
</div>
<!-- Add/Edit Modal -->
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
<div class="modal">
<div class="modal-header">
<h3>{{ editing ? 'Edit PC Type' : 'Add PC Type' }}</h3>
</div>
<form @submit.prevent="save">
<div class="modal-body">
<div class="form-group">
<label for="pctype">PC Type *</label>
<input id="pctype" v-model="form.pctype" type="text" class="form-control" required />
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description" v-model="form.description" class="form-control" rows="3"></textarea>
</div>
<div v-if="error" class="error-message">{{ error }}</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @click="closeModal">Cancel</button>
<button type="submit" class="btn btn-primary" :disabled="saving">
{{ saving ? 'Saving...' : 'Save' }}
</button>
</div>
</form>
</div>
</div>
<!-- Delete Modal -->
<div v-if="showDeleteModal" class="modal-overlay" @click.self="showDeleteModal = false">
<div class="modal">
<div class="modal-header"><h3>Delete PC Type</h3></div>
<div class="modal-body">
<p>Are you sure you want to delete <strong>{{ toDelete?.pctype }}</strong>?</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @click="showDeleteModal = false">Cancel</button>
<button class="btn btn-danger" @click="deleteItem">Delete</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { pctypesApi } from '../../api'
import PaginationBar from '../../components/PaginationBar.vue'
const pcTypes = ref([])
const loading = ref(true)
const page = ref(1)
const totalPages = ref(1)
const perPage = ref(20)
const showModal = ref(false)
const editing = ref(null)
const saving = ref(false)
const error = ref('')
const showDeleteModal = ref(false)
const toDelete = ref(null)
const form = ref({ pctype: '', description: '' })
onMounted(() => loadData())
async function loadData() {
loading.value = true
try {
const response = await pctypesApi.list({ page: page.value, perpage: perPage.value })
pcTypes.value = response.data.data || []
totalPages.value = response.data.meta?.pagination?.totalpages || response.data.meta?.pagination?.total_pages || 1
} catch (err) {
console.error('Error loading PC types:', err)
} finally {
loading.value = false
}
}
function goToPage(p) { page.value = p; loadData() }
function changePerPage(newPerPage) {
perPage.value = newPerPage
page.value = 1
loadData()
}
function openModal(item = null) {
editing.value = item
form.value = item ? { pctype: item.pctype || '', description: item.description || '' } : { pctype: '', description: '' }
error.value = ''
showModal.value = true
}
function closeModal() { showModal.value = false; editing.value = null }
async function save() {
error.value = ''
saving.value = true
try {
if (editing.value) {
await pctypesApi.update(editing.value.pctypeid, form.value)
} else {
await pctypesApi.create(form.value)
}
closeModal()
loadData()
} catch (err) {
error.value = err.response?.data?.message || 'Failed to save'
} finally {
saving.value = false
}
}
function confirmDelete(item) { toDelete.value = item; showDeleteModal.value = true }
async function deleteItem() {
try {
await pctypesApi.delete(toDelete.value.pctypeid)
showDeleteModal.value = false
toDelete.value = null
loadData()
} catch (err) {
alert('Failed to delete')
}
}
</script>
<template>
<div>
<div class="page-header">
<h2>PC Types</h2>
<button class="btn btn-primary" @click="openModal()">+ Add PC Type</button>
</div>
<div class="card">
<div v-if="loading" class="loading">Loading...</div>
<template v-else>
<div class="table-container">
<table>
<thead>
<tr>
<th>PC Type</th>
<th>Description</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="pt in pcTypes" :key="pt.computertypeid">
<td>{{ pt.computertype }}</td>
<td class="cell-truncate" :title="pt.description">{{ pt.description || '-' }}</td>
<td class="actions">
<button class="btn btn-secondary btn-sm" @click="openModal(pt)">Edit</button>
</td>
</tr>
<tr v-if="pcTypes.length === 0">
<td colspan="3" style="text-align: center; color: var(--text-light);">
No PC types found
</td>
</tr>
</tbody>
</table>
</div>
</template>
</div>
<!-- Add/Edit Modal -->
<div v-if="showModal" class="modal-overlay" @click.self="closeModal">
<div class="modal">
<div class="modal-header">
<h3>{{ editing ? 'Edit PC Type' : 'Add PC Type' }}</h3>
</div>
<form @submit.prevent="save">
<div class="modal-body">
<div class="form-group">
<label for="computertype">PC Type *</label>
<input id="computertype" v-model="form.computertype" type="text" class="form-control" required />
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description" v-model="form.description" class="form-control" rows="3"></textarea>
</div>
<div v-if="error" class="error-message">{{ error }}</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" @click="closeModal">Cancel</button>
<button type="submit" class="btn btn-primary" :disabled="saving">
{{ saving ? 'Saving...' : 'Save' }}
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { computersApi } from '../../api'
const pcTypes = ref([])
const loading = ref(true)
const showModal = ref(false)
const editing = ref(null)
const saving = ref(false)
const error = ref('')
const form = ref({ computertype: '', description: '' })
onMounted(() => loadData())
async function loadData() {
loading.value = true
try {
const response = await computersApi.types.list({ perpage: 100 })
pcTypes.value = response.data.data || []
} catch (err) {
console.error('Error loading PC types:', err)
} finally {
loading.value = false
}
}
function openModal(item = null) {
editing.value = item
form.value = item
? { computertype: item.computertype || '', description: item.description || '' }
: { computertype: '', description: '' }
error.value = ''
showModal.value = true
}
function closeModal() { showModal.value = false; editing.value = null }
async function save() {
error.value = ''
saving.value = true
try {
if (editing.value) {
await computersApi.types.update(editing.value.computertypeid, form.value)
} else {
await computersApi.types.create(form.value)
}
closeModal()
loadData()
} catch (err) {
error.value = err.response?.data?.message || 'Failed to save'
} finally {
saving.value = false
}
}
</script>