Files
ewoooc/templates/crawler_management.html
OoO 53edcc0077 refactor(templates): 統一模板目錄並移除 fallback loader
ADR-017 Phase 3f-4:根目錄模板搬入 templates/,補 trends/login_history,移除 ChoiceLoader 根目錄 fallback,搬移 components,刪除 web/templates 下的空檔/死檔與 compose 舊模板 mount。
2026-04-29 21:44:38 +08:00

427 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>爬蟲管理 - MOMO Pro System</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<style>
.crawler-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 20px;
margin-bottom: 20px;
transition: box-shadow 0.3s;
}
.crawler-card:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.crawler-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.crawler-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
.status-active {
background: #10b981;
color: white;
}
.status-paused {
background: #f59e0b;
color: white;
}
.crawler-info {
color: #666;
font-size: 14px;
margin: 8px 0;
}
.crawler-controls {
display: flex;
gap: 10px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #e5e7eb;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #10b981;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.btn-secondary {
background: #6b7280;
color: white;
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.btn-secondary:hover {
background: #4b5563;
}
.pause-reason {
background: #fef3c7;
border-left: 4px solid #f59e0b;
padding: 10px;
margin-top: 10px;
border-radius: 4px;
}
.alert {
padding: 12px;
border-radius: 4px;
margin-bottom: 20px;
}
.alert-success {
background: #d1fae5;
border: 1px solid #10b981;
color: #065f46;
}
.alert-error {
background: #fee2e2;
border: 1px solid #ef4444;
color: #991b1b;
}
.stats-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.stat-box {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
}
.stat-number {
font-size: 32px;
font-weight: bold;
color: #1f2937;
}
.stat-label {
color: #6b7280;
font-size: 14px;
margin-top: 5px;
}
.navbar-dark.bg-primary {
background: linear-gradient(135deg, #4F46E5 0%, #6366F1 100%) !important;
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
.navbar-dark .navbar-brand {
color: #ffffff !important;
font-weight: 600;
}
.navbar-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, 0.9) !important;
font-weight: 500;
transition: all 0.3s;
}
.navbar-dark .navbar-nav .nav-link:hover {
color: #ffffff !important;
background: rgba(255, 255, 255, 0.1);
border-radius: 6px;
}
.navbar-dark .navbar-nav .nav-link.active {
color: #ffffff !important;
background: rgba(255, 255, 255, 0.15);
border-radius: 6px;
font-weight: 600;
}
.navbar-dark .navbar-text {
color: rgba(255, 255, 255, 0.8) !important;
}
/* Fixed Navbar Compensation */
body {
padding-top: 70px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔧 爬蟲管理</h1>
<p>管理系統爬蟲的啟用狀態和執行頻率</p>
</div>
<div id="alert-container"></div>
<div class="stats-summary">
<div class="stat-box">
<div class="stat-number" id="enabled-count">0</div>
<div class="stat-label">啟用中</div>
</div>
<div class="stat-box">
<div class="stat-number" id="paused-count">0</div>
<div class="stat-label">已暫停</div>
</div>
<div class="stat-box">
<div class="stat-number" id="total-count">0</div>
<div class="stat-label">爬蟲總數</div>
</div>
</div>
<div id="crawlers-container">
<!-- 爬蟲卡片將通過 JavaScript 動態插入 -->
</div>
<div style="margin-top: 30px; padding: 15px; background: #f3f4f6; border-radius: 8px;">
<h3 style="margin-top: 0;">💡 使用說明</h3>
<ul style="color: #6b7280; line-height: 1.8;">
<li>切換開關可以啟用或停用爬蟲</li>
<li>停用的爬蟲程式碼和資料會保留,隨時可以重新啟用</li>
<li>變更執行頻率後,需要重啟排程器服務才會生效</li>
<li>重啟排程器:<code>sudo systemctl restart momo-scheduler</code></li>
</ul>
</div>
</div>
<script>
// 載入爬蟲配置
async function loadCrawlers() {
try {
const response = await fetch('/api/crawlers');
const result = await response.json();
if (result.status === 'success') {
renderCrawlers(result.data);
updateStats(result.data);
} else {
showAlert('載入失敗: ' + result.message, 'error');
}
} catch (error) {
showAlert('載入爬蟲配置時發生錯誤', 'error');
console.error(error);
}
}
// 渲染爬蟲卡片
function renderCrawlers(crawlers) {
const container = document.getElementById('crawlers-container');
container.innerHTML = '';
for (const [key, info] of Object.entries(crawlers)) {
const card = createCrawlerCard(key, info);
container.appendChild(card);
}
}
// 創建爬蟲卡片
function createCrawlerCard(key, info) {
const card = document.createElement('div');
card.className = 'crawler-card';
const statusClass = info.enabled ? 'status-active' : 'status-paused';
const statusText = info.enabled ? '運行中' : '已暫停';
card.innerHTML = `
<div class="crawler-header">
<div>
<div class="crawler-title">${info.name}</div>
<span class="status-badge ${statusClass}">${statusText}</span>
</div>
<label class="toggle-switch">
<input type="checkbox" ${info.enabled ? 'checked' : ''}
onchange="toggleCrawler('${key}', this.checked)">
<span class="slider"></span>
</label>
</div>
<div class="crawler-info">
<div>📝 ${info.description || 'N/A'}</div>
<div>⏰ 執行頻率:每 ${info.schedule_hours || 'N/A'} 小時</div>
${info.lpn_code ? `<div>🔖 活動代碼:${info.lpn_code}</div>` : ''}
${info.last_active_date ? `<div>📅 最後活動:${info.last_active_date}</div>` : ''}
</div>
${!info.enabled && info.pause_reason ? `
<div class="pause-reason">
<strong>⏸️ 暫停原因:</strong>${info.pause_reason}
${info.notes ? `<br><small>${info.notes}</small>` : ''}
</div>
` : ''}
<div class="crawler-controls">
${info.enabled ? `
<button class="btn-secondary" onclick="changeSchedule('${key}', ${info.schedule_hours})">
修改頻率
</button>
` : ''}
</div>
`;
return card;
}
// 更新統計資料
function updateStats(crawlers) {
const total = Object.keys(crawlers).length;
const enabled = Object.values(crawlers).filter(c => c.enabled).length;
const paused = total - enabled;
document.getElementById('enabled-count').textContent = enabled;
document.getElementById('paused-count').textContent = paused;
document.getElementById('total-count').textContent = total;
}
// 切換爬蟲狀態
async function toggleCrawler(key, enabled) {
let reason = '';
if (!enabled) {
reason = prompt('請輸入停用原因(可選):');
}
try {
const response = await fetch(`/api/crawlers/${key}/toggle`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ enabled, reason })
});
const result = await response.json();
if (result.status === 'success') {
showAlert(result.message, 'success');
loadCrawlers(); // 重新載入
} else {
showAlert('操作失敗: ' + result.message, 'error');
loadCrawlers(); // 恢復原狀
}
} catch (error) {
showAlert('操作時發生錯誤', 'error');
console.error(error);
loadCrawlers();
}
}
// 修改執行頻率
async function changeSchedule(key, currentHours) {
const newHours = prompt(`請輸入新的執行頻率(小時)\n目前:每 ${currentHours} 小時`, currentHours);
if (newHours === null || newHours === currentHours.toString()) {
return; // 使用者取消或未變更
}
try {
const response = await fetch(`/api/crawlers/${key}/schedule`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ schedule_hours: parseInt(newHours) })
});
const result = await response.json();
if (result.status === 'success') {
showAlert(result.message + '(需重啟排程器生效)', 'success');
loadCrawlers();
} else {
showAlert('更新失敗: ' + result.message, 'error');
}
} catch (error) {
showAlert('更新時發生錯誤', 'error');
console.error(error);
}
}
// 顯示提示訊息
function showAlert(message, type) {
const container = document.getElementById('alert-container');
const alert = document.createElement('div');
alert.className = `alert alert-${type}`;
alert.textContent = message;
container.appendChild(alert);
setTimeout(() => {
alert.remove();
}, 5000);
}
// 頁面載入時執行
document.addEventListener('DOMContentLoaded', loadCrawlers);
</script>
</body>
</html>