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>
<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() {