Printer assignment end to end, and a container that keeps a site's files

Assign printers to a MACHINE and let whichever PC controls it install them, with
the observed half kept strictly apart from the assigned half: a collector report
can never write an assignment row. Adoption is explicit. Adds a singular
relationship type so defaultprinter can mean exactly one, client scripts that
converge a bay's queues and correct a drifted one in place, and a wave gate so a
fleet-wide driver rollout cannot stampede its own share.

On the container side: the compose stack never persisted the instance directory,
so a rebuild discarded plugins.json and every upload. It is a volume now, the
image ships instance/ owned by the app user so the mountpoint is writable, and
the database moves to MySQL 8.4 LTS, which the Windows runbook has recommended
since 8.0 went end of life. The image also could not build at all: the frontend
stage never copied the plugin staging script it runs.

Adds a topology migration guide covering IIS to Docker and back.
This commit is contained in:
2026-08-20 07:46:35 -04:00
parent 09e9d5f047
commit d267813999
45 changed files with 6475 additions and 56 deletions

View File

@@ -344,6 +344,10 @@
<!-- Site-defined custom fields for machines -->
<CustomFieldsInputs ref="customFieldsRef" :assettypeid="MACHINE_ASSETTYPEID" :assetid="currentAssetId" />
<!-- Printers belong to the MACHINE, so whichever PC controls it installs
them - including a replacement after a reimage. -->
<PrinterAssignmentPicker ref="printerPickerRef" :assetid="currentAssetId" scope="machine" />
<div v-if="error" class="error-message">{{ error }}</div>
<div style="display: flex; gap: 0.5rem; margin-top: 1.5rem;">
@@ -366,6 +370,7 @@ import { loadMapConfig, levelOptions, levelName, state as mapConfig }
from '@/composables/mapConfig'
import Modal from '@/components/Modal.vue'
import CustomFieldsInputs from '@/components/CustomFieldsInputs.vue'
import PrinterAssignmentPicker from '@/components/PrinterAssignmentPicker.vue'
import { currentTheme } from '@/stores/theme'
import { useIdentifierFlags } from '@/composables/identifierSettings'
import { apiError } from '@/utils/apiError'
@@ -380,6 +385,7 @@ const isEdit = computed(() => !!route.params.id)
// Seeded asset-type id for machines (see /api/assets/types).
const MACHINE_ASSETTYPEID = 1
const customFieldsRef = ref(null)
const printerPickerRef = ref(null)
const currentAssetId = ref(null)
const loading = ref(true)
@@ -479,7 +485,11 @@ onMounted(async () => {
locations.value = locRes.data.data || []
models.value = allModels
businessunits.value = buRes.data.data || []
pcs.value = pcsRes.data.data || []
// listAll resolves to the ARRAY, not a response: fetchAllPages already
// unwrapped every page. Reading .data.data off it threw, and the whole
// parallel load went to the catch - so every dropdown on this form came up
// empty and the machine's own values never loaded.
pcs.value = pcsRes || []
// Load relationship types separately
try {
@@ -633,6 +643,17 @@ async function saveMachine() {
}
}
// Printer assignment, after the asset exists so a new machine can be
// assigned in the same save. A failure here must not lose the machine the
// user just entered, so it is reported and not thrown.
if (assetId && printerPickerRef.value) {
try {
await printerPickerRef.value.save(assetId)
} catch (printerErr) {
console.error('Error saving printer assignment:', printerErr)
}
}
router.push(`/machines/${savedMachine.machine?.machineid || route.params.id}`)
} catch (err) {
console.error('Error saving machine:', err)