Skip to content

Commit 750553e

Browse files
committed
ci(github): add linting and type-checking workflows
Introduce automated CI pipelines for Biome linting/formatting and TypeScript type-checking to ensure code quality and prevent regressions. Also includes various stability improvements: - Refactor `useHistoryStore` to use more robust IndexedDB transaction handling. - Enhance error boundary logging with environment-aware logic. - Improve `AudioPlayer` loop point validation logic. - Add search highlighting to `EnhancedTranscript`. - Implement safer `localStorage` access in analytics and page components.
1 parent b4a11db commit 750553e

18 files changed

Lines changed: 177 additions & 55 deletions

.github/workflows/biome.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Biome
2+
3+
on:
4+
push:
5+
branches: ["**"]
6+
pull_request:
7+
branches: ["**"]
8+
9+
jobs:
10+
biome:
11+
name: biome ci
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: oven-sh/setup-bun@v2
18+
with:
19+
bun-version: latest
20+
21+
- name: Install dependencies
22+
run: bun install --frozen-lockfile
23+
24+
- name: Lint & format check
25+
run: bun run ci

.github/workflows/typecheck.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Typecheck
2+
3+
on:
4+
push:
5+
branches: ["**"]
6+
pull_request:
7+
branches: ["**"]
8+
9+
jobs:
10+
typecheck:
11+
name: tsc --noEmit
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: oven-sh/setup-bun@v2
18+
with:
19+
bun-version: latest
20+
21+
- name: Install dependencies
22+
run: bun install --frozen-lockfile
23+
24+
- name: Type check
25+
run: bun x tsc --noEmit

src/app/error.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ export default function RouteError({
1616
reset: () => void
1717
}>) {
1818
useEffect(() => {
19-
console.error("Route error boundary triggered:", error)
19+
if (process.env.NODE_ENV === "development") {
20+
console.error("Route error boundary triggered:", error)
21+
} else if (error.digest) {
22+
console.error(`Route error (${error.digest})`)
23+
}
2024
}, [error])
2125

2226
return (

src/app/global-error.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@ export default function GlobalError({
1414
reset: () => void
1515
}>) {
1616
useEffect(() => {
17-
console.error("Global error boundary triggered:", error)
17+
if (process.env.NODE_ENV === "development") {
18+
console.error("Global error boundary triggered:", error)
19+
} else if (error.digest) {
20+
console.error(`Global error (${error.digest})`)
21+
}
1822
}, [error])
1923

2024
return (
2125
<html lang="en">
22-
<body className="bg-background min-h-screen font-sans">
26+
<body style={{ minHeight: "100vh" }} className="bg-background font-sans">
2327
<main className="flex min-h-screen items-center px-4 py-10">
2428
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6">
2529
<div className="px-1">
@@ -50,7 +54,7 @@ export default function GlobalError({
5054
]}
5155
actions={
5256
<>
53-
<Button onClick={() => reset()}>
57+
<Button onClick={reset}>
5458
<RefreshCw className="h-4 w-4" />
5559
Reset app
5660
</Button>

src/app/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ export default function UploadPage() {
9999
audioUrl = resultData.audioUrl
100100
}
101101
if (audioUrl) {
102-
localStorage.setItem(`audioUrl_${resultData.id}`, audioUrl)
102+
try {
103+
localStorage.setItem(`audioUrl_${resultData.id}`, audioUrl)
104+
} catch {
105+
// Private browsing or storage quota exceeded
106+
}
103107
}
104108

105109
addToHistory({

src/app/transcribe/[id]/page.tsx

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,31 @@ export default function TranscribePage({
101101
}
102102
}, [])
103103

104+
const handleTranscriptionSuccess = useCallback(
105+
(output: unknown, audioUrl?: string) => {
106+
setProgress(100)
107+
stopPolling()
108+
const { transcription, segments, intelligence } =
109+
parseTranscriptionOutput(
110+
output as Parameters<typeof parseTranscriptionOutput>[0],
111+
)
112+
setResult({
113+
transcription,
114+
segments,
115+
intelligence,
116+
detectedLanguage: (output as { detected_language?: string | null })
117+
?.detected_language,
118+
})
119+
setStatus("completed")
120+
patchHistory(id, {
121+
...(audioUrl ? { audioSource: { type: "file", url: audioUrl } } : {}),
122+
status: "succeeded",
123+
result: transcription.slice(0, 200),
124+
})
125+
},
126+
[id, stopPolling, patchHistory],
127+
)
128+
104129
const poll = useCallback(async () => {
105130
attemptsRef.current++
106131

@@ -123,27 +148,7 @@ export default function TranscribePage({
123148
)
124149
setProgress(progressEstimate)
125150
} else if (data.status === "succeeded") {
126-
setProgress(100)
127-
stopPolling()
128-
129-
const output = data.output
130-
const { transcription, segments, intelligence } =
131-
parseTranscriptionOutput(output)
132-
133-
setResult({
134-
transcription,
135-
segments,
136-
intelligence,
137-
detectedLanguage: output?.detected_language,
138-
})
139-
setStatus("completed")
140-
141-
// Merge result into existing history entry to preserve original options/metadata
142-
patchHistory(id, {
143-
...(audioUrl ? { audioSource: { type: "file", url: audioUrl } } : {}),
144-
status: "succeeded",
145-
result: transcription.slice(0, 200),
146-
})
151+
handleTranscriptionSuccess(data.output, audioUrl)
147152
} else if (data.status === "failed") {
148153
stopPolling()
149154
setStatus("failed")
@@ -166,7 +171,7 @@ export default function TranscribePage({
166171
patchHistory(id, { status: "failed" })
167172
}
168173
}
169-
}, [id, stopPolling, patchHistory])
174+
}, [id, stopPolling, patchHistory, handleTranscriptionSuccess])
170175

171176
useEffect(() => {
172177
poll()

src/components/analytics/VercelAnalytics.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import { Analytics, type BeforeSendEvent } from "@vercel/analytics/react"
44

55
function beforeSend(event: BeforeSendEvent) {
6-
if (globalThis.localStorage.getItem("analytics_opt_out") === "true") {
6+
if (
7+
typeof globalThis.localStorage !== "undefined" &&
8+
globalThis.localStorage.getItem("analytics_opt_out") === "true"
9+
) {
710
return null
811
}
912

src/components/errors/ErrorState.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,10 @@ export function ErrorState({
130130
</CardTitle>
131131
</CardHeader>
132132
<CardContent className="space-y-3">
133-
{hints.map((hint) => (
133+
{hints.map((hint, index) => (
134134
<div
135-
key={hint}
135+
// biome-ignore lint/suspicious/noArrayIndexKey: hints are static strings with no stable identity
136+
key={index}
136137
className="border-border/70 bg-background rounded-xl border px-4 py-3"
137138
>
138139
<p className="text-muted-foreground text-sm leading-6">

src/components/feedback/FeedbackModals.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,8 @@ export function FeedbackModals() {
7474
)
7575
}
7676

77-
// Add a proper TypeScript interface for the window object
7877
declare global {
7978
interface Window {
8079
openFeedbackModal?: (type: "general" | "issue" | "feature") => void
81-
feedbackType: "general" | "issue" | "feature" | "other"
8280
}
8381
}

src/components/layout/Header.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ export function Header() {
142142
))}
143143
</nav>
144144

145+
{/* Spacer to balance the logo on the left in mobile layout */}
145146
<div className="h-8 w-8 md:hidden" aria-hidden="true" />
146147
</div>
147148

0 commit comments

Comments
 (0)