feat: 添加主机 Cron 列表展示功能

This commit is contained in:
2026-04-14 10:34:24 +08:00
parent b44d0cfd00
commit 9a23e98dc1
8 changed files with 87900 additions and 1 deletions

248
app.py
View File

@@ -244,7 +244,7 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
</div>
<!-- 统计卡片 -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
<div class="card rounded-xl p-4">
<div class="flex items-center justify-between">
<div>
@@ -281,6 +281,34 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
<i class="ri-terminal-box-line text-3xl text-purple-400 opacity-50"></i>
</div>
</div>
<div class="card rounded-xl p-4">
<div class="flex items-center justify-between">
<div>
<p class="text-gray-400 text-sm">系统Cron</p>
<p id="systemCronCount" class="text-2xl font-bold text-orange-400">-</p>
</div>
<i class="ri-timer-line text-3xl text-orange-400 opacity-50"></i>
</div>
</div>
</div>
<!-- 系统 Cron 列表 -->
<div class="card rounded-xl p-6 mb-8">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-bold flex items-center gap-2">
<i class="ri-timer-line text-orange-400"></i>
主机 Cron 列表
</h2>
<button onclick="loadCrons()" class="btn bg-orange-600 hover:bg-orange-700 px-3 py-1 rounded text-sm flex items-center gap-1">
<i class="ri-refresh-line"></i> 刷新
</button>
</div>
<div id="cronsList" class="space-y-3">
<div class="text-center py-8 text-gray-400">
<i class="ri-loader-4-line text-2xl animate-spin"></i>
<p class="mt-2 text-sm">加载中...</p>
</div>
</div>
</div>
<!-- 筛选器 -->
@@ -634,9 +662,84 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
// 初始化
loadProjects();
loadCrons();
// 每30秒自动刷新
setInterval(loadProjects, 30000);
async function loadCrons() {
try {
const res = await fetch('/api/crons');
const data = await res.json();
renderCrons(data.crons);
document.getElementById('systemCronCount').textContent = data.crons.length;
} catch (e) {
console.error('加载Cron失败:', e);
document.getElementById('cronsList').innerHTML = `
<div class="text-center py-8 text-red-400">
<i class="ri-error-warning-line text-2xl"></i>
<p class="mt-2 text-sm">加载失败: ${e.message}</p>
</div>
`;
}
}
function renderCrons(crons) {
const list = document.getElementById('cronsList');
if (crons.length === 0) {
list.innerHTML = `
<div class="text-center py-8 text-gray-400">
<i class="ri-folder-open-line text-2xl"></i>
<p class="mt-2 text-sm">暂无 Cron 任务</p>
</div>
`;
return;
}
const sourceColors = {
'user': { bg: 'bg-blue-500/20', text: 'text-blue-400', label: '用户' },
'system': { bg: 'bg-red-500/20', text: 'text-red-400', label: '系统' },
'cron.d': { bg: 'bg-green-500/20', text: 'text-green-400', label: 'cron.d' }
};
let html = '';
crons.forEach((cron, index) => {
const sourceInfo = sourceColors[cron.source] || { bg: 'bg-gray-500/20', text: 'text-gray-400', label: cron.source };
html += `
<div class="bg-gray-800/50 rounded-lg p-3 border border-gray-700">
<div class="flex items-start justify-between gap-4">
<div class="flex-1">
<div class="flex items-center gap-2 mb-2">
<span class="px-2 py-0.5 rounded text-xs ${sourceInfo.bg} ${sourceInfo.text}">${sourceInfo.label}</span>
${cron.file ? `<span class="text-xs text-gray-500">${cron.file}</span>` : ''}
${cron.user ? `<span class="text-xs text-gray-400">用户: ${cron.user}</span>` : ''}
</div>
<div class="flex items-center gap-3">
<code class="text-yellow-400 text-sm font-mono">${cron.expression}</code>
<span class="text-gray-500 text-xs">→</span>
<span class="text-green-400 text-sm">${cron.description || ''}</span>
</div>
<div class="mt-2 text-gray-300 text-sm font-mono break-all">
${escapeHtml(cron.command)}
</div>
</div>
<div class="text-right">
<span class="text-xs text-gray-500">#${index + 1}</span>
</div>
</div>
</div>
`;
});
list.innerHTML = html;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>
@@ -821,6 +924,149 @@ def api_delete_project(project_id):
return jsonify({'message': '删除成功'})
def get_system_crons():
"""获取系统所有 cron 任务"""
crons = []
# 获取当前用户的 crontab
try:
result = subprocess.run(
"crontab -l 2>/dev/null",
shell=True, capture_output=True, text=True, timeout=5
)
user_crontab = result.stdout.strip()
if user_crontab:
for line in user_crontab.split('\n'):
line = line.strip()
if line and not line.startswith('#'):
# 解析 cron 行
parts = line.split()
if len(parts) >= 6:
cron_expr = ' '.join(parts[:5])
command = ' '.join(parts[5:])
crons.append({
'source': 'user',
'expression': cron_expr,
'command': command,
'line': line
})
except:
pass
# 获取系统级 cron (/etc/crontab)
try:
if os.path.exists('/etc/crontab'):
with open('/etc/crontab', 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
parts = line.split()
if len(parts) >= 7:
# 系统 crontab 有额外的 user 字段
cron_expr = ' '.join(parts[:5])
user = parts[5]
command = ' '.join(parts[6:])
crons.append({
'source': 'system',
'expression': cron_expr,
'user': user,
'command': command,
'line': line
})
except:
pass
# 获取 /etc/cron.d/ 目录下的任务
try:
cron_d_dir = '/etc/cron.d'
if os.path.exists(cron_d_dir):
for filename in os.listdir(cron_d_dir):
filepath = os.path.join(cron_d_dir, filename)
if os.path.isfile(filepath):
with open(filepath, 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
parts = line.split()
if len(parts) >= 7:
cron_expr = ' '.join(parts[:5])
user = parts[5]
command = ' '.join(parts[6:])
crons.append({
'source': 'cron.d',
'file': filename,
'expression': cron_expr,
'user': user,
'command': command,
'line': line
})
except:
pass
return crons
def parse_cron_expression(expr):
"""解析 cron 表达式为人类可读格式"""
parts = expr.split()
if len(parts) != 5:
return expr
minute, hour, day, month, weekday = parts
desc = []
# 分钟
if minute == '*':
desc.append('每分钟')
elif minute.startswith('*/'):
desc.append(f'{minute[2:]}分钟')
else:
desc.append(f'{minute}')
# 小时
if hour != '*':
if minute == '*':
desc = [f'{hour}点每分钟']
else:
desc = [f'{hour}{minute}']
elif minute.startswith('*/'):
# 保持 "每X分钟"
pass
# 日期
if day != '*':
desc.append(f'{day}')
# 月份
if month != '*':
months = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']
try:
desc.append(f'{months[int(month)-1]}')
except:
desc.append(f'{month}')
# 星期
if weekday != '*':
weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
try:
# 0=周日, 1=周一...
desc.append(weekdays[int(weekday)])
except:
desc.append(f'{weekday}')
return ' '.join(desc) if desc else expr
@app.route('/api/crons')
def api_crons():
"""获取系统 cron 列表"""
crons = get_system_crons()
for cron in crons:
cron['description'] = parse_cron_expression(cron['expression'])
return jsonify({'crons': crons})
if __name__ == '__main__':
os.makedirs(os.path.join(BASE_DIR, 'logs'), exist_ok=True)
print("=" * 50)