Fix AJAX navigation, WebSocket script scope, and Daphne media serving 404s
This commit is contained in:
@@ -324,8 +324,12 @@
|
|||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div id="app-root">
|
||||||
{% block body %}{% endblock %}
|
{% 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 -->
|
<!-- Voice connected status panel -->
|
||||||
<div class="voice-status-panel" id="voice-status-panel">
|
<div class="voice-status-panel" id="voice-status-panel">
|
||||||
<div class="voice-status-info">
|
<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 src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script id="base-script">
|
||||||
(function() {
|
(function() {
|
||||||
const currentUserId = {% if request.user.is_authenticated %}{{ request.user.id }}{% else %}null{% endif %};
|
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 %}";
|
const currentUsername = "{% if request.user.is_authenticated %}{{ request.user.username|escapejs }}{% endif %}";
|
||||||
@@ -596,15 +600,20 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// AJAX Shell Navigation
|
// 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) {
|
document.body.addEventListener('click', function(e) {
|
||||||
const link = e.target.closest('a');
|
const link = e.target.closest('a');
|
||||||
if (!link) return;
|
if (!link) return;
|
||||||
|
|
||||||
const href = link.getAttribute('href');
|
const href = link.getAttribute('href');
|
||||||
if (!href || href.startsWith('http') || href.startsWith('#') || href.includes('delete') || href.includes('logout') || href.includes('admin') || href.includes('profile')) {
|
if (shouldFullPageNavigate(href)) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (link.target === '_blank') return;
|
if (link.target === '_blank') return;
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -612,35 +621,47 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
function loadPage(url) {
|
function loadPage(url) {
|
||||||
|
// Close existing channel/DM WebSocket before navigating
|
||||||
|
if (window.channelWs) {
|
||||||
|
window.channelWs.close();
|
||||||
|
window.channelWs = null;
|
||||||
|
}
|
||||||
|
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then(res => res.text())
|
.then(res => {
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
return res.text();
|
||||||
|
})
|
||||||
.then(html => {
|
.then(html => {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(html, 'text/html');
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
|
|
||||||
const newMain = doc.querySelector('.main-content');
|
const newRoot = doc.getElementById('app-root');
|
||||||
const currentMain = document.querySelector('.main-content');
|
const currentRoot = document.getElementById('app-root');
|
||||||
if (newMain && currentMain) {
|
|
||||||
currentMain.replaceWith(newMain);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelectorAll('.channel-item.active').forEach(el => el.classList.remove('active'));
|
if (newRoot && currentRoot) {
|
||||||
const activeLink = document.querySelector(`.channel-item[href="${url}"]`);
|
currentRoot.replaceWith(newRoot);
|
||||||
if (activeLink) activeLink.classList.add('active');
|
} else {
|
||||||
|
// Fallback: full page load if structure doesn't match
|
||||||
if (window.channelWs) {
|
window.location.href = url;
|
||||||
window.channelWs.close();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
history.pushState({ url }, '', url);
|
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 => {
|
doc.querySelectorAll('script').forEach(script => {
|
||||||
if (!script.src && (script.textContent.includes('WebSocket') || script.textContent.includes('channelWs'))) {
|
if (script.id === 'base-script') return;
|
||||||
const newScript = document.createElement('script');
|
if (script.src) return;
|
||||||
newScript.textContent = script.textContent;
|
if (!script.textContent.trim()) return;
|
||||||
document.body.appendChild(newScript);
|
const newScript = document.createElement('script');
|
||||||
newScript.remove();
|
newScript.textContent = `(function(){${script.textContent}})();`;
|
||||||
}
|
document.body.appendChild(newScript);
|
||||||
|
newScript.remove();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.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) {
|
document.body.addEventListener('submit', function(e) {
|
||||||
const form = e.target.closest('form');
|
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')) {
|
if (!form) return;
|
||||||
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();
|
e.preventDefault();
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
const action = form.getAttribute('action') || location.pathname;
|
const submitUrl = action || location.pathname;
|
||||||
|
|
||||||
fetch(action, {
|
fetch(submitUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData,
|
body: formData,
|
||||||
headers: {
|
headers: {
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
'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);
|
loadPage(location.pathname);
|
||||||
})
|
})
|
||||||
.catch(err => console.error("Upload form failed:", err));
|
.catch(err => console.error("File upload failed:", err));
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
+33
-3
@@ -1,13 +1,43 @@
|
|||||||
from django.contrib import admin
|
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 import settings
|
||||||
from django.conf.urls.static import static
|
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 = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
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')),
|
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'
|
handler404 = 'chat.views.handler404'
|
||||||
handler500 = 'chat.views.handler500'
|
handler500 = 'chat.views.handler500'
|
||||||
|
|||||||
Reference in New Issue
Block a user