← Back
<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>KZ Channels — Dashboard</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background: #0f1117;
      color: #e4e4e7;
      min-height: 100vh;
    }
    .header {
      background: linear-gradient(135deg, #1a1b2e 0%, #16213e 100%);
      border-bottom: 1px solid #2a2d3e;
      padding: 24px 32px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .header h1 { font-size: 24px; font-weight: 700; }
    .header h1 span { color: #00bcd4; }
    .header .status { display: flex; align-items: center; gap: 8px; font-size: 14px; color: #9ca3af; }
    .header .status .dot { width: 8px; height: 8px; border-radius: 50%; background: #22c55e; animation: pulse 2s infinite; }
    @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }

    .container { max-width: 1200px; margin: 0 auto; padding: 32px 24px; }

    /* KPI Cards */
    .kpi-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 16px;
      margin-bottom: 32px;
    }
    .kpi-card {
      background: #1a1b2e;
      border: 1px solid #2a2d3e;
      border-radius: 12px;
      padding: 20px;
    }
    .kpi-card .label { font-size: 12px; color: #9ca3af; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
    .kpi-card .value { font-size: 28px; font-weight: 700; color: #fff; }
    .kpi-card .sub { font-size: 12px; color: #6b7280; margin-top: 4px; }

    /* Channels */
    .section-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; color: #fff; }
    .channels-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
      gap: 16px;
      margin-bottom: 32px;
    }
    .channel-card {
      background: #1a1b2e;
      border: 1px solid #2a2d3e;
      border-radius: 12px;
      padding: 20px;
      transition: border-color 0.2s;
    }
    .channel-card:hover { border-color: #00bcd4; }
    .channel-card .ch-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
    .channel-card .ch-name { font-size: 16px; font-weight: 600; }
    .channel-card .ch-type {
      font-size: 11px;
      padding: 3px 8px;
      border-radius: 6px;
      font-weight: 500;
    }
    .type-news { background: #1e3a5f; color: #60a5fa; }
    .type-humor { background: #3b2f1e; color: #fbbf24; }
    .type-earnings { background: #1e3b2f; color: #34d399; }
    .type-sports { background: #3b1e2f; color: #f472b6; }
    .channel-card .ch-stats { display: flex; gap: 20px; margin-bottom: 12px; }
    .channel-card .ch-stat { }
    .channel-card .ch-stat .num { font-size: 18px; font-weight: 600; color: #fff; }
    .channel-card .ch-stat .lbl { font-size: 11px; color: #9ca3af; }
    /* Recent posts */
    .posts-channel { margin-bottom: 16px; }
    .posts-channel-name { font-size: 13px; font-weight: 600; color: #00bcd4; margin-bottom: 8px; }
    .post-preview {
      background: #1a1b2e; border: 1px solid #2a2d3e; border-radius: 10px;
      padding: 12px 16px; margin-bottom: 6px; font-size: 13px; line-height: 1.5;
      max-height: 80px; overflow: hidden; position: relative;
    }
    .post-preview .post-fade {
      position: absolute; bottom: 0; left: 0; right: 0; height: 30px;
      background: linear-gradient(transparent, #1a1b2e);
    }
    .post-preview .post-time { font-size: 11px; color: #6b7280; margin-bottom: 4px; }
    .post-preview .post-type-badge {
      font-size: 10px; padding: 1px 6px; border-radius: 4px;
      background: #2a2d3e; color: #9ca3af; margin-left: 6px;
    }
    .posts-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
      gap: 16px;
    }

    /* Error log */
    .error-item {
      display: flex; gap: 12px; align-items: baseline;
      padding: 8px 14px; background: #1a1b2e; border: 1px solid #2a2d3e;
      border-left: 3px solid #ef4444; border-radius: 8px; margin-bottom: 6px; font-size: 13px;
    }
    .error-item .err-time { color: #6b7280; font-size: 11px; white-space: nowrap; }
    .error-item .err-type {
      font-size: 10px; padding: 1px 6px; border-radius: 4px;
      background: #450a0a; color: #f87171; white-space: nowrap;
    }
    .error-item .err-ch { color: #00bcd4; font-size: 11px; white-space: nowrap; }
    .error-item .err-msg { color: #e4e4e7; flex: 1; }
    .no-errors { text-align: center; padding: 16px; color: #22c55e; font-size: 14px;
      background: #1a1b2e; border: 1px solid #2a2d3e; border-radius: 12px; }

    .channel-card .ch-progress { margin-top: 8px; }
    .channel-card .ch-progress .bar-bg {
      height: 6px; background: #2a2d3e; border-radius: 3px; overflow: hidden;
    }
    .channel-card .ch-progress .bar-fill {
      height: 100%; border-radius: 3px; transition: width 0.3s;
    }
    .channel-card .ch-progress .bar-label { font-size: 11px; color: #9ca3af; margin-top: 3px; }
    .channel-card .ch-last { font-size: 11px; color: #6b7280; margin-top: 6px; }
    .channel-card .ch-last .ago { color: #9ca3af; font-weight: 500; }
    .channel-card .ch-last .stale { color: #f87171; }
    .channel-card .ch-link {
      display: inline-block;
      margin-top: 8px;
      padding: 6px 16px;
      background: #00bcd4;
      color: #000;
      text-decoration: none;
      border-radius: 8px;
      font-size: 13px;
      font-weight: 600;
      transition: background 0.2s;
    }
    .channel-card .ch-link:hover { background: #00acc1; }

    /* Links section */
    .links-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
      gap: 16px;
      margin-bottom: 32px;
    }
    .link-card {
      background: #1a1b2e;
      border: 1px solid #2a2d3e;
      border-radius: 12px;
      padding: 16px 20px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .link-card .lc-info { }
    .link-card .lc-title { font-size: 14px; font-weight: 600; }
    .link-card .lc-desc { font-size: 12px; color: #6b7280; }
    .link-card a {
      padding: 6px 14px;
      background: #2a2d3e;
      color: #e4e4e7;
      text-decoration: none;
      border-radius: 8px;
      font-size: 13px;
      transition: background 0.2s;
    }
    .link-card a:hover { background: #3a3d4e; }

    /* Schedule */
    .schedule-table {
      width: 100%;
      background: #1a1b2e;
      border: 1px solid #2a2d3e;
      border-radius: 12px;
      overflow: hidden;
      margin-bottom: 32px;
    }
    .schedule-table table { width: 100%; border-collapse: collapse; }
    .schedule-table th {
      text-align: left;
      padding: 12px 16px;
      background: #22243a;
      font-size: 12px;
      text-transform: uppercase;
      letter-spacing: 1px;
      color: #9ca3af;
    }
    .schedule-table td { padding: 10px 16px; border-top: 1px solid #2a2d3e; font-size: 14px; }
    .schedule-table tr:hover td { background: #1e1f32; }

    .footer { text-align: center; padding: 32px; color: #4b5563; font-size: 12px; }

    /* Mobile */
    @media (max-width: 600px) {
      .header { padding: 16px; flex-direction: column; gap: 8px; }
      .header h1 { font-size: 18px; }
      .container { padding: 16px 12px; }
      .kpi-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; }
      .kpi-card { padding: 14px; }
      .kpi-card .value { font-size: 22px; }
      .channels-grid { grid-template-columns: 1fr; }
      .posts-grid { grid-template-columns: 1fr; }
      .links-grid { grid-template-columns: 1fr; }
      .channel-card .ch-stats { flex-wrap: wrap; gap: 12px; }
      .modal { width: 95%; max-width: none; padding: 16px; }
      .form-row { flex-direction: column; }
      .form-row select { flex: 1; }
      .schedule-table { font-size: 12px; }
      .schedule-table th, .schedule-table td { padding: 8px 10px; }
    }

    /* Refresh */
    #last-update { font-size: 12px; color: #6b7280; }

    /* Modal */
    .modal-overlay {
      display: none;
      position: fixed; top: 0; left: 0; right: 0; bottom: 0;
      background: rgba(0,0,0,0.7);
      z-index: 1000;
      justify-content: center; align-items: center;
    }
    .modal-overlay.active { display: flex; }
    .modal {
      background: #1a1b2e;
      border: 1px solid #2a2d3e;
      border-radius: 16px;
      width: 90%; max-width: 600px; max-height: 80vh;
      overflow-y: auto;
      padding: 24px;
    }
    .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
    .modal-header h2 { font-size: 18px; font-weight: 600; }
    .modal-close {
      background: none; border: none; color: #9ca3af; font-size: 24px;
      cursor: pointer; padding: 4px 8px; border-radius: 6px;
    }
    .modal-close:hover { background: #2a2d3e; color: #fff; }

    /* Source list */
    .source-item {
      display: flex; justify-content: space-between; align-items: center;
      padding: 12px 14px; border: 1px solid #2a2d3e; border-radius: 10px;
      margin-bottom: 8px;
    }
    .source-item .si-info { flex: 1; }
    .source-item .si-name { font-size: 14px; font-weight: 500; }
    .source-item .si-url { font-size: 11px; color: #6b7280; word-break: break-all; }
    .source-item .si-badge {
      font-size: 10px; padding: 2px 8px; border-radius: 6px;
      font-weight: 600; margin-left: 8px; white-space: nowrap;
    }
    .badge-approved { background: #14532d; color: #4ade80; }
    .badge-pending { background: #422006; color: #fbbf24; }
    .badge-rejected { background: #450a0a; color: #f87171; }
    .source-item .si-actions { display: flex; gap: 6px; margin-left: 12px; }
    .si-btn {
      padding: 4px 10px; border: none; border-radius: 6px;
      font-size: 12px; cursor: pointer; font-weight: 500;
    }
    .si-btn-approve { background: #166534; color: #4ade80; }
    .si-btn-approve:hover { background: #15803d; }
    .si-btn-reject { background: #7f1d1d; color: #fca5a5; }
    .si-btn-reject:hover { background: #991b1b; }
    .si-btn-delete { background: #2a2d3e; color: #9ca3af; }
    .si-btn-delete:hover { background: #3a3d4e; color: #fff; }

    /* Add source form */
    .add-source-form {
      margin-top: 16px; padding: 16px;
      background: #22243a; border-radius: 10px;
    }
    .add-source-form h3 { font-size: 14px; margin-bottom: 12px; color: #9ca3af; }
    .form-row { display: flex; gap: 8px; margin-bottom: 8px; }
    .form-row input, .form-row select {
      flex: 1; padding: 8px 12px;
      background: #1a1b2e; border: 1px solid #2a2d3e; border-radius: 8px;
      color: #e4e4e7; font-size: 13px; outline: none;
    }
    .form-row input:focus, .form-row select:focus { border-color: #00bcd4; }
    .form-row select { flex: 0.4; }
    .btn-add {
      padding: 8px 20px; background: #00bcd4; color: #000;
      border: none; border-radius: 8px; font-size: 13px;
      font-weight: 600; cursor: pointer;
    }
    .btn-add:hover { background: #00acc1; }
    .btn-add:disabled { opacity: 0.5; cursor: not-allowed; }

    .no-sources { text-align: center; padding: 20px; color: #6b7280; font-size: 14px; }

    /* Queue items */
    .queue-item {
      background: #1a1b2e;
      border: 1px solid #2a2d3e;
      border-radius: 12px;
      padding: 16px 20px;
      margin-bottom: 12px;
    }
    .queue-item .qi-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
    .queue-item .qi-channel { font-size: 12px; color: #00bcd4; font-weight: 500; }
    .queue-item .qi-source { font-size: 11px; color: #6b7280; }
    .queue-item .qi-content {
      font-size: 13px; line-height: 1.5;
      background: #22243a; border-radius: 8px;
      padding: 12px; margin-bottom: 12px;
      max-height: 200px; overflow-y: auto;
      white-space: pre-wrap;
    }
    .queue-item .qi-actions { display: flex; gap: 8px; }
    .qi-btn {
      padding: 6px 16px; border: none; border-radius: 8px;
      font-size: 13px; cursor: pointer; font-weight: 600;
    }
    .qi-btn-approve { background: #166534; color: #4ade80; }
    .qi-btn-approve:hover { background: #15803d; }
    .qi-btn-reject { background: #7f1d1d; color: #fca5a5; }
    .qi-btn-reject:hover { background: #991b1b; }
    .qi-btn:disabled { opacity: 0.5; cursor: not-allowed; }
    .no-queue { text-align: center; padding: 24px; color: #4b5563; font-size: 14px;
      background: #1a1b2e; border: 1px solid #2a2d3e; border-radius: 12px; }
  </style>
</head>
<body>
  <div class="header">
    <h1>🇰🇿 <span>KZ Channels</span> Dashboard</h1>
    <div class="status">
      <div class="dot"></div>
      Bot Online
      <span id="last-update"></span>
    </div>
  </div>

  <div class="container">
    <!-- KPIs -->
    <div class="kpi-grid" id="kpi-grid">
      <div class="kpi-card">
        <div class="label">Подписчиков</div>
        <div class="value" id="kpi-subs-total">—</div>
        <div class="sub">по всем каналам</div>
      </div>
      <div class="kpi-card">
        <div class="label">Постов сегодня</div>
        <div class="value" id="kpi-posts-today">—</div>
        <div class="sub">по всем каналам</div>
      </div>
      <div class="kpi-card">
        <div class="label">Реклама сегодня</div>
        <div class="value" id="kpi-ads-today">—</div>
        <div class="sub">Pin-Up нативки</div>
      </div>
      <div class="kpi-card">
        <div class="label">Постов за 7 дней</div>
        <div class="value" id="kpi-posts-week">—</div>
        <div class="sub">всего</div>
      </div>
      <div class="kpi-card" id="kpi-queue-card" style="cursor:pointer" onclick="document.getElementById('queue-section').scrollIntoView({behavior:'smooth'})">
        <div class="label">На модерации</div>
        <div class="value" id="kpi-queue">0</div>
        <div class="sub">постов ждут одобрения</div>
      </div>
    </div>

    <!-- Channels -->
    <div class="section-title">Каналы</div>
    <div class="channels-grid" id="channels-grid">
      <!-- filled by JS -->
    </div>

    <!-- Recent Posts -->
    <div class="section-title">Последние посты</div>
    <div id="recent-posts" style="margin-bottom:32px"></div>

    <!-- Ad Tracker -->
    <div class="section-title">💰 Реклама (Pin-Up)</div>
    <div id="ad-tracker" style="margin-bottom:32px">
      <div class="kpi-grid" id="ad-kpis"></div>
      <div id="ad-ab" style="margin-top:12px"></div>
    </div>

    <!-- Links -->
    <div class="section-title">Полезные ссылки</div>
    <div class="links-grid">
      <div class="link-card">
        <div class="lc-info">
          <div class="lc-title">Pin-Up Partners</div>
          <div class="lc-desc">CPA кабинет, статистика, офферы</div>
        </div>
        <a href="https://pinuppartner.com" target="_blank">Открыть</a>
      </div>
      <div class="link-card">
        <div class="lc-info">
          <div class="lc-title">TGStat Kazakhstan</div>
          <div class="lc-desc">Аналитика каналов KZ</div>
        </div>
        <a href="https://kaz.tgstat.com" target="_blank">Открыть</a>
      </div>
      <div class="link-card">
        <div class="lc-info">
          <div class="lc-title">OpenRouter</div>
          <div class="lc-desc">AI API для контента</div>
        </div>
        <a href="https://openrouter.ai" target="_blank">Открыть</a>
      </div>
      <div class="link-card">
        <div class="lc-info">
          <div class="lc-title">Telega.in</div>
          <div class="lc-desc">Биржа рекламы в TG</div>
        </div>
        <a href="https://telega.in" target="_blank">Открыть</a>
      </div>
    </div>

    <!-- Moderation Queue -->
    <div class="section-title" id="queue-section">Модерация постов <span id="queue-badge" style="background:#422006;color:#fbbf24;padding:2px 8px;border-radius:6px;font-size:12px;margin-left:8px;display:none">0</span></div>
    <div id="queue-list" style="margin-bottom:32px"></div>

    <!-- Error Log -->
    <div class="section-title">Ошибки <span id="errors-badge" style="background:#450a0a;color:#f87171;padding:2px 8px;border-radius:6px;font-size:12px;margin-left:8px;display:none">0</span></div>
    <div id="errors-list" style="margin-bottom:32px"></div>

    <!-- Schedule -->
    <div class="section-title">Расписание постинга (Астана UTC+5)</div>
    <div class="schedule-table">
      <table>
        <thead>
          <tr>
            <th>Время</th>
            <th>Тип</th>
            <th>Описание</th>
          </tr>
        </thead>
        <tbody>
          <tr><td>08:15</td><td>Дайджест</td><td>Утренние новости / контент дня</td></tr>
          <tr><td>10:30</td><td>Контент</td><td>Тематический пост</td></tr>
          <tr><td>12:15</td><td>Контент</td><td>Обеденный пост</td></tr>
          <tr><td>14:30</td><td>Контент</td><td>Дневной пост</td></tr>
          <tr><td>16:00</td><td>Контент / Реклама</td><td>Конец рабочего дня — высокий CTR</td></tr>
          <tr><td>18:30</td><td>Контент / Реклама</td><td>Вечер — лучшее для офферов</td></tr>
          <tr><td>21:15</td><td>Интерактив</td><td>Итоги дня, опросы</td></tr>
        </tbody>
      </table>
    </div>
  </div>

  <!-- Sources Modal -->
  <div class="modal-overlay" id="sources-modal">
    <div class="modal">
      <div class="modal-header">
        <h2 id="modal-title">Источники</h2>
        <button class="modal-close" onclick="closeModal()">&times;</button>
      </div>
      <div id="sources-list"></div>
      <div style="margin-bottom:12px"><button class="si-btn" style="background:#1e3a5f;color:#60a5fa" onclick="checkSourceHealth()">🔍 Проверить доступность</button> <span id="health-status" style="font-size:12px;color:#6b7280"></span></div>
      <div class="add-source-form">
        <h3>Добавить источник</h3>
        <div class="form-row">
          <input type="text" id="new-src-name" placeholder="Название (напр. Tengrinews)">
          <select id="new-src-type">
            <option value="rss">RSS</option>
            <option value="scrape">Scrape</option>
          </select>
        </div>
        <div class="form-row">
          <input type="text" id="new-src-url" placeholder="URL (напр. https://site.kz/rss)">
          <button class="btn-add" id="btn-add-source" onclick="addSource()">Добавить</button>
        </div>
      </div>
    </div>
  </div>

  <!-- Manual Post Modal -->
  <div class="modal-overlay" id="post-modal">
    <div class="modal">
      <div class="modal-header">
        <h2 id="post-modal-title">Опубликовать</h2>
        <button class="modal-close" onclick="closePostModal()">&times;</button>
      </div>
      <div style="margin-bottom:12px;font-size:12px;color:#6b7280">HTML теги: &lt;b&gt;жирный&lt;/b&gt;, &lt;i&gt;курсив&lt;/i&gt;, &lt;a href="..."&gt;ссылка&lt;/a&gt;</div>
      <textarea id="post-text" rows="8" style="width:100%;padding:12px;background:#22243a;border:1px solid #2a2d3e;border-radius:8px;color:#e4e4e7;font-size:14px;resize:vertical;outline:none;font-family:inherit" placeholder="Текст поста..."></textarea>
      <input type="text" id="post-image" style="width:100%;padding:10px 12px;background:#22243a;border:1px solid #2a2d3e;border-radius:8px;color:#e4e4e7;font-size:13px;outline:none;margin-top:8px" placeholder="URL картинки (необязательно)">
      <div id="post-image-preview" style="margin-top:8px;display:none"><img style="max-height:120px;border-radius:8px;border:1px solid #2a2d3e"></div>
      <div style="display:flex;justify-content:flex-end;gap:8px;margin-top:12px">
        <button class="si-btn si-btn-delete" onclick="closePostModal()">Отмена</button>
        <button class="btn-add" id="btn-send-post" onclick="sendManualPost()">Отправить</button>
      </div>
      <div id="post-result" style="margin-top:12px;font-size:13px;display:none"></div>
    </div>
  </div>

  <div class="footer">KZ Channels Dashboard v0.3 | Bot: @kz_channels_bot</div>

  <script>
    const CHANNELS = [
      { id: 'almaty-news', name: 'Алматы Сегодня', username: 'almaty_segodnya1', type: 'news', typeLabel: 'Новости' },
      { id: 'astana-news', name: 'Астана Сегодня', username: 'astana_segodnya', type: 'news', typeLabel: 'Новости' },
      { id: 'typical-kz', name: 'Типичный Казахстан', username: 'typical_kz1', type: 'humor', typeLabel: 'Юмор' },
      { id: 'dengi-kz', name: 'Деньги KZ', username: 'dengi_kz1', type: 'earnings', typeLabel: 'Заработок' },
      { id: 'kz-champions', name: 'KZ Champions', username: 'kz_champions', type: 'sports', typeLabel: 'Спорт' }
    ];

    const TOTAL_SLOTS = 7;

    function renderProgress(todayPosts) {
      const pct = Math.min(100, Math.round((todayPosts / TOTAL_SLOTS) * 100));
      const color = pct >= 100 ? '#22c55e' : pct >= 50 ? '#eab308' : '#ef4444';
      return `<div class="ch-progress">
        <div class="bar-bg"><div class="bar-fill" style="width:${pct}%;background:${color}"></div></div>
        <div class="bar-label">${todayPosts}/${TOTAL_SLOTS} слотов (${pct}%)</div>
      </div>`;
    }

    function formatLastPost(utcStr) {
      if (!utcStr) return 'Ещё не постил';
      const postDate = new Date(utcStr + 'Z');
      const now = new Date();
      const diffMin = Math.floor((now - postDate) / 60000);
      if (diffMin < 1) return 'Последний пост: <span class="ago">только что</span>';
      if (diffMin < 60) return `Последний пост: <span class="ago">${diffMin} мин назад</span>`;
      const diffH = Math.floor(diffMin / 60);
      if (diffH < 24) return `Последний пост: <span class="ago">${diffH}ч назад</span>`;
      const diffD = Math.floor(diffH / 24);
      const cls = diffD >= 2 ? 'stale' : 'ago';
      return `Последний пост: <span class="${cls}">${diffD}д назад</span>`;
    }

    function renderChannels(stats) {
      const grid = document.getElementById('channels-grid');
      grid.innerHTML = CHANNELS.map(ch => {
        const s = stats?.[ch.id] || { today: 0, ads: 0, week: 0 };
        return `
          <div class="channel-card">
            <div class="ch-header">
              <div class="ch-name">${ch.name}</div>
              <span class="ch-type type-${ch.type}">${ch.typeLabel}</span>
            </div>
            <div class="ch-stats">
              <div class="ch-stat"><div class="num">${s.subscribers || '—'}</div><div class="lbl">подписчиков</div></div>
              <div class="ch-stat"><div class="num">${s.today}</div><div class="lbl">сегодня</div></div>
              <div class="ch-stat"><div class="num">${s.ads}</div><div class="lbl">реклама</div></div>
              <div class="ch-stat"><div class="num">${s.week}</div><div class="lbl">за 7 дней</div></div>
            </div>
            ${renderProgress(s.today)}
            <div class="ch-last">${formatLastPost(s.lastPost)}</div>
            <a class="ch-link" href="https://t.me/${ch.username}" target="_blank">Открыть канал</a>
            <button class="ch-link" style="background:#2a2d3e;color:#e4e4e7;margin-left:8px" onclick="openSources('${ch.id}','${ch.name}')">Источники</button>
            <button class="ch-link" style="background:#166534;color:#4ade80;margin-left:8px" onclick="openPostModal('${ch.id}','${ch.name}')">✏️ Пост</button>
          </div>
        `;
      }).join('');
    }

    async function fetchStats() {
      try {
        const res = await fetch('/api/stats');
        if (!res.ok) throw new Error('API error');
        const data = await res.json();

        // KPIs
        let totalToday = 0, totalAds = 0, totalWeek = 0, totalSubs = 0;
        const stats = {};
        for (const ch of CHANNELS) {
          const s = data[ch.id] || { today: 0, ads: 0, week: 0, subscribers: 0 };
          stats[ch.id] = s;
          totalToday += s.today;
          totalAds += s.ads;
          totalWeek += s.week;
          totalSubs += s.subscribers || 0;
        }

        document.getElementById('kpi-subs-total').textContent = totalSubs;
        document.getElementById('kpi-posts-today').textContent = totalToday;
        document.getElementById('kpi-ads-today').textContent = totalAds;
        document.getElementById('kpi-posts-week').textContent = totalWeek;
        renderChannels(stats);

        document.getElementById('last-update').textContent =
          `Обновлено: ${new Date().toLocaleTimeString('ru')}`;
      } catch (err) {
        console.error('Stats fetch error:', err);
        renderChannels(null);
      }
    }

    // --- Sources Modal ---
    let currentChannelId = null;

    function openSources(channelId, channelName) {
      currentChannelId = channelId;
      document.getElementById('modal-title').textContent = `Источники — ${channelName}`;
      document.getElementById('sources-modal').classList.add('active');
      document.getElementById('new-src-name').value = '';
      document.getElementById('new-src-url').value = '';
      loadSources();
    }

    function closeModal() {
      document.getElementById('sources-modal').classList.remove('active');
      currentChannelId = null;
    }

    // Close on overlay click
    document.getElementById('sources-modal').addEventListener('click', (e) => {
      if (e.target.classList.contains('modal-overlay')) closeModal();
    });

    async function loadSources() {
      const list = document.getElementById('sources-list');
      try {
        const res = await fetch(`/api/sources?channel=${currentChannelId}`);
        const { data } = await res.json();
        if (!data || data.length === 0) {
          list.innerHTML = '<div class="no-sources">Нет источников</div>';
          return;
        }
        list.innerHTML = data.map(s => {
          const badgeClass = s.status === 'approved' ? 'badge-approved' : s.status === 'pending' ? 'badge-pending' : 'badge-rejected';
          const statusLabel = s.status === 'approved' ? 'Активен' : s.status === 'pending' ? 'На модерации' : 'Отклонён';

          let actions = '';
          if (s.status === 'pending') {
            actions = `
              <button class="si-btn si-btn-approve" onclick="approveSource(${s.id})">Одобрить</button>
              <button class="si-btn si-btn-reject" onclick="rejectSource(${s.id})">Отклонить</button>
            `;
          } else if (s.status === 'rejected') {
            actions = `
              <button class="si-btn si-btn-approve" onclick="approveSource(${s.id})">Одобрить</button>
              <button class="si-btn si-btn-delete" onclick="deleteSource(${s.id})">Удалить</button>
            `;
          } else {
            actions = `<button class="si-btn si-btn-delete" onclick="deleteSource(${s.id})">Удалить</button>`;
          }

          return `
            <div class="source-item">
              <div class="si-info">
                <div class="si-name">${s.name} <span class="si-badge ${badgeClass}">${statusLabel}</span></div>
                <div class="si-url">${s.url}</div>
              </div>
              <div class="si-actions">${actions}</div>
            </div>
          `;
        }).join('');
      } catch (err) {
        list.innerHTML = '<div class="no-sources">Ошибка загрузки</div>';
      }
    }

    async function checkSourceHealth() {
      const statusEl = document.getElementById('health-status');
      statusEl.textContent = 'Проверяю...';
      statusEl.style.color = '#fbbf24';
      try {
        const res = await fetch('/api/sources/health');
        const { data } = await res.json();
        const channelSources = data[currentChannelId] || [];
        // Update source items with health indicators
        const items = document.querySelectorAll('.source-item .si-name');
        items.forEach(el => {
          const name = el.textContent.split(' ')[0].trim();
          const src = channelSources.find(s => el.textContent.includes(s.name));
          if (!src) return;
          // Remove old indicator
          const old = el.querySelector('.health-dot');
          if (old) old.remove();
          const dot = document.createElement('span');
          dot.className = 'health-dot';
          dot.style.cssText = `display:inline-block;width:8px;height:8px;border-radius:50%;margin-left:6px;vertical-align:middle;`;
          if (src.status === 'ok') {
            dot.style.background = '#22c55e';
            dot.title = `OK (${src.ms}ms)`;
          } else if (src.status === 'skip') {
            dot.style.background = '#6b7280';
            dot.title = 'Scrape (не проверяется)';
          } else {
            dot.style.background = '#ef4444';
            dot.title = `Ошибка: ${src.error}`;
          }
          el.appendChild(dot);
        });
        const ok = channelSources.filter(s => s.status === 'ok').length;
        const total = channelSources.filter(s => s.type !== 'scrape' || s.status !== 'skip').length;
        statusEl.textContent = `${ok}/${channelSources.length} доступны`;
        statusEl.style.color = ok === total ? '#22c55e' : '#ef4444';
      } catch {
        statusEl.textContent = 'Ошибка проверки';
        statusEl.style.color = '#ef4444';
      }
    }

    async function addSource() {
      const name = document.getElementById('new-src-name').value.trim();
      const url = document.getElementById('new-src-url').value.trim();
      const type = document.getElementById('new-src-type').value;
      if (!name || !url) return;

      const btn = document.getElementById('btn-add-source');
      btn.disabled = true;
      try {
        await fetch('/api/sources', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ channelId: currentChannelId, name, url, type })
        });
        document.getElementById('new-src-name').value = '';
        document.getElementById('new-src-url').value = '';
        loadSources();
      } catch (err) {
        alert('Ошибка: ' + err.message);
      }
      btn.disabled = false;
    }

    async function approveSource(id) {
      await fetch(`/api/sources/${id}/approve`, { method: 'PATCH' });
      loadSources();
    }

    async function rejectSource(id) {
      await fetch(`/api/sources/${id}/reject`, { method: 'PATCH' });
      loadSources();
    }

    async function deleteSource(id) {
      if (!confirm('Удалить источник?')) return;
      await fetch(`/api/sources/${id}`, { method: 'DELETE' });
      loadSources();
    }

    // --- Moderation Queue ---

    const channelNames = {};
    CHANNELS.forEach(ch => { channelNames[ch.id] = ch.name; });

    async function loadQueue() {
      const list = document.getElementById('queue-list');
      const badge = document.getElementById('queue-badge');
      try {
        const res = await fetch('/api/queue');
        const { data, count } = await res.json();

        // Badge
        if (count > 0) {
          badge.textContent = count;
          badge.style.display = 'inline';
          document.getElementById('kpi-queue-card').style.borderColor = '#fbbf24';
        } else {
          badge.style.display = 'none';
          document.getElementById('kpi-queue-card').style.borderColor = '#2a2d3e';
        }
        document.getElementById('kpi-queue').textContent = count;

        if (!data || data.length === 0) {
          list.innerHTML = '<div class="no-queue">Нет постов на модерации</div>';
          return;
        }

        list.innerHTML = data.map(item => {
          const chName = channelNames[item.channel_id] || item.channel_id;
          const srcName = item.source_name || 'Неизвестный источник';
          // Очистить HTML теги для превью
          const preview = item.content.replace(/<[^>]+>/g, '').substring(0, 300);
          return `
            <div class="queue-item" id="qi-${item.id}">
              <div class="qi-header">
                <span class="qi-channel">${chName}</span>
                <span class="qi-source">Источник: ${srcName}</span>
              </div>
              <div class="qi-content">${item.content}</div>
              <div class="qi-actions">
                <button class="qi-btn qi-btn-approve" onclick="approvePost(${item.id}, this)">Одобрить и отправить</button>
                <button class="qi-btn qi-btn-reject" onclick="rejectPost(${item.id}, this)">Отклонить</button>
              </div>
            </div>
          `;
        }).join('');
      } catch (err) {
        list.innerHTML = '<div class="no-queue">Ошибка загрузки очереди</div>';
      }
    }

    async function approvePost(id, btn) {
      btn.disabled = true;
      btn.textContent = 'Отправка...';
      try {
        const res = await fetch(`/api/queue/${id}/approve`, { method: 'PATCH' });
        const result = await res.json();
        if (result.success) {
          document.getElementById(`qi-${id}`).style.display = 'none';
          loadQueue();
          fetchStats();
        } else {
          alert('Ошибка: ' + (result.error || 'unknown'));
          btn.disabled = false;
          btn.textContent = 'Одобрить и отправить';
        }
      } catch (err) {
        alert('Ошибка: ' + err.message);
        btn.disabled = false;
        btn.textContent = 'Одобрить и отправить';
      }
    }

    async function rejectPost(id, btn) {
      btn.disabled = true;
      try {
        await fetch(`/api/queue/${id}/reject`, { method: 'PATCH' });
        document.getElementById(`qi-${id}`).style.display = 'none';
        loadQueue();
      } catch (err) {
        alert('Ошибка: ' + err.message);
        btn.disabled = false;
      }
    }

    // --- Manual Post ---
    let postChannelId = null;

    function openPostModal(channelId, channelName) {
      postChannelId = channelId;
      document.getElementById('post-modal-title').textContent = `Опубликовать → ${channelName}`;
      document.getElementById('post-text').value = '';
      document.getElementById('post-image').value = '';
      document.getElementById('post-image-preview').style.display = 'none';
      document.getElementById('post-result').style.display = 'none';
      document.getElementById('btn-send-post').disabled = false;
      document.getElementById('post-modal').classList.add('active');
      document.getElementById('post-text').focus();
    }

    function closePostModal() {
      document.getElementById('post-modal').classList.remove('active');
      postChannelId = null;
    }

    document.getElementById('post-modal').addEventListener('click', (e) => {
      if (e.target.classList.contains('modal-overlay')) closePostModal();
    });

    // Превью картинки
    document.getElementById('post-image').addEventListener('input', (e) => {
      const url = e.target.value.trim();
      const preview = document.getElementById('post-image-preview');
      if (url) {
        const img = preview.querySelector('img');
        img.src = url;
        img.onload = () => { preview.style.display = 'block'; };
        img.onerror = () => { preview.style.display = 'none'; };
      } else {
        preview.style.display = 'none';
      }
    });

    async function sendManualPost() {
      const text = document.getElementById('post-text').value.trim();
      const imageUrl = document.getElementById('post-image').value.trim() || null;
      if (!text || !postChannelId) return;

      const btn = document.getElementById('btn-send-post');
      const result = document.getElementById('post-result');
      btn.disabled = true;
      btn.textContent = 'Отправка...';

      try {
        const res = await fetch('/api/post', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ channelId: postChannelId, text, imageUrl })
        });
        const data = await res.json();
        if (data.success) {
          result.style.display = 'block';
          result.style.color = '#4ade80';
          result.textContent = `✅ Опубликовано! msg #${data.messageId}`;
          fetchStats();
          setTimeout(closePostModal, 1500);
        } else {
          result.style.display = 'block';
          result.style.color = '#f87171';
          result.textContent = `❌ ${data.error}`;
          btn.disabled = false;
          btn.textContent = 'Отправить';
        }
      } catch (err) {
        result.style.display = 'block';
        result.style.color = '#f87171';
        result.textContent = `❌ ${err.message}`;
        btn.disabled = false;
        btn.textContent = 'Отправить';
      }
    }

    // --- Recent Posts ---
    async function loadRecentPosts() {
      const container = document.getElementById('recent-posts');
      const chunks = [];
      for (const ch of CHANNELS) {
        try {
          const res = await fetch(`/api/posts?channel=${ch.id}&limit=3`);
          const { data } = await res.json();
          if (!data || data.length === 0) continue;
          const postsHtml = data.map(p => {
            const time = new Date(p.created_at + 'Z').toLocaleString('ru', { timeZone: 'Asia/Almaty', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' });
            const text = p.content.replace(/<[^>]+>/g, '').substring(0, 150);
            const badge = p.post_type === 'ad' ? '<span class="post-type-badge">💰 реклама</span>' : '';
            return `<div class="post-preview"><div class="post-time">${time}${badge}</div>${text}<div class="post-fade"></div></div>`;
          }).join('');
          chunks.push(`<div class="posts-channel"><div class="posts-channel-name">${ch.name}</div>${postsHtml}</div>`);
        } catch {}
      }
      container.innerHTML = chunks.length > 0
        ? `<div class="posts-grid">${chunks.join('')}</div>`
        : '<div style="text-align:center;color:#4b5563;font-size:14px;padding:16px">Нет постов</div>';
    }

    // --- Ad Tracker ---
    async function loadAdStats() {
      try {
        const res = await fetch('/api/ads');
        const data = await res.json();
        const grid = document.getElementById('ad-kpis');
        grid.innerHTML = `
          <div class="kpi-card">
            <div class="label">Реклама сегодня</div>
            <div class="value">${data.todayAds}</div>
            <div class="sub">постов Pin-Up</div>
          </div>
          <div class="kpi-card">
            <div class="label">За 7 дней</div>
            <div class="value">${data.weekAds}</div>
            <div class="sub">рекламных постов</div>
          </div>
          <div class="kpi-card">
            <div class="label">Всего</div>
            <div class="value">${data.totalAds}</div>
            <div class="sub">за всё время</div>
          </div>
          <div class="kpi-card">
            <div class="label">Частота</div>
            <div class="value">${data.weekAds > 0 ? Math.round(data.weekAds / 5) : 0}/кан</div>
            <div class="sub">в неделю на канал</div>
          </div>
        `;
        // A/B templates
        const ab = document.getElementById('ad-ab');
        if (data.templates && data.templates.length > 0) {
          const total = data.templates.reduce((s, t) => s + t.count, 0);
          ab.innerHTML = '<div style="font-size:13px;color:#9ca3af;margin-bottom:8px">A/B шаблоны:</div>' +
            data.templates.map(t => {
              const pct = Math.round((t.count / total) * 100);
              return `<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;font-size:13px">
                <span style="color:#fbbf24;width:80px">${t.template_id}</span>
                <div style="flex:1;height:6px;background:#2a2d3e;border-radius:3px;overflow:hidden">
                  <div style="width:${pct}%;height:100%;background:#eab308;border-radius:3px"></div>
                </div>
                <span style="color:#9ca3af;width:60px;text-align:right">${t.count}× (${pct}%)</span>
              </div>`;
            }).join('');
        } else {
          ab.innerHTML = '';
        }
      } catch {}
    }

    // --- Error Log ---
    async function loadErrors() {
      const list = document.getElementById('errors-list');
      const badge = document.getElementById('errors-badge');
      try {
        const res = await fetch('/api/errors?limit=15');
        const { data, total } = await res.json();
        if (total > 0) {
          badge.textContent = total;
          badge.style.display = 'inline';
        } else {
          badge.style.display = 'none';
        }
        if (!data || data.length === 0) {
          list.innerHTML = '<div class="no-errors">✅ Ошибок нет</div>';
          return;
        }
        list.innerHTML = data.map(e => {
          const chName = channelNames[e.channel] || e.channel;
          return `<div class="error-item">
            <span class="err-time">${e.time}</span>
            <span class="err-ch">${chName}</span>
            <span class="err-type">${e.type}</span>
            <span class="err-msg">${e.message}</span>
          </div>`;
        }).join('');
      } catch {
        list.innerHTML = '<div class="no-errors" style="color:#6b7280">Не удалось загрузить</div>';
      }
    }

    // Initial render + auto-refresh every 60s
    fetchStats();
    loadQueue();
    loadRecentPosts();
    loadErrors();
    loadAdStats();
    setInterval(fetchStats, 60000);
    setInterval(loadQueue, 30000);
    setInterval(loadRecentPosts, 60000);
    setInterval(loadErrors, 30000);
    setInterval(loadAdStats, 60000);
  </script>
</body>
</html>

📜 Git History

045688afeat: dashboard v0.3 — 9 новых источников + 10 улучшений дашборда7 weeks ago
5481deffeat: post moderation queue for new sources7 weeks ago
6ff5fa6feat: source management with moderation via dashboard modal7 weeks ago
0d25952feat: add dashboard on arb.szhub.space (port 3220)7 weeks ago
Show last diff
Loading...