Fix bit(1) import coercion and subpath login redirect; add GitHub export script
Loader: bool() on pymysql bit(1) bytes is always true - isinstallable and isshopfloor imported as 1 for every row; route through _truthy_bit. The employee source DB is now optional (shopdb-only imports). Frontend: under a subpath mount the 401 interceptor stored the browser path (mount base included) as the login redirect and the router applied its base again (/ops/ops). New stripBase() keeps redirects base-free. tools/export-github.sh automates the publication flow: prune + scrub + commit into ~/projects/shopdb-flask-pub and emit a transfer bundle.
This commit is contained in:
@@ -108,7 +108,7 @@ SQLite). Do not run dev or production against SQLite.
|
|||||||
|
|
||||||
### Distribution
|
### Distribution
|
||||||
|
|
||||||
The application is distributed internally through the GE Aerospace Gitea. Clone
|
The application is distributed through the internal GE Aerospace git server. Clone
|
||||||
it from there; there is no public package or image registry.
|
it from there; there is no public package or image registry.
|
||||||
|
|
||||||
### Fast path (Docker)
|
### Fast path (Docker)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import axios from 'axios'
|
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
|
// 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.
|
// 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.
|
// Preserve the destination so login returns the user to this page.
|
||||||
if (hadToken) {
|
if (hadToken) {
|
||||||
const loginPath = withBase('/login')
|
const loginPath = withBase('/login')
|
||||||
const here = window.location.pathname + window.location.search
|
// Router paths exclude the mount base; strip it or login's
|
||||||
const target = here && here !== loginPath
|
// 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 + '?redirect=' + encodeURIComponent(here)
|
||||||
: loginPath
|
: loginPath
|
||||||
window.location.href = target
|
window.location.href = target
|
||||||
|
|||||||
@@ -11,3 +11,15 @@ export function withBase(path) {
|
|||||||
if (/^([a-z]+:)?\/\//i.test(path) || path.startsWith('data:')) return path
|
if (/^([a-z]+:)?\/\//i.test(path) || path.startsWith('data:')) return path
|
||||||
return BASE_URL + String(path).replace(/^\//, '')
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ import { useRouter, useRoute } from 'vue-router'
|
|||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
import { setupApi } from '../api'
|
import { setupApi } from '../api'
|
||||||
import { getSiteLogo } from '../utils/siteSettings'
|
import { getSiteLogo } from '../utils/siteSettings'
|
||||||
import { withBase } from '../utils/basePath'
|
import { withBase, stripBase } from '../utils/basePath'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -63,7 +63,9 @@ const authStore = useAuthStore()
|
|||||||
function postLoginTarget() {
|
function postLoginTarget() {
|
||||||
const redirect = route.query.redirect
|
const redirect = route.query.redirect
|
||||||
if (typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//')) {
|
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 '/'
|
return '/'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ the machineid->assetid crosswalk this harness persists.
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
|
import pymysql
|
||||||
|
|
||||||
from .harness import Harness
|
from .harness import Harness
|
||||||
|
|
||||||
|
|
||||||
@@ -451,7 +453,7 @@ def stage_applications(h):
|
|||||||
continue
|
continue
|
||||||
payload = {'appname': name, 'appdescription': a['appdescription'],
|
payload = {'appname': name, 'appdescription': a['appdescription'],
|
||||||
'supportteamid': h.ids.get('supportteam', a['supportteamid']),
|
'supportteamid': h.ids.get('supportteam', a['supportteamid']),
|
||||||
'isinstallable': bool(a['isinstallable']),
|
'isinstallable': _truthy_bit(a['isinstallable']),
|
||||||
'applicationnotes': a['applicationnotes'], 'installpath': a['installpath'],
|
'applicationnotes': a['applicationnotes'], 'installpath': a['installpath'],
|
||||||
'applicationlink': a['applicationlink'],
|
'applicationlink': a['applicationlink'],
|
||||||
'documentationpath': a['documentationpath']}
|
'documentationpath': a['documentationpath']}
|
||||||
@@ -534,13 +536,17 @@ def stage_notifications(h):
|
|||||||
|
|
||||||
# SSO -> "First Last" from the employee source, so recognition/training
|
# SSO -> "First Last" from the employee source, so recognition/training
|
||||||
# notifications display a name, not a bare SSO (the model shows employeename
|
# 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 = {}
|
ssoname = {}
|
||||||
|
try:
|
||||||
for e in h.source.rows('wjf_employees_src',
|
for e in h.source.rows('wjf_employees_src',
|
||||||
'SELECT SSO, First_Name, Last_Name FROM employees'):
|
'SELECT SSO, First_Name, Last_Name FROM employees'):
|
||||||
full = f"{(e['First_Name'] or '').strip()} {(e['Last_Name'] or '').strip()}".strip()
|
full = f"{(e['First_Name'] or '').strip()} {(e['Last_Name'] or '').strip()}".strip()
|
||||||
if full:
|
if full:
|
||||||
ssoname[str(e['SSO']).strip()] = full
|
ssoname[str(e['SSO']).strip()] = full
|
||||||
|
except pymysql.err.OperationalError:
|
||||||
|
print(' (no wjf_employees_src - notifications keep bare SSOs)')
|
||||||
|
|
||||||
def _names(employeesso):
|
def _names(employeesso):
|
||||||
if not employeesso:
|
if not employeesso:
|
||||||
@@ -566,7 +572,7 @@ def stage_notifications(h):
|
|||||||
'appid': h.ids.get('app', n['appid']),
|
'appid': h.ids.get('app', n['appid']),
|
||||||
'starttime': str(n['starttime']) if n['starttime'] else None,
|
'starttime': str(n['starttime']) if n['starttime'] else None,
|
||||||
'endtime': endtime, 'ticketnumber': n['ticketnumber'], 'link': n['link'],
|
'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'])}
|
'employeename': _names(n['employeesso'])}
|
||||||
status, _ = h.post('/api/notifications', payload)
|
status, _ = h.post('/api/notifications', payload)
|
||||||
if status in (200, 201):
|
if status in (200, 201):
|
||||||
@@ -759,7 +765,11 @@ def stage_verify(h):
|
|||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
report = {}
|
report = {}
|
||||||
for label, sdb, sql, target_table in checks:
|
for label, sdb, sql, target_table in checks:
|
||||||
|
try:
|
||||||
src = h.source.rows(sdb, sql)[0]['c']
|
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()
|
tgt = db.session.execute(text(f'SELECT COUNT(*) FROM {target_table}')).scalar()
|
||||||
report[label] = f'source~{src} target={tgt}'
|
report[label] = f'source~{src} target={tgt}'
|
||||||
return report
|
return report
|
||||||
|
|||||||
127
tools/export-github.sh
Executable file
127
tools/export-github.sh
Executable file
@@ -0,0 +1,127 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Export the working repo to the GitHub publication repo and emit a bundle.
|
||||||
|
#
|
||||||
|
# Pipeline: working repo (full history, internal refs) -> pruned/scrubbed
|
||||||
|
# tree -> commit in ~/projects/shopdb-flask-pub (the local mirror of what
|
||||||
|
# enterprise GitHub holds) -> full git bundle in /home/camp/pxe-images/ for
|
||||||
|
# transfer to the work PC, which pushes it to GitHub.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# tools/export-github.sh "Commit message for the publication commit"
|
||||||
|
# tools/export-github.sh --dist # also rebuild the /ops frontend dist
|
||||||
|
#
|
||||||
|
# This script lives in tools/, which is itself excluded from publication.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
WORK=/home/camp/projects/shopdb-flask
|
||||||
|
PUB=/home/camp/projects/shopdb-flask-pub
|
||||||
|
OUT=/home/camp/pxe-images
|
||||||
|
|
||||||
|
BUILD_DIST=0
|
||||||
|
MSG=""
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--dist) BUILD_DIST=1 ;;
|
||||||
|
*) MSG="$arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ -n "$MSG" ] || { echo "usage: $0 [--dist] \"commit message\""; exit 1; }
|
||||||
|
[ -d "$PUB/.git" ] || { echo "publication repo missing at $PUB"; exit 1; }
|
||||||
|
|
||||||
|
# --- 1. sync the tree (working -> pub), minus everything never published ---
|
||||||
|
rsync -a --delete \
|
||||||
|
--exclude '.git' \
|
||||||
|
--exclude '.gitea' \
|
||||||
|
--exclude 'docs' \
|
||||||
|
--exclude 'tools' \
|
||||||
|
--exclude 'start-api.sh' \
|
||||||
|
--exclude 'start-ui.sh' \
|
||||||
|
--exclude 'CLAUDE.md' \
|
||||||
|
--exclude 'frontend/CLAUDE.md' \
|
||||||
|
--exclude 'tests/test_docs_contract.py' \
|
||||||
|
--exclude 'tests/test_plugins/test_geenforce_parity.py' \
|
||||||
|
--exclude 'tests/test_plugins/test_zabbix_live.py' \
|
||||||
|
--exclude 'venv' \
|
||||||
|
--exclude 'node_modules' \
|
||||||
|
--exclude 'frontend/dist*' \
|
||||||
|
--exclude 'instance' \
|
||||||
|
--exclude '.env' \
|
||||||
|
--exclude '__pycache__' \
|
||||||
|
--exclude '*.pyc' \
|
||||||
|
--exclude 'scripts/site_imports/wjf/idmap.json' \
|
||||||
|
"$WORK/" "$PUB/"
|
||||||
|
|
||||||
|
cd "$PUB"
|
||||||
|
|
||||||
|
# --- 2. re-apply the publication wording (idempotent) ---
|
||||||
|
# docs/ lives only in the wiki on the GitHub side.
|
||||||
|
grep -rlZ 'docs/' --include='*.py' --include='*.md' --include='*.sh' \
|
||||||
|
--include='*.tmpl' --include='*.vue' --include='*.example' . 2>/dev/null |
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
case "$f" in ./CHANGELOG.md) continue ;; esac
|
||||||
|
sed -i -E \
|
||||||
|
-e 's/\[`?docs\/([A-Za-z0-9_-]+)\.md`?\]\((\.\.\/)*docs\/[A-Za-z0-9_-]+\.md\)/the \1 page in the project wiki/g' \
|
||||||
|
-e 's/docs\/proposals\/ge-enforce-plugin\.md/the ge-enforce-plugin proposal in the project wiki/g' \
|
||||||
|
-e 's/`docs\/([A-Za-z0-9_-]+)\.md`/the \1 wiki page/g' \
|
||||||
|
-e 's/docs\/([A-Za-z0-9_-]+)\.md/the \1 wiki page/g' "$f"
|
||||||
|
done
|
||||||
|
sed -i 's|<code>docs/GE-ENFORCE.md</code>|the GE-ENFORCE page in the project wiki|' \
|
||||||
|
frontend/src/views/geenforce/ManifestEditor.vue 2>/dev/null || true
|
||||||
|
# internal infra never named on GitHub; this repo's own URL maps to the
|
||||||
|
# real GitHub home, anything else degrades to a placeholder.
|
||||||
|
GITHUB_URL='https://github.com/ge-aero/shopdb-flask'
|
||||||
|
grep -rlZ 'gitea\.proudtech\.net' . 2>/dev/null | while IFS= read -r -d '' f; do
|
||||||
|
sed -i -e "s|https://gitea\.proudtech\.net/ge-aerospace/shopdb-flask|$GITHUB_URL|g" \
|
||||||
|
-e 's|gitea\.proudtech\.net|<git-host>|g' "$f"
|
||||||
|
done
|
||||||
|
grep -rlZi 'gitea' --exclude-dir=.git . 2>/dev/null | while IFS= read -r -d '' f; do
|
||||||
|
sed -i -e 's/the GE Aerospace Gitea/the internal GE Aerospace git server/g' \
|
||||||
|
-e 's/Gitea Actions CI/CI/g' \
|
||||||
|
-e 's/Gitea Actions/CI/g' "$f"
|
||||||
|
done
|
||||||
|
# CHANGELOG compare/release link definitions reference tags that do not
|
||||||
|
# exist on GitHub (history is squashed there) - drop them.
|
||||||
|
sed -i '/^\[[^]]*\]: .*\/\(compare\|releases\)\//d' CHANGELOG.md
|
||||||
|
# frontend/CLAUDE.md publishes under a neutral name
|
||||||
|
if [ -f "$WORK/frontend/CLAUDE.md" ]; then
|
||||||
|
cp "$WORK/frontend/CLAUDE.md" frontend/DEVELOPMENT-STANDARDS.md
|
||||||
|
fi
|
||||||
|
sed -i "s/rootpassword/changeme/g" shopdb/config.py 2>/dev/null || true
|
||||||
|
|
||||||
|
# --- 3. scrub gate: refuse to commit if anything internal leaks ---
|
||||||
|
LEAKS=$(grep -rlIiE 'claude|anthropic|fable 5|gitea|proudtech|home/camp|rootpassword' \
|
||||||
|
--exclude-dir=.git . || true)
|
||||||
|
if [ -n "$LEAKS" ]; then
|
||||||
|
echo "SCRUB GATE FAILED - internal references in:"; echo "$LEAKS"; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 4. commit (no-op safe) ---
|
||||||
|
git add -A
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "no changes vs publication tree - nothing to export"
|
||||||
|
else
|
||||||
|
git commit -m "$MSG"
|
||||||
|
fi
|
||||||
|
git log --oneline -3
|
||||||
|
|
||||||
|
# --- 5. bundle for transfer (full bundle: stateless, fetch takes only new) ---
|
||||||
|
git bundle create "$OUT/shopdb-flask-pub.bundle" HEAD main --tags
|
||||||
|
git bundle verify "$OUT/shopdb-flask-pub.bundle" >/dev/null
|
||||||
|
echo "bundle: $OUT/shopdb-flask-pub.bundle"
|
||||||
|
|
||||||
|
# --- 6. optional /ops dist for the prod server ---
|
||||||
|
if [ "$BUILD_DIST" = 1 ]; then
|
||||||
|
cd "$WORK/frontend"
|
||||||
|
VITE_BASE_PATH=/ops/ npm run build --silent
|
||||||
|
rm -rf "$OUT/frontend-dist-subpath-ops"
|
||||||
|
cp -r dist "$OUT/frontend-dist-subpath-ops"
|
||||||
|
echo "dist: $OUT/frontend-dist-subpath-ops/"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<'EOF'
|
||||||
|
|
||||||
|
On the work PC (in the shopdb-flask-pub clone):
|
||||||
|
git fetch <path-to>\shopdb-flask-pub.bundle main
|
||||||
|
git merge --ff-only FETCH_HEAD
|
||||||
|
git push origin main
|
||||||
|
EOF
|
||||||
Reference in New Issue
Block a user