chore: scaffold initial project structure with FastAPI app, SQLite DB, and Jinja2 templates

This commit is contained in:
2025-10-02 19:17:36 +00:00
commit 93e1c282aa
16 changed files with 3281 additions and 0 deletions
+162
View File
@@ -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>
+45
View File
@@ -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 %}
+36
View File
@@ -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 %}
+36
View File
@@ -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 %}
+64
View File
@@ -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 %}
+240
View File
@@ -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 %}
+26
View File
@@ -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 %}
+198
View File
@@ -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>
+39
View File
@@ -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 %}
+73
View File
@@ -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 %}