diff --git a/.agents/workflows/compiler-apk.md b/.agents/workflows/compiler-apk.md
index 86d7c0f..d985a35 100644
--- a/.agents/workflows/compiler-apk.md
+++ b/.agents/workflows/compiler-apk.md
@@ -105,7 +105,7 @@ if [ -n "$APK_SOURCE" ]; then
MINOR=$(grep "def verMinor" app/build.gradle | awk '{print $4}')
PATCH=$(grep "def verPatch" app/build.gradle | awk '{print $4}')
- VERSION_NAME="${MAJOR}_0${MINOR}_${PATCH}_R"
+ VERSION_NAME="${MAJOR}_0${MINOR}_${PATCH}_D"
VERSION_CODE=$((MAJOR * 10000 + MINOR * 100 + PATCH))
echo "=================================================="
diff --git a/app/build.gradle b/app/build.gradle
index e9a595a..102f5c7 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -10,7 +10,7 @@ plugins {
// 避免不同尺寸機台間版本號產生衝突。
// =========================================================================
def verMinor = 2
-def verPatch = 11
+def verPatch = 13
@@ -76,9 +76,12 @@ android {
verMajor = 10 // 10 吋
}
+ // 環境後綴:release → R(正式)、debug → D(測試),供現場一眼辨識 APK 來源
+ def envSuffix = (buildType == "release") ? "R" : "D"
+
// 動態重算並覆寫此 Variant 的版本號,避免互相污染
def calculatedVersionCode = verMajor * 10000 + calculatedMinor * 100 + calculatedPatch
- def calculatedVersionName = "${verMajor}_0${calculatedMinor}_${calculatedPatch}_R"
+ def calculatedVersionName = "${verMajor}_0${calculatedMinor}_${calculatedPatch}_${envSuffix}"
variant.outputs.each { output ->
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4dc2b1e..c1f0ca3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -14,6 +14,8 @@
+
+
目的:讓後台(star-cloud)能讀取現場機台的行動網路品質,用於監控與低訊號告警。
+> 狀態:規格定案,待實作。最後更新:2026-06-18。
+> 設計原則:**全程 additive**,沿用既有 heartbeat / machine_logs 架構,不破壞線上 main(晟崴/中國醫)既有 API 與資料結構。
+
+---
+
+## 0. 全鏈路(沿用現有路徑,無新通道)
+
+```
+App (每 60s heartbeat)
+ → MQTT machine/{serial}/heartbeat ← payload 加巢狀 network 物件
+ → Go Gateway RPUSH redis: mqtt_incoming_jobs
+ → Laravel php artisan mqtt:listen (BLPOP)
+ → Job ProcessHeartbeat.php ← 解析 network + 寫入
+ → DB machines(快照欄位) + machine_logs(時序,仿溫度)
+ → Admin index.blade.php network 分頁(仿 temperatureAjax 圖表)
+```
+
+已驗證事實(原始碼對帳):
+- App:`MqttManager` 每 60s 發 `machine/{serial}/heartbeat`;payload 為 Gson 序列化的 `HeartbeatPayload`(snake_case,目前僅 `firmware_version`/`temperature`)。
+- App:`FontendActivity.getMobileSignalStrength()` 目前只呼叫 `CellSignalStrengthLte.getDbm()`,無 5G、無 RSSI/SINR。
+- App:`app/build.gradle` defaultConfig `minSdk 21`、`targetSdk 34`、`compileSdk 35`;**注意 flavor S9XYU 的 `minSdk 28`**,S12XYU 繼承 21。兩者皆低於訊號 getter 所需 API。
+- 後台:`mqtt-gateway/` Go → `ListenMqttQueue.php`(BLPOP) → `ProcessHeartbeat::dispatch`,`type=heartbeat` 路由確認。
+- 後台:`ProcessHeartbeat.php` 目前更新 `last_heartbeat_at/status/temperature/firmware_version`。
+- 後台:machines 表「無」任何訊號欄位。
+- 後台:**溫度曲線無專用時序表**,是讀 `machine_logs` 用 `message='Temperature reported: :temp°C'` + `context['temp']` 過濾(`MachineController.php` `temperatureAjax()`),且 `ProcessHeartbeat` 只在溫度變化時才寫 log → 內建降頻。
+
+---
+
+## 1. App 端
+
+### 1.1 HeartbeatPayload 擴充
+在 `HeartbeatPayload`(`structure/mqtt/HeartbeatPayload.java`)加一個**巢狀** `network` 物件(不要平鋪,確保舊解析零影響):
+
+```json
+{
+ "firmware_version": "1.2.3",
+ "temperature": 25,
+ "network": {
+ "type": "cellular", // cellular | wifi | ethernet
+ "tech": "LTE", // LTE | NR | null
+ "rsrp": -95, // dBm,行動網路主要覆蓋指標
+ "rssi": -65, // dBm
+ "sinr": 12, // dB
+ "rsrq": -10 // dB
+ }
+}
+```
+- WiFi / 有線:`type` 對應,行動欄位給 null。
+- 機台未送 `network`(舊版)→ 後台視為 null,不寫訊號 log。
+
+### 1.2 訊號讀取(抽成 util,與 UI icon 共用)
+把 `getMobileSignalStrength()` 邏輯抽成可重用方法,補齊三項並加防護:
+
+```java
+// LTE:getRsrp/getRssi 需 API 29+;getRssnr(=SINR) 需 API 32+
+CellSignalStrengthLte lte = ((CellInfoLte) cellInfo).getCellSignalStrength();
+int rsrp = (SDK_INT >= 29) ? lte.getRsrp() : lte.getDbm();
+int rssi = (SDK_INT >= 29) ? lte.getRssi() : Integer.MAX_VALUE;
+int sinr = (SDK_INT >= 32) ? lte.getRssnr() : Integer.MAX_VALUE;
+int rsrq = (SDK_INT >= 29) ? lte.getRsrq() : Integer.MAX_VALUE;
+
+// 5G NR:需判斷 CellInfoNr / CellSignalStrengthNr(API 31+)
+// getSsRsrp() / getSsRsrq() / getSsSinr(),tech 標記為 "NR"
+```
+
+**必做防護(否則低版機台崩潰 / 5G 機台誤報無訊號):**
+1. 所有 API 29+/32+ getter 一律包 `Build.VERSION.SDK_INT` 判斷;取不到的值以 null 上報(`Integer.MAX_VALUE` 視為 null)。
+2. **版本防護用 inline guard 即可(agy 詰問後共識,原判已撤回)**:
+ - 本專案 minSdk 21 = ART,採惰性驗證;對「**既有類別的新方法**」(如 `CellSignalStrengthLte.getRsrp()`,類別自 API 17 即存在)只要包 `Build.VERSION.SDK_INT` inline guard 就安全,**不會** class-load `VerifyError`/啟動崩潰。這也是本檔既有慣例([FontendActivity.java:709](file:///home/mama/projects/TaiwanStar_general_size_s_Chengwai/app/src/main/java/com/unibuy/smartdevice/ui/FontendActivity.java#L709) 的 `getSignalStrength()` API29、:1474 的 TIRAMISU API33 皆 inline 出貨)。Wrapper/Helper class 只是可選風格,非正確性必需。
+ - **唯一例外**:引用 API 29+/30+ 才有的**新類別「型別」**(`CellInfoNr`/`CellSignalStrengthNr`)時,少數客製 ROM 仍可能 `NoClassDefFoundError`。故 **5G 分支整段務必在 `SDK_INT` guard 內**(見下點)。
+3. **5G NSA fallback(agy 查核)**:現場 5G 多為 **NSA(非獨立組網)**,控制面錨定在 LTE,`getAllCellInfo()` 主小區仍是 `CellInfoLte`。所以**不能只靠 `CellInfoNr`**;邏輯應為「先取 `CellInfoNr`(SA 5G,整段包在 API 31+ guard 內),取不到則 fallback `CellInfoLte`」,否則 NSA 機台拿不到訊號。
+4. **權限:真機需驗證 + 穩健性缺口(agy 詰問後降級,非確證致命 bug)**:`checkPermissionByAccessFineLocation()` 在 Android M+ 直接 `return true`,**沒做執行期動態申請**,且全專案無 `requestPermissions`、非平台簽章。但 `getMobileSignalStrength()` 有 `try-catch(SecurityException)` → 即使未授權也只是回 -1,**不會崩**。
+ - 這些是**專用 kiosk 機**,`ACCESS_FINE_LOCATION` 多半在佈署時用 `adb shell pm grant` 或 provisioning 預授;且 cellular 分支只在 active transport==CELLULAR 時才跑(現場多走 WiFi/有線),故是否真壞**未經現場驗證**。
+ - **行動**:實作訊號上報前,先在目標機台確認權限已授(`adb shell dumpsys package | grep ACCESS_FINE_LOCATION`);若未授,選擇「佈署腳本 `pm grant`」或「App 內補執行期申請」其一即可。不列為阻斷性 bug,但**不確認就上線,行動網路訊號會全空**。
+
+### 1.3 commit 注意
+本分支 `feat/general-cart-and-payment` 正在改 `MqttManager`;`HeartbeatPayload` 擴充與訊號讀取請**獨立小 commit**,避免與購物車/支付改動纏在一起。
+
+---
+
+## 2. 後台 star-cloud
+
+### 2.1 採用既有慣例:寫 machine_logs,不另建表
+初版**不新建 `machine_network_logs` 表**,沿用溫度同一條管道(已驗證可行):
+
+`ProcessHeartbeat::handle()` 在解析 `network` 後(整段包在 `if (isset($payload['network']))`,舊機台不進):
+
+1. **更新 machines 快照欄位**(列表頁即時格數用)。**防 null 覆蓋(agy 查核)**:僅在 `network.type === 'cellular'` 時才寫 `last_rsrp/last_rssi/last_sinr`;切到 WiFi/有線時行動欄位為 null,若無條件覆蓋會把先前的 LTE 快照洗掉。
+2. **寫 machine_logs 時序**,但**降頻**(見 §3 閾值):
+ - type = `network`
+ - message = `'Signal reported'`
+ - context = `{ "rsrp": -95, "rssi": -65, "sinr": 12, "type": "cellular", "tech": "LTE" }`
+3. **低訊號告警**(見 §3):達門檻時另寫一筆 `level=warning`/`error` 的 log,複用 `MachineLog` 既有 Discord hook。
+
+> 若日後需密集採樣再升級為獨立 `machine_network_logs` 表(仿 `machine_logs` append-only、`const UPDATED_AT = null`、複合索引 `(machine_id, recorded_at)`)。初版不需要。
+
+### 2.2 machines 主表新增 nullable 快照欄位
+新 migration(仿 `add_status_to_machines_table`),全部 nullable(additive、不破壞既有 query):
+
+```php
+$table->string('network_type')->nullable(); // cellular/wifi/ethernet
+$table->smallInteger('last_rsrp')->nullable();
+$table->smallInteger('last_rssi')->nullable();
+$table->smallInteger('last_sinr')->nullable();
+$table->timestamp('last_network_at')->nullable();
+```
+同步加入 `Machine` model `$fillable`。
+
+### 2.3 Admin 介面(仿溫度圖)
+- 新增 `signalAjax()`,複製 `temperatureAjax()` 改成過濾 `message='Signal reported'`、輸出 `context['rsrp']`(及 sinr)成時間序列。
+- 前端 `admin/machines/index.blade.php`:
+ - 列表頁狀態欄旁加「訊號」格,用 `last_rsrp` 換算格數(與 App icon 邏輯一致)。
+ - 詳情面板新增 `network` 分頁,複製溫度圖 Alpine.js 邏輯,打 `signalAjax` 畫 RSRP / SINR 折線。
+
+---
+
+## 3. 閾值定義(本規格核心數值)
+
+### 3.1 指標對照(業界通用)
+| 指標 | 單位 | 優 | 良 | 中 | 差 | 極差 |
+|---|---|---|---|---|---|---|
+| **RSRP**(覆蓋,主指標) | dBm | ≥ -80 | -80~-90 | -90~-100 | -100~-110 | < -110 |
+| **RSSI**(總功率,參考) | dBm | ≥ -65 | -65~-75 | -75~-85 | -85~-95 | < -95 |
+| **SINR**(訊噪比,越大越好) | dB | ≥ 20 | 13~20 | 0~13 | < 0 | — |
+
+販賣機只做小流量(心跳/交易/同步),重點是**穩定不斷線**,非高速。**-110 dBm 為危險線,-100 以上視為健康。**
+
+### 3.2 寫 log 降頻閾值
+- **RSRP 變化 ≥ 5 dB** 才寫一筆(RSRP 自然抖動約 ±3 dB,故用 5 dB 去噪)。
+- **保底**:即使無明顯變化,**最多每 30 分鐘補寫一筆**(曲線不斷、可確認機台仍在線且穩定)。
+- 判斷依據:比對 `machines.last_rsrp` 與 `machines.last_network_at`。
+
+### 3.3 低訊號告警閾值(Discord)
+- **RSRP < -110 dBm 且連續 3 次心跳(約 3 分鐘)皆低於** → 告警(避免瞬間抖動誤報)。
+- 輔助:**SINR < 0 dB 持續** → 一併告警(干擾大、易斷)。
+- **加 RSRQ(agy 建議)**:RSRP 看似正常(如 -95)但 **RSRQ < -15 dB** 代表多重路徑反射/同頻干擾嚴重,仍會交易超時 → RSRQ < -15 dB 持續也告警。
+- 分級建議:
+ - RSRP -100 ~ -110:`warning`(觀察)
+ - RSRP < -110:`error`(派人檢查天線/機台位置)
+
+### 3.4 ⚠️ 告警盲點與修正(agy 查核,最致命)
+**問題**:RSRP < -110 時機台往往已斷網,根本**發不出**那筆低訊號 heartbeat,後台「連續 3 次 < -110」的條件永遠湊不齊 → 訊號最差的機台反而不會告警。
+
+**修正**:低訊號告警**必須與既有「離線偵測機制」結合**(比對 `last_heartbeat_at` 逾時的排程)。判定離線時主動檢查該機台 `machines.last_rsrp`:
+- 若最後一筆 RSRP 已 < -105 dBm → 告警訊息註記「機台離線,最後上報訊號極差 (RSRP: -108dBm),疑似訊號不佳斷線」。
+- 否則照原離線告警處理。
+
+如此「訊號差→斷線→離線告警帶出最後訊號」與「訊號差但仍在線→低訊號告警」兩條路徑互補,涵蓋完整。
+
+---
+
+## 4. 相容性檢查(對照後台慣例)
+- ✅ heartbeat payload 用巢狀 `network`,舊欄位零變動;舊機台不送 → null。
+- ✅ machines 全用 nullable 欄位,既有 query/API 回應結構不破壞。
+- ✅ 沿用 machine_logs + 新增 admin endpoint,不碰 B 系列既有 endpoint。
+- ⚠️ 註:「nullable/不破壞回應結構/B 系列不可刪」是後台**實務慣例**(migration/fillable 習慣),非 `api-rules.md` 明文 MUST 條款;仍應遵循。
+
+---
+
+## 5. 實作清單(給後台同事 / 後續排程)
+**App 端**
+- [ ] **先驗權限**:在目標機台確認 `ACCESS_FINE_LOCATION` 已授(`adb shell dumpsys package | grep FINE_LOCATION`);未授則 `pm grant` 或補執行期申請(§1.2-4,非阻斷但不確認會全空)
+- [ ] 訊號讀取:補 RSSI/SINR/RSRQ + inline `Build.VERSION.SDK_INT` 防護(符合本庫慣例,毋須 Wrapper class)+ `CellInfoNr`→`CellInfoLte` fallback、5G 分支整段在 API31+ guard 內(§1.2-2/3)
+- [ ] `HeartbeatPayload` 加巢狀 `network`(含對應 class,測 Gson JSON 結構;null 不序列化已驗證安全)
+- [ ] 獨立 commit(勿混入購物車/支付)
+
+**後台 star-cloud**
+- [ ] migration:machines 加 5 個 nullable 快照欄位 + 更新 `Machine` fillable
+- [ ] `ProcessHeartbeat`:解析 `network` → **僅 cellular 才更新快照(防 null 覆蓋)** + 依 §3.2 降頻寫 machine_logs + 依 §3.3 告警
+- [ ] **離線偵測排程**:判離線時帶出 `last_rsrp` 註記「疑似訊號不佳斷線」(§3.4 告警盲點修正)
+- [ ] `signalAjax()` endpoint(仿 `temperatureAjax`)
+- [ ] `index.blade.php`:列表訊號格 + 詳情 network 圖表分頁
+
+---
+
+## 6. 查核紀錄
+- 2026-06-18 初稿:內建 Agent 子代理對帳 App + star-cloud 原始碼,A1–A7 事實成立。
+- 2026-06-18 agy 交叉比對(Antigravity 獨立讀兩 repo):A1–A7 全部複核成立;提出 5 項補充——告警斷網盲點(§3.4)、權限缺口(§1.2-4)、VerifyError(§1.2-2)、5G NSA fallback(§1.2-3)、WiFi null 覆蓋保護(§2.1)、RSRQ 干擾告警(§3.3);更正 minSdk 因 flavor 而異(S9XYU=28)。
+- 2026-06-18 與 agy **交叉詰問**(非照單全收,回原始碼查證後辯論):
+ - 爭點1「權限 🔴 致命」→ **agy 修正、降級**:有 try-catch 不會崩、kiosk 多半預授、cellular 分支少觸發 → 改列「需現場驗證的穩健性缺口」(§1.2-4)。
+ - 爭點2「必須 Wrapper class 否則啟動崩」→ **agy 認錯、撤回**:minSdk21=ART 惰性驗證,inline `SDK_INT` guard 足夠且為本庫慣例(:709);唯引用 5G 新「型別」需整段 guard(§1.2-2)。
+ - 5G NSA fallback / WiFi null 覆蓋 / 告警綁離線偵測三點,我覆核後同意,維持。