diff --git a/clickhouse-1c/ai/company_intelligence_api.py b/clickhouse-1c/ai/company_intelligence_api.py
index 16e98da..ca483be 100644
--- a/clickhouse-1c/ai/company_intelligence_api.py
+++ b/clickhouse-1c/ai/company_intelligence_api.py
@@ -96,6 +96,13 @@ def severity_badge(severity: str) -> str:
return f'{html.escape(severity or "none")}'
+def company_detail_url(counterparty: str, infobase: str | None = None) -> str:
+ base = f"/manager/company/{quote(counterparty)}"
+ if infobase:
+ return f"{base}?infobase={quote(infobase)}"
+ return base
+
+
def render_manager_brief_html(payload: dict[str, Any]) -> str:
brief = payload.get("brief", {})
context = payload.get("context", {})
@@ -131,6 +138,7 @@ def render_manager_brief_html(payload: dict[str, Any]) -> str:
f"
{html.escape(item.get('company', '-'))}
{severity_badge(item.get('severity', ''))}"
f"{html.escape(item.get('reason', '-'))}
"
f"Действие: {html.escape(item.get('recommended_action', '-'))}
"
+ f"Открыть карточку компании
"
""
)
@@ -142,7 +150,7 @@ def render_manager_brief_html(payload: dict[str, Any]) -> str:
f"{html.escape(company)}
"
f"Прогноз 30д: {html.escape(item.get('forecast_30d', '-'))}
"
f"{html.escape(item.get('interpretation', '-'))}
"
- f"Карточка компании"
+ f"Карточка компании"
""
)
@@ -710,6 +718,312 @@ def manager_brief_view() -> str:
return render_manager_brief_html(load_latest_manager_brief())
+def render_company_detail_html(summary_payload: dict[str, Any], infobase: str | None = None) -> str:
+ card = summary_payload["card"]
+ company_state = summary_payload.get("company_state") or {}
+ forecasts = summary_payload.get("forecasts") or []
+ signals = summary_payload.get("signals") or []
+ recent_documents = summary_payload.get("recent_documents") or []
+
+ title = card.get("counterparty", "Карточка компании")
+ subtitle = summary_payload.get("essence", "")
+ grafana_url = grafana_company_dashboard_url()
+ summary_url = f"/api/1/analytics-1c/companies/{quote(card['counterparty'])}/summary"
+ if infobase:
+ summary_url += f"?infobase={quote(infobase)}"
+ timeline_url = f"/api/1/analytics-1c/companies/{quote(card['counterparty'])}/timeline"
+ if infobase:
+ timeline_url += f"?infobase={quote(infobase)}"
+ forecast_url = f"/api/1/analytics-1c/companies/{quote(card['counterparty'])}/forecast"
+ if infobase:
+ forecast_url += f"?infobase={quote(infobase)}"
+
+ if card.get("signal_severity") == "critical":
+ manager_comment = "Компания требует немедленного внимания: контур считает её operational-critical."
+ elif card.get("signal_severity") == "high":
+ manager_comment = "Компания в зоне повышенного внимания: нужен короткий управленческий разбор причин сигнала."
+ elif card.get("signal_severity") == "medium":
+ manager_comment = "Компания не аварийная, но требует точечной проверки причин отклонения."
+ else:
+ manager_comment = "По компании нет выраженного аварийного сигнала; смотреть контекст и тренд активности."
+
+ metrics = [
+ ("Активность 7д", fmt_number(card.get("amount_7d"))),
+ ("Активность 30д", fmt_number(card.get("amount_30d"))),
+ ("Прогноз 30д", fmt_number(card.get("amount_forecast_30d"))),
+ ("Документы 30д", fmt_number(card.get("docs_30d"))),
+ ("Кейсы", fmt_number(card.get("open_cases_total"))),
+ ("Detections", fmt_number(card.get("detections_total"))),
+ ]
+ metric_cards = "".join(
+ f"{html.escape(label)}
{html.escape(value)}
"
+ for label, value in metrics
+ )
+
+ forecast_rows = []
+ for item in forecasts:
+ forecast_rows.append(
+ ""
+ f"| {html.escape(str(item.get('metric', '-')))} | "
+ f"{fmt_number(item.get('horizon_days'))} | "
+ f"{fmt_number(item.get('baseline_daily'))} | "
+ f"{fmt_number(item.get('predicted_total'))} | "
+ f"{fmt_number(item.get('confidence'))} | "
+ f"{html.escape(str(item.get('note', '-')))} | "
+ "
"
+ )
+
+ signal_rows = []
+ for item in signals[:12]:
+ signal_rows.append(
+ ""
+ f"| {html.escape(str(item.get('generated_at', '-')))} | "
+ f"{severity_badge(str(item.get('severity', 'none')))} | "
+ f"{fmt_number(item.get('score'))} | "
+ f"{html.escape(str(item.get('signal_type', '-')))} | "
+ f"{html.escape(str(item.get('summary', '-')))} | "
+ "
"
+ )
+
+ document_rows = []
+ for item in recent_documents[:20]:
+ document_rows.append(
+ ""
+ f"| {html.escape(str(item.get('ts', '-')))} | "
+ f"{html.escape(str(item.get('doc_type', '-')))} | "
+ f"{html.escape(str(item.get('operation_type', '-')))} | "
+ f"{fmt_number(item.get('amount'))} | "
+ f"{html.escape(str(item.get('status', '-')))} | "
+ f"{html.escape(str(item.get('author', '-')))} | "
+ "
"
+ )
+
+ registry_comment = (
+ f"Сопоставление с реестром: {card.get('registry_match_mode', 'none')}. "
+ f"Ответственный: {card.get('registry_assignee_name') or 'не указан'}."
+ )
+ if card.get("registry_match_mode") == "manual":
+ registry_comment += " Требуется осторожность: запись заведена через manual override."
+
+ return f"""
+
+
+
+
+
+ {html.escape(title)} · 1C Company Brief
+
+
+
+
+
+
+
+
+ Управленческий комментарий
+
+
{html.escape(manager_comment)}
+
{html.escape(registry_comment)}
+
Сейчас база в состоянии {html.escape(str(card.get("current_status", "-")))}, активных блокировок: {fmt_number(card.get("active_locks"))}, дней с последней активности: {fmt_number(card.get("days_since_last_activity"))}.
+
+
+
+
+ Ключевые метрики
+
+ {metric_cards}
+
+
+
+
+ Карточка компании
+
+
+
+
+ Состояние файловой базы
+
+
+
+
+ Прогнозы
+
+
+
+ | Метрика |
+ Горизонт, д |
+ Baseline |
+ Прогноз total |
+ Confidence |
+ Примечание |
+
+
+
+ {''.join(forecast_rows) or '| Нет данных. |
'}
+
+
+
+
+
+ Сигналы
+
+
+
+ | Время |
+ Severity |
+ Score |
+ Тип |
+ Комментарий |
+
+
+
+ {''.join(signal_rows) or '| Нет данных. |
'}
+
+
+
+
+
+ Последние события
+
+
+
+ | Время |
+ Тип |
+ Операция |
+ Активность |
+ Статус |
+ Автор |
+
+
+
+ {''.join(document_rows) or '| Нет данных. |
'}
+
+
+
+
+
+
+"""
+
+
+@app.get("/manager/company/{counterparty}", response_class=HTMLResponse)
+def manager_company_view(counterparty: str, infobase: str | None = None) -> str:
+ return render_company_detail_html(company_summary(counterparty, infobase), infobase)
+
+
if __name__ == "__main__":
args = parse_args()
uvicorn.run(app, host=args.host, port=args.port)