chore: scaffold initial project structure with FastAPI app, SQLite DB, and Jinja2 templates
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}RSS Feed Manager{% endblock %}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: arial, sans-serif;
|
||||
background-color: #fff;
|
||||
color: #202124;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 20px 30px;
|
||||
border-bottom: 1px solid #ebebeb;
|
||||
}
|
||||
|
||||
.header a {
|
||||
color: #202124;
|
||||
text-decoration: none;
|
||||
font-size: 22px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.header a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #f8f9fa;
|
||||
border-radius: 4px;
|
||||
color: #3c4043;
|
||||
font-family: arial, sans-serif;
|
||||
font-size: 14px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #f1f3f4;
|
||||
border: 1px solid #dadce0;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #1a73e8;
|
||||
border: 1px solid #1a73e8;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #1765cc;
|
||||
border: 1px solid #1765cc;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #d93025;
|
||||
border: 1px solid #d93025;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #c5221f;
|
||||
border: 1px solid #c5221f;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="file"],
|
||||
select {
|
||||
border: 1px solid #dfe1e5;
|
||||
border-radius: 24px;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
font-family: arial, sans-serif;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
select:focus {
|
||||
border: 1px solid #1a73e8;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: #5f6368;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background-color: #fff;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ebebeb;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: #5f6368;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
color: #5f6368;
|
||||
}
|
||||
</style>
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<a href="/">RSS Feed Manager</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}RSS Feed Manager{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.splash {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 70vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.splash h1 {
|
||||
font-size: 48px;
|
||||
font-weight: normal;
|
||||
margin-bottom: 30px;
|
||||
color: #202124;
|
||||
}
|
||||
|
||||
.splash-actions {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.splash-actions a {
|
||||
margin: 0 10px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="splash">
|
||||
<h1>RSS Feed Manager</h1>
|
||||
<p style="font-size: 16px; color: #5f6368; margin-bottom: 20px;">
|
||||
Manage your RSS feeds efficiently
|
||||
</p>
|
||||
<div class="splash-actions">
|
||||
<a href="/manage" class="btn btn-primary">Manage Feeds</a>
|
||||
<a href="/newspapers" class="btn btn-primary">View Newspapers</a>
|
||||
<a href="/sync-logs" class="btn btn-primary">Sync Logs</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Create Feed - RSS Feed Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 style="margin-bottom: 20px; font-size: 32px; font-weight: normal;">Create New Feed</h1>
|
||||
|
||||
<div style="max-width: 600px; margin: 40px auto;">
|
||||
<form method="post" action="/manage/create">
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="url">URL</label>
|
||||
<input type="text" id="url" name="url" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="type">Type</label>
|
||||
<input type="text" id="type" name="type" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="category">Category</label>
|
||||
<input type="text" id="category" name="category" required>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px;">
|
||||
<button type="submit" class="btn btn-primary">Create Feed</button>
|
||||
<a href="/manage" class="btn">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,36 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Edit Feed - RSS Feed Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 style="margin-bottom: 20px; font-size: 32px; font-weight: normal;">Edit Feed</h1>
|
||||
|
||||
<div style="max-width: 600px; margin: 40px auto;">
|
||||
<form method="post" action="/manage/edit/{{ feed.id }}">
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name" value="{{ feed.name }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="url">URL</label>
|
||||
<input type="text" id="url" name="url" value="{{ feed.url }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="type">Type</label>
|
||||
<input type="text" id="type" name="type" value="{{ feed.type }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="category">Category</label>
|
||||
<input type="text" id="category" name="category" value="{{ feed.category }}" required>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px;">
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
<a href="/manage" class="btn">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,64 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Manage Feeds - RSS Feed Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 style="margin-bottom: 20px; font-size: 32px; font-weight: normal;">Manage Feeds</h1>
|
||||
|
||||
<div class="stats">
|
||||
Total Feeds: <strong>{{ total }}</strong>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<a href="/manage/create" class="btn btn-primary">Create New Feed</a>
|
||||
<a href="/manage/upload" class="btn btn-primary">Upload Feeds</a>
|
||||
<a href="/manage/sync" class="btn btn-primary">Sync All Feeds</a>
|
||||
<a href="/newspapers" class="btn btn-primary">View Newspapers</a>
|
||||
<a href="/sync-logs" class="btn btn-primary">Sync Logs</a>
|
||||
<a href="/" class="btn">Back to Home</a>
|
||||
</div>
|
||||
|
||||
{% if feeds %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>URL</th>
|
||||
<th>Type</th>
|
||||
<th>Category</th>
|
||||
<th>Last Synced</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for feed in feeds %}
|
||||
<tr>
|
||||
<td>{{ feed.name }}</td>
|
||||
<td style="font-size: 12px; color: #5f6368;">{{ feed.url }}</td>
|
||||
<td>{{ feed.type }}</td>
|
||||
<td>{{ feed.category }}</td>
|
||||
<td style="font-size: 12px; color: #5f6368;">
|
||||
{% if feed.last_synced %}
|
||||
{{ feed.last_synced[:19] }}
|
||||
{% else %}
|
||||
Never
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="actions">
|
||||
<a href="/manage/edit/{{ feed.id }}" class="btn">Edit</a>
|
||||
<form method="post" action="/manage/delete/{{ feed.id }}" style="display: inline;">
|
||||
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure?')">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p style="text-align: center; padding: 40px; color: #5f6368;">
|
||||
No feeds found. Upload a JSON file to get started.
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,240 @@
|
||||
{% 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 %}
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Upload Feeds - RSS Feed Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 style="margin-bottom: 20px; font-size: 32px; font-weight: normal;">Upload Feeds</h1>
|
||||
|
||||
<div style="max-width: 600px; margin: 40px auto;">
|
||||
<form method="post" action="/manage/upload" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="file">Select JSON File</label>
|
||||
<input type="file" id="file" name="file" accept=".json" required>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 30px;">
|
||||
<button type="submit" class="btn btn-primary">Upload and Sync</button>
|
||||
<a href="/manage" class="btn">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div style="margin-top: 40px; padding: 20px; background-color: #f8f9fa; border-radius: 8px; font-size: 14px; color: #5f6368;">
|
||||
<strong>Note:</strong> The upload will synchronize feeds based on their URL.
|
||||
Existing feeds with the same URL will be updated, and new feeds will be added.
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,198 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Tech News - {{ newspaper.created_at[:10] }}</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Georgia', 'Times New Roman', serif;
|
||||
background-color: #f8f9fa;
|
||||
color: #202124;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.newspaper {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background-color: #fff;
|
||||
padding: 60px 80px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.masthead {
|
||||
text-align: center;
|
||||
border-bottom: 4px double #202124;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.newspaper-title {
|
||||
font-size: 72px;
|
||||
font-weight: bold;
|
||||
font-family: 'Georgia', serif;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.newspaper-date {
|
||||
font-size: 16px;
|
||||
color: #5f6368;
|
||||
font-family: arial, sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.article-grid {
|
||||
column-count: 2;
|
||||
column-gap: 40px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.article {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 30px;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.article-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.3;
|
||||
color: #202124;
|
||||
}
|
||||
|
||||
.article-title a {
|
||||
color: #202124;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.article-title a:hover {
|
||||
color: #1a73e8;
|
||||
}
|
||||
|
||||
.article-meta {
|
||||
font-size: 12px;
|
||||
color: #5f6368;
|
||||
margin-bottom: 10px;
|
||||
font-family: arial, sans-serif;
|
||||
}
|
||||
|
||||
.article-source {
|
||||
font-weight: bold;
|
||||
color: #1a73e8;
|
||||
}
|
||||
|
||||
.article-description {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
text-align: justify;
|
||||
color: #3c4043;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.article-image {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
height: auto;
|
||||
margin: 15px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
border-top: 2px solid #e8eaed;
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 60px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e8eaed;
|
||||
font-size: 12px;
|
||||
color: #5f6368;
|
||||
font-family: arial, sans-serif;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
background-color: #fff;
|
||||
padding: 0;
|
||||
}
|
||||
.newspaper {
|
||||
box-shadow: none;
|
||||
padding: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.article-grid {
|
||||
column-count: 1;
|
||||
}
|
||||
.newspaper {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
.newspaper-title {
|
||||
font-size: 48px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="newspaper">
|
||||
<div class="masthead">
|
||||
<div class="newspaper-title">TECH NEWS</div>
|
||||
<div class="newspaper-date">{{ newspaper.created_at[:10] }} - {{ newspaper.created_at[11:19] }}</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-bottom: 40px;">
|
||||
<p style="font-size: 18px; color: #5f6368; font-family: arial, sans-serif;">
|
||||
{{ newspaper.article_count }} New Articles from Latest Synchronization
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="article-grid">
|
||||
{% for article in articles %}
|
||||
<article class="article">
|
||||
<h2 class="article-title">
|
||||
<a href="{{ article.link }}" target="_blank">{{ article.title }}</a>
|
||||
</h2>
|
||||
<div class="article-meta">
|
||||
<span class="article-source">{{ article.feed_name }}</span>
|
||||
{% if article.author %}
|
||||
| By {{ article.author }}
|
||||
{% endif %}
|
||||
{% if article.published %}
|
||||
| {{ article.published[:10] }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="article-description">
|
||||
{% set full_content = article.content if article.content else article.description %}
|
||||
{% set clean_text = full_content|striptags %}
|
||||
{% set display_text = clean_text[:500] if article.content else clean_text[:300] %}
|
||||
{{ display_text }}{% if clean_text|length > (500 if article.content else 300) %}...{% endif %}
|
||||
</div>
|
||||
|
||||
{% set words = full_content.split() %}
|
||||
{% for word in words %}
|
||||
{% if 'https' in word and ('.jpg' in word or '.jpeg' in word or '.png' in word or '.gif' in word or '.webp' in word) %}
|
||||
<img src="{{ word }}" alt="Article image" class="article-image" onerror="this.style.display='none'">
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
Generated from RSS Feed Manager | Synchronized at {{ newspaper.created_at[:19].replace('T', ' ') }}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,39 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Newspapers - RSS Feed Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 style="margin-bottom: 20px; font-size: 32px; font-weight: normal;">Newspapers</h1>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<a href="/sync-logs" class="btn btn-primary">View Sync Logs</a>
|
||||
<a href="/manage" class="btn">Back to Manage</a>
|
||||
</div>
|
||||
|
||||
{% if newspapers %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date & Time</th>
|
||||
<th>Articles</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for newspaper in newspapers %}
|
||||
<tr>
|
||||
<td>{{ newspaper.created_at[:19].replace('T', ' ') }}</td>
|
||||
<td>{{ newspaper.article_count }}</td>
|
||||
<td>
|
||||
<a href="/newspaper/{{ newspaper.id }}" class="btn btn-primary" target="_blank">Read Newspaper</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p style="text-align: center; padding: 40px; color: #5f6368;">
|
||||
No newspapers generated yet. Run a synchronization to generate one.
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,73 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Synchronization Logs - RSS Feed Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1 style="margin-bottom: 20px; font-size: 32px; font-weight: normal;">Synchronization Logs</h1>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<a href="/newspapers" class="btn">Back to Newspapers</a>
|
||||
<a href="/manage" class="btn">Back to Manage</a>
|
||||
</div>
|
||||
|
||||
{% if sync_logs %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Sync Time</th>
|
||||
<th>Total Feeds</th>
|
||||
<th>Completed</th>
|
||||
<th>Failed</th>
|
||||
<th>Total Articles</th>
|
||||
<th>New Articles</th>
|
||||
<th>Duration (s)</th>
|
||||
<th>Req/s</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in sync_logs %}
|
||||
<tr>
|
||||
<td>{{ log.sync_time[:19].replace('T', ' ') }}</td>
|
||||
<td>{{ log.total_feeds }}</td>
|
||||
<td>{{ log.completed_feeds }}</td>
|
||||
<td style="color: {% if log.failed_feeds > 0 %}#d93025{% else %}#5f6368{% endif %};">
|
||||
{{ log.failed_feeds }}
|
||||
</td>
|
||||
<td>{{ log.total_articles_processed }}</td>
|
||||
<td style="color: {% if log.new_articles > 0 %}#1e8e3e{% else %}#5f6368{% endif %};">
|
||||
{{ log.new_articles }}
|
||||
</td>
|
||||
<td>{{ log.elapsed_seconds }}</td>
|
||||
<td>{{ log.avg_req_per_sec }}</td>
|
||||
<td>
|
||||
{% if log.timed_out %}
|
||||
<span style="color: #f9ab00;">Timeout</span>
|
||||
{% else %}
|
||||
<span style="color: #1e8e3e;">Complete</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div style="margin-top: 40px; padding: 20px; background-color: #f8f9fa; border-radius: 8px;">
|
||||
<h3 style="font-size: 18px; font-weight: normal; margin-bottom: 15px;">Statistics Legend</h3>
|
||||
<ul style="list-style: none; padding: 0; font-size: 14px; color: #5f6368;">
|
||||
<li style="margin-bottom: 8px;"><strong>Total Feeds:</strong> Number of RSS feeds configured</li>
|
||||
<li style="margin-bottom: 8px;"><strong>Completed:</strong> Number of feeds successfully fetched</li>
|
||||
<li style="margin-bottom: 8px;"><strong>Failed:</strong> Number of feeds that failed to fetch</li>
|
||||
<li style="margin-bottom: 8px;"><strong>Total Articles:</strong> All articles processed from feeds</li>
|
||||
<li style="margin-bottom: 8px;"><strong>New Articles:</strong> Articles added (not duplicates)</li>
|
||||
<li style="margin-bottom: 8px;"><strong>Duration:</strong> Total sync time in seconds</li>
|
||||
<li style="margin-bottom: 8px;"><strong>Req/s:</strong> Average requests per second</li>
|
||||
<li style="margin-bottom: 8px;"><strong>Status:</strong> Complete or Timeout (5 min limit reached)</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<p style="text-align: center; padding: 40px; color: #5f6368;">
|
||||
No synchronization logs available. Run a sync to see logs here.
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user