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:
lingniu
2026-10-26 18:05:59 +08:00
parent 8b32ae2988
commit d6b957dfc8
290 changed files with 28238 additions and 1647 deletions
@@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""Run a local read/write API using the existing production stores and both login providers.
Requires the caller's existing ECS SSH authorization. Never logs or persists credentials.
"""
import argparse
import importlib.util
import ipaddress
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
import urllib.parse
ROOT = Path(__file__).resolve().parents[1]
def loopback_origin(value):
try:
parsed = urllib.parse.urlsplit(value)
if (parsed.scheme != "http" or not ipaddress.ip_address(parsed.hostname).is_loopback
or parsed.username is not None or parsed.password is not None
or parsed.path or parsed.query or parsed.fragment
or parsed.port is None or not 1024 <= parsed.port <= 65535
or value != "http://" + parsed.netloc):
raise ValueError()
except (ValueError, TypeError):
raise argparse.ArgumentTypeError("public origin must be an HTTP loopback origin with a port") from None
return value
def production_config(target):
remote = '''import subprocess,pathlib,json
pid=subprocess.check_output(["systemctl","show","lingniu-vehicle-platform.service","--property=MainPID"],universal_newlines=True).strip().split("=",1)[1]
values=dict(x.split("=",1) for x in pathlib.Path("/proc/"+pid+"/environ").read_text().split("\\0") if "=" in x)
keys=["MYSQL_DSN","TDENGINE_DSN","TDENGINE_DATABASE","TDENGINE_DRIVER","REDIS_ADDR","REDIS_USERNAME","REDIS_PASSWORD","REDIS_DB","AMAP_WEB_JS_KEY","AMAP_API_KEY","AMAP_SECURITY_JS_CODE"]
print(json.dumps({k:values[k] for k in keys if k in values}))
'''
result = subprocess.run(["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10", target, "python3 -"], input=remote, capture_output=True, text=True, check=True)
return json.loads(result.stdout)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--ssh-target", default="root@115.29.187.205")
parser.add_argument("--oneos-origin", choices=["https://dev-oneos.lnh2e.com", "https://lnoneos.com"], default="https://dev-oneos.lnh2e.com", help="OneOS authentication and vehicle data origin")
parser.add_argument("--port", type=int, default=20400)
parser.add_argument("--public-origin", type=loopback_origin, help="Browser origin when using a separate local frontend dev server")
parser.add_argument("--skip-web-build", action="store_true")
parser.add_argument("--reuse-tunnels", action="store_true", help="Reuse the active local backend tunnels without owning them")
parser.add_argument("--no-hydrogen-worker", action="store_true", help="Do not launch customer hydrogen projection worker")
parser.add_argument("--tdengine-transport", choices=["websocket", "rest"], default="websocket", help="TDengine transport for the local API; REST requires --no-hydrogen-worker")
args = parser.parse_args()
if args.tdengine_transport == "rest" and not args.no_hydrogen_worker:
parser.error("--tdengine-transport rest requires --no-hydrogen-worker")
if not 1024 <= args.port <= 65535:
raise RuntimeError("invalid local port")
# Refuse an occupied API port before connecting the production store.
with socket.socket() as listener:
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind(("127.0.0.1", args.port))
spec = importlib.util.spec_from_file_location("oneos_dev", Path(__file__).with_name("oneos-dev.py"))
dev = importlib.util.module_from_spec(spec)
spec.loader.exec_module(dev)
dev.DEV_ORIGIN = args.oneos_origin
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), dev.NoRedirect())
public = dev.public_config(opener)
run = Path(tempfile.mkdtemp(prefix="vehicle-oneos-backend-"))
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"})
subprocess.run([shutil.which("go"), "build", "-o", str(run / "platform"), "./cmd/platform-api"], cwd=ROOT / "apps/api", env=clean, check=True)
if not args.no_hydrogen_worker:
subprocess.run([shutil.which("go"), "build", "-o", str(run / "customer-hydrogen-worker"), "./cmd/stats-backfill"], cwd=ROOT.parent / "go/vehicle-gateway", env=clean, check=True)
if not args.skip_web_build:
with (run / "web-build.log").open("w") as output:
subprocess.run([shutil.which("pnpm"), "build"], cwd=ROOT / "apps/web", env=clean, stdout=output, stderr=subprocess.STDOUT, check=True)
values = production_config(args.ssh_target)
forwards = []
mysql = re.search(r"@tcp\(([^)]+)\)/([^?]+)", values.get("MYSQL_DSN", ""))
if not mysql:
raise RuntimeError("production MySQL TCP configuration unavailable")
forwards.append((20430, mysql.group(1)))
values["MYSQL_DSN"] = values["MYSQL_DSN"].replace("@tcp(" + mysql.group(1) + ")", "@tcp(127.0.0.1:20430)", 1)
if values.get("TDENGINE_DSN"):
td = re.search(r"@ws\(([^)]+)\)", values["TDENGINE_DSN"])
if not td:
raise RuntimeError("production TDengine websocket configuration unavailable")
forwards.append((20441, td.group(1)))
values["TDENGINE_DSN"] = values["TDENGINE_DSN"].replace("@ws(" + td.group(1) + ")", "@ws(127.0.0.1:20441)", 1)
if args.tdengine_transport == "rest":
values["TDENGINE_DRIVER"] = "taosRestful"
values["TDENGINE_DSN"] = values["TDENGINE_DSN"].replace("@ws(127.0.0.1:20441)", "@http(127.0.0.1:20441)", 1)
if values.get("REDIS_ADDR"):
forwards.append((20479, values["REDIS_ADDR"]))
values["REDIS_ADDR"] = "127.0.0.1:20479"
command = ["ssh", "-N", "-o", "BatchMode=yes", "-o", "ExitOnForwardFailure=yes", "-o", "ConnectTimeout=10", "-o", "ServerAliveInterval=15", "-o", "ServerAliveCountMax=3"]
for port, destination in forwards:
command += ["-L", "127.0.0.1:%d:%s" % (port, destination)]
command.append(args.ssh_target)
tunnel = child = hydrogen_worker = None
log = None
origin = "http://127.0.0.1:%d" % args.port
public_origin = args.public_origin or origin
try:
if not args.reuse_tunnels:
tunnel = subprocess.Popen(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
deadline = time.monotonic() + 15
for port, _ in forwards:
while True:
if tunnel is not None and tunnel.poll() is not None:
raise RuntimeError("production tunnel could not start")
try:
with socket.create_connection(("127.0.0.1", port), timeout=0.5):
break
except OSError:
if time.monotonic() > deadline:
raise RuntimeError("production tunnel timed out")
time.sleep(0.1)
env = dict(clean)
env.update(values)
env.update({
"HTTP_ADDR": "127.0.0.1:%d" % args.port, "DATA_MODE": "production", "AUTH_MODE": "enforce",
"STATIC_DIR": str(ROOT / "apps/web/dist"), "EXPORT_DIR": str(run / "exports"),
"ONEOS_LOCAL_BACKEND": "true", "ONEOS_DEV_INTEGRATION": "true", "ONEOS_AUTH_ENABLED": "true",
"ONEOS_PASSWORD_ENABLED": "true", "ONEOS_VEHICLE_QUERY_SCOPE": "true",
"ONEOS_PROFILE_BASE_URL": dev.DEV_ORIGIN + "/prod-api", "ONEOS_PROFILE_ISSUER": dev.DEV_ORIGIN,
"ONEOS_PROFILE_ALLOWED_TENANT": "000000", "ONEOS_PUBLIC_ORIGIN": public_origin,
"ONEOS_PASSWORD_CLIENT_ID": public["VITE_GLOB_APP_CLIENT_ID"],
"ONEOS_PASSWORD_RSA_PUBLIC_KEY": public["VITE_GLOB_RSA_PUBLIC_KEY"],
"ONEOS_VEHICLE_SCOPE_PATH": "", "ONEOS_LOGIN_TICKET_EXCHANGE_PATH": "",
"GODEBUG": "rsa1024min=0", "AMAP_SECURITY_SERVICE_HOST": public_origin + "/_AMapService",
"ALERT_STREAM_MODE": "disabled", "HISTORY_EXPORT_CLEANUP_AUTOMATION_ENABLED": "false",
"DAILY_GEOGRAPHY_ENABLED": "false", "REQUEST_TIMEOUT_MS": "60000",
})
log = (run / "platform.log").open("w")
os.chmod(run / "platform.log", 0o600)
child = subprocess.Popen([str(run / "platform")], env=env, stdout=log, stderr=subprocess.STDOUT)
deadline = time.monotonic() + 20
while True:
try:
with opener.open(origin + "/api/v2/auth/providers", timeout=2) as response:
providers = json.load(response)["data"]
if not providers["local"]["enabled"] or not providers["oneos"]["authorizationReady"]:
raise RuntimeError("both login providers must be available")
break
except (OSError, ValueError, KeyError):
if child.poll() is not None or time.monotonic() > deadline:
raise RuntimeError("backend startup failed; see private runtime log")
time.sleep(0.2)
if not args.no_hydrogen_worker:
worker_env = dict(env)
worker_env.update({"BACKFILL_ENV_FILES": "/dev/null", "CUSTOMER_HYDROGEN_WORKER": "true", "BACKFILL_PROTOCOLS": "GB32960,YUTONG_MQTT", "BACKFILL_PROGRESS_EVERY": "10000"})
hydrogen_worker = subprocess.Popen([str(run / "customer-hydrogen-worker")], env=worker_env, stdout=log, stderr=subprocess.STDOUT)
print(json.dumps({"platform": origin, "oneos": dev.DEV_ORIGIN, "dataSource": "production", "readWrite": True, "localLogin": True, "oneosVehicleScope": "caller-vehicle-query", "tdengineTransport": args.tdengine_transport, "runDirectory": str(run)}, ensure_ascii=False), flush=True)
while child.poll() is None:
time.sleep(0.5)
raise RuntimeError("local backend exited")
except KeyboardInterrupt:
pass
finally:
for process in (hydrogen_worker, child, tunnel):
if process is not None and process.poll() is None:
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
process.wait()
if log:
log.close()
print("Local backend stopped. Evidence: " + str(run), flush=True)
if __name__ == "__main__":
signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt()))
main()