# 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:

```text
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:

```markdown
# 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`:

```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`:

```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**

```bash
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`:

```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`:

```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`:

```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`:

```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`:

```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`:

```ts
import '@testing-library/jest-dom/vitest';
```

- [ ] **Step 4: Create API module**

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

```go
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:

```bash
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**

```bash
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`:

```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:

```bash
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`:

```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`:

```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:

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

Expected: pass.

- [ ] **Step 6: Commit**

```bash
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`:

```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:

```bash
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`:

```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`:

```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`:

```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:

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

Expected: pass.

- [ ] **Step 7: Commit**

```bash
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`:

```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:

```bash
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`:

```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`:

```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`:

```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`:

```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:

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

Expected: pass.

- [ ] **Step 7: Commit**

```bash
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`:

```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:

```bash
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`:

```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`:

```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`:

```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`:

```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`:

```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`:

```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`:

```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:

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

Expected: pass.

- [ ] **Step 7: Commit**

```bash
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`:

```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`:

```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:

```tsx
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:

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

Expected: pass.

- [ ] **Step 5: Commit**

```bash
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`:

```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:

```bash
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:

```json
{
  "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:

```bash
cd vehicle-data-platform/apps/api && go test ./...
cd ../web && npm run build
```

- [ ] **Step 7: Commit**

```bash
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:

```bash
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:

```bash
cd vehicle-data-platform/apps/api && go test ./...
cd ../web && npm run build
```

- [ ] **Step 6: Commit**

```bash
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:

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

- [ ] **Step 5: Commit**

```bash
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`:

```ini
[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:

~~~markdown
# 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:

```bash
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**

```bash
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:

```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
```

- [ ] **Step 2: Upload release**

Run:

```bash
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:

```text
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:

```bash
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:

```bash
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:

```text
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:

```bash
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.
