Endpoint telemetry, PC Detail rework, a checked backup/restore path, and setup-wizard fixes

This commit is contained in:
2026-09-06 19:25:02 -04:00
parent 993353dfc0
commit 95abe72729
2 changed files with 43 additions and 8 deletions

View File

@@ -40,9 +40,9 @@
<div v-if="showDisplays" class="nav-section">Displays</div>
<a v-if="showShopfloor" :href="withBase('/shopfloor')" target="_blank" class="external-link">Shopfloor Dashboard</a>
<a v-if="hasRoute('/tv')" :href="withBase('/tv')" target="_blank" class="external-link">TV Slideshow</a>
<a v-if="hasRoute('/parts-kiosk')" :href="withBase('/parts-kiosk')"
<a v-if="showPartsKiosk" :href="withBase('/parts-kiosk')"
target="_blank" class="external-link">Parts Kiosk</a>
<a v-if="hasRoute('/usb-kiosk')" :href="withBase('/usb-kiosk')"
<a v-if="showUsbKiosk" :href="withBase('/usb-kiosk')"
target="_blank" class="external-link">USB Kiosk</a>
<router-link v-if="authStore.isAdmin" to="/settings">Settings</router-link>
@@ -127,6 +127,7 @@ import { dashboardApi, notificationsApi } from '../api'
import { fiscalWeek } from '@/utils/fiscalWeek'
import { getFacilityName, getSiteLogo, getServicenowUrls, getEnvLabel } from '../utils/siteSettings'
import { withBase } from '../utils/basePath'
import { isPluginEnabled } from '../composables/enabledPlugins'
const router = useRouter()
const route = useRoute()
@@ -138,6 +139,12 @@ const authStore = useAuthStore()
// dead-ends on a blank page. Route existence is the true "is it in this build"
// test - more reliable than plugin-enabled state, which a copied registry can
// report for a plugin that was never staged.
//
// The kiosks need BOTH tests. Staged-but-disabled is a real state: a full build
// stages every route, so a site that turns the plugin OFF still had the link,
// pointing at a kiosk its backend refuses. Enabled-but-unstaged is the state the
// paragraph above is about, and isPluginEnabled fails OPEN when it cannot reach
// the plugin list, so it cannot carry the test on its own.
const stagedRoutePaths = new Set(router.getRoutes().map(r => r.path))
const hasRoute = (path) => stagedRoutePaths.has(path)
// Shopfloor Dashboard is a core view but runs entirely on the notifications
@@ -145,8 +152,10 @@ const hasRoute = (path) => stagedRoutePaths.has(path)
// is empty, so gate the link on the notifications plugin being in this build.
const showShopfloor = hasRoute('/shopfloor')
&& (hasRoute('/notifications') || hasRoute('/calendar'))
const showDisplays = showShopfloor || hasRoute('/tv') || hasRoute('/parts-kiosk')
|| hasRoute('/usb-kiosk')
const showPartsKiosk = hasRoute('/parts-kiosk') && isPluginEnabled('printedparts')
const showUsbKiosk = hasRoute('/usb-kiosk') && isPluginEnabled('usb')
const showDisplays = showShopfloor || hasRoute('/tv') || showPartsKiosk
|| showUsbKiosk
const routeViewKey = computed(() =>
route.path.startsWith('/settings') ? '/settings' : route.path
)

View File

@@ -28,7 +28,8 @@
<div class="form-group">
<label>Site logo <span class="hint">(optional)</span></label>
<input type="file" accept="image/*" @change="uploadSiteLogo($event)" :disabled="logoUploading" />
<img v-if="siteLogo" :src="siteLogo" class="wizard-map-thumb" alt="site logo" />
<input v-model="logoFilename" type="text" class="form-control" placeholder="No file selected" readonly />
<img v-if="siteLogo" :src="withBase(siteLogo)" class="wizard-map-thumb" alt="site logo" />
<small class="hint">Skip to keep the shipped GE branding. You can change this later under Settings &gt; Branding.</small>
</div>
</div>
@@ -92,15 +93,17 @@
<!-- Floor map -->
<div v-else-if="current.key === 'map'">
<h2>Floor map</h2>
<p class="hint">Upload your facility blueprint (light and dark), and set its native pixel size.</p>
<p class="hint">Upload your facility blueprint (light and dark) for the default level, and set its native pixel size. Additional levels can be added later in Settings.</p>
<div class="form-group">
<label>Blueprint image (light theme)</label>
<input type="file" accept="image/*" @change="uploadBlueprint('light', $event)" :disabled="mapUploading" />
<input v-model="blueprintLightFilename" type="text" class="form-control" placeholder="No file selected" readonly />
<img v-if="blueprintLight" :src="withBase(blueprintLight)" class="wizard-map-thumb" alt="light blueprint" />
</div>
<div class="form-group">
<label>Blueprint image (dark theme)</label>
<input type="file" accept="image/*" @change="uploadBlueprint('dark', $event)" :disabled="mapUploading" />
<input v-model="blueprintDarkFilename" type="text" class="form-control" placeholder="No file selected" readonly />
<img v-if="blueprintDark" :src="withBase(blueprintDark)" class="wizard-map-thumb dark" alt="dark blueprint" />
</div>
<div class="form-row">
@@ -203,14 +206,24 @@ const blueprintLight = ref('')
const blueprintDark = ref('')
const logoUploading = ref(false)
const siteLogo = ref('')
// A file input shows the chosen name in the browser chrome, which the kiosk-
// style full-screen wizard hides, so the operator had no confirmation that the
// file they picked was the one being uploaded.
const logoFilename = ref('')
const blueprintLightFilename = ref('')
const blueprintDarkFilename = ref('')
async function uploadSiteLogo(event) {
const file = event.target.files[0]
if (!file) return
logoFilename.value = file.name
logoUploading.value = true
try {
const { data } = await settingsApi.uploadBrandingLogo('site', file)
siteLogo.value = data?.data?.value || ''
// Same path every time, so without a cache-buster the preview shows the
// PREVIOUS logo and the upload reads as having silently failed. Display
// only - nothing persists this value.
siteLogo.value = data?.data?.value ? `${data.data.value}?t=${Date.now()}` : ''
toast.success('Logo uploaded')
} catch (err) {
toast.error(apiError(err, 'Upload failed'))
@@ -223,10 +236,12 @@ async function uploadSiteLogo(event) {
async function uploadBlueprint(theme, event) {
const file = event.target.files[0]
if (!file) return
if (theme === 'light') blueprintLightFilename.value = file.name
else blueprintDarkFilename.value = file.name
mapUploading.value = true
try {
const { data } = await settingsApi.uploadMapBlueprint(theme, file)
const url = data?.data?.value
const url = data?.data?.value ? `${data.data.value}?t=${Date.now()}` : ''
if (theme === 'light') blueprintLight.value = url
else blueprintDark.value = url
toast.success('Blueprint uploaded')
@@ -307,6 +322,17 @@ onMounted(async () => {
} catch (err) { /* not set yet */ }
}
}
// Show whatever logo the site is already on, so re-running the wizard does
// not present an empty slot as if branding had never been set. Stored as a
// root-absolute path; the template applies withBase, so do NOT prefix here or
// a subpath mount gets it twice (/shopdb/shopdb/...).
const SHIPPED_LOGO = '/ge-aerospace-logo.svg'
try {
const response = await settingsApi.get('site_logo')
siteLogo.value = response.data?.data?.value || SHIPPED_LOGO
} catch (err) {
siteLogo.value = SHIPPED_LOGO
}
})
async function saveStep() {