Add health check endpoints and automatic language detection
Health Check Features (Item 12): - Added /health endpoint for basic health monitoring - Added /health/detailed for comprehensive component status - Added /health/ready for Kubernetes readiness probes - Added /health/live for liveness checks - Frontend health monitoring with auto-recovery - Clear stuck requests after 60 seconds - Visual health warnings when service is degraded - Monitoring script for external health checks Automatic Language Detection (Item 13): - Added "Auto-detect" option in source language dropdown - Whisper automatically detects language when auto-detect is selected - Shows detected language in UI after transcription - Updates language selector with detected language - Caches transcriptions with correct detected language 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -106,6 +106,9 @@ function initApp(): void {
|
||||
|
||||
// Initialize queue status updates
|
||||
initQueueStatus();
|
||||
|
||||
// Start health monitoring
|
||||
startHealthMonitoring();
|
||||
|
||||
// Update TTS server URL and API key
|
||||
updateTtsServer.addEventListener('click', function() {
|
||||
@@ -153,6 +156,11 @@ function initApp(): void {
|
||||
|
||||
// Event listeners for language selection
|
||||
sourceLanguage.addEventListener('change', function() {
|
||||
// Skip conflict check for auto-detect
|
||||
if (sourceLanguage.value === 'auto') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetLanguage.value === sourceLanguage.value) {
|
||||
for (let i = 0; i < targetLanguage.options.length; i++) {
|
||||
if (targetLanguage.options[i].value !== sourceLanguage.value) {
|
||||
@@ -383,10 +391,24 @@ function initApp(): void {
|
||||
|
||||
if (data.success && data.text) {
|
||||
currentSourceText = data.text;
|
||||
sourceText.innerHTML = `<p class="fade-in">${data.text}</p>`;
|
||||
|
||||
// Handle auto-detected language
|
||||
if (data.detected_language && sourceLanguage.value === 'auto') {
|
||||
// Update the source language selector
|
||||
sourceLanguage.value = data.detected_language;
|
||||
|
||||
// Show detected language info
|
||||
sourceText.innerHTML = `<p class="fade-in">${data.text}</p>
|
||||
<small class="text-muted">Detected language: ${data.detected_language}</small>`;
|
||||
|
||||
statusIndicator.textContent = `Transcription complete (${data.detected_language} detected)`;
|
||||
} else {
|
||||
sourceText.innerHTML = `<p class="fade-in">${data.text}</p>`;
|
||||
statusIndicator.textContent = 'Transcription complete';
|
||||
}
|
||||
|
||||
playSource.disabled = false;
|
||||
translateBtn.disabled = false;
|
||||
statusIndicator.textContent = 'Transcription complete';
|
||||
statusIndicator.classList.remove('processing');
|
||||
statusIndicator.classList.add('success');
|
||||
setTimeout(() => statusIndicator.classList.remove('success'), 2000);
|
||||
@@ -394,7 +416,7 @@ function initApp(): void {
|
||||
// Cache the transcription in IndexedDB
|
||||
saveToIndexedDB('transcriptions', {
|
||||
text: data.text,
|
||||
language: sourceLanguage.value,
|
||||
language: data.detected_language || sourceLanguage.value,
|
||||
timestamp: new Date().toISOString()
|
||||
} as TranscriptionRecord);
|
||||
} else {
|
||||
@@ -753,6 +775,99 @@ function initApp(): void {
|
||||
// Initial update
|
||||
updateQueueDisplay();
|
||||
}
|
||||
|
||||
// Health monitoring and auto-recovery
|
||||
function startHealthMonitoring(): void {
|
||||
let consecutiveFailures = 0;
|
||||
const maxConsecutiveFailures = 3;
|
||||
|
||||
async function checkHealth(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch('/health', {
|
||||
method: 'GET',
|
||||
signal: AbortSignal.timeout(5000) // 5 second timeout
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
consecutiveFailures = 0;
|
||||
|
||||
// Remove any health warning if shown
|
||||
const healthWarning = document.getElementById('healthWarning');
|
||||
if (healthWarning) {
|
||||
healthWarning.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
handleHealthCheckFailure();
|
||||
}
|
||||
} catch (error) {
|
||||
handleHealthCheckFailure();
|
||||
}
|
||||
}
|
||||
|
||||
function handleHealthCheckFailure(): void {
|
||||
consecutiveFailures++;
|
||||
console.warn(`Health check failed (${consecutiveFailures}/${maxConsecutiveFailures})`);
|
||||
|
||||
if (consecutiveFailures >= maxConsecutiveFailures) {
|
||||
showHealthWarning();
|
||||
|
||||
// Attempt auto-recovery
|
||||
attemptAutoRecovery();
|
||||
}
|
||||
}
|
||||
|
||||
function showHealthWarning(): void {
|
||||
let healthWarning = document.getElementById('healthWarning');
|
||||
if (!healthWarning) {
|
||||
healthWarning = document.createElement('div');
|
||||
healthWarning.id = 'healthWarning';
|
||||
healthWarning.className = 'alert alert-warning alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3';
|
||||
healthWarning.style.zIndex = '9999';
|
||||
healthWarning.innerHTML = `
|
||||
<i class="fas fa-exclamation-triangle"></i> Service health check failed.
|
||||
Some features may be unavailable.
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(healthWarning);
|
||||
}
|
||||
healthWarning.style.display = 'block';
|
||||
}
|
||||
|
||||
async function attemptAutoRecovery(): Promise<void> {
|
||||
console.log('Attempting auto-recovery...');
|
||||
|
||||
// Clear any stuck requests in the queue
|
||||
const queue = RequestQueueManager.getInstance();
|
||||
queue.clearStuckRequests();
|
||||
|
||||
// Re-check TTS server
|
||||
checkTtsServer();
|
||||
|
||||
// Try to reload service worker if available
|
||||
if ('serviceWorker' in navigator) {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.getRegistration();
|
||||
if (registration) {
|
||||
await registration.update();
|
||||
console.log('Service worker updated');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update service worker:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset failure counter after recovery attempt
|
||||
setTimeout(() => {
|
||||
consecutiveFailures = 0;
|
||||
}, 30000); // Wait 30 seconds before resetting
|
||||
}
|
||||
|
||||
// Check health every 30 seconds
|
||||
setInterval(checkHealth, 30000);
|
||||
|
||||
// Initial health check after 5 seconds
|
||||
setTimeout(checkHealth, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@@ -216,6 +216,31 @@ export class RequestQueueManager {
|
||||
});
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
// Clear stuck requests (requests older than 60 seconds)
|
||||
clearStuckRequests(): void {
|
||||
const now = Date.now();
|
||||
const stuckThreshold = 60000; // 60 seconds
|
||||
|
||||
// Clear stuck active requests
|
||||
this.activeRequests.forEach((request, id) => {
|
||||
if (now - request.timestamp > stuckThreshold) {
|
||||
console.warn(`Clearing stuck active request: ${request.type}`);
|
||||
request.reject(new Error('Request timeout - cleared by recovery'));
|
||||
this.activeRequests.delete(id);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear old queued requests
|
||||
this.queue = this.queue.filter(request => {
|
||||
if (now - request.timestamp > stuckThreshold) {
|
||||
console.warn(`Clearing stuck queued request: ${request.type}`);
|
||||
request.reject(new Error('Request timeout - cleared by recovery'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Update settings
|
||||
updateSettings(settings: {
|
||||
|
@@ -4,6 +4,7 @@ export interface TranscriptionResponse {
|
||||
success: boolean;
|
||||
text?: string;
|
||||
error?: string;
|
||||
detected_language?: string;
|
||||
}
|
||||
|
||||
export interface TranslationResponse {
|
||||
|
Reference in New Issue
Block a user