feat: sync vehicle monitoring and accumulated platform updates
Add monitoring metrics, numeric sorting, report-time display, vehicle number labels, and map layout fixes. Include accumulated mileage, hydrogen, OneOS authentication, open platform, ingestion, deployment, and UI updates. Exclude temporary files, generated reports, and credentials. Validate frontend 632 tests and production build, API packages, vehicle-map tests, and release installer tests. Gateway suite has one pre-existing date-sensitive identity throttling test failure reproduced on the parent commit.
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the local platform against the real, explicitly allowed dev OneOS.
|
||||
|
||||
Reads dev OneOS's public browser config. Does not store login passwords, deploy,
|
||||
run migrations or modify upstream permissions. The real OneOS asset page is available
|
||||
with explicit menu grants. --real-data enables existing platform queries for verified
|
||||
OneOS administrators through a read-only bridge. Ctrl-C stops the local API and tunnel.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEV_ORIGIN = "https://dev-oneos.lnh2e.com"
|
||||
LOCAL_ORIGIN = "http://127.0.0.1:20400"
|
||||
DATA_ORIGIN = "http://127.0.0.1:20420"
|
||||
|
||||
|
||||
def read_ecs_query_config(ssh_target):
|
||||
# Read only the existing query credential and public map client key. Never
|
||||
# print them, copy database credentials, or write them into the repository.
|
||||
remote = '''import json, shlex
|
||||
from pathlib import Path
|
||||
values = {}
|
||||
for line in Path("/opt/lingniu-vehicle-platform/env/platform.env").read_text().splitlines():
|
||||
if "=" in line and not line.lstrip().startswith("#"):
|
||||
key, value = line.split("=", 1)
|
||||
values[key.strip()] = value.strip()
|
||||
raw = values["AUTH_TOKENS_JSON"]
|
||||
if raw.startswith("'"):
|
||||
raw = shlex.split(raw)[0]
|
||||
principals = json.loads(raw)
|
||||
token = next(p["token"] for p in principals if p.get("role") == "operator")
|
||||
key = values.get("AMAP_WEB_JS_KEY", "").strip("\\\"'")
|
||||
print(json.dumps({"token": token, "mapKey": key}))
|
||||
'''
|
||||
result = subprocess.run(["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", ssh_target, "python3 -"], input=remote, text=True, capture_output=True, check=True)
|
||||
value = json.loads(result.stdout)
|
||||
if len(value.get("token", "")) < 16:
|
||||
raise RuntimeError("ECS query credential is unavailable")
|
||||
return value
|
||||
|
||||
|
||||
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, *args, **kwargs):
|
||||
return None
|
||||
|
||||
|
||||
def public_config(opener):
|
||||
with opener.open(DEV_ORIGIN + "/_app.config.js", timeout=10) as response:
|
||||
raw = response.read(65537)
|
||||
if len(raw) > 65536:
|
||||
raise RuntimeError("dev OneOS public config is too large")
|
||||
match = re.search(r"window\._VBEN_ADMIN_PRO_APP_CONF_=(\{.*?\});", raw.decode())
|
||||
if not match:
|
||||
raise RuntimeError("dev OneOS public config format is unsupported")
|
||||
config = json.loads(match.group(1))
|
||||
if config.get("VITE_GLOB_API_URL") != "/prod-api" or config.get("VITE_GLOB_ENABLE_ENCRYPT") != "true":
|
||||
raise RuntimeError("dev OneOS API or encryption config changed; review required")
|
||||
for key in ("VITE_GLOB_APP_CLIENT_ID", "VITE_GLOB_RSA_PUBLIC_KEY"):
|
||||
if not isinstance(config.get(key), str) or not config[key]:
|
||||
raise RuntimeError("dev OneOS public authentication config is incomplete")
|
||||
return config
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--skip-web-build", action="store_true")
|
||||
parser.add_argument("--real-data", action="store_true", help="Read existing ECS platform data through a local SSH tunnel; writes remain blocked")
|
||||
parser.add_argument("--ssh-target", default="root@115.29.187.205")
|
||||
args = parser.parse_args()
|
||||
run = Path(tempfile.mkdtemp(prefix="vehicle-oneos-dev-"))
|
||||
os.chmod(run, 0o700)
|
||||
clean = {key: os.environ[key] for key in ("PATH", "HOME", "TMPDIR") if key in os.environ}
|
||||
clean.update({"GOPROXY": "off", "GOSUMDB": "off", "GOTOOLCHAIN": "local", "NO_UPDATE_NOTIFIER": "1"})
|
||||
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirect())
|
||||
child = None
|
||||
tunnel = None
|
||||
log = None
|
||||
try:
|
||||
config = public_config(opener)
|
||||
subprocess.run([shutil.which("go"), "build", "-o", str(run / "platform"), "./cmd/platform-api"], cwd=ROOT / "apps/api", env=clean, check=True)
|
||||
if not args.skip_web_build:
|
||||
with (run / "web-build.log").open("w") as build_log:
|
||||
subprocess.run([shutil.which("pnpm"), "build"], cwd=ROOT / "apps/web", env=clean, stdout=build_log, stderr=subprocess.STDOUT, check=True)
|
||||
if not (ROOT / "apps/web/dist/index.html").is_file():
|
||||
raise RuntimeError("local frontend build is missing")
|
||||
api_env = dict(clean)
|
||||
api_env.update({
|
||||
"HTTP_ADDR": "127.0.0.1:20400", "DATA_MODE": "mock", "AUTH_MODE": "enforce",
|
||||
"STATIC_DIR": str(ROOT / "apps/web/dist"), "EXPORT_DIR": str(run / "exports"),
|
||||
"ONEOS_DEV_INTEGRATION": "true", "ONEOS_AUTH_ENABLED": "true", "ONEOS_PASSWORD_ENABLED": "true",
|
||||
# Go 1.24+ otherwise rejects dev OneOS's published legacy 512-bit key.
|
||||
# This isolated dev process still validates the fixed HTTPS origin.
|
||||
"GODEBUG": "rsa1024min=0",
|
||||
"ONEOS_PROFILE_BASE_URL": DEV_ORIGIN + "/prod-api",
|
||||
"ONEOS_PROFILE_ISSUER": DEV_ORIGIN, "ONEOS_PROFILE_ALLOWED_TENANT": "000000",
|
||||
"ONEOS_PUBLIC_ORIGIN": LOCAL_ORIGIN,
|
||||
"ONEOS_PASSWORD_CLIENT_ID": config["VITE_GLOB_APP_CLIENT_ID"],
|
||||
"ONEOS_PASSWORD_RSA_PUBLIC_KEY": config["VITE_GLOB_RSA_PUBLIC_KEY"],
|
||||
# No invented upstream scope endpoint or SSO credentials.
|
||||
"ONEOS_VEHICLE_SCOPE_PATH": "", "ONEOS_LOGIN_TICKET_EXCHANGE_PATH": "",
|
||||
})
|
||||
if args.real_data:
|
||||
upstream = read_ecs_query_config(args.ssh_target)
|
||||
tunnel = subprocess.Popen(["ssh", "-N", "-o", "BatchMode=yes", "-o", "ExitOnForwardFailure=yes", "-o", "ConnectTimeout=10", "-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=3", "-L", "127.0.0.1:20420:127.0.0.1:20300", args.ssh_target], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
deadline = time.monotonic() + 15
|
||||
while True:
|
||||
if tunnel.poll() is not None:
|
||||
raise RuntimeError("ECS read-only tunnel could not start")
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", 20420), timeout=0.5):
|
||||
break
|
||||
except OSError:
|
||||
if time.monotonic() > deadline:
|
||||
raise RuntimeError("ECS read-only tunnel timed out")
|
||||
time.sleep(0.1)
|
||||
req = urllib.request.Request(DATA_ORIGIN + "/api/v2/monitor/summary", headers={"Authorization": "Bearer " + upstream["token"]})
|
||||
with opener.open(req, timeout=20) as response:
|
||||
if "data" not in json.load(response):
|
||||
raise RuntimeError("ECS platform query preflight failed")
|
||||
api_env.update({"ONEOS_DEV_DATA_PROXY_URL": DATA_ORIGIN, "ONEOS_DEV_DATA_PROXY_TOKEN": upstream["token"], "AMAP_WEB_JS_KEY": upstream["mapKey"], "AMAP_SECURITY_SERVICE_HOST": "/_AMapService", "REQUEST_TIMEOUT_MS": "30000"})
|
||||
log = (run / "platform.log").open("w")
|
||||
child = subprocess.Popen([str(run / "platform")], env=api_env, stdout=log, stderr=subprocess.STDOUT)
|
||||
deadline = time.monotonic() + 10
|
||||
while True:
|
||||
try:
|
||||
with opener.open(LOCAL_ORIGIN + "/api/v2/auth/providers", timeout=1) as response:
|
||||
provider = json.load(response)["data"]["oneos"]
|
||||
if not provider.get("devIntegration") or not provider.get("passwordEnabled") or bool(provider.get("realDataEnabled")) != args.real_data:
|
||||
raise RuntimeError("dev authentication startup is not ready")
|
||||
break
|
||||
except (OSError, ValueError, KeyError):
|
||||
if child.poll() is not None or time.monotonic() > deadline:
|
||||
raise RuntimeError("local dev integration failed to start; see platform.log")
|
||||
time.sleep(0.1)
|
||||
print(json.dumps({"platform": LOCAL_ORIGIN, "oneos": DEV_ORIGIN, "authentication": "real-dev-oneos", "businessAuthorization": "ecs-admin-readonly" if args.real_data else "oneos-asset-catalog-with-menu-grants", "simulator": False, "runDirectory": str(run)}, ensure_ascii=False), flush=True)
|
||||
while child.poll() is None:
|
||||
time.sleep(0.5)
|
||||
raise RuntimeError("local platform exited")
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
if child is not None and child.poll() is None:
|
||||
child.terminate()
|
||||
try:
|
||||
child.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
child.kill()
|
||||
child.wait()
|
||||
if log is not None:
|
||||
log.close()
|
||||
if tunnel is not None and tunnel.poll() is None:
|
||||
tunnel.terminate()
|
||||
try:
|
||||
tunnel.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
tunnel.kill()
|
||||
tunnel.wait()
|
||||
print(f"Local platform stopped. Evidence: {run}", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt()))
|
||||
main()
|
||||
Reference in New Issue
Block a user