🪻 distributed transcription service thistle.dunkirk.sh

feat: add consistent error handling in the admin ui

dunkirk.sh d206be10 c2a259ab

verified
+9 -7
src/components/admin-classes.ts
···
color: var(--paynes-gray);
}
-
.error-message {
-
background: #fee2e2;
-
color: #991b1b;
-
padding: 1rem;
+
.error-banner {
+
background: #fecaca;
+
border: 2px solid rgba(220, 38, 38, 0.8);
border-radius: 6px;
-
margin-bottom: 1rem;
+
padding: 1rem;
+
margin-bottom: 1.5rem;
+
color: #dc2626;
+
font-weight: 500;
}
.tabs {
···
const filteredClasses = this.getFilteredClasses();
return html`
-
${this.error ? html`<div class="error-message">${this.error}</div>` : ""}
+
${this.error ? html`<div class="error-banner">${this.error}</div>` : ""}
<div class="tabs">
<button
···
${description}
</p>
-
${this.error ? html`<div class="error-message">${this.error}</div>` : ""}
+
${this.error ? html`<div class="error-banner">${this.error}</div>` : ""}
<div class="form-grid">
<div class="form-group">
+32 -18
src/components/admin-pending-recordings.ts
···
display: block;
}
+
.error-banner {
+
background: #fecaca;
+
border: 2px solid rgba(220, 38, 38, 0.8);
+
border-radius: 6px;
+
padding: 1rem;
+
margin-bottom: 1.5rem;
+
color: #dc2626;
+
font-weight: 500;
+
}
+
.loading,
.empty-state {
text-align: center;
···
this.isLoading = true;
this.error = null;
-
try {
-
// Get all classes with their transcriptions
-
const response = await fetch("/api/classes");
-
if (!response.ok) {
-
throw new Error("Failed to load classes");
-
}
+
try {
+
// Get all classes with their transcriptions
+
const response = await fetch("/api/classes");
+
if (!response.ok) {
+
const data = await response.json();
+
throw new Error(data.error || "Failed to load classes");
+
}
const data = await response.json();
const classesGrouped = data.classes || {};
···
pendingRecordings.sort((a, b) => b.created_at - a.created_at);
this.recordings = pendingRecordings;
-
} catch (error) {
-
console.error("Failed to load pending recordings:", error);
-
this.error = "Failed to load pending recordings. Please try again.";
+
} catch (err) {
+
this.error = err instanceof Error ? err.message : "Failed to load pending recordings. Please try again.";
} finally {
this.isLoading = false;
}
}
private async handleApprove(recordingId: string) {
+
this.error = null;
try {
const response = await fetch(`/api/transcripts/${recordingId}/select`, {
method: "PUT",
});
if (!response.ok) {
-
throw new Error("Failed to approve recording");
+
const data = await response.json();
+
throw new Error(data.error || "Failed to approve recording");
}
// Reload recordings
await this.loadRecordings();
-
} catch (error) {
-
console.error("Failed to approve recording:", error);
-
alert("Failed to approve recording. Please try again.");
+
} catch (err) {
+
this.error = err instanceof Error ? err.message : "Failed to approve recording. Please try again.";
}
}
···
return;
}
+
this.error = null;
try {
const response = await fetch(`/api/admin/transcriptions/${recordingId}`, {
method: "DELETE",
});
if (!response.ok) {
-
throw new Error("Failed to delete recording");
+
const data = await response.json();
+
throw new Error(data.error || "Failed to delete recording");
}
// Reload recordings
await this.loadRecordings();
-
} catch (error) {
-
console.error("Failed to delete recording:", error);
-
alert("Failed to delete recording. Please try again.");
+
} catch (err) {
+
this.error = err instanceof Error ? err.message : "Failed to delete recording. Please try again.";
}
}
···
if (this.error) {
return html`
-
<div class="error">${this.error}</div>
+
<div class="error-banner">${this.error}</div>
<button @click=${this.loadRecordings}>Retry</button>
`;
}
···
}
return html`
+
${this.error ? html`<div class="error-banner">${this.error}</div>` : ""}
+
<div class="recordings-grid">
${this.recordings.map(
(recording) => html`
+22 -9
src/components/admin-transcriptions.ts
···
display: block;
}
+
.error-banner {
+
background: #fecaca;
+
border: 2px solid rgba(220, 38, 38, 0.8);
+
border-radius: 6px;
+
padding: 1rem;
+
margin-bottom: 1.5rem;
+
color: #dc2626;
+
font-weight: 500;
+
}
+
.search-box {
width: 100%;
max-width: 30rem;
···
try {
const response = await fetch("/api/admin/transcriptions");
if (!response.ok) {
-
throw new Error("Failed to load transcriptions");
+
const data = await response.json();
+
throw new Error(data.error || "Failed to load transcriptions");
}
this.transcriptions = await response.json();
-
} catch (error) {
-
console.error("Failed to load transcriptions:", error);
-
this.error = "Failed to load transcriptions. Please try again.";
+
} catch (err) {
+
this.error = err instanceof Error ? err.message : "Failed to load transcriptions. Please try again.";
} finally {
this.isLoading = false;
}
···
return;
}
+
this.error = null;
try {
const response = await fetch(
`/api/admin/transcriptions/${transcriptionId}`,
···
);
if (!response.ok) {
-
throw new Error("Failed to delete transcription");
+
const data = await response.json();
+
throw new Error(data.error || "Failed to delete transcription");
}
await this.loadTranscriptions();
this.dispatchEvent(new CustomEvent("transcription-deleted"));
-
} catch (error) {
-
console.error("Failed to delete transcription:", error);
-
alert("Failed to delete transcription. Please try again.");
+
} catch (err) {
+
this.error = err instanceof Error ? err.message : "Failed to delete transcription. Please try again.";
}
}
···
if (this.error) {
return html`
-
<div class="error">${this.error}</div>
+
<div class="error-banner">${this.error}</div>
<button @click=${this.loadTranscriptions}>Retry</button>
`;
}
···
const filtered = this.filteredTranscriptions;
return html`
+
${this.error ? html`<div class="error-banner">${this.error}</div>` : ""}
+
<input
type="text"
class="search-box"
+33 -18
src/components/admin-users.ts
···
display: block;
}
+
.error-banner {
+
background: #fecaca;
+
border: 2px solid rgba(220, 38, 38, 0.8);
+
border-radius: 6px;
+
padding: 1rem;
+
margin-bottom: 1.5rem;
+
color: #dc2626;
+
font-weight: 500;
+
}
+
.search-box {
width: 100%;
max-width: 30rem;
···
try {
const response = await fetch("/api/admin/users");
if (!response.ok) {
-
throw new Error("Failed to load users");
+
const data = await response.json();
+
throw new Error(data.error || "Failed to load users");
}
this.users = await response.json();
-
} catch (error) {
-
console.error("Failed to load users:", error);
-
this.error = "Failed to load users. Please try again.";
+
} catch (err) {
+
this.error = err instanceof Error ? err.message : "Failed to load users. Please try again.";
} finally {
this.isLoading = false;
}
···
});
if (!response.ok) {
-
throw new Error("Failed to update role");
+
const data = await response.json();
+
throw new Error(data.error || "Failed to update role");
}
if (isDemotingSelf) {
···
} else {
await this.loadUsers();
}
-
} catch (error) {
-
console.error("Failed to update role:", error);
-
alert("Failed to update user role");
+
} catch (err) {
+
this.error = err instanceof Error ? err.message : "Failed to update user role";
select.value = oldRole;
}
}
···
}
private async performDeleteUser(userId: number) {
+
this.error = null;
try {
const response = await fetch(`/api/admin/users/${userId}`, {
method: "DELETE",
});
if (!response.ok) {
-
throw new Error("Failed to delete user");
+
const data = await response.json();
+
throw new Error(data.error || "Failed to delete user");
}
// Remove user from local array instead of reloading
this.users = this.users.filter(u => u.id !== userId);
this.dispatchEvent(new CustomEvent("user-deleted"));
-
} catch {
-
alert("Failed to delete user. Please try again.");
+
} catch (err) {
+
this.error = err instanceof Error ? err.message : "Failed to delete user. Please try again.";
}
}
···
this.deleteState = { id: userId, type: "revoke", clicks: newClicks, timeout };
}
-
private async performRevokeSubscription(userId: number, email: string, subscriptionId: string) {
+
private async performRevokeSubscription(userId: number, _email: string, subscriptionId: string) {
this.revokingSubscriptions.add(userId);
this.requestUpdate();
+
this.error = null;
try {
const response = await fetch(`/api/admin/users/${userId}/subscription`, {
···
}
await this.loadUsers();
-
alert(`Subscription revoked for ${email}`);
-
} catch (error) {
-
alert(`Failed to revoke subscription: ${error instanceof Error ? error.message : "Unknown error"}`);
+
} catch (err) {
+
this.error = err instanceof Error ? err.message : "Failed to revoke subscription";
this.revokingSubscriptions.delete(userId);
}
}
···
this.syncingSubscriptions.add(userId);
this.requestUpdate();
+
this.error = null;
try {
const response = await fetch(`/api/admin/users/${userId}/subscription`, {
···
if (!response.ok) {
const data = await response.json();
-
// Don't alert if there's just no subscription
+
// Don't show error if there's just no subscription
if (response.status !== 404) {
-
alert(`Failed to sync subscription: ${data.error || "Unknown error"}`);
+
this.error = data.error || "Failed to sync subscription";
}
return;
}
···
if (this.error) {
return html`
-
<div class="error">${this.error}</div>
+
<div class="error-banner">${this.error}</div>
<button @click=${this.loadUsers}>Retry</button>
`;
}
···
const filtered = this.filteredUsers;
return html`
+
${this.error ? html`<div class="error-banner">${this.error}</div>` : ""}
+
<input
type="text"
class="search-box"