feat: complete web UI — dashboard, articles, publish, settings, issues

Made-with: Cursor
This commit is contained in:
cottongin
2026-04-06 15:21:18 -04:00
parent 50ff2e1533
commit ec9f31f072
13 changed files with 718 additions and 0 deletions

48
templates/articles.html Normal file
View File

@@ -0,0 +1,48 @@
{% extends "base.html" %}
{% block title %}Articles{% endblock %}
{% block content %}
<h1>Articles</h1>
<form method="get" action="/articles" class="grid">
<label>
Week
<input type="week" name="week" value="{{ week_filter }}">
</label>
<label>
Category
<select name="category">
<option value="">All</option>
{% for cat in categories %}
<option value="{{ cat }}" {% if cat == category_filter %}selected{% endif %}>{{ cat }}</option>
{% endfor %}
</select>
</label>
<label>
&nbsp;
<button type="submit">Filter</button>
</label>
</form>
<p>{{ articles|length }} articles found.</p>
<table>
<thead>
<tr>
<th>Date</th>
<th>Title</th>
<th>Author</th>
<th>Categories</th>
</tr>
</thead>
<tbody>
{% for article in articles %}
<tr>
<td>{{ article.pub_date.strftime('%b %d, %Y') }}</td>
<td><a href="{{ article.link }}" target="_blank">{{ article.title }}</a></td>
<td>{{ article.author }}</td>
<td>{{ article.categories | replace('[', '') | replace(']', '') | replace('"', '') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

40
templates/base.html Normal file
View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}PI Weekly{% endblock %} — Plymouth Independent</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<nav class="container">
<ul>
<li><a href="/" class="brand">PI Weekly</a></li>
</ul>
<ul>
<li><a href="/articles">Articles</a></li>
<li><a href="/publish">Publish</a></li>
<li><a href="/issues">Issues</a></li>
<li><a href="/settings">Settings</a></li>
</ul>
</nav>
<main class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div role="alert" {% if category == 'error' %}class="pico-background-red-500"{% endif %}>
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</main>
<footer class="container">
<small>PI Weekly Newspaper Generator</small>
</footer>
{% block scripts %}{% endblock %}
</body>
</html>

46
templates/dashboard.html Normal file
View File

@@ -0,0 +1,46 @@
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<h1>Dashboard</h1>
<div class="stats-grid">
<div class="stat-card">
<span class="number">{{ articles_this_week }}</span>
<span class="label">Articles This Week</span>
</div>
<div class="stat-card">
<span class="number">{{ total_articles }}</span>
<span class="label">Total Cached</span>
</div>
<div class="stat-card">
<span class="number">{{ total_issues }}</span>
<span class="label">Issues Published</span>
</div>
</div>
<hgroup>
<h3>Scheduler</h3>
<p>
Status: <strong>{{ "Running" if scheduler_status.running else "Stopped" }}</strong>
{% if scheduler_status.rss_fetch %}
· Next fetch: {{ scheduler_status.rss_fetch.next_run }}
· Interval: {{ scheduler_status.rss_fetch.interval_hours }}h
{% endif %}
</p>
</hgroup>
<div class="action-buttons">
<form method="post" action="/fetch-now">
<button type="submit">Fetch Now</button>
</form>
<a href="/publish" role="button" class="outline">New Issue</a>
</div>
{% if latest_issue %}
<h3>Latest Issue</h3>
<p>
{{ latest_issue.week_start }} {{ latest_issue.week_end }}
· <a href="/issues/{{ latest_issue.id }}/download">Download ePub</a>
</p>
{% endif %}
{% endblock %}

44
templates/issues.html Normal file
View File

@@ -0,0 +1,44 @@
{% extends "base.html" %}
{% block title %}Issues{% endblock %}
{% block content %}
<h1>Issues Archive</h1>
{% if issues %}
<table>
<thead>
<tr>
<th>Cover</th>
<th>Week</th>
<th>Articles</th>
<th>Cover Method</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for item in issues %}
<tr>
<td>
<img src="/issues/{{ item.issue.id }}/cover" alt="Cover"
style="max-width: 100px; max-height: 60px;">
</td>
<td>{{ item.issue.week_start.strftime('%b %d') }} {{ item.issue.week_end.strftime('%b %d, %Y') }}</td>
<td>{{ item.article_count }}</td>
<td>{{ item.issue.cover_method }}</td>
<td>{{ item.issue.created_at.strftime('%b %d, %Y %H:%M') }}</td>
<td>
<a href="/issues/{{ item.issue.id }}/download" role="button" class="outline secondary">
Download
</a>
<form method="post" action="/issues/{{ item.issue.id }}/regenerate" style="display: inline;">
<button type="submit" class="outline contrast">Regenerate</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No issues published yet. <a href="/publish">Create one?</a></p>
{% endif %}
{% endblock %}

75
templates/publish.html Normal file
View File

@@ -0,0 +1,75 @@
{% extends "base.html" %}
{% block title %}Publish{% endblock %}
{% block content %}
<h1>Publish Issue</h1>
<form method="get" action="/publish" style="margin-bottom: 1rem;">
<label>
Target Week
<input type="week" name="week" value="{{ week_str }}">
</label>
<button type="submit" class="outline">Load Week</button>
</form>
<p>{{ week_start.strftime('%b %d') }} {{ week_end.strftime('%b %d, %Y') }} · {{ articles|length }} articles</p>
{% if articles %}
<form method="post" action="/publish" id="publish-form">
<input type="hidden" name="week_start" value="{{ week_start.isoformat() }}">
<input type="hidden" name="week_end" value="{{ week_end.isoformat() }}">
<table>
<thead>
<tr>
<th><input type="checkbox" id="select-all" checked></th>
<th>Date</th>
<th>Title</th>
<th>Author</th>
</tr>
</thead>
<tbody>
{% for article in articles %}
<tr>
<td>
<input type="checkbox" name="article_ids" value="{{ article.id }}" checked
class="article-checkbox">
</td>
<td>{{ article.pub_date.strftime('%b %d') }}</td>
<td>{{ article.title }}</td>
<td>{{ article.author }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<fieldset>
<legend>Cover</legend>
<label>
<input type="radio" name="cover_method" value="ai" checked>
AI Cover (Pollinations.ai)
</label>
<label>
<input type="radio" name="cover_method" value="text">
Text Cover (fallback)
</label>
</fieldset>
<button type="submit" id="publish-btn">Generate Issue</button>
</form>
{% else %}
<p>No articles found for this week. Try fetching articles first from the <a href="/">Dashboard</a>.</p>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
document.getElementById('select-all')?.addEventListener('change', function() {
document.querySelectorAll('.article-checkbox').forEach(cb => cb.checked = this.checked);
});
document.getElementById('publish-form')?.addEventListener('submit', function() {
document.getElementById('publish-btn').setAttribute('aria-busy', 'true');
document.getElementById('publish-btn').textContent = 'Generating...';
});
</script>
{% endblock %}

79
templates/settings.html Normal file
View File

@@ -0,0 +1,79 @@
{% extends "base.html" %}
{% block title %}Settings{% endblock %}
{% block content %}
<h1>Settings</h1>
<form method="post" action="/settings">
<label>
RSS Feed URL
<input type="url" name="feed_url" value="{{ feed_url }}" required>
</label>
<label>
Fetch Interval (hours)
<input type="number" name="fetch_interval" value="{{ fetch_interval }}" min="1" max="168" required>
</label>
<fieldset>
<legend>Auto-Publish</legend>
<label>
<input type="checkbox" name="auto_publish_enabled" role="switch"
{% if auto_publish %}checked{% endif %}>
Enable auto-publish
</label>
<div class="grid">
<label>
Day
<select name="auto_publish_day">
{% for d in ['mon','tue','wed','thu','fri','sat','sun'] %}
<option value="{{ d }}" {% if auto_publish and auto_publish.day_of_week == d %}selected{% endif %}>
{{ d|capitalize }}
</option>
{% endfor %}
</select>
</label>
<label>
Hour
<input type="number" name="auto_publish_hour"
value="{{ auto_publish.hour if auto_publish else 6 }}" min="0" max="23">
</label>
<label>
Minute
<input type="number" name="auto_publish_minute"
value="{{ auto_publish.minute if auto_publish else 0 }}" min="0" max="59">
</label>
<label>
Cover
<select name="auto_publish_cover">
<option value="ai" {% if auto_publish and auto_publish.cover_method == 'ai' %}selected{% endif %}>AI</option>
<option value="text" {% if not auto_publish or auto_publish.cover_method == 'text' %}selected{% endif %}>Text</option>
</select>
</label>
</div>
</fieldset>
<fieldset>
<legend>Image Constraints</legend>
<div class="grid">
<label>
Landscape Width
<input type="number" name="landscape_w" value="{{ max_landscape[0] }}">
</label>
<label>
Landscape Height
<input type="number" name="landscape_h" value="{{ max_landscape[1] }}">
</label>
<label>
Portrait Width
<input type="number" name="portrait_w" value="{{ max_portrait[0] }}">
</label>
<label>
Portrait Height
<input type="number" name="portrait_h" value="{{ max_portrait[1] }}">
</label>
</div>
</fieldset>
<button type="submit">Save Settings</button>
</form>
{% endblock %}