This commit is contained in:
oib
2025-07-20 09:26:07 +02:00
parent da28b205e5
commit ab9d93d913
19 changed files with 1207 additions and 419 deletions

View File

@ -37,7 +37,7 @@ function handleMagicLoginRedirect() {
localStorage.setItem('uid', username);
localStorage.setItem('confirmed_uid', username);
localStorage.setItem('uid_time', Date.now().toString());
document.cookie = `uid=${encodeURIComponent(username)}; path=/`;
document.cookie = `uid=${encodeURIComponent(username)}; path=/; SameSite=Lax`;
// Update UI state
document.body.classList.add('authenticated');
@ -45,7 +45,7 @@ function handleMagicLoginRedirect() {
// Update local storage and cookies
localStorage.setItem('isAuthenticated', 'true');
document.cookie = `isAuthenticated=true; path=/`;
document.cookie = `isAuthenticated=true; path=/; SameSite=Lax`;
// Update URL and history without reloading
window.history.replaceState({}, document.title, window.location.pathname);
@ -677,25 +677,170 @@ trackedFunctions.forEach(fnName => {
}
});
// Update the visibility of the account deletion section based on authentication state
function updateAccountDeletionVisibility(isAuthenticated) {
console.log('[ACCOUNT-DELETION] updateAccountDeletionVisibility called with isAuthenticated:', isAuthenticated);
// Find the account deletion section and its auth-only wrapper
const authOnlyWrapper = document.querySelector('#privacy-page .auth-only');
const accountDeletionSection = document.getElementById('account-deletion');
console.log('[ACCOUNT-DELETION] Elements found:', {
authOnlyWrapper: !!authOnlyWrapper,
accountDeletionSection: !!accountDeletionSection
});
// Function to show an element with all necessary styles
const showElement = (element) => {
if (!element) return;
console.log('[ACCOUNT-DELETION] Showing element:', element);
// Remove any hiding classes
element.classList.remove('hidden', 'auth-only-hidden');
// Set all possible visibility properties
element.style.display = 'block';
element.style.visibility = 'visible';
element.style.opacity = '1';
element.style.height = 'auto';
element.style.position = 'relative';
element.style.clip = 'auto';
element.style.overflow = 'visible';
// Add a class to mark as visible
element.classList.add('account-visible');
};
// Function to hide an element
const hideElement = (element) => {
if (!element) return;
console.log('[ACCOUNT-DELETION] Hiding element:', element);
// Set display to none to completely remove from layout
element.style.display = 'none';
// Remove any visibility-related classes
element.classList.remove('account-visible');
};
if (isAuthenticated) {
console.log('[ACCOUNT-DELETION] User is authenticated, checking if on privacy page');
// Get the current page state - only show on #privacy-page
const currentHash = window.location.hash;
const isPrivacyPage = currentHash === '#privacy-page';
console.log('[ACCOUNT-DELETION] Debug - Page State:', {
isAuthenticated,
currentHash,
isPrivacyPage,
documentTitle: document.title
});
if (isAuthenticated && isPrivacyPage) {
console.log('[ACCOUNT-DELETION] On privacy page, showing account deletion section');
// Show the auth wrapper and account deletion section
if (authOnlyWrapper) {
authOnlyWrapper.style.display = 'block';
authOnlyWrapper.style.visibility = 'visible';
}
if (accountDeletionSection) {
accountDeletionSection.style.display = 'block';
accountDeletionSection.style.visibility = 'visible';
}
} else {
console.log('[ACCOUNT-DELETION] Not on privacy page, hiding account deletion section');
// Hide the account deletion section
if (accountDeletionSection) {
accountDeletionSection.style.display = 'none';
accountDeletionSection.style.visibility = 'hidden';
}
// Only hide the auth wrapper if we're not on the privacy page
if (authOnlyWrapper && !isPrivacyPage) {
authOnlyWrapper.style.display = 'none';
authOnlyWrapper.style.visibility = 'hidden';
}
}
// Debug: Log the current state after updates
if (accountDeletionSection) {
console.log('[ACCOUNT-DELETION] Account deletion section state after show:', {
display: window.getComputedStyle(accountDeletionSection).display,
visibility: window.getComputedStyle(accountDeletionSection).visibility,
classes: accountDeletionSection.className,
parent: accountDeletionSection.parentElement ? {
tag: accountDeletionSection.parentElement.tagName,
classes: accountDeletionSection.parentElement.className,
display: window.getComputedStyle(accountDeletionSection.parentElement).display
} : 'no parent'
});
}
} else {
console.log('[ACCOUNT-DELETION] User is not authenticated, hiding account deletion section');
// Hide the account deletion section but keep the auth-only wrapper for other potential content
if (accountDeletionSection) {
hideElement(accountDeletionSection);
}
// Only hide the auth-only wrapper if it doesn't contain other important content
if (authOnlyWrapper) {
const hasOtherContent = Array.from(authOnlyWrapper.children).some(
child => child.id !== 'account-deletion' && child.offsetParent !== null
);
if (!hasOtherContent) {
hideElement(authOnlyWrapper);
}
}
}
// Log final state for debugging
console.log('[ACCOUNT-DELETION] Final state:', {
authOnlyWrapper: authOnlyWrapper ? {
display: window.getComputedStyle(authOnlyWrapper).display,
visibility: window.getComputedStyle(authOnlyWrapper).visibility,
classes: authOnlyWrapper.className
} : 'not found',
accountDeletionSection: accountDeletionSection ? {
display: window.getComputedStyle(accountDeletionSection).display,
visibility: window.getComputedStyle(accountDeletionSection).visibility,
classes: accountDeletionSection.className,
parent: accountDeletionSection.parentElement ? {
tag: accountDeletionSection.parentElement.tagName,
classes: accountDeletionSection.parentElement.className,
display: window.getComputedStyle(accountDeletionSection.parentElement).display
} : 'no parent'
} : 'not found'
});
}
// Check authentication state and update UI
function checkAuthState() {
// Debounce rapid calls
const now = Date.now();
// Throttle the checks
if (now - lastAuthCheckTime < AUTH_CHECK_DEBOUNCE) {
return;
return wasAuthenticated === true;
}
lastAuthCheckTime = now;
// Check various auth indicators
const hasAuthCookie = document.cookie.includes('sessionid=');
authCheckCounter++;
// Check various authentication indicators
const hasAuthCookie = document.cookie.includes('isAuthenticated=true');
const hasUidCookie = document.cookie.includes('uid=');
const hasLocalStorageAuth = localStorage.getItem('isAuthenticated') === 'true';
const hasAuthToken = localStorage.getItem('authToken') !== null;
const hasAuthToken = !!localStorage.getItem('authToken');
// User is considered authenticated if any of these are true
const isAuthenticated = hasAuthCookie || hasUidCookie || hasLocalStorageAuth || hasAuthToken;
// Only log if debug is enabled or if state has changed
if (DEBUG_AUTH_STATE || isAuthenticated !== wasAuthenticated) {
console.log('Auth State Check:', {
hasAuthCookie,
@ -729,6 +874,9 @@ function checkAuthState() {
console.warn('injectNavigation function not found');
}
// Update account deletion section visibility
updateAccountDeletionVisibility(isAuthenticated);
// Update the tracked state
wasAuthenticated = isAuthenticated;
@ -755,6 +903,12 @@ function setupAuthStatePolling() {
}
// Function to handle page navigation
function handlePageNavigation() {
const isAuthenticated = checkAuthState();
updateAccountDeletionVisibility(isAuthenticated);
}
// Initialize the application when DOM is loaded
document.addEventListener("DOMContentLoaded", () => {
// Set up authentication state monitoring
@ -766,6 +920,11 @@ document.addEventListener("DOMContentLoaded", () => {
// Initialize components
initNavigation();
// Initialize account deletion section visibility
handlePageNavigation();
// Listen for hash changes to update visibility when navigating
window.addEventListener('hashchange', handlePageNavigation);
// Initialize profile player after a short delay
setTimeout(() => {
@ -861,32 +1020,96 @@ document.addEventListener("DOMContentLoaded", () => {
const deleteAccountFromPrivacyBtn = document.getElementById('delete-account-from-privacy');
const deleteAccount = async (e) => {
if (e) e.preventDefault();
if (e) {
e.preventDefault();
e.stopPropagation();
}
if (!confirm('Are you sure you want to delete your account? This action cannot be undone.')) {
if (!confirm('Are you sure you want to delete your account?\n\nThis action cannot be undone.')) {
return;
}
// Show loading state
const deleteBtn = e?.target.closest('button');
const originalText = deleteBtn?.textContent || 'Delete My Account';
if (deleteBtn) {
deleteBtn.disabled = true;
deleteBtn.textContent = 'Deleting...';
}
try {
// Get UID from localStorage
const uid = localStorage.getItem('uid');
if (!uid) {
throw new Error('User not authenticated. Please log in again.');
}
console.log('Sending delete account request for UID:', uid);
const response = await fetch('/api/delete-account', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
},
credentials: 'include',
body: JSON.stringify({
uid: uid // Include UID in the request body
})
});
if (response.ok) {
// Clear local storage and redirect to home page
localStorage.clear();
window.location.href = '/';
} else {
const error = await response.json();
throw new Error(error.detail || 'Failed to delete account');
}
} catch (error) {
console.error('Error deleting account:', error);
showToast(`${error.message || 'Failed to delete account'}`, 'error');
console.log('Received response status:', response.status, response.statusText);
// Try to parse response as JSON, but handle non-JSON responses
let data;
const text = await response.text();
try {
data = text ? JSON.parse(text) : {};
} catch (parseError) {
console.error('Failed to parse response as JSON:', parseError);
console.log('Raw response text:', text);
data = {};
}
if (response.ok) {
console.log('Account deletion successful');
showToast('✅ Account deleted successfully', 'success');
// Clear local storage and redirect to home page after a short delay
setTimeout(() => {
localStorage.clear();
window.location.href = '/';
}, 1000);
} else {
console.error('Delete account failed:', { status: response.status, data });
const errorMessage = data.detail || data.message ||
data.error ||
`Server returned ${response.status} ${response.statusText}`;
throw new Error(errorMessage);
}
} catch (error) {
console.error('Error in deleteAccount:', {
name: error.name,
message: error.message,
stack: error.stack,
error: error
});
// Try to extract a meaningful error message
let errorMessage = 'Failed to delete account';
if (error instanceof Error) {
errorMessage = error.message || error.toString();
} else if (typeof error === 'string') {
errorMessage = error;
} else if (error && typeof error === 'object') {
errorMessage = error.message || JSON.stringify(error);
}
showToast(`${errorMessage}`, 'error');
} finally {
// Restore button state
if (deleteBtn) {
deleteBtn.disabled = false;
deleteBtn.textContent = originalText;
}
}
};
// Add event listeners to both delete account buttons
@ -902,22 +1125,49 @@ document.addEventListener("DOMContentLoaded", () => {
});
// Logout function
function logout() {
async function logout(event) {
if (event) {
event.preventDefault();
event.stopPropagation();
}
// If handleLogout is available in dashboard.js, use it for comprehensive logout
if (typeof handleLogout === 'function') {
try {
await handleLogout(event);
} catch (error) {
console.error('Error during logout:', error);
// Fall back to basic logout if handleLogout fails
basicLogout();
}
} else {
// Fallback to basic logout if handleLogout is not available
basicLogout();
}
}
// Basic client-side logout as fallback
function basicLogout() {
// Clear authentication state
document.body.classList.remove('authenticated');
localStorage.removeItem('isAuthenticated');
localStorage.removeItem('uid');
localStorage.removeItem('confirmed_uid');
localStorage.removeItem('uid_time');
localStorage.removeItem('authToken');
// Clear cookies
document.cookie = 'isAuthenticated=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
document.cookie = 'uid=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
// Clear all cookies with proper SameSite attribute
document.cookie.split(';').forEach(cookie => {
const [name] = cookie.trim().split('=');
if (name) {
document.cookie = `${name}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT; domain=${window.location.hostname}; SameSite=Lax`;
}
});
// Stop any playing audio
stopMainAudio();
// Redirect to home page
// Force a hard redirect to ensure all state is cleared
window.location.href = '/';
}

View File

@ -36,16 +36,78 @@ body.authenticated .auth-only {
#me-page:not([hidden]) > .auth-only,
#me-page:not([hidden]) > section,
#me-page:not([hidden]) > article,
#me-page:not([hidden]) > div,
/* Ensure account deletion section is visible when privacy page is active and user is authenticated */
#privacy-page:not([hidden]) .auth-only,
#privacy-page:not([hidden]) #account-deletion {
#me-page:not([hidden]) > div {
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
body.authenticated .guest-only {
/* Show auth-only elements when authenticated */
body.authenticated .auth-only {
display: block !important;
visibility: visible !important;
}
/* Account deletion section - improved width and formatting */
#account-deletion {
margin: 2.5rem auto;
padding: 2.5rem;
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
max-width: 600px;
line-height: 1.6;
color: var(--text-color);
}
#account-deletion h3 {
color: var(--color-primary);
margin-top: 0;
margin-bottom: 1.5rem;
font-size: 1.5rem;
}
#account-deletion p {
color: var(--color-text);
line-height: 1.6;
margin-bottom: 1.5rem;
}
#account-deletion ul {
margin: 1rem 0 1.5rem 1.5rem;
padding: 0;
color: var(--color-text);
}
#account-deletion .centered-container {
text-align: center;
margin-top: 2rem;
}
#delete-account-from-privacy {
background-color: #ff4d4f;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
font-size: 1rem;
transition: background-color 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
#delete-account-from-privacy:hover {
background-color: #ff6b6b;
text-decoration: none;
}
/* Hide guest-only elements when authenticated */
body.authenticated .guest-only {
display: none !important;
visibility: hidden !important;
display: none;
}

View File

@ -3,19 +3,22 @@ document.addEventListener('DOMContentLoaded', () => {
// Function to update the play button with UID
function updatePersonalStreamPlayButton() {
const playButton = document.querySelector('#me-page .play-pause-btn');
if (!playButton) return;
const streamPlayer = document.querySelector('#me-page .stream-player');
if (!playButton || !streamPlayer) return;
// Get UID from localStorage or cookie
const uid = localStorage.getItem('uid') || getCookie('uid');
if (uid) {
// Set the data-uid attribute if not already set
// Show the player and set the UID if not already set
streamPlayer.style.display = 'block';
if (!playButton.dataset.uid) {
playButton.dataset.uid = uid;
console.log('[personal-stream] Set UID for personal stream play button:', uid);
}
} else {
console.warn('[personal-stream] No UID found for personal stream play button');
// Hide the player for guests
streamPlayer.style.display = 'none';
}
}

View File

@ -31,8 +31,8 @@ export async function initMagicLogin() {
const authToken = 'token-' + Math.random().toString(36).substring(2, 15);
// Set cookies and localStorage for SPA session logic
document.cookie = `uid=${encodeURIComponent(confirmedUid)}; path=/`;
document.cookie = `authToken=${authToken}; path=/`;
document.cookie = `uid=${encodeURIComponent(confirmedUid)}; path=/; SameSite=Lax`;
document.cookie = `authToken=${authToken}; path=/; SameSite=Lax; Secure`;
// Store in localStorage for client-side access
localStorage.setItem('uid', confirmedUid);
@ -53,8 +53,8 @@ export async function initMagicLogin() {
const authToken = 'token-' + Math.random().toString(36).substring(2, 15);
// Set cookies and localStorage for SPA session logic
document.cookie = `uid=${encodeURIComponent(data.confirmed_uid)}; path=/`;
document.cookie = `authToken=${authToken}; path=/`;
document.cookie = `uid=${encodeURIComponent(data.confirmed_uid)}; path=/; SameSite=Lax`;
document.cookie = `authToken=${authToken}; path=/; SameSite=Lax; Secure`;
// Store in localStorage for client-side access
localStorage.setItem('uid', data.confirmed_uid);

View File

@ -1,12 +1,22 @@
// static/streams-ui.js — public streams loader and profile-link handling
import { showOnly } from './router.js';
console.log('[streams-ui] Module loaded');
// Global variable to track if we should force refresh the stream list
let shouldForceRefresh = false;
// Function to refresh the stream list
window.refreshStreamList = function(force = true) {
shouldForceRefresh = force;
loadAndRenderStreams();
return new Promise((resolve) => {
// Resolve after a short delay to allow the stream list to update
setTimeout(resolve, 500);
});
};
// Removed loadingStreams and lastStreamsPageVisible guards for instant fetch
export function initStreamsUI() {
console.log('[streams-ui] Initializing streams UI');
initStreamLinks();
window.addEventListener('popstate', () => {
highlightActiveProfileLink();
@ -29,25 +39,55 @@ window.maybeLoadStreamsOnShow = maybeLoadStreamsOnShow;
// Global variables for audio control
let currentlyPlayingAudio = null;
// Global variable to track the active SSE connection
let activeSSEConnection = null;
// Global cleanup function for SSE connections
const cleanupConnections = () => {
if (window._streamsSSE) {
if (window._streamsSSE.abort) {
window._streamsSSE.abort();
}
window._streamsSSE = null;
}
if (window.connectionTimeout) {
clearTimeout(window.connectionTimeout);
window.connectionTimeout = null;
}
activeSSEConnection = null;
};
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
console.log('[streams-ui] DOM content loaded, initializing streams UI');
initStreamsUI();
// Also try to load streams immediately in case the page is already loaded
setTimeout(() => {
console.log('[streams-ui] Attempting initial stream load');
loadAndRenderStreams();
}, 100);
});
function loadAndRenderStreams() {
console.log('[streams-ui] loadAndRenderStreams called');
const ul = document.getElementById('stream-list');
if (!ul) {
console.warn('[streams-ui] #stream-list not found in DOM');
console.error('[STREAMS-UI] Stream list element not found');
return;
}
console.log('[STREAMS-UI] loadAndRenderStreams called, shouldForceRefresh:', shouldForceRefresh);
// Don't start a new connection if one is already active and we're not forcing a refresh
if (activeSSEConnection && !shouldForceRefresh) {
return;
}
// If we're forcing a refresh, clean up the existing connection
if (shouldForceRefresh && activeSSEConnection) {
// Clean up any existing connections
cleanupConnections();
shouldForceRefresh = false; // Reset the flag after handling
}
// Clear any existing error messages or retry buttons
ul.innerHTML = '<li>Loading public streams...</li>';
@ -59,36 +99,21 @@ function loadAndRenderStreams() {
const baseUrl = window.location.origin;
const sseUrl = `${baseUrl}/streams-sse?t=${timestamp}`;
console.log(`[streams-ui] Connecting to ${sseUrl}`);
let gotAny = false;
let streams = [];
let connectionTimeout = null;
window.connectionTimeout = null;
// Clean up previous connection and timeouts
if (window._streamsSSE) {
console.group('[streams-ui] Cleaning up previous connection');
console.log('Previous connection exists, aborting...');
if (window._streamsSSE.abort) {
window._streamsSSE.abort();
console.log('Previous connection aborted');
} else {
console.log('No abort method on previous connection');
}
window._streamsSSE = null;
console.groupEnd();
// Clean up any existing connections
cleanupConnections();
// Reset the retry count if we have a successful connection
window.streamRetryCount = 0;
if (window.connectionTimeout) {
clearTimeout(window.connectionTimeout);
window.connectionTimeout = null;
}
if (connectionTimeout) {
console.log('[streams-ui] Clearing previous connection timeout');
clearTimeout(connectionTimeout);
connectionTimeout = null;
} else {
console.log('[streams-ui] No previous connection timeout to clear');
}
console.log(`[streams-ui] Creating fetch-based SSE connection to ${sseUrl}`);
// Use fetch with ReadableStream for better CORS handling
const controller = new AbortController();
const signal = controller.signal;
@ -96,6 +121,9 @@ function loadAndRenderStreams() {
// Store the controller for cleanup
window._streamsSSE = controller;
// Track the active connection
activeSSEConnection = controller;
// Set a connection timeout with debug info
const connectionStartTime = Date.now();
const connectionTimeoutId = setTimeout(() => {
@ -123,20 +151,12 @@ function loadAndRenderStreams() {
window.streamRetryCount = retryCount + 1;
const backoffTime = Math.min(1000 * Math.pow(2, retryCount), 10000); // Exponential backoff, max 10s
setTimeout(loadAndRenderStreams, backoffTime);
} else if (process.env.NODE_ENV === 'development' || window.DEBUG_STREAMS) {
console.warn('Max retries reached for stream loading');
}
}
}, 15000); // 15 second timeout (increased from 10s)
// Store the timeout ID for cleanup
connectionTimeout = connectionTimeoutId;
console.log('[streams-ui] Making fetch request to:', sseUrl);
console.log('[streams-ui] Making fetch request to:', sseUrl);
console.log('[streams-ui] Creating fetch request with URL:', sseUrl);
window.connectionTimeout = connectionTimeoutId;
// Make the fetch request with proper error handling
fetch(sseUrl, {
@ -151,25 +171,14 @@ function loadAndRenderStreams() {
mode: 'cors',
redirect: 'follow'
})
.then(response => {
console.log('[streams-ui] Fetch response received, status:', response.status, response.statusText);
console.log('[streams-ui] Response URL:', response.url);
console.log('[streams-ui] Response type:', response.type);
console.log('[streams-ui] Response redirected:', response.redirected);
console.log('[streams-ui] Response headers:');
response.headers.forEach((value, key) => {
console.log(` ${key}: ${value}`);
});
.then(response => {
if (!response.ok) {
// Try to get the response text for error details
return response.text().then(text => {
console.error('[streams-ui] Error response body:', text);
const error = new Error(`HTTP error! status: ${response.status}, statusText: ${response.statusText}`);
error.response = { status: response.status, statusText: response.statusText, body: text };
throw error;
}).catch(textError => {
console.error('[streams-ui] Could not read error response body:', textError);
}).catch(() => {
const error = new Error(`HTTP error! status: ${response.status}, statusText: ${response.statusText}`);
error.response = { status: response.status, statusText: response.statusText };
throw error;
@ -177,13 +186,9 @@ function loadAndRenderStreams() {
}
if (!response.body) {
const error = new Error('Response body is null or undefined');
console.error('[streams-ui] No response body:', error);
throw error;
throw new Error('Response body is null or undefined');
}
console.log('[streams-ui] Response body is available, content-type:', response.headers.get('content-type'));
// Get the readable stream
const reader = response.body.getReader();
const decoder = new TextDecoder();
@ -191,15 +196,18 @@ function loadAndRenderStreams() {
// Process the stream
function processStream({ done, value }) {
console.log('[STREAMS-UI] processStream called with done:', done);
if (done) {
console.log('[streams-ui] Stream completed');
console.log('[STREAMS-UI] Stream processing complete');
// Process any remaining data in the buffer
if (buffer.trim()) {
console.log('[STREAMS-UI] Processing remaining buffer data');
try {
const data = JSON.parse(buffer);
console.log('[STREAMS-UI] Parsed data from buffer:', data);
processSSEEvent(data);
} catch (e) {
console.error('[streams-ui] Error parsing final data:', e);
console.error('[STREAMS-UI] Error parsing buffer data:', e);
}
}
return;
@ -235,68 +243,63 @@ function loadAndRenderStreams() {
return reader.read().then(processStream);
})
.catch(error => {
// Only handle the error if it's not an AbortError (from our own abort)
if (error.name === 'AbortError') {
console.log('[streams-ui] Request was aborted as expected');
return;
}
console.error('[streams-ui] Stream loading failed:', error);
// Log additional error details
if (error.name === 'TypeError') {
console.error('[streams-ui] This is likely a network error or CORS issue');
}
// Show a user-friendly error message
const ul = document.getElementById('stream-list');
if (ul) {
let errorMessage = 'Error loading streams. ';
// Only handle the error if it's not an abort error
if (error.name !== 'AbortError') {
// Clean up the controller reference
window._streamsSSE = null;
activeSSEConnection = null;
if (error.message.includes('Failed to fetch')) {
errorMessage += 'Unable to connect to the server. Please check your internet connection.';
} else if (error.message.includes('CORS')) {
errorMessage += 'A server configuration issue occurred. Please try again later.';
} else {
errorMessage += 'Please try again later.';
// Clear the connection timeout
if (connectionTimeout) {
clearTimeout(connectionTimeout);
connectionTimeout = null;
}
ul.innerHTML = `
<li class="error">
<p>${errorMessage}</p>
<button id="retry-loading" class="retry-button">
<span class="retry-icon">↻</span> Try Again
</button>
</li>
`;
// Show a user-friendly error message
const ul = document.getElementById('stream-list');
if (ul) {
let errorMessage = 'Error loading streams. ';
// Add retry handler
const retryButton = document.getElementById('retry-loading');
if (retryButton) {
retryButton.addEventListener('click', () => {
ul.innerHTML = '<li>Loading streams...</li>';
loadAndRenderStreams();
});
if (error.message && error.message.includes('Failed to fetch')) {
errorMessage += 'Unable to connect to the server. Please check your internet connection.';
} else if (error.message && error.message.includes('CORS')) {
errorMessage += 'A server configuration issue occurred. Please try again later.';
} else {
errorMessage += 'Please try again later.';
}
ul.innerHTML = `
<li class="error">
<p>${errorMessage}</p>
<button id="retry-loading" class="retry-button">
<span class="retry-icon">↻</span> Try Again
</button>
</li>
`;
// Add retry handler
const retryButton = document.getElementById('retry-loading');
if (retryButton) {
retryButton.addEventListener('click', () => {
ul.innerHTML = '<li>Loading streams...</li>';
loadAndRenderStreams();
});
}
}
}
});
// Function to process SSE events
function processSSEEvent(data) {
console.log('[streams-ui] Received SSE event:', data);
console.log('[STREAMS-UI] Processing SSE event:', data);
if (data.end) {
console.log('[streams-ui] Received end event, total streams:', streams.length);
if (streams.length === 0) {
console.log('[streams-ui] No streams found, showing empty state');
ul.innerHTML = '<li>No active streams.</li>';
return;
}
// Sort streams by mtime in descending order (newest first)
streams.sort((a, b) => (b.mtime || 0) - (a.mtime || 0));
console.log('[streams-ui] Sorted streams:', streams);
// Clear the list
ul.innerHTML = '';
@ -307,8 +310,6 @@ function loadAndRenderStreams() {
const sizeMb = stream.size ? (stream.size / (1024 * 1024)).toFixed(1) : '?';
const mtime = stream.mtime ? new Date(stream.mtime * 1000).toISOString().split('T')[0].replace(/-/g, '/') : '';
console.log(`[streams-ui] Rendering stream ${index + 1}/${streams.length}:`, { uid, sizeMb, mtime });
const li = document.createElement('li');
li.className = 'stream-item';
@ -323,9 +324,7 @@ function loadAndRenderStreams() {
</article>
`;
ul.appendChild(li);
console.log(`[streams-ui] Successfully rendered stream: ${uid}`);
} catch (error) {
console.error(`[streams-ui] Error rendering stream ${uid}:`, error);
const errorLi = document.createElement('li');
errorLi.textContent = `Error loading stream: ${uid}`;
errorLi.style.color = 'var(--error)';
@ -379,10 +378,11 @@ function loadAndRenderStreams() {
export function renderStreamList(streams) {
const ul = document.getElementById('stream-list');
if (!ul) {
console.warn('[streams-ui] renderStreamList: #stream-list not found');
console.warn('[STREAMS-UI] renderStreamList: #stream-list not found');
return;
}
console.debug('[streams-ui] Rendering stream list:', streams);
console.log('[STREAMS-UI] Rendering stream list with', streams.length, 'streams');
console.debug('[STREAMS-UI] Streams data:', streams);
if (Array.isArray(streams)) {
if (streams.length) {
// Sort by mtime descending (most recent first)
@ -551,18 +551,14 @@ function stopPlayback() {
// Load and play audio using HTML5 Audio element for Opus
async function loadAndPlayAudio(uid, playPauseBtn) {
console.log(`[streams-ui] loadAndPlayAudio called for UID: ${uid}`);
// If trying to play the currently paused audio, just resume it
if (audioElement && currentUid === uid) {
console.log('[streams-ui] Resuming existing audio');
// If we already have an audio element for this UID and it's paused, just resume it
if (audioElement && currentUid === uid && audioElement.paused) {
try {
await audioElement.play();
isPlaying = true;
updatePlayPauseButton(playPauseBtn, true);
return;
} catch (error) {
console.error('Error resuming audio:', error);
// Fall through to reload if resume fails
}
}
@ -576,11 +572,8 @@ async function loadAndPlayAudio(uid, playPauseBtn) {
currentUid = uid;
try {
console.log(`[streams-ui] Creating new audio element for ${uid}`);
// Create a new audio element with the correct MIME type
const audioUrl = `/audio/${encodeURIComponent(uid)}/stream.opus`;
console.log(`[streams-ui] Loading audio from: ${audioUrl}`);
// Create a new audio element with a small delay to prevent race conditions
await new Promise(resolve => setTimeout(resolve, 50));
@ -591,19 +584,16 @@ async function loadAndPlayAudio(uid, playPauseBtn) {
// Set up event handlers with proper binding
const onPlay = () => {
console.log('[streams-ui] Audio play event');
isPlaying = true;
updatePlayPauseButton(playPauseBtn, true);
};
const onPause = () => {
console.log('[streams-ui] Audio pause event');
isPlaying = false;
updatePlayPauseButton(playPauseBtn, false);
};
const onEnded = () => {
console.log('[streams-ui] Audio ended event');
isPlaying = false;
cleanupAudio();
};
@ -611,18 +601,14 @@ async function loadAndPlayAudio(uid, playPauseBtn) {
const onError = (e) => {
// Ignore errors from previous audio elements that were cleaned up
if (!audioElement || audioElement.readyState === 0) {
console.log('[streams-ui] Ignoring error from cleaned up audio element');
return;
}
console.error('[streams-ui] Audio error:', e);
console.error('Error details:', audioElement.error);
isPlaying = false;
updatePlayPauseButton(playPauseBtn, false);
// Don't show error to user for aborted requests
if (audioElement.error && audioElement.error.code === MediaError.MEDIA_ERR_ABORTED) {
console.log('[streams-ui] Playback was aborted as expected');
return;
}
@ -642,7 +628,6 @@ async function loadAndPlayAudio(uid, playPauseBtn) {
audioElement._eventHandlers = { onPlay, onPause, onEnded, onError };
// Start playback with error handling
console.log('[streams-ui] Starting audio playback');
try {
const playPromise = audioElement.play();
@ -650,10 +635,8 @@ async function loadAndPlayAudio(uid, playPauseBtn) {
await playPromise.catch(error => {
// Ignore abort errors when switching between streams
if (error.name !== 'AbortError') {
console.error('[streams-ui] Play failed:', error);
throw error;
}
console.log('[streams-ui] Play was aborted as expected');
});
}
@ -759,27 +742,21 @@ if (streamList) {
const uid = playPauseBtn.dataset.uid;
if (!uid) {
console.error('No UID found for play button');
return;
}
console.log(`[streams-ui] Play/pause clicked for UID: ${uid}, currentUid: ${currentUid}, isPlaying: ${isPlaying}`);
// If clicking the currently playing button, toggle pause/play
if (currentUid === uid) {
if (isPlaying) {
console.log('[streams-ui] Pausing current audio');
await audioElement.pause();
isPlaying = false;
updatePlayPauseButton(playPauseBtn, false);
} else {
console.log('[streams-ui] Resuming current audio');
try {
await audioElement.play();
isPlaying = true;
updatePlayPauseButton(playPauseBtn, true);
} catch (error) {
console.error('[streams-ui] Error resuming audio:', error);
// If resume fails, try reloading the audio
await loadAndPlayAudio(uid, playPauseBtn);
}
@ -788,7 +765,6 @@ if (streamList) {
}
// If a different stream is playing, stop it and start the new one
console.log(`[streams-ui] Switching to new audio stream: ${uid}`);
stopPlayback();
await loadAndPlayAudio(uid, playPauseBtn);
});

View File

@ -19,7 +19,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
const streamInfo = document.getElementById("stream-info");
const streamUrlEl = document.getElementById("streamUrl");
const spinner = document.getElementById("spinner");
const spinner = document.getElementById("spinner") || { style: { display: 'none' } };
let abortController;
// Upload function
@ -89,6 +89,11 @@ document.addEventListener('DOMContentLoaded', () => {
if (window.fetchAndDisplayFiles) {
await window.fetchAndDisplayFiles(uid);
}
// Refresh the stream list to update the last update time
if (window.refreshStreamList) {
await window.refreshStreamList();
}
} catch (e) {
console.error('Failed to refresh:', e);
}
@ -96,8 +101,8 @@ document.addEventListener('DOMContentLoaded', () => {
playBeep(432, 0.25, "sine");
} else {
streamInfo.hidden = true;
spinner.style.display = "none";
if (streamInfo) streamInfo.hidden = true;
if (spinner) spinner.style.display = "none";
if ((data.detail || data.error || "").includes("music")) {
showToast("🎵 Upload rejected: singing or music detected.");
} else {
@ -190,10 +195,10 @@ document.addEventListener('DOMContentLoaded', () => {
const isRenamed = file.original_name && file.original_name !== file.name;
return `
<li class="file-item" data-filename="${file.name}">
<div class="file-name" title="${displayName}">
<div class="file-name" title="${isRenamed ? `Stored as: ${file.name}` : displayName}">
${displayName}
${isRenamed ? `<div class="stored-as" title="Stored as: ${file.name}">${file.name} <button class="delete-file" data-filename="${file.name}" title="Delete file">🗑️</button></div>` :
`<button class="delete-file" data-filename="${file.name}" title="Delete file">🗑️</button>`}
${isRenamed ? `<div class="stored-as"><button class="delete-file" data-filename="${file.name}" data-original-name="${file.original_name}" title="Delete file">🗑️</button></div>` :
`<button class="delete-file" data-filename="${file.name}" data-original-name="${file.original_name}" title="Delete file">🗑️</button>`}
</div>
<span class="file-size">${sizeMB} MB</span>
</li>
@ -203,48 +208,7 @@ document.addEventListener('DOMContentLoaded', () => {
fileList.innerHTML = '<li class="empty-message">No files uploaded yet</li>';
}
// Add event listeners to delete buttons
document.querySelectorAll('.delete-file').forEach(button => {
button.addEventListener('click', async (e) => {
e.stopPropagation();
const filename = button.dataset.filename;
if (confirm(`Are you sure you want to delete ${filename}?`)) {
try {
// Get the auth token from the cookie
const token = document.cookie
.split('; ')
.find(row => row.startsWith('sessionid='))
?.split('=')[1];
if (!token) {
throw new Error('Not authenticated');
}
const response = await fetch(`/delete/${filename}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || `Failed to delete file: ${response.statusText}`);
}
// Refresh the file list
const uid = document.body.dataset.userUid;
if (uid) {
fetchAndDisplayFiles(uid);
}
} catch (error) {
console.error('Error deleting file:', error);
alert('Failed to delete file. Please try again.');
}
}
});
});
// Delete button handling is now managed by dashboard.js
// Update quota display if available
if (data.quota !== undefined) {