fix: sanitize import job summaries
Some checks failed
CD Pipeline / deploy (push) Has been cancelled

This commit is contained in:
ogt
2026-06-25 14:50:28 +08:00
parent 903cf1a27a
commit 39ff5d5605
5 changed files with 86 additions and 4 deletions

View File

@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.677"
SYSTEM_VERSION = "V10.678"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -754,3 +754,4 @@ POSTGRES_HOST=momo-db
| 2026-06-25 | 匯入、缺貨、設定與通知模板頁不可外露 SQL / 資料表 / 模板代碼 | V10.675 起匯入 job API 對前台回傳白話處置訊息,保留 raw error 於 DB/log自動匯入失敗通知不再顯示 `psycopg2``daily_sales_snapshot``snapshot_date` 等內部字串;缺貨首頁、系統設定與通知模板列表改用營運語言,並補上逾時匯入任務重置與取消 API。 |
| 2026-06-25 | 觀測台入口與通知預覽不可用工程主語干擾營運判讀 | V10.676 起觀測台導覽統一使用「AI 分工矩陣」,通知模板列表會把 K8s/Pod/資料庫/CI Pipeline 等內部詞轉成服務健康、資料連線與部署流程;主機健康事件與自癒劇本改顯示任務/問題/處置提醒,不直接露 `unknown_task``scheduler_task_failure``CODE_FIX` 等 raw code。 |
| 2026-06-25 | 部署監控不得用退役正式域名判定失敗 | V10.677 起 CI/CD 狀態 API 與 active blackbox 監控預設以 `PUBLIC_URL` / `PROD_BASE_URL` 對齊現行正式入口 `https://mo.wooo.work/health`,不再把 `momo.wooo.work` timeout 判成正式部署失敗Webcrumbs loader fallback 也改為資訊級降級訊號,避免健康頁與 log 產生假紅燈。 |
| 2026-06-25 | 匯入任務公開摘要不得回傳資料表或本機檔案定位 | V10.678 起 `/api/import_jobs` / `/api/import_job/<id>``import_summary` 只回傳營運摘要、日期範圍、匯入筆數與同步狀態,不再外露 `table_name``synced_to``daily_sales_snapshot``realtime_sales_monthly`、Google Drive file id 或本機暫存路徑。 |

View File

@@ -8124,7 +8124,7 @@ def handle_cmd(cmd, arg, chat_id, reply_to):
date_min = summ.get('date_min', '')
date_max = summ.get('date_max', '')
synced = summ.get('synced_to', '')
sync_status = summ.get('sync_status', '')
date_range_str = (
f"`{date_min}` ~ `{date_max}`"
if date_min and date_max and date_min != date_max
@@ -8138,8 +8138,8 @@ def handle_cmd(cmd, arg, chat_id, reply_to):
f"📦 *匯入筆數*`{sr:,}` / `{tr:,}` 筆\n"
f"📅 *涵蓋日期*{date_range_str}\n"
)
if synced:
result_msg += f"🔄 *同步至*`{synced}`\n"
if sync_status:
result_msg += f"🔄 *處理狀態*`{sync_status}`\n"
result_msg += f"\n_✨ 業績資料已更新可立即查詢_"
# 取涵蓋日期的 latest date 顯示快速按鈕

View File

@@ -73,11 +73,50 @@ def humanize_import_error(message: Any) -> str:
return compact[:180] + ("" if len(compact) > 180 else "")
def _public_import_summary(raw_summary: Any) -> Optional[Dict[str, Any]]:
"""Return an operator-facing import summary without table names or SQL sync internals."""
if not raw_summary:
return None
summary = raw_summary
if isinstance(raw_summary, str):
try:
summary = json.loads(raw_summary)
except Exception:
return {"message": "匯入摘要暫時無法解析,請以任務狀態為準。"}
if not isinstance(summary, dict):
return None
sync_success = summary.get("sync_success")
public = {
"message": summary.get("message") or "匯入任務已完成。",
"imported_count": summary.get("imported_count"),
"verified": bool(summary.get("verified")),
"date_min": summary.get("date_min"),
"date_max": summary.get("date_max"),
"source_sheet": summary.get("source_sheet"),
"source_header_row": summary.get("source_header_row"),
}
if sync_success is True:
public["sync_status"] = "已更新業績分析儀表板"
elif sync_success is False:
public["sync_status"] = "業績分析儀表板同步未完成"
public["sync_error_message"] = humanize_import_error(summary.get("sync_error"))
else:
public["sync_status"] = ""
return {key: value for key, value in public.items() if value not in (None, "")}
def _public_import_job_payload(job: ImportJob) -> Dict[str, Any]:
payload = job.to_dict()
display_error = humanize_import_error(payload.get("error_message"))
payload["error_message"] = display_error
payload["display_error_message"] = display_error
payload["import_summary"] = _public_import_summary(payload.get("import_summary"))
payload.pop("drive_file_id", None)
payload.pop("local_file_path", None)
return payload

View File

@@ -202,6 +202,48 @@ def test_import_job_public_payload_hides_database_error(monkeypatch, tmp_path):
session.close()
def test_import_job_public_payload_hides_import_summary_internals(monkeypatch, tmp_path):
import_service = _load_import_service(monkeypatch, f"sqlite:///{tmp_path / 'momo.db'}")
import_service.Base.metadata.create_all(import_service.engine)
service = import_service.ImportService()
job_id = service.create_import_job("daily_sales", "drive-file-1", "daily.xlsx", 1024)
session = import_service.Session()
try:
job = session.query(import_service.ImportJob).filter_by(id=job_id).one()
job.status = "completed"
job.import_summary = json.dumps({
"imported_count": 42,
"table_name": "daily_sales_snapshot",
"synced_to": "realtime_sales_monthly",
"sync_success": True,
"sync_error": None,
"verified": True,
"date_min": "2026-06-24",
"date_max": "2026-06-25",
"source_sheet": "即時業績明細",
"source_header_row": 1,
"message": "成功匯入 42 筆資料,已同步至業績分析儀表板",
}, ensure_ascii=False)
job.local_file_path = "data/temp/daily.xlsx"
session.commit()
finally:
session.close()
public_job = service.get_job_status(job_id)
public_text = json.dumps(public_job, ensure_ascii=False)
assert public_job["import_summary"]["imported_count"] == 42
assert public_job["import_summary"]["sync_status"] == "已更新業績分析儀表板"
assert "daily_sales_snapshot" not in public_text
assert "realtime_sales_monthly" not in public_text
assert "table_name" not in public_text
assert "synced_to" not in public_text
assert "drive-file-1" not in public_text
assert "local_file_path" not in public_text
def test_google_drive_auth_refuses_browser_without_json_token(monkeypatch, tmp_path):
import services.google_drive_service as google_drive_service