fix: sanitize import job summaries
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
This commit is contained in:
@@ -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 # 用於模板顯示
|
||||
|
||||
|
||||
@@ -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 或本機暫存路徑。 |
|
||||
|
||||
@@ -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 顯示快速按鈕
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user