From 95cfa9e939c1ab76604a0339836c93c40608c6ac Mon Sep 17 00:00:00 2001 From: Cam Proudlock Date: Thu, 16 Jul 2026 14:08:19 -0400 Subject: [PATCH] Fix bit(1) import coercion and subpath login redirect The WJ import loader coerced MySQL bit(1) columns with bool(), which is always true for the bytes pymysql returns; applications.isinstallable and notifications.isshopfloor imported as 1 for every row. Route both through the existing _truthy_bit helper. The loader also no longer requires the employee source database: without it, notifications keep the bare SSO and the verify stage skips the employee count. Under a subpath mount, the session-expiry interceptor stored the browser path (which includes the mount base) as the login redirect, and the router applied its base again after login. Add stripBase() and store base-free router paths; the login page also strips a stale base from old bookmarked redirects. Also point the external-plugin CI helper at the repo's GitHub URL. --- frontend/src/api/index.js | 8 ++++-- frontend/src/utils/basePath.js | 12 ++++++++ frontend/src/views/Login.vue | 6 ++-- .../src/views/geenforce/ManifestEditor.vue | 2 +- scripts/site_imports/wjf/run.py | 28 +++++++++++++------ scripts/test-external-plugin.sh | 4 +-- 6 files changed, 43 insertions(+), 17 deletions(-) diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index e96c0cf1..ec19d772 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -1,5 +1,5 @@ import axios from 'axios' -import { withBase } from './../utils/basePath' +import { withBase, stripBase } from './../utils/basePath' // BASE_URL ends in '/', so this is '/api' at root or '/ops/api' under a subpath // mount. Keeps the SPA, its API, and IIS all on the same mount path. @@ -35,8 +35,10 @@ api.interceptors.response.use( // Preserve the destination so login returns the user to this page. if (hadToken) { const loginPath = withBase('/login') - const here = window.location.pathname + window.location.search - const target = here && here !== loginPath + // Router paths exclude the mount base; strip it or login's + // router.push double-prefixes under a subpath mount. + const here = stripBase(window.location.pathname) + window.location.search + const target = here && here !== '/login' ? loginPath + '?redirect=' + encodeURIComponent(here) : loginPath window.location.href = target diff --git a/frontend/src/utils/basePath.js b/frontend/src/utils/basePath.js index c8e6318a..b77cbbbb 100644 --- a/frontend/src/utils/basePath.js +++ b/frontend/src/utils/basePath.js @@ -11,3 +11,15 @@ export function withBase(path) { if (/^([a-z]+:)?\/\//i.test(path) || path.startsWith('data:')) return path return BASE_URL + String(path).replace(/^\//, '') } + +// Inverse of withBase: turn a browser pathname (which includes the mount +// base, e.g. '/ops/computers') into a router path ('/computers'). Router +// navigation already applies the base; feeding it an un-stripped pathname +// double-prefixes ('/ops/ops/...'). +export function stripBase(path) { + if (!path) return path + if (BASE_URL !== '/' && path.startsWith(BASE_URL)) { + return '/' + path.slice(BASE_URL.length) + } + return path +} diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index c1f2f6cb..a5c43be8 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -52,7 +52,7 @@ import { useRouter, useRoute } from 'vue-router' import { useAuthStore } from '../stores/auth' import { setupApi } from '../api' import { getSiteLogo } from '../utils/siteSettings' -import { withBase } from '../utils/basePath' +import { withBase, stripBase } from '../utils/basePath' const router = useRouter() const route = useRoute() @@ -63,7 +63,9 @@ const authStore = useAuthStore() function postLoginTarget() { const redirect = route.query.redirect if (typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//')) { - return redirect + // Tolerate a redirect that still carries the mount base (old bookmarks, + // pre-fix interceptor URLs): router paths must be base-free. + return stripBase(redirect) } return '/' } diff --git a/frontend/src/views/geenforce/ManifestEditor.vue b/frontend/src/views/geenforce/ManifestEditor.vue index 05dd0dd4..d16f3e74 100644 --- a/frontend/src/views/geenforce/ManifestEditor.vue +++ b/frontend/src/views/geenforce/ManifestEditor.vue @@ -23,7 +23,7 @@ common manifest uses PC types to hit a subset; a per-type manifest already only runs on its own type, so it shows just the niche gate it needs (CMM version, a bay number, a subtype).

-

Full guide: the GE-ENFORCE page in the project wiki.

+

Full guide: the GE-ENFORCE wiki page.

diff --git a/scripts/site_imports/wjf/run.py b/scripts/site_imports/wjf/run.py index fa88ec57..6ffcfc0c 100644 --- a/scripts/site_imports/wjf/run.py +++ b/scripts/site_imports/wjf/run.py @@ -16,6 +16,8 @@ the machineid->assetid crosswalk this harness persists. import argparse +import pymysql + from .harness import Harness @@ -451,7 +453,7 @@ def stage_applications(h): continue payload = {'appname': name, 'appdescription': a['appdescription'], 'supportteamid': h.ids.get('supportteam', a['supportteamid']), - 'isinstallable': bool(a['isinstallable']), + 'isinstallable': _truthy_bit(a['isinstallable']), 'applicationnotes': a['applicationnotes'], 'installpath': a['installpath'], 'applicationlink': a['applicationlink'], 'documentationpath': a['documentationpath']} @@ -534,13 +536,17 @@ def stage_notifications(h): # SSO -> "First Last" from the employee source, so recognition/training # notifications display a name, not a bare SSO (the model shows employeename - # or falls back to employeesso). + # or falls back to employeesso). Employee source is optional: without it + # (shopdb-only import) notifications keep the bare SSO. ssoname = {} - for e in h.source.rows('wjf_employees_src', - 'SELECT SSO, First_Name, Last_Name FROM employees'): - full = f"{(e['First_Name'] or '').strip()} {(e['Last_Name'] or '').strip()}".strip() - if full: - ssoname[str(e['SSO']).strip()] = full + try: + for e in h.source.rows('wjf_employees_src', + 'SELECT SSO, First_Name, Last_Name FROM employees'): + full = f"{(e['First_Name'] or '').strip()} {(e['Last_Name'] or '').strip()}".strip() + if full: + ssoname[str(e['SSO']).strip()] = full + except pymysql.err.OperationalError: + print(' (no wjf_employees_src - notifications keep bare SSOs)') def _names(employeesso): if not employeesso: @@ -566,7 +572,7 @@ def stage_notifications(h): 'appid': h.ids.get('app', n['appid']), 'starttime': str(n['starttime']) if n['starttime'] else None, 'endtime': endtime, 'ticketnumber': n['ticketnumber'], 'link': n['link'], - 'isshopfloor': bool(n['isshopfloor']), 'employeesso': n['employeesso'], + 'isshopfloor': _truthy_bit(n['isshopfloor']), 'employeesso': n['employeesso'], 'employeename': _names(n['employeesso'])} status, _ = h.post('/api/notifications', payload) if status in (200, 201): @@ -759,7 +765,11 @@ def stage_verify(h): from sqlalchemy import text report = {} for label, sdb, sql, target_table in checks: - src = h.source.rows(sdb, sql)[0]['c'] + try: + src = h.source.rows(sdb, sql)[0]['c'] + except pymysql.err.OperationalError: + report[label] = 'source db absent - skipped' + continue tgt = db.session.execute(text(f'SELECT COUNT(*) FROM {target_table}')).scalar() report[label] = f'source~{src} target={tgt}' return report diff --git a/scripts/test-external-plugin.sh b/scripts/test-external-plugin.sh index aa61e99d..4c10e014 100755 --- a/scripts/test-external-plugin.sh +++ b/scripts/test-external-plugin.sh @@ -22,7 +22,7 @@ # PLUGIN_DIR ($1) path to the plugin directory (holds manifest.json). Required. # FRAMEWORK_REF ($2) git ref to test against in CI mode. Default: main. # FRAMEWORK_URL framework git URL for CI mode. -# Default: https:///ge-aerospace/shopdb-flask.git +# Default: https://github.com/ge-aero/shopdb-flask.git # LOCAL_FRAMEWORK path to an existing framework checkout. Set it to run offline. # # Examples: @@ -43,7 +43,7 @@ set -eu PLUGIN_DIR="${PLUGIN_DIR:-${1:-}}" FRAMEWORK_REF="${FRAMEWORK_REF:-${2:-main}}" -FRAMEWORK_URL="${FRAMEWORK_URL:-https:///ge-aerospace/shopdb-flask.git}" +FRAMEWORK_URL="${FRAMEWORK_URL:-https://github.com/ge-aero/shopdb-flask.git}" LOCAL_FRAMEWORK="${LOCAL_FRAMEWORK:-}" if [ -z "$PLUGIN_DIR" ]; then