Initial Commit for rss-link-app

Analyze links from rss feeds
This commit is contained in:
Waylon Walker 2025-09-03 20:22:39 -05:00
commit 060f998c59
8 changed files with 1837 additions and 0 deletions

View file

@ -0,0 +1,62 @@
{% set pct = (100 * host.count // (max_count or 1)) %}
<article class="rounded-2xl bg-[var(--ra-panel)] border border-[var(--ra-copper)] overflow-hidden">
<header class="p-4 flex items-center justify-between gap-4">
<div class="min-w-0">
<h2 class="text-xl font-semibold break-all">{{ host.hostname }}</h2>
<div class="text-sm opacity-80">
<span class="mr-3">Links: <strong>{{ host.count }}</strong></span>
<span>Unique: <strong>{{ host.unique_link_count }}</strong></span>
</div>
</div>
{% if host.feed_url %}
<a href="{{ host.feed_url }}" target="_blank" rel="noopener"
class="shrink-0 px-3 py-1 rounded-lg bg-[var(--ra-amber)] text-[var(--ra-ink)] font-semibold hover:opacity-90">
RSS / Atom
</a>
{% endif %}
</header>
<div class="bar-wrap">
<div class="bar" style="width: {{ pct }}%"></div>
</div>
<div class="p-4 space-y-4">
{% if host.top_links %}
<div>
<div class="text-sm font-semibold mb-2">Top links (mentioned &gt; 1):</div>
<ul class="space-y-1 text-sm">
{% for tl in host.top_links %}
<li class="flex items-baseline gap-2">
<span class="inline-block px-2 py-0.5 rounded-md bg-[var(--ra-ruby)]">{{ tl.count }}</span>
<a class="link" href="{{ tl.url }}" target="_blank" rel="noopener">{{ tl.url }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% set list_id = "links-" ~ index %}
{% set links = host.links %}
{% set preview = links[:8] %}
{% set remainder = links[8:] %}
<div>
<div class="text-sm font-semibold mb-2">Links:</div>
<ul class="space-y-1 text-sm">
{% for url in preview %}
<li><a class="link" href="{{ url }}" target="_blank" rel="noopener">{{ url }}</a></li>
{% endfor %}
</ul>
{% if remainder %}
<div id="{{ list_id }}" class="more-list" data-expanded="false">
<ul class="space-y-1 text-sm">
{% for url in remainder %}
<li><a class="link" href="{{ url }}" target="_blank" rel="noopener">{{ url }}</a></li>
{% endfor %}
</ul>
</div>
<button class="btn-more mt-2" data-more-btn data-target="{{ list_id }}">More</button>
{% endif %}
</div>
</div>
</article>

104
templates/index.html Normal file
View file

@ -0,0 +1,104 @@
{% extends "layout.html" %}
{% block content %}
<section class="mx-auto max-w-3xl p-6">
<h1 class="text-3xl font-bold mb-2">RSS Link Audit</h1>
<p class="mb-6 opacity-90">Paste a feed URL. This version uses <strong>SQLite/SQLModel caching</strong> and streams progress over <strong>SSE</strong>.</p>
<form id="feed-form" class="space-y-4 bg-[var(--ra-panel)] p-5 rounded-2xl shadow">
<label class="block">
<span class="block mb-2 font-semibold">Feed URL</span>
<input id="feed-input" type="url" name="feed_url" placeholder="https://example.com/feed.xml"
required
class="w-full p-3 rounded-xl bg-[var(--ra-ink)] text-[var(--ra-cream)] border border-[var(--ra-copper)] focus:outline-none focus:ring-2 focus:ring-[var(--ra-amber)]" />
</label>
<button class="px-4 py-2 rounded-xl font-semibold bg-[var(--ra-ruby)] hover:bg-[var(--ra-ruby-dark)]">
Analyze
</button>
</form>
<div id="status" class="mt-6 text-sm opacity-80"></div>
<section id="summary" class="mt-6"></section>
<section id="hosts" class="mt-4 space-y-6"></section>
</section>
<script>
const statusEl = document.getElementById('status');
const hostsEl = document.getElementById('hosts');
const summaryEl = document.getElementById('summary');
const form = document.getElementById('feed-form');
function setStatus(html) { statusEl.innerHTML = html; }
function appendHostCard(html) {
const div = document.createElement('div');
div.innerHTML = html;
hostsEl.appendChild(div.firstElementChild);
}
function setSummary(feed_url, post_count, host_count) {
summaryEl.innerHTML = `
<div class="rounded-2xl bg-[var(--ra-panel)] border border-[var(--ra-copper)] p-4">
<div class="font-semibold mb-1">Summary</div>
<div>Feed: <a class="underline" href="${feed_url}" target="_blank" rel="noopener">${feed_url}</a></div>
<div>Posts parsed: <strong>${post_count}</strong></div>
<div>Hosts found: <strong>${host_count}</strong></div>
</div>`;
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
hostsEl.innerHTML = '';
summaryEl.innerHTML = '';
setStatus('Starting…');
const fd = new FormData(form);
const resp = await fetch('/start', { method: 'POST', body: fd });
if (!resp.ok) {
setStatus('Failed to start.');
return;
}
const { job_id } = await resp.json();
setStatus('Job started. Connecting…');
const es = new EventSource(`/events/${job_id}`);
let postCount = 0, hostsCount = 0, seenCards = 0;
es.addEventListener('hello', () => setStatus('Connected. Parsing feed…'));
es.addEventListener('status', (ev) => {
const d = JSON.parse(ev.data).data;
setStatus(`${d.message}`);
});
es.addEventListener('posts', (ev) => {
const data = JSON.parse(ev.data).data;
postCount = data.count || 0;
setStatus(`Posts: ${postCount}. Fetching pages…`);
});
es.addEventListener('post_progress', (ev) => {
const d = JSON.parse(ev.data).data;
setStatus(`Fetching posts ${d.current}/${d.total}…`);
});
es.addEventListener('hosts', (ev) => {
const data = JSON.parse(ev.data).data;
hostsCount = data.count || 0;
setStatus(`Found ${hostsCount} hosts. Discovering their feeds…`);
});
es.addEventListener('host_card', (ev) => {
const data = JSON.parse(ev.data).data;
appendHostCard(data.html);
seenCards = data.index;
setStatus(`Rendered ${seenCards}/${data.total} hosts… Still discovering feeds…`);
});
es.addEventListener('summary', (ev) => {
const data = JSON.parse(ev.data).data;
setSummary(data.feed_url, postCount, hostsCount);
});
es.addEventListener('error', (ev) => {
const data = JSON.parse(ev.data).data;
setStatus('Error: ' + (data.message || 'Unknown'));
});
es.addEventListener('done', () => {
setStatus('Done.');
es.close();
});
});
</script>
{% endblock %}

24
templates/layout.html Normal file
View file

@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>RSS Link Audit</title>
<link rel="stylesheet" href="/static/styles.css"/>
</head>
<body>
<header class="px-6 py-4 border-b border-[var(--ra-copper)]">
<div class="max-w-5xl mx-auto flex items-center gap-4">
<div class="w-3 h-3 rounded-full bg-[var(--ra-gold)]"></div>
<a href="/" class="font-bold hover:underline">RSS Link Audit</a>
<span class="opacity-70 text-sm">with SQLite cache + SSE</span>
</div>
</header>
<main class="max-w-5xl mx-auto">
{% block content %}{% endblock %}
</main>
<footer class="px-6 py-10 text-sm opacity-70">
<div class="max-w-5xl mx-auto">Built with FastAPI • Palette: Royal Armory</div>
</footer>
</body>
</html>