@rmdes/indiekit-endpoint-conversations 2.3.2 → 2.4.1

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.
@@ -80,6 +80,19 @@ async function dashboard(request, response) {
80
80
  }
81
81
  }
82
82
 
83
+ // Get item counts by channel (ingestion path)
84
+ let channelCounts = {};
85
+ if (itemsCollection) {
86
+ const counts = await itemsCollection
87
+ .aggregate([
88
+ { $group: { _id: "$channel", count: { $sum: 1 } } },
89
+ ])
90
+ .toArray();
91
+ for (const c of counts) {
92
+ channelCounts[c._id] = c.count;
93
+ }
94
+ }
95
+
83
96
  response.render("conversations", {
84
97
  title: response.__
85
98
  ? response.__("conversations.title")
@@ -90,6 +103,7 @@ async function dashboard(request, response) {
90
103
  summaries,
91
104
  recentItems,
92
105
  platformCounts,
106
+ channelCounts,
93
107
  typeCounts,
94
108
  baseUrl: config.mountPath || "/conversations",
95
109
  });
@@ -103,6 +117,7 @@ async function dashboard(request, response) {
103
117
  summaries: [],
104
118
  recentItems: [],
105
119
  platformCounts: {},
120
+ channelCounts: {},
106
121
  typeCounts: {},
107
122
  });
108
123
  }
@@ -399,6 +414,7 @@ async function ingest(request, response) {
399
414
  const item = {
400
415
  canonical_url: canonicalUrl,
401
416
  source: classification.source,
417
+ channel: "webhook",
402
418
  type: classification.type,
403
419
  author: webmention.author || {
404
420
  name: "Unknown",
@@ -103,6 +103,9 @@ export async function runPollCycle(indiekit, options) {
103
103
 
104
104
  // Backfill platform names for items stored as "activitypub" (one-time)
105
105
  await backfillPlatformNames(indiekit, stateCollection);
106
+
107
+ // Backfill channel field for items predating its introduction (one-time)
108
+ await backfillChannelField(indiekit, stateCollection);
106
109
  }
107
110
 
108
111
  /**
@@ -377,6 +380,84 @@ async function backfillPlatformNames(indiekit, stateCollection) {
377
380
  }
378
381
  }
379
382
 
383
+ /**
384
+ * Backfill the `channel` field for existing items that predate its introduction.
385
+ * Derives channel from platform_id prefix and bridgy_url presence.
386
+ * One-time operation — marks complete after first successful run.
387
+ */
388
+ async function backfillChannelField(indiekit, stateCollection) {
389
+ try {
390
+ const itemsCollection = indiekit.collections.get("conversation_items");
391
+ if (!itemsCollection) return;
392
+
393
+ const state = await stateCollection.findOne({ _id: "poll_cursors" });
394
+ if (state?.channel_backfill_complete) return;
395
+
396
+ // Find all items missing the channel field
397
+ const itemsWithoutChannel = await itemsCollection
398
+ .find({ channel: { $exists: false } })
399
+ .project({ _id: 1, platform_id: 1, bridgy_url: 1 })
400
+ .toArray();
401
+
402
+ if (itemsWithoutChannel.length === 0) {
403
+ await stateCollection.findOneAndUpdate(
404
+ { _id: "poll_cursors" },
405
+ { $set: { channel_backfill_complete: true } },
406
+ { upsert: true },
407
+ );
408
+ return;
409
+ }
410
+
411
+ // Group items by derived channel for batched updateMany
412
+ const groups = new Map();
413
+
414
+ for (const item of itemsWithoutChannel) {
415
+ const pid = item.platform_id || "";
416
+ const hasBridgy = !!item.bridgy_url;
417
+ let channel;
418
+
419
+ if (pid.startsWith("activitypub:")) {
420
+ channel = "activitypub_inbox";
421
+ } else if (hasBridgy) {
422
+ channel = "webhook";
423
+ } else if (pid.startsWith("mastodon:")) {
424
+ channel = "mastodon_api";
425
+ } else if (pid.startsWith("bluesky:")) {
426
+ channel = "bluesky_api";
427
+ } else {
428
+ channel = "webhook";
429
+ }
430
+
431
+ if (!groups.has(channel)) groups.set(channel, []);
432
+ groups.get(channel).push(item._id);
433
+ }
434
+
435
+ let updated = 0;
436
+
437
+ for (const [channel, ids] of groups) {
438
+ const result = await itemsCollection.updateMany(
439
+ { _id: { $in: ids } },
440
+ { $set: { channel } },
441
+ );
442
+ updated += result.modifiedCount;
443
+ }
444
+
445
+ if (updated > 0) {
446
+ console.info(
447
+ `[Conversations] Channel backfill: set channel on ${updated} items`,
448
+ );
449
+ }
450
+
451
+ await stateCollection.findOneAndUpdate(
452
+ { _id: "poll_cursors" },
453
+ { $set: { channel_backfill_complete: true } },
454
+ { upsert: true },
455
+ );
456
+ } catch (error) {
457
+ console.warn("[Conversations] Channel backfill error:", error.message);
458
+ }
459
+ }
460
+
380
461
  /**
381
462
  * Poll Mastodon notifications and store matching interactions
382
463
  */
@@ -431,6 +512,7 @@ async function pollMastodon(indiekit, stateCollection, state, credentials) {
431
512
  await upsertConversationItem(indiekit, {
432
513
  canonical_url: canonicalUrl,
433
514
  source: "mastodon",
515
+ channel: "mastodon_api",
434
516
  type: notification.type,
435
517
  author: notification.author,
436
518
  content: notification.content,
@@ -527,6 +609,7 @@ async function pollBluesky(indiekit, stateCollection, state, credentials) {
527
609
  await upsertConversationItem(indiekit, {
528
610
  canonical_url: canonicalUrl,
529
611
  source: "bluesky",
612
+ channel: "bluesky_api",
530
613
  type: notification.type,
531
614
  author: notification.author,
532
615
  content: notification.content,
@@ -644,6 +727,7 @@ async function pollActivityPub(indiekit, stateCollection, state) {
644
727
  await upsertConversationItem(indiekit, {
645
728
  canonical_url: interaction.canonical_url,
646
729
  source: interaction.platform,
730
+ channel: "activitypub_inbox",
647
731
  type: interaction.type,
648
732
  author: interaction.author,
649
733
  content: interaction.content,
@@ -164,4 +164,9 @@ export async function createIndexes(application) {
164
164
  { name: "source_filter" },
165
165
  );
166
166
 
167
+ await collection.createIndex(
168
+ { channel: 1, received_at: -1 },
169
+ { name: "channel_filter" },
170
+ );
171
+
167
172
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmdes/indiekit-endpoint-conversations",
3
- "version": "2.3.2",
3
+ "version": "2.4.1",
4
4
  "description": "Conversation aggregation endpoint for Indiekit. Backend enrichment service that polls Mastodon/Bluesky notifications and serves JF2-compatible data for the interactions page.",
5
5
  "keywords": [
6
6
  "indiekit",
@@ -2,8 +2,6 @@
2
2
 
3
3
  {% block content %}
4
4
  <div class="panel">
5
- <h1>{{ __("conversations.title") }}</h1>
6
-
7
5
  {% if error %}
8
6
  <p class="badge badge--error">{{ error }}</p>
9
7
  {% endif %}
@@ -37,7 +35,7 @@
37
35
  </p>
38
36
  {% endif %}
39
37
  <p style="font-size: 0.85em; margin: 0.25rem 0">
40
- {{ platformCounts.mastodon or 0 }} {{ __("conversations.dashboard.itemsCollected") }}
38
+ {{ channelCounts.mastodon_api or 0 }} {{ __("conversations.dashboard.itemsCollected") }}
41
39
  </p>
42
40
  {% else %}
43
41
  <p style="font-size: 0.85em; color: #6b7280; margin: 0.25rem 0">
@@ -71,7 +69,7 @@
71
69
  </p>
72
70
  {% endif %}
73
71
  <p style="font-size: 0.85em; margin: 0.25rem 0">
74
- {{ platformCounts.bluesky or 0 }} {{ __("conversations.dashboard.itemsCollected") }}
72
+ {{ channelCounts.bluesky_api or 0 }} {{ __("conversations.dashboard.itemsCollected") }}
75
73
  </p>
76
74
  {% else %}
77
75
  <p style="font-size: 0.85em; color: #6b7280; margin: 0.25rem 0">
@@ -105,7 +103,7 @@
105
103
  </p>
106
104
  {% endif %}
107
105
  <p style="font-size: 0.85em; margin: 0.25rem 0">
108
- {{ platformCounts.activitypub or 0 }} {{ __("conversations.dashboard.itemsCollected") }}
106
+ {{ channelCounts.activitypub_inbox or 0 }} {{ __("conversations.dashboard.itemsCollected") }}
109
107
  </p>
110
108
  {% else %}
111
109
  <p style="font-size: 0.85em; color: #6b7280; margin: 0.25rem 0">
@@ -130,7 +128,7 @@
130
128
  POST {{ baseUrl }}/ingest
131
129
  </code>
132
130
  <p style="font-size: 0.85em; margin: 0.25rem 0">
133
- {{ platformCounts.webmention or 0 }} {{ __("conversations.dashboard.itemsReceived") }}
131
+ {{ channelCounts.webhook or 0 }} {{ __("conversations.dashboard.itemsReceived") }}
134
132
  </p>
135
133
  </div>
136
134
  </div>