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,32 @@
#!/usr/bin/env python3
"""Server-local administrative runner. Loads credentials without printing them."""
import os
from pathlib import Path
import shlex
import subprocess
import sys
root = Path('/opt/lingniu-gzev/current')
env = os.environ.copy()
for path in [Path('/opt/lingniu-go-native/env/base.env'),
Path('/opt/lingniu-vehicle-platform/env/platform.env'),
Path('/opt/lingniu-gzev/env/gzev.env')]:
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line.startswith('#'):
continue
key, sep, value = line.partition('=')
if not sep:
raise SystemExit('Invalid environment file assignment')
# Existing service env files use single-line assignments, optionally quoted.
if value.startswith(('"', "'")):
parts = shlex.split(value, comments=False)
if len(parts) != 1:
raise SystemExit('Invalid quoted environment value')
value = parts[0]
env[key.strip()] = value
if len(sys.argv) > 1 and sys.argv[1] == 'migrate':
args = [str(root / 'platform-migrate'), str(root / '049_gzev_daily_mileage.sql')]
else:
args = [str(root / 'gzev-mileage')] + sys.argv[1:]
raise SystemExit(subprocess.call(args, env=env))
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Import a browser session using hidden prompts, verify account, then replace atomically."""
import argparse
import getpass
import json
import os
from pathlib import Path
import tempfile
import urllib.error
import urllib.parse
import urllib.request
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--file', default='/var/lib/lingniu-gzev/session.json')
parser.add_argument('--account', default='guangdonglingniu')
args = parser.parse_args()
token = getpass.getpass('Chrome token Cookie (hidden): ').strip()
routing = getpass.getpass('Chrome bigs Cookie (hidden, may be empty): ').strip()
if not token or any(ch in token + routing for ch in '\r\n;'):
raise SystemExit('Invalid cookie value')
session = {'authorization': 'Bearer ' + token,
'cookies': [{'name': 'token', 'value': token}]}
if routing:
session['cookies'].append({'name': 'bigs', 'value': routing})
url = 'https://gzev.d.lnoneos.com/evdata-sso/api/base/getUserInfo?' + urllib.parse.urlencode({'token': token})
request = urllib.request.Request(url, headers={
'Authorization': session['authorization'],
'Cookie': '; '.join(c['name'] + '=' + c['value'] for c in session['cookies'])})
try:
with urllib.request.build_opener(NoRedirect()).open(request, timeout=30) as response:
result = json.load(response)
if str(result.get('code')) != '0' or result.get('data', {}).get('loginname') != args.account:
raise ValueError('identity mismatch')
except Exception:
raise SystemExit('Session validation failed; existing file unchanged') from None
path = Path(args.file)
path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
fd, name = tempfile.mkstemp(prefix='.session-', dir=str(path.parent))
try:
with os.fdopen(fd, 'w', encoding='utf-8') as stream:
json.dump(session, stream)
stream.flush()
os.fsync(stream.fileno())
os.replace(name, str(path))
finally:
if os.path.exists(name):
os.unlink(name)
print('Session validated and replaced; restart the mileage service to resume.')
if __name__ == '__main__':
main()
@@ -0,0 +1,14 @@
# Add inside the existing HTTPS vehicle.d.lnoneos.com server.
# Production uploader binds the existing private VPC interface.
location = /gzev/session {
return 308 /gzev/session/;
}
location /gzev/session/ {
client_max_body_size 32k;
proxy_pass http://172.17.111.55:20312;
proxy_set_header Host vehicle.d.lnoneos.com;
proxy_set_header X-Forwarded-Proto https;
proxy_read_timeout 45s;
# Do not log POST bodies or credentials.
access_log off;
}
@@ -0,0 +1,8 @@
GZEV_ACCOUNT=guangdonglingniu
GZEV_START_DATE=2024-01-01
GZEV_SESSION_FILE=/var/lib/lingniu-gzev/session.json
GZEV_UPLOAD_ADDR=172.17.111.55:20312
GZEV_SESSION_UPLOAD_URL=https://vehicle.d.lnoneos.com/gzev/session/
# MYSQL_DSN is inherited from the existing platform environment.
# Configure only on the server in a mode-0600 file. Never commit a real SendKey.
# SERVERCHAN_SENDKEY=
@@ -95,9 +95,11 @@ for _ in $(seq 1 20); do
sleep 1
done
test "$ready" = true || { printf 'open platform service did not become ready\n' >&2; exit 1; }
"$CURL_BIN" -fsS "$BASE_URL/" | grep -q '<div id="root"></div>'
"$CURL_BIN" -fsS "$BASE_URL/open-api/openapi.yaml" | grep -q '^openapi: 3.0.3'
"$CURL_BIN" -fsS "$BASE_URL/portal-api/catalog" | grep -q 'daily_hydrogen'
# Read the entire response: grep -q can close a large document's pipe early,
# making curl fail with code 23 under pipefail and roll back a healthy release.
"$CURL_BIN" -fsS "$BASE_URL/" | grep '<div id="root"></div>' >/dev/null
"$CURL_BIN" -fsS "$BASE_URL/open-api/openapi.yaml" | grep '^openapi: 3.0.3' >/dev/null
"$CURL_BIN" -fsS "$BASE_URL/portal-api/catalog" | grep 'daily_hydrogen' >/dev/null
python3 - "$ROOT" "$RELEASE_HISTORY_LIMIT" "$next" "$old" <<'PY'
import pathlib
@@ -29,7 +29,10 @@ set -e
url=${@: -1}
case "$url" in
*/healthz) printf '{"status":"ok"}\n' ;;
*/open-api/openapi.yaml) printf 'openapi: 3.0.3\n' ;;
*/open-api/openapi.yaml)
printf 'openapi: 3.0.3\n'
python3 -c 'import sys; sys.stdout.write("schema-padding: " + "x" * 150000 + "\n")'
;;
*/portal-api/catalog) printf '{"data":[{"code":"daily_hydrogen"}]}\n' ;;
*) printf '<div id="root"></div>\n' ;;
esac
@@ -103,7 +103,7 @@ trap rollback ERR
test ! -e "$next" || { printf 'release already exists: %s\n' "$next" >&2; exit 1; }
mkdir -p "$next/web"
for runtime_file in platform-api alert-evaluator alert-stream-evaluator alert-notification-dispatcher platform-migrate oneos-scope-sync reconciliation-evaluator alert-benchmark open-platform-stat; do
for runtime_file in platform-api customer-hydrogen-worker alert-evaluator alert-stream-evaluator alert-notification-dispatcher platform-migrate oneos-scope-sync reconciliation-evaluator alert-benchmark open-platform-stat; do
if test -f "$old/$runtime_file"; then
cp "$old/$runtime_file" "$next/$runtime_file"
fi
@@ -148,7 +148,7 @@ cp "$SCRIPT_DIR/verify-customer-demo.py" "$next/deploy/verify-customer-demo.py"
cp "$SCRIPT_DIR/import-source-providers.py" "$next/deploy/import-source-providers.py"
cp "$0" "$next/deploy/install-web-release.sh"
chmod +x "$next/platform-api" "$next/deploy/"*.sh "$next/deploy/prune-release-history.py" "$next/deploy/verify-customer-demo.py" "$next/deploy/import-source-providers.py"
for runtime_file in alert-evaluator alert-stream-evaluator alert-notification-dispatcher platform-migrate oneos-scope-sync reconciliation-evaluator alert-benchmark open-platform-stat; do
for runtime_file in customer-hydrogen-worker alert-evaluator alert-stream-evaluator alert-notification-dispatcher platform-migrate oneos-scope-sync reconciliation-evaluator alert-benchmark open-platform-stat; do
test ! -f "$next/$runtime_file" || chmod +x "$next/$runtime_file"
done
@@ -0,0 +1,13 @@
-- Profile-jump identities are intentionally separate from legacy HMAC identities.
-- No username-based backfill. Existing-account binding requires reviewed explicit IDs.
CREATE TABLE IF NOT EXISTS platform_external_identity (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
issuer VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
tenant_id VARCHAR(96) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
external_user_id VARCHAR(96) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE KEY uq_external_identity (issuer,tenant_id,external_user_id),
UNIQUE KEY uq_external_platform_user (user_id),
CONSTRAINT fk_external_identity_user FOREIGN KEY(user_id) REFERENCES platform_user(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -0,0 +1,54 @@
-- External reported mileage is isolated from telemetry-derived mileage.
CREATE TABLE IF NOT EXISTS gzev_mileage_day (
account VARCHAR(64) NOT NULL,
stat_date DATE NOT NULL,
row_count INT NOT NULL,
missing_count INT NOT NULL,
last_success_at DATETIME(3) NOT NULL,
snapshot_hash CHAR(64) NOT NULL,
PRIMARY KEY(account,stat_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS gzev_mileage_snapshot (
account VARCHAR(64) NOT NULL,
stat_date DATE NOT NULL,
snapshot_hash CHAR(64) NOT NULL,
fetched_at DATETIME(3) NOT NULL,
raw_pages JSON NOT NULL,
PRIMARY KEY(account,stat_date,snapshot_hash)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS gzev_daily_mileage (
account VARCHAR(64) NOT NULL,
vin VARCHAR(17) NOT NULL,
stat_date DATE NOT NULL,
plate_number VARCHAR(32) NOT NULL,
vehicle_seq VARCHAR(64) NOT NULL,
production_company VARCHAR(255) NOT NULL,
operation_company VARCHAR(255) NOT NULL,
brand VARCHAR(128) NOT NULL,
model VARCHAR(128) NOT NULL,
total_km DECIMAL(14,3) NULL,
hydrogen_km DECIMAL(14,3) NULL,
charge_km DECIMAL(14,3) NULL,
quality_status VARCHAR(24) NOT NULL,
raw_row JSON NOT NULL,
fetched_at DATETIME(3) NOT NULL,
PRIMARY KEY(account,vin,stat_date),
KEY idx_gzev_plate_date(account,plate_number,stat_date),
KEY idx_gzev_date(account,stat_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS gzev_auth_alert (
account VARCHAR(64) NOT NULL PRIMARY KEY,
active BOOLEAN NOT NULL DEFAULT TRUE,
first_failed_at DATETIME(3) NOT NULL,
last_attempt_at DATETIME(3) NULL,
last_sent_at DATETIME(3) NULL,
last_error VARCHAR(255) NOT NULL DEFAULT '',
resolved_at DATETIME(3) NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS gzev_mileage_attempt (
account VARCHAR(64) NOT NULL,
stat_date DATE NOT NULL,
retry_after DATETIME(3) NOT NULL,
PRIMARY KEY(account,stat_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS platform_customer_mileage_rules (
scope_key VARCHAR(160) NOT NULL PRIMARY KEY,
version BIGINT NOT NULL,
rules_json JSON NOT NULL,
updated_by VARCHAR(160) NOT NULL,
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE IF NOT EXISTS platform_customer_vehicle_assignment (
tenant_ref VARCHAR(100) NOT NULL,
vin VARCHAR(32) NOT NULL,
plate VARCHAR(40) NOT NULL DEFAULT '',
customer_name VARCHAR(255) NOT NULL DEFAULT '',
department_name VARCHAR(255) NOT NULL DEFAULT '',
responsible_person VARCHAR(255) NOT NULL DEFAULT '',
observed_at DATETIME(3) NOT NULL,
PRIMARY KEY (tenant_ref, vin),
KEY idx_observed (observed_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS platform_customer_hydrogen_daily (
vin VARCHAR(32) NOT NULL,
stat_date DATE NOT NULL,
protocol VARCHAR(32) NOT NULL,
source_key VARCHAR(256) NOT NULL,
hydrogen_km DECIMAL(18,3) NOT NULL,
observed_km DECIMAL(18,3) NOT NULL,
classified_km DECIMAL(18,3) NOT NULL,
sample_count BIGINT NOT NULL,
known_sample_count BIGINT NOT NULL,
interval_count BIGINT NOT NULL,
rejected_intervals BIGINT NOT NULL,
first_event_time DATETIME(3) NOT NULL,
last_event_time DATETIME(3) NOT NULL,
algorithm_version VARCHAR(64) NOT NULL,
calculated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY(vin,stat_date,protocol,source_key),
KEY idx_date(stat_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS platform_customer_hydrogen_job (
stat_date DATE NOT NULL PRIMARY KEY,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
requested_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
started_at DATETIME(3) NULL,
completed_at DATETIME(3) NULL,
lease_until DATETIME(3) NULL,
lease_owner VARCHAR(64) NOT NULL DEFAULT '',
attempts INT NOT NULL DEFAULT 0,
error_message VARCHAR(500) NOT NULL DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
@@ -0,0 +1,19 @@
[Unit]
Description=Daily pure hydrogen mileage synchronization conclusion via ServerChan
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
WorkingDirectory=/opt/lingniu-gzev/current
EnvironmentFile=/opt/lingniu-go-native/env/base.env
EnvironmentFile=/opt/lingniu-vehicle-platform/env/platform.env
EnvironmentFile=/opt/lingniu-gzev/env/gzev.env
ExecStart=/opt/lingniu-gzev/current/gzev-daily-report
TimeoutStartSec=3min
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
@@ -0,0 +1,12 @@
[Unit]
Description=Report pure hydrogen mileage synchronization daily at 11:00 Beijing time
[Timer]
# Production host timezone is verified as Asia/Shanghai before installation.
OnCalendar=*-*-* 11:00:00
Persistent=true
AccuracySec=1s
Unit=lingniu-gzev-daily-report.service
[Install]
WantedBy=timers.target
@@ -0,0 +1,20 @@
[Unit]
Description=Guangzhou daily vehicle mileage ingestion and resumable backfill
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
WorkingDirectory=/opt/lingniu-gzev/current
EnvironmentFile=/opt/lingniu-go-native/env/base.env
EnvironmentFile=/opt/lingniu-vehicle-platform/env/platform.env
EnvironmentFile=/opt/lingniu-gzev/env/gzev.env
ExecStart=/opt/lingniu-gzev/current/gzev-mileage --mode sync --max-days 90 --lookback 7
TimeoutStartSec=50min
Nice=10
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
@@ -0,0 +1,11 @@
[Unit]
Description=Collect completed Guangzhou mileage days hourly, replay recent days, resume history
[Timer]
OnCalendar=*-*-* *:15:00
Persistent=true
AccuracySec=1min
Unit=lingniu-gzev-mileage.service
[Install]
WantedBy=timers.target
@@ -0,0 +1,21 @@
[Unit]
Description=Guangzhou mileage session upload and validation
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/lingniu-gzev/current
EnvironmentFile=/opt/lingniu-gzev/env/gzev.env
ExecStart=/opt/lingniu-gzev/current/gzev-session-upload
Restart=on-failure
RestartSec=5
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,21 @@
[Unit]
Description=Lingniu customer pure hydrogen mileage worker
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/lingniu-vehicle-platform/current
EnvironmentFile=/opt/lingniu-go-native/env/base.env
EnvironmentFile=/opt/lingniu-vehicle-platform/env/platform.env
Environment=CUSTOMER_HYDROGEN_WORKER=true
Environment=CUSTOMER_HYDROGEN_MAX_JOBS=0
Environment=BACKFILL_PROTOCOLS=GB32960,YUTONG_MQTT
ExecStart=/opt/lingniu-vehicle-platform/current/customer-hydrogen-worker
Restart=always
RestartSec=10
KillSignal=SIGTERM
TimeoutStopSec=45
[Install]
WantedBy=multi-user.target