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:
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `swarm_nodes` ADD `media_count` integer;
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -32,10 +32,11 @@ const announcementSchema = z.object({
|
|||||||
softwareVersion: z.string().optional(),
|
softwareVersion: z.string().optional(),
|
||||||
userCount: z.number().optional(),
|
userCount: z.number().optional(),
|
||||||
postCount: z.number().optional(),
|
postCount: z.number().optional(),
|
||||||
|
mediaCount: z.number().optional(),
|
||||||
isNsfw: z.boolean().optional(),
|
isNsfw: z.boolean().optional(),
|
||||||
capabilities: z.array(z.enum(['handles', 'gossip', 'relay', 'search', 'interactions', 'e2ee_dm_v1'])).optional(),
|
capabilities: z.array(z.enum(['handles', 'gossip', 'relay', 'search', 'interactions', 'e2ee_dm_v1'])).optional(),
|
||||||
timestamp: z.string().optional(),
|
timestamp: z.string().optional(),
|
||||||
});
|
}).passthrough();
|
||||||
|
|
||||||
// Schema including signature for verification
|
// Schema including signature for verification
|
||||||
const signedAnnouncementSchema = announcementSchema.extend({
|
const signedAnnouncementSchema = announcementSchema.extend({
|
||||||
@@ -101,6 +102,7 @@ export async function POST(request: Request) {
|
|||||||
softwareVersion: data.softwareVersion,
|
softwareVersion: data.softwareVersion,
|
||||||
userCount: data.userCount,
|
userCount: data.userCount,
|
||||||
postCount: data.postCount,
|
postCount: data.postCount,
|
||||||
|
mediaCount: data.mediaCount,
|
||||||
isNsfw: data.isNsfw,
|
isNsfw: data.isNsfw,
|
||||||
capabilities: data.capabilities,
|
capabilities: data.capabilities,
|
||||||
lastSeenAt: new Date().toISOString(),
|
lastSeenAt: new Date().toISOString(),
|
||||||
@@ -122,6 +124,7 @@ export async function POST(request: Request) {
|
|||||||
softwareVersion: ourAnnouncement.softwareVersion,
|
softwareVersion: ourAnnouncement.softwareVersion,
|
||||||
userCount: ourAnnouncement.userCount,
|
userCount: ourAnnouncement.userCount,
|
||||||
postCount: ourAnnouncement.postCount,
|
postCount: ourAnnouncement.postCount,
|
||||||
|
mediaCount: ourAnnouncement.mediaCount,
|
||||||
isNsfw: ourAnnouncement.isNsfw,
|
isNsfw: ourAnnouncement.isNsfw,
|
||||||
capabilities: ourAnnouncement.capabilities,
|
capabilities: ourAnnouncement.capabilities,
|
||||||
lastSeenAt: new Date().toISOString(),
|
lastSeenAt: new Date().toISOString(),
|
||||||
|
|||||||
@@ -30,10 +30,11 @@ const nodeInfoSchema = z.object({
|
|||||||
softwareVersion: z.string().optional(),
|
softwareVersion: z.string().optional(),
|
||||||
userCount: z.number().optional(),
|
userCount: z.number().optional(),
|
||||||
postCount: z.number().optional(),
|
postCount: z.number().optional(),
|
||||||
|
mediaCount: z.number().optional(),
|
||||||
isNsfw: z.boolean().optional(),
|
isNsfw: z.boolean().optional(),
|
||||||
capabilities: z.array(z.enum(['handles', 'gossip', 'relay', 'search', 'interactions', 'e2ee_dm_v1'])).optional(),
|
capabilities: z.array(z.enum(['handles', 'gossip', 'relay', 'search', 'interactions', 'e2ee_dm_v1'])).optional(),
|
||||||
lastSeenAt: z.string().optional(),
|
lastSeenAt: z.string().optional(),
|
||||||
});
|
}).passthrough();
|
||||||
|
|
||||||
const gossipPayloadSchema = z.object({
|
const gossipPayloadSchema = z.object({
|
||||||
sender: z.string().min(1),
|
sender: z.string().min(1),
|
||||||
@@ -41,7 +42,7 @@ const gossipPayloadSchema = z.object({
|
|||||||
handles: z.array(handleSchema).optional(),
|
handles: z.array(handleSchema).optional(),
|
||||||
timestamp: z.string(),
|
timestamp: z.string(),
|
||||||
since: z.string().optional(),
|
since: z.string().optional(),
|
||||||
});
|
}).passthrough();
|
||||||
|
|
||||||
// Schema including signature for verification
|
// Schema including signature for verification
|
||||||
const signedGossipSchema = gossipPayloadSchema.extend({
|
const signedGossipSchema = gossipPayloadSchema.extend({
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export async function GET() {
|
|||||||
softwareVersion: announcement.softwareVersion,
|
softwareVersion: announcement.softwareVersion,
|
||||||
userCount: announcement.userCount,
|
userCount: announcement.userCount,
|
||||||
postCount: announcement.postCount,
|
postCount: announcement.postCount,
|
||||||
|
mediaCount: announcement.mediaCount,
|
||||||
isNsfw: announcement.isNsfw,
|
isNsfw: announcement.isNsfw,
|
||||||
capabilities: announcement.capabilities,
|
capabilities: announcement.capabilities,
|
||||||
lastSeenAt: new Date().toISOString(),
|
lastSeenAt: new Date().toISOString(),
|
||||||
|
|||||||
@@ -22,6 +22,17 @@ interface NodeInfo {
|
|||||||
isNsfw: boolean;
|
isNsfw: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NetworkStats {
|
||||||
|
totalNodes: number;
|
||||||
|
totalUsers: number;
|
||||||
|
totalMedia: number;
|
||||||
|
totalPosts: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNetworkTotal(value: number | undefined): string {
|
||||||
|
return typeof value === 'number' ? value.toLocaleString() : '—';
|
||||||
|
}
|
||||||
|
|
||||||
export function RightSidebar() {
|
export function RightSidebar() {
|
||||||
const fallbackDescription = process.env.NEXT_PUBLIC_NODE_DESCRIPTION || 'A swarm social network node.';
|
const fallbackDescription = process.env.NEXT_PUBLIC_NODE_DESCRIPTION || 'A swarm social network node.';
|
||||||
const [nodeInfo, setNodeInfo] = useState<NodeInfo>({
|
const [nodeInfo, setNodeInfo] = useState<NodeInfo>({
|
||||||
@@ -39,6 +50,7 @@ export function RightSidebar() {
|
|||||||
commitCount: number | null;
|
commitCount: number | null;
|
||||||
buildDate: string | null;
|
buildDate: string | null;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [networkStats, setNetworkStats] = useState<NetworkStats | null>(null);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
@@ -87,7 +99,18 @@ export function RightSidebar() {
|
|||||||
.then(data => setVersion(data))
|
.then(data => setVersion(data))
|
||||||
.catch(() => setVersion({ version: 'unknown', commit: null, commitCount: null, buildDate: null }));
|
.catch(() => setVersion({ version: 'unknown', commit: null, commitCount: null, buildDate: null }));
|
||||||
|
|
||||||
return () => window.removeEventListener('synapsis:node-updated', handleNodeUpdated);
|
const loadNetworkStats = () => fetch('/api/swarm/nodes?stats=true&limit=1', { cache: 'no-store' })
|
||||||
|
.then(res => res.ok ? res.json() : Promise.reject(new Error('Failed to load network stats')))
|
||||||
|
.then(data => setNetworkStats(data?.stats ?? null))
|
||||||
|
.catch(() => setNetworkStats(null));
|
||||||
|
|
||||||
|
void loadNetworkStats();
|
||||||
|
const networkStatsInterval = window.setInterval(loadNetworkStats, 60_000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('synapsis:node-updated', handleNodeUpdated);
|
||||||
|
window.clearInterval(networkStatsInterval);
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -165,6 +188,22 @@ export function RightSidebar() {
|
|||||||
: ''}
|
: ''}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '16px', paddingTop: '16px', borderTop: '1px solid var(--border-hover)', display: 'grid', gap: '9px' }}>
|
||||||
|
{[
|
||||||
|
['Total nodes', networkStats?.totalNodes],
|
||||||
|
['Total users', networkStats?.totalUsers],
|
||||||
|
['Total media', networkStats?.totalMedia],
|
||||||
|
['Total posts', networkStats?.totalPosts],
|
||||||
|
].map(([label, value]) => (
|
||||||
|
<div key={label} style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: '16px', fontSize: '13px' }}>
|
||||||
|
<span style={{ color: 'var(--foreground-secondary)' }}>{label}</span>
|
||||||
|
<strong style={{ color: 'var(--foreground)', fontVariantNumeric: 'tabular-nums' }}>
|
||||||
|
{formatNetworkTotal(value as number | undefined)}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
{nodeInfo.admins.length > 0 && (
|
{nodeInfo.admins.length > 0 && (
|
||||||
<div style={{ marginTop: '16px', paddingTop: '16px', borderTop: '1px solid var(--border-hover)' }}>
|
<div style={{ marginTop: '16px', paddingTop: '16px', borderTop: '1px solid var(--border-hover)' }}>
|
||||||
<h4 style={{ fontSize: '11px', fontWeight: 700, textTransform: 'uppercase', color: 'var(--foreground-tertiary)', marginBottom: '12px', letterSpacing: '0.05em' }}>
|
<h4 style={{ fontSize: '11px', fontWeight: 700, textTransform: 'uppercase', color: 'var(--foreground-tertiary)', marginBottom: '12px', letterSpacing: '0.05em' }}>
|
||||||
|
|||||||
@@ -699,6 +699,7 @@ export const swarmNodes = sqliteTable('swarm_nodes', {
|
|||||||
// Stats (updated periodically)
|
// Stats (updated periodically)
|
||||||
userCount: integer('user_count'),
|
userCount: integer('user_count'),
|
||||||
postCount: integer('post_count'),
|
postCount: integer('post_count'),
|
||||||
|
mediaCount: integer('media_count'),
|
||||||
|
|
||||||
// NSFW flag (synced from remote node)
|
// NSFW flag (synced from remote node)
|
||||||
isNsfw: integer('is_nsfw', { mode: 'boolean' }).default(false).notNull(),
|
isNsfw: integer('is_nsfw', { mode: 'boolean' }).default(false).notNull(),
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
* Handles node discovery and announcement in the swarm network.
|
* Handles node discovery and announcement in the swarm network.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { db, users, posts } from '@/db';
|
import { db, users, posts, media } from '@/db';
|
||||||
import { sql } from 'drizzle-orm';
|
import { isNotNull, sql } from 'drizzle-orm';
|
||||||
import type { SwarmAnnouncement, SwarmNodeInfo, SwarmCapability } from './types';
|
import type { SwarmAnnouncement, SwarmNodeInfo, SwarmCapability } from './types';
|
||||||
import { getCurrentBuildInfo } from '@/lib/version';
|
import { getCurrentBuildInfo } from '@/lib/version';
|
||||||
import { upsertSwarmNode, getSeedNodes, markNodeSuccess, markNodeFailure } from './registry';
|
import { upsertSwarmNode, getSeedNodes, markNodeSuccess, markNodeFailure } from './registry';
|
||||||
@@ -31,6 +31,7 @@ export async function buildAnnouncement(): Promise<SwarmAnnouncement> {
|
|||||||
let publicKey = '';
|
let publicKey = '';
|
||||||
let userCount = 0;
|
let userCount = 0;
|
||||||
let postCount = 0;
|
let postCount = 0;
|
||||||
|
let mediaCount = 0;
|
||||||
let isNsfw = false;
|
let isNsfw = false;
|
||||||
|
|
||||||
if (db) {
|
if (db) {
|
||||||
@@ -50,9 +51,13 @@ export async function buildAnnouncement(): Promise<SwarmAnnouncement> {
|
|||||||
// Get counts
|
// Get counts
|
||||||
const userResult = await db.select({ count: sql<number>`count(*)` }).from(users);
|
const userResult = await db.select({ count: sql<number>`count(*)` }).from(users);
|
||||||
const postResult = await db.select({ count: sql<number>`count(*)` }).from(posts);
|
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);
|
userCount = Number(userResult[0]?.count ?? 0);
|
||||||
postCount = Number(postResult[0]?.count ?? 0);
|
postCount = Number(postResult[0]?.count ?? 0);
|
||||||
|
mediaCount = Number(mediaResult[0]?.count ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const capabilities: SwarmCapability[] = ['handles', 'gossip', 'interactions', 'e2ee_dm_v1'];
|
const capabilities: SwarmCapability[] = ['handles', 'gossip', 'interactions', 'e2ee_dm_v1'];
|
||||||
@@ -68,6 +73,7 @@ export async function buildAnnouncement(): Promise<SwarmAnnouncement> {
|
|||||||
: buildInfo.version,
|
: buildInfo.version,
|
||||||
userCount,
|
userCount,
|
||||||
postCount,
|
postCount,
|
||||||
|
mediaCount,
|
||||||
capabilities,
|
capabilities,
|
||||||
isNsfw,
|
isNsfw,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
@@ -212,6 +218,7 @@ export async function fetchNodeInfo(domain: string): Promise<SwarmNodeInfo | nul
|
|||||||
softwareVersion: data.softwareVersion,
|
softwareVersion: data.softwareVersion,
|
||||||
userCount: data.userCount,
|
userCount: data.userCount,
|
||||||
postCount: data.postCount,
|
postCount: data.postCount,
|
||||||
|
mediaCount: data.mediaCount,
|
||||||
capabilities: data.capabilities,
|
capabilities: data.capabilities,
|
||||||
isNsfw: data.isNsfw,
|
isNsfw: data.isNsfw,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export async function buildGossipPayload(since?: string): Promise<SwarmGossipPay
|
|||||||
softwareVersion: announcement.softwareVersion,
|
softwareVersion: announcement.softwareVersion,
|
||||||
userCount: announcement.userCount,
|
userCount: announcement.userCount,
|
||||||
postCount: announcement.postCount,
|
postCount: announcement.postCount,
|
||||||
|
mediaCount: announcement.mediaCount,
|
||||||
isNsfw: announcement.isNsfw,
|
isNsfw: announcement.isNsfw,
|
||||||
capabilities: announcement.capabilities,
|
capabilities: announcement.capabilities,
|
||||||
lastSeenAt: new Date().toISOString(),
|
lastSeenAt: new Date().toISOString(),
|
||||||
|
|||||||
+51
-11
@@ -4,8 +4,8 @@
|
|||||||
* Manages the local registry of known swarm nodes.
|
* Manages the local registry of known swarm nodes.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { db, swarmNodes, swarmSeeds, swarmSyncLog } from '@/db';
|
import { db, media, posts, swarmNodes, swarmSeeds, swarmSyncLog, users } from '@/db';
|
||||||
import { eq, desc, and, gt, lt, sql } from 'drizzle-orm';
|
import { eq, desc, and, gt, isNotNull, lt, sql } from 'drizzle-orm';
|
||||||
import type { SwarmNodeInfo, SwarmCapability, SwarmSyncResult } from './types';
|
import type { SwarmNodeInfo, SwarmCapability, SwarmSyncResult } from './types';
|
||||||
import { SWARM_CONFIG, DEFAULT_SEED_NODES } from './types';
|
import { SWARM_CONFIG, DEFAULT_SEED_NODES } from './types';
|
||||||
import {
|
import {
|
||||||
@@ -14,6 +14,40 @@ import {
|
|||||||
isPublicSwarmDomain,
|
isPublicSwarmDomain,
|
||||||
} from './node-domain';
|
} 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
|
* Get or create a swarm node entry
|
||||||
*/
|
*/
|
||||||
@@ -46,6 +80,7 @@ export async function upsertSwarmNode(
|
|||||||
softwareVersion: node.softwareVersion,
|
softwareVersion: node.softwareVersion,
|
||||||
userCount: node.userCount,
|
userCount: node.userCount,
|
||||||
postCount: node.postCount,
|
postCount: node.postCount,
|
||||||
|
mediaCount: node.mediaCount,
|
||||||
isNsfw: node.isNsfw ?? false,
|
isNsfw: node.isNsfw ?? false,
|
||||||
discoveredVia,
|
discoveredVia,
|
||||||
capabilities,
|
capabilities,
|
||||||
@@ -64,6 +99,7 @@ export async function upsertSwarmNode(
|
|||||||
softwareVersion: node.softwareVersion ?? existing.softwareVersion,
|
softwareVersion: node.softwareVersion ?? existing.softwareVersion,
|
||||||
userCount: node.userCount ?? existing.userCount,
|
userCount: node.userCount ?? existing.userCount,
|
||||||
postCount: node.postCount ?? existing.postCount,
|
postCount: node.postCount ?? existing.postCount,
|
||||||
|
mediaCount: node.mediaCount ?? existing.mediaCount,
|
||||||
isNsfw: node.isNsfw ?? existing.isNsfw,
|
isNsfw: node.isNsfw ?? existing.isNsfw,
|
||||||
capabilities: capabilities ?? existing.capabilities,
|
capabilities: capabilities ?? existing.capabilities,
|
||||||
lastSeenAt: new Date(),
|
lastSeenAt: new Date(),
|
||||||
@@ -317,22 +353,25 @@ export async function getSwarmStats() {
|
|||||||
activeNodes: 0,
|
activeNodes: 0,
|
||||||
totalUsers: 0,
|
totalUsers: 0,
|
||||||
totalPosts: 0,
|
totalPosts: 0,
|
||||||
|
totalMedia: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const allNodes = await db.query.swarmNodes.findMany();
|
const allNodes = await db.query.swarmNodes.findMany();
|
||||||
const publicNodes = allNodes.filter(n => isPublicSwarmDomain(n.domain));
|
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 [localUsers, localPosts, localMedia] = await Promise.all([
|
||||||
const totalPosts = activeNodes.reduce((sum, n) => sum + (n.postCount || 0), 0);
|
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 {
|
const hasPublicLocalNode = isPublicSwarmDomain(process.env.NEXT_PUBLIC_NODE_DOMAIN);
|
||||||
totalNodes: publicNodes.length,
|
return aggregateSwarmStats(publicNodes, {
|
||||||
activeNodes: activeNodes.length,
|
users: Number(localUsers[0]?.count ?? 0),
|
||||||
totalUsers,
|
posts: Number(localPosts[0]?.count ?? 0),
|
||||||
totalPosts,
|
media: Number(localMedia[0]?.count ?? 0),
|
||||||
};
|
}, hasPublicLocalNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to convert DB node to SwarmNodeInfo
|
// Helper to convert DB node to SwarmNodeInfo
|
||||||
@@ -346,6 +385,7 @@ function nodeToInfo(node: typeof swarmNodes.$inferSelect): SwarmNodeInfo {
|
|||||||
softwareVersion: node.softwareVersion ?? undefined,
|
softwareVersion: node.softwareVersion ?? undefined,
|
||||||
userCount: node.userCount ?? undefined,
|
userCount: node.userCount ?? undefined,
|
||||||
postCount: node.postCount ?? undefined,
|
postCount: node.postCount ?? undefined,
|
||||||
|
mediaCount: node.mediaCount ?? undefined,
|
||||||
capabilities: node.capabilities ? JSON.parse(node.capabilities) : undefined,
|
capabilities: node.capabilities ? JSON.parse(node.capabilities) : undefined,
|
||||||
isNsfw: node.isNsfw,
|
isNsfw: node.isNsfw,
|
||||||
lastSeenAt: node.lastSeenAt.toISOString(),
|
lastSeenAt: node.lastSeenAt.toISOString(),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,6 +13,7 @@ export interface SwarmNodeInfo {
|
|||||||
softwareVersion?: string;
|
softwareVersion?: string;
|
||||||
userCount?: number;
|
userCount?: number;
|
||||||
postCount?: number;
|
postCount?: number;
|
||||||
|
mediaCount?: number;
|
||||||
capabilities?: SwarmCapability[];
|
capabilities?: SwarmCapability[];
|
||||||
isNsfw?: boolean;
|
isNsfw?: boolean;
|
||||||
lastSeenAt?: string;
|
lastSeenAt?: string;
|
||||||
@@ -29,6 +30,7 @@ export interface SwarmAnnouncement {
|
|||||||
softwareVersion: string;
|
softwareVersion: string;
|
||||||
userCount: number;
|
userCount: number;
|
||||||
postCount: number;
|
postCount: number;
|
||||||
|
mediaCount: number;
|
||||||
capabilities: SwarmCapability[];
|
capabilities: SwarmCapability[];
|
||||||
isNsfw: boolean;
|
isNsfw: boolean;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
@@ -91,6 +93,7 @@ export interface SwarmStats {
|
|||||||
activeNodes: number;
|
activeNodes: number;
|
||||||
totalUsers: number;
|
totalUsers: number;
|
||||||
totalPosts: number;
|
totalPosts: number;
|
||||||
|
totalMedia: number;
|
||||||
lastUpdated: string;
|
lastUpdated: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user