Files
lingniu-vehicle-ingest/docs/superpowers/plans/2026-07-03-vehicle-data-platform-implementation.md

47 KiB

Vehicle Data Platform Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build and deploy a new vehicle-data-platform project with a Go BFF backend and a React + TypeScript + Semi UI frontend for vehicle data management.

Architecture: The platform is a new project folder under the existing repository. The Go API acts as a BFF that reads MySQL, Redis, TDengine, and local ops endpoints, then serves unified /api/* responses plus the built frontend. The frontend is a Semi UI console with dashboard, vehicles, realtime, vehicle detail, history, mileage, and quality/ops pages.

Tech Stack: Go, React, TypeScript, Vite, Semi UI, ECharts, MySQL, Redis, TDengine, systemd.


File Structure

Create this structure:

vehicle-data-platform/
  apps/
    api/
      go.mod
      cmd/platform-api/main.go
      internal/app/server.go
      internal/app/routes.go
      internal/config/config.go
      internal/httpx/response.go
      internal/httpx/response_test.go
      internal/platform/model.go
      internal/platform/mock_store.go
      internal/platform/service.go
      internal/platform/service_test.go
      internal/platform/handler.go
      internal/platform/handler_test.go
      internal/static/static.go
    web/
      package.json
      index.html
      tsconfig.json
      vite.config.ts
      src/main.tsx
      src/App.tsx
      src/styles/tokens.css
      src/styles/global.css
      src/api/client.ts
      src/api/types.ts
      src/layout/AppShell.tsx
      src/components/StatusTag.tsx
      src/components/PageHeader.tsx
      src/components/DataEmpty.tsx
      src/pages/Dashboard.tsx
      src/pages/Vehicles.tsx
      src/pages/Realtime.tsx
      src/pages/VehicleDetail.tsx
      src/pages/History.tsx
      src/pages/Mileage.tsx
      src/pages/Quality.tsx
      src/test/App.test.tsx
      src/test/setup.ts
  deploy/
    systemd/lingniu-vehicle-platform.service
  docs/
    product-spec.md
    api-contract.md
    deployment.md
  package.json

Do not modify the existing Go gateway services unless a task explicitly says so.

Task 1: Visual Concept And Frontend Design System

Files:

  • Create: vehicle-data-platform/docs/product-spec.md

  • Create: vehicle-data-platform/apps/web/src/styles/tokens.css

  • Create: vehicle-data-platform/apps/web/src/styles/global.css

  • Step 1: Write the product UI brief

Create vehicle-data-platform/docs/product-spec.md with:

# Vehicle Data Platform Product Spec

## Visual Direction

The UI is a professional vehicle data operations console. It uses Semi UI as the component base, a restrained gray-white control-room palette, dense but readable tables, right-side drawers for detail, and blue-cyan accents for primary actions. It should feel closer to a cloud data console than a marketing dashboard.

## Primary Screens

1. Dashboard: KPI summary, protocol distribution, vehicle map preview, quality issues, and link health.
2. Vehicles: table-first identity and vehicle registry.
3. Realtime: table/map switch for current vehicle state.
4. Vehicle Detail: identity header and tabs for latest state, history, raw, mileage, and quality.
5. History: location and raw-frame query workspace.
6. Mileage: daily and range mileage analysis.
7. Quality: data quality and link health workspace.

## Interaction Rules

- Tables are the default data surface.
- Filters stay above the table.
- Details open in a right drawer unless a full route is needed.
- Error and empty states must be explicit.
- No decorative hero, no oversized marketing card layout, no dark large-screen style.
  • Step 2: Create design tokens

Create vehicle-data-platform/apps/web/src/styles/tokens.css:

:root {
  --vp-bg: #f5f7fb;
  --vp-surface: #ffffff;
  --vp-surface-muted: #f9fafc;
  --vp-border: #e6e9f0;
  --vp-text: #172033;
  --vp-text-muted: #667085;
  --vp-text-subtle: #98a2b3;
  --vp-primary: #1664ff;
  --vp-primary-hover: #0f4fd6;
  --vp-cyan: #00a4b8;
  --vp-success: #12b76a;
  --vp-warning: #f79009;
  --vp-danger: #f04438;
  --vp-radius: 8px;
  --vp-shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
  --vp-shell-sidebar: 232px;
  --vp-shell-header: 56px;
  --vp-page-gutter: 24px;
}
  • Step 3: Create global styling

Create vehicle-data-platform/apps/web/src/styles/global.css:

@import './tokens.css';

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-width: 1280px;
  background: var(--vp-bg);
  color: var(--vp-text);
  font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

.vp-page {
  padding: var(--vp-page-gutter);
}

.vp-section {
  background: var(--vp-surface);
  border: 1px solid var(--vp-border);
  border-radius: var(--vp-radius);
  box-shadow: var(--vp-shadow-sm);
}
  • Step 4: Commit
git add vehicle-data-platform/docs/product-spec.md vehicle-data-platform/apps/web/src/styles/tokens.css vehicle-data-platform/apps/web/src/styles/global.css
git commit -m "docs(platform): define product UI direction"

Task 2: Scaffold Monorepo And Tooling

Files:

  • Create: vehicle-data-platform/package.json

  • Create: vehicle-data-platform/apps/web/package.json

  • Create: vehicle-data-platform/apps/web/index.html

  • Create: vehicle-data-platform/apps/web/tsconfig.json

  • Create: vehicle-data-platform/apps/web/vite.config.ts

  • Create: vehicle-data-platform/apps/web/src/test/setup.ts

  • Create: vehicle-data-platform/apps/api/go.mod

  • Step 1: Create root package file

Create vehicle-data-platform/package.json:

{
  "private": true,
  "scripts": {
    "web:dev": "npm --prefix apps/web run dev",
    "web:build": "npm --prefix apps/web run build",
    "web:test": "npm --prefix apps/web run test",
    "api:test": "cd apps/api && go test ./...",
    "build": "npm run web:build && cd apps/api && go build -o ../../dist/platform-api ./cmd/platform-api"
  }
}
  • Step 2: Create web package

Create vehicle-data-platform/apps/web/package.json:

{
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite --host 0.0.0.0 --port 20301",
    "build": "tsc -b && vite build",
    "test": "vitest run"
  },
  "dependencies": {
    "@douyinfe/semi-icons": "^2.71.0",
    "@douyinfe/semi-ui": "^2.71.0",
    "@vitejs/plugin-react": "^4.3.4",
    "echarts": "^5.6.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "vite": "^6.0.0"
  },
  "devDependencies": {
    "@testing-library/jest-dom": "^6.6.3",
    "@testing-library/react": "^16.1.0",
    "@types/react": "^18.3.18",
    "@types/react-dom": "^18.3.5",
    "typescript": "^5.7.2",
    "vitest": "^2.1.8"
  }
}
  • Step 3: Create Vite entry files

Create vehicle-data-platform/apps/web/index.html:

<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>车辆数据管理中台</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

Create vehicle-data-platform/apps/web/tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["DOM", "DOM.Iterable", "ES2020"],
    "allowJs": false,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": ["src"]
}

Create vehicle-data-platform/apps/web/vite.config.ts:

import react from '@vitejs/plugin-react';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [react()],
  server: {
    proxy: {
      '/api': 'http://127.0.0.1:20300'
    }
  },
  test: {
    environment: 'jsdom',
    setupFiles: './src/test/setup.ts'
  }
});

Create vehicle-data-platform/apps/web/src/test/setup.ts:

import '@testing-library/jest-dom/vitest';
  • Step 4: Create API module

Create vehicle-data-platform/apps/api/go.mod:

module lingniu-vehicle-platform/api

go 1.23

require (
	github.com/go-sql-driver/mysql v1.8.1
	github.com/redis/go-redis/v9 v9.7.0
	github.com/taosdata/driver-go/v3 v3.6.0
)
  • Step 5: Verify scaffolding

Run:

cd vehicle-data-platform/apps/api && go test ./...
cd ../../apps/web && npm install && npm run test

Expected:

  • Go reports no packages or passing packages.

  • Web dependencies install and Vitest reports no tests or passing tests.

  • Step 6: Commit

git add vehicle-data-platform/package.json vehicle-data-platform/apps/web vehicle-data-platform/apps/api/go.mod
git commit -m "chore(platform): scaffold vehicle data platform"

Task 3: Backend Response Envelope And Config

Files:

  • Create: vehicle-data-platform/apps/api/internal/httpx/response.go

  • Create: vehicle-data-platform/apps/api/internal/httpx/response_test.go

  • Create: vehicle-data-platform/apps/api/internal/config/config.go

  • Step 1: Write response tests

Create vehicle-data-platform/apps/api/internal/httpx/response_test.go:

package httpx

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

func TestWriteOKWrapsData(t *testing.T) {
	rec := httptest.NewRecorder()
	WriteOK(rec, "trace-1", map[string]any{"online": 12})
	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d", rec.Code)
	}
	var body map[string]any
	if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
		t.Fatal(err)
	}
	if body["traceId"] != "trace-1" {
		t.Fatalf("traceId = %#v", body["traceId"])
	}
	data := body["data"].(map[string]any)
	if data["online"].(float64) != 12 {
		t.Fatalf("data = %#v", data)
	}
}

func TestWriteErrorWrapsError(t *testing.T) {
	rec := httptest.NewRecorder()
	WriteError(rec, http.StatusBadRequest, "BAD_QUERY", "查询失败", "vin required", "trace-2")
	if rec.Code != http.StatusBadRequest {
		t.Fatalf("status = %d", rec.Code)
	}
	if got := rec.Body.String(); !containsAll(got, []string{"BAD_QUERY", "查询失败", "vin required", "trace-2"}) {
		t.Fatalf("body = %s", got)
	}
}

func containsAll(value string, parts []string) bool {
	for _, part := range parts {
		if !strings.Contains(value, part) {
			return false
		}
	}
	return true
}
  • Step 2: Run response tests and see failure

Run:

cd vehicle-data-platform/apps/api
go test ./internal/httpx

Expected: fail because WriteOK and WriteError are undefined.

  • Step 3: Implement response helpers

Create vehicle-data-platform/apps/api/internal/httpx/response.go:

package httpx

import (
	"encoding/json"
	"net/http"
	"time"
)

type Envelope struct {
	Data      any    `json:"data,omitempty"`
	Error     *Error `json:"error,omitempty"`
	TraceID   string `json:"traceId"`
	Timestamp int64  `json:"timestamp"`
}

type Error struct {
	Code    string `json:"code"`
	Message string `json:"message"`
	Detail  string `json:"detail,omitempty"`
}

func WriteOK(w http.ResponseWriter, traceID string, data any) {
	writeJSON(w, http.StatusOK, Envelope{Data: data, TraceID: traceID, Timestamp: time.Now().UnixMilli()})
}

func WriteError(w http.ResponseWriter, status int, code string, message string, detail string, traceID string) {
	writeJSON(w, status, Envelope{Error: &Error{Code: code, Message: message, Detail: detail}, TraceID: traceID, Timestamp: time.Now().UnixMilli()})
}

func writeJSON(w http.ResponseWriter, status int, value any) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(value)
}
  • Step 4: Implement config

Create vehicle-data-platform/apps/api/internal/config/config.go:

package config

import (
	"os"
	"strconv"
	"strings"
	"time"
)

type Config struct {
	HTTPAddr         string
	MySQLDSN         string
	RedisAddr        string
	RedisUsername    string
	RedisPassword    string
	RedisDB          int
	TDengineDSN      string
	TDengineDatabase string
	CapacityCheckBin string
	AuthToken        string
	RequestTimeout   time.Duration
}

func Load() Config {
	return Config{
		HTTPAddr:         env("HTTP_ADDR", ":20300"),
		MySQLDSN:         env("MYSQL_DSN", ""),
		RedisAddr:        env("REDIS_ADDR", "127.0.0.1:6379"),
		RedisUsername:    env("REDIS_USERNAME", ""),
		RedisPassword:    env("REDIS_PASSWORD", ""),
		RedisDB:          envInt("REDIS_DB", 0),
		TDengineDSN:      env("TDENGINE_DSN", ""),
		TDengineDatabase: env("TDENGINE_DATABASE", "lingniu_vehicle_ts"),
		CapacityCheckBin: env("CAPACITY_CHECK_BIN", "/opt/lingniu-go-native/current/capacity-check"),
		AuthToken:        env("AUTH_TOKEN", ""),
		RequestTimeout:   time.Duration(envInt("REQUEST_TIMEOUT_MS", 5000)) * time.Millisecond,
	}
}

func env(key string, fallback string) string {
	value := strings.TrimSpace(os.Getenv(key))
	if value == "" {
		return fallback
	}
	return value
}

func envInt(key string, fallback int) int {
	value := strings.TrimSpace(os.Getenv(key))
	if value == "" {
		return fallback
	}
	parsed, err := strconv.Atoi(value)
	if err != nil {
		return fallback
	}
	return parsed
}
  • Step 5: Run tests

Run:

cd vehicle-data-platform/apps/api
go test ./...

Expected: pass.

  • Step 6: Commit
git add vehicle-data-platform/apps/api/internal/httpx vehicle-data-platform/apps/api/internal/config
git commit -m "feat(platform-api): add response envelope and config"

Task 4: Backend Platform Service With Mock Store

Files:

  • Create: vehicle-data-platform/apps/api/internal/platform/model.go

  • Create: vehicle-data-platform/apps/api/internal/platform/mock_store.go

  • Create: vehicle-data-platform/apps/api/internal/platform/service.go

  • Create: vehicle-data-platform/apps/api/internal/platform/service_test.go

  • Step 1: Write service tests

Create vehicle-data-platform/apps/api/internal/platform/service_test.go:

package platform

import (
	"context"
	"testing"
)

func TestServiceDashboardSummary(t *testing.T) {
	service := NewService(NewMockStore())
	summary, err := service.DashboardSummary(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	if summary.OnlineVehicles == 0 {
		t.Fatalf("online vehicles should be positive: %#v", summary)
	}
	if len(summary.Protocols) == 0 {
		t.Fatalf("protocols missing: %#v", summary)
	}
}

func TestServiceVehicleListPaginates(t *testing.T) {
	service := NewService(NewMockStore())
	page, err := service.ListVehicles(context.Background(), VehicleQuery{Limit: 1, Offset: 0})
	if err != nil {
		t.Fatal(err)
	}
	if len(page.Items) != 1 || page.Total < 1 {
		t.Fatalf("page = %#v", page)
	}
}
  • Step 2: Run service tests and see failure

Run:

cd vehicle-data-platform/apps/api
go test ./internal/platform

Expected: fail because package types are undefined.

  • Step 3: Implement models

Create vehicle-data-platform/apps/api/internal/platform/model.go:

package platform

type Page[T any] struct {
	Items  []T   `json:"items"`
	Total  int64 `json:"total"`
	Limit  int   `json:"limit"`
	Offset int   `json:"offset"`
}

type ProtocolStat struct {
	Protocol string `json:"protocol"`
	Online   int64  `json:"online"`
	Total    int64  `json:"total"`
}

type DashboardSummary struct {
	OnlineVehicles int64          `json:"onlineVehicles"`
	ActiveToday    int64          `json:"activeToday"`
	FrameToday     int64          `json:"frameToday"`
	IssueVehicles  int64          `json:"issueVehicles"`
	KafkaLag       int64          `json:"kafkaLag"`
	Protocols      []ProtocolStat `json:"protocols"`
	LinkHealth     []LinkHealth   `json:"linkHealth"`
}

type LinkHealth struct {
	Name   string `json:"name"`
	Status string `json:"status"`
	Detail string `json:"detail,omitempty"`
}

type VehicleQuery struct {
	Keyword  string
	Protocol string
	Online   string
	Limit    int
	Offset   int
}

type VehicleRow struct {
	VIN          string `json:"vin"`
	Plate        string `json:"plate"`
	Phone        string `json:"phone"`
	OEM          string `json:"oem"`
	Protocol     string `json:"protocol"`
	Online       bool   `json:"online"`
	LastSeen     string `json:"lastSeen"`
	LocationText string `json:"locationText"`
	BindingScore int    `json:"bindingScore"`
}
  • Step 4: Implement mock store

Create vehicle-data-platform/apps/api/internal/platform/mock_store.go:

package platform

import "context"

type Store interface {
	DashboardSummary(context.Context) (DashboardSummary, error)
	ListVehicles(context.Context, VehicleQuery) (Page[VehicleRow], error)
}

type MockStore struct {
	vehicles []VehicleRow
}

func NewMockStore() *MockStore {
	return &MockStore{vehicles: []VehicleRow{
		{VIN: "LB9A32A24R0LS1426", Plate: "粤AG18312", Phone: "13307795425", OEM: "G7s", Protocol: "JT808", Online: true, LastSeen: "2026-07-03 20:00:00", LocationText: "广东 广州", BindingScore: 100},
		{VIN: "LNXNEGRR7SR318212", Plate: "川A00001", Phone: "", OEM: "Hyundai", Protocol: "GB32960", Online: true, LastSeen: "2026-07-03 20:00:03", LocationText: "四川 成都", BindingScore: 80},
	}}
}

func (s *MockStore) DashboardSummary(context.Context) (DashboardSummary, error) {
	return DashboardSummary{
		OnlineVehicles: 2,
		ActiveToday:    2,
		FrameToday:     138240,
		IssueVehicles:  1,
		KafkaLag:       0,
		Protocols: []ProtocolStat{
			{Protocol: "GB32960", Online: 1, Total: 1},
			{Protocol: "JT808", Online: 1, Total: 1},
			{Protocol: "YUTONG_MQTT", Online: 0, Total: 0},
		},
		LinkHealth: []LinkHealth{
			{Name: "Gateway", Status: "ok"},
			{Name: "NATS", Status: "ok"},
			{Name: "Kafka", Status: "ok"},
			{Name: "TDengine", Status: "ok"},
			{Name: "Redis", Status: "ok"},
			{Name: "MySQL", Status: "ok"},
		},
	}, nil
}

func (s *MockStore) ListVehicles(_ context.Context, query VehicleQuery) (Page[VehicleRow], error) {
	limit := query.Limit
	if limit <= 0 {
		limit = 20
	}
	offset := query.Offset
	if offset < 0 {
		offset = 0
	}
	end := offset + limit
	if end > len(s.vehicles) {
		end = len(s.vehicles)
	}
	items := []VehicleRow{}
	if offset < len(s.vehicles) {
		items = s.vehicles[offset:end]
	}
	return Page[VehicleRow]{Items: items, Total: int64(len(s.vehicles)), Limit: limit, Offset: offset}, nil
}
  • Step 5: Implement service

Create vehicle-data-platform/apps/api/internal/platform/service.go:

package platform

import "context"

type Service struct {
	store Store
}

func NewService(store Store) *Service {
	if store == nil {
		store = NewMockStore()
	}
	return &Service{store: store}
}

func (s *Service) DashboardSummary(ctx context.Context) (DashboardSummary, error) {
	return s.store.DashboardSummary(ctx)
}

func (s *Service) ListVehicles(ctx context.Context, query VehicleQuery) (Page[VehicleRow], error) {
	if query.Limit <= 0 {
		query.Limit = 20
	}
	if query.Limit > 200 {
		query.Limit = 200
	}
	if query.Offset < 0 {
		query.Offset = 0
	}
	return s.store.ListVehicles(ctx, query)
}
  • Step 6: Run tests

Run:

cd vehicle-data-platform/apps/api
go test ./internal/platform

Expected: pass.

  • Step 7: Commit
git add vehicle-data-platform/apps/api/internal/platform
git commit -m "feat(platform-api): add platform service model"

Task 5: Backend HTTP Routes And Static Serving

Files:

  • Create: vehicle-data-platform/apps/api/internal/platform/handler.go

  • Create: vehicle-data-platform/apps/api/internal/platform/handler_test.go

  • Create: vehicle-data-platform/apps/api/internal/app/server.go

  • Create: vehicle-data-platform/apps/api/internal/app/routes.go

  • Create: vehicle-data-platform/apps/api/cmd/platform-api/main.go

  • Step 1: Write handler tests

Create vehicle-data-platform/apps/api/internal/platform/handler_test.go:

package platform

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

func TestHandlerDashboardSummary(t *testing.T) {
	handler := NewHandler(NewService(NewMockStore()))
	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, "/api/dashboard/summary", nil)
	handler.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
	}
	for _, want := range []string{"onlineVehicles", "protocols", "linkHealth"} {
		if !strings.Contains(rec.Body.String(), want) {
			t.Fatalf("response missing %s: %s", want, rec.Body.String())
		}
	}
}

func TestHandlerVehicleList(t *testing.T) {
	handler := NewHandler(NewService(NewMockStore()))
	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, "/api/vehicles?limit=1", nil)
	handler.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
	}
	for _, want := range []string{"LB9A32A24R0LS1426", "items", "total"} {
		if !strings.Contains(rec.Body.String(), want) {
			t.Fatalf("response missing %s: %s", want, rec.Body.String())
		}
	}
}
  • Step 2: Run handler tests and see failure

Run:

cd vehicle-data-platform/apps/api
go test ./internal/platform -run Handler

Expected: fail because NewHandler is undefined.

  • Step 3: Implement handler

Create vehicle-data-platform/apps/api/internal/platform/handler.go:

package platform

import (
	"net/http"
	"strconv"
	"strings"

	"lingniu-vehicle-platform/api/internal/httpx"
)

type Handler struct {
	service *Service
}

func NewHandler(service *Service) *Handler {
	if service == nil {
		service = NewService(NewMockStore())
	}
	return &Handler{service: service}
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	traceID := r.Header.Get("X-Trace-Id")
	if traceID == "" {
		traceID = "local"
	}
	path := strings.Trim(r.URL.Path, "/")
	switch {
	case r.Method == http.MethodGet && path == "api/dashboard/summary":
		data, err := h.service.DashboardSummary(r.Context())
		if err != nil {
			httpx.WriteError(w, http.StatusInternalServerError, "QUERY_FAILED", "查询失败", err.Error(), traceID)
			return
		}
		httpx.WriteOK(w, traceID, data)
	case r.Method == http.MethodGet && path == "api/vehicles":
		limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
		offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
		data, err := h.service.ListVehicles(r.Context(), VehicleQuery{
			Keyword:  r.URL.Query().Get("keyword"),
			Protocol: r.URL.Query().Get("protocol"),
			Online:   r.URL.Query().Get("online"),
			Limit:    limit,
			Offset:   offset,
		})
		if err != nil {
			httpx.WriteError(w, http.StatusInternalServerError, "QUERY_FAILED", "查询失败", err.Error(), traceID)
			return
		}
		httpx.WriteOK(w, traceID, data)
	default:
		httpx.WriteError(w, http.StatusNotFound, "NOT_FOUND", "接口不存在", path, traceID)
	}
}
  • Step 4: Implement app routes

Create vehicle-data-platform/apps/api/internal/app/routes.go:

package app

import (
	"net/http"

	"lingniu-vehicle-platform/api/internal/platform"
)

func NewMux(service *platform.Service, static http.Handler) *http.ServeMux {
	mux := http.NewServeMux()
	api := platform.NewHandler(service)
	mux.Handle("/api/", api)
	if static != nil {
		mux.Handle("/", static)
	}
	return mux
}

Create vehicle-data-platform/apps/api/internal/app/server.go:

package app

import (
	"net/http"
	"time"
)

func NewServer(addr string, handler http.Handler) *http.Server {
	return &http.Server{
		Addr:              addr,
		Handler:           handler,
		ReadHeaderTimeout: 5 * time.Second,
	}
}
  • Step 5: Implement main

Create vehicle-data-platform/apps/api/cmd/platform-api/main.go:

package main

import (
	"log"
	"net/http"
	"os"

	"lingniu-vehicle-platform/api/internal/app"
	"lingniu-vehicle-platform/api/internal/config"
	"lingniu-vehicle-platform/api/internal/platform"
)

func main() {
	cfg := config.Load()
	service := platform.NewService(platform.NewMockStore())
	static := http.FileServer(http.Dir(os.Getenv("STATIC_DIR")))
	server := app.NewServer(cfg.HTTPAddr, app.NewMux(service, static))
	log.Printf("vehicle platform api listening on %s", cfg.HTTPAddr)
	if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		log.Fatal(err)
	}
}
  • Step 6: Run tests and build

Run:

cd vehicle-data-platform/apps/api
go test ./...
go build ./cmd/platform-api

Expected: pass.

  • Step 7: Commit
git add vehicle-data-platform/apps/api
git commit -m "feat(platform-api): expose dashboard and vehicle APIs"

Task 6: Frontend App Shell With Semi UI

Files:

  • Create: vehicle-data-platform/apps/web/src/main.tsx

  • Create: vehicle-data-platform/apps/web/src/App.tsx

  • Create: vehicle-data-platform/apps/web/src/layout/AppShell.tsx

  • Create: vehicle-data-platform/apps/web/src/components/PageHeader.tsx

  • Create: vehicle-data-platform/apps/web/src/components/StatusTag.tsx

  • Create: vehicle-data-platform/apps/web/src/components/DataEmpty.tsx

  • Create: vehicle-data-platform/apps/web/src/test/App.test.tsx

  • Step 1: Write app shell test

Create vehicle-data-platform/apps/web/src/test/App.test.tsx:

import { render, screen } from '@testing-library/react';
import App from '../App';

test('renders vehicle data platform navigation', () => {
  render(<App />);
  expect(screen.getByText('车辆数据管理中台')).toBeInTheDocument();
  expect(screen.getByText('总览工作台')).toBeInTheDocument();
  expect(screen.getByText('车辆台账')).toBeInTheDocument();
  expect(screen.getByText('实时监控')).toBeInTheDocument();
});
  • Step 2: Run test and see failure

Run:

cd vehicle-data-platform/apps/web
npm run test

Expected: fail because App does not exist.

  • Step 3: Implement entry and shell

Create vehicle-data-platform/apps/web/src/main.tsx:

import React from 'react';
import ReactDOM from 'react-dom/client';
import '@douyinfe/semi-ui/dist/css/semi.min.css';
import './styles/global.css';
import App from './App';

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Create vehicle-data-platform/apps/web/src/App.tsx:

import { useState } from 'react';
import { Dashboard } from './pages/Dashboard';
import { Vehicles } from './pages/Vehicles';
import { Realtime } from './pages/Realtime';
import { History } from './pages/History';
import { Mileage } from './pages/Mileage';
import { Quality } from './pages/Quality';
import { VehicleDetail } from './pages/VehicleDetail';
import { AppShell, PageKey } from './layout/AppShell';

export default function App() {
  const [page, setPage] = useState<PageKey>('dashboard');
  return (
    <AppShell active={page} onChange={setPage}>
      {page === 'dashboard' && <Dashboard />}
      {page === 'vehicles' && <Vehicles />}
      {page === 'realtime' && <Realtime />}
      {page === 'detail' && <VehicleDetail />}
      {page === 'history' && <History />}
      {page === 'mileage' && <Mileage />}
      {page === 'quality' && <Quality />}
    </AppShell>
  );
}

Create vehicle-data-platform/apps/web/src/layout/AppShell.tsx:

import { Layout, Nav, Typography } from '@douyinfe/semi-ui';
import { IconHome, IconMapPin, IconServer, IconSetting, IconHistogram, IconSearch, IconList } from '@douyinfe/semi-icons';
import type { ReactNode } from 'react';

export type PageKey = 'dashboard' | 'vehicles' | 'realtime' | 'detail' | 'history' | 'mileage' | 'quality';

const items = [
  { itemKey: 'dashboard', text: '总览工作台', icon: <IconHome /> },
  { itemKey: 'vehicles', text: '车辆台账', icon: <IconList /> },
  { itemKey: 'realtime', text: '实时监控', icon: <IconMapPin /> },
  { itemKey: 'detail', text: '车辆详情', icon: <IconSearch /> },
  { itemKey: 'history', text: '历史数据', icon: <IconServer /> },
  { itemKey: 'mileage', text: '里程与统计', icon: <IconHistogram /> },
  { itemKey: 'quality', text: '数据质量与链路健康', icon: <IconSetting /> }
];

export function AppShell({ active, onChange, children }: { active: PageKey; onChange: (key: PageKey) => void; children: ReactNode }) {
  return (
    <Layout style={{ minHeight: '100vh' }}>
      <Layout.Sider style={{ width: 'var(--vp-shell-sidebar)', background: '#fff', borderRight: '1px solid var(--vp-border)' }}>
        <div style={{ height: 56, display: 'flex', alignItems: 'center', padding: '0 20px', borderBottom: '1px solid var(--vp-border)' }}>
          <Typography.Text strong>车辆数据管理中台</Typography.Text>
        </div>
        <Nav selectedKeys={[active]} items={items} onSelect={({ itemKey }) => onChange(itemKey as PageKey)} />
      </Layout.Sider>
      <Layout>
        <Layout.Header style={{ height: 56, background: '#fff', borderBottom: '1px solid var(--vp-border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '0 24px' }}>
          <Typography.Text type="tertiary">生产环境 · 自动刷新 60s</Typography.Text>
          <Typography.Text type="tertiary">ECS 115.29.187.205</Typography.Text>
        </Layout.Header>
        <Layout.Content>{children}</Layout.Content>
      </Layout>
    </Layout>
  );
}
  • Step 4: Implement shared components

Create vehicle-data-platform/apps/web/src/components/PageHeader.tsx:

import { Typography } from '@douyinfe/semi-ui';

export function PageHeader({ title, description }: { title: string; description: string }) {
  return (
    <div style={{ marginBottom: 16 }}>
      <Typography.Title heading={3} style={{ margin: 0 }}>{title}</Typography.Title>
      <Typography.Text type="tertiary">{description}</Typography.Text>
    </div>
  );
}

Create vehicle-data-platform/apps/web/src/components/StatusTag.tsx:

import { Tag } from '@douyinfe/semi-ui';

export function StatusTag({ status }: { status: 'ok' | 'warning' | 'error' | 'offline' }) {
  const map = {
    ok: { color: 'green' as const, text: '正常' },
    warning: { color: 'orange' as const, text: '关注' },
    error: { color: 'red' as const, text: '异常' },
    offline: { color: 'grey' as const, text: '离线' }
  };
  return <Tag color={map[status].color}>{map[status].text}</Tag>;
}

Create vehicle-data-platform/apps/web/src/components/DataEmpty.tsx:

import { Empty } from '@douyinfe/semi-ui';

export function DataEmpty({ text = '暂无数据' }: { text?: string }) {
  return <Empty description={text} style={{ padding: 48 }} />;
}
  • Step 5: Create first-pass page components

Create vehicle-data-platform/apps/web/src/pages/Dashboard.tsx:

import { PageHeader } from '../components/PageHeader';

export function Dashboard() {
  return (
    <div className="vp-page">
      <PageHeader title="总览工作台" description="车辆在线、协议分布、数据质量和链路健康的统一入口" />
      <div className="vp-section" style={{ padding: 24 }}>加载总览数据...</div>
    </div>
  );
}

Create the remaining page files with the exact title and description below. Each file imports PageHeader, exports a function named after the file, wraps content in <div className="vp-page">, and renders one <div className="vp-section" style={{ padding: 24 }}> with the listed loading text.

File Export Title Description Loading text
Vehicles.tsx Vehicles 车辆台账 车辆身份、协议绑定、车牌、手机号和 OEM 的运营台账 加载车辆台账...
Realtime.tsx Realtime 实时状态 按协议和车辆查看最新实时位置、在线状态和核心数据 加载实时状态...
VehicleDetail.tsx VehicleDetail 车辆详情 单车身份、实时、历史、RAW、里程和质量的综合视图 加载车辆详情...
History.tsx History 历史查询 位置历史和 RAW 帧历史的分页查询工作台 加载历史查询...
Mileage.tsx Mileage 里程分析 每日里程、区间里程和异常差值分析 加载里程分析...
Quality.tsx Quality 数据质量 断链、VIN 缺失、字段缺失和链路健康的排查入口 加载数据质量...
  • Step 6: Run tests and build

Run:

cd vehicle-data-platform/apps/web
npm run test
npm run build

Expected: pass.

  • Step 7: Commit
git add vehicle-data-platform/apps/web/src
git commit -m "feat(platform-web): add Semi UI app shell"

Task 7: Frontend API Client And Dashboard

Files:

  • Create: vehicle-data-platform/apps/web/src/api/types.ts

  • Create: vehicle-data-platform/apps/web/src/api/client.ts

  • Modify: vehicle-data-platform/apps/web/src/pages/Dashboard.tsx

  • Step 1: Create API types

Create vehicle-data-platform/apps/web/src/api/types.ts:

export interface ApiEnvelope<T> {
  data: T;
  traceId: string;
  timestamp: number;
}

export interface ProtocolStat {
  protocol: string;
  online: number;
  total: number;
}

export interface LinkHealth {
  name: string;
  status: string;
  detail?: string;
}

export interface DashboardSummary {
  onlineVehicles: number;
  activeToday: number;
  frameToday: number;
  issueVehicles: number;
  kafkaLag: number;
  protocols: ProtocolStat[];
  linkHealth: LinkHealth[];
}

export interface VehicleRow {
  vin: string;
  plate: string;
  phone: string;
  oem: string;
  protocol: string;
  online: boolean;
  lastSeen: string;
  locationText: string;
  bindingScore: number;
}

export interface Page<T> {
  items: T[];
  total: number;
  limit: number;
  offset: number;
}
  • Step 2: Create API client

Create vehicle-data-platform/apps/web/src/api/client.ts:

import type { ApiEnvelope, DashboardSummary, Page, VehicleRow } from './types';

async function request<T>(path: string): Promise<T> {
  const response = await fetch(path);
  if (!response.ok) {
    throw new Error(`request failed ${response.status}`);
  }
  const envelope = (await response.json()) as ApiEnvelope<T>;
  return envelope.data;
}

export const api = {
  dashboardSummary: () => request<DashboardSummary>('/api/dashboard/summary'),
  vehicles: (params: URLSearchParams) => request<Page<VehicleRow>>(`/api/vehicles?${params.toString()}`)
};
  • Step 3: Update dashboard

Replace Dashboard.tsx with a complete data-bound page. It must import useEffect, useState, Semi UI Card, Col, Row, Spin, Table, Tag, and Toast, plus api, DashboardSummary, and PageHeader. The component state and loading lifecycle must be:

const [summary, setSummary] = useState<DashboardSummary | null>(null);
const [loading, setLoading] = useState(true);

useEffect(() => {
  api.dashboardSummary()
    .then(setSummary)
    .catch(error => Toast.error(error.message))
    .finally(() => setLoading(false));
}, []);

Render inside <Spin spinning={loading}>:

  • Four KPI cards: online vehicles, active today, frames today, issue vehicles.

  • A protocol table with columns protocol, online, total.

  • A link-health table with columns name, status, detail; status renders Tag with green for ok, orange for warning, red for error, and grey for other values.

  • A compact Kafka lag card showing summary.kafkaLag.

  • Step 4: Run frontend build

Run:

cd vehicle-data-platform/apps/web
npm run build

Expected: pass.

  • Step 5: Commit
git add vehicle-data-platform/apps/web/src/api vehicle-data-platform/apps/web/src/pages/Dashboard.tsx
git commit -m "feat(platform-web): connect dashboard summary"

Task 8: Vehicles And Realtime Pages

Files:

  • Modify: vehicle-data-platform/apps/web/src/pages/Vehicles.tsx

  • Modify: vehicle-data-platform/apps/web/src/pages/Realtime.tsx

  • Modify: vehicle-data-platform/apps/api/internal/platform/model.go

  • Modify: vehicle-data-platform/apps/api/internal/platform/handler.go

  • Step 1: Write backend test for realtime location route

Add to handler_test.go:

func TestHandlerRealtimeLocations(t *testing.T) {
	handler := NewHandler(NewService(NewMockStore()))
	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, "/api/realtime/locations?limit=10", nil)
	handler.ServeHTTP(rec, req)
	if rec.Code != http.StatusOK {
		t.Fatalf("status = %d body=%s", rec.Code, rec.Body.String())
	}
	if !strings.Contains(rec.Body.String(), "LB9A32A24R0LS1426") {
		t.Fatalf("response missing vehicle: %s", rec.Body.String())
	}
}
  • Step 2: Run backend test and see failure

Run:

cd vehicle-data-platform/apps/api
go test ./internal/platform -run RealtimeLocations

Expected: fail with 404.

  • Step 3: Implement realtime location models and route

Add RealtimeLocationRow to model.go and expose GET /api/realtime/locations using mock data derived from vehicles.

Expected JSON fields:

{
  "vin": "LB9A32A24R0LS1426",
  "plate": "粤AG18312",
  "protocol": "JT808",
  "longitude": 113.2644,
  "latitude": 23.1291,
  "speedKmh": 42.5,
  "socPercent": 78.4,
  "totalMileageKm": 119925,
  "lastSeen": "2026-07-03 20:00:00"
}
  • Step 4: Implement Vehicles page

Use Semi UI Form, Input, Select, Button, Table, Drawer, and Tag. The table must show VIN, plate, phone, OEM, protocol, online status, lastSeen, locationText, and bindingScore.

  • Step 5: Implement Realtime page

Use two tabs:

  • 表格视图 with realtime location table.
  • 地图视图 with a professional schematic map panel that plots rows as simple positioned dots inside a bounded panel.

Do not use a full-screen decorative map.

  • Step 6: Verify

Run:

cd vehicle-data-platform/apps/api && go test ./...
cd ../web && npm run build
  • Step 7: Commit
git add vehicle-data-platform/apps/api vehicle-data-platform/apps/web/src/pages/Vehicles.tsx vehicle-data-platform/apps/web/src/pages/Realtime.tsx
git commit -m "feat(platform): add vehicles and realtime pages"

Task 9: History, Mileage, Quality, And Vehicle Detail Pages

Files:

  • Modify: vehicle-data-platform/apps/api/internal/platform/model.go

  • Modify: vehicle-data-platform/apps/api/internal/platform/mock_store.go

  • Modify: vehicle-data-platform/apps/api/internal/platform/service.go

  • Modify: vehicle-data-platform/apps/api/internal/platform/handler.go

  • Modify: vehicle-data-platform/apps/web/src/pages/VehicleDetail.tsx

  • Modify: vehicle-data-platform/apps/web/src/pages/History.tsx

  • Modify: vehicle-data-platform/apps/web/src/pages/Mileage.tsx

  • Modify: vehicle-data-platform/apps/web/src/pages/Quality.tsx

  • Step 1: Add backend tests

Add handler tests for:

  • GET /api/history/locations
  • POST /api/history/raw-frames/query
  • GET /api/mileage/daily
  • GET /api/quality/issues
  • GET /api/ops/health

Each test should assert HTTP 200 and a domain-specific field, such as rawSizeBytes, dailyMileageKm, issueType, or linkHealth.

  • Step 2: Run backend tests and see failure

Run:

cd vehicle-data-platform/apps/api
go test ./internal/platform -run 'History|Mileage|Quality|Ops'

Expected: fail because routes are missing.

  • Step 3: Implement mock-backed routes

Implement mock-backed service methods and routes for all five endpoints with these concrete response models:

  • HistoryLocationRow: vin, plate, protocol, longitude, latitude, speedKmh, totalMileageKm, deviceTime, serverTime.
  • RawFrameRow: id, vin, protocol, frameType, deviceTime, serverTime, rawSizeBytes, parsedFields.
  • DailyMileageRow: vin, plate, date, startMileageKm, endMileageKm, dailyMileageKm, source.
  • QualityIssueRow: vin, plate, protocol, issueType, severity, lastSeen, detail.
  • OpsHealth: linkHealth, kafkaLag, redisOnlineKeys, tdengineWritable, mysqlWritable.

The mock store returns at least two rows per table so frontend empty and non-empty table states can both be verified by changing query filters.

  • Step 4: Implement pages

Use Semi UI:

  • VehicleDetail: Tabs, Descriptions, Table, CodeHighlight style JSON block.
  • History: filters, Tabs, table, raw detail drawer.
  • Mileage: date range controls, daily mileage table, anomaly tags.
  • Quality: issue summary cards, issue table, ops health table.

Each page must call its matching API endpoint through src/api/client.ts, show a loading state, show DataEmpty when no rows are returned, and render Toast.error(error.message) on request failure.

  • Step 5: Verify

Run:

cd vehicle-data-platform/apps/api && go test ./...
cd ../web && npm run build
  • Step 6: Commit
git add vehicle-data-platform/apps/api vehicle-data-platform/apps/web/src/pages
git commit -m "feat(platform): add history mileage quality workflows"

Task 10: Replace Mock Store With Production Repositories

Files:

  • Create: vehicle-data-platform/apps/api/internal/platform/mysql_store.go

  • Create: vehicle-data-platform/apps/api/internal/platform/redis_store.go

  • Create: vehicle-data-platform/apps/api/internal/platform/tdengine_store.go

  • Modify: vehicle-data-platform/apps/api/cmd/platform-api/main.go

  • Step 1: Write repository query-builder tests

Do not add a SQL mocking dependency. Create pure query-builder functions and unit test SQL strings plus argument order.

Create tests for these query builders:

  • Vehicle list from vehicle_identity_binding plus realtime location.

  • Mileage daily from vehicle_daily_mileage.

  • Raw frame query from TDengine raw_frames.

  • Step 2: Implement production store

Create a ProductionStore that satisfies platform.Store. It should:

  • Use MySQL for vehicle list, realtime snapshot, realtime location, mileage, identity.

  • Use TDengine for raw frames and history locations.

  • Use Redis for realtime raw and online state.

  • Use capacity-check executable or local metrics endpoints for ops health.

  • Step 3: Keep mock fallback

In main.go, if production DSNs are empty, use NewMockStore(). If DSNs are set, use NewProductionStore(...). This makes local UI development possible without production credentials.

  • Step 4: Verify

Run:

cd vehicle-data-platform/apps/api
go test ./...
  • Step 5: Commit
git add vehicle-data-platform/apps/api
git commit -m "feat(platform-api): connect production data stores"

Task 11: Static Serving, Deployment, And Systemd

Files:

  • Create: vehicle-data-platform/deploy/systemd/lingniu-vehicle-platform.service

  • Create: vehicle-data-platform/docs/deployment.md

  • Modify: vehicle-data-platform/package.json

  • Modify: vehicle-data-platform/apps/api/cmd/platform-api/main.go

  • Step 1: Create systemd unit

Create vehicle-data-platform/deploy/systemd/lingniu-vehicle-platform.service:

[Unit]
Description=Lingniu Vehicle Data Platform
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=/opt/lingniu-vehicle-platform/current
EnvironmentFile=/opt/lingniu-vehicle-platform/env/platform.env
ExecStart=/opt/lingniu-vehicle-platform/current/platform-api
Restart=always
RestartSec=3
LimitNOFILE=1048576
KillSignal=SIGTERM
TimeoutStopSec=30

[Install]
WantedBy=multi-user.target
  • Step 2: Write deployment doc

Create vehicle-data-platform/docs/deployment.md with:

# Deployment

## Build

Run from repository root:

```bash
cd vehicle-data-platform
npm install
npm --prefix apps/web install
npm run web:build
cd apps/api
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o ../../dist/platform-api ./cmd/platform-api
```

## ECS Paths

```text
/opt/lingniu-vehicle-platform/current
/opt/lingniu-vehicle-platform/releases
/opt/lingniu-vehicle-platform/env/platform.env
```

## Health

```bash
curl -fsS http://127.0.0.1:20300/api/ops/health
```
  • Step 3: Verify build

Run:

cd vehicle-data-platform
npm run web:build
cd apps/api
go test ./...
go build -o ../../dist/platform-api ./cmd/platform-api
  • Step 4: Commit
git add vehicle-data-platform/deploy vehicle-data-platform/docs vehicle-data-platform/package.json vehicle-data-platform/apps/api/cmd/platform-api/main.go
git commit -m "ops(platform): add deployment unit and docs"

Task 12: ECS Deploy And Browser Verification

Files:

  • Modify only if deployment reveals a concrete defect.

  • Step 1: Build release locally

Run:

cd vehicle-data-platform
npm install
npm --prefix apps/web install
npm run web:build
cd apps/api
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o ../../dist/platform-api ./cmd/platform-api
  • Step 2: Upload release

Run:

release="platform-$(date +%Y%m%d%H%M%S)"
ssh root@115.29.187.205 "mkdir -p /opt/lingniu-vehicle-platform/releases/$release /opt/lingniu-vehicle-platform/env"
scp vehicle-data-platform/dist/platform-api root@115.29.187.205:/opt/lingniu-vehicle-platform/releases/$release/
scp -r vehicle-data-platform/apps/web/dist root@115.29.187.205:/opt/lingniu-vehicle-platform/releases/$release/web
scp vehicle-data-platform/deploy/systemd/lingniu-vehicle-platform.service root@115.29.187.205:/etc/systemd/system/
ssh root@115.29.187.205 "chmod 755 /opt/lingniu-vehicle-platform/releases/$release/platform-api && ln -sfn /opt/lingniu-vehicle-platform/releases/$release /opt/lingniu-vehicle-platform/current"
  • Step 3: Create environment file

Create /opt/lingniu-vehicle-platform/env/platform.env on ECS with production DSNs. Do not commit secrets.

Required keys:

HTTP_ADDR=:20300
STATIC_DIR=/opt/lingniu-vehicle-platform/current/web
MYSQL_DSN=...
REDIS_ADDR=...
REDIS_USERNAME=...
REDIS_PASSWORD=...
REDIS_DB=50
TDENGINE_DSN=...
TDENGINE_DATABASE=lingniu_vehicle_ts
CAPACITY_CHECK_BIN=/opt/lingniu-go-native/current/capacity-check
AUTH_TOKEN=...
  • Step 4: Start service

Run:

ssh root@115.29.187.205 "systemctl daemon-reload && systemctl enable --now lingniu-vehicle-platform.service && systemctl status lingniu-vehicle-platform.service --no-pager"
  • Step 5: Verify APIs

Run:

curl -fsS http://127.0.0.1:20300/api/dashboard/summary
curl -fsS 'http://127.0.0.1:20300/api/vehicles?limit=5'
curl -fsS http://127.0.0.1:20300/api/ops/health

Expected:

  • All return JSON envelopes.

  • Dashboard has onlineVehicles.

  • Vehicles has items.

  • Ops health has link health state.

  • Step 6: Browser verification

Open:

http://115.29.187.205:20300

Verify:

  • Dashboard loads.

  • Vehicles page table loads.

  • Realtime page loads.

  • History page renders query controls.

  • Mileage page renders tables.

  • Quality page renders issue and health sections.

  • Visual style is Semi UI console style: clean, dense, professional, no marketing hero.

  • Step 7: Commit deployment fixes

If deployment required code changes:

git add vehicle-data-platform
git commit -m "fix(platform): stabilize ecs deployment"

Self Review

Spec coverage:

  • New project folder: Task 2 creates vehicle-data-platform.
  • Semi UI frontend: Tasks 1, 2, 6, 7, 8, 9.
  • Go backend: Tasks 2, 3, 4, 5, 10.
  • Dashboard, vehicles, realtime, detail, history, mileage, quality: Tasks 6 through 9.
  • RAW query with POST and field filtering: Tasks 9 and 10.
  • Production data sources: Task 10.
  • ECS deployment and browser testing: Tasks 11 and 12.

Completeness scan:

  • This plan contains concrete files, routes, models, verification commands, and commit points.
  • Production secrets are intentionally described as environment values and must not be committed.

Type consistency:

  • DashboardSummary, VehicleRow, Page<T>, and API envelope names match between backend and frontend tasks.
  • API route names match the design spec.