# 實施計劃：遠端 UART Log 持久化

**日期**: 2025-11-10
**優先級範圍**: P0 和 P1
**研究基礎**: `.ai/prpi/research/2025-11-10-log-path-management.md`

## 執行摘要

### 問題陳述

當前 FWAuto flash 工作流程在處理包含 `while(1)` 迴圈的嵌入式韌體時存在關鍵限制：

1. **同步 SSH 執行阻塞主執行緒**: `flash.sh` 無限期等待遠端二進位檔案退出，阻塞 FWAuto 60 秒
2. **Timeout 只終止本地 SSH 客戶端**: 遠端韌體變成孤兒進程，繼續執行但無法捕獲日誌
3. **無 UART Log 持久化**: 終端輸出在 SSH 斷開後丟失
4. **需要手動日誌分析**: 使用者必須手動指定 `--log-path` 進行日誌分析工作流程

### 解決方案概覽

透過三個階段實施背景執行與遠端日誌檔案持久化：

- **P0 階段 1**: 重寫 `execute_on_board()` 使用 nohup 背景執行搭配 1 秒驗證
- **P0 階段 2**: 新增遠端日誌檔案配置與自動路徑渲染
- **P1 階段 3**: 自動填充 `log_file_path` 到 state，實現無縫日誌分析工作流程

### 成功標準

- ✅ Flash node 在 10 秒內完成（從 60 秒降低）
- ✅ UART logs 持久化到遠端檔案，使用基於日期的檔名
- ✅ 同一天的執行追加到單一檔案並帶有分隔符
- ✅ `fwauto run` 自動傳遞日誌路徑到日誌分析
- ✅ 不破壞現有 CLI 介面

---

## 架構背景

### Maketool 抽象模式

FWAuto 遵循嚴格的關注點分離：

- **Python 層** (`flash.py`): 編排、state 管理、routing
- **Script 層** (`flash.sh`): 工具鏈特定的實作細節

**設計規則**: Python 程式碼不應包含 SSH 命令或部署邏輯。所有部署機制存在於 shell scripts 中。

### State 資料流

```mermaid
graph LR
    A["flash_node"] -->|"return dict"| B["FirmwareState"]
    B --> C["routing logic"]
    C -->|"flash_status == 'success'"| D["log_node"]
    D -->|"reads log_file_path"| E["Claude Agent SDK"]
```

Nodes 透過 `FirmwareState` 回傳字典進行通訊。Routing 使用狀態欄位（`flash_status`, `build_status`）決定下一個 node。

### 配置繼承

```mermaid
graph TD
    A["User Config<br/>~/.config/fwauto/user-config.toml"] -->|"default values"| C["ProjectConfig.deployment_config"]
    B["Project Config<br/>.fwauto/config.toml"] -->|"project overrides"| C
    C --> D["flash.py reads config"]
    D --> E["passes to flash.sh via env vars"]
```

兩層系統：User Config 提供預設值，Project Config 可以針對專案覆寫。

---

## P0 階段 1：背景執行機制

### 目標

將同步 SSH 執行替換為背景進程 + 1 秒驗證。

### 當前實作分析

**檔案**: `src/fwauto/templates/sdk_am62x_linux/flash/flash.sh`

```bash
# Lines 67-87: execute_on_board()
execute_on_board() {
    local binary_name="$1"
    local binary_args="${BINARY_ARGS:-}"

    log_info "Executing $binary_name on board..."

    # 問題：阻塞直到二進位檔案退出（對 while(1) 韌體永遠不會返回）
    if ! ssh $SSH_OPTS "$BOARD_USER@$BOARD_IP" \
        "cd $DEPLOY_PATH && chmod +x ./$binary_name && ./$binary_name $binary_args"; then
        log_error "Execution failed"
        exit 1
    fi

    log_success "Execution completed"
}
```

**根本原因**: SSH 命令等待遠端二進位檔案退出。對於包含 `while(1)` 迴圈的韌體（典型嵌入式模式），這永遠不會返回。

### 實施任務

#### 任務 1.1：使用背景執行重寫 `execute_on_board()`

**檔案**: `src/fwauto/templates/sdk_am62x_linux/flash/flash.sh`
**位置**: Lines 67-87

**新實作**:

```bash
execute_on_board() {
    local binary_name="$1"
    local binary_args="${BINARY_ARGS:-}"

    log_info "Executing $binary_name on board (background mode)..."

    if [ -n "$binary_args" ]; then
        log_info "With arguments: $binary_args"
    fi

    # 使用 here-doc 安全地傳遞變數
    ssh $SSH_OPTS "$BOARD_USER@$BOARD_IP" bash <<'REMOTE_EOF'
        set -euo pipefail

        BINARY_NAME="$1"
        BINARY_ARGS="$2"
        DEPLOY_PATH="$3"

        cd "$DEPLOY_PATH"

        # 清理：終止同名的現有實例
        pkill -9 "$BINARY_NAME" 2>/dev/null || true

        # 使二進位檔案可執行
        chmod +x "./$BINARY_NAME"

        # 使用 nohup 在背景啟動
        nohup "./$BINARY_NAME" $BINARY_ARGS </dev/null >/dev/null 2>&1 &

        # 捕獲 PID
        FIRMWARE_PID=$!

        # 驗證進程成功啟動
        sleep 1
        if kill -0 $FIRMWARE_PID 2>/dev/null; then
            echo "SUCCESS: Firmware started with PID $FIRMWARE_PID"
            exit 0
        else
            echo "ERROR: Firmware failed to start"
            exit 1
        fi
REMOTE_EOF

    # 將變數作為位置參數傳遞（防止注入攻擊）
    if ! ssh $SSH_OPTS "$BOARD_USER@$BOARD_IP" "$(cat <<'REMOTE_EOF'
        # （與上面相同的內容）
REMOTE_EOF
    )" "$binary_name" "$binary_args" "$DEPLOY_PATH"; then
        log_error "Execution failed"
        exit 1
    fi

    log_success "Firmware started successfully (background mode)"
}
```

**關鍵設計決策**:

- **Here-doc 語法**: 將變數視為資料而非程式碼，防止 shell 注入
- **pkill -9**: 在啟動新韌體前移除舊的實例（防止日誌混淆）
- **1 秒驗證**: `kill -0 $pid` 檢查進程是否存活，不發送實際信號
- **nohup**: 確保韌體在 SSH 斷開後仍存活

**為什麼使用 Here-doc？**: SSH 命令中的直接變數替換容易受到 shell 注入攻擊。Here-doc 將變數作為位置參數傳遞，安全地轉義特殊字元。

#### 任務 1.2：降低 flash_node Timeout

**檔案**: `src/fwauto/nodes/flash.py`
**位置**: Line 115

**變更**:

```python
# 之前：
result = subprocess.run(
    make_command,
    capture_output=True,
    text=True,
    check=True,
    timeout=60,  # ← 修改這裡
    cwd=project_root,
    env=deployment_env,
)

# 之後：
result = subprocess.run(
    make_command,
    capture_output=True,
    text=True,
    check=True,
    timeout=10,  # 背景執行約 5 秒內完成
    cwd=project_root,
    env=deployment_env,
)
```

**理由**: 使用背景執行後，flash.sh 應該在約 5 秒內返回（SCP 上傳 + SSH 連接 + 1 秒驗證）。10 秒 timeout 為慢速網路提供安全邊際。

### 測試策略

**手動驗證**:

```bash
# 直接測試背景執行
cd /path/to/fwauto
uv run fwauto flash

# 預期輸出：
# ✅ Firmware started successfully (background mode)
# Flash node 應在 < 10 秒內完成

# 驗證韌體在板子上運行：
ssh root@192.168.50.170 "ps aux | grep your_binary_name"

# 預期：進程應該以 nohup 作為父進程運行
```

**邊界情況測試**:

1. **韌體立即崩潰**: 1 秒驗證應檢測並報告失敗
2. **網路超時**: 10 秒 timeout 應防止無限掛起
3. **多次 Flash 命令**: pkill 清理應防止重複進程

### 成功標準

- ✅ `fwauto flash` 在 10 秒內完成
- ✅ 韌體進程在 SSH 斷開後仍存活
- ✅ 1 秒驗證檢測啟動失敗
- ✅ pkill 清理防止重複進程

---

## P0 階段 2：遠端日誌檔案生成

### 目標

自動建立並重定向 UART logs 到遠端檔案，使用基於日期的命名和追加模式。

### 配置擴展

#### 任務 2.1：新增遠端日誌屬性到 ProjectConfig

**檔案**: `src/fwauto/config.py`
**位置**: Line 159 之後（在 `ssh_options` 屬性之後）

**新屬性**:

```python
@property
def remote_log_enabled(self) -> bool:
    """是否啟用遠端日誌檔案"""
    return self.deployment_config.get("remote_log_enabled", True)

@property
def remote_log_pattern(self) -> str:
    """遠端日誌檔案命名模板 (Jinja2 格式)"""
    default_pattern = "{{ deploy_path }}/uart_{{ date }}.log"
    return self.deployment_config.get("remote_log_pattern", default_pattern)

@property
def last_log_file_path(self) -> str | None:
    """上次 flash 產生的日誌路徑（程式自動寫入）"""
    return self.deployment_config.get("last_log_file_path")

def update_last_log_path(self, log_path: str) -> None:
    """
    更新 last_log_file_path 到 config.toml

    Args:
        log_path: 遠端日誌路徑（例如 root@192.168.50.170:/home/root/uart.log）
    """
    import toml

    config_path = self.project_root / ".fwauto" / "config.toml"

    # 讀取現有 config
    if config_path.exists():
        config_data = toml.load(config_path)
    else:
        config_data = {}

    # 確保結構存在
    sdk_type = self.sdk_type
    if sdk_type:
        if "deployment" not in config_data:
            config_data["deployment"] = {}
        if sdk_type not in config_data["deployment"]:
            config_data["deployment"][sdk_type] = {}

        # 更新路徑
        config_data["deployment"][sdk_type]["last_log_file_path"] = log_path

    # 寫回檔案
    with open(config_path, "w") as f:
        toml.dump(config_data, f)

    logger.debug(f"💾 Updated last_log_file_path in config: {log_path}")
```

**設計決策**:

- **預設啟用**: 大多數使用者預設需要日誌持久化
- **Jinja2 模板**: 彈性模式允許自訂（例如 `logs/{{ binary_name }}_{{ date }}.log`）
- **可用變數**:
  - `{{ deploy_path }}`: `/home/root`
  - `{{ date }}`: `2025-11-10`（FWAuto 執行機器的本地日期）
  - `{{ binary_name }}`: 可執行檔名（例如 `led_blink`）

**為什麼使用本地日期？**: 使用 FWAuto 機器的日期（而非板子的日期）確保即使板子時鐘錯誤或未同步，日誌檔名仍保持一致。

#### 任務 2.2：User Config Schema 新增

**檔案**: 僅供文件參考（無需程式碼變更）

使用者可以在 `~/.config/fwauto/user-config.toml` 中覆寫：

```toml
[deployment.am62x]
board_ip = "192.168.50.170"
board_user = "root"
deploy_path = "/home/root"
ssh_options = "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
remote_log_enabled = true
remote_log_pattern = "{{ deploy_path }}/uart_{{ date }}.log"
```

或在 `.fwauto/config.toml` 中進行專案特定覆寫：

```toml
[deployment.am62x]
remote_log_pattern = "logs/{{ binary_name }}_{{ date }}.log"

# 程式自動更新（使用者也可以手動修改）
last_log_file_path = "root@192.168.50.170:/home/root/uart-2025-11-10.log"
```

**重要說明**：
- `last_log_file_path` 由程式自動寫入，使用者通常不需要手動設定
- 使用者可以手動修改此值來分析特定日誌
- 此欄位在 `.fwauto/config.toml` 中，不應放在 user-config.toml

### Flash Node 日誌路徑渲染

#### 任務 2.3：在 flash.py 中渲染遠端日誌路徑

**檔案**: `src/fwauto/nodes/flash.py`
**位置**: Line 106 之後（在建立 `deployment_env` 之後）

**新函數**:

```python
def _render_remote_log_path(config: ProjectConfig, binary_name: str) -> str:
    """
    使用 Jinja2 模板渲染遠端日誌檔案路徑。

    Args:
        config: 專案配置
        binary_name: 可執行檔名（例如 "led_blink"）

    Returns:
        渲染後的遠端日誌路徑（例如 "/home/root/uart_2025-11-10.log"）
    """
    from datetime import datetime
    from jinja2 import Template

    template_str = config.remote_log_pattern
    template = Template(template_str)

    context = {
        "deploy_path": config.deploy_path,
        "date": datetime.now().strftime("%Y-%m-%d"),
        "binary_name": binary_name,
    }

    return template.render(context)
```

**整合點**（在 line 106 之後插入）:

```python
# 建立部署環境
deployment_env = {
    "BOARD_IP": config.board_ip,
    "BOARD_USER": config.board_user,
    "DEPLOY_PATH": config.deploy_path,
    "SSH_OPTIONS": config.ssh_options,
}

if binary_args:
    deployment_env["BINARY_ARGS"] = binary_args

# 新增：加入遠端日誌配置
if config.remote_log_enabled:
    # 從韌體路徑提取二進位檔名
    # 注意：firmware_path 目前從未在 state 中設定（見研究發現）
    # 對於階段 2，我們將從建置工件中提取
    binary_name = _extract_binary_name(project_root, config)
    remote_log_path = _render_remote_log_path(config, binary_name)

    deployment_env["REMOTE_LOG_ENABLED"] = "1"
    deployment_env["REMOTE_LOG_PATH"] = remote_log_path
```

**輔助函數**（在 `_render_remote_log_path` 之前加入）:

```python
def _extract_binary_name(project_root: str, config: ProjectConfig) -> str:
    """
    從建置目錄提取二進位檔名。

    Args:
        project_root: 專案根目錄路徑
        config: 專案配置

    Returns:
        二進位檔名（例如 "led_blink"）

    Raises:
        RuntimeError: 如果未找到可執行檔
    """
    import os
    from pathlib import Path

    build_dir = Path(project_root) / "build"

    # 在建置目錄中尋找第一個可執行檔
    for file in build_dir.iterdir():
        if file.is_file() and os.access(file, os.X_OK):
            return file.name

    raise RuntimeError(f"No executable binary found in {build_dir}")
```

### Shell Script 日誌重定向

#### 任務 2.4：使用日誌重定向和追加模式更新 flash.sh

**檔案**: `src/fwauto/templates/sdk_am62x_linux/flash/flash.sh`
**位置**: 在 `execute_on_board()` 函數的 here-doc 內

**更新的 Here-doc**:

```bash
ssh $SSH_OPTS "$BOARD_USER@$BOARD_IP" bash <<'REMOTE_EOF'
    set -euo pipefail

    BINARY_NAME="$1"
    BINARY_ARGS="$2"
    DEPLOY_PATH="$3"
    REMOTE_LOG_ENABLED="${4:-0}"
    REMOTE_LOG_PATH="${5:-/dev/null}"

    cd "$DEPLOY_PATH"

    # 清理：終止現有實例
    pkill -9 "$BINARY_NAME" 2>/dev/null || true

    chmod +x "./$BINARY_NAME"

    # 決定日誌重定向目標
    if [ "$REMOTE_LOG_ENABLED" = "1" ]; then
        # 追加模式帶分隔符
        {
            echo ""
            echo "=========================================="
            echo "Firmware Execution: $(date '+%Y-%m-%d %H:%M:%S')"
            echo "Binary: $BINARY_NAME"
            echo "Arguments: $BINARY_ARGS"
            echo "=========================================="
            echo ""
        } >> "$REMOTE_LOG_PATH"

        LOG_TARGET="$REMOTE_LOG_PATH"
    else
        LOG_TARGET="/dev/null"
    fi

    # 使用日誌重定向啟動（追加模式）
    nohup "./$BINARY_NAME" $BINARY_ARGS </dev/null >> "$LOG_TARGET" 2>&1 &

    FIRMWARE_PID=$!

    # 驗證進程已啟動
    sleep 1
    if kill -0 $FIRMWARE_PID 2>/dev/null; then
        if [ "$REMOTE_LOG_ENABLED" = "1" ]; then
            echo "SUCCESS: Firmware started with PID $FIRMWARE_PID, logging to $REMOTE_LOG_PATH"
        else
            echo "SUCCESS: Firmware started with PID $FIRMWARE_PID"
        fi
        exit 0
    else
        echo "ERROR: Firmware failed to start"
        exit 1
    fi
REMOTE_EOF
```

**關鍵功能**:

- **追加模式（`>>`）**: 同一天的執行追加到單一檔案
- **執行分隔符**: 時間戳標題清楚分隔每次執行
- **條件日誌記錄**: 如果禁用則回退到 `/dev/null`
- **單一重定向**: stdout 和 stderr 都導向同一檔案（`2>&1`）

**函數呼叫更新**:

```bash
# 將日誌配置作為位置參數傳遞
if ! ssh $SSH_OPTS "$BOARD_USER@$BOARD_IP" "$(cat <<'REMOTE_EOF'
    # ... （上面的 here-doc 內容）
REMOTE_EOF
)" "$binary_name" "$binary_args" "$DEPLOY_PATH" \
   "${REMOTE_LOG_ENABLED:-0}" "${REMOTE_LOG_PATH:-/dev/null}"; then
    log_error "Execution failed"
    exit 1
fi
```

### 測試策略

**手動驗證**:

```bash
# 使用預設配置測試
uv run fwauto flash

# 檢查板子上是否建立日誌檔案：
ssh root@192.168.50.170 "cat /home/root/uart_2025-11-10.log"

# 預期輸出：
# ==========================================
# Firmware Execution: 2025-11-10 15:30:45
# Binary: led_blink
# Arguments: --mode fast
# ==========================================
#
# [LED] Initializing...
# [LED] Blinking at 2Hz...

# 測試追加模式（再次執行 flash）：
uv run fwauto flash
ssh root@192.168.50.170 "cat /home/root/uart_2025-11-10.log | grep 'Firmware Execution'"

# 預期：兩個時間戳標題
```

**測試自訂模式**:

加入到 `.fwauto/config.toml`：

```toml
[deployment.am62x]
remote_log_pattern = "logs/{{ binary_name }}_{{ date }}.log"
```

執行並驗證：

```bash
uv run fwauto flash
ssh root@192.168.50.170 "ls logs/"
# 預期：led_blink_2025-11-10.log
```

### 成功標準

- ✅ 使用基於日期的命名建立日誌檔案
- ✅ 追加模式正常工作（多次執行 → 單一檔案）
- ✅ 時間戳分隔符清楚標記每次執行
- ✅ 自訂模式從配置中正確應用
- ✅ 禁用模式優雅地回退到 `/dev/null`

---

## P1 階段 3：State 自動日誌檔案路徑傳遞

### 目標

在成功 flash 後自動填充 `log_file_path` 到 `FirmwareState`，實現無縫 `fwauto run` 工作流程（flash → 日誌分析）。

### 當前工作流程缺口

**P1 之前**:

```bash
# 使用者必須手動指定日誌路徑
fwauto flash
fwauto log "分析 LED 錯誤" --log-path root@192.168.50.170:/home/root/uart_2025-11-10.log
```

**P1 之後**:

```bash
# 自動日誌路徑傳遞
fwauto run "分析 LED 錯誤"

# 內部流程：
# 1. flash_node 在 state 中設定 log_file_path
# 2. Routing 邏輯導向 log_node
# 3. log_node 從 state 讀取 log_file_path
```

### State 欄位新增

**檔案**: `src/fwauto/state.py`
**位置**: 現有欄位（無需變更）

`log_file_path` 已存在於 `FirmwareState` 中（line 31）：

```python
class FirmwareState(TypedDict):
    # ... （其他欄位）
    log_file_path: str | None  # UART log 檔案路徑用於分析
    log_analysis_result: str | None
```

**當前使用方式**: 僅由 CLI `--log-path` 參數填充。P1 之後，也由 `flash_node` 填充。

### Flash Node State 更新

#### 任務 3.1：在 flash_node 回傳字典中設定 log_file_path

**檔案**: `src/fwauto/nodes/flash.py`
**位置**: Lines 124-129（結果字典建構）

**之前**:

```python
# Lines 124-129
logger.info("✅ Flash Node: Flash successful")
return {
    **state,
    "flash_status": "success",
    "flash_log": stdout,
}
```

**之後**:

```python
logger.info("✅ Flash Node: Flash successful")

# 如果啟用則建立遠端日誌路徑
remote_log_path = None
if config.remote_log_enabled:
    binary_name = _extract_binary_name(project_root, config)
    local_path = _render_remote_log_path(config, binary_name)
    # 轉換為遠端 SCP 格式：user@host:path
    remote_log_path = f"{config.board_user}@{config.board_ip}:{local_path}"
    logger.info(f"📊 Flash Node: Auto-populated log_file_path = {remote_log_path}")

    # 持久化到 config.toml（支援跨執行）
    config.update_last_log_path(remote_log_path)

return {
    **state,
    "flash_status": "success",
    "flash_log": stdout,
    "log_file_path": remote_log_path,  # 新增：自動填充給 log_node（State 傳遞）
}
```

**關鍵實作細節**:

- **遠端路徑格式**: `user@host:path`（例如 `root@192.168.50.170:/home/root/uart_2025-11-10.log`）
- **條件填充**: 僅在 `remote_log_enabled=True` 時設定
- **重用渲染邏輯**: 呼叫與階段 2 使用的相同 `_render_remote_log_path()` 函數
- **SCP 相容格式**: log_node 已透過 `_is_remote_path()` 支援此格式（參見 src/fwauto/nodes/log.py:190-212）

### Routing 邏輯增強

**檔案**: `src/fwauto/graph.py`
**位置**: 無需變更

當前 routing 邏輯已支援自動流程：

```python
# Lines 92-118: route_after_flash()
def route_after_flash(state: FirmwareState) -> str:
    if state["flash_status"] == "success":
        # 檢查執行模式
        execution_mode = state.get("execution_mode")

        if execution_mode == "run":
            # 'run' 模式帶 user_prompt → 導向日誌分析
            if state.get("user_prompt"):
                return "log_node"
            # 'run' 模式無 prompt → 結束
            return END

        # 其他模式 → flash 後結束
        return END
    # ... （失敗處理）
```

**運作方式**:

1. 使用者執行 `fwauto run "分析 LED"`
2. CLI 設定 `execution_mode="run"` 和 `user_prompt="分析 LED"`
3. flash_node 完成，在 state 中設定 `log_file_path`
4. `route_after_flash()` 看到 `execution_mode=="run"` 和 `user_prompt` 存在
5. 導向 `log_node`
6. log_node 從 state 讀取 `log_file_path`（src/fwauto/nodes/log.py:42）

**無需 routing 變更** - 現有邏輯已支援此工作流程。

#### 任務 3.2：log_node 優先順序與 Fallback 機制

**檔案**: `src/fwauto/nodes/log.py`
**位置**: Lines 40-50（log_file_path 取得邏輯）

**當前邏輯**:

```python
# Line 42
log_file_path = state.get("log_file_path")

# Validate log_file_path is provided
if not log_file_path:
    logger.error("❌ Log Analysis: No log file path provided")
    return {
        **state,
        "log_analysis_result": "Error: No log file path provided. Use --log-path option.",
    }
```

**更新後邏輯**（三層 fallback）:

```python
# 優先順序 1: 從 State 讀取（CLI 參數或 flash_node 設定）
log_file_path = state.get("log_file_path")

# 優先順序 2: Fallback 從 config 讀取（跨執行持久化）
if not log_file_path:
    try:
        project_root = state.get("project_root", os.getcwd())
        from ..config import load_project_config
        config = load_project_config(Path(project_root))
        log_file_path = config.last_log_file_path

        if log_file_path:
            logger.info(f"📂 Using log path from config (last flash): {log_file_path}")
    except Exception as e:
        logger.debug(f"Failed to load last log path from config: {e}")

# 驗證：兩種來源都沒有則報錯
if not log_file_path:
    logger.error("❌ Log Analysis: No log file path provided")
    return {
        **state,
        "log_analysis_result": (
            "Error: No log file path provided.\n"
            "Please use --log-path option or run 'fwauto flash' first."
        ),
    }
```

**優先順序設計**:

| 優先級 | 來源 | 使用情境 | 範例 |
|--------|------|---------|------|
| **1** | CLI 參數 (`--log-path`) | 使用者明確指定 | `fwauto log "問題" --log-path xxx` |
| **2** | State 記憶體 | `fwauto run`（同一次執行） | flash_node → log_node |
| **3** | Config 持久化 (`last_log_file_path`) | `fwauto flash` → 稍後 → `fwauto log` | 跨執行 fallback |

**關鍵實作細節**:
- CLI 參數已在 `cli.py:325` 設定到 State，所以優先級最高
- State 傳遞適用於 `fwauto run` 工作流程
- Config fallback 支援跨執行場景
- 錯誤訊息提示使用者兩種解決方案

### 向後相容性

**三種使用方式都支援**:

```bash
# 方式 1: fwauto run（State 傳遞）
fwauto run "分析 LED"

# 方式 2: fwauto flash 後執行 log（Config fallback）
fwauto flash
# ... 等待一段時間 ...
fwauto log "有錯誤嗎?"  # 自動從 config 讀取路徑

# 方式 3: 手動指定路徑（CLI 優先級最高）
fwauto log "分析 LED" --log-path root@192.168.50.170:/home/root/uart_2025-11-09.log
```

**優先級確保**: CLI 參數 > State > Config

### 測試策略

**整合測試**:

```bash
# 測試自動工作流程
uv run fwauto run "有任何 error 嗎?"

# 預期行為：
# 1. Flash node 完成，印出：
#    "📊 Flash Node: Auto-populated log_file_path = root@192.168.50.170:/home/root/uart_2025-11-10.log"
#
# 2. Routing 自動導向 log_node
#
# 3. Log node 印出：
#    "📡 Detected remote path: root@192.168.50.170:/home/root/uart_2025-11-10.log"
#    "📥 Downloading remote log: ..."
#
# 4. Claude Agent SDK 分析下載的日誌
#
# 5. 結果顯示給使用者
```

**邊界情況測試**:

1. **測試跨執行 fallback**:
   ```bash
   # 第一次執行
   uv run fwauto flash

   # 檢查 config.toml 是否寫入 last_log_file_path
   cat .fwauto/config.toml | grep last_log_file_path

   # 第二次執行（不同的工作流程）
   uv run fwauto log "有錯誤嗎?"

   # 預期：log_node 從 config 讀取路徑並分析
   ```

2. **測試優先順序**:
   ```bash
   # 先 flash（寫入 config）
   uv run fwauto flash

   # 手動覆寫（應該優先使用 CLI 參數）
   uv run fwauto log "問題" --log-path /other/path.log

   # 預期：使用 /other/path.log，不是 config 中的路徑
   ```

3. **遠端日誌禁用**: 設定 `remote_log_enabled=false`，log_node 應提示使用者手動指定路徑

4. **無使用者提示**: `fwauto run` 無 prompt 應在 flash 後結束（無日誌分析）

### 成功標準

- ✅ `fwauto run "prompt"` 自動傳遞日誌路徑到 log_node（State 傳遞）
- ✅ `fwauto flash` 後 `fwauto log "prompt"` 自動讀取上次路徑（Config fallback）
- ✅ CLI `--log-path` 覆寫優先級最高
- ✅ 三種使用方式都正常工作
- ✅ 遠端日誌禁用模式優雅處理
- ✅ Config 持久化不影響現有專案（向下相容）

---

## 實施順序

### 建議順序

1. **階段 1 任務（第 1 天）**:
   - 任務 1.1：使用背景執行重寫 `execute_on_board()`
   - 任務 1.2：將 flash_node timeout 降低到 10 秒
   - **手動測試**: 驗證背景執行工作，韌體在 SSH 斷開後存活

2. **階段 2 任務（第 2 天）**:
   - 任務 2.1：新增 config.py 屬性
     - `remote_log_enabled`, `remote_log_pattern` (讀取)
     - `last_log_file_path` (讀取)
     - `update_last_log_path()` (寫入)
   - 任務 2.3：在 flash.py 中實作 `_render_remote_log_path()` 和 `_extract_binary_name()`
   - 任務 2.4：使用日誌重定向和追加模式更新 flash.sh
   - **手動測試**: 驗證日誌檔案已建立，追加模式工作，分隔符存在

3. **階段 3 任務（第 3 天）**:
   - 任務 3.1：flash_node 設定 State 並呼叫 `config.update_last_log_path()`
   - 任務 3.2：log_node 加入 config fallback 邏輯
   - **整合測試**:
     - 端到端測試 `fwauto run` 工作流程（State 傳遞）
     - 跨執行測試 `fwauto flash` → `fwauto log`（Config fallback）
     - 優先順序測試（CLI > State > Config）

### 回滾策略

每個階段都是獨立功能的：

- **階段 1 之後**: 背景執行工作，日誌仍導向 `/dev/null`
- **階段 2 之後**: 日誌持久化到檔案，但需要手動 `--log-path` 進行分析
- **階段 3 之後**: 完整自動工作流程

如果階段 3 失敗，使用者仍可手動指定 `--log-path`。如果階段 2 失敗，背景執行仍改善 flash 效能。

---

## 風險評估

### 高風險區域

1. **Here-doc Shell 注入**: 不正確的 here-doc 語法可能允許命令注入
   - **緩解**: 使用位置參數（`"$1"`, `"$2"`）而非直接變數替換
   - **測試**: 嘗試惡意 binary_args 如 `; rm -rf /` 驗證轉義工作

2. **pkill -9 過於激進**: 可能終止同名的無關進程
   - **緩解**: 文件說明使用者應使用唯一的二進位檔名
   - **未來增強**: 使用 PID 檔案或進程組進行精確定位

3. **遠端路徑渲染時區問題**: 本地日期可能不匹配板子的日期
   - **緩解**: 文件說明使用 FWAuto 機器的日期（設計決策）
   - **好處**: 即使板子時鐘錯誤仍保持一致命名

### 中風險區域

1. **網路延遲**: 1 秒驗證對慢速網路可能太短
   - **緩解**: 未來使驗證延遲可配置（預設 1 秒）

2. **磁碟空間**: 追加模式可能無限增長日誌檔案
   - **緩解**: 文件說明日誌輪換最佳實踐
   - **未來增強**: 新增最大檔案大小限制

### 低風險區域

1. **Jinja2 模板錯誤**: 無效模式可能導致 flash_node 崩潰
   - **緩解**: 在模板渲染周圍使用 try-except，回退到預設模式

2. **二進位檔名提取**: 如果建置目錄有多個可執行檔怎麼辦？
   - **當前行為**: 使用找到的第一個可執行檔
   - **未來增強**: 允許在配置中明確指定二進位檔名

---

## 測試檢查清單

### 階段 1 測試

- [ ] Flash node 在 10 秒內完成
- [ ] 韌體進程在 SSH 斷開後存活
- [ ] `kill -0` 驗證檢測啟動失敗
- [ ] pkill 清理防止重複進程
- [ ] 網路 timeout（10 秒）防止無限掛起

### 階段 2 測試

- [ ] 預設日誌模式建立 `uart_YYYY-MM-DD.log`
- [ ] 自訂模式從配置中正確應用
- [ ] 追加模式工作（多次執行 → 單一檔案）
- [ ] 時間戳分隔符存在於執行之間
- [ ] 禁用模式（`remote_log_enabled=false`）使用 `/dev/null`
- [ ] 日誌檔案可透過 SSH 讀取

### 階段 3 測試

**State 傳遞測試**:
- [ ] `fwauto run "prompt"` 自動傳遞 log_file_path（State）
- [ ] 遠端路徑格式正確（`user@host:path`）
- [ ] log_node 檢測遠端路徑並下載檔案

**Config 持久化測試**:
- [ ] `fwauto flash` 寫入 `last_log_file_path` 到 config.toml
- [ ] `fwauto flash` 後執行 `fwauto log "prompt"` 自動讀取路徑（Config fallback）
- [ ] Config 中的路徑格式正確

**優先順序測試**:
- [ ] CLI `--log-path` 覆寫 State 和 Config
- [ ] State 優先於 Config（`fwauto run` 時）
- [ ] 無 State 時 fallback 到 Config

**邊界情況**:
- [ ] `fwauto run` 無 prompt 在 flash 後結束（無日誌分析）
- [ ] 遠端日誌禁用模式優雅處理（有用的錯誤訊息）
- [ ] 無 config 和 State 時提示清楚的錯誤訊息

### 安全測試

- [ ] binary_args 中的 shell 注入嘗試安全失敗
- [ ] binary_name 中的 shell 注入嘗試安全失敗
- [ ] 惡意日誌模式不會逃離部署目錄

---

## 文件更新（排除在 P0/P1 範圍外）

*注意：這些任務是 P2 文件階段的一部分，不包含在此計劃中。*

---

## 成功指標

### 效能改善

- **Flash 持續時間**: 60 秒 → 10 秒（減少 83%）
- **Run 命令延遲**: 消除手動日誌路徑指定

### 使用者體驗

- **零手動步驟**: `fwauto run "prompt"` 端到端工作，無需額外標誌
- **跨執行記憶**: `fwauto flash` 後稍後執行 `fwauto log` 自動記住路徑
- **三種工作模式**: run（自動）、flash+log（記憶）、log --log-path（手動）
- **日誌持久化**: UART logs 在 SSH 斷開後存活並跨重啟持久化
- **同日追加**: 多次測試執行建立單一日誌檔案並帶有清楚的分隔符

### 可靠性

- **進程驗證**: 1 秒檢查確保韌體成功啟動
- **清理自動化**: pkill 防止孤兒進程累積
- **配置繼承**: 使用者預設 + 專案覆寫支援多專案工作流程
- **路徑優先順序**: CLI > State > Config，確保使用者總能覆寫
- **KISS 設計**: 不新增檔案（state_cache），直接用 config.toml

---

## 未來增強（超出範圍）

*這些是超出 P0/P1 範圍的潛在改進：*

1. **日誌輪換**: 自動清理舊日誌檔案
2. **可配置驗證延遲**: 針對不同韌體啟動時間調整 1 秒延遲
3. **進程組管理**: 使用 PID 檔案進行更精確的進程清理
4. **二進位檔名明確配置**: 覆寫多二進位專案的自動檢測
5. **即時日誌串流**: `tail -f` 整合用於即時日誌監控

---

## 結論

此計劃透過三個緊密耦合的階段實施遠端 UART log 持久化：

- **P0 階段 1** 解決直接阻塞問題（60 秒 timeout）
- **P0 階段 2** 新增日誌檔案基礎設施（持久化日誌）
- **P1 階段 3** 連接工作流程（自動路徑傳遞）

所有階段遵循 FWAuto 的架構原則：
- Maketool 抽象（Python 編排，shell scripts 實作）
- 基於 State 的通訊（回傳字典更新共享 state）
- 配置繼承（使用者預設 + 專案覆寫）

實施設計為增量部署，每個階段都有獨立的回滾點。
