Add live network totals for nodes, users, published media, and posts, including local-node counts and media-count federation support.

Hop-State: A_06FPDCRHF0BSN8BDYYXW3ER
Hop-Proposal: R_06FPDCQJTBM7EZK7BXFA8D0
Hop-Task: T_06FPD59337GBJTP7D756FEG
Hop-Attempt: AT_06FPD59337493XRD47TM0H0
This commit is contained in:
2026-07-15 09:53:59 -07:00
committed by Hop
parent 9e01bff9e8
commit 6990568baf
12 changed files with 7358 additions and 17 deletions
+9 -2
View File
@@ -4,8 +4,8 @@
* Handles node discovery and announcement in the swarm network.
*/
import { db, users, posts } from '@/db';
import { sql } from 'drizzle-orm';
import { db, users, posts, media } from '@/db';
import { isNotNull, sql } from 'drizzle-orm';
import type { SwarmAnnouncement, SwarmNodeInfo, SwarmCapability } from './types';
import { getCurrentBuildInfo } from '@/lib/version';
import { upsertSwarmNode, getSeedNodes, markNodeSuccess, markNodeFailure } from './registry';
@@ -31,6 +31,7 @@ export async function buildAnnouncement(): Promise<SwarmAnnouncement> {
let publicKey = '';
let userCount = 0;
let postCount = 0;
let mediaCount = 0;
let isNsfw = false;
if (db) {
@@ -50,9 +51,13 @@ export async function buildAnnouncement(): Promise<SwarmAnnouncement> {
// Get counts
const userResult = await db.select({ count: sql<number>`count(*)` }).from(users);
const postResult = await db.select({ count: sql<number>`count(*)` }).from(posts);
const mediaResult = await db.select({ count: sql<number>`count(*)` })
.from(media)
.where(isNotNull(media.postId));
userCount = Number(userResult[0]?.count ?? 0);
postCount = Number(postResult[0]?.count ?? 0);
mediaCount = Number(mediaResult[0]?.count ?? 0);
}
const capabilities: SwarmCapability[] = ['handles', 'gossip', 'interactions', 'e2ee_dm_v1'];
@@ -68,6 +73,7 @@ export async function buildAnnouncement(): Promise<SwarmAnnouncement> {
: buildInfo.version,
userCount,
postCount,
mediaCount,
capabilities,
isNsfw,
timestamp: new Date().toISOString(),
@@ -212,6 +218,7 @@ export async function fetchNodeInfo(domain: string): Promise<SwarmNodeInfo | nul
softwareVersion: data.softwareVersion,
userCount: data.userCount,
postCount: data.postCount,
mediaCount: data.mediaCount,
capabilities: data.capabilities,
isNsfw: data.isNsfw,
};
+1
View File
@@ -47,6 +47,7 @@ export async function buildGossipPayload(since?: string): Promise<SwarmGossipPay
softwareVersion: announcement.softwareVersion,
userCount: announcement.userCount,
postCount: announcement.postCount,
mediaCount: announcement.mediaCount,
isNsfw: announcement.isNsfw,
capabilities: announcement.capabilities,
lastSeenAt: new Date().toISOString(),
+51 -11
View File
@@ -4,8 +4,8 @@
* Manages the local registry of known swarm nodes.
*/
import { db, swarmNodes, swarmSeeds, swarmSyncLog } from '@/db';
import { eq, desc, and, gt, lt, sql } from 'drizzle-orm';
import { db, media, posts, swarmNodes, swarmSeeds, swarmSyncLog, users } from '@/db';
import { eq, desc, and, gt, isNotNull, lt, sql } from 'drizzle-orm';
import type { SwarmNodeInfo, SwarmCapability, SwarmSyncResult } from './types';
import { SWARM_CONFIG, DEFAULT_SEED_NODES } from './types';
import {
@@ -14,6 +14,40 @@ import {
isPublicSwarmDomain,
} from './node-domain';
interface NetworkStatNode {
isActive: boolean;
userCount: number | null;
postCount: number | null;
mediaCount: number | null;
}
interface LocalNetworkStats {
users: number;
posts: number;
media: number;
}
export function aggregateSwarmStats(
publicNodes: NetworkStatNode[],
local: LocalNetworkStats,
includeLocalNode: boolean,
) {
const activeNodes = publicNodes.filter(node => node.isActive);
const localNodeOffset = includeLocalNode ? 1 : 0;
return {
totalNodes: publicNodes.length + localNodeOffset,
// Keep this as the active peer count; the scheduler uses zero to trigger seed recovery.
activeNodes: activeNodes.length,
totalUsers: activeNodes.reduce((sum, node) => sum + (node.userCount || 0), 0)
+ (includeLocalNode ? local.users : 0),
totalPosts: activeNodes.reduce((sum, node) => sum + (node.postCount || 0), 0)
+ (includeLocalNode ? local.posts : 0),
totalMedia: activeNodes.reduce((sum, node) => sum + (node.mediaCount || 0), 0)
+ (includeLocalNode ? local.media : 0),
};
}
/**
* Get or create a swarm node entry
*/
@@ -46,6 +80,7 @@ export async function upsertSwarmNode(
softwareVersion: node.softwareVersion,
userCount: node.userCount,
postCount: node.postCount,
mediaCount: node.mediaCount,
isNsfw: node.isNsfw ?? false,
discoveredVia,
capabilities,
@@ -64,6 +99,7 @@ export async function upsertSwarmNode(
softwareVersion: node.softwareVersion ?? existing.softwareVersion,
userCount: node.userCount ?? existing.userCount,
postCount: node.postCount ?? existing.postCount,
mediaCount: node.mediaCount ?? existing.mediaCount,
isNsfw: node.isNsfw ?? existing.isNsfw,
capabilities: capabilities ?? existing.capabilities,
lastSeenAt: new Date(),
@@ -317,22 +353,25 @@ export async function getSwarmStats() {
activeNodes: 0,
totalUsers: 0,
totalPosts: 0,
totalMedia: 0,
};
}
const allNodes = await db.query.swarmNodes.findMany();
const publicNodes = allNodes.filter(n => isPublicSwarmDomain(n.domain));
const activeNodes = publicNodes.filter(n => n.isActive);
const totalUsers = activeNodes.reduce((sum, n) => sum + (n.userCount || 0), 0);
const totalPosts = activeNodes.reduce((sum, n) => sum + (n.postCount || 0), 0);
const [localUsers, localPosts, localMedia] = await Promise.all([
db.select({ count: sql<number>`count(*)` }).from(users),
db.select({ count: sql<number>`count(*)` }).from(posts),
db.select({ count: sql<number>`count(*)` }).from(media).where(isNotNull(media.postId)),
]);
return {
totalNodes: publicNodes.length,
activeNodes: activeNodes.length,
totalUsers,
totalPosts,
};
const hasPublicLocalNode = isPublicSwarmDomain(process.env.NEXT_PUBLIC_NODE_DOMAIN);
return aggregateSwarmStats(publicNodes, {
users: Number(localUsers[0]?.count ?? 0),
posts: Number(localPosts[0]?.count ?? 0),
media: Number(localMedia[0]?.count ?? 0),
}, hasPublicLocalNode);
}
// Helper to convert DB node to SwarmNodeInfo
@@ -346,6 +385,7 @@ function nodeToInfo(node: typeof swarmNodes.$inferSelect): SwarmNodeInfo {
softwareVersion: node.softwareVersion ?? undefined,
userCount: node.userCount ?? undefined,
postCount: node.postCount ?? undefined,
mediaCount: node.mediaCount ?? undefined,
capabilities: node.capabilities ? JSON.parse(node.capabilities) : undefined,
isNsfw: node.isNsfw,
lastSeenAt: node.lastSeenAt.toISOString(),
+42
View File
@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest';
import { aggregateSwarmStats } from './registry';
describe('aggregateSwarmStats', () => {
it('includes the local node and totals active peer statistics', () => {
const result = aggregateSwarmStats([
{ isActive: true, userCount: 3, postCount: 24, mediaCount: 7 },
{ isActive: true, userCount: 2, postCount: 8, mediaCount: null },
{ isActive: false, userCount: 50, postCount: 500, mediaCount: 100 },
], {
users: 4,
posts: 12,
media: 5,
}, true);
expect(result).toEqual({
totalNodes: 4,
activeNodes: 2,
totalUsers: 9,
totalPosts: 44,
totalMedia: 12,
});
});
it('excludes local counts when the node is not publicly participating', () => {
const result = aggregateSwarmStats([
{ isActive: true, userCount: 3, postCount: 24, mediaCount: 7 },
], {
users: 100,
posts: 200,
media: 300,
}, false);
expect(result).toEqual({
totalNodes: 1,
activeNodes: 1,
totalUsers: 3,
totalPosts: 24,
totalMedia: 7,
});
});
});
+3
View File
@@ -13,6 +13,7 @@ export interface SwarmNodeInfo {
softwareVersion?: string;
userCount?: number;
postCount?: number;
mediaCount?: number;
capabilities?: SwarmCapability[];
isNsfw?: boolean;
lastSeenAt?: string;
@@ -29,6 +30,7 @@ export interface SwarmAnnouncement {
softwareVersion: string;
userCount: number;
postCount: number;
mediaCount: number;
capabilities: SwarmCapability[];
isNsfw: boolean;
timestamp: string;
@@ -91,6 +93,7 @@ export interface SwarmStats {
activeNodes: number;
totalUsers: number;
totalPosts: number;
totalMedia: number;
lastUpdated: string;
}