diff --git a/clickhouse-1c/ai/company_intelligence_api.py b/clickhouse-1c/ai/company_intelligence_api.py
index dea59d4..27e462f 100644
--- a/clickhouse-1c/ai/company_intelligence_api.py
+++ b/clickhouse-1c/ai/company_intelligence_api.py
@@ -69,6 +69,14 @@ def weekly_digest_state_dir() -> Path:
return root / "state" / "weekly-digest"
+def recovery_brief_state_dir() -> Path:
+ root = Path(os.getenv("AW_1C_ROOT", "/opt/activitywatch/clickhouse-1c"))
+ configured = os.getenv("AW_1C_RECOVERY_BRIEF_STATE_DIR")
+ if configured:
+ return Path(configured)
+ return root / "state" / "recovery-brief"
+
+
def load_latest_manager_brief() -> dict[str, Any]:
latest_path = manager_brief_state_dir() / "latest.json"
if not latest_path.exists():
@@ -83,6 +91,13 @@ def load_latest_weekly_digest() -> dict[str, Any]:
return json.loads(latest_path.read_text(encoding="utf-8"))
+def load_latest_recovery_brief() -> dict[str, Any]:
+ latest_path = recovery_brief_state_dir() / "latest.json"
+ if not latest_path.exists():
+ raise HTTPException(status_code=404, detail="recovery brief not generated yet")
+ return json.loads(latest_path.read_text(encoding="utf-8"))
+
+
def load_brief_history_records(limit: int = 20) -> list[dict[str, Any]]:
history_dir = manager_brief_state_dir() / "history"
if not history_dir.exists():
@@ -366,6 +381,81 @@ def build_company_priority_context(summary_payload: dict[str, Any], infobase: st
}
+def find_recovery_incident_for_company(
+ items: list[dict[str, Any]],
+ counterparty: str,
+ infobase: str | None = None,
+) -> dict[str, Any] | None:
+ fallback: dict[str, Any] | None = None
+ for item in items:
+ if str(item.get("company") or "") != counterparty:
+ continue
+ if infobase and str(item.get("infobase") or "") == infobase:
+ return item
+ if fallback is None:
+ fallback = item
+ return fallback
+
+
+def build_company_recovery_context(summary_payload: dict[str, Any], infobase: str | None = None) -> dict[str, Any]:
+ card = summary_payload.get("card") or {}
+ priority_context = summary_payload.get("priority_context") or build_company_priority_context(summary_payload, infobase)
+ counterparty = str(card.get("counterparty") or "")
+ matched_incident = None
+ recovery_generated_at = None
+
+ try:
+ recovery_payload = load_latest_recovery_brief()
+ matched_incident = find_recovery_incident_for_company(
+ recovery_payload.get("recovery", {}).get("top_incidents", []),
+ counterparty,
+ infobase or str(card.get("infobase") or "") or None,
+ )
+ recovery_generated_at = recovery_payload.get("generated_at")
+ except HTTPException:
+ recovery_payload = None
+
+ if matched_incident:
+ diagnosis = str(matched_incident.get("diagnosis") or priority_context.get("current_priority_reason") or "-")
+ actions = [str(item) for item in matched_incident.get("actions", []) if str(item).strip()]
+ stop_doing = str(matched_incident.get("stop_doing") or "")
+ target_state = str(matched_incident.get("target_state_24h") or "")
+ confidence = "recovery-brief/codex"
+ else:
+ diagnosis_parts = [str(priority_context.get("current_priority_reason") or "").strip()]
+ if int(card.get("open_cases_total") or 0) > 0:
+ diagnosis_parts.append(f"открытых кейсов {int(card.get('open_cases_total') or 0)}")
+ if int(card.get("active_locks") or 0) > 0:
+ diagnosis_parts.append(f"активных блокировок {int(card.get('active_locks') or 0)}")
+ if int(card.get("detections_total") or 0) > 0:
+ diagnosis_parts.append(f"detections {int(card.get('detections_total') or 0)}")
+ diagnosis = "; ".join(part for part in diagnosis_parts if part) or "явного recovery-диагноза пока нет"
+ actions = list(priority_context.get("actions", []))
+ if int(card.get("open_cases_total") or 0) > 0 and "Назначить владельца на закрытие открытых кейсов в течение 24 часов." not in actions:
+ actions.insert(0, "Назначить владельца на закрытие открытых кейсов в течение 24 часов.")
+ if int(card.get("active_locks") or 0) > 0 and "Снять busy/lock-контур прежде чем обсуждать долгосрочный прогноз." not in actions:
+ actions.append("Снять busy/lock-контур прежде чем обсуждать долгосрочный прогноз.")
+ if card.get("registry_match_mode") == "manual" and "Проверить корректность manual-сопоставления до жёстких управленческих выводов." not in actions:
+ actions.append("Проверить корректность manual-сопоставления до жёстких управленческих выводов.")
+ stop_doing = "Не проводить общие обсуждения без владельца, срока и числовой цели на день."
+ target_cases = max(int(card.get("open_cases_total") or 0) - 3, 0)
+ target_state = (
+ f"Снизить открытые кейсы ниже {target_cases}, "
+ f"снять новый прирост и подтвердить отсутствие лишних блокировок по следующему запуску."
+ )
+ confidence = "deterministic-fallback"
+
+ return {
+ "generated_at": recovery_generated_at,
+ "confidence": confidence,
+ "diagnosis": diagnosis,
+ "actions": actions[:5],
+ "stop_doing": stop_doing,
+ "target_state_24h": target_state,
+ "source_incident": matched_incident,
+ }
+
+
def grafana_company_dashboard_url() -> str:
return os.getenv(
"AW_1C_MANAGER_BRIEF_GRAFANA_URL",
@@ -422,6 +512,7 @@ def render_manager_brief_html(payload: dict[str, Any]) -> str:
delta_html_url = "/manager/changes"
weekly_html_url = "/manager/trends/weekly"
weekly_digest_url = "/manager/digest/weekly"
+ recovery_html_url = "/manager/recovery"
problematic_1d_url = "/manager/problematic?days=1"
problematic_7d_url = "/manager/problematic?days=7"
json_url = "/api/1/analytics-1c/manager/brief/latest"
@@ -749,6 +840,7 @@ def render_manager_brief_html(payload: dict[str, Any]) -> str:
Что изменилось
Неделя
Weekly digest
+ AI recovery
Проблемные 1д
Проблемные 7д
History API
@@ -965,6 +1057,7 @@ def render_brief_history_html(items: list[dict[str, Any]]) -> str:
Текущий brief
Что изменилось
Неделя
+ AI recovery
Проблемные 1д
Проблемные 7д
@@ -1054,6 +1147,7 @@ def render_problematic_companies_html(items: list[dict[str, Any]], days: int) ->
Текущий brief
История brief
Неделя
+ AI recovery
Срез 1д
Срез 7д
@@ -1200,6 +1294,7 @@ def render_brief_delta_html(payload: dict[str, Any]) -> str:
История brief
Неделя
Weekly digest
+ AI recovery
Проблемные 1д
Проблемные 7д
@@ -1356,6 +1451,7 @@ def render_weekly_trend_html(report: dict[str, Any]) -> str:
Что изменилось
История brief
Проблемные 7д
+ AI recovery
Weekly digest
@@ -1513,6 +1609,7 @@ def render_weekly_digest_html(payload: dict[str, Any]) -> str:
Что изменилось
Неделя
Проблемные 7д
+ AI recovery
JSON
Markdown
@@ -1560,6 +1657,101 @@ def render_weekly_digest_html(payload: dict[str, Any]) -> str: