proxy-nostr-relay 0.3.1

A Nostr proxy relay with advanced bot filtering and an admin UI.
Documentation
import { useState, useEffect } from 'react';
import { api } from '../api';
import { StatsChart } from './StatsChart';
import type { Stats } from '../types';

function formatReason(reason: string): string {
  const map: Record<string, string> = {
    'banned_npub': 'Banned Npub',
    'banned_ip': 'Banned IP',
    'kind_blacklist': 'Kind Blacklist',
    'bot_filter': 'Bot Filter',
    'not_in_safelist': 'Not in Safelist',
    'filter_rule': 'Filter Rule',
  };
  return map[reason] || reason;
}

function getValueClass(value: number, max: number): string {
  const ratio = value / max;
  if (ratio > 0.7) return 'high';
  if (ratio > 0.3) return 'medium';
  return 'low';
}

export function DashboardSection() {
  const [stats, setStats] = useState<Stats | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchStats = () => {
      api.getStats()
        .then(data => { setStats(data); setLoading(false); })
        .catch(() => setLoading(false));
    };
    fetchStats();
    const interval = setInterval(fetchStats, 10000);
    return () => clearInterval(interval);
  }, []);

  if (loading) return <div className="loading">Loading dashboard...</div>;
  if (!stats) return <div className="empty-state">Failed to load statistics</div>;

  const maxRejections = Math.max(...stats.top_npubs_by_rejections.map(r => r.count), 1);

  return (
    <>
      <div className="info-box" style={{ marginBottom: '1.5rem' }}>
        <h4>Proxy Nostr Relay</h4>
        <p>This relay sits between clients and backend relays. It enforces a safelist for posting (EVENT), filters incoming events (REQ responses) with Simple BAN rules and DSL filter rules, and provides relay health monitoring and metrics.</p>
      </div>
      <div className="stats-grid">
        <div className="stat-card">
          <h3>Total Connections</h3>
          <p className="stat-value">{stats.total_connections.toLocaleString()}</p>
        </div>
        <div className="stat-card">
          <h3>Active Sessions</h3>
          <p className="stat-value">{stats.active_connections.toLocaleString()}</p>
        </div>
        <div className="stat-card">
          <h3>Events Rejected</h3>
          <p className="stat-value">{stats.total_rejections.toLocaleString()}</p>
        </div>
      </div>

      <div className="stats-chart-wrapper" style={{ marginBottom: '1.5rem' }}>
        <StatsChart />
      </div>
      <div className="dashboard-grid">
        <div className="mini-panel">
          <div className="mini-panel-header">
            <span className="icon red"></span>
            Rejections by Reason
          </div>
          <div className="mini-list">
            {stats.rejections_by_reason.length === 0 ? (
              <div className="mini-list-item"><span className="label">No rejections yet</span></div>
            ) : (
              stats.rejections_by_reason.map(r => (
                <div className="mini-list-item" key={r.reason}>
                  <span className="label">{formatReason(r.reason)}</span>
                  <span className={`value ${getValueClass(r.count, maxRejections)}`}>{r.count}</span>
                </div>
              ))
            )}
          </div>
        </div>

        <div className="mini-panel">
          <div className="mini-panel-header">
            <span className="icon purple"></span>
            Top Rejected Npubs
          </div>
          <div className="mini-list">
            {stats.top_npubs_by_rejections.length === 0 ? (
              <div className="mini-list-item"><span className="label">No data</span></div>
            ) : (
              stats.top_npubs_by_rejections.slice(0, 8).map(r => (
                <div className="mini-list-item" key={r.npub}>
                  <span className="label">{r.npub.slice(0, 20)}...</span>
                  <span className={`value ${getValueClass(r.count, maxRejections)}`}>{r.count}</span>
                </div>
              ))
            )}
          </div>
        </div>

        <div className="mini-panel">
          <div className="mini-panel-header">
            <span className="icon blue"></span>
            Top Rejected IPs
          </div>
          <div className="mini-list">
            {stats.top_ips_by_rejections.length === 0 ? (
              <div className="mini-list-item"><span className="label">No data</span></div>
            ) : (
              stats.top_ips_by_rejections.slice(0, 8).map(r => (
                <div className="mini-list-item" key={r.ip_address}>
                  <span className="label">{r.ip_address}</span>
                  <span className={`value ${getValueClass(r.count, maxRejections)}`}>{r.count}</span>
                </div>
              ))
            )}
          </div>
        </div>
      </div>
    </>
  );
}