🪻 distributed transcription service thistle.dunkirk.sh
1
fork

Configure Feed

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

feat: add consistent error handling in the admin ui

+96 -52
+9 -7
src/components/admin-classes.ts
··· 227 227 color: var(--paynes-gray); 228 228 } 229 229 230 - .error-message { 231 - background: #fee2e2; 232 - color: #991b1b; 233 - padding: 1rem; 230 + .error-banner { 231 + background: #fecaca; 232 + border: 2px solid rgba(220, 38, 38, 0.8); 234 233 border-radius: 6px; 235 - margin-bottom: 1rem; 234 + padding: 1rem; 235 + margin-bottom: 1.5rem; 236 + color: #dc2626; 237 + font-weight: 500; 236 238 } 237 239 238 240 .tabs { ··· 656 658 const filteredClasses = this.getFilteredClasses(); 657 659 658 660 return html` 659 - ${this.error ? html`<div class="error-message">${this.error}</div>` : ""} 661 + ${this.error ? html`<div class="error-banner">${this.error}</div>` : ""} 660 662 661 663 <div class="tabs"> 662 664 <button ··· 926 928 ${description} 927 929 </p> 928 930 929 - ${this.error ? html`<div class="error-message">${this.error}</div>` : ""} 931 + ${this.error ? html`<div class="error-banner">${this.error}</div>` : ""} 930 932 931 933 <div class="form-grid"> 932 934 <div class="form-group">
+32 -18
src/components/admin-pending-recordings.ts
··· 46 46 display: block; 47 47 } 48 48 49 + .error-banner { 50 + background: #fecaca; 51 + border: 2px solid rgba(220, 38, 38, 0.8); 52 + border-radius: 6px; 53 + padding: 1rem; 54 + margin-bottom: 1.5rem; 55 + color: #dc2626; 56 + font-weight: 500; 57 + } 58 + 49 59 .loading, 50 60 .empty-state { 51 61 text-align: center; ··· 236 246 this.isLoading = true; 237 247 this.error = null; 238 248 239 - try { 240 - // Get all classes with their transcriptions 241 - const response = await fetch("/api/classes"); 242 - if (!response.ok) { 243 - throw new Error("Failed to load classes"); 244 - } 249 + try { 250 + // Get all classes with their transcriptions 251 + const response = await fetch("/api/classes"); 252 + if (!response.ok) { 253 + const data = await response.json(); 254 + throw new Error(data.error || "Failed to load classes"); 255 + } 245 256 246 257 const data = await response.json(); 247 258 const classesGrouped = data.classes || {}; ··· 305 316 pendingRecordings.sort((a, b) => b.created_at - a.created_at); 306 317 307 318 this.recordings = pendingRecordings; 308 - } catch (error) { 309 - console.error("Failed to load pending recordings:", error); 310 - this.error = "Failed to load pending recordings. Please try again."; 319 + } catch (err) { 320 + this.error = err instanceof Error ? err.message : "Failed to load pending recordings. Please try again."; 311 321 } finally { 312 322 this.isLoading = false; 313 323 } 314 324 } 315 325 316 326 private async handleApprove(recordingId: string) { 327 + this.error = null; 317 328 try { 318 329 const response = await fetch(`/api/transcripts/${recordingId}/select`, { 319 330 method: "PUT", 320 331 }); 321 332 322 333 if (!response.ok) { 323 - throw new Error("Failed to approve recording"); 334 + const data = await response.json(); 335 + throw new Error(data.error || "Failed to approve recording"); 324 336 } 325 337 326 338 // Reload recordings 327 339 await this.loadRecordings(); 328 - } catch (error) { 329 - console.error("Failed to approve recording:", error); 330 - alert("Failed to approve recording. Please try again."); 340 + } catch (err) { 341 + this.error = err instanceof Error ? err.message : "Failed to approve recording. Please try again."; 331 342 } 332 343 } 333 344 ··· 340 351 return; 341 352 } 342 353 354 + this.error = null; 343 355 try { 344 356 const response = await fetch(`/api/admin/transcriptions/${recordingId}`, { 345 357 method: "DELETE", 346 358 }); 347 359 348 360 if (!response.ok) { 349 - throw new Error("Failed to delete recording"); 361 + const data = await response.json(); 362 + throw new Error(data.error || "Failed to delete recording"); 350 363 } 351 364 352 365 // Reload recordings 353 366 await this.loadRecordings(); 354 - } catch (error) { 355 - console.error("Failed to delete recording:", error); 356 - alert("Failed to delete recording. Please try again."); 367 + } catch (err) { 368 + this.error = err instanceof Error ? err.message : "Failed to delete recording. Please try again."; 357 369 } 358 370 } 359 371 ··· 369 381 370 382 if (this.error) { 371 383 return html` 372 - <div class="error">${this.error}</div> 384 + <div class="error-banner">${this.error}</div> 373 385 <button @click=${this.loadRecordings}>Retry</button> 374 386 `; 375 387 } ··· 383 395 } 384 396 385 397 return html` 398 + ${this.error ? html`<div class="error-banner">${this.error}</div>` : ""} 399 + 386 400 <div class="recordings-grid"> 387 401 ${this.recordings.map( 388 402 (recording) => html`
+22 -9
src/components/admin-transcriptions.ts
··· 24 24 display: block; 25 25 } 26 26 27 + .error-banner { 28 + background: #fecaca; 29 + border: 2px solid rgba(220, 38, 38, 0.8); 30 + border-radius: 6px; 31 + padding: 1rem; 32 + margin-bottom: 1.5rem; 33 + color: #dc2626; 34 + font-weight: 500; 35 + } 36 + 27 37 .search-box { 28 38 width: 100%; 29 39 max-width: 30rem; ··· 180 190 try { 181 191 const response = await fetch("/api/admin/transcriptions"); 182 192 if (!response.ok) { 183 - throw new Error("Failed to load transcriptions"); 193 + const data = await response.json(); 194 + throw new Error(data.error || "Failed to load transcriptions"); 184 195 } 185 196 186 197 this.transcriptions = await response.json(); 187 - } catch (error) { 188 - console.error("Failed to load transcriptions:", error); 189 - this.error = "Failed to load transcriptions. Please try again."; 198 + } catch (err) { 199 + this.error = err instanceof Error ? err.message : "Failed to load transcriptions. Please try again."; 190 200 } finally { 191 201 this.isLoading = false; 192 202 } ··· 201 211 return; 202 212 } 203 213 214 + this.error = null; 204 215 try { 205 216 const response = await fetch( 206 217 `/api/admin/transcriptions/${transcriptionId}`, ··· 210 221 ); 211 222 212 223 if (!response.ok) { 213 - throw new Error("Failed to delete transcription"); 224 + const data = await response.json(); 225 + throw new Error(data.error || "Failed to delete transcription"); 214 226 } 215 227 216 228 await this.loadTranscriptions(); 217 229 this.dispatchEvent(new CustomEvent("transcription-deleted")); 218 - } catch (error) { 219 - console.error("Failed to delete transcription:", error); 220 - alert("Failed to delete transcription. Please try again."); 230 + } catch (err) { 231 + this.error = err instanceof Error ? err.message : "Failed to delete transcription. Please try again."; 221 232 } 222 233 } 223 234 ··· 257 268 258 269 if (this.error) { 259 270 return html` 260 - <div class="error">${this.error}</div> 271 + <div class="error-banner">${this.error}</div> 261 272 <button @click=${this.loadTranscriptions}>Retry</button> 262 273 `; 263 274 } ··· 265 276 const filtered = this.filteredTranscriptions; 266 277 267 278 return html` 279 + ${this.error ? html`<div class="error-banner">${this.error}</div>` : ""} 280 + 268 281 <input 269 282 type="text" 270 283 class="search-box"
+33 -18
src/components/admin-users.ts
··· 29 29 display: block; 30 30 } 31 31 32 + .error-banner { 33 + background: #fecaca; 34 + border: 2px solid rgba(220, 38, 38, 0.8); 35 + border-radius: 6px; 36 + padding: 1rem; 37 + margin-bottom: 1.5rem; 38 + color: #dc2626; 39 + font-weight: 500; 40 + } 41 + 32 42 .search-box { 33 43 width: 100%; 34 44 max-width: 30rem; ··· 310 320 try { 311 321 const response = await fetch("/api/admin/users"); 312 322 if (!response.ok) { 313 - throw new Error("Failed to load users"); 323 + const data = await response.json(); 324 + throw new Error(data.error || "Failed to load users"); 314 325 } 315 326 316 327 this.users = await response.json(); 317 - } catch (error) { 318 - console.error("Failed to load users:", error); 319 - this.error = "Failed to load users. Please try again."; 328 + } catch (err) { 329 + this.error = err instanceof Error ? err.message : "Failed to load users. Please try again."; 320 330 } finally { 321 331 this.isLoading = false; 322 332 } ··· 369 379 }); 370 380 371 381 if (!response.ok) { 372 - throw new Error("Failed to update role"); 382 + const data = await response.json(); 383 + throw new Error(data.error || "Failed to update role"); 373 384 } 374 385 375 386 if (isDemotingSelf) { ··· 377 388 } else { 378 389 await this.loadUsers(); 379 390 } 380 - } catch (error) { 381 - console.error("Failed to update role:", error); 382 - alert("Failed to update user role"); 391 + } catch (err) { 392 + this.error = err instanceof Error ? err.message : "Failed to update user role"; 383 393 select.value = oldRole; 384 394 } 385 395 } ··· 438 448 } 439 449 440 450 private async performDeleteUser(userId: number) { 451 + this.error = null; 441 452 try { 442 453 const response = await fetch(`/api/admin/users/${userId}`, { 443 454 method: "DELETE", 444 455 }); 445 456 446 457 if (!response.ok) { 447 - throw new Error("Failed to delete user"); 458 + const data = await response.json(); 459 + throw new Error(data.error || "Failed to delete user"); 448 460 } 449 461 450 462 // Remove user from local array instead of reloading 451 463 this.users = this.users.filter(u => u.id !== userId); 452 464 this.dispatchEvent(new CustomEvent("user-deleted")); 453 - } catch { 454 - alert("Failed to delete user. Please try again."); 465 + } catch (err) { 466 + this.error = err instanceof Error ? err.message : "Failed to delete user. Please try again."; 455 467 } 456 468 } 457 469 ··· 501 513 this.deleteState = { id: userId, type: "revoke", clicks: newClicks, timeout }; 502 514 } 503 515 504 - private async performRevokeSubscription(userId: number, email: string, subscriptionId: string) { 516 + private async performRevokeSubscription(userId: number, _email: string, subscriptionId: string) { 505 517 this.revokingSubscriptions.add(userId); 506 518 this.requestUpdate(); 519 + this.error = null; 507 520 508 521 try { 509 522 const response = await fetch(`/api/admin/users/${userId}/subscription`, { ··· 518 531 } 519 532 520 533 await this.loadUsers(); 521 - alert(`Subscription revoked for ${email}`); 522 - } catch (error) { 523 - alert(`Failed to revoke subscription: ${error instanceof Error ? error.message : "Unknown error"}`); 534 + } catch (err) { 535 + this.error = err instanceof Error ? err.message : "Failed to revoke subscription"; 524 536 this.revokingSubscriptions.delete(userId); 525 537 } 526 538 } ··· 530 542 531 543 this.syncingSubscriptions.add(userId); 532 544 this.requestUpdate(); 545 + this.error = null; 533 546 534 547 try { 535 548 const response = await fetch(`/api/admin/users/${userId}/subscription`, { ··· 539 552 540 553 if (!response.ok) { 541 554 const data = await response.json(); 542 - // Don't alert if there's just no subscription 555 + // Don't show error if there's just no subscription 543 556 if (response.status !== 404) { 544 - alert(`Failed to sync subscription: ${data.error || "Unknown error"}`); 557 + this.error = data.error || "Failed to sync subscription"; 545 558 } 546 559 return; 547 560 } ··· 626 639 627 640 if (this.error) { 628 641 return html` 629 - <div class="error">${this.error}</div> 642 + <div class="error-banner">${this.error}</div> 630 643 <button @click=${this.loadUsers}>Retry</button> 631 644 `; 632 645 } ··· 634 647 const filtered = this.filteredUsers; 635 648 636 649 return html` 650 + ${this.error ? html`<div class="error-banner">${this.error}</div>` : ""} 651 + 637 652 <input 638 653 type="text" 639 654 class="search-box"