Rescue the work-PC wizard and kiosk-link edits
Some checks failed
CI / naming (push) Successful in 4s
CI / frontend (push) Successful in 12s
CI / backend (push) Failing after 4m58s
CI / migrations-mysql (push) Successful in 42s

Two tracked files had been edited directly in the work-PC relay clone, which
only fetches bundles and pushes them to GitHub. Nothing there reaches gitea, and
the next bundle pull would have overwritten both silently. Brought across here,
with three changes to what was written.

Kiosk links now need the route AND the plugin. The edit swapped hasRoute for
isPluginEnabled, which trades one wrong answer for another: a full build stages
every route, so a site that turns printedparts or usb OFF still saw a link to a
kiosk its backend refuses - the symptom being fixed - but isPluginEnabled fails
OPEN when it cannot reach the plugin list, and a copied registry can report a
plugin that was never staged into this build. That is what the comment above
those lines was already warning about. Both tests together answer both states.

The wizard shows the filename it is uploading. The browser puts it in chrome the
full-screen wizard hides, so an operator picking a file got no confirmation it
was the one being sent.

Image previews get a cache-buster. The stored path does not change between
uploads, so the preview showed the PREVIOUS logo or blueprint and the upload
read as having silently failed. Display only - saveStep persists form values,
never these, so no cache-busted URL is ever written to a setting.

The wizard also loads the current site logo on mount, so re-running it does not
present an empty slot as though branding had never been set. The shipped-asset
fallback stays a root-absolute path: the template applies withBase, and prefixing
it here as well produced /shopdb/shopdb/ge-aerospace-logo.svg on a subpath mount.

Frontend 391 passing.

Scope: product
This commit is contained in:
cproudlock
2026-09-06 19:24:50 -04:00
parent 75ee3b7c1c
commit 64e932ce5c
2 changed files with 43 additions and 8 deletions

View File

@@ -40,9 +40,9 @@
<div v-if="showDisplays" class="nav-section">Displays</div> <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="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('/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> 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> target="_blank" class="external-link">USB Kiosk</a>
<router-link v-if="authStore.isAdmin" to="/settings">Settings</router-link> <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 { fiscalWeek } from '@/utils/fiscalWeek'
import { getFacilityName, getSiteLogo, getServicenowUrls, getEnvLabel } from '../utils/siteSettings' import { getFacilityName, getSiteLogo, getServicenowUrls, getEnvLabel } from '../utils/siteSettings'
import { withBase } from '../utils/basePath' import { withBase } from '../utils/basePath'
import { isPluginEnabled } from '../composables/enabledPlugins'
const router = useRouter() const router = useRouter()
const route = useRoute() 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" // 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 // test - more reliable than plugin-enabled state, which a copied registry can
// report for a plugin that was never staged. // 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 stagedRoutePaths = new Set(router.getRoutes().map(r => r.path))
const hasRoute = (path) => stagedRoutePaths.has(path) const hasRoute = (path) => stagedRoutePaths.has(path)
// Shopfloor Dashboard is a core view but runs entirely on the notifications // 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. // is empty, so gate the link on the notifications plugin being in this build.
const showShopfloor = hasRoute('/shopfloor') const showShopfloor = hasRoute('/shopfloor')
&& (hasRoute('/notifications') || hasRoute('/calendar')) && (hasRoute('/notifications') || hasRoute('/calendar'))
const showDisplays = showShopfloor || hasRoute('/tv') || hasRoute('/parts-kiosk') const showPartsKiosk = hasRoute('/parts-kiosk') && isPluginEnabled('printedparts')
|| hasRoute('/usb-kiosk') const showUsbKiosk = hasRoute('/usb-kiosk') && isPluginEnabled('usb')
const showDisplays = showShopfloor || hasRoute('/tv') || showPartsKiosk
|| showUsbKiosk
const routeViewKey = computed(() => const routeViewKey = computed(() =>
route.path.startsWith('/settings') ? '/settings' : route.path route.path.startsWith('/settings') ? '/settings' : route.path
) )

View File

@@ -28,7 +28,8 @@
<div class="form-group"> <div class="form-group">
<label>Site logo <span class="hint">(optional)</span></label> <label>Site logo <span class="hint">(optional)</span></label>
<input type="file" accept="image/*" @change="uploadSiteLogo($event)" :disabled="logoUploading" /> <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> <small class="hint">Skip to keep the shipped GE branding. You can change this later under Settings &gt; Branding.</small>
</div> </div>
</div> </div>
@@ -92,15 +93,17 @@
<!-- Floor map --> <!-- Floor map -->
<div v-else-if="current.key === 'map'"> <div v-else-if="current.key === 'map'">
<h2>Floor map</h2> <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"> <div class="form-group">
<label>Blueprint image (light theme)</label> <label>Blueprint image (light theme)</label>
<input type="file" accept="image/*" @change="uploadBlueprint('light', $event)" :disabled="mapUploading" /> <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" /> <img v-if="blueprintLight" :src="withBase(blueprintLight)" class="wizard-map-thumb" alt="light blueprint" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Blueprint image (dark theme)</label> <label>Blueprint image (dark theme)</label>
<input type="file" accept="image/*" @change="uploadBlueprint('dark', $event)" :disabled="mapUploading" /> <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" /> <img v-if="blueprintDark" :src="withBase(blueprintDark)" class="wizard-map-thumb dark" alt="dark blueprint" />
</div> </div>
<div class="form-row"> <div class="form-row">
@@ -203,14 +206,24 @@ const blueprintLight = ref('')
const blueprintDark = ref('') const blueprintDark = ref('')
const logoUploading = ref(false) const logoUploading = ref(false)
const siteLogo = ref('') 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) { async function uploadSiteLogo(event) {
const file = event.target.files[0] const file = event.target.files[0]
if (!file) return if (!file) return
logoFilename.value = file.name
logoUploading.value = true logoUploading.value = true
try { try {
const { data } = await settingsApi.uploadBrandingLogo('site', file) 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') toast.success('Logo uploaded')
} catch (err) { } catch (err) {
toast.error(apiError(err, 'Upload failed')) toast.error(apiError(err, 'Upload failed'))
@@ -223,10 +236,12 @@ async function uploadSiteLogo(event) {
async function uploadBlueprint(theme, event) { async function uploadBlueprint(theme, event) {
const file = event.target.files[0] const file = event.target.files[0]
if (!file) return if (!file) return
if (theme === 'light') blueprintLightFilename.value = file.name
else blueprintDarkFilename.value = file.name
mapUploading.value = true mapUploading.value = true
try { try {
const { data } = await settingsApi.uploadMapBlueprint(theme, file) 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 if (theme === 'light') blueprintLight.value = url
else blueprintDark.value = url else blueprintDark.value = url
toast.success('Blueprint uploaded') toast.success('Blueprint uploaded')
@@ -307,6 +322,17 @@ onMounted(async () => {
} catch (err) { /* not set yet */ } } 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() { async function saveStep() {