This repository has been archived on 2026-11-14. You can view files and clone it, but cannot push or open issues or pull requests.
korabayev/index.html

1281 lines
62 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ExecutiveOS AI — Помощник для руководителя (Lotus)</title>
<link rel="manifest" href="manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj4KICA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZyIgeDE9IjAlIiB5MT0iMCUiIHgyPSIxMDAlIiB5Mj0iMTAwJSI+CiAgICAgIDxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiM3YmI3ZmYiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjNWNmMmEwIi8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMTIxNjIwIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzBhMGMxMCIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgPHJlY3Qgd2lkdGg9IjUxMiIgaGVpZ2h0PSI1MTIiIHJ4PSI5NiIgZmlsbD0idXJsKCNiZykiLz4KICA8cmVjdCB4PSI4MCIgIHk9IjEyMCIgd2lkdGg9IjM1MiIgaGVpZ2h0PSI1NiIgcng9IjE0IiBmaWxsPSJ1cmwoI2cpIi8+CiAgPHJlY3QgeD0iODAiICB5PSIyMDgiIHdpZHRoPSIyNDAiIGhlaWdodD0iNDAiIHJ4PSIxMCIgZmlsbD0iIzdiYjdmZiIgb3BhY2l0eT0iMC40NSIvPgogIDxyZWN0IHg9IjgwIiAgeT0iMjY4IiB3aWR0aD0iMzAwIiBoZWlnaHQ9IjQwIiByeD0iMTAiIGZpbGw9IiM3YmI3ZmYiIG9wYWNpdHk9IjAuMzAiLz4KICA8cmVjdCB4PSI4MCIgIHk9IjMyOCIgd2lkdGg9IjE4MCIgaGVpZ2h0PSI0MCIgcng9IjEwIiBmaWxsPSIjN2JiN2ZmIiBvcGFjaXR5PSIwLjIwIi8+CiAgPGNpcmNsZSBjeD0iNDAyIiBjeT0iMzcwIiByPSI0NCIgZmlsbD0iIzVjZjJhMCIvPgogIDxwYXRoIGQ9Ik0zODUgMzcwIGwxNCAxNCBsMzAgLTMwIiBzdHJva2U9IiMwYTBjMTAiIHN0cm9rZS13aWR0aD0iMTAiIGZpbGw9Im5vbmUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K" />
<link rel="apple-touch-icon" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj4KICA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZyIgeDE9IjAlIiB5MT0iMCUiIHgyPSIxMDAlIiB5Mj0iMTAwJSI+CiAgICAgIDxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiM3YmI3ZmYiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjNWNmMmEwIi8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJiZyIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjMTIxNjIwIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzBhMGMxMCIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICA8L2RlZnM+CiAgPHJlY3Qgd2lkdGg9IjUxMiIgaGVpZ2h0PSI1MTIiIHJ4PSI5NiIgZmlsbD0idXJsKCNiZykiLz4KICA8cmVjdCB4PSI4MCIgIHk9IjEyMCIgd2lkdGg9IjM1MiIgaGVpZ2h0PSI1NiIgcng9IjE0IiBmaWxsPSJ1cmwoI2cpIi8+CiAgPHJlY3QgeD0iODAiICB5PSIyMDgiIHdpZHRoPSIyNDAiIGhlaWdodD0iNDAiIHJ4PSIxMCIgZmlsbD0iIzdiYjdmZiIgb3BhY2l0eT0iMC40NSIvPgogIDxyZWN0IHg9IjgwIiAgeT0iMjY4IiB3aWR0aD0iMzAwIiBoZWlnaHQ9IjQwIiByeD0iMTAiIGZpbGw9IiM3YmI3ZmYiIG9wYWNpdHk9IjAuMzAiLz4KICA8cmVjdCB4PSI4MCIgIHk9IjMyOCIgd2lkdGg9IjE4MCIgaGVpZ2h0PSI0MCIgcng9IjEwIiBmaWxsPSIjN2JiN2ZmIiBvcGFjaXR5PSIwLjIwIi8+CiAgPGNpcmNsZSBjeD0iNDAyIiBjeT0iMzcwIiByPSI0NCIgZmlsbD0iIzVjZjJhMCIvPgogIDxwYXRoIGQ9Ik0zODUgMzcwIGwxNCAxNCBsMzAgLTMwIiBzdHJva2U9IiMwYTBjMTAiIHN0cm9rZS13aWR0aD0iMTAiIGZpbGw9Im5vbmUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K" />
<meta name="theme-color" content="#7bb7ff" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="ExecutiveOS" />
<style>
:root {
--ink: #0a0c10;
--panel: #121620;
--panel-2: #181d29;
--line: #1e2535;
--muted: #7a8699;
--text: #e7ecf3;
--accent: #7bb7ff;
--accent-2: #5cf2a0;
--warn: #ffb454;
--danger: #ff5c7a;
--urgent: #ff3d65;
}
* { box-sizing: border-box; }
html, body {
margin: 0; padding: 0;
background: var(--ink); color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", system-ui, sans-serif;
font-size: 14px; line-height: 1.5;
}
.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; }
.muted { color: var(--muted); }
.accent { color: var(--accent); }
.warn { color: var(--warn); }
.danger { color: var(--danger); }
.small { font-size: 12px; }
.r { text-align: right; }
.wrap { flex-wrap: wrap; gap: 12px; }
.spread { justify-content: space-between; align-items: baseline; }
.gap { gap: 12px; }
.stack { display: flex; flex-direction: column; gap: 12px; }
/* Topbar */
.topbar {
display: flex; align-items: center; gap: 24px;
padding: 14px 28px;
border-bottom: 1px solid var(--line);
background: rgba(10, 12, 16, 0.85);
backdrop-filter: blur(8px);
position: sticky; top: 0; z-index: 10;
}
.brand { display: flex; align-items: center; gap: 10px; font-size: 16px; letter-spacing: -0.01em; }
.logo-dot {
width: 10px; height: 10px;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
border-radius: 3px;
display: inline-block;
}
.nav { display: flex; gap: 4px; }
.nav button {
background: transparent; border: 1px solid transparent;
color: var(--muted); padding: 6px 12px; border-radius: 6px;
font-size: 13px; cursor: pointer; transition: 0.15s;
font-family: inherit;
}
.nav button:hover { color: var(--text); }
.nav button.active { background: var(--panel); border-color: var(--line); color: var(--text); }
.user {
margin-left: auto; display: flex; align-items: center; gap: 8px;
color: var(--muted); font-size: 12px;
}
.status-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--accent-2);
box-shadow: 0 0 8px var(--accent-2);
transition: 0.3s;
}
.status-dot.offline {
background: var(--warn);
box-shadow: 0 0 8px var(--warn);
}
.install-btn {
padding: 4px 10px;
font-size: 11px;
}
/* Layout */
main { max-width: 1280px; margin: 0 auto; padding: 32px 28px; }
section { display: flex; flex-direction: column; gap: 20px; }
h1 { font-size: 22px; font-weight: 600; letter-spacing: -0.01em; margin: 0; }
.row { display: flex; align-items: center; }
/* Card */
.card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 12px;
padding: 20px;
}
/* KPI */
.kpi-grid {
display: grid; gap: 16px;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
@media (max-width: 880px) { .kpi-grid { grid-template-columns: repeat(2, 1fr); } }
.kpi { padding: 18px 20px; }
.kpi-label {
font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em;
color: var(--muted);
}
.kpi-value {
font-size: 28px; font-weight: 600; margin-top: 8px;
font-variant-numeric: tabular-nums; letter-spacing: -0.01em;
}
.grid-2-1 {
display: grid; gap: 16px;
grid-template-columns: 2fr 1fr;
}
@media (max-width: 880px) { .grid-2-1 { grid-template-columns: 1fr; } }
/* Briefing */
.briefing {
margin-top: 14px;
white-space: pre-wrap;
line-height: 1.7;
font-size: 14px;
}
.priorities {
margin: 12px 0 0; padding: 0; list-style: none;
display: flex; flex-direction: column; gap: 10px;
}
.priorities li {
background: var(--ink); border: 1px solid var(--line);
border-radius: 8px; padding: 12px;
}
.priorities .sub { display: block; font-size: 12px; color: var(--muted); margin-top: 4px; }
/* Inputs */
input[type="text"], input:not([type]), select {
background: var(--ink); border: 1px solid var(--line); color: var(--text);
padding: 8px 12px; border-radius: 6px; font-size: 13px;
font-family: inherit; outline: none;
}
input:focus, select:focus { border-color: var(--accent); }
input { flex: 1; min-width: 0; }
button.btn-primary {
background: var(--accent); color: var(--ink); border: 0;
padding: 8px 16px; border-radius: 6px; font-weight: 600;
font-size: 13px; cursor: pointer; font-family: inherit;
}
button.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }
button.btn-ghost {
background: transparent; border: 1px solid var(--line); color: var(--text);
padding: 6px 14px; border-radius: 6px; font-size: 13px;
cursor: pointer; font-family: inherit;
}
button.btn-ghost:hover { border-color: var(--accent); color: var(--accent); }
/* Copilot */
.copilot { margin-top: 10px; display: flex; flex-direction: column; gap: 12px; }
.chips { display: flex; gap: 6px; flex-wrap: wrap; }
.chip {
background: transparent; border: 1px solid var(--line); color: var(--muted);
font-size: 11px; padding: 4px 10px; border-radius: 999px;
cursor: pointer; font-family: inherit;
}
.chip:hover { color: var(--accent); border-color: var(--accent); }
.answer {
background: var(--ink); border: 1px solid var(--line);
border-radius: 8px; padding: 14px 16px;
font-size: 13px; line-height: 1.7;
white-space: pre-wrap; display: none;
}
.answer.show { display: block; }
/* Email list */
.email-list { display: flex; flex-direction: column; gap: 8px; }
.email {
background: var(--panel); border: 1px solid var(--line);
border-radius: 10px; padding: 14px 16px;
display: grid; grid-template-columns: 60px 1fr auto; gap: 14px;
align-items: start; cursor: pointer; transition: 0.15s;
}
.email:hover { border-color: var(--accent); }
.email.unread { border-left: 3px solid var(--accent); }
.email.urgent { border-left: 3px solid var(--urgent); }
.email .pri-badge {
display: inline-block; text-align: center;
padding: 4px 8px; font-size: 11px; font-weight: 600;
border-radius: 4px;
background: var(--ink); border: 1px solid var(--line);
}
.pri-P1 { color: var(--urgent); border-color: var(--urgent) !important; }
.pri-P2 { color: var(--warn); border-color: var(--warn) !important; }
.pri-P3 { color: var(--muted); }
.email .meta { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.email .subject { font-weight: 500; }
.email .from { color: var(--muted); font-size: 12px; }
.email .preview { color: var(--muted); font-size: 12px; margin-top: 4px; line-height: 1.5; }
.email .tags { display: flex; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
.tag {
font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em;
padding: 2px 8px; border-radius: 3px;
background: var(--ink); border: 1px solid var(--line); color: var(--muted);
}
.tag.cat-approval { color: var(--warn); border-color: var(--warn); }
.tag.cat-board { color: var(--accent); border-color: var(--accent); }
.tag.cat-escalation, .tag.cat-urgent { color: var(--urgent); border-color: var(--urgent); }
.tag.cat-financial, .tag.cat-legal, .tag.cat-hr { color: var(--accent-2); border-color: var(--accent-2); }
.tag.cat-informational { color: var(--muted); }
.email .actions {
display: flex; flex-direction: column; align-items: flex-end; gap: 6px;
min-width: 140px;
}
.email .age { font-size: 11px; color: var(--muted); font-family: ui-monospace, monospace; }
.email .reco {
font-size: 11px; padding: 4px 10px; border-radius: 999px;
background: var(--ink); border: 1px solid var(--line);
}
.reco-approve { color: var(--accent-2); border-color: var(--accent-2); }
.reco-sign { color: var(--accent); border-color: var(--accent); }
.reco-vote { color: var(--accent); border-color: var(--accent); }
.reco-review { color: var(--warn); border-color: var(--warn); }
.reco-reject { color: var(--danger); border-color: var(--danger); }
.reco-escalate{ color: var(--urgent); border-color: var(--urgent); }
.reco-delegate{ color: var(--muted); }
.reco-ignore { color: var(--muted); }
.score-bar {
width: 100%; height: 4px; background: var(--ink);
border-radius: 2px; margin-top: 6px; overflow: hidden;
}
.score-fill {
height: 100%; background: linear-gradient(90deg, var(--accent), var(--accent-2));
}
/* Approval cards */
.approval-list { display: flex; flex-direction: column; gap: 12px; }
.approval {
background: var(--panel); border: 1px solid var(--line);
border-radius: 12px; padding: 18px 20px;
display: grid; gap: 16px;
grid-template-columns: 1fr auto;
}
@media (max-width: 720px) { .approval { grid-template-columns: 1fr; } }
.approval .head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.approval .subject { font-weight: 600; font-size: 15px; }
.approval .meta-row {
margin-top: 8px; color: var(--muted); font-size: 12px;
display: flex; gap: 14px; flex-wrap: wrap;
}
.approval .summary { margin-top: 12px; font-size: 13px; line-height: 1.6; color: var(--text); }
.approval .ai-box {
margin-top: 12px; padding: 12px 14px;
background: var(--ink); border: 1px solid var(--line);
border-radius: 8px; font-size: 13px;
}
.approval .ai-box strong { color: var(--accent); }
.approval .controls {
display: flex; flex-direction: column; gap: 8px;
min-width: 160px; align-items: stretch;
}
.approval .controls button {
font-family: inherit; font-size: 13px;
padding: 8px 12px; border-radius: 6px; cursor: pointer;
background: var(--ink); border: 1px solid var(--line); color: var(--text);
}
.approval .controls .btn-approve { background: var(--accent-2); color: var(--ink); border: 0; font-weight: 600; }
.approval .controls .btn-reject { color: var(--danger); border-color: var(--danger); }
.approval .controls .btn-delegate{ color: var(--muted); }
.approval .controls .btn-sign { background: var(--accent); color: var(--ink); border: 0; font-weight: 600; }
.approval.resolved { opacity: 0.55; }
.approval .resolved-tag { color: var(--accent-2); font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em; }
/* Tables */
table.data {
width: 100%; border-collapse: collapse; margin-top: 6px; font-size: 13px;
}
table.data th {
color: var(--muted); font-weight: 500;
text-transform: uppercase; font-size: 11px; letter-spacing: 0.08em;
text-align: left; padding: 8px 6px; border-bottom: 1px solid var(--line);
}
table.data td {
padding: 10px 6px; border-bottom: 1px solid var(--line);
font-variant-numeric: tabular-nums;
}
table.data tr:last-child td { border-bottom: 0; }
</style>
</head>
<body>
<header class="topbar">
<div class="brand">
<span class="logo-dot"></span>
<span><strong>ExecutiveOS</strong> <span class="muted">AI</span></span>
</div>
<nav class="nav" id="nav">
<button data-route="briefing" class="active">Брифинг</button>
<button data-route="inbox">Входящие</button>
<button data-route="approvals">Согласования</button>
<button data-route="escalations">Эскалации</button>
<button data-route="search">AI-поиск</button>
</nav>
<div class="user">
<span id="online-dot" class="status-dot"></span>
<span class="mono">ceo@acme.corp</span>
<span class="muted small">· HCL Notes</span>
<button id="btn-install" class="btn-ghost install-btn" hidden>↓ Установить</button>
</div>
</header>
<main id="view"></main>
<!-- Брифинг -->
<template id="tpl-briefing">
<section>
<div class="row spread wrap">
<div>
<h1>Ежедневный брифинг</h1>
<div class="muted small" id="brief-date"></div>
</div>
<div class="row gap">
<button id="btn-refresh" class="btn-ghost">↻ Синхронизация с Lotus</button>
<button id="btn-mark-read" class="btn-ghost">Отметить все просмотренными</button>
</div>
</div>
<div class="kpi-grid">
<div class="card kpi"><div class="kpi-label">Критических непрочитанных</div><div class="kpi-value danger" data-k="critical"></div></div>
<div class="card kpi"><div class="kpi-label">Ожидают решения</div><div class="kpi-value warn" data-k="pending"></div></div>
<div class="card kpi"><div class="kpi-label">К Совету директоров</div><div class="kpi-value" data-k="board"></div></div>
<div class="card kpi"><div class="kpi-label">Нарушений SLA</div><div class="kpi-value danger" data-k="sla"></div></div>
</div>
<div class="grid-2-1">
<div class="card">
<div class="kpi-label">Сводка дня</div>
<div id="brief-summary" class="briefing"></div>
</div>
<div class="card">
<div class="kpi-label">Топ-3 приоритета</div>
<ol id="brief-top3" class="priorities"></ol>
</div>
</div>
<div class="card">
<div class="kpi-label">AI-копилот</div>
<div class="copilot">
<div class="row gap">
<input id="cp-input" placeholder="«Что требует моей подписи?», «Покажи просроченные»…" />
<button id="cp-ask" class="btn-primary">Спросить</button>
</div>
<div id="cp-suggest" class="chips"></div>
<div id="cp-answer" class="answer"></div>
</div>
</div>
</section>
</template>
<!-- Входящие -->
<template id="tpl-inbox">
<section>
<h1>Приоритетные входящие</h1>
<div class="card row gap wrap">
<input id="inbox-search" placeholder="Поиск по теме, отправителю, тексту…" />
<select id="inbox-cat">
<option value="">Все категории</option>
</select>
<select id="inbox-pri">
<option value="">Все приоритеты</option>
<option value="P1">P1 — Критический</option>
<option value="P2">P2 — Высокий</option>
<option value="P3">P3 — Обычный</option>
</select>
<label class="row gap small"><input type="checkbox" id="inbox-unread" /> <span>Только непрочитанные</span></label>
</div>
<div id="inbox-list" class="email-list"></div>
</section>
</template>
<!-- Согласования -->
<template id="tpl-approvals">
<section>
<div class="row spread wrap">
<h1>Ожидают согласования</h1>
<div class="muted small">AI оценил риск и предложил действие. Решение всегда за вами.</div>
</div>
<div id="appr-list" class="approval-list"></div>
</section>
</template>
<!-- Эскалации -->
<template id="tpl-escalations">
<section>
<h1>Монитор эскалаций</h1>
<div class="card">
<table class="data">
<thead><tr>
<th>Возраст</th><th>Тема</th><th>От кого</th><th>Должность</th>
<th class="r">SLA</th><th>Статус</th>
</tr></thead>
<tbody id="esc-rows"></tbody>
</table>
</div>
</section>
</template>
<!-- Поиск -->
<template id="tpl-search">
<section>
<h1>AI-поиск по корпоративной почте</h1>
<div class="card stack">
<div class="row gap">
<input id="srch-input" placeholder="Семантический поиск по письмам, согласованиям, материалам Совета…" />
<button id="srch-go" class="btn-primary">Найти</button>
</div>
<div class="chips" id="srch-suggest"></div>
</div>
<div id="srch-results" class="email-list"></div>
</section>
</template>
<script>
// ExecutiveOS AI — мок-данные (демо интеграции HCL Notes / Lotus Notes)
// Все имена/отправители/суммы вымышлены. Компания: АО «АКМЕ Холдинг».
// "сегодня" = 2026-05-22 для расчета возраста писем и SLA в app.js.
window.MOCK_DATA = {
meta: {
today: "2026-05-22",
user: "ceo@acme.corp",
source: "HCL Notes / Lotus Notes (Domino bridge)",
last_sync: "2026-05-22T08:14:00Z"
},
// Канонические ключи (для логики) → русские подписи (для UI) — в app.js
categories: ["Approval", "Board", "Escalation", "Financial", "Legal", "HR", "Informational", "Urgent"],
priorities: ["P1", "P2", "P3"],
emails: [
{
id: "e01",
received_at: "2026-05-22T07:42:00Z",
from: { name: "Анна Петрова", email: "chairman@acme.corp", title: "Председатель совета директоров" },
subject: "Утверждение CAPEX Q2 — пересмотренный план $42 млн",
preview: "Прошу рассмотреть и подписать пересмотренный план CAPEX до заседания Совета в пятницу. Ключевые изменения: +$8M на AI-инфраструктуру, −$3M из устаревшего ЦОД.",
summary: "Председатель Совета просит подпись CEO на пересмотренном CAPEX Q2 ($42 млн). Главный сдвиг: +$8M в AI-инфра, −$3M из легаси-ЦОД. Привязано к голосованию Совета в пятницу.",
category: "Approval",
priority: "P1",
ai_score: 96,
recommendation: "Sign",
ai_rationale: "Отправитель — Председатель Совета (высшая инстанция). Тема пересекается с голосованием через 3 дня. Риск: срыв дедлайна срывает закрытие Q2-бюджета. Исторически подобные пересмотры CAPEX одобряются в 9 случаях из 10.",
action_required: true,
unread: true,
age_days: 0,
attachments: 2,
sla_hours: 48
},
{
id: "e02",
received_at: "2026-05-21T18:11:00Z",
from: { name: "Маркус Лян", email: "cfo@acme.corp", title: "Финансовый директор (CFO)" },
subject: "Резерв непредвиденных расходов FY26 — на подпись",
preview: "Финансы закрыли резерв на уровне 3,2% от opex. Требуется электронная подпись на приложенном меморандуме до конца квартала.",
summary: "CFO подтверждает резерв FY26 на уровне 3,2% от opex ($14,1 млн). Стандартная подпись меморандума, изменений политики vs прошлого года нет.",
category: "Financial",
priority: "P2",
ai_score: 82,
recommendation: "Sign",
ai_rationale: "Рутинный финансовый контроль. Соответствует прошлогоднему паттерну. Низкий операционный риск. Рекомендация: подписать.",
action_required: true,
unread: true,
age_days: 1,
attachments: 1,
sla_hours: 72
},
{
id: "e03",
received_at: "2026-05-21T14:30:00Z",
from: { name: "Юридический департамент", email: "legal@acme.corp", title: "Главный юрист" },
subject: "Продление контракта с TechCo MSA — $1,2 млн/год",
preview: "Продление MSA с TechCo. Лимит ответственности снижен с 12 до 6 месяцев платежей. Просьба рассмотреть или делегировать.",
summary: "Legal эскалирует продление MSA с TechCo. Особенность: лимит ответственности уменьшен в 2 раза. Стоимость контракта $1,2 млн/год.",
category: "Legal",
priority: "P2",
ai_score: 74,
recommendation: "Review",
ai_rationale: "Условия ответственности существенно слабее предыдущего MSA. Ниже обязательного порога ревью CEO ($5 млн), но нестандартное изменение требует внимания.",
action_required: true,
unread: true,
age_days: 1,
attachments: 3,
sla_hours: 96
},
{
id: "e04",
received_at: "2026-05-20T22:05:00Z",
from: { name: "SecOps Alert", email: "secops@acme.corp", title: "Служба безопасности" },
subject: "ИНЦИДЕНТ P1 — Подозрительный вход админа с внешнего IP",
preview: "Привилегированный аккаунт 'svc-domino-admin' аутентифицировался с не-VPN IP 185.x.x.x в 22:04 UTC. Учётные данные автоматически ротированы. Ожидаем решение об эскалации.",
summary: "SecOps зафиксировал подозрительный привилегированный вход. Учётные данные авторотированы. Утечки данных не зафиксировано. SecOps просит CEO эскалировать к CISO + уведомить аудит-комитет Совета.",
category: "Escalation",
priority: "P1",
ai_score: 99,
recommendation: "Escalate",
ai_rationale: "Паттерн компрометации привилегированной учётки. Вероятно сработал порог обязательного раскрытия. Рекомендация: немедленная эскалация к CISO и предварительное уведомление аудит-комитета.",
action_required: true,
unread: true,
age_days: 2,
attachments: 1,
sla_hours: 4
},
{
id: "e05",
received_at: "2026-05-20T11:30:00Z",
from: { name: "Сара Ким", email: "chro@acme.corp", title: "Директор по персоналу (CHRO)" },
subject: "Промоушены до уровня директор — 3 кандидата",
preview: "Три промоушена до директора в Q2. Все прошли Совет по талантам. Требуется финальное одобрение CEO.",
summary: "CHRO просит подпись по 3 промоушенам директоров. Все предварительно одобрены Советом по талантам. Годовое влияние на ФОТ +$420K.",
category: "HR",
priority: "P2",
ai_score: 71,
recommendation: "Approve",
ai_rationale: "Стандартный пакет промоушенов. Совет по талантам уже одобрил. Стоимость в пределах Q2-плана. Исторический процент одобрения CEO предодобренных промоушенов: 94%.",
action_required: true,
unread: false,
age_days: 2,
attachments: 1,
sla_hours: 120
},
{
id: "e06",
received_at: "2026-05-19T16:45:00Z",
from: { name: "Секретариат Совета", email: "board-sec@acme.corp", title: "Секретариат Совета директоров" },
subject: "Заседание Совета 24 мая — материалы pre-read (4 документа)",
preview: "Pre-read: обновление стратегии Q2, M&A-pipeline, риск-дашборд, ESG-отчет. Просьба ознакомиться до сессии 09:00 в пятницу.",
summary: "Пакет материалов к заседанию Совета 24 мая. 4 документа: стратегия Q2, M&A-pipeline, риск-дашборд, ESG. Всего ~62 страницы. AI-сводка приложена.",
category: "Board",
priority: "P2",
ai_score: 88,
recommendation: "Review",
ai_rationale: "Стандартный pre-read Совета. Сгенерирована автоматическая сводка. M&A-pipeline содержит 1 материальный пункт, требующий знакомства CEO до голосования.",
action_required: true,
unread: true,
age_days: 3,
attachments: 4,
sla_hours: 96
},
{
id: "e07",
received_at: "2026-05-19T09:00:00Z",
from: { name: "Investor Relations", email: "ir@acme.corp", title: "Глава IR" },
subject: "Сценарий звонка с инвесторами — финальный драфт",
preview: "Финальный сценарий звонка 28 мая. Тезисы согласованы с CFO. Просьба проверить вступительное слово.",
summary: "Сценарий звонка с инвесторами v3. Вступление акцентирует ROI AI-инфраструктуры. Согласовано с CFO. Два варианта Q&A ожидают предпочтения CEO.",
category: "Financial",
priority: "P2",
ai_score: 78,
recommendation: "Review",
ai_rationale: "Публичный материал. Две формулировки про AI ROI — рекомендую консервативный вариант, проверенный юристами, учитывая предыдущий запрос SEC.",
action_required: true,
unread: false,
age_days: 3,
attachments: 1,
sla_hours: 144
},
{
id: "e08",
received_at: "2026-05-18T20:11:00Z",
from: { name: "Внутренний аудит", email: "audit@acme.corp", title: "Глава внутреннего аудита" },
subject: "Аудит — нарушение разделения полномочий (Закупки)",
preview: "Аудит выявил 2 случая, когда один и тот же пользователь утверждал PO и проводил оплату. План устранения приложен.",
summary: "Внутренний аудит поднимает разрыв SoD в Закупках (2 случая). План устранения готов. Рекомендуется уведомление Аудит-комитета.",
category: "Escalation",
priority: "P2",
ai_score: 84,
recommendation: "Escalate",
ai_rationale: "Находка по SoD имеет комплаенс-последствия. Рекомендую эскалировать в Аудит-комитет на следующем заседании. План устранения уже подготовлен.",
action_required: true,
unread: true,
age_days: 4,
attachments: 2,
sla_hours: 120
},
{
id: "e09",
received_at: "2026-05-18T13:22:00Z",
from: { name: "Команда M&A", email: "ma@acme.corp", title: "Глава Corporate Development" },
subject: "Проект «Сокол» — term sheet, требуется голосование",
preview: "Term sheet по проекту «Сокол» (цель — NorthernCloud). Голосование Совета 24 мая. Просьба предварительного одобрения.",
summary: "Команда M&A подаёт term sheet по NorthernCloud ($380 млн). Стратегическое соответствие: расширение AI-инфраструктуры. Due diligence выявил 1 IP-замечание (mitigated).",
category: "Board",
priority: "P1",
ai_score: 92,
recommendation: "Vote",
ai_rationale: "Материальная M&A-сделка. Требуется голосование Совета. Предварительное одобрение CEO сигнализирует о поддержке. IP-вопрос отмечен в DD; юридическое заключение приложено.",
action_required: true,
unread: true,
age_days: 4,
attachments: 5,
sla_hours: 72
},
{
id: "e10",
received_at: "2026-05-17T08:00:00Z",
from: { name: "Операционный директор", email: "coo@acme.corp", title: "COO" },
subject: "Сбой в EU-регионе — драфт post-mortem",
preview: "Post-mortem сбоя в EU-регионе 14 мая. RCA: неверно настроенный failover. План действий: 4 направления. FYI.",
summary: "COO делится post-mortem сбоя в EU 14 мая. Простой 47 мин, затронуло 12K пользователей. Причина: misconfig failover. 4 направления устранения.",
category: "Informational",
priority: "P3",
ai_score: 38,
recommendation: "Ignore",
ai_rationale: "Информационное. Owner — COO. Действия CEO не требуются, если только Risk-комитет Совета не эскалирует. Помечено FYI.",
action_required: false,
unread: false,
age_days: 5,
attachments: 1,
sla_hours: null
},
{
id: "e11",
received_at: "2026-05-17T07:14:00Z",
from: { name: "Анна Петрова", email: "chairman@acme.corp", title: "Председатель совета директоров" },
subject: "Re: Стратегическая сессия — даты в июне",
preview: "Подтверждаю 18–19 июня для стратегической сессии. Прошу ответить со списком участников исполкома.",
summary: "Председатель подтверждает даты стратегической сессии (18–19 июня). Ждёт подтверждения CEO + список участников.",
category: "Approval",
priority: "P2",
ai_score: 75,
recommendation: "Approve",
ai_rationale: "Подтверждение календаря от Председателя. VIP-отправитель. Быстрый ответ усиливает сигнал доверия. Черновик ответа готов.",
action_required: true,
unread: false,
age_days: 5,
attachments: 0,
sla_hours: 48
},
{
id: "e12",
received_at: "2026-05-16T19:33:00Z",
from: { name: "Маркетинг", email: "cmo@acme.corp", title: "CMO" },
subject: "Бренд-кампания — финальное согласование креатива",
preview: "Финальный креатив для бренд-кампании Q3. Просьба проверить приложенное видео + 4 print-варианта.",
summary: "CMO просит подпись по креативу бренд-кампании Q3. Соответствует бренд-гайдлайнам. Согласовано с командой Comms.",
category: "Approval",
priority: "P3",
ai_score: 58,
recommendation: "Delegate",
ai_rationale: "Операционное маркетинговое решение. Рекомендую делегировать CMO + VP Comms, если у CEO нет стратегических предпочтений.",
action_required: true,
unread: true,
age_days: 6,
attachments: 5,
sla_hours: 168
},
{
id: "e13",
received_at: "2026-05-15T12:00:00Z",
from: { name: "ESG-комитет", email: "esg@acme.corp", title: "Глава ESG" },
subject: "Годовой отчет по устойчивому развитию — на подпись",
preview: "Годовой ESG-отчет приложен. Требуется подпись руководства перед публичной подачей.",
summary: "ESG-отчет готов к публичной подаче. Проверен Legal + ESG-комитетом. Требуется подпись CEO на сопроводительном письме.",
category: "Approval",
priority: "P2",
ai_score: 76,
recommendation: "Sign",
ai_rationale: "Годовая публикация. Стандартный процесс. Все upstream-проверки завершены. Дедлайн подачи — 31 мая.",
action_required: true,
unread: false,
age_days: 7,
attachments: 1,
sla_hours: 96
},
{
id: "e14",
received_at: "2026-05-14T10:15:00Z",
from: { name: "HR Operations", email: "hr-ops@acme.corp", title: "Директор HR" },
subject: "Whistleblower-обращение — чувствительное (HR-2026-0042)",
preview: "Конфиденциальное обращение по анонимному каналу. Запущено внутреннее расследование. Брифинг для CEO eyes-only.",
summary: "Конфиденциальный whistleblower-кейс (HR-2026-0042). Расследуют HR + Legal. На этом этапе — только процедурное уведомление.",
category: "Escalation",
priority: "P1",
ai_score: 95,
recommendation: "Escalate",
ai_rationale: "Чувствительный HR-вопрос. Рекомендую конфиденциальный брифинг с Главным юристом и Главой HR. Возможно уведомление Аудит-комитета в зависимости от масштаба.",
action_required: true,
unread: true,
age_days: 8,
attachments: 0,
sla_hours: 48
},
{
id: "e15",
received_at: "2026-05-14T09:00:00Z",
from: { name: "Благотворительный фонд", email: "donations@external.org", title: "Директор" },
subject: "Партнёрство — спонсорство ежегодного гала",
preview: "Предложение спонсорства гала-вечера. Уровни от $25K до $200K. Возможность для бренда.",
summary: "Внешний питч благотворительного партнёрства. Уровни $25K–$200K. Бренд-соответствие умеренное.",
category: "Informational",
priority: "P3",
ai_score: 22,
recommendation: "Delegate",
ai_rationale: "Внешнее обращение. Ниже порога CEO. Передать в Comms или Фонд.",
action_required: false,
unread: false,
age_days: 8,
attachments: 1,
sla_hours: null
},
{
id: "e16",
received_at: "2026-05-12T15:00:00Z",
from: { name: "Риск-комитет", email: "risk@acme.corp", title: "Директор по рискам" },
subject: "Обновление реестра рисков за квартал",
preview: "Реестр рисков Q2 обновлён. Два новых «красных» пункта: цепочка поставок (полупроводники) и регуляторика (EU AI Act).",
summary: "Реестр рисков Q2: два новых «красных» пункта — риск полупроводниковой цепочки, комплаенс EU AI Act. Назначены ответственные за митигацию.",
category: "Board",
priority: "P2",
ai_score: 83,
recommendation: "Review",
ai_rationale: "Существенные изменения в реестре рисков. Стоит ознакомиться CEO до риск-сессии Совета на следующей неделе.",
action_required: true,
unread: false,
age_days: 10,
attachments: 1,
sla_hours: 120
},
{
id: "e17",
received_at: "2026-05-10T08:45:00Z",
from: { name: "Speaker Bureau", email: "bookings@speakerbureau.com", title: "Букинг-агент" },
subject: "Приглашение с keynote — TechSummit Берлин, окт 2026",
preview: "Приглашение с keynote. Гонорар предложен. Тема: «AI-native enterprises». Аудитория: ~2000 руководителей.",
summary: "Внешнее приглашение к выступлению. Берлин, октябрь 2026. Соответствует thought leadership.",
category: "Informational",
priority: "P3",
ai_score: 45,
recommendation: "Delegate",
ai_rationale: "Внешнее приглашение. Передать в Comms для оценки. Срочного решения не требуется.",
action_required: false,
unread: false,
age_days: 12,
attachments: 0,
sla_hours: null
},
{
id: "e18",
received_at: "2026-05-08T16:30:00Z",
from: { name: "Регулятор (Еврокомиссия)", email: "ec-comm@eu.regulator", title: "Официальное уведомление" },
subject: "Информационный запрос — соответствие EU AI Act (статья 13)",
preview: "Формальный запрос информации по статье 13 EU AI Act. Дедлайн ответа: 5 июня 2026.",
summary: "Формальный запрос Еврокомиссии по EU AI Act, ст. 13. Жёсткий дедлайн 5 июня. Legal + Compliance готовят ответ.",
category: "Legal",
priority: "P1",
ai_score: 97,
recommendation: "Escalate",
ai_rationale: "Формальный регуляторный запрос с законодательным дедлайном. Рекомендую: CEO в курсе, Legal ведёт, уведомить Совет. Штрафные риски значительные.",
action_required: true,
unread: true,
age_days: 14,
attachments: 2,
sla_hours: 96
}
],
briefing_text:
"Доброе утро. Сегодня 22 мая 2026 г.\n\n" +
"В работе 18 писем за последние 14 дней. AI-приоритизация выделила:\n\n" +
"• 5 критических (P1) — включая инцидент безопасности SecOps (svc-domino-admin) и формальный запрос регулятора EU AI Act с дедлайном 5 июня.\n" +
"• 4 ожидают подписи — CAPEX Q2 ($42 млн), бюджетный резерв FY26, ESG-отчёт, продление MSA TechCo.\n" +
"• 3 материала к заседанию Совета 24 мая (M&A «Сокол», риск-регистр, ESG).\n\n" +
"Рекомендую начать день с инцидента SecOps (P1, окно SLA <4 ч) и сразу подписать CAPEX-меморандум Председателя — голосование Совета через 2 дня."
};
</script>
<script>
// ExecutiveOS AI — vanilla JS SPA (RU UI)
(function () {
"use strict";
// Локализация ключей → подписи UI
const CATEGORY_LABEL = {
Approval: "Согласование",
Board: "Совет директоров",
Escalation: "Эскалация",
Financial: "Финансы",
Legal: "Правовое",
HR: "Кадры",
Informational: "Инфо",
Urgent: "Срочное"
};
const RECO_LABEL = {
Sign: "Подписать",
Approve: "Согласовать",
Vote: "Голосовать",
Review: "Проверить",
Reject: "Отклонить",
Escalate: "Эскалировать",
Delegate: "Делегировать",
Ignore: "Игнорировать"
};
const PRI_LABEL = {
P1: "P1 · Критический",
P2: "P2 · Высокий",
P3: "P3 · Обычный"
};
const RESOLVED_LABEL = {
"Signed": "Подписано",
"Approved": "Согласовано",
"Rejected": "Отклонено",
"Delegated": "Делегировано",
"Escalated": "Эскалировано",
"Ignored": "Проигнорировано",
"Reviewed": "Просмотрено",
"Voted FOR": "Голос ЗА",
"Voted AGAINST": "Голос ПРОТИВ",
"Done": "Готово"
};
const STATE = {
route: "briefing",
resolved: {} // локальное состояние, без backend
};
const D = window.MOCK_DATA;
// ---------- Утилиты ----------
function $(sel, root = document) { return root.querySelector(sel); }
function $$(sel, root = document) { return Array.from(root.querySelectorAll(sel)); }
function fmtAge(days) {
if (days <= 0) return "сегодня";
if (days === 1) return "1 день назад";
if (days >= 2 && days <= 4) return `${days} дня назад`;
return `${days} дн. назад`;
}
function slaState(email) {
if (!email.sla_hours) return { label: "—", state: "ok" };
const hoursElapsed = email.age_days * 24;
const remaining = email.sla_hours - hoursElapsed;
if (remaining < 0) return { label: `просрочен на ${Math.abs(remaining).toFixed(0)} ч`, state: "breach" };
if (remaining < 12) return { label: `< ${remaining.toFixed(0)} ч`, state: "near" };
return { label: `${remaining.toFixed(0)} ч`, state: "ok" };
}
function mount(name) {
const tpl = document.getElementById("tpl-" + name);
const view = document.getElementById("view");
view.innerHTML = "";
if (tpl) view.appendChild(tpl.content.cloneNode(true));
}
function navigate(route) {
STATE.route = route;
$$("#nav button").forEach(b => b.classList.toggle("active", b.dataset.route === route));
location.hash = route;
if (route === "briefing") renderBriefing();
else if (route === "inbox") renderInbox();
else if (route === "approvals") renderApprovals();
else if (route === "escalations") renderEscalations();
else if (route === "search") renderSearch();
}
// ---------- Брифинг ----------
function renderBriefing() {
mount("briefing");
const dateEl = $("#brief-date");
dateEl.textContent =
`${D.meta.today} · последняя синхронизация ${new Date(D.meta.last_sync).toLocaleString("ru-RU")} · источник: ${D.meta.source}`;
const emails = D.emails;
const critical = emails.filter(e => e.priority === "P1" && e.unread).length;
const pending = emails.filter(e => e.action_required && !STATE.resolved[e.id]).length;
const boardItems = emails.filter(e => e.category === "Board").length;
const slaBreaches = emails.filter(e => slaState(e).state === "breach").length;
setKpi("critical", critical);
setKpi("pending", pending);
setKpi("board", boardItems);
setKpi("sla", slaBreaches);
$("#brief-summary").textContent = D.briefing_text;
const top3 = emails
.filter(e => e.action_required && !STATE.resolved[e.id])
.sort((a, b) => b.ai_score - a.ai_score)
.slice(0, 3);
const ol = $("#brief-top3");
top3.forEach(e => {
const li = document.createElement("li");
li.innerHTML = `
<div><strong>${escapeHtml(e.subject)}</strong></div>
<span class="sub">
<span class="pri-${e.priority}" style="font-weight:600">${PRI_LABEL[e.priority]}</span> ·
${escapeHtml(e.from.name)} (${escapeHtml(e.from.title)}) ·
AI-балл ${e.ai_score} · рекомендация: <strong>${RECO_LABEL[e.recommendation]}</strong>
</span>
`;
ol.appendChild(li);
});
$("#btn-refresh").onclick = () => {
$("#btn-refresh").textContent = "Синхронизация…";
setTimeout(() => navigate("briefing"), 400);
};
$("#btn-mark-read").onclick = () => {
D.emails.forEach(e => e.unread = false);
navigate("briefing");
};
initCopilot();
}
function setKpi(key, value) {
const el = $(`[data-k="${key}"]`);
if (el) el.textContent = value;
}
// ---------- AI-копилот ----------
const CP_SUGGESTIONS = [
"Что требует моей подписи?",
"Что просрочено?",
"Покажи критические инциденты",
"Что готовить к заседанию Совета?",
"Что можно делегировать?"
];
function initCopilot() {
const sug = $("#cp-suggest");
CP_SUGGESTIONS.forEach(q => {
const b = document.createElement("button");
b.className = "chip"; b.textContent = q;
b.onclick = () => askCopilot(q);
sug.appendChild(b);
});
$("#cp-ask").onclick = () => {
const v = $("#cp-input").value.trim();
if (v) askCopilot(v);
};
$("#cp-input").addEventListener("keydown", e => {
if (e.key === "Enter") $("#cp-ask").click();
});
}
function askCopilot(question) {
const q = question.toLowerCase();
const out = $("#cp-answer");
const items = D.emails;
const parts = [];
const matchSign = /(подпис|sign|подпишу|sign-off)/i.test(q);
const matchOverdue = /(просроч|overdue|опазд|дедлайн)/i.test(q);
const matchUrgent = /(критич|срочн|urgent|p1|инцидент|p-?1)/i.test(q);
const matchBoard = /(совет|board|заседан|комитет)/i.test(q);
const matchDelegate = /(делегир|delegate|раздать|передать)/i.test(q);
const matchPriority = /(приоритет|priorit|важн|top|топ)/i.test(q);
if (matchSign) {
const sign = items.filter(e => e.recommendation === "Sign" && !STATE.resolved[e.id]);
parts.push(formatList("✍️ Требуют подписи:", sign));
}
if (matchOverdue) {
const over = items.filter(e => slaState(e).state === "breach");
parts.push(formatList("⏰ Просрочены по SLA:", over));
}
if (matchUrgent) {
const urg = items.filter(e => e.priority === "P1");
parts.push(formatList("🚨 Критические (P1):", urg));
}
if (matchBoard) {
const brd = items.filter(e => e.category === "Board");
parts.push(formatList("🏛 К заседанию Совета:", brd));
}
if (matchDelegate) {
const del = items.filter(e => e.recommendation === "Delegate");
parts.push(formatList("↪ Можно делегировать:", del));
}
if (matchPriority || parts.length === 0) {
const top = items
.filter(e => e.action_required && !STATE.resolved[e.id])
.sort((a, b) => b.ai_score - a.ai_score)
.slice(0, 5);
parts.push(formatList("🎯 Топ-5 по AI-баллу:", top));
}
out.textContent = parts.join("\n\n");
out.classList.add("show");
}
function formatList(header, items) {
if (items.length === 0) return header + "\n (пусто)";
const lines = items.map(e => {
const sla = slaState(e);
return ` • [${e.priority}] ${e.subject}\n от ${e.from.name} (${e.from.title}) · ${fmtAge(e.age_days)} · SLA ${sla.label} · → ${RECO_LABEL[e.recommendation]}`;
});
return header + "\n" + lines.join("\n");
}
// ---------- Входящие ----------
function renderInbox() {
mount("inbox");
const catSel = $("#inbox-cat");
D.categories.forEach(c => {
const o = document.createElement("option");
o.value = c; o.textContent = CATEGORY_LABEL[c] || c;
catSel.appendChild(o);
});
const draw = () => {
const q = $("#inbox-search").value.toLowerCase().trim();
const cat = catSel.value;
const pri = $("#inbox-pri").value;
const unreadOnly = $("#inbox-unread").checked;
const list = $("#inbox-list");
list.innerHTML = "";
const filtered = D.emails
.filter(e => !q ||
e.subject.toLowerCase().includes(q) ||
e.from.name.toLowerCase().includes(q) ||
(e.preview || "").toLowerCase().includes(q))
.filter(e => !cat || e.category === cat)
.filter(e => !pri || e.priority === pri)
.filter(e => !unreadOnly || e.unread)
.sort((a, b) => b.ai_score - a.ai_score);
if (filtered.length === 0) {
list.innerHTML = `<div class="card muted small" style="text-align:center;padding:24px">Ничего не найдено.</div>`;
return;
}
filtered.forEach(e => list.appendChild(renderEmailCard(e)));
};
$("#inbox-search").addEventListener("input", draw);
catSel.addEventListener("change", draw);
$("#inbox-pri").addEventListener("change", draw);
$("#inbox-unread").addEventListener("change", draw);
draw();
}
function renderEmailCard(e) {
const div = document.createElement("div");
div.className = `email${e.unread ? " unread" : ""}${e.priority === "P1" ? " urgent" : ""}`;
const cat = e.category.toLowerCase();
const sla = slaState(e);
const slaColor = sla.state === "breach" ? "danger" : sla.state === "near" ? "warn" : "muted";
div.innerHTML = `
<div>
<span class="pri-badge pri-${e.priority}">${e.priority}</span>
<div class="score-bar" title="AI-балл ${e.ai_score}"><div class="score-fill" style="width:${e.ai_score}%"></div></div>
<div class="muted small mono r" style="margin-top:4px">${e.ai_score}</div>
</div>
<div class="meta">
<div class="subject">${escapeHtml(e.subject)}</div>
<div class="from">${escapeHtml(e.from.name)} · ${escapeHtml(e.from.title)} · ${escapeHtml(e.from.email)}</div>
<div class="preview">${escapeHtml(e.preview)}</div>
<div class="tags">
<span class="tag cat-${cat}">${CATEGORY_LABEL[e.category] || e.category}</span>
${e.attachments ? `<span class="tag">📎 ${e.attachments}</span>` : ""}
${e.unread ? `<span class="tag" style="color:var(--accent);border-color:var(--accent)">новое</span>` : ""}
</div>
</div>
<div class="actions">
<div class="age">${fmtAge(e.age_days)}</div>
${e.sla_hours ? `<div class="age ${slaColor}">SLA ${sla.label}</div>` : ""}
<div class="reco reco-${e.recommendation.toLowerCase()}">→ ${RECO_LABEL[e.recommendation]}</div>
</div>
`;
div.onclick = () => alert(
`AI-обоснование:\n\n${e.ai_rationale}\n\n— Сводка —\n${e.summary}`
);
return div;
}
// ---------- Согласования ----------
function renderApprovals() {
mount("approvals");
const list = $("#appr-list");
list.innerHTML = "";
const items = D.emails
.filter(e => e.action_required &&
["Sign", "Approve", "Vote", "Review", "Reject", "Escalate", "Delegate"].includes(e.recommendation))
.sort((a, b) => b.ai_score - a.ai_score);
items.forEach(e => {
const resolved = STATE.resolved[e.id];
const card = document.createElement("div");
card.className = "approval" + (resolved ? " resolved" : "");
const sla = slaState(e);
const slaClass = sla.state === "breach" ? "danger" : sla.state === "near" ? "warn" : "muted";
card.innerHTML = `
<div>
<div class="head">
<span class="pri-badge pri-${e.priority}">${e.priority}</span>
<span class="tag cat-${e.category.toLowerCase()}">${CATEGORY_LABEL[e.category] || e.category}</span>
${resolved ? `<span class="resolved-tag">✓ ${RESOLVED_LABEL[resolved] || resolved}</span>` : ""}
</div>
<div class="subject" style="margin-top:10px">${escapeHtml(e.subject)}</div>
<div class="meta-row">
<span>от <strong>${escapeHtml(e.from.name)}</strong> (${escapeHtml(e.from.title)})</span>
<span>${fmtAge(e.age_days)}</span>
${e.sla_hours ? `<span class="${slaClass}">SLA ${sla.label}</span>` : ""}
${e.attachments ? `<span>📎 вложений: ${e.attachments}</span>` : ""}
</div>
<div class="summary">${escapeHtml(e.summary)}</div>
<div class="ai-box">
<strong>AI · балл ${e.ai_score}/100 · → ${RECO_LABEL[e.recommendation]}</strong><br/>
<span class="muted small">${escapeHtml(e.ai_rationale)}</span>
</div>
</div>
<div class="controls">
${approvalButtons(e)}
</div>
`;
card.querySelectorAll("button[data-act]").forEach(b => {
b.onclick = () => {
STATE.resolved[e.id] = b.dataset.act;
renderApprovals();
};
});
list.appendChild(card);
});
if (items.length === 0) {
list.innerHTML = `<div class="card muted small" style="text-align:center;padding:24px">Открытых решений нет.</div>`;
}
}
function approvalButtons(e) {
const r = e.recommendation;
if (r === "Sign") return `<button class="btn-sign" data-act="Signed">✍️ Подписать</button><button data-act="Delegated" class="btn-delegate">Делегировать</button>`;
if (r === "Approve") return `<button class="btn-approve" data-act="Approved">✓ Согласовать</button><button data-act="Rejected" class="btn-reject">Отклонить</button><button data-act="Delegated" class="btn-delegate">Делегировать</button>`;
if (r === "Vote") return `<button class="btn-approve" data-act="Voted FOR">Голос ЗА</button><button data-act="Voted AGAINST" class="btn-reject">Голос ПРОТИВ</button>`;
if (r === "Review") return `<button class="btn-approve" data-act="Reviewed">✓ Просмотрено</button><button data-act="Delegated" class="btn-delegate">Делегировать</button>`;
if (r === "Escalate") return `<button class="btn-reject" data-act="Escalated">↑ Эскалировать</button><button data-act="Ignored" class="btn-delegate">Игнорировать</button>`;
if (r === "Delegate") return `<button data-act="Delegated" class="btn-delegate">↪ Делегировать</button><button class="btn-approve" data-act="Approved">Решить самому</button>`;
return `<button data-act="Done" class="btn-approve">Готово</button>`;
}
// ---------- Эскалации ----------
function renderEscalations() {
mount("escalations");
const rows = D.emails
.filter(e => e.category === "Escalation" || e.priority === "P1")
.sort((a, b) => b.age_days - a.age_days);
const tbody = $("#esc-rows");
rows.forEach(e => {
const sla = slaState(e);
const slaCls = sla.state === "breach" ? "danger" : sla.state === "near" ? "warn" : "muted";
const tr = document.createElement("tr");
const status = STATE.resolved[e.id]
? `<span class="accent">${RESOLVED_LABEL[STATE.resolved[e.id]] || STATE.resolved[e.id]}</span>`
: `<span class="${e.priority === 'P1' ? 'danger' : 'warn'}">${RECO_LABEL[e.recommendation]}</span>`;
tr.innerHTML = `
<td class="mono">${fmtAge(e.age_days)}</td>
<td>${escapeHtml(e.subject)}</td>
<td class="muted small">${escapeHtml(e.from.name)}</td>
<td>${escapeHtml(e.from.title)}</td>
<td class="r ${slaCls}">${sla.label}</td>
<td>${status}</td>
`;
tbody.appendChild(tr);
});
if (rows.length === 0) {
tbody.innerHTML = `<tr><td colspan="6" class="muted small" style="text-align:center;padding:24px">Открытых эскалаций нет.</td></tr>`;
}
}
// ---------- Поиск ----------
function renderSearch() {
mount("search");
const sug = $("#srch-suggest");
["бюджет", "EU AI Act", "Совет", "Председатель", "аудит", "Сокол", "SecOps", "ESG"].forEach(s => {
const b = document.createElement("button");
b.className = "chip"; b.textContent = s;
b.onclick = () => { $("#srch-input").value = s; doSearch(); };
sug.appendChild(b);
});
$("#srch-go").onclick = doSearch;
$("#srch-input").addEventListener("keydown", e => { if (e.key === "Enter") doSearch(); });
}
function doSearch() {
const q = $("#srch-input").value.toLowerCase().trim();
const out = $("#srch-results");
out.innerHTML = "";
if (!q) return;
const hay = (e) =>
(e.subject + " " + e.preview + " " + e.summary + " " +
e.from.name + " " + e.from.title + " " +
(CATEGORY_LABEL[e.category] || "") + " " + e.category).toLowerCase();
const results = D.emails.filter(e => hay(e).includes(q));
if (results.length === 0) {
out.innerHTML = `<div class="card muted small" style="text-align:center;padding:24px">Совпадений нет.</div>`;
return;
}
results.forEach(e => out.appendChild(renderEmailCard(e)));
}
// ---------- Helpers ----------
function escapeHtml(s) {
if (s == null) return "";
return String(s)
.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
.replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}
// ---------- Router ----------
document.getElementById("nav").addEventListener("click", (e) => {
const r = e.target.dataset.route;
if (r) navigate(r);
});
window.addEventListener("hashchange", () => {
const r = location.hash.replace("#", "") || "briefing";
if (r !== STATE.route) navigate(r);
});
const start = location.hash.replace("#", "") || "briefing";
navigate(start);
})();
</script>
<script>
// ---- PWA: регистрация Service Worker ----
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("./sw.js")
.then(reg => console.log("[PWA] SW registered:", reg.scope))
.catch(err => console.error("[PWA] SW error:", err));
});
}
// ---- Install prompt ----
let deferredPrompt = null;
const installBtn = document.getElementById("btn-install");
window.addEventListener("beforeinstallprompt", (e) => {
e.preventDefault();
deferredPrompt = e;
installBtn.hidden = false;
});
installBtn.addEventListener("click", async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log("[PWA] install outcome:", outcome);
deferredPrompt = null;
installBtn.hidden = true;
});
window.addEventListener("appinstalled", () => {
installBtn.hidden = true;
console.log("[PWA] installed");
});
// ---- Online/offline индикатор ----
const dot = document.getElementById("online-dot");
function updateOnline() {
const online = navigator.onLine;
dot.classList.toggle("offline", !online);
dot.title = online ? "Online — Lotus синхронизирован" : "Offline — данные из кэша";
}
window.addEventListener("online", updateOnline);
window.addEventListener("offline", updateOnline);
updateOnline();
</script>
</body>
</html>