enki 3b9bf95247
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 / E2E Tests (push) Blocked by required conditions
more fuckingfixes
2025-08-28 14:15:37 -07:00

2315 lines
117 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>
<div class="header-content">
<h1 id="site-title">⚡ BitTorrent Gateway</h1>
<div class="header-right">
<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>
<button id="mobile-menu-toggle" class="mobile-menu-toggle" onclick="toggleMobileMenu()" style="display: none;">
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
<span class="hamburger-line"></span>
</button>
</div>
</div>
<nav id="main-nav">
<a href="#about" onclick="showAbout(); return false;">About</a>
<a href="#services" onclick="showServices(); return false;" id="stats-link" style="display: none;">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>
</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">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
<h3 style="margin: 0;">Recent Uploads</h3>
<button class="action-btn" onclick="gatewayUI.loadServerFiles(true)" style="font-size: 0.8rem; padding: 5px 10px;">
↻ Refresh
</button>
</div>
<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>
<!-- Enhanced Stats Sections -->
<div class="enhanced-stats">
<div class="stats-section">
<h3>📊 Real-Time Performance</h3>
<div class="service-grid">
<div class="stat-card">
<div class="stat-header">Response Time</div>
<div class="stat-value" id="perf-avg-response-time">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Requests/Second</div>
<div class="stat-value" id="perf-requests-per-second">--</div>
</div>
<div class="stat-card">
<div class="stat-header">CPU Usage</div>
<div class="stat-value" id="perf-cpu-usage">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Memory</div>
<div class="stat-value" id="perf-memory-usage">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Goroutines</div>
<div class="stat-value" id="perf-goroutines">--</div>
</div>
<div class="stat-card">
<div class="stat-header">GC Pause</div>
<div class="stat-value" id="perf-gc-pause">--</div>
</div>
</div>
</div>
<div class="stats-section">
<h3>🎥 Video Streaming Analytics</h3>
<div class="service-grid">
<div class="stat-card">
<div class="stat-header">Active Streams</div>
<div class="stat-value" id="stream-active-streams">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Total Bandwidth</div>
<div class="stat-value" id="stream-total-bandwidth">--</div>
</div>
<div class="stat-card">
<div class="stat-header">HLS Requests Today</div>
<div class="stat-value" id="stream-hls-requests">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Avg Bitrate</div>
<div class="stat-value" id="stream-avg-bitrate">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Concurrent Viewers</div>
<div class="stat-value" id="stream-concurrent-viewers">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Transcoded Files</div>
<div class="stat-value" id="stream-transcoded-files">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Failed Transcodes</div>
<div class="stat-value" id="stream-failed-transcodes">--</div>
</div>
</div>
</div>
<div class="stats-section">
<h3>🔄 P2P Network Health</h3>
<div class="service-grid">
<div class="stat-card">
<div class="stat-header">Health Score</div>
<div class="stat-value" id="p2p-health-score">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Active Peers</div>
<div class="stat-value" id="p2p-active-peers">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Seeding Ratio</div>
<div class="stat-value" id="p2p-seeding-ratio">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Bandwidth In</div>
<div class="stat-value" id="p2p-bandwidth-in">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Bandwidth Out</div>
<div class="stat-value" id="p2p-bandwidth-out">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Total Shared</div>
<div class="stat-value" id="p2p-total-shared">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Pieces Shared Today</div>
<div class="stat-value" id="p2p-pieces-shared">--</div>
</div>
</div>
</div>
<div class="stats-section">
<h3>🌐 DHT Network</h3>
<div class="service-grid">
<div class="stat-card">
<div class="stat-header">Status</div>
<div class="stat-value status-indicator" id="dht-status">🔴</div>
</div>
<div class="stat-card">
<div class="stat-header">Routing Table</div>
<div class="stat-value" id="dht-peers">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Announced Torrents</div>
<div class="stat-value" id="dht-torrents">--</div>
</div>
</div>
</div>
<div class="stats-section">
<h3>📊 Tracker Stats</h3>
<div class="service-grid">
<div class="stat-card">
<div class="stat-header">Status</div>
<div class="stat-value status-indicator" id="tracker-status">🔴</div>
</div>
<div class="stat-card">
<div class="stat-header">Torrents</div>
<div class="stat-value" id="tracker-torrents">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Total Peers</div>
<div class="stat-value" id="tracker-peers">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Seeders</div>
<div class="stat-value" id="tracker-seeders">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Leechers</div>
<div class="stat-value" id="tracker-leechers">--</div>
</div>
</div>
</div>
<div class="stats-section">
<h3>📱 WebTorrent Integration</h3>
<div class="service-grid">
<div class="stat-card">
<div class="stat-header">Active Connections</div>
<div class="stat-value" id="wt-active-connections">--</div>
</div>
<div class="stat-card">
<div class="stat-header">WebSeed Requests</div>
<div class="stat-value" id="wt-webseed-requests">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Bytes Served Today</div>
<div class="stat-value" id="wt-bytes-served">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Avg Speed</div>
<div class="stat-value" id="wt-avg-speed">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Browser Clients</div>
<div class="stat-value" id="wt-browser-clients">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Success Rate</div>
<div class="stat-value" id="wt-success-rate">--</div>
</div>
</div>
</div>
<div class="stats-section">
<h3>💾 Storage Efficiency</h3>
<div class="service-grid">
<div class="stat-card">
<div class="stat-header">Dedup Ratio</div>
<div class="stat-value" id="storage-dedup-ratio">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Space Saved</div>
<div class="stat-value" id="storage-space-saved">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Chunk Efficiency</div>
<div class="stat-value" id="storage-chunk-efficiency">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Blob Files</div>
<div class="stat-value" id="storage-blob-files">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Chunked Files</div>
<div class="stat-value" id="storage-chunked-files">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Total Chunks</div>
<div class="stat-value" id="storage-total-chunks">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Unique Chunks</div>
<div class="stat-value" id="storage-unique-chunks">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Avg Chunk Size</div>
<div class="stat-value" id="storage-avg-chunk-size">--</div>
</div>
</div>
</div>
<div class="stats-section">
<h3>⚡ System Health</h3>
<div class="service-grid">
<div class="stat-card">
<div class="stat-header">Disk Usage</div>
<div class="stat-value" id="sys-disk-usage">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Network I/O</div>
<div class="stat-value" id="sys-network-io">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Open Files</div>
<div class="stat-value" id="sys-open-files">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Load Average</div>
<div class="stat-value" id="sys-load-average">--</div>
</div>
<div class="stat-card">
<div class="stat-header">FFmpeg Status</div>
<div class="stat-value" id="sys-ffmpeg-status">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Transcode Queue</div>
<div class="stat-value" id="sys-transcode-queue">--</div>
</div>
<div class="stat-card">
<div class="stat-header">Transcode Storage</div>
<div class="stat-value" id="sys-transcode-storage">--</div>
</div>
</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 (&lt;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()">&times;</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()">&times;</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()">&times;</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()">&times;</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();
}
async function showServices() {
// Check if user is admin before showing stats
if (!window.nostrAuth || !window.nostrAuth.isAuthenticated()) {
showToast('Please login to access server stats', 'error');
showLogin();
return;
}
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();
if (!data.is_admin) {
showToast('Admin access required for server stats', 'error');
return;
}
} else {
showToast('Failed to verify admin access', 'error');
return;
}
} catch (error) {
showToast('Error checking admin status', 'error');
return;
}
hideAllSections();
document.getElementById('services-section').classList.add('active');
loadEnhancedServiceStats();
}
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 statsLink = document.getElementById('stats-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';
statsLink.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';
statsLink.style.display = 'none';
}
} catch (error) {
adminLink.style.display = 'none';
statsLink.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 (statsLink) {
statsLink.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="console.log('Button clicked!', this.dataset.hash); shareFile(this.dataset.hash)" data-hash="${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(this.dataset.hash)" data-hash="${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) {
console.log('DEBUG: shareFile function called!', hash);
const file = userFiles.find(f => f.hash === hash);
if (!file) {
console.error('File not found in userFiles array');
return;
}
const baseUrl = window.location.origin;
// Use torrent info hash for magnet if available, otherwise use file hash
let magnetHash = hash;
if (file.torrent_info_hash) {
magnetHash = file.torrent_info_hash;
console.log('Using torrent InfoHash for magnet:', magnetHash);
} else {
console.log('No torrent InfoHash found, using file hash:', magnetHash);
}
const links = {
direct: `${baseUrl}/api/download/${hash}`,
torrent: `${baseUrl}/api/torrent/${hash}`,
magnet: `magnet:?xt=urn:btih:${magnetHash}&dn=${encodeURIComponent(file.name)}`,
stream: file.name.match(/\.(mp4|mkv|avi|mov)$/i) ? `${baseUrl}/api/stream/${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 video-streaming">
<label>Video Streaming:</label>
<div class="video-quality-section">
<div class="quality-loading" id="quality-loading-${file.hash}">
<p>Loading available qualities...</p>
</div>
<div class="quality-links" id="quality-links-${file.hash}" style="display: none;">
<!-- Quality links will be populated here -->
</div>
</div>
</div>
`;
// Load video qualities after modal is shown
setTimeout(() => loadVideoQualities(file.hash), 100);
}
linksContainer.innerHTML = linksHTML;
modal.style.display = 'flex';
}
function hideShareModal() {
document.getElementById('share-modal').style.display = 'none';
}
async function loadVideoQualities(fileHash) {
const loadingElement = document.getElementById(`quality-loading-${fileHash}`);
const linksElement = document.getElementById(`quality-links-${fileHash}`);
if (!loadingElement || !linksElement) return;
try {
const response = await fetch(`/api/stream/${fileHash}/qualities`, {
credentials: 'include'
});
let availableQualities = [];
if (response.ok) {
availableQualities = await response.json() || [];
}
// Build quality links HTML
let qualityHTML = `
<div class="quality-item">
<div class="quality-info">
<strong>Auto Quality (Default)</strong>
<span>Best available quality</span>
</div>
<div class="quality-actions">
<a href="/api/stream/${fileHash}" target="_blank" class="stream-btn">Stream</a>
<button onclick="copyToClipboard('${window.location.origin}/api/stream/${fileHash}')" class="copy-btn">Copy URL</button>
</div>
</div>
`;
// Add specific quality versions
availableQualities.forEach(quality => {
qualityHTML += `
<div class="quality-item">
<div class="quality-info">
<strong>${quality.name || 'Unknown'}</strong>
<span>${quality.width || '?'}×${quality.height || '?'}, ${quality.bitrate || '?'}</span>
</div>
<div class="quality-actions">
<a href="/api/stream/${fileHash}/${quality.name}" target="_blank" class="stream-btn">Stream</a>
<button onclick="copyToClipboard('${window.location.origin}/api/stream/${fileHash}/${quality.name}')" class="copy-btn">Copy URL</button>
</div>
</div>
`;
});
linksElement.innerHTML = qualityHTML;
loadingElement.style.display = 'none';
linksElement.style.display = 'block';
} catch (error) {
console.error('Failed to load video qualities:', error);
loadingElement.innerHTML = '<p class="error">Failed to load video qualities. Using default stream only.</p>';
// Show fallback with just the default stream
linksElement.innerHTML = `
<div class="quality-item">
<div class="quality-info">
<strong>Default Stream</strong>
<span>Direct MP4 streaming</span>
</div>
<div class="quality-actions">
<a href="/api/stream/${fileHash}" target="_blank" class="stream-btn">Stream</a>
<button onclick="copyToClipboard('${window.location.origin}/api/stream/${fileHash}')" class="copy-btn">Copy URL</button>
</div>
</div>
`;
linksElement.style.display = 'block';
}
}
// 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 = '🔴';
});
}
}
// Enhanced service stats function for admin users
async function loadEnhancedServiceStats() {
try {
// Load basic stats first
await loadServiceStats();
// Then load enhanced stats
const response = await fetch('/api/admin/stats/enhanced', {
credentials: 'include',
headers: {
'Authorization': `Bearer ${window.nostrAuth.sessionToken}`
}
});
if (!response.ok) {
console.error('Failed to load enhanced stats:', response.statusText);
return;
}
const data = await response.json();
// Update performance metrics
if (data.performance) {
updateElement('perf-avg-response-time', (data.performance.avg_response_time || 0) + 'ms');
updateElement('perf-requests-per-second', data.performance.requests_per_second || 0);
updateElement('perf-cpu-usage', (data.performance.cpu_usage || 0) + '%');
updateElement('perf-memory-usage', formatBytes(data.performance.memory_usage || 0));
updateElement('perf-goroutines', data.performance.goroutines || 0);
updateElement('perf-gc-pause', ((data.performance.gc_pause_ns || 0) / 1000000) + 'ms');
}
// Update streaming analytics
if (data.streaming) {
updateElement('stream-active-streams', data.streaming.active_streams || 0);
updateElement('stream-total-bandwidth', formatBytes(data.streaming.total_bandwidth_bytes || 0) + '/s');
updateElement('stream-hls-requests', data.streaming.hls_requests_today || 0);
updateElement('stream-avg-bitrate', (data.streaming.avg_bitrate || 0) + ' Kbps');
updateElement('stream-concurrent-viewers', data.streaming.concurrent_viewers || 0);
updateElement('stream-transcoded-files', data.streaming.transcoded_files || 0);
updateElement('stream-failed-transcodes', data.streaming.failed_transcodes || 0);
}
// Update P2P health metrics
if (data.p2p) {
updateElement('p2p-health-score', (data.p2p.health_score || 0) + '%');
updateElement('p2p-active-peers', data.p2p.active_peers || 0);
updateElement('p2p-seeding-ratio', (data.p2p.seeding_ratio || 0).toFixed(2) + ':1');
updateElement('p2p-bandwidth-in', formatBytes(data.p2p.bandwidth_in_bytes || 0) + '/s');
updateElement('p2p-bandwidth-out', formatBytes(data.p2p.bandwidth_out_bytes || 0) + '/s');
updateElement('p2p-total-shared', formatBytes(data.p2p.total_shared_bytes || 0));
updateElement('p2p-pieces-shared', data.p2p.pieces_shared_today || 0);
}
// Update WebTorrent stats
if (data.webtorrent) {
updateElement('wt-active-connections', data.webtorrent.active_connections || 0);
updateElement('wt-webseed-requests', data.webtorrent.webseed_requests_today || 0);
updateElement('wt-bytes-served', formatBytes(data.webtorrent.bytes_served_today || 0));
updateElement('wt-avg-speed', formatBytes(data.webtorrent.avg_download_speed || 0) + '/s');
updateElement('wt-browser-clients', data.webtorrent.browser_clients || 0);
updateElement('wt-success-rate', ((data.webtorrent.success_rate || 0) * 100).toFixed(1) + '%');
}
// Update storage efficiency metrics
if (data.storage) {
updateElement('storage-dedup-ratio', ((data.storage.deduplication_ratio || 0) * 100).toFixed(1) + '%');
updateElement('storage-space-saved', formatBytes(data.storage.space_saved_bytes || 0));
updateElement('storage-chunk-efficiency', ((data.storage.chunk_utilization || 0) * 100).toFixed(1) + '%');
updateElement('storage-blob-files', data.storage.blob_files || 0);
updateElement('storage-chunked-files', data.storage.chunked_files || 0);
updateElement('storage-total-chunks', data.storage.total_chunks || 0);
updateElement('storage-unique-chunks', data.storage.unique_chunks || 0);
updateElement('storage-avg-chunk-size', formatBytes(data.storage.avg_chunk_size || 0));
}
// Update system health
if (data.system) {
updateElement('sys-disk-usage', ((data.system.disk_usage || 0) * 100).toFixed(1) + '%');
updateElement('sys-network-io', formatBytes(data.system.network_io_bytes || 0) + '/s');
updateElement('sys-open-files', data.system.open_files || 0);
updateElement('sys-load-average', (data.system.load_average || 0).toFixed(2));
// Update FFmpeg status with color coding
const ffmpegEl = document.getElementById('sys-ffmpeg-status');
if (ffmpegEl) {
ffmpegEl.textContent = data.system.ffmpeg_status || 'Unknown';
ffmpegEl.className = (data.system.ffmpeg_status === 'Available') ? 'status-ok' : 'status-error';
}
// Update transcoding queue
updateElement('sys-transcode-queue', data.system.transcoding_queue || 0);
updateElement('sys-transcode-storage', formatBytes(data.system.transcoding_storage_bytes || 0));
}
} catch (error) {
console.error('Failed to load enhanced service stats:', error);
showToast('Failed to load enhanced stats', 'error');
}
}
// Helper function to safely update elements
function updateElement(id, value) {
const element = document.getElementById(id);
if (element) {
element.textContent = value || 'N/A';
}
}
function refreshDHTStats() {
if (document.getElementById('services-section').classList.contains('active')) {
loadEnhancedServiceStats();
} else {
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')) {
loadEnhancedServiceStats();
}
}, 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';
}
}
function toggleMobileMenu() {
const nav = document.getElementById('main-nav');
const toggle = document.getElementById('mobile-menu-toggle');
nav.classList.toggle('mobile-nav-open');
toggle.classList.toggle('active');
}
function closeShareModal() {
document.getElementById('share-modal').style.display = 'none';
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
showToast('Copied to clipboard!', 'success');
}).catch(() => {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
showToast('Copied to clipboard!', 'success');
});
}
// Add modal close on outside click
window.onclick = function(event) {
const shareModal = document.getElementById('share-modal');
if (event.target == shareModal) {
shareModal.style.display = 'none';
}
}
</script>
<!-- Share Modal -->
<div id="share-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>Share File</h3>
<button class="close-btn" onclick="closeShareModal()">&times;</button>
</div>
<div class="modal-body">
<h4 id="share-file-name"></h4>
<div id="share-links"></div>
</div>
</div>
</div>
</body>
</html>