ADR-017 Phase 3f-4:根目錄模板搬入 templates/,補 trends/login_history,移除 ChoiceLoader 根目錄 fallback,搬移 components,刪除 web/templates 下的空檔/死檔與 compose 舊模板 mount。
427 lines
13 KiB
HTML
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>
|