Add track filtering, WS keepalive, and improve error handling
- Add track filtering by uploader (my tracks / all tracks) with UI tabs - Add WebSocket ping/pong keepalive (30s interval) to prevent disconnects - Add auto-reconnect on WebSocket close (3s delay) - Add request logging middleware with DATABASE_URL output on startup - Handle missing S3 files gracefully (return 404 instead of 500) - Add debug logging for audio ended event 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,10 @@ export const useActiveRoomStore = defineStore('activeRoom', () => {
|
||||
let pendingPlay = false
|
||||
let pendingPosition = null
|
||||
|
||||
// WebSocket keepalive
|
||||
let pingInterval = null
|
||||
let reconnectTimeout = null
|
||||
|
||||
const isInRoom = computed(() => roomId.value !== null)
|
||||
|
||||
function initAudio() {
|
||||
@@ -53,10 +57,15 @@ export const useActiveRoomStore = defineStore('activeRoom', () => {
|
||||
})
|
||||
|
||||
audio.addEventListener('ended', () => {
|
||||
console.log('Track ended event fired')
|
||||
if (onTrackEndedCallback) {
|
||||
onTrackEndedCallback()
|
||||
}
|
||||
})
|
||||
|
||||
audio.addEventListener('error', (e) => {
|
||||
console.error('Audio error:', e, audio.error)
|
||||
})
|
||||
}
|
||||
|
||||
function connect(id, name) {
|
||||
@@ -74,10 +83,36 @@ export const useActiveRoomStore = defineStore('activeRoom', () => {
|
||||
ws.value.onopen = () => {
|
||||
connected.value = true
|
||||
send({ type: 'sync_request' })
|
||||
|
||||
// Start ping interval (every 30 seconds)
|
||||
if (pingInterval) clearInterval(pingInterval)
|
||||
pingInterval = setInterval(() => {
|
||||
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
||||
send({ type: 'ping' })
|
||||
}
|
||||
}, 30000)
|
||||
}
|
||||
|
||||
ws.value.onclose = () => {
|
||||
connected.value = false
|
||||
if (pingInterval) {
|
||||
clearInterval(pingInterval)
|
||||
pingInterval = null
|
||||
}
|
||||
|
||||
// Auto-reconnect after 3 seconds if we still have room info
|
||||
if (roomId.value && !reconnectTimeout) {
|
||||
reconnectTimeout = setTimeout(() => {
|
||||
reconnectTimeout = null
|
||||
if (roomId.value) {
|
||||
console.log('Reconnecting WebSocket...')
|
||||
const savedId = roomId.value
|
||||
const savedName = roomName.value
|
||||
ws.value = null
|
||||
connect(savedId, savedName)
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
ws.value.onerror = () => {}
|
||||
@@ -94,13 +129,26 @@ export const useActiveRoomStore = defineStore('activeRoom', () => {
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
// Clear timers
|
||||
if (pingInterval) {
|
||||
clearInterval(pingInterval)
|
||||
pingInterval = null
|
||||
}
|
||||
if (reconnectTimeout) {
|
||||
clearTimeout(reconnectTimeout)
|
||||
reconnectTimeout = null
|
||||
}
|
||||
|
||||
// Clear room info first to prevent auto-reconnect
|
||||
const wasInRoom = roomId.value
|
||||
roomId.value = null
|
||||
roomName.value = null
|
||||
|
||||
if (ws.value) {
|
||||
ws.value.close()
|
||||
ws.value = null
|
||||
}
|
||||
connected.value = false
|
||||
roomId.value = null
|
||||
roomName.value = null
|
||||
chatMessages.value = []
|
||||
playerStore.reset()
|
||||
if (audio) {
|
||||
|
||||
@@ -6,10 +6,10 @@ export const useTracksStore = defineStore('tracks', () => {
|
||||
const tracks = ref([])
|
||||
const loading = ref(false)
|
||||
|
||||
async function fetchTracks() {
|
||||
async function fetchTracks(myOnly = false) {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await api.get('/api/tracks')
|
||||
const response = await api.get('/api/tracks', { params: { my: myOnly } })
|
||||
tracks.value = response.data
|
||||
} finally {
|
||||
loading.value = false
|
||||
|
||||
@@ -5,6 +5,21 @@
|
||||
<button class="btn-primary" @click="showUpload = true">Загрузить трек</button>
|
||||
</div>
|
||||
|
||||
<div class="filter-tabs">
|
||||
<button
|
||||
:class="['filter-tab', { active: !showMyOnly }]"
|
||||
@click="setFilter(false)"
|
||||
>
|
||||
Все треки
|
||||
</button>
|
||||
<button
|
||||
:class="['filter-tab', { active: showMyOnly }]"
|
||||
@click="setFilter(true)"
|
||||
>
|
||||
Мои треки
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="tracksStore.loading" class="loading">Загрузка...</div>
|
||||
|
||||
<div v-else-if="tracksStore.tracks.length === 0" class="empty">
|
||||
@@ -34,11 +49,17 @@ import Modal from '../components/common/Modal.vue'
|
||||
const tracksStore = useTracksStore()
|
||||
|
||||
const showUpload = ref(false)
|
||||
const showMyOnly = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
tracksStore.fetchTracks()
|
||||
})
|
||||
|
||||
function setFilter(myOnly) {
|
||||
showMyOnly.value = myOnly
|
||||
tracksStore.fetchTracks(myOnly)
|
||||
}
|
||||
|
||||
async function handleDelete(track) {
|
||||
if (confirm(`Удалить трек "${track.title}"?`)) {
|
||||
await tracksStore.deleteTrack(track.id)
|
||||
@@ -71,4 +92,29 @@ async function handleDelete(track) {
|
||||
padding: 40px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
padding: 8px 16px;
|
||||
background: #333;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
color: #aaa;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-tab:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
background: var(--color-primary, #1db954);
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user