diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 40c2166..41c517a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -42,35 +42,34 @@ jobs:
with:
fetch-depth: 0
- - uses: actions/setup-node@v4
+ - uses: oven-sh/setup-bun@v1
with:
- node-version: 22
- cache: 'npm'
+ bun-version: "1.3.4"
- name: Install dependencies
- run: npm ci
+ run: bun install --frozen-lockfile
- name: TypeScript check
- run: npx tsc --noEmit
+ run: bunx tsc --noEmit
- name: ESLint
- run: npx eslint .
+ run: bun run lint
- name: Push database schema
- run: npx prisma db push --accept-data-loss
+ run: bun run db:push
- name: Seed database
- run: npm run db:seed
+ run: bun run db:seed
- name: Unit tests
- run: CI=true npx vitest run
+ run: CI=true bun run test
- name: Build
- run: npm run build
+ run: bun run build
- name: Start server
run: |
- npm run start -- -p 3000 &
+ nohup bun run start -- -p 3000 &
echo $! > server.pid
for i in $(seq 1 30); do
if curl -fsS http://localhost:3000 >/dev/null 2>&1; then
@@ -83,7 +82,7 @@ jobs:
exit 1
- name: Integration tests (live server)
- run: CI=true TEST_BASE_URL=http://localhost:3000 npx vitest run __tests__/api/auth.test.ts __tests__/api/resources.test.ts __tests__/api/questions-interview-ai.test.ts __tests__/api/user-paths.test.ts
+ run: CI=true TEST_BASE_URL=http://localhost:3000 bun run test -- __tests__/api/auth.test.ts __tests__/api/resources.test.ts __tests__/api/questions-interview-ai.test.ts __tests__/api/user-paths.test.ts
- name: Stop server
if: always()
@@ -92,4 +91,4 @@ jobs:
- name: Secret scan (gitleaks)
uses: gitleaks/gitleaks-action@v2
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
diff --git a/__tests__/api/user-paths.test.ts b/__tests__/api/user-paths.test.ts
index 4dce181..8f930ac 100644
--- a/__tests__/api/user-paths.test.ts
+++ b/__tests__/api/user-paths.test.ts
@@ -148,7 +148,7 @@ describe('User Path: Resume Review Flow', () => {
expect(status).toBe(200);
expect(body).toHaveProperty('score');
expect(body).toHaveProperty('missingKeywords');
- expect(body).toHaveProperty('improvedVersion');
+ expect(body).toHaveProperty('improvedSummary');
});
testIfServer('Step 3: Update resume with AI feedback', async () => {
diff --git a/public/images/illustrations/ai-feedback-score.svg b/public/images/illustrations/ai-feedback-score.svg
index 718593f..dac3e60 100644
--- a/public/images/illustrations/ai-feedback-score.svg
+++ b/public/images/illustrations/ai-feedback-score.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/public/images/illustrations/cover-letter-transformation.svg b/public/images/illustrations/cover-letter-transformation.svg
index 511991d..d634d4a 100644
--- a/public/images/illustrations/cover-letter-transformation.svg
+++ b/public/images/illustrations/cover-letter-transformation.svg
@@ -1,8 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/public/images/illustrations/download-center.svg b/public/images/illustrations/download-center.svg
index 71be6da..4a301af 100644
--- a/public/images/illustrations/download-center.svg
+++ b/public/images/illustrations/download-center.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/public/images/illustrations/interview-complete-celebration.svg b/public/images/illustrations/interview-complete-celebration.svg
index a4cc2b1..afd3a4c 100644
--- a/public/images/illustrations/interview-complete-celebration.svg
+++ b/public/images/illustrations/interview-complete-celebration.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/public/images/illustrations/learning-levels.svg b/public/images/illustrations/learning-levels.svg
index b276161..4a6f38c 100644
--- a/public/images/illustrations/learning-levels.svg
+++ b/public/images/illustrations/learning-levels.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/public/images/illustrations/mock-interview-setup.svg b/public/images/illustrations/mock-interview-setup.svg
index 407257d..3f0f9d9 100644
--- a/public/images/illustrations/mock-interview-setup.svg
+++ b/public/images/illustrations/mock-interview-setup.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/public/images/illustrations/onboarding-role-selection.svg b/public/images/illustrations/onboarding-role-selection.svg
index 5390528..7afb965 100644
--- a/public/images/illustrations/onboarding-role-selection.svg
+++ b/public/images/illustrations/onboarding-role-selection.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/public/images/illustrations/practice-test-analysis.svg b/public/images/illustrations/practice-test-analysis.svg
index c625096..a59bb8b 100644
--- a/public/images/illustrations/practice-test-analysis.svg
+++ b/public/images/illustrations/practice-test-analysis.svg
@@ -1,10 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/public/images/illustrations/question-bank-library.svg b/public/images/illustrations/question-bank-library.svg
index 4f16faf..b95bee9 100644
--- a/public/images/illustrations/question-bank-library.svg
+++ b/public/images/illustrations/question-bank-library.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/public/images/illustrations/resume-transformation.svg b/public/images/illustrations/resume-transformation.svg
index cada030..1c730a1 100644
--- a/public/images/illustrations/resume-transformation.svg
+++ b/public/images/illustrations/resume-transformation.svg
@@ -1,7 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/components/interview-lab/UpgradeModal.tsx b/src/components/interview-lab/UpgradeModal.tsx
new file mode 100644
index 0000000..b2d752d
--- /dev/null
+++ b/src/components/interview-lab/UpgradeModal.tsx
@@ -0,0 +1,59 @@
+'use client';
+
+import React from 'react';
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+} from '@/components/ui/dialog';
+import { Button } from '@/components/ui/button';
+
+export interface UpgradeModalProps {
+ open: boolean;
+ onClose: () => void;
+ onUpgrade?: () => void;
+ feature: string;
+ reason: string;
+ currentTier: string;
+ recommendedTier: string;
+}
+
+export function UpgradeModal({
+ open,
+ onClose,
+ onUpgrade,
+ feature,
+ reason,
+ currentTier,
+ recommendedTier,
+}: UpgradeModalProps) {
+ return (
+
+ );
+}
+
+export default UpgradeModal;
diff --git a/src/lib/ai/assessment.ts b/src/lib/ai/assessment.ts
index 8f34e12..492e09e 100644
--- a/src/lib/ai/assessment.ts
+++ b/src/lib/ai/assessment.ts
@@ -54,4 +54,17 @@ export const assessmentScoreConfig: AIHandlerConfig
`Assessment: ${body.assessmentTitle}\n\nAssessment Data: ${body.assessmentData ? JSON.stringify(body.assessmentData).substring(0, 5000) : 'N/A'}\n\nUser's Answers: ${body.userAnswers}`,
onParseFailure: () => ({ ok: false, status: 500, error: 'Failed to score assessment' }),
+ // Graceful degradation when the AI provider is unavailable (missing key, outage).
+ onProviderError: () => ({
+ ok: true,
+ value: {
+ score: 0,
+ correctDecisions: [],
+ incorrectDecisions: [],
+ missedOpportunities: [],
+ recommendedNextStep:
+ 'The AI assessment scoring service is currently unavailable. Please try again shortly.',
+ modelAnswer: 'The AI assessment scoring service is currently unavailable. Please try again shortly.',
+ },
+ }),
};
diff --git a/src/lib/ai/coach.ts b/src/lib/ai/coach.ts
index 9a35e2c..fcd16c8 100644
--- a/src/lib/ai/coach.ts
+++ b/src/lib/ai/coach.ts
@@ -122,6 +122,8 @@ export const coachConfig: AIHandlerConfig = {
}
return result;
},
+ // Graceful degradation when the AI provider is unavailable (missing key, outage).
+ onProviderError: () => ({ ok: true, value: errorFeedback() }),
};
export { errorFeedback };
diff --git a/src/lib/ai/cover-letter.ts b/src/lib/ai/cover-letter.ts
index f9a9ac8..ea8aea9 100644
--- a/src/lib/ai/cover-letter.ts
+++ b/src/lib/ai/cover-letter.ts
@@ -59,4 +59,6 @@ export const coverLetterConfig: AIHandlerConfig ({ ok: true, value: EMPTY_RESULT }),
+ // Graceful degradation when the AI provider is unavailable (missing key, outage).
+ onProviderError: () => ({ ok: true, value: EMPTY_RESULT }),
};
diff --git a/src/lib/ai/handlers.ts b/src/lib/ai/handlers.ts
index 9a2808b..ba21866 100644
--- a/src/lib/ai/handlers.ts
+++ b/src/lib/ai/handlers.ts
@@ -35,6 +35,14 @@ export interface AIHandlerConfig {
onParseFailure: (body: TBody) => { ok: true; value: TResult } | { ok: false; status: number; error: string };
/** Optional post-parse normalization/validation of the model output. */
normalize?: (result: TResult, body: TBody) => TResult;
+ /**
+ * What to return when the provider call throws (e.g. missing API key,
+ * network/outage, or timeout). Returning a value degrades gracefully to 200
+ * instead of surfacing a 500; returning an error responds with `status`.
+ */
+ onProviderError?: (
+ body: TBody,
+ ) => { ok: true; value: TResult } | { ok: false; status: number; error: string };
/** Optional timeout (ms) for the model call. */
timeoutMs?: number;
}
@@ -90,6 +98,13 @@ export function createAIHandler, TResult = unkno
return NextResponse.json(result);
} catch (error) {
console.error('AI handler error:', error);
+ if (config.onProviderError) {
+ const fallback = config.onProviderError(body);
+ if (fallback.ok) {
+ return NextResponse.json(fallback.value);
+ }
+ return NextResponse.json({ error: fallback.error }, { status: fallback.status });
+ }
return NextResponse.json({ error: 'AI request failed' }, { status: 500 });
}
};
diff --git a/src/lib/ai/resume.ts b/src/lib/ai/resume.ts
index 39f0351..62edba1 100644
--- a/src/lib/ai/resume.ts
+++ b/src/lib/ai/resume.ts
@@ -53,4 +53,18 @@ export const resumeReviewConfig: AIHandlerConfig
`Target Role: ${body.targetRole || 'Amazon VA'}\n\nResume Text:\n${body.resumeText}`,
onParseFailure: () => ({ ok: false, status: 500, error: 'Failed to parse resume review' }),
+ // Graceful degradation when the AI provider is unavailable (missing key, outage).
+ onProviderError: () => ({
+ ok: true,
+ value: {
+ score: 0,
+ missingKeywords: [],
+ weakSections: ['The AI resume review service is currently unavailable. Please try again shortly.'],
+ improvedSummary:
+ 'The AI resume review service is currently unavailable. Please try again shortly.',
+ improvedBullets: [],
+ skillsRecommendations: [],
+ truthWarnings: [],
+ },
+ }),
};