142 Commits

Author SHA1 Message Date
4583406efa feat(api): 推論期切換解析方式與上傳 label 檔
新增 POST /api/devices/:id/inference/options,讓已燒錄的模型不重燒
即可切換 detection/classification 與更換 label。

KL520 一次只能載一個模型(換模型必須重燒),但解析方式與 label 都
只是後處理與顯示層,不碰 NPU,因此可即時生效。

- JSON: {taskType?, labels?};multipart: taskType + labelFile
- 欄位用指標型別以區分「省略 = 不動」與「labels:[] = 清空」
- label 解析器(internal/labelfile):格式 <index> <名稱>
  略過空行與 # 註解、trim CRLF、名稱含空白只 split 第一個空白、
  index 非整數/負數/重複則整檔拒絕並回報行號、稀疏 index 補空字串
- index 上限 4095、檔案上限 256KB,防記憶體耗盡
- 非法 taskType 一律拒絕(含舊別名 detection),因為 bridge 對無法
  辨識的值是靜默 fallback,忽略會回 200 但用錯解析方式
- 走窄能力介面 InferenceOptionsDriver,不動 DeviceDriver 簽章

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 19:27:16 +08:00
ddd1aae5d1 feat(server): 把 model metadata 傳進推論鏈路
models.json 宣告的 taskType/labels/inputSize 原本在 flash 時被丟棄
(只傳 modelPath),導致 bridge 只能靠檔名猜測模型類型與尺寸。

- FlashOptions 帶 TaskType/Labels/InputWidth/InputHeight
- 抽出 buildLoadModelCommand,四處 load_model 呼叫點(初次 + 三條
  retry 路徑)統一走它,並加測試釘住呼叫點數量與「不得有手寫 payload」
  —— 讓漏改 retry 路徑在結構上不可能發生
- ClassResult 加 ClassIndex(不加 omitempty,index 0 是合法值)

用 FlashOptions struct 而非裸參數,未來擴充欄位不需再動 interface
與所有 test fake。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 19:27:02 +08:00
f14d24bd7b feat(bridge): classification 自適應解析 + 推論期動態切換
新增 classification 推論支援,核心是「不預設模型長相」:

- output shape 自適應:支援 (1,C)/(C,)/(1,C,1,1)/(C,1,1)/(1,1,1,C)
  及任何 squeeze 後為一維的張量;類別數從 shape 動態取得,
  移除原本寫死的 num_classes=1000
- 無法解析時明確報錯(附實際 shape),不靜默回空結果
- logits vs 已 softmax 自動偵測:sum≈1.0 且全非負才跳過 softmax
  (容差 1e-4,實測 float32 softmax 偏差最大僅 ~4e-7)
- label 為純顯示層:有注入用 label、沒有則輸出原始 enum class_N
- input size 三層來源:SDK > 宣告值 > 檔名猜測,log 標示來源
- 新增 handle_set_inference_options:不重載模型即可改解析方式與 label

修正:
- _detect_model_type 改為外部指定優先,解決未知檔名被誤判成
  tiny_yolov3 而回傳空結果的根因
- handle_disconnect/reset 未清 label 狀態,導致換模型後沿用舊 label 表
- load_model 失敗路徑會污染全域 metadata,改為 defer-until-success
- taskType 值域統一為 object_detection(原本回 detection)
- 檔名解析原本只取 width 丟棄 height,非正方形模型會被壓成正方形

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 19:26:51 +08:00
af44a9f9ba feat(frontend): flash 完成工作區入口醒目化 + 影片上限 200→500MB
驗收後兩個 UX 微調:
- flash 載入模型完成後「開啟工作區」入口不明顯、使用者要自己找 → Button
  改 size=lg + ArrowRight 前往圖示 + ring-primary/40 highlight(設計系統
  token、不裸色碼)。保留手動導航(點擊才去、無自動 router.push)、serial
  空 disable + 離線 disable gate 維持
- 影片上傳上限 MAX_VIDEO_BYTES 200→500MB + i18n 兩語系 hint 文案。影片
  upload timeout 本來就是 0(不限、後端決定)→ 500MB 不會撞 timeout

Reviewer 通過(0C/0M/0Mi/1Sug、timeout 獨立驗證、i18n 無殘留 200MB)。
tsc/eslint 0、vitest 39 passed(499/500/501MB 邊界 mock size 無 OOM +
醒目化樣式/行為)、next build 0。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 01:17:11 +08:00
67737334c8 fix(device): GET /api/devices/:id proxy 拿即時 driver status(方案 Y-2,解載入模型 disabled)
問題:連線後裝置詳情頁連線狀態顯示 unknown、載入模型按鈕永遠 disabled。
根因=雲端 GET /api/devices/:id 是純 DB 讀、沒 proxy 到 local agent → DB 只有
tunnel 層 status(online/offline/unknown)、沒有 driver 七態(detected/
connected/...)→ 前端 gate isDriverConnected 永遠 false。(上輪 C1/C5 查證
假設 status 拿得到、沒追到寫入點的漏洞)

修法(方案 Y-2、local agent 零改、gate 零改):
- backend device_driver_status.go(新):driverStatusFetcher 介面 +
  forwarderDriverStatusFetcher(走既有 session.Forwarder proxy)+ envelope 解析
- devices.go devicesGetHandler:讀 DB metadata 後,device 有序號時額外 proxy
  打 local agent GET /api/devices/{serial}(serial 路由對齊 WP-C)拿即時 driver
  status 覆蓋 USBStatus;remoteStatus/tunnel_online 保留(offline banner 不壞)
- graceful fallback(全回 200 不掛 500):無序號/tunnel 離線/不可達/timeout/
  非2xx/success:false/空status/Forwarder未配置 → 保留 DB status + Debug log
- 2s 短 timeout(不拖詳情頁)、serial path url.PathEscape 防禦
- frontend:DeviceHardwareStatus 加 unknown + coerceHardwareStatus(非七值→
  unknown)+ normalizeDevice fallback disconnected→unknown(修誤顯未連接)+
  i18n devices.status.unknown 兩語系(未確認/Unknown)

Reviewer 通過(0C/0M/3Mi/2Sug、Y-2 10/10、端到端追證 gate 放行 + 8 fallback
分支無一掛 500)。backend 8 測試 + frontend 49 passed、build/vet/test 綠、
gitleaks 0。端到端「即時 connected 覆蓋 unknown」需在線 agent+登入實測(單元
測試已覆蓋合併+fallback)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 09:25:26 +08:00
1da385f345 feat(frontend): 裝置詳情頁連線/斷線按鈕 + 兩步式 gate(解載入模型 device not connected)
問題:device 配對後停在 detected(rescan 偵測到硬體但沒 connect driver),
載入模型被 flash service IsConnected() 擋成 400。根因=前端漏了「連線」按鈕
(POC 有、移植時當 local-tool 專屬刪掉、詳情頁沒補回雲端版)。前端誤用
remoteStatus=online(tunnel) 當 flash 門檻,≠ device driver IsConnected()。

修法(方案 A、兩步式):
- device-detail-client.tsx:連線/斷線按鈕 + 三分支 gate(未連線→連線鈕/
  連線中→loading disabled/已連線→斷線鈕+載入模型可用)+ driver status 呈現
- connect 走 serial(對齊 WP-C)、成功後 fetchDevice(UUID) 刷新(C4 混合模型,
  WS 推送因 backend 501 stub 不通故用輪詢)
- 60s connect UX:loading hint「首次連線需載入韌體約1分鐘,請勿關閉頁面」
- FlashDialog gate 收緊 disabled !isOnline → !isOnline || !isDriverConnected
- device-store connect 的 api.post 加 timeoutMs:130_000(全域 30s < connect
  最壞 65s,單點覆寫;> local agent 120s ctx)
- i18n devices.connect.* 13 key 兩語系
- TDD §10.5.1 過時型別修正 + 新增 §10.5.1.1 two-step connect 流程

推論線一併解決:能進 workspace 的裝置必已 connect(原無 IsConnected 檢查
會靜默空跑無框)。

Reviewer 通過(0C/0M/2Mi/3Sug、規格 12/12、gate 三分支全組合走查)。
tsc/eslint 0、vitest 44 passed、next build 0。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 15:30:33 +08:00
fac07c39d0 fix(local-agent): 配對前自動 rescan USB,解序號偵測時序問題
問題:配對時 exchange 撈 GET /api/devices(ListDevices=讀快取、不重新
偵測)。若 agent 啟動時 USB 尚未插入/未偵測到,快取為空 → 配對撈不到
序號 → payload 不帶 devices → 雲端裝置「尚未回報序號」,即便之後插上
USB 也不會自動重偵測。

修法(方案 A、最小侵入):
- NewLocalDeviceLister 改打 POST /api/devices/scan(ScanDevices →
  Manager.Rescan() 重新偵測 USB),配對前強制重掃一次
- localDeviceListTimeout 2s → localDeviceScanTimeout 8s(真 SDK scan
  kp.core.scan_devices 較慢、給餘裕,逾時走 fallback 不卡配對)
- app.go DeviceLister 注入註解同步更新(Reviewer Mi-1)

不變量保留(Reviewer 獨立驗證):
- rescan 失敗/逾時 → lister return nil(不 error)→ payload omit devices
  → 配對照常(序號是加值資訊,不中斷配對)
- Rescan 對序號身分未變的 session 保持連線(serialIdentity 相等 continue)、
  只斷被拔除的 → 不誤斷在線 tunnel/inference session
- Pair 由 lifecycleMu 序列化、Rescan write lock 全量重建 → 重試冪等安全

Reviewer 通過(0C/0M/1Mi/2Sug)。兩 module build/vet/test/race 全綠、
8 個新 tunnel 行為測試 PASS(端到端快取空→rescan→序號進 payload +
逾時 fallback)、gitleaks 0。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 15:02:31 +08:00
fc7e1e0bc1 fix(local-agent): 修 vendor wheels 多版本累積導致 SDK 沒裝進 bundled runtime
根因:Makefile vendor-wheels 用 pip download --dest 只加不清空,跨多次
vendor-sync 累積出同套件多版本(certifi 三版/numpy 二版等共 17 wheel)→
agent bootstrap 的 ensureBundledPython 把目錄所有 .whl 當獨立參數丟 pip
install → 版本衝突 ResolutionImpossible → pip 失敗 → venv 建了但空 →
無 Kneron PLUS SDK → bridge scan 走 pyusb fallback 回假序號 0x00000000 →
裝置「尚未回報序號」+ 載入模型 device not connected。

修法(3 個 vendor-wheels target):
- 開頭 rm -f vendor/wheels/<plat>/*.whl 清空,確保每次 sync 單版本
- 結尾 dedup 驗證(sort|uniq -d 偵測多版本 → exit 1 fail-fast)
- linux 補「無專屬 KneronPLUS wheel 時退回複製 macos py3-none-any」
- .gitignore 補 local-agent/vendor|payload|dist 對稱規則(保留 ffmpeg
  binary + .gitkeep 例外),防 vendor-sync 後 git add -A 撈進 build 產物

決定性 evidence(agent 實際 bundled runtime):清空 runtime → 起剛 build
的 .app → agent 自己 bootstrap 裝 9 個乾淨單版 wheel(修前 17 含重複必失敗)
→ venv 有 kp → GET /api/devices 回真序號 0xB906162C(非 0x00000000)。

Reviewer 通過(0C/0M/4Mi/3Sug)。win/linux 同邏輯已補、未在對應 runner
驗證(已知限制)。follow-up:ensureBundledPython 列舉 wheel 脆弱設計建議
改 find-links 自解析(backend 另案)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 14:21:08 +08:00
51dfd0f2d8 fix(frontend): normalizeDevice serialNumber 加 trim(WP-C Minor #1)
純空白字串(" ")原判 truthy → 會以空白 serial 組出必失敗的請求路由。
改為先 trim 再判定:serial !== "" ? serial : null。順帶消除字面 "0"
被 falsy 誤判為 null 的隱性行為。補純空白 → null 邊界測試。

Reviewer 通過(0C/0M/0Mi)。tsc/eslint 0、18 test 綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 11:40:35 +08:00
26b433eb10 fix(local-agent): review follow-up 小批(TLS Mi-3/4 + WP-0 S-3/4)
- Mi-3: insecure Transport 改用 DefaultTransport.Clone() 只覆寫 TLSConfig
  (保留 proxy/timeout,消除「開 skip 順便改掉 proxy 行為」副作用)
- Mi-4: exchange 200 分支檢查 Success 欄位(避免 200+success:false 落到
  誤導性的 missing session_token;既有回歸測試改用直接斷言防護不減反增)
- S-3: collectLocalDevices 全空 serial 濾掉不送 devices 陣列(payload 對稱)
- S-4: 假序號比對統一用 EqualFold(防未來 bridge 輸出 casing 變化)

Reviewer 通過(0C/0M/1Mi/3Sug)。兩 module build/vet/test + -race 綠、
gitleaks 0、TLS 行為級測試全 PASS。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 11:40:35 +08:00
59c57fa481 feat(device): WP-B repository 接 agents 模型 + exchange 重塑(A' 走向第二階段 Go 層)
- Device struct 加 4 欄(agent_id/agent_local_device_id/registered_at/
  is_representative)+ deviceColumns 13→17 + scanDevice/SaveTx 讀寫新欄
- 新增 internal/agent package(domain + interface + in-memory + PG repo):
  GetOrCreateAgentTx/GetByOwnerTx,advisory lock 序列化同 owner get-or-create
- exchange 重塑:建/復用 agent → representative device(綁 session_tokens、
  serial=NULL)→ loop 建 N 顆真 USB device(R1 完整 N 顆非只第一顆)
- List filter is_representative=false + DeviceListItem 回傳 agent_id/registered_at
- 併入 WP-0/0005 follow-up Minor:Mi#2 lost-update 收斂(tx 內查詢+局部更新)
  / Mi#3 過時註解 / Mi#4 空 serial 回 ErrNotFound / Mi#5 serial 白名單
  ^0x[0-9A-Fa-f]{8}$ + 去重 / S-1 firmware forward-compat / S-2 device Name 衍生

守 ADR-018 A'(一 owner N agents、session_tokens FK 物理不動、不加 owner
unique 為多機器留路)。Reviewer 通過(0C/0M)。5 套件 dbtest 130 全綠
(db 19/device 37/agent 13/api 172/cmd 60)、build/vet/test 綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 11:40:17 +08:00
8369cab85c feat(db): migration 0005 agents 模型(個人設備管理 A' 走向第二階段地基)
- 新增 agents 表 + devices 加 4 欄(agent_id/agent_local_device_id/
  registered_at/is_representative)+ 2 index + 純 SQL data migration
- data migration 採 R-A:agent.id := device.id 決定性推導、冪等、
  soft-deleted device 排除(WHERE deleted_at IS NULL)
- 守住 ADR-018 走向 A':不碰 session_tokens FK、不改現有 partial
  unique index uq_devices_owner_serial_active、對 0001-0003 零破壞
- migrate_0005_db_test.go 6 個 dbtest case(apply/data migration/
  rollback 對稱/re-apply 冪等/session_tokens 零影響/既有 device 讀寫回歸)
- 修 TestMigrate_UpDownUp 編號 gap 假設(0001/2/3/5 無 0004):
  assert.Equal(topVer-1) → assert.Less(downVer, topVer)
- .gitignore 加 .logs/ + **/.logs/(本機執行 log per-branch 不進 git)

Reviewer 通過(0 Critical/0 Major/3 Minor/4 Sug)。全 db package 41
dbtest 130 綠、build/vet/test 綠、gitleaks 0。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 04:05:53 +08:00
72544d00ba docs(adr): ADR-018 §8 addendum — C5 實作偏差回填(路由段維持 UUID)
- §5.5 加補註指向、原文一字未動(ADR 不可變原則)
- §8.1 記錄 C5 偏差證據鏈(fetchDevice 純 DB / API 無 GetBySerial /
  帶 serial deep-link 必 404)與最終實作(路由段 UUID、serial 頁內派生、
  入口 disable 全落實);決策本體 FE-A 與 C1–C4/C6–C8 不變
- 裁定來源:wp-c-frontend-serial-routing-review.md(reviewer 獨立驗證)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 08:39:32 +08:00
12f5bf6c47 feat(frontend): WP-C serial 路由落地(ADR-018 FE-A 混合模型)
- DeviceSummary 加 serialNumber(serial_number/serialNumber 雙 key 容錯、
  缺欄/空字串 → null)
- 五個 proxy 操作識別值換 serial:connect/disconnect、flash POST+進度 WS
  (單一派生點保 room key 一致)、inference WS、camera start/stop+WS、
  media 三 upload
- serial 為空全面 disable + 提示(workspace banner/media 占位/flash-dialog/
  device-card tooltip/device-detail 兩鈕+序號 InfoRow/選擇頁);i18n 兩語系
- UUID 組維持:fetchDevices/fetchDevice/unpair/詳情頁路由
- C5 偏差(路由段維持 UUID):與 FE-A 定案一致、reviewer 獨立驗證成立
  (API 層無 GetBySerial、帶 serial deep-link 必 404);文件回填另派
- evidence:tsc 0 / eslint 0 / next build 15 route / 觸及 11 測試檔 99 passed
- review:通過 0C/0M/3Mi/2Sug(.autoflow/05-implementation/review/
  wp-c-frontend-serial-routing-review.md)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 09:58:52 +08:00
12b1fe3bad fix(local-agent): Rescan 序號身分比對替換 stale session + 鎖外 Disconnect
- serialIdentity() 三態比對(空/假序號 0x00000000 正規化為無身分、對齊
  ADR-018 R2):Rescan 對既存合成 ID 比對身分——同一顆保留 live session、
  換位/拔插以新 info 替換 session,serial 索引重建即正確
- stale drivers 改釋放鎖後 Disconnect(不再阻塞 GetDevice 路由)+
  Disconnect 錯誤改 WARNING log
- 測試 seam:detectFn/newDriverFn 注入 + newSessionDriverLocked 統一建構
  (Start 行為零變動);新 4 支行為級測試(拔除位移/換位 rebind/
  同序號保 session/無身分回歸)
- evidence:build/vet/test 全綠 + -race ok;review 通過 0C/0M/0Mi/4Sug
  (wp0-serial-routing-review.md「Minor #1 修復審查」章節)
- WP-C 阻擋項解除;觀察項:多裝置實測留意 changed identity log
  (偵測順序抖動時解法在 detector 端排序)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 09:44:47 +08:00
b9ee184586 feat(device): WP-0 序號串通 + serial 路由(ADR-018 走向 A' 第一階段)
- local-agent/server:detector 保留 kn_number(parseScanDevices 可測化、
  合成 ID 語意不動)+ DeviceInfo.SerialNumber + Manager serialToLocalID
  反查表 + GetDevice 雙查(sessions 先、serial 後,純加法)
- visiona-agent:pairing exchange 帶本地裝置清單(DeviceLister 失敗不中斷
  配對、timeout 2s、omitempty 舊版相容)
- visionA-backend:exchange 收 devices —— R1 取第一顆可用序號、R2 假序號
  0x00000000 寫 NULL、R4 同 owner 同序號復用既有 device_id(防 23505)、
  pg+mem 兩實作對齊
- 五個 proxy 操作(flash/inference/camera/connect/disconnect)收斂於
  GetDevice 單一入口,serial 路由一處涵蓋
- docs:api-spec.md §2 增補 POST /api/pairing/exchange(schema + R1/R2/R4)
- 測試:行為級四環節鏈 + dbtest 130 實跑 6/6 + DBOn 回歸 4/4;
  三 module build/vet/test 全綠
- review:通過 0C/0M/6Mi/5Sug(.autoflow/05-implementation/review/
  wp0-serial-routing-review.md);Minor #1 Rescan stale session 掛 WP-C 前置

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 09:31:51 +08:00
3d30fdc580 feat(local-agent): TLS skip opt-in for self-signed stage + exchange envelope 對齊
- 新增 VISIONA_INSECURE_SKIP_TLS_VERIFY(DEV/TEST ONLY、須明確 opt-in "true"):
  pairing exchange HTTP client、tunnel WSS dialer、設定頁 TestConnection 三路徑
  共用 TLSConfigForDial(含 ALPN 釘 http/1.1);NewApp 唯一 env 讀取點注入欄位
- exchangeResponse 對齊雲端 /api/pairing/exchange success envelope
  (account/relay_url 選填 fallback 保留、舊頂層格式回歸防護)
- .gitignore:.env.stage* + !.env.stage.example + *.pptx(堵 secrets 誤入)
- start-agent.sh(新增):public 模式 export skip env + 預檢 curl https 帶 -k
- 測試:自簽 TLS server 行為級(預設拒絕驗 x509 / 開啟通過)+ opt-in 規則
  11+5 案例 + TestConnection 3 測試;go build/vet/test 3 packages 全綠
- review:2 輪通過(.autoflow/05-implementation/review/tls-skip-uncommitted-batch-review.md)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 09:01:10 +08:00
276910854e docs(adr): ADR-018 一 agent 多 device 模型 + serial 路由(走向 A')
個人設備管理資料模型決策,使用者裁決 Accepted。

- 走向 A':新增 agents 表 + devices 改實體 USB 掛 agent_id +
  session_tokens.device_id 保留物理 schema 改綁代表 device(避開破壞性
  FK migration)+ 路由統一用 serial(kn_number) + local agent serial 反查
  雙查(純加法、保護剛做完的推論)
- 前端 FE-A 混合:DB 操作用 UUID、路由到 local agent 操作用 serial、
  serial 空的 device 路由操作 disable
- 關鍵論證:推論路由不經 session_tokens.device_id(proxy pickActiveSessionToken
  按 userID),單一 FK 非硬傷;真缺口是 UUID↔local key 未對齊
- migration 0004(序號、無 DDL 可先交付)+ 0005(agents 模型)

替代方案(mapping 版 A/B、FE-B)與否決理由、R-Route 查證結論已併入 ADR。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 08:39:27 +08:00
4f50ad7350 feat(ws): flash-progress WS tunnel proxy(後端,載入模型進度回顯)
flash「載入模型到裝置」後端缺口:flash-progress WS 從 501 stub 換真
tunnel proxy,複用 inference WS 的 path-agnostic handler。

- camera.go registerWebSocketRoutes:加 /ws/devices/:id/flash-progress
  (複用 newWebSocketProxyHandler、掛 wsAuthGroup same-origin cookie)
- stubs.go:刪 flash-progress 501 stub(成對防 radix panic)+ 更新註解
- api.go:更新 wsAuthGroup 註解(現含 inference + flash-progress 兩條裝置級 WS)
- camera_ws_test.go:+3 test(NoForwarder/TunnelDisconnected/CoexistsWithInference)
- all_endpoints_require_auth_test.go:收窄白名單,flash-progress 從 skipped→
  covered 納入「無 cookie 應 401」回歸檢查(守得住證明:移除 auth 即 FAIL)

契約:WS /ws/devices/:id/flash-progress、payload raw {percent,stage,message?,error?}
透明轉發。Reviewer 0C/0M/1Mi 通過(Mi 註解已修)。build/vet/全回歸綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 06:30:36 +08:00
051994ed54 feat(workspace): 圖片/影片/批次推論 tab + 載入模型到裝置 FlashDialog(前端)
推論工作區前端塔3 + flash 前端(i18n/device-detail 交纏、一批 commit)。

塔3(圖片/影片/批次 tab、複用塊1/2 顯示管線 0 重造):
- media.ts 上傳 helper(XHR image/video/batch)+ media-uploader + media-tab
- workspace-client 3 tab 打開 + activeTab 互斥 camera/media WS
- video seek 用後端 durationSeconds 換算(脫離 fps 耦合、Reviewer M-1 修正)
- 補 tab 切換 WS reset 測試(S-1)
- Reviewer 2 輪通過

flash 前端(載入模型到裝置、補「進工作區前置」缺口,照 POC 移植):
- flash-dialog + flash-progress(選 model + 相容性檢查 + WS 進度)
- flash-store(throw-based ApiError)
- use-flash-progress(onOpen 才 POST 防 race + hasStartedRef 防重連重複)
- hardware-compat(targetChip 單值、寧鬆勿嚴、UX 警示非安全閘)
- device-detail-client 掛 FlashDialog(gate 未改、flash 完 fetchDevice 解鎖)
- Reviewer 0C/0M

契約:same-origin cookie、WS/stream/upload URL 相對路徑無 token、i18n 雙語對稱、
設計 token 不裸色值。全前端 tsc/eslint clean、塔3 30 test + flash 45 test 綠。
(全套 11 failed 全屬 conversion-store 既有 flaky、未觸碰)

follow-up:flash WS 逾時提示(Mi)、--success token(design)、conversion-store flaky 釐清。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 06:27:14 +08:00
81a32a5bf8 feat(stage): nginx 加 /ws/ location(推論 inference WS + 其餘 /ws/* stub)
兩個 server block(公網 stage-9527 + 內網 IP 直連 192.168.0.130)各加
一條 location /ws/ → proxy_pass api-server(:3721)。缺這條時 /ws/* 會落到
catch-all → Next.js → 404,擋住前端推論頁 WS 握手。

- WS upgrade 三要素(http/1.1 + Upgrade + Connection $connection_upgrade)
- 長 timeout 86400s + proxy_buffering off(比照 /tunnel/connect)
- 不影響既有 /tunnel/connect(不同 path 前綴、互不 shadow)
- 兩 block header 對齊(含 X-Forwarded-Host)

驗證:/ws/devices/test-id/inference 不帶 cookie → 401(過 backend auth)
不再 404;/tunnel/connect 回歸 401;preset 下載 200/206;demo 設定保留。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 06:00:59 +08:00
0010cc35c3 feat(workspace): canvas overlay 疊 bbox + WS 即時推論面板(前端塊2)
推論工作區前端塊2:canvas overlay 疊在 MJPEG <img> 上、接 WS
inference:<deviceId> 推的 raw InferenceResult、即時繪 bbox+label+信心度;
右側 Inference panel 從 Phase 1 佔位換成 WS 即時結果清單 + FPS/延遲。
照 edge-ai-platform POC 移植(唯讀參考)。

- camera-overlay.tsx:normalized bbox→canvas 像素換算、label 框
- inference-panel.tsx:指標 + 清單 + aria-live 無障礙
- inference-store.ts:fps/avgLatency、MAX_RESULTS=100 上限
- use-inference-stream.ts:用既有 useWebSocket(same-origin cookie、無 token URL)
- workspace-client:overlay 塞 CameraFeed slot、訂閱 WS(僅 isRunning && isOnline)

canvas 顏色讀設計 token(--chart-*/--background,getComputedStyle 快取、
主題切換才重讀)、跟隨深淺主題、不裸色值。

Reviewer 2 輪通過(0C/0M)。修正:
- M1 overlay 座標錯位:feedSize 初值 null、未拿真實顯示尺寸不繪(MJPEG 非
  4:3 不錯位)
- M2 效能 jank:palette 快取 useState + MutationObserver 主題切換才重讀
  (不再每 frame getComputedStyle)
- m2 label fallback 色改實際色(canvas 不認 currentColor)
- m3 isValidBBox guard:壞 payload 略過不 crash

+6 新測試(M1 640×360→height=360、M2 呼叫數不增、m3 NaN/undefined 略過)。
tsc/eslint clean、塊2 範圍 37 test 綠。(全套 11 failed 全屬 conversion-store
既有 time-based flaky、未觸碰)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 05:03:35 +08:00
dc6ca211ae feat(ws): tunnel WS forward — 推論結果經 tunnel 推回雲端(後端塊2)
推論工作區後端塊2:實作 WS forward,讓 local agent 的推論結果 WS
(inference:<deviceId>)經 tunnel 推回雲端瀏覽器 canvas overlay。照
edge-ai-platform POC relay/server.go proxyWebSocket 移植(唯讀參考)。

- forwarder.go ForwardWebSocket:OpenStream→寫 upgrade→讀 101→WebSocketConn
  (新增 WebSocketConn/wsUpgradeError/AsWSUpgradeError)
- proxy.go newWebSocketProxyHandler:hijack browser→回寫 101→雙向 io.Copy
  cross-close(無 goroutine leak)+ copyWebSocketUpgradeHeaders(保留
  Sec-WebSocket-*、剝 Authorization/Origin)
- camera.go registerWebSocketRoutes:GET /ws/devices/:id/inference
- api.go wsAuthGroup(/ws + AuthMiddleware):same-origin cookie 認證,
  無 token-in-URL(security 定案)
- stubs.go:移除 WS inference 501 stub、更新過時 doc comment(Mi-1/2)

架構差異:POC 單 binary,visionA api-server(auth)+remote-proxy 雙 binary,
remote-proxy raw byte pipe 透明穿過 WS upgrade bytes。local agent 端未動。

認證/授權(security 定案 + S1/S2):same-origin cookie、剝 Auth/Origin、
WS 不套 300s timeout、走 pickActiveSessionToken(帶別人 deviceId 也只打到
自己 tunnel;多租戶嚴格綁定屬 Phase 1 M2 debt)。

Reviewer 0C/1M/3Mi 通過(修後)。Major-1 已修:收窄 all_endpoints_require_auth_test
的 /ws/ 白名單,讓 authed inference WS 納入「無 cookie 應 401」回歸檢查
(附守得住證明:移除 auth 測試即 FAIL)。+forwarder 5 test + camera_ws
端到端四層真連線雙向 pipe。build/vet/全回歸綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 04:56:54 +08:00
ba8fd454ea feat(workspace): Camera tab 接真 MJPEG stream + start/stop 狀態機(前端塊1)
推論工作區前端塊1:Camera tab 從 placeholder 換成真正接 MJPEG stream
的即時畫面。照 edge-ai-platform POC camera-feed.tsx 移植(唯讀參考、
重寫為 visionA 慣例)。

- camera-feed.tsx:MJPEG <img> + ResizeObserver + overlay slot(塊2 用)+
  角落標籤
- camera.ts buildStreamUrl:cache-bust 走 _t= query(encode)、token 絕不
  放 URL(same-origin cookie 認證、對齊 use-websocket §10)
- workspace-client Camera tab:接 CameraFeed + 串流狀態機、掉線清串流採
  render 期衍生(非 effect setState)
- use-websocket.ts:修 same-origin WS URL bug——getWsBaseUrl 回 "" 時
  new WebSocket() 需絕對 URL,補 window.location wss/ws(塊2 WS 用)
- types/camera.ts + types/inference.ts(後者鏡射 local-tool driver.InferenceResult)
- start/stop 契約改打 /api/camera/start|stop
- i18n 9 key 雙語齊全

Reviewer 0C/0M 通過。13 test(camera-feed 8 + camera 5)PASS、tsc/eslint clean。
(全套 11 failed 全在 conversion-store.test.ts 既有 flaky、本次未觸碰、經 grep 證實無關)

follow-up(交塊2/3):掉線→回線 stream 卡舊 URL,建議掉線一併 clearStream。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 04:42:41 +08:00
adab000987 feat(camera): camera/media 推論改走 tunnel proxy(塊1,取代 501 stub)
推論工作區後端塊1:把 camera/media 9 條路徑從 501 stub 換成走
newProxyHandler 的 tunnel proxy 宣告(比照 devices.go 慣例,新增
camera.go 而非塞 stubs.go)。

- GET /camera/stream → streaming:true(MJPEG multipart 長連線逐 chunk flush)
- 其餘 8 條(camera list/start/stop、media upload×3、batch-images/:index、
  seek)→ request-response(upload 大檔的 request body 由 proxy 直接
  streaming 送出,streaming flag 只控 response)
- 掛在已套 AuthMiddleware 的 apiGroup 下,沿用剝 Origin 修正

WS inference:<deviceId> 結果推播為塊2(ForwardWebSocket);MJPEG <img>
的 stream 認證分支(R-C4)待 security 定案短期 stream-token 方案,本塊未做。

Reviewer 0C/0M/1Mi/3Sug 通過。+camera_test.go 路由黑箱測試(路徑不再
501、無 session 回 502 TUNNEL_DISCONNECTED),全套 api 回歸綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 04:36:06 +08:00
c9f7eab682 fix(proxy): tunnel 轉發剝除 Origin header 修「開始推論」403
雲端瀏覽器點「開始推論」→ POST /api/devices/{id}/inference/start,
瀏覽器自動帶的 Origin(stage 網域)經 nginx→api-server proxy→tunnel
透傳到 local agent,被 local agent CORSMiddleware(只允許 loopback
Origin)擋回 403。local agent 的 CORS 是為「本機模式 UI 直連」設計,
不該套用在經 tunnel 中繼的 server-to-server 請求。

修法:api-server 的 copyProxyRequestHeaders 剝除 Origin header(比照
既有 Authorization 過濾、EqualFold case-insensitive),local agent 收到
無 Origin → 走 same-origin 放行路徑。

安全邊界:copyProxyRequestHeaders 全 codebase 只用於 tunnel 轉發
(proxy.go newProxyHandler),不影響 api-server 面向瀏覽器的
CORSMiddleware(另一套 gin-contrib/cors)。Reviewer 0C/0M 通過、
Security APPROVE(剝 Origin 不開 CSRF 面,真正閘門在 api-server
AuthMiddleware + SameSite=Lax cookie)。

+2 test(Origin/Authorization 剝除 + case-insensitive)6/6 PASS。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 04:27:49 +08:00
c0e2ac0a59 fix(devices): 前端讀 tunnel_online 修雲端裝置恆離線
normalizeDevice 原本只讀 remote_status(DB 靜態值、exchange 建 device
寫死 offline 永不更新),沒讀後端即時算出的 tunnel_online → tunnel 已通
但雲端裝置列表恆顯示離線。

改為:tunnel_online === true 時 remoteStatus 覆蓋為 online,否則 fallback
回原本 remote_status 判定,兩者皆缺維持 unknown。嚴格布林比對避免 truthy
誤判。多裝置誤判限制(後端 resolveTunnelStatus 寬鬆比對、正解 R3/Phase 1)
已在 code 註解標明,不在本次前端修法範圍。

新增 4 test(true→online / false→沿用 / 缺欄位→沿用 / 皆缺→unknown),
共 16 test 全綠、tsc/eslint 乾淨。Reviewer 0C/0M。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 06:46:44 +08:00
dbe5d6a78e docs(architecture): WS tunnel 網路/防火牆需求文件
整理 WS tunnel 要通需要的網路設定(正式環境交接用):
- 核心:邊界反代開 WebSocket upgrade 轉發(或 L4 TCP passthrough),
  現在剝 WS upgrade header → tunnel 400
- 配套:idle timeout≥90s、wss TLS、DNS、上線前移除 demo 繞法
- Port 清單:唯一對公網開 :9527,:3800/:3801 絕不對外
- 含 HAProxy/ALB/Cloudflare/F5 等效設定 + 驗證方法 + 回傳碼對照

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 03:27:45 +08:00
58313169c2 feat(stage): nginx 加內網 IP 直連 server block(demo 用)
agent 本機 demo 需直連內網 192.168.0.130:9527 建 tunnel(繞過公網邊界
proxy — 那台剝 WS upgrade header、碰不到)。但直連 IP 時 Host=192.168.0.130
不命中 stage-9527 白名單被 444 擋。

加一個 server_name 192.168.0.130 的 server block(複製 stage-9527 block、
XFP 改 http),8 個 location 全對齊含 /tunnel/connect WS upgrade。現有兩
block(default 白名單 + stage-9527)一字未動。

安全:公網經 proxy 進來 Host 固定 stage-9527、偽造 Host 打不進;IP block
只內網可達;default 444 白名單未削弱。⚠️ demo 專用、正式上線前應移除或
加 allow/deny(註解已標)。

搭配 .env.stage VISIONA_RELAY_PUBLIC_URL 改內網 ws(部署時設、缺一不可)。
Reviewer 通過 0C/0M/0Mi。stage 純 demo 無正式使用者。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 20:59:13 +08:00
ab50691f8a fix(conversion): 轉檔完成頁「已加入」按鈕不再重複 promote
SuccessView「加到模型庫」按鈕文字依 importedModelId 切換(已加入/加到模型庫),
但 onClick 永遠是 handleImportClick(開 PromoteDialog)→ 已 promote 後再點又
開 dialog 又 promote。

- 按鈕 onClick 改 handleImportButtonClick 分流:importedModelId 有值→
  router.push(/models/{id}) 導航(不再 promote);空→開 PromoteDialog(現狀)
- aria-label 同步切換(import.aria.done)對齊實際行為
- 後端 PromoteToModels 本有冪等(同 jobID 不重複建),前端 onClick 為防禦層

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 00:30:18 +08:00
7d8ad4857e fix(dashboard): 修首頁白屏(React #185 無限 re-render)
connected-devices-list 的 selector 內 .filter() 每次回新陣列,zustand v5
移除淺比較後 Object.is 永 false → 無限 re-render → #185 整頁白屏(配對後
有 device 資料才觸發)。

- selector 改回只取 s.devices(穩定 ref),filter 移到 component body
- activity-timeline 未知 type 加 fallback(?? Circle / muted)防 F7/F8 WS 事件 crash
- 測試 render not.toThrow + act flush 驗不再迴圈

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 00:17:38 +08:00
6de7c1b4a3 feat(devices): 裝置詳細頁加移除(unpair)功能
- device-store 加 unpairDevice action(接 POST /api/devices/:id/unpair)+ unpairingId
- 詳細頁加移除按鈕 + AlertDialog 二次確認(說明解除配對需重新配對、不可逆)
- 成功 toast + 導回 /devices;403/404/unknown 分流 toast、失敗不導頁
- i18n devices.remove.* 雙語

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 00:17:37 +08:00
0901ffafda feat(models): 模型庫頁按來源分三區(B2)+ 來源標籤 chart token 上色
- /models 改三區渲染:預設模型/我轉檔的/我上傳的(groupModelsBySource)
- 移除 source 篩選器(三區已分來源)、保留 targetChip 篩選跨三區
- 空區顯示標題+空狀態;新 model-section 分區元件
- 來源標籤三色:preset=chart-1/converted=chart-2/uploaded=chart-3
  (設計系統 token、badge + 區標題色點呼應、dark mode 自動跟隨)
- i18n models.section.* + .empty.* 雙語

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 00:17:19 +08:00
838d10b084 fix(stage): nginx 加 /preset-models/ 反代到 api-server(B8 stage 修正)
preset .nef 公用下載端點 GET /preset-models/{id}.nef 由 api-server :3721
串流。nginx.stage.conf 原本無此 location → 落 catch-all 反代到 frontend
:3000 → 404。

加 /preset-models/ location(在 /storage 後、catch-all 前),比照 /storage
pattern:proxy_buffering off 串流大檔、X-Forwarded-Proto https、安全 header
re-add、cache public max-age=86400(preset 公用內容固定)。nginx -t 通過。

配套(同一交付):Dockerfile.stage preset COPY(1d4977a)+ .env.stage 設
VISIONA_PRESET_BASE_URL(git-ignored 不進)。stage e2e 驗證:
GET /preset-models/kl520-yolov5-detection.nef → 200 + 7506224 bytes(與原檔一致)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:52:11 +08:00
1d4977a974 fix(stage): Dockerfile.stage 補 preset 模型 COPY(B8 stage 修正)
B8 的 preset COPY 原本只加到 dev 的 Dockerfile.api-server,漏了 stage
實際用的 docker/Dockerfile.stage(multi-stage nginx runtime)→ stage image
無 preset 檔、/preset-models/* 下載 404。

runtime stage 補:
- COPY --from=backend-builder /src/assets/preset-models → /app/assets/preset-models
- ENV VISIONA_PRESET_MODELS_DIR=/app/assets/preset-models(baked 資產、隨 image 走)

docker build --check 通過。Reviewer 複審通過。
注意:VISIONA_PRESET_BASE_URL + nginx /preset-models/ location 由 devops 在
stage 部署時補(Reviewer 指出的兩個 DevOps 配套缺口)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:46:43 +08:00
9ab1a11ea4 feat(models): 補 7 個預設模型(B8)+ 詳細頁下載按鈕(B5)
# B8 預設模型(簡單版:寫死常數 + 打包 + 不簽 token)
模型庫「預設模型」原本實際是空的(舊 seed 是 demo 假資料、production 不啟用)。
補 7 個公用預設模型(kl520×4 + kl720×3,來源 local-tool models.json):
- metadata 寫死成 Go 常數(presets.go),不進 DB → 公用、不可刪、無 seed 重複
- 7 個 .nef 打包進 image(assets/preset-models/,~61MB;Dockerfile COPY)
- download 走 visionA 自己(簡單版不簽 token,preset 公用本不需授權):
  download handler 三分支 preset→visionA URL / converted→FAA / uploaded→501
- 新 GET /preset-models/*filepath 靜態 serve(無 auth、Content-Disposition
  attachment 用 mime.FormatMediaType、path-traversal 防禦)
- list/get 含 preset(對所有人可見)

# B5 詳細頁下載按鈕 + preset 可下載
- model-detail-client.tsx 補下載按鈕(行為對齊列表卡片)
- isModelDownloadable 加 preset(source==="preset" → true)
- normalizeModelSummary 對 target_chip toLowerCase(修 preset 大寫 KL520
  在小寫晶片篩選下被隱藏的 bug;收斂所有來源大小寫)

# 測試
backend:presets/preset 下載三分支/靜態 serve/path-traversal/條件對稱 全綠
frontend:詳細頁下載鈕 + preset 可下載 + target_chip 大小寫篩選回歸 38 PASS
Reviewer 兩輪通過(Major-1 + Minor-1/2 修畢、複審 0 問題)

# backlog(未做的優化)
preset 下載改 HMAC presigned / preset .nef 改放 FAA 減 image / metadata 後台可管理

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 07:38:25 +08:00
3e45532f55 fix(model-download): 改 redirect/query-string token 下載,根除 CORS preflight 405
模型庫下載原本前端用 fetch + Authorization: Bearer 跨 origin 直連 FAA,
觸發 CORS preflight(OPTIONS);FAA 未設 CORS、OPTIONS 回 405 → 下載失敗。

FAA 設計本就支援「token 放 query string(access_token) + redirect 導航下載」,
故不需 FAA 設 CORS。改採 ADR-017 §11 v1.3 乙案:

- backend: download_url 組成含 ?access_token={url.QueryEscape(token)} 完整 FAA URL;
  ModelDownloadResponse.Token 標 deprecated 保留(向下相容);token 不進 log
- frontend: 移除 downloadModelFile/deriveDownloadFilename(fetch+blob),
  改 triggerNavDownload 用 <a download href> 導航;ModelDownloadGrant 移除 token 欄
- 整條鏈無 CORS:前端→backend 同 origin、前端→FAA 導航無 preflight

docs: ADR-017 增補 §11(v1.3) + TDD.md §9.5
tests: backend 8 PASS(含 token escape 邊界)、frontend 35 PASS;Reviewer 通過(0C/0M)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 06:18:05 +08:00
c2f0b1549e feat: OIDC 登入修復(email fallback / prompt=login / logout 連動)+ 真轉檔鏈路 e2e
接 DB 後真人 OIDC 登入暴露 MC OIDC provider 實作不完整,visionA 端逐項繞過,
讓登入/換帳號可用;另補真轉檔服務的整合 e2e。

OIDC 登入修復(MC 端根因另有交接檔,visionA 先繞過):
- email fallback:MC id_token 不發 email claim(ASP.NET Identity 預設 factory 只發
  sub/name)→ A7 email 必填擋住登入。callback email 空時用 <sub>@noemail.visiona.local
  placeholder,不污染 schema,MC 修好發真 email 後 ON CONFLICT 自動覆寫
- prompt=login:authorize 帶 prompt=login(config VISIONA_OIDC_PROMPT_LOGIN,預設關)
- logout 連動 MC:logout 回 idp_logout(MC Web :7880 /account/logout,GET),前端用
  隱藏 iframe 觸發清 MC session(Web/Api 共享 DataProtection)→ 能換帳號。
  config VISIONA_OIDC_LOGOUT_URL、向下相容(未設則只清本地)

真轉檔鏈路 e2e(//go:build realconv,按需對 stage 跑、不污染主測試集):
- real_converter_e2e:give 真轉檔服務 contract(init→poll→completed/promote/result)
- real_chain_e2e:真轉檔→PromoteToModels→model 進 PG→冪等 全鏈路(對 stage 跑 PASS)

交接檔(給對應團隊根治):
- mc-email-claim-handoff:MC 加 email claim(自訂 UserClaimsPrincipalFactory)
- converter-promote-oauth-handoff:轉檔服務 OAuth 用 form body 非 Basic Auth

全程 Reviewer 審查 + 對 stage 真環境驗證。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 03:18:59 +08:00
dc1c0dbee4 feat(visionA-frontend): B4 model 詳細頁顯示 metadata(input_shape/classes/framework)
對應 backend B4:model 詳細頁顯示轉檔帶來的 metadata。
- Model 型別加 inputShape/classes/framework(optional);normalize 雙吃 snake/camel
- input_shape 顯示成 1 × 3 × 224 × 224;classes 前 8 個 + N 折疊;framework 直接顯示
- 有值才顯示(沒值不渲染空白);中英 i18n
- tsc / lint / next build 綠

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 06:37:36 +08:00
cabbdde495 feat(visionA-backend): DB 接入後續 — OIDC/pairing FK 收尾 + B4 metadata + nginx healthz + 補測試
DB 接入塊 0-5 上主幹後的收尾工作,讓 DB-on 模式可真人使用 + 補齊功能與測試。

OIDC / pairing FK 修復(接 DB 上線必要):
- 新建 internal/user package(User + Store + InMemory + Postgres);OIDC callback
  驗證 id_token 成功後 fail-closed upsert users(sub 直接當 users.id,MC sub 為 UUID)
- pairing exchange 雲端自建 device(不動 local-tool)+ 同 tx 綁 session token;
  自建 device 空 serial 寫 NULL(避免撞 partial unique)
- device.SaveTx / session.CreateTx 新增 tx-aware 版本

B4 model metadata:
- 轉檔 result 的 analysis_info(input_shape/classes/framework)串進 model:
  converter_client → flow → adapter → model.Model → PG → ModelResponse DTO
- input_shape 優先用陣列、後備四維組 NCHW、缺一不亂組;全 optional 防禦性
- 前端詳細頁顯示(另 repo);轉檔端串接交接檔 b4-converter-handoff.md

nginx healthz(部署層):
- 新增 /healthz/deep 轉發 backend(ping PG+Redis、down 回 503)給 LB
- 修掉 default_server return 444 短路 bug(docker healthcheck 長期 unhealthy 真因)

storage error 統一映射(不洩漏 storage 後端細節)。

測試:補 internal/api(storage/errors handler)、cmd/api-server(seed/adapter)、
internal/db(redis)、relay/session 弱處,含 testcontainers integration。
DB 接入相關 package 真環境覆蓋達 88-94%。全程 Reviewer 審查 + 130 真 PG/Redis dbtest 綠。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 06:36:35 +08:00
4d0b870480 feat(visionA-backend): DB 接入 — 6 store 接 PostgreSQL/Redis 持久化(塊 0-5)
把 visionA-backend 6 個 in-memory store 接到資料庫持久化,範圍=完整
(PG 全接 + session 接 Redis + 交易韌性)。interface / handler 不動,
只加 DB 實作 + 換 wiring,config 未設 DB 時保留 in-memory fallback。

- 塊 0 基礎建設:pgx/v5 連線池 + DatabaseConfig/RedisConfig + golang-migrate
  runner(embed)+ cmd/migrate + testcontainers 測試基礎建設
- 塊 1 model → Postgres:array 映射、upsert 保留 CreatedAt、faa_object_key、
  三維 filter(owner/chip/source)、soft-delete partial index
- 塊 2 device → Postgres:partial unique(已刪 serial 可重註冊)、雙狀態欄位
- 塊 3 token → Postgres:pairing_tokens + session_tokens 分表、token_hash 當 PK
- 塊 4 userSession → Redis:idle + absolute 雙 TTL 取代 cleanup goroutine
  (tunnel session 維持 in-memory,yamux handle 不可序列化)
- 塊 5 交易/韌性:WithTx helper + 刪 device cascade 撤銷 token(同 tx 原子)
  + /healthz ping PG/Redis(fail-fast 503)+ pgx error 統一映射(不洩漏 raw error)

降級策略(fail-fast):PG 掉 → 持久資料 API 回 503;Redis 掉 → session 失敗
不自動 fallback in-memory(避免多機 session 不同步)。

DB:PostgreSQL 14.23(gen_random_uuid 內建、無 citext → email 用 lower() unique
index)。每塊經 Reviewer 審查 + 真 PG/Redis testcontainers 全量 dbtest 綠燈,
in-memory fallback 未受影響。

docs: 同步更新 database.md(schema/config/migration 清單)+ api-spec.md
(409/503 錯誤碼、/healthz 新行為、device unpair cascade)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:28:04 +08:00
22f329cdf3 docs(architecture): DB 規劃淡化測試占比強調
測試是工時的自然組成,不再特別強調占比 / 與業界對照:
- 移除前言「測試占比」獨立小節,併入估算假設(中性敘述)
- §5 標題去掉「測試占比分析」,§5.3 精簡為各塊測試 hrs 明細
- 移除「約 45% vs 業界 25-35%」對照句
- 保留實用建議(要砍先砍邊界、別砍 integration 與回歸)
工時數字全未動。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:20:11 +08:00
46958200eb docs(architecture): DB 接入規劃補 MySQL 版本估算 + 文件修訂
- 新增 §3.5 MySQL 版本估算(並列補充、不改 PG 版數字):
  MySQL 三範圍 9.2–15.7 / 18.3–31.4 / 24.6–42.5 人天,比 PG 約 +13%(中位)
  delta 主來源:model array 欄位→JSON 序列化、driver/migration/testcontainers 換、
  UUID + partial index 替代、交易隔離級別差異驗證;Redis delta=0
  含「該不該換」中立建議(由維運能力與組織標準主導)
- 移除 130 上其他專案 DB container 的具體名稱(保留「不該共用、用獨立 DB」結論)
- 測試占比敘述改為「接 DB 的客觀測試需要」(integration/邊界/回歸),數字未動

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:15:01 +08:00
d41a57097f docs(architecture): DB 接入工時規劃 — man-hours/man-day 估算文件
visionA backend 從 in-memory 接資料庫的規劃與工時估算(規劃,未實作)。

範圍與工時(三種):
- 最小可行(只 model): 7.8–13.5 人天
- 持久資料(model+device+pairing/token): 16–27.4 人天
- 完整(+ session→Redis + 韌性): 22–37.7 人天

關鍵結論:
- DB 由他人在 stage docker host(192.168.0.130)開好並提供連線,visionA 端不 provision
- ~80% Go 端工作(repository/migration/測試 via testcontainers)拿連線前就能開工,
  等 DB 只卡最後 1.5–4 天 stage 收尾
- 測試占比 ~45%(依需求刻意拉高、業界常態 25–35%)

DB 選型: Postgres(model/device/pairing/session_token)+ Redis(userSession;
tunnel session 因 yamux Handle 不可序列化維持 in-memory)。

含 Executive Summary(主管)/ 子任務 man-hours 明細(工程師)/ man-day 表(PM)三層視角。
過程草案保留於 .autoflow/04-architecture/(個人層)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 20:01:21 +08:00
53e8ab4ae1 feat(visionA-frontend): Phase 0.9 模型庫下載 — 前端對接 FAA delegated download
對齊 ADR-017 v1.2 決策 2:model-card 下載按鈕 → 打 visionA endpoint 拿 url+token
→ fetch 跨 origin 直連 FAA + Authorization Bearer → blob 觸發下載(不經 visionA、不經 AWS)。

- src/lib/api/model-download.ts: getModelDownload(打 GET /api/models/:id/download)+
  downloadModelFile(fetch + Bearer header + blob,credentials:"omit" 不帶 visionA cookie)+
  triggerBlobDownload(延遲 revoke + finally 釋放)+ deriveDownloadFilename + ModelDownloadError
- model-store: downloadingId 互斥(同時只一個下載、finally 必清)+ downloadModel action +
  isModelDownloadable(source==converted && status==ready)
- model-card: 下載按鈕(converted+ready 才顯示、上傳類隱藏;preventDefault+stopPropagation
  防觸發外層 Link 導航;loading;toast 錯誤)
- i18n zh/en: models.action.download.* / models.download.*(各 error code 對應訊息)

關鍵差異:模型庫下載跨 origin + 需 Bearer header,不能用 <a href> navigation(無法帶 header),
必須 fetch+blob。與既有 conversion download(同 origin navigation)分流。

測試: 43 unit + 互動 test(mock fetch / 按鈕互動 / 顯示條件 / error 分流);tsc/lint/build 全綠。
Reviewer: 0 Critical / 0 Major / 3 Minor / 4 Suggestion,通過(ADR 決策 2 規格 7/7 符合)。

待 stage 實測: FAA 端須設 CORS(允許 visionA origin + preflight Allow-Headers: Authorization),
撞到會落 network_error。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 04:50:17 +08:00
c63886a194 feat(visionA-backend): Phase 0.9 模型庫存取 — FAA delegated download token(B1+B2)
對齊 ADR-017 v1.2:模型庫下載走 visionA 簽 MC delegated token → Client 直連 FAA。

B2 — MC download token client(internal/fileaccess):
- DownloadTokenIssuer: GetServiceToken(打 MC /oauth/token,client_credentials +
  scope files:download.delegate,含 token cache)+ IssueDownloadToken(打 MC Issue 簽 fdt_)
- secret / service token / fdt token 三層全程用 hashShort 遮罩不 log
- FileAccessConfig + VISIONA_FILE_ACCESS_* env + main.go wire(Enabled() 才接)

B1 — object_key 斷層:
- model.Model 加 FAAObjectKey(json:"-" 不揭露前端)
- PromoteToModels 寫入(用 promote response TargetObjectKey = models/{userID}/{jobID}.nef)
- 三方對映天然一致(visionA Issue / FAA path / MC validate)
- 第一階段框死只 Source=converted 類 model,上傳類 download 回 501

download endpoint:
- GET /api/models/:id/download(owner-only)→ {download_url, token, expires_at}
- 前端帶 Authorization: Bearer 直連 FAA(不經 visionA、不經 AWS)
- 401/403/404/501/502 分明,502 對外 mask 不洩漏 MC 內部狀態

測試: 13 + 8 unit test(mock MC + fake issuer,httptest 驗真 HTTP);go build/vet/test 全綠。
Reviewer: 0 Critical / 0 Major / 3 Minor / 4 Suggestion,通過。

技術債(正式上線前): 第一階段 PoC 共用 FAA service client,MC 規範禁止 client 混用
usage、secret 不共用,須 MC 配發 visionA 專屬 usage=file_api client。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 04:06:09 +08:00
88a8ddbd82 docs(architecture): ADR-017 模型庫存取架構(FAA delegated token)— v1.2 stage e2e 實證
決策:模型庫下載走 (a) MC delegated download token + Client 直連 FAA(照 pptx)
- visionA 向 MC POST /file-access/download-tokens 簽 fdt_ token
- Client 帶 Authorization: Bearer 直接打 FAA GET /files/{key}(不經 visionA、不經 AWS)
- FAA 用 MemberCenterDelegatedDownloadTokenValidator 打 MC validate

v1.0→v1.1→v1.2 演進(保留歷史):
- v1.0 推 (c) visionA 自簽(誤判 MC endpoint fictional,只看了 master)
- v1.1 改推 (a)(發現 MC develop 已實作 Issue/Validate、FAA validator 配套)
- v1.2 stage 真環境 + 真 secret + 真 user e2e 實測打通(§10 證據):
  MC Issue HTTP 200 簽出 fdt token → FAA 僅因假 object_key 回 404 file_not_found(認證鏈全綠)

剩餘 visionA 端 blocking(backend 接手):
- B1 object_key 斷層:model.Model 加 FAAObjectKey,第一階段只支援轉檔→promote 類 model
- B2 加打 MC Issue 簽 fdt 的 client code + .env

技術債(正式上線前處理):短期共用 FAA service client 4242ba63 做 PoC,
正式上線須 MC 配發 visionA 專屬 usage=file_api client(MC 規範禁止 client 混用 usage)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 03:42:46 +08:00
8c27da7cca test(local-tool): M9-5 — three-platform validation plan + e2e scripts + MJ3 fix
A 階段最後 milestone、出測試計畫 + 自動化腳本 + 三平台人工 checklist、使用者下週手動跑實機驗證。

Testing artifacts (8 檔、2630 行):
- .autoflow/06-testing/m9-5-validation-plan.md: 656 行(4 情境 × 3 平台 × 2 chip = 24 combo)
- 4 e2e specs (vitest + RTL + mock WS / mock fetch):
  - firmware-upgrade-happy-path.spec.ts (357 / 4 cases)
  - firmware-upgrade-error-recovery.spec.ts (356 / 4 cases + 8 reason it.each)
  - firmware-r-fw-11-modal-not-closable.spec.ts (303 / 6 cases)
  - wails-onbeforeclose-firmware-active.spec.ts (217 / 9 cases、含 5 todo 占位 M9-12)
- 3 manual checklists: macOS 264 / Windows 234 / Linux 243 行

設計取捨:
- 不引入 Playwright/Cypress (visionA-local frontend 沒裝、屬 architect 決策)、走 vitest + mock
- E2E 腳本放 06-testing/scripts/ 作 spec doc + 可選實作參考
- 實機驗證走人工 checklist (dongle 插拔 / kill process / SIGTERM 等需要實體互動)

MJ3 修復 (M9-4 reviewer round 1 留的 follow-up):
- server/internal/api/ws/firmware_ws_test.go: +16/-8
- "type": "firmware:progress" → "firmware_progress" (對齊 firmwareProgressMessage.Type)
- "phase" → "stage" (對齊 TDD §4.2 + FirmwareProgress.Stage)
- 不動 production code、只 test schema 對齊

執行建議 (給你下週):
- Day 1 P0: macOS+Win+Linux × KL520+KL720 happy path (~3h)
- Day 2 P1: R-FW-11 + disconnect_during_op + upgrade_mid_failed + 失敗注入 (4h)
- Day 3 P2: SIGTERM 延遲關閉 + Wails OnBeforeClose force-quit modal (2-3h)

測試:
- go test ./... -race 全綠 (server / wails / frontend 60 tests)
- MJ3 修復不破壞既有測試

A 階段開發 6/7 完成 (M9 文件 + M9-1 ~ M9-4.5)、剩 M9-5 實機驗證 (你下週跑)、跑完依結果決定 A 階段交付或派 sub-agent 修。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:34:17 +08:00
ff9bbc81ed feat(local-tool): M9-4.5 — server SIGTERM + Wails OnBeforeClose firmware-aware shutdown
A 階段尾端 milestone、雙層防護避免使用者在 firmware 升級進行中關閉 app 造成 dongle brick。

Server 端 (3 改):
- main.go: SIGTERM/SIGINT goroutine 加 firmware-aware preamble
- server/internal/firmware/shutdown.go: 新 211 行(AwaitActiveTasksOrTimeout + 3 interfaces + shutdownBroadcastTask minimal struct + toBroadcastTasks helper)
- server/internal/firmware/shutdown_test.go: 新 384 行、8 tests

Wails 端 (3 新 + 2 改):
- visiona-local/main.go: OnBeforeClose 從 inline → app.OnBeforeClose
- visiona-local/app.go: App struct 加 firmwareCloseGuard
- visiona-local/firmware_close_guard.go: 新 244 行(CloseGuard + OnBeforeClose + ConfirmForceClose)
- visiona-local/firmware_close_guard_test.go: 新 280 行、8 tests
- visiona-local/query_firmware_active_tasks.go: 新 111 行(HTTP helper、fail-open、1s timeout)
- visiona-local/query_firmware_active_tasks_test.go: 新 250 行、7 tests

行為:
- Server SIGTERM 有 active task → broadcast `server:shutdown-pending` to "system" room → RequestShutdown + WaitForActiveTasks(220s) → 走原本 shutdownFn
- Wails OnBeforeClose 有 active task → emit Wails event `app:firmware-in-progress` + return true 擋住關閉
- ConfirmForceClose binding 給 frontend 第二層 FORCE 確認用、走 graceful 7+1s shutdown(不是 SIGKILL bypass、雙層防護)

Reviewer 兩輪審查:
- Round 1: 0 Critical / 1 Major / 3 Minor / 4 Suggestion
- 第 2 輪修法(3 sub-agent 平行):
  - Architect: TDD §8.6 改 event 名 `firmware:shutdown-rejected` → `server:shutdown-pending`、標題「拒絕」→「延遲」、補 payload schema 註明 tasks 不含 startTs
  - Design: control-panel.md §6a 改「SIGKILL bypass」→「graceful 7+1s 雙層防護」、補「為何不採 SIGKILL」5 點設計理由、§6a.11 IPC 規格對齊
  - Backend: MaxShutdownWait 180s → 220s(KL720 200s upgrade + 20s buffer)+ broadcast 過濾 startTs(shutdownBroadcastTask minimal struct + toBroadcastTasks helper)

測試:
- server: go test ./... -race 全綠(firmware 2.7s + api/ws/handlers)
- wails: go test ./... -race 全綠(visiona-local 11.2s、21 tests)
- 合計新增 23 unit tests race-clean、0 regression

下一步: M9-5 三平台實機驗證 + 順手修 MJ3(backend smoke test schema phase→stage / firmware:progress→firmware_progress)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 15:07:29 +08:00
06ff2fe987 feat(local-tool): M9-4 — Frontend FW badge + 升級 modal + WS hot-fix
A 階段第四個 milestone、完整 Frontend FW UI(badge / modal / 8 種 reason 復原)+ backend WS hot-fix(補對稱於 flash 的 firmware WS endpoint)。

Frontend(13 修改 / 7 新檔):
- 新 firmware/ component group (badge / upgrade-button / upgrade-dialog 4-phase / progress-view / error-view 8-reason / index)
- Zustand store (firmware-store.ts) + WS hook (use-firmware-progress.ts) 對齊既有 useFlashProgress pattern
- DeviceCard 整合 FirmwareBadge + FirmwareUpgradeButton
- i18n: settings.firmware.* namespace (對齊 Design Spec §9 SoT) + devices.card.fwBadge.* (zh-TW + en, 57 leaf keys × 2 lang = 114 strings)
- toast.ts ToastOptions interface (duration param)
- types/device.ts: FW 衍生欄位 + FirmwareStage/Reason/ProgressEvent/ActiveTask types

Backend WS hot-fix (3 檔):
- ws/firmware_ws.go (50 行、純對稱 flash_ws.go)
- ws/firmware_ws_test.go (165 行、2 smoke tests: broadcast + room isolation)
- router.go: GET /ws/devices/:id/firmware-progress

關鍵設計:
- R-FW-11 緩解: upgrading phase modal 不可關 (onInteractOutside/onEscapeKeyDown preventDefault + 隱藏 X)
- 多裝置隔離 defense in depth: store handleEvent activeDeviceId mismatch 直接 return
- 8 種 reason → 4 種 UX (recoverable/destructive/brick 警告/contactSupport)
- ContactSupport mailto handler (RFC 6068 + encodeURIComponent)

Reviewer 兩輪審查:
- Round 1: 0 Critical / 3 Major / 8 Minor / 5 Suggestion
- Round 2: 0 Critical / 0 Major / 0 Minor / 2 Suggestion(接受方案 A、不需 frontend 第 3 輪)
- MJ1 i18n namespace 採方案 A (settings.firmware.*)、Design SoT 優先、Reviewer 同意

測試:
- pnpm test --run: 60 tests pass (32 firmware: 22 store + 10 badge + 新 9 error-view + 19 既有)
- npx tsc --noEmit: 0 error
- pnpm build: production build 成功
- go test ./internal/api/ws/... -race: 1.964s 全綠
- pnpm lint firmware/: 0 hit (17 既有 lint 問題不屬 M9-4、follow-up)

未做(範圍外):
- Settings 韌體面板 (M9-12 B 階段)
- 手動降版 UI (M9-12)
- 版本切換 dropdown (B 階段)
- Wails 控制台 force-quit modal (M9-4.5)

A 階段 MVP 後端 + 前端開發全部完成、剩 M9-4.5 (SIGTERM + Wails OnBeforeClose) + M9-5 (三平台實機驗證)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:57:21 +08:00