Update all files
This commit is contained in:
7
Makefile
7
Makefile
@@ -1,8 +1,11 @@
|
|||||||
.PHONY: up down build logs migrate seed test test-api test-worker test-watcher lint check format
|
.PHONY: up down build logs migrate seed test test-api test-worker test-watcher lint check format
|
||||||
|
|
||||||
up:
|
up: validate-env
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
|
||||||
|
validate-env:
|
||||||
|
bash scripts/validate-env.sh
|
||||||
|
|
||||||
down:
|
down:
|
||||||
docker compose down
|
docker compose down
|
||||||
|
|
||||||
@@ -22,7 +25,7 @@ migrate-auto:
|
|||||||
|
|
||||||
# ── Setup ─────────────────────────────────────────────────────────────────────
|
# ── Setup ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
setup: up
|
setup: validate-env up
|
||||||
@echo "Waiting for Nextcloud to initialize (this can take ~60s)..."
|
@echo "Waiting for Nextcloud to initialize (this can take ~60s)..."
|
||||||
@sleep 60
|
@sleep 60
|
||||||
bash scripts/nc-setup.sh
|
bash scripts/nc-setup.sh
|
||||||
|
|||||||
@@ -61,3 +61,10 @@ ignore_missing_imports = true
|
|||||||
source = ["src/rehearsalhub"]
|
source = ["src/rehearsalhub"]
|
||||||
omit = ["src/rehearsalhub/db/models.py"]
|
omit = ["src/rehearsalhub/db/models.py"]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"httpx>=0.28.1",
|
||||||
|
"pytest>=9.0.2",
|
||||||
|
"pytest-asyncio>=1.3.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
|||||||
171
api/tests/integration/test_versions_streaming.py
Normal file
171
api/tests/integration/test_versions_streaming.py
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
"""Integration tests for version streaming endpoints."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, patch, MagicMock
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from rehearsalhub.routers.versions import stream_version, get_waveform
|
||||||
|
from rehearsalhub.db.models import Member, AudioVersion
|
||||||
|
from rehearsalhub.schemas.audio_version import AudioVersionRead
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_stream_version_connection_error():
|
||||||
|
"""Test stream_version endpoint handles connection errors gracefully."""
|
||||||
|
# Mock dependencies
|
||||||
|
mock_session = MagicMock()
|
||||||
|
mock_member = Member(id=1, name="Test User")
|
||||||
|
|
||||||
|
# Mock version with nc_file_path
|
||||||
|
mock_version = AudioVersion(
|
||||||
|
id="test-version-id",
|
||||||
|
nc_file_path="test/path/file.mp3",
|
||||||
|
waveform_url="test/path/waveform.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock the storage client to raise connection error
|
||||||
|
with patch("rehearsalhub.routers.versions.NextcloudClient") as mock_client_class:
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.download = AsyncMock(side_effect=httpx.ConnectError("Connection failed"))
|
||||||
|
mock_client_class.return_value = mock_client
|
||||||
|
|
||||||
|
# Mock the membership check
|
||||||
|
with patch("rehearsalhub.routers.versions._get_version_and_assert_band_membership",
|
||||||
|
return_value=(mock_version, None)):
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
await stream_version(
|
||||||
|
version_id="test-version-id",
|
||||||
|
session=mock_session,
|
||||||
|
current_member=mock_member
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should return 503 Service Unavailable
|
||||||
|
assert exc_info.value.status_code == 503
|
||||||
|
assert "Failed to connect to storage" in str(exc_info.value.detail)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_stream_version_file_not_found():
|
||||||
|
"""Test stream_version endpoint handles 404 errors gracefully."""
|
||||||
|
# Mock dependencies
|
||||||
|
mock_session = MagicMock()
|
||||||
|
mock_member = Member(id=1, name="Test User")
|
||||||
|
|
||||||
|
# Mock version with nc_file_path
|
||||||
|
mock_version = AudioVersion(
|
||||||
|
id="test-version-id",
|
||||||
|
nc_file_path="test/path/file.mp3",
|
||||||
|
waveform_url="test/path/waveform.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock the storage client to raise 404 error
|
||||||
|
with patch("rehearsalhub.routers.versions.NextcloudClient") as mock_client_class:
|
||||||
|
mock_client = MagicMock()
|
||||||
|
|
||||||
|
# Create mock response with 404 status
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 404
|
||||||
|
mock_response.text = "Not Found"
|
||||||
|
|
||||||
|
mock_client.download = AsyncMock(
|
||||||
|
side_effect=httpx.HTTPStatusError("Not found", request=MagicMock(), response=mock_response)
|
||||||
|
)
|
||||||
|
mock_client_class.return_value = mock_client
|
||||||
|
|
||||||
|
# Mock the membership check
|
||||||
|
with patch("rehearsalhub.routers.versions._get_version_and_assert_band_membership",
|
||||||
|
return_value=(mock_version, None)):
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
await stream_version(
|
||||||
|
version_id="test-version-id",
|
||||||
|
session=mock_session,
|
||||||
|
current_member=mock_member
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should return 404 Not Found
|
||||||
|
assert exc_info.value.status_code == 404
|
||||||
|
assert "File not found in storage" in str(exc_info.value.detail)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_get_waveform_connection_error():
|
||||||
|
"""Test get_waveform endpoint handles connection errors gracefully."""
|
||||||
|
# Mock dependencies
|
||||||
|
mock_session = MagicMock()
|
||||||
|
mock_member = Member(id=1, name="Test User")
|
||||||
|
|
||||||
|
# Mock version with waveform_url
|
||||||
|
mock_version = AudioVersion(
|
||||||
|
id="test-version-id",
|
||||||
|
nc_file_path="test/path/file.mp3",
|
||||||
|
waveform_url="test/path/waveform.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock the storage client to raise connection error
|
||||||
|
with patch("rehearsalhub.routers.versions.NextcloudClient") as mock_client_class:
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.download = AsyncMock(side_effect=httpx.ConnectError("Connection failed"))
|
||||||
|
mock_client_class.return_value = mock_client
|
||||||
|
|
||||||
|
# Mock the membership check
|
||||||
|
with patch("rehearsalhub.routers.versions._get_version_and_assert_band_membership",
|
||||||
|
return_value=(mock_version, None)):
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc_info:
|
||||||
|
await get_waveform(
|
||||||
|
version_id="test-version-id",
|
||||||
|
session=mock_session,
|
||||||
|
current_member=mock_member
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should return 503 Service Unavailable
|
||||||
|
assert exc_info.value.status_code == 503
|
||||||
|
assert "Failed to connect to storage" in str(exc_info.value.detail)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.integration
|
||||||
|
async def test_stream_version_success():
|
||||||
|
"""Test successful streaming when connection works."""
|
||||||
|
# Mock dependencies
|
||||||
|
mock_session = MagicMock()
|
||||||
|
mock_member = Member(id=1, name="Test User")
|
||||||
|
|
||||||
|
# Mock version with nc_file_path
|
||||||
|
mock_version = AudioVersion(
|
||||||
|
id="test-version-id",
|
||||||
|
nc_file_path="test/path/file.mp3",
|
||||||
|
waveform_url="test/path/waveform.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mock the storage client to return success
|
||||||
|
with patch("rehearsalhub.routers.versions.NextcloudClient") as mock_client_class:
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.download = AsyncMock(return_value=b"audio_data")
|
||||||
|
mock_client_class.return_value = mock_client
|
||||||
|
|
||||||
|
# Mock the membership check
|
||||||
|
with patch("rehearsalhub.routers.versions._get_version_and_assert_band_membership",
|
||||||
|
return_value=(mock_version, None)):
|
||||||
|
|
||||||
|
result = await stream_version(
|
||||||
|
version_id="test-version-id",
|
||||||
|
session=mock_session,
|
||||||
|
current_member=mock_member
|
||||||
|
)
|
||||||
|
|
||||||
|
# Should return Response with audio data
|
||||||
|
assert result.status_code == 200
|
||||||
|
assert result.body == b"audio_data"
|
||||||
|
assert result.media_type == "audio/mpeg"
|
||||||
85
api/tests/unit/test_versions_error_handling.py
Normal file
85
api/tests/unit/test_versions_error_handling.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
"""Test error handling in versions router."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import AsyncMock, patch, MagicMock
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from rehearsalhub.routers.versions import _download_with_retry
|
||||||
|
from rehearsalhub.storage.nextcloud import NextcloudClient
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_download_with_retry_success():
|
||||||
|
"""Test successful download on first attempt."""
|
||||||
|
mock_storage = MagicMock()
|
||||||
|
mock_storage.download = AsyncMock(return_value=b"test_data")
|
||||||
|
|
||||||
|
result = await _download_with_retry(mock_storage, "test_path")
|
||||||
|
assert result == b"test_data"
|
||||||
|
mock_storage.download.assert_awaited_once_with("test_path")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_download_with_retry_connection_error():
|
||||||
|
"""Test retry logic on connection errors."""
|
||||||
|
mock_storage = MagicMock()
|
||||||
|
mock_storage.download = AsyncMock(side_effect=httpx.ConnectError("Connection failed"))
|
||||||
|
|
||||||
|
with pytest.raises(httpx.ConnectError):
|
||||||
|
await _download_with_retry(mock_storage, "test_path", max_retries=2)
|
||||||
|
|
||||||
|
# Should retry max_retries times
|
||||||
|
assert mock_storage.download.await_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_download_with_retry_server_error():
|
||||||
|
"""Test retry logic on 500 server errors."""
|
||||||
|
mock_storage = MagicMock()
|
||||||
|
|
||||||
|
# Create a mock response with 500 status
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 500
|
||||||
|
mock_response.text = "Internal Server Error"
|
||||||
|
|
||||||
|
mock_storage.download = AsyncMock(side_effect=httpx.HTTPStatusError("Server error", request=MagicMock(), response=mock_response))
|
||||||
|
|
||||||
|
with pytest.raises(httpx.HTTPStatusError):
|
||||||
|
await _download_with_retry(mock_storage, "test_path", max_retries=2)
|
||||||
|
|
||||||
|
# Should retry max_retries times for 5xx errors
|
||||||
|
assert mock_storage.download.await_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_download_with_retry_client_error():
|
||||||
|
"""Test no retry on 4xx client errors."""
|
||||||
|
mock_storage = MagicMock()
|
||||||
|
|
||||||
|
# Create a mock response with 404 status
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.status_code = 404
|
||||||
|
mock_response.text = "Not Found"
|
||||||
|
|
||||||
|
mock_storage.download = AsyncMock(side_effect=httpx.HTTPStatusError("Not found", request=MagicMock(), response=mock_response))
|
||||||
|
|
||||||
|
with pytest.raises(httpx.HTTPStatusError):
|
||||||
|
await _download_with_retry(mock_storage, "test_path")
|
||||||
|
|
||||||
|
# Should not retry on 4xx errors
|
||||||
|
assert mock_storage.download.await_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_download_with_retry_fallback_error():
|
||||||
|
"""Test fallback HTTPException when no specific error is caught."""
|
||||||
|
mock_storage = MagicMock()
|
||||||
|
mock_storage.download = AsyncMock(side_effect=Exception("Unknown error"))
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
with pytest.raises(Exception) as exc_info:
|
||||||
|
await _download_with_retry(mock_storage, "test_path")
|
||||||
|
|
||||||
|
# Should raise the original exception for unknown errors
|
||||||
|
assert str(exc_info.value) == "Unknown error"
|
||||||
1724
api/uv.lock
generated
Normal file
1724
api/uv.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,17 +4,19 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: ${POSTGRES_DB:-rehearsalhub}
|
POSTGRES_DB: ${POSTGRES_DB:-rehearsalhub}
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-rh_user}
|
POSTGRES_USER: ${POSTGRES_USER:-rh_user}
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-default_secure_password}
|
||||||
volumes:
|
volumes:
|
||||||
- pg_data:/var/lib/postgresql/data
|
- pg_data:/var/lib/postgresql/data
|
||||||
networks:
|
networks:
|
||||||
- rh_net
|
- rh_net
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-rh_user} -d ${POSTGRES_DB:-rehearsalhub}"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-rh_user} -d ${POSTGRES_DB:-rehearsalhub} || exit 1"]
|
||||||
interval: 5s
|
interval: 15s
|
||||||
timeout: 5s
|
timeout: 10s
|
||||||
retries: 10
|
retries: 30
|
||||||
|
start_period: 45s
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
command: ["postgres", "-c", "max_connections=200", "-c", "shared_buffers=256MB"]
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
@@ -24,11 +26,16 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- rh_net
|
- rh_net
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
test: ["CMD-SHELL", "redis-cli ping || exit 1"]
|
||||||
interval: 5s
|
interval: 10s
|
||||||
timeout: 3s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 15
|
||||||
|
start_period: 25s
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
api:
|
api:
|
||||||
build:
|
build:
|
||||||
@@ -36,12 +43,12 @@ services:
|
|||||||
target: production
|
target: production
|
||||||
image: rehearsalhub/api:latest
|
image: rehearsalhub/api:latest
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-rh_user}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-rehearsalhub}
|
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-rh_user}:${POSTGRES_PASSWORD:-default_secure_password}@db:5432/${POSTGRES_DB:-rehearsalhub}
|
||||||
NEXTCLOUD_URL: ${NEXTCLOUD_URL}
|
NEXTCLOUD_URL: ${NEXTCLOUD_URL:-https://cloud.example.com}
|
||||||
NEXTCLOUD_USER: ${NEXTCLOUD_USER}
|
NEXTCLOUD_USER: ${NEXTCLOUD_USER:-rh_service}
|
||||||
NEXTCLOUD_PASS: ${NEXTCLOUD_PASS}
|
NEXTCLOUD_PASS: ${NEXTCLOUD_PASS:-default_password}
|
||||||
REDIS_URL: redis://redis:6379/0
|
REDIS_URL: redis://redis:6379/0
|
||||||
SECRET_KEY: ${SECRET_KEY}
|
SECRET_KEY: ${SECRET_KEY:-replace_me_with_32_byte_hex_default}
|
||||||
DOMAIN: ${DOMAIN:-localhost}
|
DOMAIN: ${DOMAIN:-localhost}
|
||||||
networks:
|
networks:
|
||||||
- rh_net
|
- rh_net
|
||||||
@@ -50,7 +57,17 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"]
|
||||||
|
interval: 20s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 5
|
||||||
|
start_period: 60s
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
audio-worker:
|
audio-worker:
|
||||||
build:
|
build:
|
||||||
@@ -58,11 +75,11 @@ services:
|
|||||||
target: production
|
target: production
|
||||||
image: rehearsalhub/audio-worker:latest
|
image: rehearsalhub/audio-worker:latest
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-rh_user}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-rehearsalhub}
|
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-rh_user}:${POSTGRES_PASSWORD:-default_secure_password}@db:5432/${POSTGRES_DB:-rehearsalhub}
|
||||||
REDIS_URL: redis://redis:6379/0
|
REDIS_URL: redis://redis:6379/0
|
||||||
NEXTCLOUD_URL: ${NEXTCLOUD_URL}
|
NEXTCLOUD_URL: ${NEXTCLOUD_URL:-https://cloud.example.com}
|
||||||
NEXTCLOUD_USER: ${NEXTCLOUD_USER}
|
NEXTCLOUD_USER: ${NEXTCLOUD_USER:-rh_service}
|
||||||
NEXTCLOUD_PASS: ${NEXTCLOUD_PASS}
|
NEXTCLOUD_PASS: ${NEXTCLOUD_PASS:-default_password}
|
||||||
ANALYSIS_VERSION: "1.0.0"
|
ANALYSIS_VERSION: "1.0.0"
|
||||||
volumes:
|
volumes:
|
||||||
- audio_tmp:/tmp/audio
|
- audio_tmp:/tmp/audio
|
||||||
@@ -73,6 +90,8 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
api:
|
||||||
|
condition: service_started
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
nc-watcher:
|
nc-watcher:
|
||||||
@@ -81,9 +100,9 @@ services:
|
|||||||
target: production
|
target: production
|
||||||
image: rehearsalhub/nc-watcher:latest
|
image: rehearsalhub/nc-watcher:latest
|
||||||
environment:
|
environment:
|
||||||
NEXTCLOUD_URL: ${NEXTCLOUD_URL}
|
NEXTCLOUD_URL: ${NEXTCLOUD_URL:-https://cloud.example.com}
|
||||||
NEXTCLOUD_USER: ${NEXTCLOUD_USER}
|
NEXTCLOUD_USER: ${NEXTCLOUD_USER:-rh_service}
|
||||||
NEXTCLOUD_PASS: ${NEXTCLOUD_PASS}
|
NEXTCLOUD_PASS: ${NEXTCLOUD_PASS:-default_password}
|
||||||
API_URL: http://api:8000
|
API_URL: http://api:8000
|
||||||
REDIS_URL: redis://redis:6379/0
|
REDIS_URL: redis://redis:6379/0
|
||||||
POLL_INTERVAL: "30"
|
POLL_INTERVAL: "30"
|
||||||
@@ -94,6 +113,8 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
api:
|
||||||
|
condition: service_started
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
web:
|
web:
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "→ Checking for Nextcloud service..."
|
||||||
|
|
||||||
|
# Check if nextcloud service exists
|
||||||
|
if ! docker compose ps | grep -q nextcloud; then
|
||||||
|
echo " Nextcloud service not found in compose setup"
|
||||||
|
echo " Skipping Nextcloud configuration (external setup required)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
echo "→ Configuring Nextcloud via occ..."
|
echo "→ Configuring Nextcloud via occ..."
|
||||||
|
|
||||||
NC="docker compose exec -T nextcloud php occ"
|
NC="docker compose exec -T nextcloud php occ"
|
||||||
|
|||||||
40
test_websocket.html
Normal file
40
test_websocket.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>WebSocket Test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>WebSocket Test</h1>
|
||||||
|
<p id="status">Connecting...</p>
|
||||||
|
<script>
|
||||||
|
const versionId = "625b4a46-bb84-44c5-bfce-13f62b2b4dcf"; // Use the same ID from the error
|
||||||
|
const ws = new WebSocket(`ws://${window.location.host}/ws/versions/${versionId}`);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
document.getElementById("status").textContent = "Connected!";
|
||||||
|
document.getElementById("status").style.color = "green";
|
||||||
|
|
||||||
|
// Send a ping
|
||||||
|
ws.send(JSON.stringify({ event: "ping" }));
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
console.log("Message received:", event.data);
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.event === "pong") {
|
||||||
|
document.getElementById("status").textContent += " Pong received!";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
document.getElementById("status").textContent = "Error: " + error.message;
|
||||||
|
document.getElementById("status").style.color = "red";
|
||||||
|
console.error("WebSocket error:", error);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
document.getElementById("status").textContent += " Connection closed";
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Stage 1: Essentia builder
|
# Stage 1: Essentia builder
|
||||||
# Essentia doesn't have wheels for Python 3.12 yet; we use the official image
|
# Essentia doesn't have wheels for Python 3.12 yet; we use the official image
|
||||||
# and copy the bindings into our final stage via a bind mount.
|
# and copy the bindings into our final stage via a bind mount.
|
||||||
FROM mtgupf/essentia:latest AS essentia-builder
|
FROM docker.io/mtgupf/essentia:latest AS essentia-builder
|
||||||
|
|
||||||
FROM python:3.12-slim AS base
|
FROM python:3.12-slim AS base
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
Reference in New Issue
Block a user