Fix AJAX navigation, WebSocket script scope, and Daphne media serving 404s
This commit is contained in:
@@ -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>
|
||||
|
||||
+33
-3
@@ -1,13 +1,43 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.urls import path, re_path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
from django.views.static import serve as static_serve
|
||||
from django.http import FileResponse, Http404
|
||||
import os
|
||||
from urllib.parse import unquote
|
||||
|
||||
|
||||
def serve_media(request, path):
|
||||
"""Serve media files — works under Daphne/ASGI where static() does not."""
|
||||
# URL-decode the path (handles %20 for spaces, etc.)
|
||||
decoded_path = unquote(path)
|
||||
file_path = os.path.join(settings.MEDIA_ROOT, decoded_path)
|
||||
|
||||
if os.path.isfile(file_path):
|
||||
return FileResponse(open(file_path, 'rb'))
|
||||
|
||||
# Fallback: try the raw (non-decoded) path
|
||||
raw_path = os.path.join(settings.MEDIA_ROOT, path)
|
||||
if os.path.isfile(raw_path):
|
||||
return FileResponse(open(raw_path, 'rb'))
|
||||
|
||||
print(f"[MEDIA 404] Requested: {path}")
|
||||
print(f"[MEDIA 404] Decoded: {decoded_path}")
|
||||
print(f"[MEDIA 404] Full path: {file_path}")
|
||||
print(f"[MEDIA 404] Exists: {os.path.exists(file_path)}")
|
||||
print(f"[MEDIA 404] Dir listing: {os.listdir(os.path.dirname(file_path)) if os.path.isdir(os.path.dirname(file_path)) else 'DIR NOT FOUND'}")
|
||||
raise Http404(f"Media file not found: {decoded_path}")
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
# Serve media files explicitly — needed because Daphne (ASGI) does not
|
||||
# honour the static() helper the way Django's runserver does.
|
||||
# Must be BEFORE the catch-all chat.urls include.
|
||||
re_path(r'^media/(?P<path>.*)$', serve_media),
|
||||
path('', include('chat.urls')),
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \
|
||||
+ static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
|
||||
handler404 = 'chat.views.handler404'
|
||||
handler500 = 'chat.views.handler500'
|
||||
|
||||
Reference in New Issue
Block a user