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:
|
||||
login_attempts_per_minute: 10 # Login attempts per IP per minute
|
||||
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)
|
||||
}
|
||||
|
||||
// Fetch profile metadata for all users
|
||||
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
|
||||
}
|
||||
}
|
||||
// Skip profile fetching - let frontend handle it asynchronously
|
||||
|
||||
// Log admin action
|
||||
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)
|
||||
}
|
||||
|
||||
// Fetch profile metadata for file owners
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Skip profile fetching - let frontend handle it asynchronously
|
||||
|
||||
// Log admin action
|
||||
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)
|
||||
r.HandleFunc("/stats", systemStatsHandler(storage, trackerInstance)).Methods("GET")
|
||||
|
||||
// Branding configuration endpoint (public)
|
||||
r.HandleFunc("/branding", brandingHandler(cfg)).Methods("GET")
|
||||
|
||||
// DHT stats endpoint (public)
|
||||
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"})
|
||||
}
|
||||
|
||||
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 {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
@ -23,6 +23,7 @@ type Config struct {
|
||||
Proxy ProxyConfig `yaml:"proxy"`
|
||||
Admin AdminConfig `yaml:"admin"`
|
||||
RateLimiting RateLimitingConfig `yaml:"rate_limiting"`
|
||||
Branding BrandingConfig `yaml:"branding"`
|
||||
}
|
||||
|
||||
// GatewayConfig configures the HTTP API gateway
|
||||
@ -155,6 +156,18 @@ type AuthRateConfig struct {
|
||||
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
|
||||
func LoadConfig(filename string) (*Config, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
@ -172,6 +185,14 @@ func LoadConfig(filename string) (*Config, error) {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -64,6 +64,26 @@
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
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 {
|
||||
@ -143,16 +163,12 @@
|
||||
color: black;
|
||||
}
|
||||
|
||||
.hash-short {
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>🛡️ Admin Dashboard</h1>
|
||||
<h1 id="admin-site-title">🛡️ Admin Dashboard</h1>
|
||||
<nav>
|
||||
<a href="/">← Back to Gateway</a>
|
||||
<div id="admin-auth-status" class="auth-status">
|
||||
@ -575,6 +591,8 @@
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
const tbody = document.getElementById('users-table');
|
||||
|
||||
try {
|
||||
const response = await makeAdminRequest('/api/admin/users');
|
||||
const data = await response.json();
|
||||
@ -585,15 +603,15 @@
|
||||
}
|
||||
|
||||
const users = Array.isArray(data) ? data : [];
|
||||
const tbody = document.getElementById('users-table');
|
||||
|
||||
// Render immediately with fallback display
|
||||
tbody.innerHTML = users.map(user => {
|
||||
const displayName = user.profile?.display_name || user.profile?.name || user.display_name || 'Anonymous';
|
||||
const profilePic = user.profile?.picture ? `<img src="${user.profile.picture}" style="width: 24px; height: 24px; border-radius: 50%; margin-right: 8px;">` : '';
|
||||
const displayName = user.display_name || (user.pubkey.substring(0, 8) + '...');
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<tr data-user-pubkey="${user.pubkey}">
|
||||
<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>${formatBytes(user.storage_used)}</td>
|
||||
<td>${new Date(user.last_login).toLocaleDateString()}</td>
|
||||
@ -611,12 +629,94 @@
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Fetch profiles in background
|
||||
const pubkeys = users.map(user => user.pubkey);
|
||||
if (pubkeys.length > 0) {
|
||||
fetchUsersProfiles(pubkeys);
|
||||
}
|
||||
|
||||
} catch (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() {
|
||||
const tbody = document.getElementById('files-table');
|
||||
|
||||
try {
|
||||
const storageFilter = document.getElementById('file-storage-filter').value;
|
||||
const accessFilter = document.getElementById('file-access-filter').value;
|
||||
@ -634,15 +734,13 @@
|
||||
}
|
||||
|
||||
const files = Array.isArray(data) ? data : [];
|
||||
const tbody = document.getElementById('files-table');
|
||||
|
||||
// Render immediately with fallback display
|
||||
tbody.innerHTML = files.map(file => {
|
||||
const ownerName = file.owner_profile?.display_name || file.owner_profile?.name ||
|
||||
(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;">` : '';
|
||||
const ownerName = file.owner_pubkey ? file.owner_pubkey.substring(0, 8) + '...' : 'System';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<tr data-file-hash="${file.hash}">
|
||||
<td><input type="checkbox" class="file-select" value="${file.hash}"></td>
|
||||
<td>${file.name}</td>
|
||||
<td class="hash-short">${file.hash.substring(0, 12)}...</td>
|
||||
@ -653,7 +751,7 @@
|
||||
${file.access_level}
|
||||
</span>
|
||||
</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>
|
||||
<button class="action-btn danger" onclick="deleteFile('${file.hash}', '${file.name}')">Delete</button>
|
||||
@ -661,11 +759,91 @@
|
||||
</tr>
|
||||
`;
|
||||
}).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) {
|
||||
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() {
|
||||
try {
|
||||
const statusFilter = document.getElementById('report-status-filter').value;
|
||||
@ -887,8 +1065,55 @@
|
||||
}, 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
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadAdminBranding();
|
||||
// Admin login event listener
|
||||
document.getElementById('admin-nip07-login').addEventListener('click', async () => {
|
||||
const result = await window.nostrAuth.loginNIP07();
|
||||
|
@ -9,7 +9,7 @@
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>⚡ BitTorrent Gateway</h1>
|
||||
<h1 id="site-title">⚡ BitTorrent Gateway</h1>
|
||||
<nav>
|
||||
<a href="#about" onclick="showAbout(); return false;">About</a>
|
||||
<a href="#services" onclick="showServices(); return false;">Server Stats</a>
|
||||
@ -1537,8 +1537,63 @@
|
||||
}, 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
|
||||
|
Loading…
x
Reference in New Issue
Block a user