From 6fa86a18805f0adeae3e12e80d491a9ad6e9d0e6 Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Fri, 22 May 2026 14:46:53 +0300 Subject: [PATCH] feat(1c): add human company detail page --- clickhouse-1c/ai/company_intelligence_api.py | 316 ++++++++++++++++++- 1 file changed, 315 insertions(+), 1 deletion(-) 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 + + + +
+
+
+
+
AW-rus · Company Brief · {severity_badge(str(card.get("signal_severity", "none")))}
+

{html.escape(title)}

+
{html.escape(subtitle)}
+
+ +
+
+ +
+
+

Управленческий комментарий

+
+

{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} +
+
+ +
+

Карточка компании

+
    +
  • Компания: {html.escape(str(card.get("company_name") or card.get("counterparty") or "-"))}
  • +
  • Нормализованное имя: {html.escape(str(card.get("normalized_counterparty") or "-"))}
  • +
  • Инфобаза: {html.escape(str(card.get("infobase") or "-"))}
  • +
  • Ответственный: {html.escape(str(card.get("registry_assignee_name") or card.get("owner_user") or "-"))}
  • +
  • Match mode: {html.escape(str(card.get("registry_match_mode") or "-"))}
  • +
  • ИНН / КПП: {html.escape(str(card.get("registry_inn") or "-"))} / {html.escape(str(card.get("registry_kpp") or "-"))}
  • +
  • Путь базы: {html.escape(str(card.get("base_path") or "-"))}
  • +
+
+ +
+

Состояние файловой базы

+
    +
  • Статус: {html.escape(str(company_state.get("current_status") or card.get("current_status") or "-"))}
  • +
  • Размер базы: {fmt_number(company_state.get("db_size_bytes") or card.get("db_size_bytes"))}
  • +
  • Размер reglog: {fmt_number(company_state.get("reglog_size_bytes") or card.get("reglog_size_bytes"))}
  • +
  • Активные блокировки: {fmt_number(company_state.get("active_locks") or card.get("active_locks"))}
  • +
  • Текущий activity score: {fmt_number(company_state.get("current_activity_score") or card.get("current_activity_score"))}
  • +
  • Последний snapshot: {html.escape(str(company_state.get("ts") or card.get("last_company_snapshot_at") or "-"))}
  • +
+
+ +
+

Прогнозы

+ + + + + + + + + + + + + {''.join(forecast_rows) or ''} + +
МетрикаГоризонт, дBaselineПрогноз totalConfidenceПримечание
Нет данных.
+
+ +
+

Сигналы

+ + + + + + + + + + + + {''.join(signal_rows) or ''} + +
ВремяSeverityScoreТипКомментарий
Нет данных.
+
+ +
+

Последние события

+ + + + + + + + + + + + + {''.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)