fix: refollow UI - progress bar, pause button, status endpoint

Three issues fixed:

1. Progress bar invisible: used --color-accent (doesn't exist in
   Indiekit theme). Changed to --color-primary.

2. Pause/resume buttons non-functional: the /admin/refollow/status
   GET endpoint was intercepted by Fedify middleware (content
   negotiation routes) returning 404 before Express saw it. Added
   /admin path skip to content negotiation middleware. Also made
   buttons toggle dynamically via Alpine.js x-show instead of
   server-rendered {% if %}.

3. Status badge static: replaced Nunjucks badge macro with Alpine.js
   x-text bound to a computed statusLabel property.
This commit is contained in:
Ricardo
2026-02-20 10:29:04 +01:00
parent 505daa68c5
commit 06b8509d8a
3 changed files with 34 additions and 23 deletions
+2
View File
@@ -166,6 +166,8 @@ export default class ActivityPubEndpoint {
router.use((req, res, next) => { router.use((req, res, next) => {
if (!self._fedifyMiddleware) return next(); if (!self._fedifyMiddleware) return next();
if (req.method !== "GET" && req.method !== "HEAD") return next(); if (req.method !== "GET" && req.method !== "HEAD") return next();
// Skip Fedify for admin routes — handled by authenticated router
if (req.path.startsWith("/admin")) return next();
return self._fedifyMiddleware(req, res, next); return self._fedifyMiddleware(req, res, next);
}); });
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@rmdes/indiekit-endpoint-activitypub", "name": "@rmdes/indiekit-endpoint-activitypub",
"version": "1.0.17", "version": "1.0.18",
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
"keywords": [ "keywords": [
"indiekit", "indiekit",
+24 -15
View File
@@ -39,8 +39,8 @@
{# Progress bar #} {# Progress bar #}
<div style="background: var(--color-offset); border-radius: 4px; height: 1.5rem; margin-block-end: var(--space-m); overflow: hidden;"> <div style="background: var(--color-offset); border-radius: 4px; height: 1.5rem; margin-block-end: var(--space-m); overflow: hidden;">
<div <div
x-bind:style="'width:' + progress + '%; background: var(--color-accent); height: 100%; transition: width 0.5s ease;'" x-bind:style="'width:' + progress + '%; height: 100%; transition: width 0.5s ease; background: var(--color-primary);'"
style="width: {{ refollowStatus.progressPercent }}%; background: var(--color-accent); height: 100%; transition: width 0.5s ease;"> style="width: {{ refollowStatus.progressPercent }}%; height: 100%; transition: width 0.5s ease; background: var(--color-primary);">
</div> </div>
</div> </div>
@@ -48,39 +48,42 @@
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); gap: var(--space-s); margin-block-end: var(--space-m);"> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); gap: var(--space-s); margin-block-end: var(--space-m);">
<div style="padding: var(--space-s); background: var(--color-offset); border-radius: 4px; text-align: center;"> <div style="padding: var(--space-s); background: var(--color-offset); border-radius: 4px; text-align: center;">
<div style="font-size: var(--font-size-xl);" x-text="remaining">{{ refollowStatus.remaining }}</div> <div style="font-size: var(--font-size-xl);" x-text="remaining">{{ refollowStatus.remaining }}</div>
<div style="font-size: var(--font-size-s); color: var(--color-text-offset);">{{ __("activitypub.refollow.remaining") }}</div> <div style="font-size: var(--font-size-s); color: var(--color-outline);">{{ __("activitypub.refollow.remaining") }}</div>
</div> </div>
<div style="padding: var(--space-s); background: var(--color-offset); border-radius: 4px; text-align: center;"> <div style="padding: var(--space-s); background: var(--color-offset); border-radius: 4px; text-align: center;">
<div style="font-size: var(--font-size-xl);" x-text="sent">{{ refollowStatus.sent }}</div> <div style="font-size: var(--font-size-xl);" x-text="sent">{{ refollowStatus.sent }}</div>
<div style="font-size: var(--font-size-s); color: var(--color-text-offset);">{{ __("activitypub.refollow.awaitingAccept") }}</div> <div style="font-size: var(--font-size-s); color: var(--color-outline);">{{ __("activitypub.refollow.awaitingAccept") }}</div>
</div> </div>
<div style="padding: var(--space-s); background: var(--color-offset); border-radius: 4px; text-align: center;"> <div style="padding: var(--space-s); background: var(--color-offset); border-radius: 4px; text-align: center;">
<div style="font-size: var(--font-size-xl);" x-text="federated">{{ refollowStatus.federated }}</div> <div style="font-size: var(--font-size-xl);" x-text="federated">{{ refollowStatus.federated }}</div>
<div style="font-size: var(--font-size-s); color: var(--color-text-offset);">{{ __("activitypub.refollow.accepted") }}</div> <div style="font-size: var(--font-size-s); color: var(--color-outline);">{{ __("activitypub.refollow.accepted") }}</div>
</div> </div>
<div style="padding: var(--space-s); background: var(--color-offset); border-radius: 4px; text-align: center;"> <div style="padding: var(--space-s); background: var(--color-offset); border-radius: 4px; text-align: center;">
<div style="font-size: var(--font-size-xl);" x-text="failed">{{ refollowStatus.failed }}</div> <div style="font-size: var(--font-size-xl);" x-text="failed">{{ refollowStatus.failed }}</div>
<div style="font-size: var(--font-size-s); color: var(--color-text-offset);">{{ __("activitypub.refollow.failed") }}</div> <div style="font-size: var(--font-size-s); color: var(--color-outline);">{{ __("activitypub.refollow.failed") }}</div>
</div> </div>
</div> </div>
{# Status + controls #} {# Status + controls #}
<div style="display: flex; align-items: center; gap: var(--space-s);"> <div style="display: flex; align-items: center; gap: var(--space-s);">
{{ badge({ text: __("activitypub.refollow.status." + refollowStatus.status) }) }} <span class="badge" x-text="statusLabel">{{ __("activitypub.refollow.status." + refollowStatus.status) }}</span>
{% if refollowStatus.status === "running" %} <form x-show="status === 'running'" method="post" action="{{ mountPath }}/admin/refollow/pause" x-on:submit.prevent="pause" style="display: none;">
<form method="post" action="{{ mountPath }}/admin/refollow/pause" x-on:submit.prevent="pause">
<button type="submit" class="button" style="font-size: var(--font-size-s);">{{ __("activitypub.refollow.pause") }}</button> <button type="submit" class="button" style="font-size: var(--font-size-s);">{{ __("activitypub.refollow.pause") }}</button>
</form> </form>
{% elif refollowStatus.status === "paused" %} <form x-show="status === 'paused'" method="post" action="{{ mountPath }}/admin/refollow/resume" x-on:submit.prevent="resume" style="display: none;">
<form method="post" action="{{ mountPath }}/admin/refollow/resume" x-on:submit.prevent="resume">
<button type="submit" class="button" style="font-size: var(--font-size-s);">{{ __("activitypub.refollow.resume") }}</button> <button type="submit" class="button" style="font-size: var(--font-size-s);">{{ __("activitypub.refollow.resume") }}</button>
</form> </form>
{% endif %}
</div> </div>
</section> </section>
<script> <script>
function refollowProgress(mountPath) { function refollowProgress(mountPath) {
const statusLabels = {
idle: '{{ __("activitypub.refollow.status.idle") }}',
running: '{{ __("activitypub.refollow.status.running") }}',
paused: '{{ __("activitypub.refollow.status.paused") }}',
completed: '{{ __("activitypub.refollow.status.completed") }}'
};
return { return {
progress: {{ refollowStatus.progressPercent }}, progress: {{ refollowStatus.progressPercent }},
remaining: {{ refollowStatus.remaining }}, remaining: {{ refollowStatus.remaining }},
@@ -89,6 +92,9 @@
failed: {{ refollowStatus.failed }}, failed: {{ refollowStatus.failed }},
status: '{{ refollowStatus.status }}', status: '{{ refollowStatus.status }}',
interval: null, interval: null,
get statusLabel() {
return statusLabels[this.status] || this.status;
},
init() { init() {
if (this.status === 'running' || this.status === 'paused') { if (this.status === 'running' || this.status === 'paused') {
this.interval = setInterval(() => this.poll(), 10000); this.interval = setInterval(() => this.poll(), 10000);
@@ -100,6 +106,7 @@
async poll() { async poll() {
try { try {
const res = await fetch(mountPath + '/admin/refollow/status'); const res = await fetch(mountPath + '/admin/refollow/status');
if (!res.ok) return;
const data = await res.json(); const data = await res.json();
this.progress = data.progressPercent; this.progress = data.progressPercent;
this.remaining = data.remaining; this.remaining = data.remaining;
@@ -113,16 +120,18 @@
} catch {} } catch {}
}, },
async pause() { async pause() {
await fetch(mountPath + '/admin/refollow/pause', { method: 'POST' }); const res = await fetch(mountPath + '/admin/refollow/pause', { method: 'POST' });
this.status = 'paused'; if (res.ok) this.status = 'paused';
}, },
async resume() { async resume() {
await fetch(mountPath + '/admin/refollow/resume', { method: 'POST' }); const res = await fetch(mountPath + '/admin/refollow/resume', { method: 'POST' });
if (res.ok) {
this.status = 'running'; this.status = 'running';
if (!this.interval) { if (!this.interval) {
this.interval = setInterval(() => this.poll(), 10000); this.interval = setInterval(() => this.poll(), 10000);
} }
} }
}
}; };
} }
</script> </script>