cron.html 9.27 KB
{% extends "base.html" %}
{% block title %}Cron 管理 - WeChat Viewer{% endblock %}
{% block page_title %}/ cron{% endblock %}
{% block body %}
<style>
body{overflow-y:auto!important;height:auto!important;}
.cron-container{max-width:960px;margin:0 auto;padding:20px;}
.cron-header{display:flex;align-items:center;gap:12px;margin-bottom:20px;flex-wrap:wrap;}
.cron-header h1{font-size:20px;color:#2c3e50;}
.cron-header .spacer{flex:1;}
.cron-header .auto-refresh{font-size:11px;color:#999;display:flex;align-items:center;gap:4px;}
.cron-header .auto-refresh .dot{width:8px;height:8px;border-radius:50%;background:#27ae60;animation:pulse 2s infinite;}
@keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}}

/* Tabs */
.tabs{display:flex;gap:0;margin-bottom:20px;border-bottom:2px solid #e0e0e0;}
.tab-btn{padding:10px 20px;border:none;background:none;font-size:14px;color:#999;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-2px;transition:.15s;}
.tab-btn:hover{color:#333;}
.tab-btn.active{color:#2c3e50;font-weight:bold;border-bottom-color:#3498db;}

/* Config tab: job cards */
.job-card{display:flex;align-items:center;gap:12px;background:white;border-radius:8px;padding:14px 18px;margin-bottom:10px;box-shadow:0 1px 2px rgba(0,0,0,0.05);cursor:pointer;transition:.15s;text-decoration:none;color:inherit;border-left:3px solid transparent;}
.job-card:hover{background:#f8f9fa;}
.job-card.active{border-left-color:#3498db;background:#e8f0fe;}
.job-status{width:12px;height:12px;border-radius:50%;flex-shrink:0;}
.job-status.on{background:#27ae60;}
.job-status.off{background:#bdc3c7;}
.job-status.paused{background:#f39c12;}
.job-info{flex:1;min-width:0;}
.job-info .name{font-size:14px;font-weight:600;}
.job-info .meta{font-size:11px;color:#999;margin-top:2px;}
.job-schedule{font-size:11px;color:#666;background:#f0f0f0;padding:3px 10px;border-radius:10px;white-space:nowrap;}
.job-last{font-size:11px;text-align:right;min-width:50px;}
.job-last .status-ok{color:#27ae60;font-weight:600;}
.job-last .status-failed{color:#e74c3c;font-weight:600;}
.job-last .time{color:#999;font-size:10px;display:block;margin-top:2px;}
.job-tag{font-size:10px;padding:2px 6px;border-radius:8px;font-weight:600;}
.job-tag.skills{background:#3498db;color:white;}
.job-tag.no-agent{background:#e67e22;color:white;}
.job-tag.script{background:#9b59b6;color:white;}

/* Logs tab */
.log-header{display:flex;align-items:center;gap:12px;margin-bottom:16px;flex-wrap:wrap;}
.log-header select{padding:6px 12px;border:1px solid #ddd;border-radius:6px;font-size:13px;min-width:180px;background:white;}
.log-entry{background:white;border-radius:8px;padding:14px 18px;margin-bottom:10px;box-shadow:0 1px 2px rgba(0,0,0,0.05);cursor:pointer;}
.log-entry .log-time{font-size:12px;color:#999;margin-bottom:6px;}
.log-entry .log-preview{font-size:13px;color:#555;line-height:1.5;white-space:pre-wrap;font-family:monospace;max-height:60px;overflow:hidden;text-overflow:ellipsis;}
.log-entry.open .log-preview{max-height:none;overflow:visible;}
.log-entry .log-hint{font-size:11px;color:#3498db;margin-top:4px;}
.log-entry .log-full{display:none;font-size:13px;color:#333;line-height:1.5;white-space:pre-wrap;font-family:monospace;margin-top:8px;padding:10px;background:#f8f9fa;border-radius:4px;max-height:400px;overflow-y:auto;}
.log-entry.open .log-full{display:block;}
.log-entry.open .log-hint{display:none;}

/* Empty */
.empty-state{text-align:center;padding:40px;color:#999;}
.empty-state .icon{font-size:40px;margin-bottom:8px;}
.loading{text-align:center;padding:30px;color:#999;}
</style>

<div class="cron-container">
  <div class="cron-header">
    <h1>⏱ Cron 管理</h1>
    <span class="spacer"></span>
    <span class="auto-refresh"><span class="dot"></span> 每 60s 自动刷新</span>
  </div>

  <!-- Tabs -->
  <div class="tabs">
    <button id="btn-tab-config" class="tab-btn active" onclick="switchTab('config')">📋 配置概览</button>
    <button id="btn-tab-logs" class="tab-btn" onclick="switchTab('logs')">📄 执行记录</button>
  </div>

  <!-- Tab: 配置概览 -->
  <div id="tab-config">
    <div id="config-content" class="loading">加载中...</div>
  </div>

  <!-- Tab: 执行记录 -->
  <div id="tab-logs" style="display:none;">
    <div class="log-header">
      <select id="job-select" onchange="loadLogs()">
        <option value="">选择 Job...</option>
      </select>
    </div>
    <div id="logs-content" class="loading">请先选择一个 Job</div>
  </div>
</div>

<script>
var currentTab = 'config';
var allJobs = [];
var selectedJobId = null;

function switchTab(tab) {
  currentTab = tab;
  document.getElementById('btn-tab-config').classList.toggle('active', tab === 'config');
  document.getElementById('btn-tab-logs').classList.toggle('active', tab === 'logs');
  document.getElementById('tab-config').style.display = tab === 'config' ? 'block' : 'none';
  document.getElementById('tab-logs').style.display = tab === 'logs' ? 'block' : 'none';
  if (tab === 'config') {
    loadJobs();
  } else if (tab === 'logs' && allJobs.length > 0) {
    if (!selectedJobId) {
      selectedJobId = allJobs[0].id;
      document.getElementById('job-select').value = selectedJobId;
    }
    loadLogs();
  }
}

function loadJobs() {
  var el = document.getElementById('config-content');
  el.innerHTML = '<div class="loading">加载中...</div>';
  fetch('/api/cron/list').then(function(r){return r.json()}).then(function(j){
    if (!j.ok || !j.data.length) {
      el.innerHTML = '<div class="empty-state"><div class="icon">📋</div><p>暂无 Cron Job</p></div>';
      return;
    }
    allJobs = j.data;

    // Populate job selector
    var sel = document.getElementById('job-select');
    var opt = '<option value="">选择 Job...</option>';
    for (var i = 0; i < allJobs.length; i++) {
      var jj = allJobs[i];
      opt += '<option value="' + jj.id + '">' + escHtml(jj.name) + '</option>';
    }
    sel.innerHTML = opt;

    // Render cards
    var h = '';
    for (var i = 0; i < allJobs.length; i++) {
      var jb = allJobs[i];
      var sc = jb.enabled ? 'on' : 'paused';
      if (jb.state === 'completed') sc = 'off';
      var lastStatus = jb.last_status || '?';
      var lastClass = lastStatus === 'ok' ? 'status-ok' : 'status-failed';
      var lastTime = jb.last_run ? jb.last_run.split('T')[1].slice(0,8) : '';
      var nextTime = jb.next_run ? '→ ' + jb.next_run.split('T')[1].slice(0,8) : '';

      // Tags
      var tags = '';
      if (jb.no_agent) tags += '<span class="job-tag script">script</span> ';
      if (jb.skills && jb.skills.length) {
        tags += '<span class="job-tag skills">' + escHtml(jb.skills.join(',')) + '</span> ';
      }

      h += '<a class="job-card" href="/cron/' + jb.id + '" style="border-left-color:' + (sc === 'on' ? '#27ae60' : sc === 'paused' ? '#f39c12' : '#bdc3c7') + '">';
      h += '<span class="job-status ' + sc + '"></span>';
      h += '<div class="job-info"><div class="name">' + escHtml(jb.name) + '</div>';
      h += '<div class="meta">' + tags + (jb.deliver ? 'deliver: ' + escHtml(jb.deliver) : '') + '</div></div>';
      h += '<span class="job-schedule">' + escHtml(jb.schedule) + '</span>';
      h += '<div class="job-last"><span class="' + lastClass + '">' + (lastStatus === 'ok' ? '✅' : '❌') + '</span>';
      h += '<span class="time">' + lastTime + ' ' + nextTime + '</span></div>';
      h += '</a>';
    }
    el.innerHTML = h;
  }).catch(function(e){
    el.innerHTML = '<div class="empty-state"><p>加载失败: ' + e.message + '</p></div>';
  });
}

function viewLogs(jobId) {
  selectedJobId = jobId;
  document.getElementById('job-select').value = jobId;
  switchTab('logs');
}

function loadLogs() {
  var sid = document.getElementById('job-select').value;
  if (!sid) {
    document.getElementById('logs-content').innerHTML = '<div class="empty-state"><div class="icon">📄</div><p>请先选择一个 Job</p></div>';
    return;
  }
  selectedJobId = sid;

  var el = document.getElementById('logs-content');
  el.innerHTML = '<div class="loading">加载中...</div>';
  fetch('/api/cron/logs/' + sid).then(function(r){return r.json()}).then(function(j){
    if (!j.ok || !j.data || !j.data.length) {
      el.innerHTML = '<div class="empty-state"><div class="icon">📄</div><p>暂无执行记录</p></div>';
      return;
    }
    var h = '';
    for (var i = 0; i < j.data.length; i++) {
      var log = j.data[i];
      var ts = log.timestamp || log.filename;
      var size = log.size ? ' (' + (log.size > 1024 ? (log.size/1024).toFixed(1) + 'KB' : log.size + 'B') + ')' : '';
      var full = log.full_content || log.content || '';

      h += '<div class="log-entry" onclick="toggleLog(this)">';
      h += '<div class="log-time">📅 ' + escHtml(ts) + size + '</div>';
      h += '<div class="log-preview">' + escHtml((log.content || '').slice(0, 200)) + '</div>';
      h += '<div class="log-hint">▶ 点击展开完整日志</div>';
      h += '<div class="log-full">' + escHtml(full) + '</div>';
      h += '</div>';
    }
    el.innerHTML = h;
  }).catch(function(e){
    el.innerHTML = '<div class="empty-state"><p>加载失败: ' + e.message + '</p></div>';
  });
}

function toggleLog(el) {
  el.classList.toggle('open');
}

// Init
loadJobs();

// Auto-refresh every 60s
setInterval(function(){
  if (currentTab === 'config') loadJobs();
  else if (currentTab === 'logs' && selectedJobId) loadLogs();
}, 60000);
</script>
{% endblock %}