geenforce display dispatcher: purge legacy HKLM Run autostart

This commit is contained in:
2026-07-30 14:30:04 -04:00
parent 2afe977ed5
commit 4610108a0c
7 changed files with 2028 additions and 1 deletions

View File

@@ -198,7 +198,27 @@ Get-ChildItem -LiteralPath $startup -Filter '*.url' -ErrorAction SilentlyContinu
}} catch {{}}
}}
$edgeArgs = "--kiosk `"$kioskUrl`" --edge-kiosk-type=fullscreen --no-first-run --no-default-browser-check"
# Legacy HKLM Run autostarts the OLD Dashboard/Lobby Inno installers planted.
# The .lnk/.url sweep above misses these -- a Run value relaunches the old kiosk
# URL at every logon and BEATS our Startup shortcut (this is why a display with
# our new .lnk still opened the old page). Purge them, then kill any running
# old-URL Edge so the display self-heals to the resolved target this cycle.
$legacyRun = 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run'
foreach ($legacyName in @('GE Aerospace Dashboard','GE Aerospace Lobby Display')) {{
try {{
if ($null -ne (Get-ItemProperty -LiteralPath $legacyRun -Name $legacyName -ErrorAction SilentlyContinue)) {{
Remove-ItemProperty -LiteralPath $legacyRun -Name $legacyName -Force -ErrorAction SilentlyContinue
Write-Host "removed legacy Run value $legacyName"
}}
}} catch {{}}
}}
try {{
Get-CimInstance Win32_Process -Filter "Name = 'msedge.exe'" -ErrorAction SilentlyContinue |
Where-Object {{ $_.CommandLine -match 'tv-dashboard|shopfloor-dashboard' }} |
ForEach-Object {{ Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue; Write-Host "killed old-URL kiosk Edge pid $($_.ProcessId)" }}
}} catch {{}}
$edgeArgs = "--kiosk `"$kioskUrl`" --edge-kiosk-type=fullscreen"
$lnkPath = Join-Path $startup 'ShopDB Kiosk.lnk'
$lnk = $shell.CreateShortcut($lnkPath)
$lnk.TargetPath = $edge

View File

@@ -25,6 +25,7 @@ EXCLUDES=(
--exclude-dir=dist
--exclude-dir=.git
--exclude-dir=versions
--exclude-dir=staticdocs
)
INCLUDES_CODE=(

104
scripts/gen_openapi.py Normal file
View File

@@ -0,0 +1,104 @@
"""Generate docs/openapi.json from docs/api-inventory.json.
The inventory is a list of {surface, endpoints:[{method,path,auth,params,purpose,
example}]} objects (one per API surface). Re-run after adding/changing endpoints
(update api-inventory.json first, or regenerate it). Served interactively at
/api/docs (see shopdb/core/api/docs.py).
venv/bin/python scripts/gen_openapi.py
"""
import json
import os
import re
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = os.path.dirname(HERE)
INVENTORY = os.path.join(REPO, 'docs', 'api-inventory.json')
OUT = os.path.join(REPO, 'docs', 'openapi.json')
VERBS = ('get', 'post', 'put', 'patch', 'delete', 'head', 'options')
def _norm_path(p):
# Flask <int:id> / <id> -> OpenAPI {id}
return re.sub(r'<(?:[^:>]+:)?([^>]+)>', r'{\1}', p or '')
def _security(auth):
a = (auth or '').lower()
if a in ('none', 'public', ''):
return []
if 'api-key' in a or 'api_key' in a or 'x-api-key' in a:
return [{'apiKeyAuth': []}]
return [{'bearerAuth': []}]
def build(surfaces):
paths = {}
tags = []
for s in surfaces:
tags.append({'name': s['surface']})
for e in s.get('endpoints', []):
for verb in re.split(r'[\/,]', (e.get('method') or 'GET')):
verb = verb.strip().lower()
if verb not in VERBS:
continue
path = _norm_path(e.get('path'))
if not path:
continue
desc = []
if e.get('purpose'):
desc.append(e['purpose'])
if e.get('auth'):
desc.append('\n\n**Auth:** ' + e['auth'])
if e.get('params'):
desc.append('\n\n**Params:** ' + e['params'])
if e.get('example'):
desc.append('\n\n**Example:**\n```\n' + e['example'] + '\n```')
paths.setdefault(path, {})[verb] = {
'tags': [s['surface']],
'summary': (e.get('purpose') or path)[:120],
'description': ''.join(desc),
'security': _security(e.get('auth')),
'responses': {'200': {
'description': 'Success (success_response envelope)'}},
}
return {
'openapi': '3.1.0',
'info': {
'title': 'ShopDB Flask API',
'version': '0.7.0',
'description': (
'Asset-management API (core + plugins). Responses use a '
'`success_response` envelope: `{status, data, meta}`. Auth: Bearer '
'JWT (login or a managed PAT) for `jwt`/`admin`/`permission:*`; '
'`X-API-Key` for collector/managed-token endpoints; public endpoints '
'need neither.'),
},
'servers': [
{'url': 'https://tsgwp00525.wjs.geaerospace.net/shopdb', 'description': 'WJ prod'},
{'url': '/', 'description': 'relative to the deployed mount'},
],
'components': {'securitySchemes': {
'bearerAuth': {'type': 'http', 'scheme': 'bearer', 'bearerFormat': 'JWT',
'description': 'Login token or a managed Personal Access Token (scoped).'},
'apiKeyAuth': {'type': 'apiKey', 'in': 'header', 'name': 'X-API-Key',
'description': 'Managed service token (e.g. collector.ingest, geenforce.fetch).'},
}},
'tags': tags,
'paths': paths,
}
def main():
surfaces = json.load(open(INVENTORY))
spec = build(surfaces)
json.dump(spec, open(OUT, 'w'), indent=1)
ops = sum(len(v) for v in spec['paths'].values())
print('wrote %s: %d paths, %d operations from %d surfaces'
% (OUT, len(spec['paths']), ops, len(surfaces)))
if __name__ == '__main__':
main()

View File

@@ -143,6 +143,7 @@ CORE_BLUEPRINT_NAMES = (
'setup',
'pluginui',
'apitokens',
'docs',
)

View File

@@ -23,6 +23,7 @@ from .customfields import customfields_bp
from .setup import setup_bp
from .pluginui import pluginui_bp
from .apitokens import apitokens_bp
from .docs import docs_bp
__all__ = [
'auth_bp',
@@ -48,4 +49,5 @@ __all__ = [
'setup_bp',
'pluginui_bp',
'apitokens_bp',
'docs_bp',
]

61
shopdb/core/api/docs.py Normal file
View File

@@ -0,0 +1,61 @@
"""Interactive API docs: self-hosted Redoc over the generated OpenAPI spec.
Served at /api/docs (relative to the mount). The Redoc bundle is vendored in
staticdocs/ so this works fully offline on the air-gapped prod box - no CDN.
The spec is docs/openapi.json in the repo (regenerate with
scripts/gen_openapi.py after API changes).
"""
import os
from flask import Blueprint, Response, send_file, url_for
docs_bp = Blueprint('docs', __name__)
_HERE = os.path.dirname(os.path.abspath(__file__))
# shopdb/core/api -> repo root
_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(_HERE)))
_SPEC = os.path.join(_REPO_ROOT, 'docs', 'openapi.json')
_LLMS = os.path.join(_REPO_ROOT, 'docs', 'llms.txt')
_REDOC = os.path.join(_HERE, 'staticdocs', 'redoc.standalone.js')
@docs_bp.route('/', strict_slashes=False)
def docs_index():
"""Redoc page. url_for keeps the asset URLs correct under any mount."""
spec_url = url_for('docs.openapi_spec')
redoc_url = url_for('docs.redoc_js')
page = (
'<!doctype html><html><head>'
'<title>ShopDB API</title>'
'<meta charset="utf-8">'
'<meta name="viewport" content="width=device-width, initial-scale=1">'
'<style>body{margin:0;padding:0}</style></head><body>'
'<redoc spec-url="%s"></redoc>'
'<script src="%s"></script>'
'</body></html>' % (spec_url, redoc_url)
)
return Response(page, mimetype='text/html')
@docs_bp.route('/openapi.json')
def openapi_spec():
"""The generated OpenAPI 3.1 spec (machine + LLM readable)."""
if not os.path.isfile(_SPEC):
return Response('{"error":"openapi.json not generated"}',
status=404, mimetype='application/json')
return send_file(_SPEC, mimetype='application/json')
@docs_bp.route('/redoc.standalone.js')
def redoc_js():
"""Vendored Redoc bundle (offline)."""
return send_file(_REDOC, mimetype='application/javascript')
@docs_bp.route('/llms.txt')
def llms_txt():
"""Concise LLM-oriented API guide."""
if not os.path.isfile(_LLMS):
return Response('llms.txt not found', status=404, mimetype='text/plain')
return send_file(_LLMS, mimetype='text/plain')

File diff suppressed because one or more lines are too long