Initial Commit for rss-link-app
Analyze links from rss feeds
This commit is contained in:
commit
060f998c59
8 changed files with 1837 additions and 0 deletions
62
templates/components/host_card.html
Normal file
62
templates/components/host_card.html
Normal 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 > 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
104
templates/index.html
Normal 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
24
templates/layout.html
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue