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:
2025-12-12 18:10:25 +03:00
parent 3dd10d6dab
commit fdc854256c
7 changed files with 144 additions and 11 deletions

View File

@@ -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) {

View File

@@ -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

View File

@@ -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>