feat: 添加主机 Cron 列表展示功能
This commit is contained in:
248
app.py
248
app.py
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user