Fix AJAX navigation, WebSocket script scope, and Daphne media serving 404s

This commit is contained in:
2026-05-24 17:38:00 +02:00
parent 88bdfc797b
commit 6767695639
2 changed files with 93 additions and 33 deletions
+60 -30
View File
@@ -324,8 +324,12 @@
</style>
</head>
<body>
<div id="app-root">
{% block body %}{% endblock %}
</div>
<!-- Build version badge -->
<div id="app-version" style="position:fixed;bottom:4px;left:4px;font-size:0.6rem;color:rgba(255,255,255,0.25);z-index:9999;pointer-events:none;font-family:monospace;">v2026.05.24-b</div>
<!-- Voice connected status panel -->
<div class="voice-status-panel" id="voice-status-panel">
<div class="voice-status-info">
@@ -341,7 +345,7 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
<script id="base-script">
(function() {
const currentUserId = {% if request.user.is_authenticated %}{{ request.user.id }}{% else %}null{% endif %};
const currentUsername = "{% if request.user.is_authenticated %}{{ request.user.username|escapejs }}{% endif %}";
@@ -596,15 +600,20 @@
});
// AJAX Shell Navigation
// Pages that should always do a full-page load (non-SPA layouts, auth pages)
const FULL_PAGE_KEYWORDS = ['create', 'profile', 'admin', 'login', 'register', 'logout', 'delete', 'block'];
function shouldFullPageNavigate(href) {
if (!href || href.startsWith('http') || href.startsWith('#') || href.startsWith('javascript')) return true;
return FULL_PAGE_KEYWORDS.some(kw => href.includes(kw));
}
document.body.addEventListener('click', function(e) {
const link = e.target.closest('a');
if (!link) return;
const href = link.getAttribute('href');
if (!href || href.startsWith('http') || href.startsWith('#') || href.includes('delete') || href.includes('logout') || href.includes('admin') || href.includes('profile')) {
return;
}
if (shouldFullPageNavigate(href)) return;
if (link.target === '_blank') return;
e.preventDefault();
@@ -612,35 +621,47 @@
});
function loadPage(url) {
// Close existing channel/DM WebSocket before navigating
if (window.channelWs) {
window.channelWs.close();
window.channelWs = null;
}
fetch(url)
.then(res => res.text())
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.text();
})
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newMain = doc.querySelector('.main-content');
const currentMain = document.querySelector('.main-content');
if (newMain && currentMain) {
currentMain.replaceWith(newMain);
}
const newRoot = doc.getElementById('app-root');
const currentRoot = document.getElementById('app-root');
document.querySelectorAll('.channel-item.active').forEach(el => el.classList.remove('active'));
const activeLink = document.querySelector(`.channel-item[href="${url}"]`);
if (activeLink) activeLink.classList.add('active');
if (window.channelWs) {
window.channelWs.close();
if (newRoot && currentRoot) {
currentRoot.replaceWith(newRoot);
} else {
// Fallback: full page load if structure doesn't match
window.location.href = url;
return;
}
history.pushState({ url }, '', url);
// Execute only PAGE-SPECIFIC scripts (those inside the scripts block).
// SKIP the base-script IIFE — it's already running and re-executing
// it would duplicate global WebSocket connections and event handlers.
// Wrap each script in an IIFE to prevent 'const' redeclaration errors
// when navigating between channels (each has const channelId, etc.).
doc.querySelectorAll('script').forEach(script => {
if (!script.src && (script.textContent.includes('WebSocket') || script.textContent.includes('channelWs'))) {
const newScript = document.createElement('script');
newScript.textContent = script.textContent;
document.body.appendChild(newScript);
newScript.remove();
}
if (script.id === 'base-script') return;
if (script.src) return;
if (!script.textContent.trim()) return;
const newScript = document.createElement('script');
newScript.textContent = `(function(){${script.textContent}})();`;
document.body.appendChild(newScript);
newScript.remove();
});
})
.catch(err => {
@@ -657,27 +678,36 @@
}
};
// AJAX form submit handler — only for file upload forms inside chat/DM views.
// Forms for auth, profile, channel create, admin actions submit natively.
document.body.addEventListener('submit', function(e) {
const form = e.target.closest('form');
if (!form || form.getAttribute('action')?.includes('login') || form.getAttribute('action')?.includes('register') || form.getAttribute('action')?.includes('profile') || form.getAttribute('action')?.includes('create')) {
return;
}
if (!form) return;
const action = form.getAttribute('action') || '';
// Let these forms submit natively (full page reload)
if (FULL_PAGE_KEYWORDS.some(kw => action.includes(kw))) return;
// Only AJAX-submit file upload forms (they have enctype=multipart)
if (!form.enctype || form.enctype !== 'multipart/form-data') return;
e.preventDefault();
const formData = new FormData(form);
const action = form.getAttribute('action') || location.pathname;
const submitUrl = action || location.pathname;
fetch(action, {
fetch(submitUrl, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(() => {
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
// Reload current page to show the uploaded file
loadPage(location.pathname);
})
.catch(err => console.error("Upload form failed:", err));
.catch(err => console.error("File upload failed:", err));
});
})();
</script>