241 lines
7.3 KiB
HTML
241 lines
7.3 KiB
HTML
|
|
{% extends "base.html" %}
|
||
|
|
|
||
|
|
{% block title %}Sync Feeds - RSS Feed Manager{% endblock %}
|
||
|
|
|
||
|
|
{% block extra_css %}
|
||
|
|
<style>
|
||
|
|
.sync-container {
|
||
|
|
max-width: 900px;
|
||
|
|
margin: 0 auto;
|
||
|
|
}
|
||
|
|
|
||
|
|
.sync-stats {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
|
|
gap: 15px;
|
||
|
|
margin-bottom: 30px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-box {
|
||
|
|
background-color: #f8f9fa;
|
||
|
|
border-radius: 8px;
|
||
|
|
padding: 20px;
|
||
|
|
text-align: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-value {
|
||
|
|
font-size: 32px;
|
||
|
|
font-weight: bold;
|
||
|
|
color: #1a73e8;
|
||
|
|
display: block;
|
||
|
|
margin-bottom: 5px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.stat-label {
|
||
|
|
font-size: 14px;
|
||
|
|
color: #5f6368;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-container {
|
||
|
|
background-color: #f8f9fa;
|
||
|
|
border-radius: 8px;
|
||
|
|
padding: 20px;
|
||
|
|
height: 400px;
|
||
|
|
overflow-y: auto;
|
||
|
|
font-family: 'Courier New', monospace;
|
||
|
|
font-size: 13px;
|
||
|
|
margin-bottom: 20px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-entry {
|
||
|
|
padding: 5px 0;
|
||
|
|
border-bottom: 1px solid #e8eaed;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-entry:last-child {
|
||
|
|
border-bottom: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-time {
|
||
|
|
color: #5f6368;
|
||
|
|
margin-right: 10px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-fetching {
|
||
|
|
color: #1a73e8;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-parsed {
|
||
|
|
color: #1e8e3e;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-error {
|
||
|
|
color: #d93025;
|
||
|
|
}
|
||
|
|
|
||
|
|
.log-complete {
|
||
|
|
color: #1e8e3e;
|
||
|
|
font-weight: bold;
|
||
|
|
}
|
||
|
|
|
||
|
|
.progress-bar {
|
||
|
|
background-color: #e8eaed;
|
||
|
|
border-radius: 4px;
|
||
|
|
height: 8px;
|
||
|
|
overflow: hidden;
|
||
|
|
margin-bottom: 30px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.progress-fill {
|
||
|
|
background-color: #1a73e8;
|
||
|
|
height: 100%;
|
||
|
|
width: 0%;
|
||
|
|
transition: width 0.3s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
#syncButton:disabled {
|
||
|
|
opacity: 0.5;
|
||
|
|
cursor: not-allowed;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
{% endblock %}
|
||
|
|
|
||
|
|
{% block content %}
|
||
|
|
<div class="sync-container">
|
||
|
|
<h1 style="margin-bottom: 20px; font-size: 32px; font-weight: normal;">Synchronize RSS Feeds</h1>
|
||
|
|
|
||
|
|
<div class="stats" style="background-color: #f8f9fa; border-radius: 8px; padding: 16px; margin-bottom: 20px; font-size: 14px; color: #5f6368;">
|
||
|
|
Total Feeds: <strong>{{ total_feeds }}</strong> | Total Articles in DB: <strong>{{ total_articles }}</strong>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div style="margin-bottom: 30px;">
|
||
|
|
<button id="syncButton" class="btn btn-primary">Start Synchronization</button>
|
||
|
|
<a href="/manage" class="btn">Back to Manage</a>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="progress-bar" id="progressBar" style="display: none;">
|
||
|
|
<div class="progress-fill" id="progressFill"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="sync-stats" id="statsContainer" style="display: none;">
|
||
|
|
<div class="stat-box">
|
||
|
|
<span class="stat-value" id="statCompleted">0</span>
|
||
|
|
<span class="stat-label">Completed</span>
|
||
|
|
</div>
|
||
|
|
<div class="stat-box">
|
||
|
|
<span class="stat-value" id="statTotal">0</span>
|
||
|
|
<span class="stat-label">Total Feeds</span>
|
||
|
|
</div>
|
||
|
|
<div class="stat-box">
|
||
|
|
<span class="stat-value" id="statArticles">0</span>
|
||
|
|
<span class="stat-label">Articles Synced</span>
|
||
|
|
</div>
|
||
|
|
<div class="stat-box">
|
||
|
|
<span class="stat-value" id="statReqPerSec">0</span>
|
||
|
|
<span class="stat-label">Req/s</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="log-container" id="logContainer" style="display: none;"></div>
|
||
|
|
</div>
|
||
|
|
{% endblock %}
|
||
|
|
|
||
|
|
{% block extra_js %}
|
||
|
|
<script>
|
||
|
|
const syncButton = document.getElementById('syncButton');
|
||
|
|
const logContainer = document.getElementById('logContainer');
|
||
|
|
const statsContainer = document.getElementById('statsContainer');
|
||
|
|
const progressBar = document.getElementById('progressBar');
|
||
|
|
const progressFill = document.getElementById('progressFill');
|
||
|
|
|
||
|
|
const statCompleted = document.getElementById('statCompleted');
|
||
|
|
const statTotal = document.getElementById('statTotal');
|
||
|
|
const statArticles = document.getElementById('statArticles');
|
||
|
|
const statReqPerSec = document.getElementById('statReqPerSec');
|
||
|
|
|
||
|
|
function addLog(message, type = 'info') {
|
||
|
|
const entry = document.createElement('div');
|
||
|
|
entry.className = `log-entry log-${type}`;
|
||
|
|
|
||
|
|
const time = new Date().toLocaleTimeString();
|
||
|
|
entry.innerHTML = `<span class="log-time">[${time}]</span> ${message}`;
|
||
|
|
|
||
|
|
logContainer.appendChild(entry);
|
||
|
|
logContainer.scrollTop = logContainer.scrollHeight;
|
||
|
|
}
|
||
|
|
|
||
|
|
function updateStats(completed, total, articles, reqPerSec) {
|
||
|
|
statCompleted.textContent = completed;
|
||
|
|
statTotal.textContent = total;
|
||
|
|
statArticles.textContent = articles;
|
||
|
|
statReqPerSec.textContent = reqPerSec;
|
||
|
|
|
||
|
|
const percentage = (completed / total) * 100;
|
||
|
|
progressFill.style.width = percentage + '%';
|
||
|
|
}
|
||
|
|
|
||
|
|
syncButton.addEventListener('click', () => {
|
||
|
|
syncButton.disabled = true;
|
||
|
|
logContainer.style.display = 'block';
|
||
|
|
statsContainer.style.display = 'grid';
|
||
|
|
progressBar.style.display = 'block';
|
||
|
|
logContainer.innerHTML = '';
|
||
|
|
progressFill.style.width = '0%';
|
||
|
|
|
||
|
|
const ws = new WebSocket('ws://127.0.0.1:8592/ws/sync');
|
||
|
|
|
||
|
|
ws.onopen = () => {
|
||
|
|
addLog('WebSocket connection established', 'info');
|
||
|
|
};
|
||
|
|
|
||
|
|
ws.onmessage = (event) => {
|
||
|
|
const data = JSON.parse(event.data);
|
||
|
|
|
||
|
|
switch(data.type) {
|
||
|
|
case 'start':
|
||
|
|
addLog(data.message, 'info');
|
||
|
|
updateStats(0, data.total, 0, 0);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'fetching':
|
||
|
|
addLog(`Fetching: ${data.feed} (${data.url})`, 'fetching');
|
||
|
|
updateStats(data.completed, data.total, statArticles.textContent, data.req_per_sec);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'parsed':
|
||
|
|
addLog(`✓ Parsed: ${data.feed} - ${data.articles} articles added`, 'parsed');
|
||
|
|
updateStats(data.completed, data.total, data.total_articles, data.req_per_sec);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'timeout':
|
||
|
|
addLog(data.message, 'info');
|
||
|
|
updateStats(data.completed, data.total, statArticles.textContent, statReqPerSec.textContent);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'error':
|
||
|
|
addLog(`✗ Error: ${data.feed} - ${data.error}`, 'error');
|
||
|
|
if (data.completed) {
|
||
|
|
updateStats(data.completed, data.total, statArticles.textContent, data.req_per_sec);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'complete':
|
||
|
|
addLog(`✓ Synchronization complete! ${data.total_feeds} feeds processed, ${data.total_articles} articles synced in ${data.elapsed}s (avg ${data.avg_req_per_sec} req/s)`, 'complete');
|
||
|
|
syncButton.disabled = false;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
ws.onerror = (error) => {
|
||
|
|
addLog('WebSocket error occurred', 'error');
|
||
|
|
syncButton.disabled = false;
|
||
|
|
};
|
||
|
|
|
||
|
|
ws.onclose = () => {
|
||
|
|
addLog('WebSocket connection closed', 'info');
|
||
|
|
syncButton.disabled = false;
|
||
|
|
};
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
{% endblock %}
|