Some checks are pending
CI Pipeline / Run Tests (push) Waiting to run
CI Pipeline / Lint Code (push) Waiting to run
CI Pipeline / Security Scan (push) Waiting to run
CI Pipeline / Build Docker Images (push) Blocked by required conditions
CI Pipeline / E2E Tests (push) Blocked by required conditions
1762 lines
88 KiB
HTML
1762 lines
88 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>BitTorrent Gateway</title>
|
|
<link rel="stylesheet" href="/static/style.css">
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<header>
|
|
<h1 id="site-title">⚡ BitTorrent Gateway</h1>
|
|
<nav>
|
|
<a href="#about" onclick="showAbout(); return false;">About</a>
|
|
<a href="#services" onclick="showServices(); return false;">Server Stats</a>
|
|
<a href="#upload" onclick="showUpload(); return false;" id="upload-link" style="display: none;">Upload</a>
|
|
<a href="#files" onclick="showFiles(); return false;" id="files-link" style="display: none;">My Files</a>
|
|
<a href="/admin" id="admin-link" style="display: none;">Admin</a>
|
|
<div id="auth-status" class="auth-status">
|
|
<button id="login-btn" onclick="showLogin()">Login</button>
|
|
<div id="user-info" style="display: none;">
|
|
<span id="user-pubkey-short"></span>
|
|
<button id="logout-btn" onclick="logout()">Logout</button>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
</header>
|
|
|
|
<main>
|
|
<!-- Upload Section -->
|
|
<div id="upload-section" class="section">
|
|
<div class="upload-area" id="upload-area">
|
|
<div class="upload-icon">📁</div>
|
|
<h3>Drag & drop files here</h3>
|
|
<p>or click to select files</p>
|
|
<input type="file" id="file-input" multiple>
|
|
</div>
|
|
|
|
<div id="upload-progress" class="upload-progress hidden">
|
|
<div class="progress-header">
|
|
<h4 id="upload-filename">Uploading...</h4>
|
|
<span id="upload-cancel" onclick="cancelUpload()">✖</span>
|
|
</div>
|
|
<div class="progress-bar">
|
|
<div id="progress-fill" class="progress-fill"></div>
|
|
</div>
|
|
<div class="progress-info">
|
|
<span id="progress-percent">0%</span>
|
|
<span id="progress-speed">0 MB/s</span>
|
|
<span id="progress-eta">--:--</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="upload-options">
|
|
<label>
|
|
<input type="checkbox" id="announce-dht" checked>
|
|
Announce to DHT
|
|
</label>
|
|
<label>
|
|
<input type="checkbox" id="store-blossom" checked>
|
|
Store in Blossom
|
|
</label>
|
|
</div>
|
|
|
|
<div id="recent-uploads" class="recent-uploads">
|
|
<h3>Recent Uploads</h3>
|
|
<div id="uploads-list" class="uploads-list">
|
|
<p class="empty-state">No recent uploads</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Services Section -->
|
|
<div id="services-section" class="section">
|
|
<h2>Server Stats</h2>
|
|
|
|
<div class="service-grid">
|
|
<div class="service-card">
|
|
<div class="service-header">
|
|
<h3>🚀 Gateway API</h3>
|
|
<span id="gateway-status" class="status-indicator">⚪</span>
|
|
</div>
|
|
<div class="service-info">
|
|
<p><strong>Total Files:</strong> <span id="gateway-uploads">0</span></p>
|
|
<p><strong>Storage Used:</strong> <span id="gateway-storage">0 MB</span></p>
|
|
<p><strong>Downloads Served:</strong> <span id="gateway-downloads">0</span></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="service-card">
|
|
<div class="service-header">
|
|
<h3>🌸 Blossom Server</h3>
|
|
<span id="blossom-status" class="status-indicator">⚪</span>
|
|
</div>
|
|
<div class="service-info">
|
|
<p><strong>Blobs Stored:</strong> <span id="blossom-blobs">0</span></p>
|
|
<p><strong>Blob Storage:</strong> <span id="blossom-storage">0 MB</span></p>
|
|
<p><strong>Requests Today:</strong> <span id="blossom-requests">0</span></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="service-card">
|
|
<div class="service-header">
|
|
<h3>🕸️ DHT Node</h3>
|
|
<span id="dht-status" class="status-indicator">⚪</span>
|
|
</div>
|
|
<div class="service-info">
|
|
<p><strong>Network Peers:</strong> <span id="dht-peers">0</span></p>
|
|
<p><strong>Torrents Seeding:</strong> <span id="dht-torrents">0</span></p>
|
|
<p><strong>Announces Today:</strong> <span id="dht-announces">0</span></p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="service-card">
|
|
<div class="service-header">
|
|
<h3>📡 BitTorrent Tracker</h3>
|
|
<span id="tracker-status" class="status-indicator">⚪</span>
|
|
</div>
|
|
<div class="service-info">
|
|
<p><strong>Active Torrents:</strong> <span id="tracker-torrents">0</span></p>
|
|
<p><strong>Connected Peers:</strong> <span id="tracker-peers">0</span></p>
|
|
<p><strong>Seeders/Leechers:</strong> <span id="tracker-seeders">0</span>/<span id="tracker-leechers">0</span></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="system-info">
|
|
<h3>System Information</h3>
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<label>Mode:</label>
|
|
<span id="system-mode">unified</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<label>Uptime:</label>
|
|
<span id="system-uptime">--</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<label>Total Storage:</label>
|
|
<span id="system-storage">0 MB</span>
|
|
</div>
|
|
<div class="info-item">
|
|
<label>Active Connections:</label>
|
|
<span id="system-connections">0</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- User Files Section -->
|
|
<div id="files-section" class="section">
|
|
<div class="dashboard-header">
|
|
<h2>My Files Dashboard</h2>
|
|
<div class="dashboard-controls">
|
|
<div class="view-toggle">
|
|
<button id="grid-view-btn" class="view-btn active" onclick="setViewMode('grid')">⊞</button>
|
|
<button id="list-view-btn" class="view-btn" onclick="setViewMode('list')">☰</button>
|
|
</div>
|
|
<button id="refresh-files" class="action-btn">↻ Refresh</button>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div id="user-stats" class="user-stats">
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="user-file-count">0</div>
|
|
<div class="stat-label">Files</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="user-storage-used">0 MB</div>
|
|
<div class="stat-label">Storage Used</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-placeholder">
|
|
<div class="stat-number">Coming Soon</div>
|
|
<div class="stat-note">Account limits & quotas</div>
|
|
</div>
|
|
<div class="stat-label">Storage Limit</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number" id="user-last-login">Never</div>
|
|
<div class="stat-label">Last Login</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="file-filters">
|
|
<button class="filter-btn active" onclick="filterFiles('all')">All Files</button>
|
|
<button class="filter-btn" onclick="filterFiles('blob')">Blobs</button>
|
|
<button class="filter-btn" onclick="filterFiles('torrent')">Torrents</button>
|
|
<button class="filter-btn" onclick="filterFiles('private')">Private</button>
|
|
</div>
|
|
|
|
<div id="file-container" class="file-container">
|
|
<div id="file-list" class="file-grid">
|
|
<p class="empty-state">No files uploaded yet.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="loading-files" class="loading-state" style="display: none;">
|
|
<div class="spinner"></div>
|
|
<p>Loading files...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- About Section -->
|
|
<div id="about-section" class="section active">
|
|
<div class="about-header">
|
|
<h2>⚡ BitTorrent Gateway</h2>
|
|
<p class="about-subtitle">Decentralized Content Distribution with Nostr Integration</p>
|
|
</div>
|
|
|
|
<div class="about-content">
|
|
<div class="intro-section">
|
|
<h3>What This Platform Does</h3>
|
|
<p>The BitTorrent Gateway is a next-generation content distribution system that combines the reliability of traditional web hosting with the power of peer-to-peer networks, automatic video transcoding, and decentralized social discovery through Nostr. It automatically chooses the best way to store, process, and distribute your files:</p>
|
|
|
|
<div class="storage-flow">
|
|
<div class="flow-item">
|
|
<div class="flow-icon">📄</div>
|
|
<div class="flow-content">
|
|
<strong>Small Files (<100MB)</strong>
|
|
<p>Stored as blobs for instant access and immediate availability</p>
|
|
</div>
|
|
</div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-item">
|
|
<div class="flow-icon">🧩</div>
|
|
<div class="flow-content">
|
|
<strong>Large Files (≥100MB)</strong>
|
|
<p>Automatically chunked into 2MB pieces with torrent + DHT distribution</p>
|
|
</div>
|
|
</div>
|
|
<div class="flow-arrow">→</div>
|
|
<div class="flow-item">
|
|
<div class="flow-icon">🎬</div>
|
|
<div class="flow-content">
|
|
<strong>Video Processing</strong>
|
|
<p>Automatic H.264/AAC transcoding for universal web compatibility</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="key-benefits">
|
|
<h4>Why This Approach Works</h4>
|
|
<div class="benefit-list">
|
|
<div class="benefit-item">
|
|
<span class="benefit-icon">🚀</span>
|
|
<div class="benefit-content">
|
|
<strong>Always Available:</strong> Files are instantly accessible via WebSeed, with P2P peers providing additional bandwidth
|
|
</div>
|
|
</div>
|
|
<div class="benefit-item">
|
|
<span class="benefit-icon">🌐</span>
|
|
<div class="benefit-content">
|
|
<strong>Scales Automatically:</strong> Popular files get more peers, reducing server load naturally
|
|
</div>
|
|
</div>
|
|
<div class="benefit-item">
|
|
<span class="benefit-icon">🔒</span>
|
|
<div class="benefit-content">
|
|
<strong>Censorship Resistant:</strong> Content announced on Nostr can't be taken down by any single party
|
|
</div>
|
|
</div>
|
|
<div class="benefit-item">
|
|
<span class="benefit-icon">💰</span>
|
|
<div class="benefit-content">
|
|
<strong>Cost Effective:</strong> P2P distribution reduces bandwidth costs for large files
|
|
</div>
|
|
</div>
|
|
<div class="benefit-item">
|
|
<span class="benefit-icon">🎬</span>
|
|
<div class="benefit-content">
|
|
<strong>Universal Video Playback:</strong> Automatic transcoding ensures videos play in any browser
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="nostr-integration">
|
|
<h3>🌟 How Nostr Integration Works</h3>
|
|
<p class="nostr-intro">Nostr (Notes and Other Stuff Transmitted by Relays) is a decentralized protocol that enables censorship-resistant social networking. Here's how we integrate it for content distribution:</p>
|
|
|
|
<div class="nostr-flow">
|
|
<div class="nostr-step">
|
|
<div class="step-number">1</div>
|
|
<div class="step-content">
|
|
<h5>Content Upload</h5>
|
|
<p>You upload a file using your Nostr identity (no email or passwords needed)</p>
|
|
</div>
|
|
</div>
|
|
<div class="flow-arrow">↓</div>
|
|
<div class="nostr-step">
|
|
<div class="step-number">2</div>
|
|
<div class="step-content">
|
|
<h5>Automatic Processing</h5>
|
|
<p>System creates torrents, transcodes videos to web formats, generates magnet links, and sets up WebSeed URLs</p>
|
|
</div>
|
|
</div>
|
|
<div class="flow-arrow">↓</div>
|
|
<div class="nostr-step">
|
|
<div class="step-number">3</div>
|
|
<div class="step-content">
|
|
<h5>Nostr Announcement</h5>
|
|
<p>File metadata is broadcast to Nostr relays as a structured event</p>
|
|
</div>
|
|
</div>
|
|
<div class="flow-arrow">↓</div>
|
|
<div class="nostr-step">
|
|
<div class="step-number">4</div>
|
|
<div class="step-content">
|
|
<h5>Social Discovery</h5>
|
|
<p>Other users can discover, comment on, and share your content through Nostr clients</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="nostr-benefits">
|
|
<h4>Benefits of Nostr Integration:</h4>
|
|
<ul>
|
|
<li><strong>No Central Control:</strong> Content discovery happens through the distributed Nostr relay network</li>
|
|
<li><strong>Social Features:</strong> Users can comment, react, and share content using any Nostr client</li>
|
|
<li><strong>Network Effects:</strong> Content spreads naturally through social connections</li>
|
|
<li><strong>Privacy:</strong> You control your identity and data through cryptographic keys</li>
|
|
<li><strong>Interoperability:</strong> Works with existing Nostr apps and clients</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="nostr-example">
|
|
<h4>Example Nostr Event:</h4>
|
|
<div class="code-block">
|
|
<pre>{
|
|
"kind": 2003,
|
|
"content": "New torrent: example-video.mp4",
|
|
"tags": [
|
|
["title", "Example Video"],
|
|
["x", "d1c5a0f85a4e4f3d8e7d8f9c6b5a4e3f2d1c0b9a8e7d6f5c4b3a2e1d0c9b8a7f6"],
|
|
["file", "example-video.mp4", "157286400"],
|
|
["magnet", "magnet:?xt=urn:btih:..."],
|
|
["webseed", "https://gateway.example.com/api/webseed/..."],
|
|
["blossom", "sha256_blob_hash"],
|
|
["stream", "https://gateway.example.com/api/stream/hash"],
|
|
["hls", "https://gateway.example.com/api/stream/hash/playlist.m3u8"],
|
|
["duration", "3600"],
|
|
["video", "1920x1080", "30fps", "h264"],
|
|
["m", "video/mp4"],
|
|
["t", "torrent"],
|
|
["t", "video"],
|
|
["t", "streaming"],
|
|
["tcat", "video,streaming"]
|
|
]
|
|
}</pre>
|
|
</div>
|
|
<p class="code-explanation">This event gets distributed across Nostr relays, allowing anyone subscribed to file announcements to discover new content.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="architecture-section">
|
|
<h3>How It Works</h3>
|
|
|
|
<div class="arch-overview">
|
|
<h4>📡 P2P Coordination</h4>
|
|
<p>The system features a sophisticated <strong>P2P coordinator</strong> that manages all networking components, providing unified peer discovery, smart peer ranking, load balancing, and health monitoring across tracker, DHT, and WebSeed sources.</p>
|
|
|
|
<h4>🎯 Built-in BitTorrent Tracker</h4>
|
|
<div class="tech-details">
|
|
<p>Includes a full BitTorrent tracker with advanced features:</p>
|
|
<ul>
|
|
<li><strong>Client Compatibility:</strong> Optimized for qBittorrent, Transmission, WebTorrent, Deluge, uTorrent</li>
|
|
<li><strong>Abuse Prevention:</strong> Advanced detection and rate limiting systems</li>
|
|
<li><strong>Geographic Optimization:</strong> Proximity-based peer selection for faster transfers</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<h4>🔄 WebSeed Implementation (BEP-19)</h4>
|
|
<div class="tech-details">
|
|
<p>Advanced WebSeed features for guaranteed availability:</p>
|
|
<ul>
|
|
<li><strong>LRU Caching:</strong> Intelligent piece caching with configurable size limits</li>
|
|
<li><strong>Concurrent Optimization:</strong> Prevents duplicate loads, manages request limits</li>
|
|
<li><strong>Standards Compliant:</strong> Full BEP-19 specification support</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<h4>🌊 File Processing Pipeline</h4>
|
|
<div class="pipeline-steps">
|
|
<div class="step">
|
|
<span class="step-number">1</span>
|
|
<div class="step-content">
|
|
<strong>Upload & Analysis</strong>
|
|
<p>File received, SHA-256 hash calculated, size analysis determines storage strategy</p>
|
|
</div>
|
|
</div>
|
|
<div class="step">
|
|
<span class="step-number">2</span>
|
|
<div class="step-content">
|
|
<strong>Storage & Processing</strong>
|
|
<p>Small files stored as blobs, large files chunked, videos queued for transcoding</p>
|
|
</div>
|
|
</div>
|
|
<div class="step">
|
|
<span class="step-number">3</span>
|
|
<div class="step-content">
|
|
<strong>P2P Announcement</strong>
|
|
<p>Content announced to DHT network and Nostr relays for discovery</p>
|
|
</div>
|
|
</div>
|
|
<div class="step">
|
|
<span class="step-number">4</span>
|
|
<div class="step-content">
|
|
<strong>Immediate Availability</strong>
|
|
<p>Content immediately available via WebSeed, P2P peers join over time</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="features-section">
|
|
<h3>Key Features</h3>
|
|
<div class="feature-grid">
|
|
<div class="feature-card">
|
|
<div class="feature-icon">🚀</div>
|
|
<div class="feature-content">
|
|
<h4>Intelligent Content Distribution</h4>
|
|
<p>Automatic selection between blob storage and BitTorrent based on file size, with WebSeed fallback ensuring 100% availability</p>
|
|
</div>
|
|
</div>
|
|
<div class="feature-card">
|
|
<div class="feature-icon">🌐</div>
|
|
<div class="feature-content">
|
|
<h4>Multi-Protocol Support</h4>
|
|
<p>Full BitTorrent protocol, WebSeed (BEP-19), DHT, and Nostr integration for comprehensive P2P networking</p>
|
|
</div>
|
|
</div>
|
|
<div class="feature-card">
|
|
<div class="feature-icon">🔒</div>
|
|
<div class="feature-content">
|
|
<h4>Production-Ready Security</h4>
|
|
<p>Multi-layer rate limiting, advanced abuse detection, input sanitization, and comprehensive security headers</p>
|
|
</div>
|
|
</div>
|
|
<div class="feature-card">
|
|
<div class="feature-icon">📊</div>
|
|
<div class="feature-content">
|
|
<h4>Comprehensive Monitoring</h4>
|
|
<p>Real-time P2P health monitoring with 0-100 scoring system, performance metrics, and automatic alerting</p>
|
|
</div>
|
|
</div>
|
|
<div class="feature-card">
|
|
<div class="feature-icon">🎯</div>
|
|
<div class="feature-content">
|
|
<h4>BitTorrent Client Compatibility</h4>
|
|
<p>Optimized for popular clients with client-specific adjustments and proper announce intervals</p>
|
|
</div>
|
|
</div>
|
|
<div class="feature-card">
|
|
<div class="feature-icon">⚡</div>
|
|
<div class="feature-content">
|
|
<h4>Smart Caching & Optimization</h4>
|
|
<p>LRU caching system, concurrent processing, geographic peer selection, and connection pooling</p>
|
|
</div>
|
|
</div>
|
|
<div class="feature-card">
|
|
<div class="feature-icon">🎬</div>
|
|
<div class="feature-content">
|
|
<h4>Automatic Video Transcoding</h4>
|
|
<p>Background H.264/AAC conversion with priority queuing, progress tracking, and smart serving</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="services-section">
|
|
<h3>System Architecture</h3>
|
|
<div class="component-list">
|
|
<div class="component">
|
|
<h4>🚀 Gateway Server (Port 9877)</h4>
|
|
<p>Main API server with WebSeed implementation, smart proxy for chunked content reassembly, advanced LRU caching system, video transcoding engine, and comprehensive security middleware.</p>
|
|
<div class="component-specs">
|
|
<span>WebSeed (BEP-19) • Video Transcoding • Rate Limiting • Abuse Prevention</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="component">
|
|
<h4>🌸 Blossom Server (Port 8082)</h4>
|
|
<p>Content-addressed blob storage with Nostr protocol compatibility, SHA-256 addressing, and direct storage for files under 100MB.</p>
|
|
<div class="component-specs">
|
|
<span>Nostr Compatible • Content Addressing • Efficient Blob Storage</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="component">
|
|
<h4>🕸️ DHT Node (Port 6883)</h4>
|
|
<p>Full Kademlia DHT implementation with bootstrap connectivity to major networks, automatic torrent announcement, and peer discovery.</p>
|
|
<div class="component-specs">
|
|
<span>Distributed Peer Discovery • Bootstrap Networks • Announce</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="component">
|
|
<h4>📡 Built-in BitTorrent Tracker</h4>
|
|
<p>Full announce/scrape protocol support, client compatibility optimizations, abuse detection, and peer management with geographic proximity selection.</p>
|
|
<div class="component-specs">
|
|
<span>Multi-Client Support • Abuse Detection • Smart Peer Selection</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="component">
|
|
<h4>🎯 P2P Coordinator</h4>
|
|
<p>Unified management of all P2P components, smart peer ranking algorithm, load balancing across sources, and comprehensive health monitoring.</p>
|
|
<div class="component-specs">
|
|
<span>Unified Discovery • Smart Ranking • Health Monitoring</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="api-section">
|
|
<h3>API Reference</h3>
|
|
<div class="api-tabs">
|
|
<button class="api-tab active" onclick="showApiTab('core')">Core API</button>
|
|
<button class="api-tab" onclick="showApiTab('streaming')">Streaming</button>
|
|
<button class="api-tab" onclick="showApiTab('blossom')">Blossom</button>
|
|
<button class="api-tab" onclick="showApiTab('tracker')">Tracker</button>
|
|
</div>
|
|
|
|
<div id="api-core" class="api-content active">
|
|
<h4>Core API Endpoints</h4>
|
|
<div class="endpoint-list">
|
|
<div class="endpoint">
|
|
<span class="method post">POST</span>
|
|
<code>/api/upload</code>
|
|
<span class="desc">Upload files with intelligent storage routing</span>
|
|
</div>
|
|
<div class="endpoint">
|
|
<span class="method get">GET</span>
|
|
<code>/api/download/{hash}</code>
|
|
<span class="desc">Download files by hash (range requests supported)</span>
|
|
</div>
|
|
<div class="endpoint">
|
|
<span class="method get">GET</span>
|
|
<code>/api/torrent/{hash}</code>
|
|
<span class="desc">Get .torrent file for BitTorrent clients</span>
|
|
</div>
|
|
<div class="endpoint">
|
|
<span class="method get">GET</span>
|
|
<code>/api/stats</code>
|
|
<span class="desc">Overall system statistics</span>
|
|
</div>
|
|
<div class="endpoint">
|
|
<span class="method get">GET</span>
|
|
<code>/api/p2p/stats</code>
|
|
<span class="desc">Detailed P2P statistics</span>
|
|
</div>
|
|
<div class="endpoint">
|
|
<span class="method get">GET</span>
|
|
<code>/api/health</code>
|
|
<span class="desc">Component health status</span>
|
|
</div>
|
|
<div class="endpoint">
|
|
<span class="method get">GET</span>
|
|
<code>/api/users/me/files/{hash}/transcoding-status</code>
|
|
<span class="desc">Video transcoding progress and status</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="api-streaming" class="api-content">
|
|
<h4>WebSeed & P2P Endpoints</h4>
|
|
<div class="endpoint-list">
|
|
<div class="endpoint">
|
|
<span class="method get">GET</span>
|
|
<code>/api/webseed/{hash}</code>
|
|
<span class="desc">WebSeed endpoint for BitTorrent clients (BEP-19)</span>
|
|
</div>
|
|
<div class="endpoint">
|
|
<span class="method get">GET</span>
|
|
<code>/api/webseed/health</code>
|
|
<span class="desc">WebSeed health check and cache statistics</span>
|
|
</div>
|
|
<div class="endpoint">
|
|
<span class="method get">GET</span>
|
|
<code>/api/diagnostics</code>
|
|
<span class="desc">Comprehensive system diagnostics</span>
|
|
</div>
|
|
</div>
|
|
<div class="webseed-info">
|
|
<h5>WebSeed Features:</h5>
|
|
<ul>
|
|
<li>✅ BEP-19 compliant WebSeed implementation</li>
|
|
<li>✅ Advanced LRU piece caching</li>
|
|
<li>✅ Concurrent request optimization</li>
|
|
<li>✅ Client-specific optimizations</li>
|
|
<li>✅ Range request support</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="api-blossom" class="api-content">
|
|
<h4>Blossom Protocol Endpoints</h4>
|
|
<div class="endpoint-list">
|
|
<div class="endpoint">
|
|
<span class="method put">PUT</span>
|
|
<code>/upload</code>
|
|
<span class="desc">Upload blob to Blossom server</span>
|
|
</div>
|
|
<div class="endpoint">
|
|
<span class="method get">GET</span>
|
|
<code>/{hash}</code>
|
|
<span class="desc">Download blob by SHA-256 hash</span>
|
|
</div>
|
|
<div class="endpoint">
|
|
<span class="method get">GET</span>
|
|
<code>/info</code>
|
|
<span class="desc">Server information and capabilities</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="api-tracker" class="api-content">
|
|
<h4>BitTorrent Tracker Endpoints</h4>
|
|
<div class="endpoint-list">
|
|
<div class="endpoint">
|
|
<span class="method get">GET</span>
|
|
<code>/announce</code>
|
|
<span class="desc">BitTorrent announce endpoint for peer discovery</span>
|
|
</div>
|
|
<div class="endpoint">
|
|
<span class="method get">GET</span>
|
|
<code>/scrape</code>
|
|
<span class="desc">BitTorrent scrape endpoint for torrent statistics</span>
|
|
</div>
|
|
</div>
|
|
<div class="tracker-info">
|
|
<h5>Tracker Features:</h5>
|
|
<ul>
|
|
<li>✅ Built-in tracker eliminates external dependencies</li>
|
|
<li>✅ Always includes gateway as WebSeed peer</li>
|
|
<li>✅ Supports both compact and dictionary peer formats</li>
|
|
<li>✅ Automatic peer cleanup after 45 minutes</li>
|
|
<li>✅ Full BitTorrent protocol compliance</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tech-section">
|
|
<h3>Technical Implementation</h3>
|
|
<div class="tech-grid">
|
|
<div class="tech-item">
|
|
<h4>Intelligent Storage Strategy</h4>
|
|
<p>Files under 100MB stored as blobs for immediate access. Larger files automatically chunked into 2MB pieces with BitTorrent-compatible structure for parallel transfers.</p>
|
|
</div>
|
|
<div class="tech-item">
|
|
<h4>Advanced P2P Coordination</h4>
|
|
<p>Sophisticated peer ranking algorithm considering geographic proximity (30%), source reliability (25%), and historical performance (20%) for optimal peer selection.</p>
|
|
</div>
|
|
<div class="tech-item">
|
|
<h4>Production-Grade Performance</h4>
|
|
<p>LRU piece caching, concurrent request handling, connection pooling, and comprehensive health monitoring with automatic alerting systems.</p>
|
|
</div>
|
|
<div class="tech-item">
|
|
<h4>Multi-Client Optimization</h4>
|
|
<p>Built-in tracker with client detection and specific optimizations for qBittorrent, Transmission, WebTorrent, and other popular BitTorrent clients.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="protocols-section">
|
|
<h3>Supported Protocols & Standards</h3>
|
|
<div class="protocol-badges">
|
|
<span class="protocol-badge">HTTP/1.1 Range Requests</span>
|
|
<span class="protocol-badge">BitTorrent Protocol</span>
|
|
<span class="protocol-badge">WebSeed (BEP-19)</span>
|
|
<span class="protocol-badge">HLS Streaming</span>
|
|
<span class="protocol-badge">H.264/AAC Transcoding</span>
|
|
<span class="protocol-badge">Blossom Protocol</span>
|
|
<span class="protocol-badge">Nostr (NIP-35)</span>
|
|
<span class="protocol-badge">Kademlia DHT</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<div id="toast-container" class="toast-container"></div>
|
|
|
|
<!-- Login Modal -->
|
|
<div id="login-modal" class="modal" style="display: none;">
|
|
<div class="modal-content">
|
|
<span class="close" onclick="hideLogin()">×</span>
|
|
<h2>Login with Nostr</h2>
|
|
|
|
<div class="extension-info">
|
|
<h4>Need a Nostr Extension?</h4>
|
|
<div class="browser-extensions">
|
|
<div class="browser-group">
|
|
<strong>🌐 Chrome / Edge / Brave:</strong>
|
|
<ul>
|
|
<li><a href="https://getalby.com" target="_blank">Alby</a> - Most popular, supports Lightning</li>
|
|
<li><a href="https://github.com/fiatjaf/nos2x" target="_blank">nos2x</a> - Simple and lightweight</li>
|
|
</ul>
|
|
</div>
|
|
<div class="browser-group">
|
|
<strong>🦊 Firefox:</strong>
|
|
<ul>
|
|
<li><a href="https://github.com/fiatjaf/nos2x-fox" target="_blank">nos2x-fox</a> - Firefox version of nos2x</li>
|
|
<li><a href="https://getalby.com" target="_blank">Alby</a> - Also available for Firefox</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="login-methods">
|
|
<button id="nip07-login" class="login-btn">
|
|
Login with Browser Extension (NIP-07)
|
|
</button>
|
|
|
|
<button id="create-account-btn" class="login-btn" style="background-color: var(--bg-tertiary); color: var(--text-primary); margin-top: 10px;">
|
|
Don't have an account? Create one
|
|
</button>
|
|
</div>
|
|
|
|
<div class="bunker-section">
|
|
<h4>Remote Signer (NIP-46)</h4>
|
|
<input type="text" id="bunker-url" class="bunker-input"
|
|
placeholder="bunker://pubkey?relay=wss://relay.domain.com&secret=...">
|
|
<button id="nip46-login" class="login-btn">Connect Bunker</button>
|
|
</div>
|
|
|
|
<div id="login-status" class="status" style="display: none;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Share Modal -->
|
|
<div id="share-modal" class="modal" style="display: none;">
|
|
<div class="modal-content">
|
|
<span class="close" onclick="hideShareModal()">×</span>
|
|
<h2>Share File</h2>
|
|
<h3 id="share-file-name"></h3>
|
|
|
|
<div id="share-links" class="share-links">
|
|
<!-- Dynamic content -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- File Details Modal -->
|
|
<div id="file-details-modal" class="modal" style="display: none;">
|
|
<div class="modal-content">
|
|
<span class="close" onclick="hideFileDetailsModal()">×</span>
|
|
<h2>File Details</h2>
|
|
|
|
<div id="file-details-content">
|
|
<div class="file-preview-large">
|
|
<div id="file-icon-large"></div>
|
|
<h3 id="file-name-large"></h3>
|
|
</div>
|
|
|
|
<div class="file-metadata">
|
|
<div class="metadata-row">
|
|
<label>File Size:</label>
|
|
<span id="file-size-detail"></span>
|
|
</div>
|
|
<div class="metadata-row">
|
|
<label>Storage Type:</label>
|
|
<span id="file-storage-type"></span>
|
|
</div>
|
|
<div class="metadata-row">
|
|
<label>Upload Date:</label>
|
|
<span id="file-upload-date"></span>
|
|
</div>
|
|
<div class="metadata-row">
|
|
<label>File Hash:</label>
|
|
<span id="file-hash-detail" class="hash-text"></span>
|
|
</div>
|
|
<div class="metadata-row">
|
|
<label>Access Level:</label>
|
|
<div class="access-controls">
|
|
<select id="file-access-level">
|
|
<option value="public">Public</option>
|
|
<option value="private">Private</option>
|
|
</select>
|
|
<button id="update-access" class="action-btn">Update</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="file-actions-detail">
|
|
<button class="action-btn" onclick="shareFileFromModal()">🔗 Share</button>
|
|
<button class="action-btn" onclick="downloadFileFromModal()">⬇ Download</button>
|
|
<button class="action-btn danger" onclick="deleteFileFromModal()">🗑 Delete</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Account Creation Modal -->
|
|
<div id="create-account-modal" class="modal" style="display: none;">
|
|
<div class="modal-content" style="max-width: 600px;">
|
|
<span class="close" onclick="hideCreateAccount()">×</span>
|
|
<h2>Create Nostr Account</h2>
|
|
|
|
<div class="account-explanation">
|
|
<h4>What is Nostr?</h4>
|
|
<p>Nostr is a decentralized protocol that gives you control over your digital identity. Unlike traditional accounts, there's no company storing your data or controlling your access.</p>
|
|
|
|
<h4>How it works:</h4>
|
|
<ul>
|
|
<li><strong>No passwords</strong> - You get a pair of cryptographic keys instead</li>
|
|
<li><strong>You own your identity</strong> - No one can delete or suspend your account</li>
|
|
<li><strong>Privacy focused</strong> - We never see or store your private key</li>
|
|
<li><strong>Works everywhere</strong> - Same identity across all Nostr apps</li>
|
|
</ul>
|
|
|
|
<h4>Your Keys Explained:</h4>
|
|
<div class="key-explanation">
|
|
<div class="key-info">
|
|
<strong>🔓 Public Key (npub):</strong>
|
|
<p>Like your username - safe to share publicly. Others use this to find you.</p>
|
|
</div>
|
|
<div class="key-info">
|
|
<strong>🔐 Private Key (nsec):</strong>
|
|
<p>Like your password but MORE IMPORTANT - never share this! It proves you own your identity.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="warning-box">
|
|
<h4>⚠️ IMPORTANT SECURITY NOTICE</h4>
|
|
<p>Your private key is generated in your browser and <strong>never sent to our servers</strong>. You MUST save it securely - if you lose it, your account cannot be recovered!</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="key-generation" id="key-generation">
|
|
<button id="generate-keys" class="login-btn">Generate My Nostr Keys</button>
|
|
</div>
|
|
|
|
<div class="key-display" id="key-display" style="display: none;">
|
|
<h4>Your New Nostr Keys</h4>
|
|
<div class="key-pair">
|
|
<div class="key-item">
|
|
<label>🔓 Public Key (share this):</label>
|
|
<div class="key-row">
|
|
<input type="text" id="generated-npub" readonly onclick="this.select()">
|
|
<button onclick="copyKey('generated-npub')">Copy</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="key-item">
|
|
<label>🔐 Private Key (SAVE THIS SECURELY!):</label>
|
|
<div class="key-row">
|
|
<input type="password" id="generated-nsec" readonly onclick="this.select()">
|
|
<button onclick="toggleKeyVisibility('generated-nsec')">Show</button>
|
|
<button onclick="copyKey('generated-nsec')">Copy</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="save-instructions">
|
|
<h4>Save Your Private Key Now!</h4>
|
|
<p>Write down your private key (nsec) and store it safely. Recommended methods:</p>
|
|
<ul>
|
|
<li>✅ Password manager (1Password, Bitwarden, etc.)</li>
|
|
<li>✅ Written on paper stored securely</li>
|
|
<li>✅ Encrypted text file</li>
|
|
<li>❌ Screenshot or unencrypted file</li>
|
|
<li>❌ Cloud storage without encryption</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="extension-reminder">
|
|
<h4>Next Step: Install a Browser Extension</h4>
|
|
<p>To use your new Nostr keys, you'll need a browser extension:</p>
|
|
<div class="browser-extensions">
|
|
<div class="browser-group">
|
|
<strong>🌐 Chrome / Edge / Brave:</strong>
|
|
<ul>
|
|
<li><a href="https://getalby.com" target="_blank">Alby</a> - Most popular, supports Lightning</li>
|
|
<li><a href="https://github.com/fiatjaf/nos2x" target="_blank">nos2x</a> - Simple and lightweight</li>
|
|
</ul>
|
|
</div>
|
|
<div class="browser-group">
|
|
<strong>🦊 Firefox:</strong>
|
|
<ul>
|
|
<li><a href="https://github.com/fiatjaf/nos2x-fox" target="_blank">nos2x-fox</a> - Firefox version of nos2x</li>
|
|
<li><a href="https://getalby.com" target="_blank">Alby</a> - Also available for Firefox</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<p><strong>After installing:</strong> Import your private key (nsec) into the extension, then come back here to login!</p>
|
|
</div>
|
|
|
|
<div class="account-actions">
|
|
<button onclick="hideCreateAccount()" class="login-btn">I've Saved My Keys Securely</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/static/nostr-crypto.js"></script>
|
|
<script src="/static/nostr-auth.js"></script>
|
|
<script src="/static/upload.js"></script>
|
|
<script>
|
|
// Navigation functions
|
|
function showUpload() {
|
|
if (!window.nostrAuth || !window.nostrAuth.isAuthenticated()) {
|
|
showToast('Please login to upload files', 'warning');
|
|
showLogin();
|
|
return;
|
|
}
|
|
hideAllSections();
|
|
document.getElementById('upload-section').classList.add('active');
|
|
}
|
|
|
|
function showFiles() {
|
|
if (!window.nostrAuth || !window.nostrAuth.isAuthenticated()) {
|
|
showLogin();
|
|
return;
|
|
}
|
|
hideAllSections();
|
|
const filesSection = document.getElementById('files-section');
|
|
filesSection.classList.add('active');
|
|
console.log('Files section classes:', filesSection.className);
|
|
console.log('Files section display:', window.getComputedStyle(filesSection).display);
|
|
loadUserFiles();
|
|
loadUserStats();
|
|
}
|
|
|
|
function showServices() {
|
|
hideAllSections();
|
|
document.getElementById('services-section').classList.add('active');
|
|
loadServiceStats();
|
|
}
|
|
|
|
function showAbout() {
|
|
hideAllSections();
|
|
document.getElementById('about-section').classList.add('active');
|
|
}
|
|
|
|
function showApiTab(tabName) {
|
|
// Hide all API content sections
|
|
document.querySelectorAll('.api-content').forEach(content => {
|
|
content.classList.remove('active');
|
|
});
|
|
|
|
// Remove active class from all tabs
|
|
document.querySelectorAll('.api-tab').forEach(tab => {
|
|
tab.classList.remove('active');
|
|
});
|
|
|
|
// Show selected content and activate tab
|
|
document.getElementById(`api-${tabName}`).classList.add('active');
|
|
event.target.classList.add('active');
|
|
}
|
|
|
|
function hideAllSections() {
|
|
document.querySelectorAll('.section').forEach(section => {
|
|
section.classList.remove('active');
|
|
});
|
|
}
|
|
|
|
// Auth functions
|
|
function showLogin() {
|
|
document.getElementById('login-modal').style.display = 'flex';
|
|
}
|
|
|
|
function hideLogin() {
|
|
document.getElementById('login-modal').style.display = 'none';
|
|
}
|
|
|
|
async function updateAuthStatus() {
|
|
const loginBtn = document.getElementById('login-btn');
|
|
const userInfo = document.getElementById('user-info');
|
|
const userPubkeyShort = document.getElementById('user-pubkey-short');
|
|
const filesLink = document.getElementById('files-link');
|
|
const adminLink = document.getElementById('admin-link');
|
|
const uploadLink = document.getElementById('upload-link');
|
|
|
|
if (window.nostrAuth && window.nostrAuth.isAuthenticated()) {
|
|
const pubkey = window.nostrAuth.getCurrentUser();
|
|
console.log('User authenticated, updating UI...', pubkey);
|
|
|
|
loginBtn.style.display = 'none';
|
|
userInfo.style.display = 'flex';
|
|
|
|
if (filesLink) {
|
|
filesLink.style.display = 'block';
|
|
console.log('Files link shown');
|
|
}
|
|
|
|
if (uploadLink) {
|
|
uploadLink.style.display = 'block';
|
|
console.log('Upload link shown');
|
|
}
|
|
|
|
// Check if user is admin
|
|
try {
|
|
const response = await fetch('/api/users/me/admin-status', {
|
|
credentials: 'include',
|
|
headers: {
|
|
'Authorization': `Bearer ${window.nostrAuth.sessionToken}`
|
|
}
|
|
});
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
adminLink.style.display = data.is_admin ? 'block' : 'none';
|
|
} else {
|
|
if (response.status === 401 || response.status === 403) {
|
|
// Clear invalid session data and update UI
|
|
window.nostrAuth.sessionToken = null;
|
|
window.nostrAuth.pubkey = null;
|
|
localStorage.removeItem('session_token');
|
|
localStorage.removeItem('user_pubkey');
|
|
updateAuthStatus();
|
|
return; // Exit early since auth state changed
|
|
}
|
|
adminLink.style.display = 'none';
|
|
}
|
|
} catch (error) {
|
|
adminLink.style.display = 'none';
|
|
}
|
|
|
|
// Show pubkey immediately, fetch profile in background
|
|
userPubkeyShort.textContent = pubkey.substring(0, 8) + '...';
|
|
|
|
// Fetch profile in background with retries
|
|
fetchUserProfile(pubkey, userPubkeyShort);
|
|
} else {
|
|
console.log('User not authenticated, hiding auth links');
|
|
loginBtn.style.display = 'block';
|
|
userInfo.style.display = 'none';
|
|
|
|
if (filesLink) {
|
|
filesLink.style.display = 'none';
|
|
}
|
|
|
|
if (adminLink) {
|
|
adminLink.style.display = 'none';
|
|
}
|
|
|
|
if (uploadLink) {
|
|
uploadLink.style.display = 'none';
|
|
}
|
|
}
|
|
}
|
|
|
|
async function fetchUserProfile(pubkey, displayElement, retryCount = 0) {
|
|
const maxRetries = 3;
|
|
const timeout = 3000; // 3 second timeout per attempt
|
|
|
|
try {
|
|
console.log(`Fetching profile (attempt ${retryCount + 1}/${maxRetries})...`);
|
|
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
|
|
const response = await fetch(`/api/profile/${pubkey}`, {
|
|
signal: controller.signal
|
|
});
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.success && data.profile) {
|
|
const profile = data.profile;
|
|
const displayName = profile.display_name || profile.name || (pubkey.substring(0, 8) + '...');
|
|
|
|
if (profile.picture) {
|
|
displayElement.innerHTML = `
|
|
<img src="${profile.picture}" style="width: 24px; height: 24px; border-radius: 50%; margin-right: 8px; vertical-align: middle;">
|
|
${displayName}
|
|
`;
|
|
} else {
|
|
displayElement.textContent = displayName;
|
|
}
|
|
console.log('Profile loaded successfully');
|
|
return;
|
|
}
|
|
}
|
|
throw new Error('Profile not found');
|
|
|
|
} catch (error) {
|
|
console.log(`Profile fetch attempt ${retryCount + 1} failed:`, error.message);
|
|
|
|
if (retryCount < maxRetries - 1) {
|
|
// Retry with exponential backoff
|
|
const delay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s
|
|
setTimeout(() => {
|
|
fetchUserProfile(pubkey, displayElement, retryCount + 1);
|
|
}, delay);
|
|
} else {
|
|
console.log('Profile fetching failed after all retries, keeping pubkey display');
|
|
}
|
|
}
|
|
}
|
|
|
|
function showStatus(message, isError = false) {
|
|
const status = document.getElementById('login-status');
|
|
status.textContent = message;
|
|
status.className = `status ${isError ? 'error' : 'success'}`;
|
|
status.style.display = 'block';
|
|
|
|
setTimeout(() => {
|
|
status.style.display = 'none';
|
|
}, 5000);
|
|
}
|
|
|
|
async function logout() {
|
|
if (window.nostrAuth) {
|
|
await window.nostrAuth.logout();
|
|
updateAuthStatus();
|
|
showUpload();
|
|
showToast('Logged out successfully', 'success');
|
|
}
|
|
}
|
|
|
|
// Dashboard state
|
|
let currentViewMode = 'grid';
|
|
let currentFilter = 'all';
|
|
let userFiles = [];
|
|
|
|
// Dashboard functions
|
|
function setViewMode(mode) {
|
|
currentViewMode = mode;
|
|
document.getElementById('grid-view-btn').classList.toggle('active', mode === 'grid');
|
|
document.getElementById('list-view-btn').classList.toggle('active', mode === 'list');
|
|
|
|
const fileList = document.getElementById('file-list');
|
|
fileList.className = mode === 'grid' ? 'file-grid' : 'file-list-view';
|
|
|
|
renderFiles();
|
|
}
|
|
|
|
function filterFiles(filter) {
|
|
currentFilter = filter;
|
|
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
|
|
event.target.classList.add('active');
|
|
renderFiles();
|
|
}
|
|
|
|
// User file management with debugging
|
|
async function loadUserStats() {
|
|
try {
|
|
console.log('Loading user stats...');
|
|
const stats = await window.nostrAuth.getUserStats();
|
|
console.log('User stats loaded:', stats);
|
|
|
|
document.getElementById('user-file-count').textContent = stats.file_count;
|
|
document.getElementById('user-storage-used').textContent = formatBytes(stats.storage_used);
|
|
document.getElementById('user-last-login').textContent = stats.last_login ?
|
|
new Date(stats.last_login).toLocaleDateString() : 'Never';
|
|
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load user stats:', error);
|
|
if (error.message.includes('401') || error.message.includes('Unauthorized')) {
|
|
// Update auth UI since session was cleared
|
|
updateAuthStatus();
|
|
}
|
|
}
|
|
}
|
|
|
|
async function loadUserFiles() {
|
|
const loadingEl = document.getElementById('loading-files');
|
|
const fileList = document.getElementById('file-list');
|
|
|
|
// Show loading state
|
|
loadingEl.style.display = 'block';
|
|
fileList.innerHTML = '<div class="loading-state"><div class="spinner"></div>LOADING FILES...</div>';
|
|
|
|
try {
|
|
console.log('Loading user files...');
|
|
console.log('Auth status:', window.nostrAuth.isAuthenticated());
|
|
console.log('Session token:', window.nostrAuth.sessionToken ? 'present' : 'missing');
|
|
|
|
const response = await window.nostrAuth.getUserFiles();
|
|
userFiles = response.files || [];
|
|
console.log('User files loaded:', userFiles);
|
|
|
|
if (userFiles.length === 0) {
|
|
fileList.innerHTML = '<div class="empty-state">NO FILES UPLOADED YET</div>';
|
|
} else {
|
|
renderFiles();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load user files:', error);
|
|
let errorMessage = 'FAILED TO LOAD FILES';
|
|
|
|
if (error.message.includes('401') || error.message.includes('Unauthorized')) {
|
|
errorMessage = 'SESSION EXPIRED - PLEASE LOGIN AGAIN';
|
|
// Update auth UI since session was cleared
|
|
updateAuthStatus();
|
|
} else if (error.message.includes('403')) {
|
|
errorMessage = 'ACCESS DENIED';
|
|
} else if (error.message.includes('500')) {
|
|
errorMessage = 'SERVER ERROR - TRY AGAIN LATER';
|
|
} else if (error.message.includes('NetworkError') || error.message.includes('fetch')) {
|
|
errorMessage = 'CONNECTION ERROR - CHECK NETWORK';
|
|
}
|
|
|
|
fileList.innerHTML = `<div class="error-state">${errorMessage}<br><button onclick="loadUserFiles()" class="action-btn" style="margin-top: 10px;">RETRY</button></div>`;
|
|
} finally {
|
|
loadingEl.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function renderFiles() {
|
|
console.log('Rendering files:', userFiles.length, 'files');
|
|
console.log('Current filter:', currentFilter);
|
|
console.log('Current view mode:', currentViewMode);
|
|
|
|
const fileList = document.getElementById('file-list');
|
|
console.log('File list element:', fileList);
|
|
console.log('File list display:', fileList ? window.getComputedStyle(fileList).display : 'not found');
|
|
|
|
if (!fileList) {
|
|
console.error('file-list element not found!');
|
|
return;
|
|
}
|
|
|
|
// Filter files based on current filter
|
|
let filteredFiles = userFiles;
|
|
if (currentFilter !== 'all') {
|
|
filteredFiles = userFiles.filter(file => {
|
|
switch (currentFilter) {
|
|
case 'blob': return file.storage_type === 'blob';
|
|
case 'torrent': return file.storage_type === 'torrent';
|
|
case 'private': return file.access_level === 'private';
|
|
default: return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
console.log('Filtered files:', filteredFiles.length, 'files');
|
|
|
|
if (filteredFiles.length === 0) {
|
|
fileList.innerHTML = '<p class="empty-state">No files found.</p>';
|
|
return;
|
|
}
|
|
|
|
if (currentViewMode === 'grid') {
|
|
console.log('Rendering grid view for', filteredFiles.length, 'files');
|
|
console.log('First file:', filteredFiles[0]);
|
|
|
|
try {
|
|
const html = filteredFiles.map(file => {
|
|
console.log('Processing file:', file.name, file.hash);
|
|
return `
|
|
<div class="file-card" data-hash="${file.hash}" onclick="showFileDetails('${file.hash}')">
|
|
<div class="file-preview">
|
|
${getFileIcon(file.name)}
|
|
</div>
|
|
<div class="file-info">
|
|
<h4 class="file-name" title="${escapeHtml(file.name)}">${escapeHtml(file.name)}</h4>
|
|
<div class="file-meta">
|
|
<span class="file-size">${formatBytes(file.size)}</span>
|
|
<span class="file-type">${file.storage_type}</span>
|
|
<span class="access-level ${file.access_level}">${file.access_level}</span>
|
|
</div>
|
|
<div class="file-date">${new Date(file.uploaded_at).toLocaleDateString()}</div>
|
|
</div>
|
|
<div class="file-actions" onclick="event.stopPropagation()">
|
|
<button class="action-btn" onclick="shareFile('${file.hash}')" title="Share">🔗</button>
|
|
<button class="action-btn" onclick="downloadFile('${file.hash}')" title="Download">⬇</button>
|
|
<button class="action-btn danger" onclick="deleteFile('${file.hash}')" title="Delete">🗑</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
console.log('Generated HTML length:', html.length);
|
|
fileList.innerHTML = html;
|
|
console.log('Grid HTML set successfully');
|
|
} catch (error) {
|
|
console.error('Error in grid rendering:', error);
|
|
}
|
|
} else {
|
|
fileList.innerHTML = filteredFiles.map(file => `
|
|
<div class="file-row" data-hash="${file.hash}">
|
|
<div class="file-icon">${getFileIcon(file.name)}</div>
|
|
<div class="file-details">
|
|
<div class="file-name">${escapeHtml(file.name)}</div>
|
|
<div class="file-meta">
|
|
${formatBytes(file.size)} • ${file.storage_type} • ${file.access_level} •
|
|
${new Date(file.uploaded_at).toLocaleDateString()}
|
|
</div>
|
|
</div>
|
|
<div class="file-actions">
|
|
<button class="action-btn" onclick="shareFile('${file.hash}')">Share</button>
|
|
<button class="action-btn" onclick="downloadFile('${file.hash}')">Download</button>
|
|
<button class="action-btn danger" onclick="deleteFile('${file.hash}')">Delete</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
}
|
|
|
|
function getFileIcon(filename) {
|
|
const ext = filename.split('.').pop().toLowerCase();
|
|
const iconMap = {
|
|
'mp4': '🎥', 'mkv': '🎥', 'avi': '🎥', 'mov': '🎥',
|
|
'mp3': '🎵', 'wav': '🎵', 'flac': '🎵', 'm4a': '🎵',
|
|
'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️', 'webp': '🖼️',
|
|
'pdf': '📄', 'doc': '📄', 'docx': '📄', 'txt': '📄',
|
|
'zip': '📦', 'rar': '📦', '7z': '📦', 'tar': '📦',
|
|
'exe': '⚙️', 'deb': '⚙️', 'dmg': '⚙️', 'msi': '⚙️'
|
|
};
|
|
return iconMap[ext] || '📁';
|
|
}
|
|
|
|
async function shareFile(hash) {
|
|
const file = userFiles.find(f => f.hash === hash);
|
|
if (!file) return;
|
|
|
|
const baseUrl = window.location.origin;
|
|
const links = {
|
|
direct: `${baseUrl}/api/download/${hash}`,
|
|
torrent: `${baseUrl}/api/torrent/${hash}`,
|
|
magnet: `magnet:?xt=urn:btih:${hash}&dn=${encodeURIComponent(file.name)}`,
|
|
stream: file.name.match(/\.(mp4|mkv|avi|mov)$/i) ? `${baseUrl}/player.html?hash=${hash}` : null
|
|
};
|
|
|
|
showShareModal(file, links);
|
|
}
|
|
|
|
function showShareModal(file, links) {
|
|
const modal = document.getElementById('share-modal');
|
|
const fileName = document.getElementById('share-file-name');
|
|
const linksContainer = document.getElementById('share-links');
|
|
|
|
fileName.textContent = file.name;
|
|
|
|
let linksHTML = `
|
|
<div class="share-link">
|
|
<label>Direct Download:</label>
|
|
<div class="link-row">
|
|
<input type="text" value="${links.direct}" readonly onclick="this.select()">
|
|
<button onclick="copyToClipboard('${links.direct}')">Copy</button>
|
|
</div>
|
|
</div>
|
|
<div class="share-link">
|
|
<label>Torrent File:</label>
|
|
<div class="link-row">
|
|
<input type="text" value="${links.torrent}" readonly onclick="this.select()">
|
|
<button onclick="copyToClipboard('${links.torrent}')">Copy</button>
|
|
</div>
|
|
</div>
|
|
<div class="share-link">
|
|
<label>Magnet Link:</label>
|
|
<div class="link-row">
|
|
<input type="text" value="${links.magnet}" readonly onclick="this.select()">
|
|
<button onclick="copyToClipboard('${links.magnet}')">Copy</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
if (links.stream) {
|
|
linksHTML += `
|
|
<div class="share-link">
|
|
<label>Stream Player:</label>
|
|
<div class="link-row">
|
|
<input type="text" value="${links.stream}" readonly onclick="this.select()">
|
|
<button onclick="copyToClipboard('${links.stream}')">Copy</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
linksContainer.innerHTML = linksHTML;
|
|
modal.style.display = 'flex';
|
|
}
|
|
|
|
function hideShareModal() {
|
|
document.getElementById('share-modal').style.display = 'none';
|
|
}
|
|
|
|
// File details modal
|
|
let currentFileDetails = null;
|
|
|
|
function showFileDetails(hash) {
|
|
const file = userFiles.find(f => f.hash === hash);
|
|
if (!file) return;
|
|
|
|
currentFileDetails = file;
|
|
|
|
document.getElementById('file-icon-large').innerHTML = getFileIcon(file.name);
|
|
document.getElementById('file-name-large').textContent = file.name;
|
|
document.getElementById('file-size-detail').textContent = formatBytes(file.size);
|
|
document.getElementById('file-storage-type').textContent = file.storage_type;
|
|
document.getElementById('file-upload-date').textContent = new Date(file.uploaded_at).toLocaleDateString();
|
|
document.getElementById('file-hash-detail').textContent = file.hash;
|
|
document.getElementById('file-access-level').value = file.access_level;
|
|
|
|
document.getElementById('file-details-modal').style.display = 'flex';
|
|
}
|
|
|
|
function hideFileDetailsModal() {
|
|
document.getElementById('file-details-modal').style.display = 'none';
|
|
currentFileDetails = null;
|
|
}
|
|
|
|
async function updateFileAccess() {
|
|
if (!currentFileDetails) return;
|
|
|
|
const newAccessLevel = document.getElementById('file-access-level').value;
|
|
if (newAccessLevel === currentFileDetails.access_level) return;
|
|
|
|
try {
|
|
const response = await window.nostrAuth.updateFileAccess(currentFileDetails.hash, newAccessLevel);
|
|
if (response.success) {
|
|
showToast('Access level updated successfully', 'success');
|
|
currentFileDetails.access_level = newAccessLevel;
|
|
|
|
// Update the file in userFiles array
|
|
const fileIndex = userFiles.findIndex(f => f.hash === currentFileDetails.hash);
|
|
if (fileIndex !== -1) {
|
|
userFiles[fileIndex].access_level = newAccessLevel;
|
|
}
|
|
|
|
renderFiles();
|
|
hideFileDetailsModal();
|
|
} else {
|
|
showToast('Failed to update access level: ' + response.message, 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating access level:', error);
|
|
showToast('Error updating access level: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
function shareFileFromModal() {
|
|
if (currentFileDetails) {
|
|
shareFile(currentFileDetails.hash);
|
|
hideFileDetailsModal();
|
|
}
|
|
}
|
|
|
|
function downloadFileFromModal() {
|
|
if (currentFileDetails) {
|
|
downloadFile(currentFileDetails.hash);
|
|
}
|
|
}
|
|
|
|
function deleteFileFromModal() {
|
|
if (currentFileDetails) {
|
|
hideFileDetailsModal();
|
|
deleteFile(currentFileDetails.hash);
|
|
}
|
|
}
|
|
|
|
async function copyToClipboard(text) {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
showToast('Copied to clipboard!', 'success');
|
|
} catch (error) {
|
|
console.error('Failed to copy:', error);
|
|
showToast('Failed to copy to clipboard', 'error');
|
|
}
|
|
}
|
|
|
|
async function deleteFile(hash) {
|
|
if (!confirm('Are you sure you want to delete this file?')) return;
|
|
|
|
try {
|
|
await window.nostrAuth.deleteFile(hash);
|
|
showToast('File deleted successfully', 'success');
|
|
userFiles = userFiles.filter(f => f.hash !== hash);
|
|
renderFiles();
|
|
loadUserStats();
|
|
} catch (error) {
|
|
console.error('Failed to delete file:', error);
|
|
showToast('Failed to delete file: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
function downloadFile(hash) {
|
|
const a = document.createElement('a');
|
|
a.href = `/api/download/${hash}`;
|
|
a.download = '';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
}
|
|
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Service stats functions
|
|
async function loadServiceStats() {
|
|
try {
|
|
const response = await fetch('/api/stats');
|
|
const data = await response.json();
|
|
|
|
if (data.gateway) {
|
|
document.getElementById('gateway-status').textContent = data.gateway.status === 'healthy' ? '🟢' : '🔴';
|
|
document.getElementById('gateway-uploads').textContent = data.gateway.uploads;
|
|
document.getElementById('gateway-storage').textContent = formatBytes(data.gateway.storage);
|
|
}
|
|
|
|
if (data.blossom) {
|
|
document.getElementById('blossom-status').textContent = data.blossom.status === 'healthy' ? '🟢' : '🔴';
|
|
document.getElementById('blossom-blobs').textContent = data.blossom.blobs;
|
|
document.getElementById('blossom-storage').textContent = formatBytes(data.blossom.storage);
|
|
}
|
|
|
|
if (data.dht) {
|
|
document.getElementById('dht-status').textContent = data.dht.status === 'healthy' ? '🟢' : '🔴';
|
|
document.getElementById('dht-peers').textContent = data.dht.peers;
|
|
document.getElementById('dht-torrents').textContent = data.dht.torrents;
|
|
}
|
|
|
|
if (data.tracker) {
|
|
document.getElementById('tracker-status').textContent = data.tracker.status === 'healthy' ? '🟢' : '🔴';
|
|
document.getElementById('tracker-torrents').textContent = data.tracker.torrents;
|
|
document.getElementById('tracker-peers').textContent = data.tracker.peers;
|
|
document.getElementById('tracker-seeders').textContent = data.tracker.seeders;
|
|
document.getElementById('tracker-leechers').textContent = data.tracker.leechers;
|
|
}
|
|
|
|
if (data.system) {
|
|
document.getElementById('system-mode').textContent = data.system.mode;
|
|
document.getElementById('system-uptime').textContent = data.system.uptime;
|
|
document.getElementById('system-storage').textContent = formatBytes(data.system.storage);
|
|
document.getElementById('system-connections').textContent = data.system.connections;
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Failed to load service stats:', error);
|
|
// Set error indicators
|
|
document.querySelectorAll('.status-indicator').forEach(indicator => {
|
|
indicator.textContent = '🔴';
|
|
});
|
|
}
|
|
}
|
|
|
|
function refreshDHTStats() {
|
|
loadServiceStats();
|
|
}
|
|
|
|
function showToast(message, type = 'info') {
|
|
const container = document.getElementById('toast-container');
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast ${type}`;
|
|
toast.textContent = message;
|
|
|
|
container.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
toast.remove();
|
|
}, 5000);
|
|
}
|
|
|
|
// Load branding configuration
|
|
async function loadBranding() {
|
|
try {
|
|
const response = await fetch('/api/branding');
|
|
if (response.ok) {
|
|
const branding = await response.json();
|
|
|
|
// Update site title
|
|
if (branding.site_name) {
|
|
document.getElementById('site-title').textContent = branding.site_name;
|
|
document.title = branding.site_name;
|
|
}
|
|
|
|
// Add logo if configured
|
|
if (branding.logo_url) {
|
|
const title = document.getElementById('site-title');
|
|
const logo = document.createElement('img');
|
|
logo.src = branding.logo_url;
|
|
logo.alt = branding.site_name || 'Logo';
|
|
logo.style.height = branding.logo_height || '32px';
|
|
logo.style.width = branding.logo_width || 'auto';
|
|
logo.style.marginRight = '10px';
|
|
logo.style.verticalAlign = 'middle';
|
|
|
|
// Replace text with logo + text or just logo
|
|
title.innerHTML = '';
|
|
title.appendChild(logo);
|
|
if (branding.site_name) {
|
|
title.appendChild(document.createTextNode(branding.site_name));
|
|
}
|
|
}
|
|
|
|
// Update favicon if configured
|
|
if (branding.favicon_url) {
|
|
let favicon = document.querySelector('link[rel="icon"]');
|
|
if (!favicon) {
|
|
favicon = document.createElement('link');
|
|
favicon.rel = 'icon';
|
|
document.head.appendChild(favicon);
|
|
}
|
|
favicon.href = branding.favicon_url;
|
|
}
|
|
|
|
// Update description in about section if configured
|
|
if (branding.description) {
|
|
// This will be used when the about section is shown
|
|
window.brandingDescription = branding.description;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log('Could not load branding configuration, using defaults');
|
|
}
|
|
}
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadBranding();
|
|
updateAuthStatus();
|
|
|
|
// Login event listeners
|
|
document.getElementById('nip07-login').addEventListener('click', async () => {
|
|
const result = await window.nostrAuth.loginNIP07();
|
|
if (result.success) {
|
|
showStatus(result.message);
|
|
await updateAuthStatus();
|
|
setTimeout(() => {
|
|
hideLogin();
|
|
updateAuthStatus(); // Force another update for Firefox
|
|
showToast('Logged in successfully!', 'success');
|
|
}, 50);
|
|
} else {
|
|
showStatus(result.message, true);
|
|
}
|
|
});
|
|
|
|
document.getElementById('nip46-login').addEventListener('click', async () => {
|
|
const bunkerURL = document.getElementById('bunker-url').value.trim();
|
|
if (!bunkerURL) {
|
|
showStatus('Please enter a bunker URL', true);
|
|
return;
|
|
}
|
|
|
|
const result = await window.nostrAuth.loginNIP46(bunkerURL);
|
|
if (result.success) {
|
|
showStatus(result.message);
|
|
await updateAuthStatus();
|
|
setTimeout(() => {
|
|
hideLogin();
|
|
updateAuthStatus(); // Force another update for Firefox
|
|
showToast('Logged in successfully!', 'success');
|
|
}, 50);
|
|
} else {
|
|
showStatus(result.message, true);
|
|
}
|
|
});
|
|
|
|
document.getElementById('refresh-files').addEventListener('click', () => {
|
|
loadUserFiles();
|
|
loadUserStats();
|
|
});
|
|
|
|
// Auto-refresh services stats every 30 seconds if on services page
|
|
setInterval(() => {
|
|
if (document.getElementById('services-section').classList.contains('active')) {
|
|
loadServiceStats();
|
|
}
|
|
}, 30000);
|
|
|
|
document.getElementById('update-access').addEventListener('click', updateFileAccess);
|
|
|
|
// Account creation event listeners
|
|
document.getElementById('create-account-btn').addEventListener('click', () => {
|
|
hideLogin();
|
|
showCreateAccount();
|
|
});
|
|
|
|
document.getElementById('generate-keys').addEventListener('click', generateNostrKeys);
|
|
});
|
|
|
|
// Close modals when clicking outside
|
|
window.addEventListener('click', (event) => {
|
|
const loginModal = document.getElementById('login-modal');
|
|
const shareModal = document.getElementById('share-modal');
|
|
const fileDetailsModal = document.getElementById('file-details-modal');
|
|
|
|
if (event.target === loginModal) {
|
|
hideLogin();
|
|
}
|
|
if (event.target === shareModal) {
|
|
hideShareModal();
|
|
}
|
|
if (event.target === fileDetailsModal) {
|
|
hideFileDetailsModal();
|
|
}
|
|
});
|
|
|
|
// Account creation functions
|
|
function showCreateAccount() {
|
|
document.getElementById('create-account-modal').style.display = 'flex';
|
|
}
|
|
|
|
function hideCreateAccount() {
|
|
document.getElementById('create-account-modal').style.display = 'none';
|
|
// Reset the modal state
|
|
document.getElementById('key-generation').style.display = 'block';
|
|
document.getElementById('key-display').style.display = 'none';
|
|
}
|
|
|
|
async function generateNostrKeys() {
|
|
try {
|
|
showToast('Generating cryptographic keys...', 'info');
|
|
|
|
// Use the proper cryptographic implementation
|
|
const keyPair = await window.NostrCrypto.generateKeyPair();
|
|
|
|
// Display the keys
|
|
document.getElementById('generated-npub').value = keyPair.npub;
|
|
document.getElementById('generated-nsec').value = keyPair.nsec;
|
|
|
|
// Store for potential extension import
|
|
window.generatedKeys = keyPair;
|
|
|
|
// Show key display
|
|
document.getElementById('key-generation').style.display = 'none';
|
|
document.getElementById('key-display').style.display = 'block';
|
|
|
|
showToast('Keys generated successfully!', 'success');
|
|
|
|
} catch (error) {
|
|
console.error('Key generation failed:', error);
|
|
showToast('Failed to generate keys. Please try again.', 'error');
|
|
}
|
|
}
|
|
|
|
function copyKey(elementId) {
|
|
const element = document.getElementById(elementId);
|
|
element.select();
|
|
document.execCommand('copy');
|
|
showToast('Copied to clipboard!', 'success');
|
|
}
|
|
|
|
function toggleKeyVisibility(elementId) {
|
|
const element = document.getElementById(elementId);
|
|
const button = event.target;
|
|
|
|
if (element.type === 'password') {
|
|
element.type = 'text';
|
|
button.textContent = 'Hide';
|
|
} else {
|
|
element.type = 'password';
|
|
button.textContent = 'Show';
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |