mirror of
https://github.com/svemagie/indiekit-endpoint-microsub.git
synced 2026-04-02 15:35:00 +02:00
feat: Feed management with status tracking, edit, and rediscover
- Integrate updateFeedStatus into polling processor for health tracking - Add feed management UI showing status (active/error), errors, actions - Add edit feed URL feature to change non-RSS URLs to actual feeds - Add rediscover feature to run feed discovery and update URL - Add refresh button to force immediate poll - Update UI to use Indiekit's badge/button classes (badge--green/red, button--warning) - Add routes: /feeds/:feedId/edit, /feeds/:feedId/rediscover, /feeds/:feedId/refresh Fixes broken feeds by allowing users to: 1. Edit URL directly to the RSS/Atom feed 2. Click "Rediscover" to auto-find the feed from a blog URL 3. View error details and consecutive error counts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -765,36 +765,13 @@
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Badges (for feed types, validation status)
|
||||
Badge extensions for search results
|
||||
========================================================================== */
|
||||
|
||||
.badge {
|
||||
border-radius: var(--border-radius);
|
||||
display: inline-block;
|
||||
/* Extend Indiekit badges with small variant for inline use */
|
||||
.badge--small {
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: 500;
|
||||
padding: 2px var(--space-xs);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.badge--info {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-background);
|
||||
}
|
||||
|
||||
.badge--warning {
|
||||
background: var(--color-warning, #ffcc00);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.badge--error {
|
||||
background: var(--color-error, #ff4444);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge--success {
|
||||
background: var(--color-success, #22c55e);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
@@ -826,14 +803,6 @@
|
||||
border-left: 3px solid var(--color-warning, #ffcc00);
|
||||
}
|
||||
|
||||
.search__invalid-badge {
|
||||
background: var(--color-error, #ff4444);
|
||||
border-radius: var(--border-radius);
|
||||
color: #fff;
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: 500;
|
||||
padding: var(--space-xs) var(--space-s);
|
||||
}
|
||||
|
||||
.search__subscribe {
|
||||
align-items: center;
|
||||
@@ -842,29 +811,128 @@
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Notices (errors, warnings)
|
||||
Notices (inline errors, warnings)
|
||||
========================================================================== */
|
||||
|
||||
.notice {
|
||||
border-radius: var(--border-radius);
|
||||
border-radius: var(--border-radius-small, var(--border-radius));
|
||||
margin-bottom: var(--space-m);
|
||||
padding: var(--space-m);
|
||||
}
|
||||
|
||||
.notice--error {
|
||||
background: rgba(var(--color-error-rgb, 255, 68, 68), 0.1);
|
||||
border: 1px solid var(--color-error, #ff4444);
|
||||
color: var(--color-error, #ff4444);
|
||||
background: var(--color-red90, #fef2f2);
|
||||
border: 1px solid var(--color-error, var(--color-red45));
|
||||
color: var(--color-red10, #7f1d1d);
|
||||
}
|
||||
|
||||
.notice--warning {
|
||||
background: rgba(255, 204, 0, 0.1);
|
||||
border: 1px solid var(--color-warning, #ffcc00);
|
||||
color: #856404;
|
||||
background: var(--color-yellow90, #fefce8);
|
||||
border: 1px solid var(--color-yellow50, #eab308);
|
||||
color: var(--color-yellow10, #713f12);
|
||||
}
|
||||
|
||||
.notice--success {
|
||||
background: rgba(34, 197, 94, 0.1);
|
||||
border: 1px solid var(--color-success, #22c55e);
|
||||
color: var(--color-success, #22c55e);
|
||||
background: var(--color-green90, #f0fdf4);
|
||||
border: 1px solid var(--color-success, var(--color-green50));
|
||||
color: var(--color-green10, #14532d);
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Feed Management Enhancements
|
||||
========================================================================== */
|
||||
|
||||
.feeds__item--error {
|
||||
border-left: 3px solid var(--color-error, #ff4444);
|
||||
}
|
||||
|
||||
.feeds__error {
|
||||
color: var(--color-error, #ff4444);
|
||||
display: block;
|
||||
font-size: var(--font-size-small);
|
||||
margin-top: var(--space-xs);
|
||||
}
|
||||
|
||||
.feeds__error-count {
|
||||
color: var(--color-warning, #ffcc00);
|
||||
display: block;
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.feeds__meta {
|
||||
color: var(--color-text-muted);
|
||||
display: block;
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
.feeds__details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.feeds__actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.feeds__actions form {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
/* ==========================================================================
|
||||
Feed Edit Page
|
||||
========================================================================== */
|
||||
|
||||
.feed-edit {
|
||||
max-width: 40rem;
|
||||
}
|
||||
|
||||
.feed-edit__current {
|
||||
background: var(--color-offset);
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: var(--space-l);
|
||||
padding: var(--space-m);
|
||||
}
|
||||
|
||||
.feed-edit__url {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-small);
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.feed-edit__title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.feed-edit__form {
|
||||
margin-bottom: var(--space-l);
|
||||
}
|
||||
|
||||
.feed-edit__help {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--font-size-small);
|
||||
margin-bottom: var(--space-m);
|
||||
}
|
||||
|
||||
.feed-edit__actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-m);
|
||||
}
|
||||
|
||||
.feed-edit__action {
|
||||
background: var(--color-offset);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--space-m);
|
||||
}
|
||||
|
||||
.feed-edit__action p {
|
||||
margin-bottom: var(--space-s);
|
||||
}
|
||||
|
||||
20
index.js
20
index.js
@@ -91,6 +91,26 @@ export default class MicrosubEndpoint {
|
||||
"/channels/:uid/feeds/remove",
|
||||
readerController.removeFeed,
|
||||
);
|
||||
readerRouter.get(
|
||||
"/channels/:uid/feeds/:feedId",
|
||||
readerController.feedDetails,
|
||||
);
|
||||
readerRouter.get(
|
||||
"/channels/:uid/feeds/:feedId/edit",
|
||||
readerController.editFeedForm,
|
||||
);
|
||||
readerRouter.post(
|
||||
"/channels/:uid/feeds/:feedId/edit",
|
||||
readerController.updateFeedUrl,
|
||||
);
|
||||
readerRouter.post(
|
||||
"/channels/:uid/feeds/:feedId/rediscover",
|
||||
readerController.rediscoverFeed,
|
||||
);
|
||||
readerRouter.post(
|
||||
"/channels/:uid/feeds/:feedId/refresh",
|
||||
readerController.refreshFeed,
|
||||
);
|
||||
readerRouter.get("/item/:id", readerController.item);
|
||||
readerRouter.get("/compose", readerController.compose);
|
||||
readerRouter.post("/compose", readerController.submitCompose);
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
* @module controllers/reader
|
||||
*/
|
||||
|
||||
import { discoverAndValidateFeeds } from "../feeds/discovery.js";
|
||||
import { discoverAndValidateFeeds, getBestFeed } from "../feeds/discovery.js";
|
||||
import { validateFeedUrl } from "../feeds/validator.js";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { refreshFeedNow } from "../polling/scheduler.js";
|
||||
import {
|
||||
getChannels,
|
||||
@@ -15,8 +16,11 @@ import {
|
||||
} from "../storage/channels.js";
|
||||
import {
|
||||
getFeedsForChannel,
|
||||
getFeedById,
|
||||
createFeed,
|
||||
deleteFeed,
|
||||
updateFeed,
|
||||
updateFeedStatus,
|
||||
} from "../storage/feeds.js";
|
||||
import {
|
||||
getTimelineItems,
|
||||
@@ -701,6 +705,210 @@ export async function markAllRead(request, response) {
|
||||
response.redirect(`${request.baseUrl}/channels/${channelUid}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* View single feed details with status - redirects to edit form
|
||||
* @param {object} request - Express request
|
||||
* @param {object} response - Express response
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function feedDetails(request, response) {
|
||||
const { uid, feedId } = request.params;
|
||||
// Redirect to edit form which shows all details
|
||||
response.redirect(`${request.baseUrl}/channels/${uid}/feeds/${feedId}/edit`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit feed URL form
|
||||
* @param {object} request - Express request
|
||||
* @param {object} response - Express response
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function editFeedForm(request, response) {
|
||||
const { application } = request.app.locals;
|
||||
const userId = getUserId(request);
|
||||
const { uid, feedId } = request.params;
|
||||
|
||||
const channelDocument = await getChannel(application, uid, userId);
|
||||
if (!channelDocument) {
|
||||
return response.status(404).render("404");
|
||||
}
|
||||
|
||||
const feed = await getFeedById(application, feedId);
|
||||
if (!feed || feed.channelId.toString() !== channelDocument._id.toString()) {
|
||||
return response.status(404).render("404");
|
||||
}
|
||||
|
||||
response.render("feed-edit", {
|
||||
title: request.__("microsub.feeds.edit"),
|
||||
channel: channelDocument,
|
||||
feed,
|
||||
baseUrl: request.baseUrl,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update feed URL
|
||||
* @param {object} request - Express request
|
||||
* @param {object} response - Express response
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function updateFeedUrl(request, response) {
|
||||
const { application } = request.app.locals;
|
||||
const userId = getUserId(request);
|
||||
const { uid, feedId } = request.params;
|
||||
const { url: newUrl } = request.body;
|
||||
|
||||
const channelDocument = await getChannel(application, uid, userId);
|
||||
if (!channelDocument) {
|
||||
return response.status(404).render("404");
|
||||
}
|
||||
|
||||
const feed = await getFeedById(application, feedId);
|
||||
if (!feed || feed.channelId.toString() !== channelDocument._id.toString()) {
|
||||
return response.status(404).render("404");
|
||||
}
|
||||
|
||||
// Validate the new URL is a valid feed
|
||||
const validation = await validateFeedUrl(newUrl);
|
||||
|
||||
if (!validation.valid) {
|
||||
return response.render("feed-edit", {
|
||||
title: request.__("microsub.feeds.edit"),
|
||||
channel: channelDocument,
|
||||
feed,
|
||||
error: validation.error,
|
||||
baseUrl: request.baseUrl,
|
||||
});
|
||||
}
|
||||
|
||||
// Update the feed URL and reset error state
|
||||
await updateFeed(application, feedId, {
|
||||
url: newUrl,
|
||||
title: validation.title || feed.title,
|
||||
status: "active",
|
||||
lastError: undefined,
|
||||
lastErrorAt: undefined,
|
||||
consecutiveErrors: 0,
|
||||
});
|
||||
|
||||
// Trigger immediate fetch
|
||||
refreshFeedNow(application, feedId).catch((error) => {
|
||||
console.error(
|
||||
`[Microsub] Error refreshing updated feed ${newUrl}:`,
|
||||
error.message,
|
||||
);
|
||||
});
|
||||
|
||||
response.redirect(`${request.baseUrl}/channels/${uid}/feeds`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rediscover feed - run discovery on URL to find actual RSS feed
|
||||
* @param {object} request - Express request
|
||||
* @param {object} response - Express response
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function rediscoverFeed(request, response) {
|
||||
const { application } = request.app.locals;
|
||||
const userId = getUserId(request);
|
||||
const { uid, feedId } = request.params;
|
||||
|
||||
const channelDocument = await getChannel(application, uid, userId);
|
||||
if (!channelDocument) {
|
||||
return response.status(404).render("404");
|
||||
}
|
||||
|
||||
const feed = await getFeedById(application, feedId);
|
||||
if (!feed || feed.channelId.toString() !== channelDocument._id.toString()) {
|
||||
return response.status(404).render("404");
|
||||
}
|
||||
|
||||
// Run feed discovery on the current URL
|
||||
try {
|
||||
const discoveredFeeds = await discoverAndValidateFeeds(feed.url);
|
||||
const bestFeed = getBestFeed(discoveredFeeds);
|
||||
|
||||
if (bestFeed && bestFeed.url !== feed.url) {
|
||||
// Found a different (better) feed URL - update the record
|
||||
await updateFeed(application, feedId, {
|
||||
url: bestFeed.url,
|
||||
title: bestFeed.title || feed.title,
|
||||
status: "active",
|
||||
lastError: undefined,
|
||||
lastErrorAt: undefined,
|
||||
consecutiveErrors: 0,
|
||||
});
|
||||
|
||||
console.info(
|
||||
`[Microsub] Rediscovered feed: ${feed.url} -> ${bestFeed.url}`,
|
||||
);
|
||||
|
||||
// Trigger immediate fetch
|
||||
refreshFeedNow(application, feedId).catch((error) => {
|
||||
console.error(
|
||||
`[Microsub] Error refreshing rediscovered feed:`,
|
||||
error.message,
|
||||
);
|
||||
});
|
||||
} else if (bestFeed) {
|
||||
// Same URL but valid - just reset error state and refresh
|
||||
await updateFeedStatus(application, feedId, { success: true });
|
||||
await updateFeed(application, feedId, {
|
||||
status: "active",
|
||||
lastError: undefined,
|
||||
lastErrorAt: undefined,
|
||||
consecutiveErrors: 0,
|
||||
});
|
||||
|
||||
refreshFeedNow(application, feedId).catch((error) => {
|
||||
console.error(`[Microsub] Error refreshing feed:`, error.message);
|
||||
});
|
||||
} else {
|
||||
// No valid feed found
|
||||
await updateFeedStatus(application, feedId, {
|
||||
success: false,
|
||||
error: "No valid feed found at this URL",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
await updateFeedStatus(application, feedId, {
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
response.redirect(`${request.baseUrl}/channels/${uid}/feeds`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force refresh a feed
|
||||
* @param {object} request - Express request
|
||||
* @param {object} response - Express response
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function refreshFeed(request, response) {
|
||||
const { application } = request.app.locals;
|
||||
const userId = getUserId(request);
|
||||
const { uid, feedId } = request.params;
|
||||
|
||||
const channelDocument = await getChannel(application, uid, userId);
|
||||
if (!channelDocument) {
|
||||
return response.status(404).render("404");
|
||||
}
|
||||
|
||||
const feed = await getFeedById(application, feedId);
|
||||
if (!feed || feed.channelId.toString() !== channelDocument._id.toString()) {
|
||||
return response.status(404).render("404");
|
||||
}
|
||||
|
||||
// Trigger immediate fetch
|
||||
refreshFeedNow(application, feedId).catch((error) => {
|
||||
console.error(`[Microsub] Error refreshing feed ${feed.url}:`, error.message);
|
||||
});
|
||||
|
||||
response.redirect(`${request.baseUrl}/channels/${uid}/feeds`);
|
||||
}
|
||||
|
||||
export const readerController = {
|
||||
index,
|
||||
channels,
|
||||
@@ -714,6 +922,11 @@ export const readerController = {
|
||||
feeds,
|
||||
addFeed,
|
||||
removeFeed,
|
||||
feedDetails,
|
||||
editFeedForm,
|
||||
updateFeedUrl,
|
||||
rediscoverFeed,
|
||||
refreshFeed,
|
||||
item,
|
||||
compose,
|
||||
submitCompose,
|
||||
|
||||
@@ -6,7 +6,11 @@
|
||||
import { getRedisClient, publishEvent } from "../cache/redis.js";
|
||||
import { fetchAndParseFeed } from "../feeds/fetcher.js";
|
||||
import { getChannel } from "../storage/channels.js";
|
||||
import { updateFeedAfterFetch, updateFeedWebsub } from "../storage/feeds.js";
|
||||
import {
|
||||
updateFeedAfterFetch,
|
||||
updateFeedStatus,
|
||||
updateFeedWebsub,
|
||||
} from "../storage/feeds.js";
|
||||
import { passesRegexFilter, passesTypeFilter } from "../storage/filters.js";
|
||||
import { addItem } from "../storage/items.js";
|
||||
import {
|
||||
@@ -167,9 +171,21 @@ export async function processFeed(application, feed) {
|
||||
|
||||
result.success = true;
|
||||
result.tier = tierResult.tier;
|
||||
|
||||
// Update feed status to active on success
|
||||
await updateFeedStatus(application, feed._id, {
|
||||
success: true,
|
||||
itemCount: parsed.items?.length || 0,
|
||||
});
|
||||
} catch (error) {
|
||||
result.error = error.message;
|
||||
|
||||
// Update feed status to error
|
||||
await updateFeedStatus(application, feed._id, {
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
|
||||
// Still update the feed to prevent retry storms
|
||||
try {
|
||||
const tierResult = calculateNewTier({
|
||||
@@ -182,8 +198,6 @@ export async function processFeed(application, feed) {
|
||||
tier: Math.min(tierResult.tier + 1, 10), // Increase tier on error
|
||||
unmodified: tierResult.consecutiveUnchanged,
|
||||
nextFetchAt: tierResult.nextFetchAt,
|
||||
lastError: error.message,
|
||||
lastErrorAt: new Date(),
|
||||
});
|
||||
} catch {
|
||||
// Ignore update errors
|
||||
|
||||
@@ -34,7 +34,15 @@
|
||||
"unfollow": "Unfollow",
|
||||
"empty": "No feeds followed in this channel",
|
||||
"url": "Feed URL",
|
||||
"urlPlaceholder": "https://example.com/feed.xml"
|
||||
"urlPlaceholder": "https://example.com/feed.xml",
|
||||
"edit": "Edit feed",
|
||||
"rediscover": "Rediscover feed",
|
||||
"refresh": "Refresh now",
|
||||
"status": {
|
||||
"active": "Active",
|
||||
"error": "Error",
|
||||
"stale": "Stale"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"reply": "Reply",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@rmdes/indiekit-endpoint-microsub",
|
||||
"version": "1.0.23",
|
||||
"version": "1.0.24",
|
||||
"description": "Microsub endpoint for Indiekit. Enables subscribing to feeds and reading content using the Microsub protocol.",
|
||||
"keywords": [
|
||||
"indiekit",
|
||||
|
||||
84
views/feed-edit.njk
Normal file
84
views/feed-edit.njk
Normal file
@@ -0,0 +1,84 @@
|
||||
{% extends "layouts/reader.njk" %}
|
||||
|
||||
{% block reader %}
|
||||
<div class="settings">
|
||||
<a href="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="back-link">
|
||||
{{ icon("previous") }} {{ __("microsub.feeds.title") }}
|
||||
</a>
|
||||
|
||||
<h2>{{ __("microsub.feeds.edit") }}</h2>
|
||||
|
||||
{% if error %}
|
||||
<div class="notice notice--error">
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="feed-edit">
|
||||
<div class="feed-edit__current">
|
||||
<h3>Current Feed</h3>
|
||||
<p class="feed-edit__url">{{ feed.url }}</p>
|
||||
{% if feed.title %}
|
||||
<p class="feed-edit__title">{{ feed.title }}</p>
|
||||
{% endif %}
|
||||
{% if feed.status == 'error' %}
|
||||
<div class="notice notice--error">
|
||||
<p><strong>Status:</strong> Error</p>
|
||||
{% if feed.lastError %}
|
||||
<p><strong>Last error:</strong> {{ feed.lastError }}</p>
|
||||
{% endif %}
|
||||
{% if feed.consecutiveErrors %}
|
||||
<p><strong>Consecutive errors:</strong> {{ feed.consecutiveErrors }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/{{ feed._id }}/edit" class="feed-edit__form">
|
||||
{{ input({
|
||||
id: "url",
|
||||
name: "url",
|
||||
label: "New Feed URL",
|
||||
type: "url",
|
||||
required: true,
|
||||
value: feed.url,
|
||||
placeholder: "https://example.com/feed.xml",
|
||||
autocomplete: "off"
|
||||
}) }}
|
||||
|
||||
<p class="feed-edit__help">
|
||||
Enter the direct URL to the RSS, Atom, or JSON Feed. The URL will be validated before updating.
|
||||
</p>
|
||||
|
||||
<div class="button-group">
|
||||
{{ button({ text: "Update Feed URL" }) }}
|
||||
<a href="{{ baseUrl }}/channels/{{ channel.uid }}/feeds" class="button button--secondary">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="feed-edit__actions">
|
||||
<h3>Other Actions</h3>
|
||||
|
||||
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/{{ feed._id }}/rediscover" class="feed-edit__action">
|
||||
<p>Run feed discovery on the current URL to find the actual RSS/Atom feed.</p>
|
||||
{{ button({
|
||||
text: "Rediscover Feed",
|
||||
classes: "button--secondary"
|
||||
}) }}
|
||||
</form>
|
||||
|
||||
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/{{ feed._id }}/refresh" class="feed-edit__action">
|
||||
<p>Force refresh this feed now.</p>
|
||||
{{ button({
|
||||
text: "Refresh Now",
|
||||
classes: "button--secondary"
|
||||
}) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -13,7 +13,7 @@
|
||||
{% if feeds.length > 0 %}
|
||||
<div class="feeds__list">
|
||||
{% for feed in feeds %}
|
||||
<div class="feeds__item">
|
||||
<div class="feeds__item{% if feed.status == 'error' %} feeds__item--error{% endif %}">
|
||||
<div class="feeds__info">
|
||||
{% if feed.photo %}
|
||||
<img src="{{ feed.photo }}"
|
||||
@@ -25,19 +25,51 @@
|
||||
onerror="this.style.display='none'">
|
||||
{% endif %}
|
||||
<div class="feeds__details">
|
||||
<span class="feeds__name">{{ feed.title or feed.url }}</span>
|
||||
<span class="feeds__name">
|
||||
{{ feed.title or feed.url }}
|
||||
{% if feed.status == 'error' %}
|
||||
<span class="badge badge--red">Error</span>
|
||||
{% elif feed.status == 'active' %}
|
||||
<span class="badge badge--green">Active</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<a href="{{ feed.url }}" class="feeds__url" target="_blank" rel="noopener">
|
||||
{{ feed.url | replace("https://", "") | replace("http://", "") }}
|
||||
</a>
|
||||
{% if feed.lastError %}
|
||||
<span class="feeds__error">{{ feed.lastError }}</span>
|
||||
{% endif %}
|
||||
{% if feed.consecutiveErrors > 0 %}
|
||||
<span class="feeds__error-count">{{ feed.consecutiveErrors }} consecutive errors</span>
|
||||
{% endif %}
|
||||
{% if feed.lastSuccessAt %}
|
||||
<span class="feeds__meta">Last success: {{ feed.lastSuccessAt | date("relative") }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/remove" class="feeds__actions">
|
||||
<input type="hidden" name="url" value="{{ feed.url }}">
|
||||
{{ button({
|
||||
text: __("microsub.feeds.unfollow"),
|
||||
classes: "button--secondary button--small"
|
||||
}) }}
|
||||
</form>
|
||||
<div class="feeds__actions">
|
||||
<a href="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/{{ feed._id }}/edit"
|
||||
class="button button--secondary button--small"
|
||||
title="Edit feed URL">
|
||||
{{ icon("edit") }}
|
||||
</a>
|
||||
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/{{ feed._id }}/rediscover" style="display:inline;">
|
||||
<button type="submit" class="button button--secondary button--small" title="Rediscover feed">
|
||||
{{ icon("discover") }}
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/{{ feed._id }}/refresh" style="display:inline;">
|
||||
<button type="submit" class="button button--secondary button--small" title="Refresh now">
|
||||
{{ icon("refresh") }}
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" action="{{ baseUrl }}/channels/{{ channel.uid }}/feeds/remove" style="display:inline;">
|
||||
<input type="hidden" name="url" value="{{ feed.url }}">
|
||||
<button type="submit" class="button button--warning button--small" title="Unfollow">
|
||||
{{ icon("delete") }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -46,11 +46,11 @@
|
||||
<div class="search__feed">
|
||||
<span class="search__name">
|
||||
{{ result.title or "Feed" }}
|
||||
<span class="search__type badge badge--{% if result.valid %}info{% else %}warning{% endif %}">
|
||||
<span class="search__type badge badge--small{% if result.valid %} badge--green{% else %} badge--yellow{% endif %}">
|
||||
{{ result.typeLabel }}
|
||||
</span>
|
||||
{% if result.isCommentsFeed %}
|
||||
<span class="search__type badge badge--warning">Comments</span>
|
||||
<span class="search__type badge badge--small badge--yellow">Comments</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="search__url">{{ result.url | replace("https://", "") | replace("http://", "") }}</span>
|
||||
@@ -73,7 +73,7 @@
|
||||
}) }}
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="search__invalid-badge">Invalid</span>
|
||||
<span class="badge badge--small badge--red">Invalid</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
Reference in New Issue
Block a user