admin page updates and branding update
Some checks failed
CI Pipeline / Run Tests (push) Has been cancelled
CI Pipeline / Lint Code (push) Has been cancelled
CI Pipeline / Security Scan (push) Has been cancelled
CI Pipeline / Build Docker Images (push) Has been cancelled
CI Pipeline / E2E Tests (push) Has been cancelled
Some checks failed
CI Pipeline / Run Tests (push) Has been cancelled
CI Pipeline / Lint Code (push) Has been cancelled
CI Pipeline / Security Scan (push) Has been cancelled
CI Pipeline / Build Docker Images (push) Has been cancelled
CI Pipeline / E2E Tests (push) Has been cancelled
This commit is contained in:
parent
958d4e61b4
commit
03c2af56ab
@ -105,3 +105,25 @@ rate_limiting:
|
|||||||
auth:
|
auth:
|
||||||
login_attempts_per_minute: 10 # Login attempts per IP per minute
|
login_attempts_per_minute: 10 # Login attempts per IP per minute
|
||||||
burst_size: 5 # Login burst allowance per IP
|
burst_size: 5 # Login burst allowance per IP
|
||||||
|
|
||||||
|
# Site branding configuration
|
||||||
|
branding:
|
||||||
|
site_name: "Sovbit Gateway" # Site name shown in headers/titles
|
||||||
|
logo_url: "https://files.sovbit.host/media/44dc1c2db9c3fbd7bee9257eceb52be3cf8c40baf7b63f46e56b58a131c74f0b/7c96148e8244bfdb0294f44824267b6b08f2dcc29db8c6571d88ab8e22a1abf1.webp" # URL to your logo image (optional)
|
||||||
|
logo_width: "auto" # Logo width (e.g., "150px", "auto")
|
||||||
|
logo_height: "32px" # Logo height (e.g., "32px", "auto")
|
||||||
|
favicon_url: "" # URL to your favicon (optional)
|
||||||
|
description: "Decentralized file sharing gateway" # Site description
|
||||||
|
footer_text: "" # Footer text (optional)
|
||||||
|
support_url: "" # Support/contact URL (optional)
|
||||||
|
|
||||||
|
# Example sovbit.host configuration:
|
||||||
|
# branding:
|
||||||
|
# site_name: "sovbit.host"
|
||||||
|
# logo_url: "https://sovbit.host/logo.png"
|
||||||
|
# logo_width: "120px"
|
||||||
|
# logo_height: "30px"
|
||||||
|
# favicon_url: "https://sovbit.host/favicon.ico"
|
||||||
|
# description: "Sovereign Bitcoin hosting services"
|
||||||
|
# footer_text: "Powered by sovbit.host"
|
||||||
|
# support_url: "https://sovbit.host/support"
|
||||||
|
@ -210,18 +210,7 @@ func (ah *AdminHandlers) AdminUsersHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
users = append(users, user)
|
users = append(users, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch profile metadata for all users
|
// Skip profile fetching - let frontend handle it asynchronously
|
||||||
pubkeys := make([]string, len(users))
|
|
||||||
for i, user := range users {
|
|
||||||
pubkeys[i] = user.Pubkey
|
|
||||||
}
|
|
||||||
|
|
||||||
profiles := ah.profileFetcher.GetBatchProfiles(pubkeys)
|
|
||||||
for i := range users {
|
|
||||||
if profile, exists := profiles[users[i].Pubkey]; exists {
|
|
||||||
users[i].Profile = profile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log admin action
|
// Log admin action
|
||||||
ah.adminAuth.LogAdminAction(adminPubkey, "view_users", "", "Admin viewed user list")
|
ah.adminAuth.LogAdminAction(adminPubkey, "view_users", "", "Admin viewed user list")
|
||||||
@ -305,24 +294,7 @@ func (ah *AdminHandlers) AdminFilesHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
files = append(files, file)
|
files = append(files, file)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch profile metadata for file owners
|
// Skip profile fetching - let frontend handle it asynchronously
|
||||||
ownerPubkeys := make([]string, 0)
|
|
||||||
for _, file := range files {
|
|
||||||
if file.OwnerPubkey != "" {
|
|
||||||
ownerPubkeys = append(ownerPubkeys, file.OwnerPubkey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ownerPubkeys) > 0 {
|
|
||||||
profiles := ah.profileFetcher.GetBatchProfiles(ownerPubkeys)
|
|
||||||
for i := range files {
|
|
||||||
if files[i].OwnerPubkey != "" {
|
|
||||||
if profile, exists := profiles[files[i].OwnerPubkey]; exists {
|
|
||||||
files[i].OwnerProfile = profile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log admin action
|
// Log admin action
|
||||||
ah.adminAuth.LogAdminAction(adminPubkey, "view_files", "", "Admin viewed file list")
|
ah.adminAuth.LogAdminAction(adminPubkey, "view_files", "", "Admin viewed file list")
|
||||||
|
@ -2729,6 +2729,9 @@ func RegisterRoutes(r *mux.Router, cfg *config.Config, storage *storage.Backend)
|
|||||||
// System stats endpoint (public)
|
// System stats endpoint (public)
|
||||||
r.HandleFunc("/stats", systemStatsHandler(storage, trackerInstance)).Methods("GET")
|
r.HandleFunc("/stats", systemStatsHandler(storage, trackerInstance)).Methods("GET")
|
||||||
|
|
||||||
|
// Branding configuration endpoint (public)
|
||||||
|
r.HandleFunc("/branding", brandingHandler(cfg)).Methods("GET")
|
||||||
|
|
||||||
// DHT stats endpoint (public)
|
// DHT stats endpoint (public)
|
||||||
r.HandleFunc("/dht/stats", gateway.DHTStatsHandler).Methods("GET")
|
r.HandleFunc("/dht/stats", gateway.DHTStatsHandler).Methods("GET")
|
||||||
|
|
||||||
@ -3129,6 +3132,27 @@ func healthHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func brandingHandler(cfg *config.Config) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
// Return branding configuration
|
||||||
|
branding := map[string]string{
|
||||||
|
"site_name": cfg.Branding.SiteName,
|
||||||
|
"logo_url": cfg.Branding.LogoURL,
|
||||||
|
"logo_width": cfg.Branding.LogoWidth,
|
||||||
|
"logo_height": cfg.Branding.LogoHeight,
|
||||||
|
"favicon_url": cfg.Branding.FaviconURL,
|
||||||
|
"description": cfg.Branding.Description,
|
||||||
|
"footer_text": cfg.Branding.FooterText,
|
||||||
|
"support_url": cfg.Branding.SupportURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(branding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func systemStatsHandler(storage *storage.Backend, trackerInstance *tracker.Tracker) http.HandlerFunc {
|
func systemStatsHandler(storage *storage.Backend, trackerInstance *tracker.Tracker) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
@ -23,6 +23,7 @@ type Config struct {
|
|||||||
Proxy ProxyConfig `yaml:"proxy"`
|
Proxy ProxyConfig `yaml:"proxy"`
|
||||||
Admin AdminConfig `yaml:"admin"`
|
Admin AdminConfig `yaml:"admin"`
|
||||||
RateLimiting RateLimitingConfig `yaml:"rate_limiting"`
|
RateLimiting RateLimitingConfig `yaml:"rate_limiting"`
|
||||||
|
Branding BrandingConfig `yaml:"branding"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GatewayConfig configures the HTTP API gateway
|
// GatewayConfig configures the HTTP API gateway
|
||||||
@ -155,6 +156,18 @@ type AuthRateConfig struct {
|
|||||||
BurstSize int `yaml:"burst_size"`
|
BurstSize int `yaml:"burst_size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BrandingConfig configures site branding and appearance
|
||||||
|
type BrandingConfig struct {
|
||||||
|
SiteName string `yaml:"site_name"`
|
||||||
|
LogoURL string `yaml:"logo_url"`
|
||||||
|
LogoWidth string `yaml:"logo_width"`
|
||||||
|
LogoHeight string `yaml:"logo_height"`
|
||||||
|
FaviconURL string `yaml:"favicon_url"`
|
||||||
|
Description string `yaml:"description"`
|
||||||
|
FooterText string `yaml:"footer_text"`
|
||||||
|
SupportURL string `yaml:"support_url"`
|
||||||
|
}
|
||||||
|
|
||||||
// LoadConfig loads configuration from a YAML file
|
// LoadConfig loads configuration from a YAML file
|
||||||
func LoadConfig(filename string) (*Config, error) {
|
func LoadConfig(filename string) (*Config, error) {
|
||||||
data, err := os.ReadFile(filename)
|
data, err := os.ReadFile(filename)
|
||||||
@ -172,6 +185,14 @@ func LoadConfig(filename string) (*Config, error) {
|
|||||||
config.Mode = "unified"
|
config.Mode = "unified"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set branding defaults
|
||||||
|
if config.Branding.SiteName == "" {
|
||||||
|
config.Branding.SiteName = "BitTorrent Gateway"
|
||||||
|
}
|
||||||
|
if config.Branding.Description == "" {
|
||||||
|
config.Branding.Description = "Decentralized file sharing gateway"
|
||||||
|
}
|
||||||
|
|
||||||
return &config, nil
|
return &config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +64,26 @@
|
|||||||
padding: 12px;
|
padding: 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table th:first-child,
|
||||||
|
.admin-table td:first-child {
|
||||||
|
max-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table .hash-short {
|
||||||
|
max-width: 120px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-table .owner-cell,
|
||||||
|
.admin-table .user-profile-cell {
|
||||||
|
max-width: 180px;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-table th {
|
.admin-table th {
|
||||||
@ -143,16 +163,12 @@
|
|||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hash-short {
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header>
|
<header>
|
||||||
<h1>🛡️ Admin Dashboard</h1>
|
<h1 id="admin-site-title">🛡️ Admin Dashboard</h1>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/">← Back to Gateway</a>
|
<a href="/">← Back to Gateway</a>
|
||||||
<div id="admin-auth-status" class="auth-status">
|
<div id="admin-auth-status" class="auth-status">
|
||||||
@ -575,6 +591,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
|
const tbody = document.getElementById('users-table');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await makeAdminRequest('/api/admin/users');
|
const response = await makeAdminRequest('/api/admin/users');
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@ -585,15 +603,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const users = Array.isArray(data) ? data : [];
|
const users = Array.isArray(data) ? data : [];
|
||||||
const tbody = document.getElementById('users-table');
|
|
||||||
|
// Render immediately with fallback display
|
||||||
tbody.innerHTML = users.map(user => {
|
tbody.innerHTML = users.map(user => {
|
||||||
const displayName = user.profile?.display_name || user.profile?.name || user.display_name || 'Anonymous';
|
const displayName = user.display_name || (user.pubkey.substring(0, 8) + '...');
|
||||||
const profilePic = user.profile?.picture ? `<img src="${user.profile.picture}" style="width: 24px; height: 24px; border-radius: 50%; margin-right: 8px;">` : '';
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr data-user-pubkey="${user.pubkey}">
|
||||||
<td class="hash-short">${user.pubkey.substring(0, 16)}...</td>
|
<td class="hash-short">${user.pubkey.substring(0, 16)}...</td>
|
||||||
<td>${profilePic}${displayName}</td>
|
<td class="user-profile-cell" data-pubkey="${user.pubkey}">${displayName}</td>
|
||||||
<td>${user.file_count}</td>
|
<td>${user.file_count}</td>
|
||||||
<td>${formatBytes(user.storage_used)}</td>
|
<td>${formatBytes(user.storage_used)}</td>
|
||||||
<td>${new Date(user.last_login).toLocaleDateString()}</td>
|
<td>${new Date(user.last_login).toLocaleDateString()}</td>
|
||||||
@ -611,12 +629,94 @@
|
|||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
// Fetch profiles in background
|
||||||
|
const pubkeys = users.map(user => user.pubkey);
|
||||||
|
if (pubkeys.length > 0) {
|
||||||
|
fetchUsersProfiles(pubkeys);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast('Failed to load users: ' + error.message, 'error');
|
showToast('Failed to load users: ' + error.message, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchUsersProfiles(pubkeys, retryCount = 0) {
|
||||||
|
const maxRetries = 2;
|
||||||
|
const timeout = 4000;
|
||||||
|
|
||||||
|
if (pubkeys.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Fetching user profiles (attempt ${retryCount + 1}/${maxRetries})...`);
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||||
|
|
||||||
|
const promises = pubkeys.slice(0, 10).map(async pubkey => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/profile/${pubkey}`, {
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success && data.profile) {
|
||||||
|
return { pubkey, profile: data.profile };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Individual profile fetch failed, skip it
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(promises);
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
// Update UI with successful profile fetches
|
||||||
|
results.forEach(result => {
|
||||||
|
if (result.status === 'fulfilled' && result.value) {
|
||||||
|
const { pubkey, profile } = result.value;
|
||||||
|
updateUserProfile(pubkey, profile);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process remaining pubkeys if we had to limit batch size
|
||||||
|
if (pubkeys.length > 10) {
|
||||||
|
setTimeout(() => {
|
||||||
|
fetchUsersProfiles(pubkeys.slice(10), retryCount);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`User profile fetch attempt ${retryCount + 1} failed:`, error.message);
|
||||||
|
|
||||||
|
if (retryCount < maxRetries - 1) {
|
||||||
|
const delay = Math.pow(2, retryCount) * 1000;
|
||||||
|
setTimeout(() => {
|
||||||
|
fetchUsersProfiles(pubkeys, retryCount + 1);
|
||||||
|
}, delay);
|
||||||
|
} else {
|
||||||
|
console.log('User profile fetching failed after all retries');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUserProfile(pubkey, profile) {
|
||||||
|
const profileCells = document.querySelectorAll(`[data-pubkey="${pubkey}"].user-profile-cell`);
|
||||||
|
const displayName = profile.display_name || profile.name || (pubkey.substring(0, 8) + '...');
|
||||||
|
const profilePic = profile.picture ?
|
||||||
|
`<img src="${profile.picture}" style="width: 24px; height: 24px; border-radius: 50%; margin-right: 8px;">` : '';
|
||||||
|
|
||||||
|
profileCells.forEach(cell => {
|
||||||
|
cell.innerHTML = `${profilePic}${displayName}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function loadFiles() {
|
async function loadFiles() {
|
||||||
|
const tbody = document.getElementById('files-table');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const storageFilter = document.getElementById('file-storage-filter').value;
|
const storageFilter = document.getElementById('file-storage-filter').value;
|
||||||
const accessFilter = document.getElementById('file-access-filter').value;
|
const accessFilter = document.getElementById('file-access-filter').value;
|
||||||
@ -634,15 +734,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const files = Array.isArray(data) ? data : [];
|
const files = Array.isArray(data) ? data : [];
|
||||||
const tbody = document.getElementById('files-table');
|
|
||||||
|
// Render immediately with fallback display
|
||||||
tbody.innerHTML = files.map(file => {
|
tbody.innerHTML = files.map(file => {
|
||||||
const ownerName = file.owner_profile?.display_name || file.owner_profile?.name ||
|
const ownerName = file.owner_pubkey ? file.owner_pubkey.substring(0, 8) + '...' : 'System';
|
||||||
(file.owner_pubkey ? file.owner_pubkey.substring(0, 8) + '...' : 'System');
|
|
||||||
const ownerPic = file.owner_profile?.picture ?
|
|
||||||
`<img src="${file.owner_profile.picture}" style="width: 20px; height: 20px; border-radius: 50%; margin-right: 6px;">` : '';
|
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr data-file-hash="${file.hash}">
|
||||||
<td><input type="checkbox" class="file-select" value="${file.hash}"></td>
|
<td><input type="checkbox" class="file-select" value="${file.hash}"></td>
|
||||||
<td>${file.name}</td>
|
<td>${file.name}</td>
|
||||||
<td class="hash-short">${file.hash.substring(0, 12)}...</td>
|
<td class="hash-short">${file.hash.substring(0, 12)}...</td>
|
||||||
@ -653,7 +751,7 @@
|
|||||||
${file.access_level}
|
${file.access_level}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>${ownerPic}${ownerName}</td>
|
<td class="owner-cell" data-pubkey="${file.owner_pubkey || ''}">${ownerName}</td>
|
||||||
<td>${file.report_count > 0 ? `<span class="status-badge pending">${file.report_count}</span>` : '0'}</td>
|
<td>${file.report_count > 0 ? `<span class="status-badge pending">${file.report_count}</span>` : '0'}</td>
|
||||||
<td>
|
<td>
|
||||||
<button class="action-btn danger" onclick="deleteFile('${file.hash}', '${file.name}')">Delete</button>
|
<button class="action-btn danger" onclick="deleteFile('${file.hash}', '${file.name}')">Delete</button>
|
||||||
@ -661,10 +759,90 @@
|
|||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
// Fetch profiles in background for files with owners
|
||||||
|
const ownerPubkeys = files.filter(file => file.owner_pubkey).map(file => file.owner_pubkey);
|
||||||
|
if (ownerPubkeys.length > 0) {
|
||||||
|
fetchFileOwnersProfiles(ownerPubkeys);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast('Failed to load files: ' + error.message, 'error');
|
showToast('Failed to load files: ' + error.message, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchFileOwnersProfiles(pubkeys, retryCount = 0) {
|
||||||
|
const maxRetries = 2;
|
||||||
|
const timeout = 4000;
|
||||||
|
|
||||||
|
if (pubkeys.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Fetching file owner profiles (attempt ${retryCount + 1}/${maxRetries})...`);
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||||
|
|
||||||
|
const promises = pubkeys.slice(0, 10).map(async pubkey => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/profile/${pubkey}`, {
|
||||||
|
signal: controller.signal
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success && data.profile) {
|
||||||
|
return { pubkey, profile: data.profile };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Individual profile fetch failed, skip it
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(promises);
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
// Update UI with successful profile fetches
|
||||||
|
results.forEach(result => {
|
||||||
|
if (result.status === 'fulfilled' && result.value) {
|
||||||
|
const { pubkey, profile } = result.value;
|
||||||
|
updateFileOwnerProfile(pubkey, profile);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process remaining pubkeys if we had to limit batch size
|
||||||
|
if (pubkeys.length > 10) {
|
||||||
|
setTimeout(() => {
|
||||||
|
fetchFileOwnersProfiles(pubkeys.slice(10), retryCount);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`File owner profile fetch attempt ${retryCount + 1} failed:`, error.message);
|
||||||
|
|
||||||
|
if (retryCount < maxRetries - 1) {
|
||||||
|
const delay = Math.pow(2, retryCount) * 1000;
|
||||||
|
setTimeout(() => {
|
||||||
|
fetchFileOwnersProfiles(pubkeys, retryCount + 1);
|
||||||
|
}, delay);
|
||||||
|
} else {
|
||||||
|
console.log('File owner profile fetching failed after all retries');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFileOwnerProfile(pubkey, profile) {
|
||||||
|
const ownerCells = document.querySelectorAll(`[data-pubkey="${pubkey}"]`);
|
||||||
|
const displayName = profile.display_name || profile.name || (pubkey.substring(0, 8) + '...');
|
||||||
|
const ownerPic = profile.picture ?
|
||||||
|
`<img src="${profile.picture}" style="width: 20px; height: 20px; border-radius: 50%; margin-right: 6px;">` : '';
|
||||||
|
|
||||||
|
ownerCells.forEach(cell => {
|
||||||
|
cell.innerHTML = `${ownerPic}${displayName}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function loadReports() {
|
async function loadReports() {
|
||||||
try {
|
try {
|
||||||
@ -887,8 +1065,55 @@
|
|||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load branding configuration
|
||||||
|
async function loadAdminBranding() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/branding');
|
||||||
|
if (response.ok) {
|
||||||
|
const branding = await response.json();
|
||||||
|
|
||||||
|
// Update admin dashboard title
|
||||||
|
if (branding.site_name) {
|
||||||
|
document.getElementById('admin-site-title').textContent = `🛡️ ${branding.site_name} Admin`;
|
||||||
|
document.title = `${branding.site_name} Admin`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add logo if configured
|
||||||
|
if (branding.logo_url) {
|
||||||
|
const title = document.getElementById('admin-site-title');
|
||||||
|
const logo = document.createElement('img');
|
||||||
|
logo.src = branding.logo_url;
|
||||||
|
logo.alt = branding.site_name || 'Logo';
|
||||||
|
logo.style.height = '28px';
|
||||||
|
logo.style.width = 'auto';
|
||||||
|
logo.style.marginRight = '10px';
|
||||||
|
logo.style.verticalAlign = 'middle';
|
||||||
|
|
||||||
|
// Replace text with logo + text
|
||||||
|
title.innerHTML = '';
|
||||||
|
title.appendChild(logo);
|
||||||
|
title.appendChild(document.createTextNode(`🛡️ ${branding.site_name || 'BitTorrent Gateway'} Admin`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Could not load branding configuration for admin, using defaults');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadAdminBranding();
|
||||||
// Admin login event listener
|
// Admin login event listener
|
||||||
document.getElementById('admin-nip07-login').addEventListener('click', async () => {
|
document.getElementById('admin-nip07-login').addEventListener('click', async () => {
|
||||||
const result = await window.nostrAuth.loginNIP07();
|
const result = await window.nostrAuth.loginNIP07();
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header>
|
<header>
|
||||||
<h1>⚡ BitTorrent Gateway</h1>
|
<h1 id="site-title">⚡ BitTorrent Gateway</h1>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="#about" onclick="showAbout(); return false;">About</a>
|
<a href="#about" onclick="showAbout(); return false;">About</a>
|
||||||
<a href="#services" onclick="showServices(); return false;">Server Stats</a>
|
<a href="#services" onclick="showServices(); return false;">Server Stats</a>
|
||||||
@ -1537,8 +1537,63 @@
|
|||||||
}, 5000);
|
}, 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
|
// Initialize
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
loadBranding();
|
||||||
updateAuthStatus();
|
updateAuthStatus();
|
||||||
|
|
||||||
// Login event listeners
|
// Login event listeners
|
||||||
|
Loading…
x
Reference in New Issue
Block a user