🪻 distributed transcription service thistle.dunkirk.sh
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: multiple low-priority improvements

- Session cleanup: Run every 15 minutes instead of hourly (#26)
- Health check: Add detailed status for database, whisper, and storage (#25)
- Admin email change: Require verification unless skipVerification flag set (#36)
- Transcription selection: Validate existence and status before selecting (#28)

These changes improve system monitoring, security, and error handling.

+133 -19
+133 -19
src/index.ts
··· 164 164 transcriptionEvents, 165 165 ); 166 166 167 - // Clean up expired sessions every hour 167 + // Clean up expired sessions every 15 minutes 168 168 const sessionCleanupInterval = setInterval( 169 169 cleanupExpiredSessions, 170 - 60 * 60 * 1000, 170 + 15 * 60 * 1000, 171 171 ); 172 172 173 173 // Helper function to sync user subscriptions from Polar ··· 1795 1795 }, 1796 1796 "/api/transcriptions/health": { 1797 1797 GET: async () => { 1798 - const isHealthy = await whisperService.checkHealth(); 1799 - return Response.json({ available: isHealthy }); 1798 + const health = { 1799 + status: "healthy", 1800 + timestamp: new Date().toISOString(), 1801 + services: { 1802 + database: false, 1803 + whisper: false, 1804 + storage: false, 1805 + }, 1806 + details: {} as Record<string, unknown>, 1807 + }; 1808 + 1809 + // Check database 1810 + try { 1811 + db.query("SELECT 1").get(); 1812 + health.services.database = true; 1813 + } catch (error) { 1814 + health.status = "unhealthy"; 1815 + health.details.databaseError = 1816 + error instanceof Error ? error.message : String(error); 1817 + } 1818 + 1819 + // Check Whisper service 1820 + try { 1821 + const whisperHealthy = await whisperService.checkHealth(); 1822 + health.services.whisper = whisperHealthy; 1823 + if (!whisperHealthy) { 1824 + health.status = "degraded"; 1825 + health.details.whisperNote = "Whisper service unavailable"; 1826 + } 1827 + } catch (error) { 1828 + health.status = "degraded"; 1829 + health.details.whisperError = 1830 + error instanceof Error ? error.message : String(error); 1831 + } 1832 + 1833 + // Check storage (uploads and transcripts directories) 1834 + try { 1835 + const uploadsDir = Bun.file("./uploads"); 1836 + const transcriptsDir = Bun.file("./transcripts"); 1837 + const uploadsExists = await uploadsDir.exists(); 1838 + const transcriptsExists = await transcriptsDir.exists(); 1839 + health.services.storage = uploadsExists && transcriptsExists; 1840 + if (!health.services.storage) { 1841 + health.status = "unhealthy"; 1842 + health.details.storageNote = `Missing directories: ${[ 1843 + !uploadsExists && "uploads", 1844 + !transcriptsExists && "transcripts", 1845 + ] 1846 + .filter(Boolean) 1847 + .join(", ")}`; 1848 + } 1849 + } catch (error) { 1850 + health.status = "unhealthy"; 1851 + health.details.storageError = 1852 + error instanceof Error ? error.message : String(error); 1853 + } 1854 + 1855 + const statusCode = health.status === "healthy" ? 200 : 503; 1856 + return Response.json(health, { status: statusCode }); 1800 1857 }, 1801 1858 }, 1802 1859 "/api/transcriptions/:id": { ··· 2583 2640 }); 2584 2641 } 2585 2642 2586 - updateUserEmailAddress(userId, email); 2587 - return Response.json({ success: true }); 2643 + // Get user's current email 2644 + const user = db 2645 + .query<{ email: string; name: string | null }, [number]>( 2646 + "SELECT email, name FROM users WHERE id = ?", 2647 + ) 2648 + .get(userId); 2649 + 2650 + if (!user) { 2651 + return Response.json({ error: "User not found" }, { status: 404 }); 2652 + } 2653 + 2654 + // Send verification email to user's current email 2655 + try { 2656 + const token = createEmailChangeToken(userId, email); 2657 + const origin = process.env.ORIGIN || "http://localhost:3000"; 2658 + const verifyUrl = `${origin}/api/user/email/verify?token=${token}`; 2659 + 2660 + await sendEmail({ 2661 + to: user.email, 2662 + subject: "Verify your email change", 2663 + html: emailChangeTemplate({ 2664 + name: user.name, 2665 + currentEmail: user.email, 2666 + newEmail: email, 2667 + verifyLink: verifyUrl, 2668 + }), 2669 + }); 2670 + 2671 + return Response.json({ 2672 + success: true, 2673 + message: `Verification email sent to ${user.email}`, 2674 + pendingEmail: email, 2675 + }); 2676 + } catch (emailError) { 2677 + console.error( 2678 + "[Admin] Failed to send email change verification:", 2679 + emailError, 2680 + ); 2681 + return Response.json( 2682 + { error: "Failed to send verification email" }, 2683 + { status: 500 }, 2684 + ); 2685 + } 2588 2686 } catch (error) { 2589 2687 return handleError(error); 2590 2688 } ··· 3137 3235 requireAdmin(req); 3138 3236 const transcriptId = req.params.id; 3139 3237 3140 - // Update status to 'selected' and start transcription 3141 - db.run("UPDATE transcriptions SET status = ? WHERE id = ?", [ 3142 - "selected", 3143 - transcriptId, 3144 - ]); 3145 - 3146 - // Get filename to start transcription 3238 + // Check if transcription exists and get its current status 3147 3239 const transcription = db 3148 - .query<{ filename: string }, [string]>( 3149 - "SELECT filename FROM transcriptions WHERE id = ?", 3240 + .query<{ filename: string; status: string }, [string]>( 3241 + "SELECT filename, status FROM transcriptions WHERE id = ?", 3150 3242 ) 3151 3243 .get(transcriptId); 3152 3244 3153 - if (transcription) { 3154 - whisperService.startTranscription( 3155 - transcriptId, 3156 - transcription.filename, 3245 + if (!transcription) { 3246 + return Response.json( 3247 + { error: "Transcription not found" }, 3248 + { status: 404 }, 3157 3249 ); 3158 3250 } 3251 + 3252 + // Validate that status is appropriate for selection (e.g., 'uploading' or 'pending') 3253 + const validStatuses = ["uploading", "pending", "failed"]; 3254 + if (!validStatuses.includes(transcription.status)) { 3255 + return Response.json( 3256 + { 3257 + error: `Cannot select transcription with status: ${transcription.status}`, 3258 + }, 3259 + { status: 400 }, 3260 + ); 3261 + } 3262 + 3263 + // Update status to 'selected' and start transcription 3264 + db.run("UPDATE transcriptions SET status = ? WHERE id = ?", [ 3265 + "selected", 3266 + transcriptId, 3267 + ]); 3268 + 3269 + whisperService.startTranscription( 3270 + transcriptId, 3271 + transcription.filename, 3272 + ); 3159 3273 3160 3274 return Response.json({ success: true }); 3161 3275 } catch (error) {