@rmdes/indiekit-endpoint-rss 1.0.4 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -0
- package/lib/controllers/dashboard.js +33 -26
- package/lib/rss-client.js +13 -4
- package/lib/sync.js +51 -1
- package/package.json +1 -1
- package/views/rss.njk +470 -400
package/index.js
CHANGED
|
@@ -12,16 +12,16 @@ export const dashboardController = {
|
|
|
12
12
|
|
|
13
13
|
if (!rssConfig) {
|
|
14
14
|
return response.status(500).render("rss", {
|
|
15
|
-
title: response.
|
|
16
|
-
|
|
15
|
+
title: response.__("rss.title"),
|
|
16
|
+
configError: response.__("rss.error.noConfig"),
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const db = request.app.locals.application.getRssDb?.();
|
|
21
21
|
if (!db) {
|
|
22
22
|
return response.render("rss", {
|
|
23
|
-
title: response.
|
|
24
|
-
|
|
23
|
+
title: response.__("rss.title"),
|
|
24
|
+
configError: response.__("rss.error.noDatabase"),
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -42,7 +42,7 @@ export const dashboardController = {
|
|
|
42
42
|
const syncState = getSyncState();
|
|
43
43
|
|
|
44
44
|
response.render("rss", {
|
|
45
|
-
title: response.
|
|
45
|
+
title: response.__("rss.title"),
|
|
46
46
|
feeds: feeds.map(formatFeed),
|
|
47
47
|
recentItems: recentItems.map((item) => formatItem(item)),
|
|
48
48
|
totalFeeds: feeds.length,
|
|
@@ -52,7 +52,14 @@ export const dashboardController = {
|
|
|
52
52
|
lastSync: syncState.lastSync?.toISOString(),
|
|
53
53
|
lastError: syncState.lastError,
|
|
54
54
|
},
|
|
55
|
+
mountPath: request.baseUrl,
|
|
55
56
|
publicUrl: rssEndpoint,
|
|
57
|
+
synced: request.query.synced,
|
|
58
|
+
syncedFeeds: request.query.feeds,
|
|
59
|
+
syncedItems: request.query.items,
|
|
60
|
+
syncedPruned: request.query.pruned,
|
|
61
|
+
syncInProgress: request.query.syncing,
|
|
62
|
+
queryError: request.query.error,
|
|
56
63
|
});
|
|
57
64
|
} catch (error) {
|
|
58
65
|
next(error);
|
|
@@ -121,45 +128,45 @@ export const dashboardController = {
|
|
|
121
128
|
const { rssConfig, getRssDb } = request.app.locals.application;
|
|
122
129
|
|
|
123
130
|
if (!rssConfig) {
|
|
124
|
-
return response.
|
|
125
|
-
error
|
|
126
|
-
|
|
131
|
+
return response.redirect(
|
|
132
|
+
request.baseUrl + "?error=" + encodeURIComponent("Not configured"),
|
|
133
|
+
);
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
const db = getRssDb?.();
|
|
130
137
|
if (!db) {
|
|
131
|
-
return response.
|
|
132
|
-
|
|
133
|
-
|
|
138
|
+
return response.redirect(
|
|
139
|
+
request.baseUrl +
|
|
140
|
+
"?error=" +
|
|
141
|
+
encodeURIComponent("Database not available"),
|
|
142
|
+
);
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
const syncState = getSyncState();
|
|
137
146
|
if (syncState.syncing) {
|
|
138
|
-
return response.
|
|
139
|
-
error: "Sync already in progress",
|
|
140
|
-
syncing: true,
|
|
141
|
-
});
|
|
147
|
+
return response.redirect(request.baseUrl + "?syncing=true");
|
|
142
148
|
}
|
|
143
149
|
|
|
144
|
-
// Start sync and wait for result - pass db directly
|
|
145
150
|
const result = await runSync(db, rssConfig);
|
|
146
151
|
|
|
147
152
|
if (result.error) {
|
|
148
|
-
return response.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
});
|
|
153
|
+
return response.redirect(
|
|
154
|
+
request.baseUrl + "?error=" + encodeURIComponent(result.error),
|
|
155
|
+
);
|
|
152
156
|
}
|
|
153
157
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
const params = new URLSearchParams({
|
|
159
|
+
synced: "true",
|
|
160
|
+
feeds: result.feedsProcessed || 0,
|
|
161
|
+
items: result.itemsAdded || 0,
|
|
162
|
+
pruned: result.itemsPruned || 0,
|
|
159
163
|
});
|
|
164
|
+
response.redirect(request.baseUrl + "?" + params.toString());
|
|
160
165
|
} catch (error) {
|
|
161
166
|
console.error("[RSS] Sync error:", error.message);
|
|
162
|
-
response.
|
|
167
|
+
response.redirect(
|
|
168
|
+
request.baseUrl + "?error=" + encodeURIComponent(error.message),
|
|
169
|
+
);
|
|
163
170
|
}
|
|
164
171
|
},
|
|
165
172
|
};
|
package/lib/rss-client.js
CHANGED
|
@@ -326,14 +326,23 @@ export class RssClient {
|
|
|
326
326
|
* @returns {string|null}
|
|
327
327
|
*/
|
|
328
328
|
extractFeedImage(parsed) {
|
|
329
|
+
// Helper to get first string from value (handles arrays)
|
|
330
|
+
const getString = (val) => {
|
|
331
|
+
if (!val) return null;
|
|
332
|
+
if (Array.isArray(val)) return val[0] || null;
|
|
333
|
+
if (typeof val === "object" && val.url) return val.url;
|
|
334
|
+
return typeof val === "string" ? val : null;
|
|
335
|
+
};
|
|
336
|
+
|
|
329
337
|
// RSS 2.0 image
|
|
330
|
-
if (parsed.image?.url) return parsed.image.url;
|
|
338
|
+
if (parsed.image?.url) return getString(parsed.image.url);
|
|
339
|
+
if (parsed.image) return getString(parsed.image);
|
|
331
340
|
// Atom icon
|
|
332
|
-
if (parsed.icon) return parsed.icon;
|
|
341
|
+
if (parsed.icon) return getString(parsed.icon);
|
|
333
342
|
// Atom logo
|
|
334
|
-
if (parsed.logo) return parsed.logo;
|
|
343
|
+
if (parsed.logo) return getString(parsed.logo);
|
|
335
344
|
// itunes:image
|
|
336
|
-
if (parsed.itunes?.image) return parsed.itunes.image;
|
|
345
|
+
if (parsed.itunes?.image) return getString(parsed.itunes.image);
|
|
337
346
|
return null;
|
|
338
347
|
}
|
|
339
348
|
|
package/lib/sync.js
CHANGED
|
@@ -116,16 +116,25 @@ export async function runSync(dbOrIndiekit, options) {
|
|
|
116
116
|
syncState.feedsProcessed++;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// Prune old items
|
|
120
|
+
const retentionDays = options.retentionDays || 30;
|
|
121
|
+
const itemsPruned = await pruneOldItems(
|
|
122
|
+
itemsCollection,
|
|
123
|
+
feedsCollection,
|
|
124
|
+
retentionDays,
|
|
125
|
+
);
|
|
126
|
+
|
|
119
127
|
syncState.lastSync = new Date();
|
|
120
128
|
syncState.syncing = false;
|
|
121
129
|
|
|
122
130
|
console.log(
|
|
123
|
-
`[RSS] Sync complete: ${syncState.feedsProcessed} feeds, ${syncState.itemsAdded} new items`
|
|
131
|
+
`[RSS] Sync complete: ${syncState.feedsProcessed} feeds, ${syncState.itemsAdded} new items, ${itemsPruned} pruned`
|
|
124
132
|
);
|
|
125
133
|
|
|
126
134
|
return {
|
|
127
135
|
feedsProcessed: syncState.feedsProcessed,
|
|
128
136
|
itemsAdded: syncState.itemsAdded,
|
|
137
|
+
itemsPruned,
|
|
129
138
|
};
|
|
130
139
|
} catch (error) {
|
|
131
140
|
syncState.lastError = error.message;
|
|
@@ -248,6 +257,47 @@ async function createIndexes(feedsCollection, itemsCollection) {
|
|
|
248
257
|
await itemsCollection.createIndex({ fetchedAt: -1 });
|
|
249
258
|
}
|
|
250
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Prune items older than retention period and update feed item counts
|
|
262
|
+
* @param {Collection} itemsCollection - Items collection
|
|
263
|
+
* @param {Collection} feedsCollection - Feeds collection
|
|
264
|
+
* @param {number} retentionDays - Days to keep items
|
|
265
|
+
* @returns {Promise<number>} Number of items pruned
|
|
266
|
+
*/
|
|
267
|
+
async function pruneOldItems(itemsCollection, feedsCollection, retentionDays) {
|
|
268
|
+
const cutoff = new Date();
|
|
269
|
+
cutoff.setDate(cutoff.getDate() - retentionDays);
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const result = await itemsCollection.deleteMany({
|
|
273
|
+
pubDate: { $lt: cutoff },
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (result.deletedCount > 0) {
|
|
277
|
+
console.log(
|
|
278
|
+
`[RSS] Pruned ${result.deletedCount} items older than ${retentionDays} days`,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// Update item counts for all feeds
|
|
282
|
+
const feeds = await feedsCollection.find({}).toArray();
|
|
283
|
+
for (const feed of feeds) {
|
|
284
|
+
const count = await itemsCollection.countDocuments({
|
|
285
|
+
feedId: feed._id,
|
|
286
|
+
});
|
|
287
|
+
await feedsCollection.updateOne(
|
|
288
|
+
{ _id: feed._id },
|
|
289
|
+
{ $set: { itemCount: count } },
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return result.deletedCount;
|
|
295
|
+
} catch (err) {
|
|
296
|
+
console.error("[RSS] Prune error:", err.message);
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
251
301
|
/**
|
|
252
302
|
* Process feeds with concurrency limit
|
|
253
303
|
* @param {Array} feeds - Array of feeds
|
package/package.json
CHANGED
package/views/rss.njk
CHANGED
|
@@ -1,309 +1,383 @@
|
|
|
1
1
|
{% extends "document.njk" %}
|
|
2
2
|
|
|
3
3
|
{% block content %}
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
display: grid;
|
|
11
|
-
grid-template-columns: repeat(3, 1fr);
|
|
12
|
-
gap: 1rem;
|
|
13
|
-
margin-bottom: 1.5rem;
|
|
14
|
-
}
|
|
15
|
-
.rss-stat {
|
|
16
|
-
display: flex;
|
|
17
|
-
flex-direction: column;
|
|
18
|
-
align-items: center;
|
|
19
|
-
padding: 1rem;
|
|
20
|
-
background: var(--color-offset, #f5f5f5);
|
|
21
|
-
border-radius: 0.5rem;
|
|
22
|
-
text-align: center;
|
|
23
|
-
}
|
|
24
|
-
.rss-stat-value {
|
|
25
|
-
font-size: 1.5rem;
|
|
26
|
-
font-weight: 700;
|
|
27
|
-
color: var(--color-accent, #ff6600);
|
|
28
|
-
}
|
|
29
|
-
.rss-stat-label {
|
|
30
|
-
font-size: 0.75rem;
|
|
31
|
-
color: var(--color-text-secondary, #666);
|
|
32
|
-
text-transform: uppercase;
|
|
33
|
-
letter-spacing: 0.05em;
|
|
34
|
-
}
|
|
4
|
+
<style>
|
|
5
|
+
.rss-dashboard {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: var(--space-xl, 2rem);
|
|
9
|
+
}
|
|
35
10
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
padding: 1rem;
|
|
42
|
-
background: var(--color-offset, #f5f5f5);
|
|
43
|
-
border-radius: 0.5rem;
|
|
44
|
-
margin-bottom: 1.5rem;
|
|
45
|
-
}
|
|
46
|
-
.rss-sync-info {
|
|
47
|
-
display: flex;
|
|
48
|
-
flex-direction: column;
|
|
49
|
-
gap: 0.25rem;
|
|
50
|
-
}
|
|
51
|
-
.rss-sync-label {
|
|
52
|
-
font-size: 0.75rem;
|
|
53
|
-
color: var(--color-text-secondary, #666);
|
|
54
|
-
text-transform: uppercase;
|
|
55
|
-
}
|
|
56
|
-
.rss-sync-time {
|
|
57
|
-
font-weight: 500;
|
|
58
|
-
}
|
|
59
|
-
.rss-syncing {
|
|
60
|
-
display: inline-flex;
|
|
61
|
-
align-items: center;
|
|
62
|
-
gap: 0.5rem;
|
|
63
|
-
color: var(--color-accent, #ff6600);
|
|
64
|
-
}
|
|
65
|
-
.rss-spinner {
|
|
66
|
-
width: 16px;
|
|
67
|
-
height: 16px;
|
|
68
|
-
border: 2px solid currentColor;
|
|
69
|
-
border-top-color: transparent;
|
|
70
|
-
border-radius: 50%;
|
|
71
|
-
animation: rss-spin 0.8s linear infinite;
|
|
72
|
-
}
|
|
73
|
-
@keyframes rss-spin {
|
|
74
|
-
to { transform: rotate(360deg); }
|
|
75
|
-
}
|
|
11
|
+
.rss-section {
|
|
12
|
+
background: var(--color-offset, #f5f5f5);
|
|
13
|
+
border-radius: var(--border-radius-small, 0.5rem);
|
|
14
|
+
padding: var(--space-m, 1.5rem);
|
|
15
|
+
}
|
|
76
16
|
|
|
77
|
-
|
|
78
|
-
.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
.rss-add-form input {
|
|
84
|
-
flex: 1;
|
|
85
|
-
padding: 0.5rem 0.75rem;
|
|
86
|
-
border: 1px solid var(--color-border, #ddd);
|
|
87
|
-
border-radius: 0.25rem;
|
|
88
|
-
font-size: 0.875rem;
|
|
89
|
-
}
|
|
90
|
-
.rss-add-form input:focus {
|
|
91
|
-
outline: none;
|
|
92
|
-
border-color: var(--color-accent, #ff6600);
|
|
93
|
-
}
|
|
17
|
+
.rss-section h2 {
|
|
18
|
+
font: var(--font-heading, bold 1.25rem/1.4 sans-serif);
|
|
19
|
+
margin-block-end: var(--space-s, 0.75rem);
|
|
20
|
+
padding-block-end: var(--space-xs, 0.5rem);
|
|
21
|
+
border-block-end: 1px solid var(--color-outline-variant, #ddd);
|
|
22
|
+
}
|
|
94
23
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
border-radius: 0.25rem;
|
|
114
|
-
flex-shrink: 0;
|
|
115
|
-
}
|
|
116
|
-
.rss-feed-icon-placeholder {
|
|
117
|
-
width: 32px;
|
|
118
|
-
height: 32px;
|
|
119
|
-
background: var(--color-border, #ddd);
|
|
120
|
-
border-radius: 0.25rem;
|
|
121
|
-
flex-shrink: 0;
|
|
122
|
-
display: flex;
|
|
123
|
-
align-items: center;
|
|
124
|
-
justify-content: center;
|
|
125
|
-
color: var(--color-text-secondary, #666);
|
|
126
|
-
font-size: 1rem;
|
|
127
|
-
}
|
|
128
|
-
.rss-feed-info {
|
|
129
|
-
flex: 1;
|
|
130
|
-
min-width: 0;
|
|
131
|
-
}
|
|
132
|
-
.rss-feed-title {
|
|
133
|
-
font-weight: 500;
|
|
134
|
-
white-space: nowrap;
|
|
135
|
-
overflow: hidden;
|
|
136
|
-
text-overflow: ellipsis;
|
|
137
|
-
}
|
|
138
|
-
.rss-feed-meta {
|
|
139
|
-
display: flex;
|
|
140
|
-
gap: 0.5rem;
|
|
141
|
-
font-size: 0.75rem;
|
|
142
|
-
color: var(--color-text-secondary, #666);
|
|
143
|
-
}
|
|
144
|
-
.rss-feed-actions {
|
|
145
|
-
display: flex;
|
|
146
|
-
gap: 0.5rem;
|
|
147
|
-
align-items: center;
|
|
148
|
-
}
|
|
149
|
-
.rss-feed-error {
|
|
150
|
-
color: #d32f2f;
|
|
151
|
-
font-size: 0.75rem;
|
|
152
|
-
}
|
|
24
|
+
/* Notifications */
|
|
25
|
+
.rss-notification {
|
|
26
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
27
|
+
padding: var(--space-s, 0.75rem) var(--space-m, 1rem);
|
|
28
|
+
margin-block-end: var(--space-s, 0.75rem);
|
|
29
|
+
}
|
|
30
|
+
.rss-notification--success {
|
|
31
|
+
background: var(--color-success, #d4edda);
|
|
32
|
+
color: var(--color-on-success, #155724);
|
|
33
|
+
}
|
|
34
|
+
.rss-notification--warning {
|
|
35
|
+
background: #fff3cd;
|
|
36
|
+
color: #856404;
|
|
37
|
+
}
|
|
38
|
+
.rss-notification--error {
|
|
39
|
+
background: var(--color-error, #f8d7da);
|
|
40
|
+
color: var(--color-on-error, #721c24);
|
|
41
|
+
}
|
|
153
42
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
181
|
-
.rss-item-info {
|
|
182
|
-
flex: 1;
|
|
183
|
-
min-width: 0;
|
|
184
|
-
}
|
|
185
|
-
.rss-item-title {
|
|
186
|
-
font-weight: 500;
|
|
187
|
-
text-decoration: none;
|
|
188
|
-
color: inherit;
|
|
189
|
-
display: block;
|
|
190
|
-
white-space: nowrap;
|
|
191
|
-
overflow: hidden;
|
|
192
|
-
text-overflow: ellipsis;
|
|
193
|
-
}
|
|
194
|
-
.rss-item-title:hover { text-decoration: underline; }
|
|
195
|
-
.rss-item-description {
|
|
196
|
-
font-size: 0.875rem;
|
|
197
|
-
color: var(--color-text-secondary, #666);
|
|
198
|
-
margin: 0.25rem 0;
|
|
199
|
-
display: -webkit-box;
|
|
200
|
-
-webkit-line-clamp: 2;
|
|
201
|
-
-webkit-box-orient: vertical;
|
|
202
|
-
overflow: hidden;
|
|
203
|
-
}
|
|
204
|
-
.rss-item-meta {
|
|
205
|
-
font-size: 0.75rem;
|
|
206
|
-
color: var(--color-text-secondary, #666);
|
|
207
|
-
}
|
|
43
|
+
/* Stats grid */
|
|
44
|
+
.rss-stats-grid {
|
|
45
|
+
display: grid;
|
|
46
|
+
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
47
|
+
gap: var(--space-s, 0.75rem);
|
|
48
|
+
}
|
|
49
|
+
.rss-stat {
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
align-items: center;
|
|
53
|
+
padding: var(--space-s, 0.75rem);
|
|
54
|
+
background: var(--color-background, #fff);
|
|
55
|
+
border-radius: var(--border-radius-small, 0.5rem);
|
|
56
|
+
text-align: center;
|
|
57
|
+
}
|
|
58
|
+
.rss-stat-value {
|
|
59
|
+
font-size: 1.5rem;
|
|
60
|
+
font-weight: 700;
|
|
61
|
+
color: var(--color-accent, #ff6600);
|
|
62
|
+
}
|
|
63
|
+
.rss-stat-label {
|
|
64
|
+
font-size: 0.75rem;
|
|
65
|
+
color: var(--color-on-offset, #666);
|
|
66
|
+
text-transform: uppercase;
|
|
67
|
+
letter-spacing: 0.05em;
|
|
68
|
+
}
|
|
208
69
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
70
|
+
/* Sync status */
|
|
71
|
+
.rss-sync-bar {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: space-between;
|
|
75
|
+
gap: var(--space-m, 1rem);
|
|
76
|
+
flex-wrap: wrap;
|
|
77
|
+
}
|
|
78
|
+
.rss-sync-info {
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
gap: 0.25rem;
|
|
82
|
+
}
|
|
83
|
+
.rss-sync-label {
|
|
84
|
+
font-size: 0.75rem;
|
|
85
|
+
color: var(--color-on-offset, #666);
|
|
86
|
+
text-transform: uppercase;
|
|
87
|
+
}
|
|
88
|
+
.rss-sync-time {
|
|
89
|
+
font-weight: 500;
|
|
90
|
+
}
|
|
91
|
+
.rss-syncing {
|
|
92
|
+
display: inline-flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
gap: 0.5rem;
|
|
95
|
+
color: var(--color-accent, #ff6600);
|
|
96
|
+
}
|
|
97
|
+
.rss-spinner {
|
|
98
|
+
width: 16px;
|
|
99
|
+
height: 16px;
|
|
100
|
+
border: 2px solid currentColor;
|
|
101
|
+
border-top-color: transparent;
|
|
102
|
+
border-radius: 50%;
|
|
103
|
+
animation: rss-spin 0.8s linear infinite;
|
|
104
|
+
}
|
|
105
|
+
@keyframes rss-spin {
|
|
106
|
+
to { transform: rotate(360deg); }
|
|
107
|
+
}
|
|
248
108
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
109
|
+
/* Add feed form */
|
|
110
|
+
.rss-add-form {
|
|
111
|
+
display: flex;
|
|
112
|
+
gap: 0.5rem;
|
|
113
|
+
}
|
|
114
|
+
.rss-add-form input {
|
|
115
|
+
flex: 1;
|
|
116
|
+
appearance: none;
|
|
117
|
+
background-color: var(--color-background, #fff);
|
|
118
|
+
border: 1px solid var(--color-outline-variant, #ccc);
|
|
119
|
+
border-radius: var(--border-radius-small, 0.25rem);
|
|
120
|
+
font: var(--font-body, 0.875rem/1.4 sans-serif);
|
|
121
|
+
padding: calc(var(--space-s, 0.75rem) / 2) var(--space-s, 0.75rem);
|
|
122
|
+
}
|
|
123
|
+
.rss-add-form input:focus {
|
|
124
|
+
border-color: var(--color-primary, #0066cc);
|
|
125
|
+
outline: 2px solid var(--color-primary, #0066cc);
|
|
126
|
+
outline-offset: 1px;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Feed list */
|
|
130
|
+
.rss-feed-list {
|
|
131
|
+
list-style: none;
|
|
132
|
+
padding: 0;
|
|
133
|
+
margin: 0;
|
|
134
|
+
}
|
|
135
|
+
.rss-feed-item {
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
gap: 0.75rem;
|
|
139
|
+
padding: 0.75rem 0;
|
|
140
|
+
border-bottom: 1px solid var(--color-outline-variant, #eee);
|
|
141
|
+
}
|
|
142
|
+
.rss-feed-item:last-child { border-bottom: none; }
|
|
143
|
+
.rss-feed-icon {
|
|
144
|
+
width: 32px;
|
|
145
|
+
height: 32px;
|
|
146
|
+
object-fit: cover;
|
|
147
|
+
border-radius: 0.25rem;
|
|
148
|
+
flex-shrink: 0;
|
|
149
|
+
}
|
|
150
|
+
.rss-feed-icon-placeholder {
|
|
151
|
+
width: 32px;
|
|
152
|
+
height: 32px;
|
|
153
|
+
background: var(--color-outline-variant, #ddd);
|
|
154
|
+
border-radius: 0.25rem;
|
|
155
|
+
flex-shrink: 0;
|
|
156
|
+
display: flex;
|
|
157
|
+
align-items: center;
|
|
158
|
+
justify-content: center;
|
|
159
|
+
color: var(--color-on-offset, #666);
|
|
160
|
+
font-size: 1rem;
|
|
161
|
+
}
|
|
162
|
+
.rss-feed-info {
|
|
163
|
+
flex: 1;
|
|
164
|
+
min-width: 0;
|
|
165
|
+
}
|
|
166
|
+
.rss-feed-title {
|
|
167
|
+
font-weight: 500;
|
|
168
|
+
white-space: nowrap;
|
|
169
|
+
overflow: hidden;
|
|
170
|
+
text-overflow: ellipsis;
|
|
171
|
+
}
|
|
172
|
+
.rss-feed-meta {
|
|
173
|
+
display: flex;
|
|
174
|
+
gap: 0.5rem;
|
|
175
|
+
font-size: 0.75rem;
|
|
176
|
+
color: var(--color-on-offset, #666);
|
|
177
|
+
}
|
|
178
|
+
.rss-feed-actions {
|
|
179
|
+
display: flex;
|
|
180
|
+
gap: 0.5rem;
|
|
181
|
+
align-items: center;
|
|
182
|
+
}
|
|
183
|
+
.rss-feed-error {
|
|
184
|
+
color: #d32f2f;
|
|
185
|
+
font-size: 0.75rem;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* Item list */
|
|
189
|
+
.rss-item-list {
|
|
190
|
+
list-style: none;
|
|
191
|
+
padding: 0;
|
|
192
|
+
margin: 0;
|
|
193
|
+
}
|
|
194
|
+
.rss-item {
|
|
195
|
+
display: flex;
|
|
196
|
+
gap: 0.75rem;
|
|
197
|
+
padding: 0.75rem 0;
|
|
198
|
+
border-bottom: 1px solid var(--color-outline-variant, #eee);
|
|
199
|
+
}
|
|
200
|
+
.rss-item:last-child { border-bottom: none; }
|
|
201
|
+
.rss-item-image {
|
|
202
|
+
width: 60px;
|
|
203
|
+
height: 60px;
|
|
204
|
+
object-fit: cover;
|
|
205
|
+
border-radius: 0.25rem;
|
|
206
|
+
flex-shrink: 0;
|
|
207
|
+
}
|
|
208
|
+
.rss-item-placeholder {
|
|
209
|
+
width: 60px;
|
|
210
|
+
height: 60px;
|
|
211
|
+
background: var(--color-outline-variant, #ddd);
|
|
212
|
+
border-radius: 0.25rem;
|
|
213
|
+
flex-shrink: 0;
|
|
214
|
+
}
|
|
215
|
+
.rss-item-info {
|
|
216
|
+
flex: 1;
|
|
217
|
+
min-width: 0;
|
|
218
|
+
}
|
|
219
|
+
.rss-item-title {
|
|
220
|
+
font-weight: 500;
|
|
221
|
+
text-decoration: none;
|
|
222
|
+
color: inherit;
|
|
223
|
+
display: block;
|
|
224
|
+
white-space: nowrap;
|
|
225
|
+
overflow: hidden;
|
|
226
|
+
text-overflow: ellipsis;
|
|
227
|
+
}
|
|
228
|
+
.rss-item-title:hover { text-decoration: underline; }
|
|
229
|
+
.rss-item-description {
|
|
230
|
+
font-size: 0.875rem;
|
|
231
|
+
color: var(--color-on-offset, #666);
|
|
232
|
+
margin: 0.25rem 0;
|
|
233
|
+
display: -webkit-box;
|
|
234
|
+
-webkit-line-clamp: 2;
|
|
235
|
+
-webkit-box-orient: vertical;
|
|
236
|
+
overflow: hidden;
|
|
237
|
+
}
|
|
238
|
+
.rss-item-meta {
|
|
239
|
+
font-size: 0.75rem;
|
|
240
|
+
color: var(--color-on-offset, #666);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* Toggle switch */
|
|
244
|
+
.rss-toggle {
|
|
245
|
+
position: relative;
|
|
246
|
+
width: 40px;
|
|
247
|
+
height: 22px;
|
|
248
|
+
cursor: pointer;
|
|
249
|
+
}
|
|
250
|
+
.rss-toggle input {
|
|
251
|
+
opacity: 0;
|
|
252
|
+
width: 0;
|
|
253
|
+
height: 0;
|
|
254
|
+
}
|
|
255
|
+
.rss-toggle-slider {
|
|
256
|
+
position: absolute;
|
|
257
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
258
|
+
background-color: #ccc;
|
|
259
|
+
border-radius: 22px;
|
|
260
|
+
transition: 0.3s;
|
|
261
|
+
}
|
|
262
|
+
.rss-toggle-slider:before {
|
|
263
|
+
position: absolute;
|
|
264
|
+
content: "";
|
|
265
|
+
height: 16px;
|
|
266
|
+
width: 16px;
|
|
267
|
+
left: 3px;
|
|
268
|
+
bottom: 3px;
|
|
269
|
+
background-color: white;
|
|
270
|
+
border-radius: 50%;
|
|
271
|
+
transition: 0.3s;
|
|
272
|
+
}
|
|
273
|
+
.rss-toggle input:checked + .rss-toggle-slider {
|
|
274
|
+
background-color: var(--color-accent, #ff6600);
|
|
275
|
+
}
|
|
276
|
+
.rss-toggle input:checked + .rss-toggle-slider:before {
|
|
277
|
+
transform: translateX(18px);
|
|
278
|
+
}
|
|
263
279
|
|
|
264
|
-
|
|
265
|
-
|
|
280
|
+
/* Delete button */
|
|
281
|
+
.rss-delete-btn {
|
|
282
|
+
background: none;
|
|
283
|
+
border: none;
|
|
284
|
+
color: #d32f2f;
|
|
285
|
+
cursor: pointer;
|
|
286
|
+
padding: 0.25rem;
|
|
287
|
+
font-size: 1rem;
|
|
288
|
+
opacity: 0.6;
|
|
289
|
+
}
|
|
290
|
+
.rss-delete-btn:hover { opacity: 1; }
|
|
291
|
+
|
|
292
|
+
/* Public link */
|
|
293
|
+
.rss-public-link {
|
|
294
|
+
display: flex;
|
|
295
|
+
align-items: center;
|
|
296
|
+
justify-content: space-between;
|
|
297
|
+
gap: var(--space-m, 1rem);
|
|
298
|
+
flex-wrap: wrap;
|
|
299
|
+
}
|
|
300
|
+
.rss-public-link p {
|
|
301
|
+
margin: 0;
|
|
302
|
+
color: var(--color-on-offset, #666);
|
|
303
|
+
font: var(--font-caption, 0.875rem/1.4 sans-serif);
|
|
304
|
+
}
|
|
305
|
+
</style>
|
|
306
|
+
|
|
307
|
+
{# Notifications #}
|
|
308
|
+
{% if synced %}
|
|
309
|
+
<div class="rss-notification rss-notification--success">
|
|
310
|
+
{% if syncedItems and syncedItems != "0" %}
|
|
311
|
+
Synced {{ syncedItems }} new items from {{ syncedFeeds }} feeds
|
|
266
312
|
{% else %}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
313
|
+
Feeds are up to date, nothing new to sync
|
|
314
|
+
{% endif %}
|
|
315
|
+
{% if syncedPruned and syncedPruned != "0" %}
|
|
316
|
+
({{ syncedPruned }} old items pruned)
|
|
317
|
+
{% endif %}
|
|
318
|
+
</div>
|
|
319
|
+
{% endif %}
|
|
320
|
+
|
|
321
|
+
{% if syncInProgress %}
|
|
322
|
+
<div class="rss-notification rss-notification--warning">
|
|
323
|
+
A sync is already in progress, please wait
|
|
324
|
+
</div>
|
|
325
|
+
{% endif %}
|
|
326
|
+
|
|
327
|
+
{% if queryError %}
|
|
328
|
+
<div class="rss-notification rss-notification--error">
|
|
329
|
+
{{ queryError }}
|
|
330
|
+
</div>
|
|
331
|
+
{% endif %}
|
|
332
|
+
|
|
333
|
+
{% if configError %}
|
|
334
|
+
<div class="rss-notification rss-notification--error">
|
|
335
|
+
{{ configError }}
|
|
336
|
+
</div>
|
|
337
|
+
{% endif %}
|
|
338
|
+
|
|
339
|
+
<div class="rss-dashboard">
|
|
340
|
+
{% if not configError %}
|
|
341
|
+
{# Sync Status & Actions #}
|
|
342
|
+
<section class="rss-section">
|
|
343
|
+
<h2>{{ __("rss.syncStatus") }}</h2>
|
|
344
|
+
<div class="rss-sync-bar">
|
|
345
|
+
<div class="rss-sync-info">
|
|
346
|
+
<span class="rss-sync-label">{{ __("rss.lastSynced") }}</span>
|
|
347
|
+
<span class="rss-sync-time">
|
|
348
|
+
{% if syncState.syncing %}
|
|
349
|
+
<span class="rss-syncing">
|
|
350
|
+
<span class="rss-spinner"></span>
|
|
351
|
+
{{ __("rss.syncing") }}
|
|
352
|
+
</span>
|
|
353
|
+
{% elif syncState.lastSync %}
|
|
354
|
+
{{ syncState.lastSync | date("PPpp") }}
|
|
355
|
+
{% else %}
|
|
356
|
+
{{ __("rss.never") }}
|
|
357
|
+
{% endif %}
|
|
358
|
+
</span>
|
|
359
|
+
{% if syncState.lastError %}
|
|
360
|
+
<span class="rss-feed-error">{{ syncState.lastError }}</span>
|
|
281
361
|
{% endif %}
|
|
282
|
-
</
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
{{
|
|
290
|
-
type
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
</
|
|
295
|
-
<form action="{{ publicUrl }}/clear-resync" method="post" style="margin: 0;" id="clear-resync-form">
|
|
296
|
-
{{ button({
|
|
297
|
-
type: "submit",
|
|
298
|
-
text: __("rss.clearResync"),
|
|
299
|
-
disabled: syncState.syncing
|
|
300
|
-
}) }}
|
|
301
|
-
</form>
|
|
362
|
+
</div>
|
|
363
|
+
<div class="button-group" style="display: flex; gap: 0.5rem;">
|
|
364
|
+
<form action="{{ mountPath }}/sync" method="post" style="display: inline;">
|
|
365
|
+
<button type="submit" class="button button--primary" {% if syncState.syncing %}disabled{% endif %}>
|
|
366
|
+
{{ __("rss.syncNow") }}
|
|
367
|
+
</button>
|
|
368
|
+
</form>
|
|
369
|
+
<form action="{{ mountPath }}/clear-resync" method="post" style="display: inline;" id="clear-resync-form">
|
|
370
|
+
<button type="submit" class="button" {% if syncState.syncing %}disabled{% endif %}>
|
|
371
|
+
{{ __("rss.clearResync") }}
|
|
372
|
+
</button>
|
|
373
|
+
</form>
|
|
374
|
+
</div>
|
|
302
375
|
</div>
|
|
303
|
-
</
|
|
376
|
+
</section>
|
|
304
377
|
|
|
305
378
|
{# Stats #}
|
|
306
379
|
<section class="rss-section">
|
|
380
|
+
<h2>{{ __("rss.status") }}</h2>
|
|
307
381
|
<div class="rss-stats-grid">
|
|
308
382
|
<div class="rss-stat">
|
|
309
383
|
<span class="rss-stat-value">{{ totalFeeds }}</span>
|
|
@@ -323,24 +397,23 @@
|
|
|
323
397
|
{# Add Feed Form #}
|
|
324
398
|
<section class="rss-section">
|
|
325
399
|
<h2>{{ __("rss.addFeed") }}</h2>
|
|
326
|
-
<form class="rss-add-form" action="{{
|
|
400
|
+
<form class="rss-add-form" action="{{ mountPath }}/api/feeds" method="post" id="add-feed-form">
|
|
327
401
|
<input
|
|
328
402
|
type="url"
|
|
329
403
|
name="url"
|
|
330
404
|
placeholder="{{ __("rss.feedUrlPlaceholder") }}"
|
|
331
405
|
required
|
|
332
406
|
>
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}) }}
|
|
407
|
+
<button type="submit" class="button button--primary">
|
|
408
|
+
{{ __("rss.addFeed") }}
|
|
409
|
+
</button>
|
|
337
410
|
</form>
|
|
338
411
|
</section>
|
|
339
412
|
|
|
340
413
|
{# Feeds List #}
|
|
414
|
+
{% if feeds and feeds.length > 0 %}
|
|
341
415
|
<section class="rss-section">
|
|
342
416
|
<h2>{{ __("rss.feeds") }}</h2>
|
|
343
|
-
{% if feeds and feeds.length > 0 %}
|
|
344
417
|
<ul class="rss-feed-list">
|
|
345
418
|
{% for feed in feeds %}
|
|
346
419
|
<li class="rss-feed-item" data-feed-id="{{ feed.id }}">
|
|
@@ -380,15 +453,13 @@
|
|
|
380
453
|
</li>
|
|
381
454
|
{% endfor %}
|
|
382
455
|
</ul>
|
|
383
|
-
{% else %}
|
|
384
|
-
{{ prose({ text: __("rss.noFeeds") }) }}
|
|
385
|
-
{% endif %}
|
|
386
456
|
</section>
|
|
457
|
+
{% endif %}
|
|
387
458
|
|
|
388
459
|
{# Recent Items #}
|
|
460
|
+
{% if recentItems and recentItems.length > 0 %}
|
|
389
461
|
<section class="rss-section">
|
|
390
462
|
<h2>{{ __("rss.recentItems") }}</h2>
|
|
391
|
-
{% if recentItems and recentItems.length > 0 %}
|
|
392
463
|
<ul class="rss-item-list">
|
|
393
464
|
{% for item in recentItems %}
|
|
394
465
|
<li class="rss-item">
|
|
@@ -414,133 +485,132 @@
|
|
|
414
485
|
</li>
|
|
415
486
|
{% endfor %}
|
|
416
487
|
</ul>
|
|
417
|
-
{% else %}
|
|
418
|
-
{{ prose({ text: __("rss.noItems") }) }}
|
|
419
|
-
{% endif %}
|
|
420
488
|
</section>
|
|
489
|
+
{% endif %}
|
|
421
490
|
|
|
422
|
-
{#
|
|
423
|
-
<
|
|
424
|
-
<
|
|
425
|
-
|
|
426
|
-
href
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
</
|
|
491
|
+
{# Public Page Link #}
|
|
492
|
+
<section class="rss-section">
|
|
493
|
+
<div class="rss-public-link">
|
|
494
|
+
<p>{{ __("rss.widget.description") }}</p>
|
|
495
|
+
<a href="/news/" class="button button--secondary" target="_blank">
|
|
496
|
+
{{ __("rss.widget.view") }}
|
|
497
|
+
</a>
|
|
498
|
+
</div>
|
|
499
|
+
</section>
|
|
431
500
|
{% endif %}
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
<script>
|
|
504
|
+
// Handle add feed form
|
|
505
|
+
document.getElementById('add-feed-form')?.addEventListener('submit', async (e) => {
|
|
506
|
+
e.preventDefault();
|
|
507
|
+
const form = e.target;
|
|
508
|
+
const url = form.url.value;
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
const response = await fetch(form.action, {
|
|
512
|
+
method: 'POST',
|
|
513
|
+
headers: { 'Content-Type': 'application/json' },
|
|
514
|
+
body: JSON.stringify({ url })
|
|
515
|
+
});
|
|
432
516
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
517
|
+
const data = await response.json();
|
|
518
|
+
if (response.ok) {
|
|
519
|
+
location.reload();
|
|
520
|
+
} else {
|
|
521
|
+
alert(data.error || 'Failed to add feed');
|
|
522
|
+
}
|
|
523
|
+
} catch (err) {
|
|
524
|
+
alert('Failed to add feed: ' + err.message);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Handle toggle feed
|
|
529
|
+
document.querySelectorAll('[data-toggle-feed]').forEach(input => {
|
|
530
|
+
input.addEventListener('change', async (e) => {
|
|
531
|
+
const feedId = e.target.dataset.toggleFeed;
|
|
532
|
+
const enabled = e.target.checked;
|
|
439
533
|
|
|
440
534
|
try {
|
|
441
|
-
const response = await fetch(
|
|
442
|
-
method: '
|
|
535
|
+
const response = await fetch(`{{ mountPath }}/api/feeds/${feedId}`, {
|
|
536
|
+
method: 'PATCH',
|
|
443
537
|
headers: { 'Content-Type': 'application/json' },
|
|
444
|
-
body: JSON.stringify({
|
|
538
|
+
body: JSON.stringify({ enabled })
|
|
445
539
|
});
|
|
446
540
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
alert(data.error || 'Failed to add feed');
|
|
541
|
+
if (!response.ok) {
|
|
542
|
+
e.target.checked = !enabled;
|
|
543
|
+
const data = await response.json();
|
|
544
|
+
alert(data.error || 'Failed to update feed');
|
|
452
545
|
}
|
|
453
546
|
} catch (err) {
|
|
454
|
-
|
|
547
|
+
e.target.checked = !enabled;
|
|
548
|
+
alert('Failed to update feed: ' + err.message);
|
|
455
549
|
}
|
|
456
550
|
});
|
|
551
|
+
});
|
|
457
552
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if (!response.ok) {
|
|
472
|
-
e.target.checked = !enabled;
|
|
473
|
-
const data = await response.json();
|
|
474
|
-
alert(data.error || 'Failed to update feed');
|
|
475
|
-
}
|
|
476
|
-
} catch (err) {
|
|
477
|
-
e.target.checked = !enabled;
|
|
478
|
-
alert('Failed to update feed: ' + err.message);
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
});
|
|
553
|
+
// Handle clear & re-sync
|
|
554
|
+
document.getElementById('clear-resync-form')?.addEventListener('submit', async (e) => {
|
|
555
|
+
e.preventDefault();
|
|
556
|
+
if (!confirm('This will delete all cached items and re-fetch them from feeds. Continue?')) return;
|
|
557
|
+
|
|
558
|
+
const form = e.target;
|
|
559
|
+
const submitBtn = form.querySelector('button[type="submit"]');
|
|
560
|
+
const originalText = submitBtn?.textContent;
|
|
561
|
+
if (submitBtn) {
|
|
562
|
+
submitBtn.disabled = true;
|
|
563
|
+
submitBtn.textContent = 'Clearing...';
|
|
564
|
+
}
|
|
482
565
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
566
|
+
try {
|
|
567
|
+
const response = await fetch(form.action, {
|
|
568
|
+
method: 'POST',
|
|
569
|
+
headers: { 'Content-Type': 'application/json' }
|
|
570
|
+
});
|
|
487
571
|
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
572
|
+
const data = await response.json();
|
|
573
|
+
if (response.ok) {
|
|
574
|
+
alert(`Cleared ${data.itemsCleared} items, re-fetched ${data.itemsAdded} items from ${data.feedsProcessed} feeds.`);
|
|
575
|
+
location.reload();
|
|
576
|
+
} else {
|
|
577
|
+
alert(data.error || 'Failed to clear & re-sync');
|
|
578
|
+
if (submitBtn) {
|
|
579
|
+
submitBtn.disabled = false;
|
|
580
|
+
submitBtn.textContent = originalText;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
} catch (err) {
|
|
584
|
+
alert('Failed to clear & re-sync: ' + err.message);
|
|
491
585
|
if (submitBtn) {
|
|
492
|
-
submitBtn.disabled =
|
|
493
|
-
submitBtn.textContent =
|
|
586
|
+
submitBtn.disabled = false;
|
|
587
|
+
submitBtn.textContent = originalText;
|
|
494
588
|
}
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// Handle delete feed
|
|
593
|
+
document.querySelectorAll('[data-delete-feed]').forEach(btn => {
|
|
594
|
+
btn.addEventListener('click', async (e) => {
|
|
595
|
+
const feedId = e.target.dataset.deleteFeed;
|
|
596
|
+
|
|
597
|
+
if (!confirm('Are you sure you want to remove this feed?')) return;
|
|
495
598
|
|
|
496
599
|
try {
|
|
497
|
-
const response = await fetch(
|
|
498
|
-
method: '
|
|
499
|
-
headers: { 'Content-Type': 'application/json' }
|
|
600
|
+
const response = await fetch(`{{ mountPath }}/api/feeds/${feedId}`, {
|
|
601
|
+
method: 'DELETE'
|
|
500
602
|
});
|
|
501
603
|
|
|
502
|
-
const data = await response.json();
|
|
503
604
|
if (response.ok) {
|
|
504
|
-
alert(`Cleared ${data.itemsCleared} items, re-fetched ${data.itemsAdded} items from ${data.feedsProcessed} feeds.`);
|
|
505
605
|
location.reload();
|
|
506
606
|
} else {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
submitBtn.disabled = false;
|
|
510
|
-
submitBtn.textContent = originalText;
|
|
511
|
-
}
|
|
607
|
+
const data = await response.json();
|
|
608
|
+
alert(data.error || 'Failed to remove feed');
|
|
512
609
|
}
|
|
513
610
|
} catch (err) {
|
|
514
|
-
alert('Failed to
|
|
515
|
-
if (submitBtn) {
|
|
516
|
-
submitBtn.disabled = false;
|
|
517
|
-
submitBtn.textContent = originalText;
|
|
518
|
-
}
|
|
611
|
+
alert('Failed to remove feed: ' + err.message);
|
|
519
612
|
}
|
|
520
613
|
});
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
document.querySelectorAll('[data-delete-feed]').forEach(btn => {
|
|
524
|
-
btn.addEventListener('click', async (e) => {
|
|
525
|
-
const feedId = e.target.dataset.deleteFeed;
|
|
526
|
-
|
|
527
|
-
if (!confirm('Are you sure you want to remove this feed?')) return;
|
|
528
|
-
|
|
529
|
-
try {
|
|
530
|
-
const response = await fetch(`{{ publicUrl }}/api/feeds/${feedId}`, {
|
|
531
|
-
method: 'DELETE'
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
if (response.ok) {
|
|
535
|
-
location.reload();
|
|
536
|
-
} else {
|
|
537
|
-
const data = await response.json();
|
|
538
|
-
alert(data.error || 'Failed to remove feed');
|
|
539
|
-
}
|
|
540
|
-
} catch (err) {
|
|
541
|
-
alert('Failed to remove feed: ' + err.message);
|
|
542
|
-
}
|
|
543
|
-
});
|
|
544
|
-
});
|
|
545
|
-
</script>
|
|
614
|
+
});
|
|
615
|
+
</script>
|
|
546
616
|
{% endblock %}
|