Conversation
WalkthroughConsolidates UI API calls around a single environment-driven base URL, removes UI Docker and compose service, converts several Next.js pages/components to client-side data fetching, drops apiURL props across UI, eliminates UI middleware auth, updates UI env var naming, bumps Node in CI, and merges Go lint and tests into one workflow. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Page as Client Page (Next.js)
participant APIClient as ui/src/lib/api.ts
participant API as Backend API
rect rgba(230, 243, 255, 0.5)
note over Page: Client-side mount
User->>Page: Navigate
Page->>Page: useEffect() start (set loading)
end
Page->>APIClient: getSurvey(urlSlug)
APIClient->>API: GET /v1/surveys/:slug (base: NEXT_PUBLIC_API_ADDR)
API-->>APIClient: 200/4xx JSON
APIClient-->>Page: { status, data|error }
alt success
Page->>APIClient: getSurveySession(...) / getSurveySessions(...)
APIClient->>API: GET session(s)
API-->>APIClient: JSON
APIClient-->>Page: session data
Page-->>User: Render content
else error
Page-->>User: Show error/not found
end
sequenceDiagram
autonumber
participant Browser
participant MW as Next.js Middleware
participant App as Route Handler/Page
Browser->>MW: Incoming request
rect rgba(240, 255, 240, 0.6)
note over MW: New behavior
MW-->>Browser: (no auth checks)
MW->>App: NextResponse.next()
end
App-->>Browser: Response
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
.github/workflows/test.yaml (1)
42-46: setup-node input name bug: use node-version (not node_version)This prevents Node 22 from being set, causing the job to run on a default version.
Apply:
- name: Use Node.js ${{ matrix.node_version }} uses: actions/setup-node@v4 with: - node_version: ${{ matrix.node_version }} + node-version: ${{ matrix.node_version }} + cache: 'npm' + cache-dependency-path: ./ui/package-lock.jsonREADME.md (1)
260-266: Typos in user-facing docs.
- “resposnes” → “responses”
- “Where {SURVEY_ID} id the UUID” → “Where {SURVEY_ID} is the UUID”
-Responses can be shown in the UI and exported as a JSON. Alternatively you can use REST API to get survey resposnes: +Responses can be shown in the UI and exported as JSON. Alternatively you can use REST API to get survey responses: ... -Where `{SURVEY_ID}` id the UUID of a given survey. +Where `{SURVEY_ID}` is the UUID of a given survey.Also applies to: 267-267
ui/src/components/app/survey/SurveyQuestions.tsx (1)
407-413: Reset selectedFile when moving between questions.Without clearing, Next may be enabled on subsequent questions even with no input.
function resetValues() { setSelectedStringValue(undefined) setSelectedArrayValue([]) setSortableItems([]) + setSelectedFile(undefined) setErrorMsg(undefined) }ui/src/components/app/SurveyResponsesPage.tsx (1)
205-206: webhookData may be undefined — prevent runtime crashAccessing statusCode without a guard can throw.
- <Table.Cell>{session.webhookData.statusCode}</Table.Cell> + <Table.Cell>{session.webhookData?.statusCode ?? '-'}</Table.Cell>
🧹 Nitpick comments (27)
ui/src/components/ui/LogoIcon.tsx (1)
1-2: Avoid unnecessary client component for static SVGThis component is purely presentational; making it a Client Component increases bundle size without benefit. Keep it server by default.
Apply:
-'use client' -ui/src/components/ui/BgPattern.tsx (1)
1-2: Make non-interactive layout components server by defaultNo interactivity or browser-only APIs here; prefer Server Component to reduce JS shipped to the browser.
Apply:
-'use client' -.github/workflows/test.yaml (3)
27-29: Don’t run benchmarks in CI by defaultRunning with -bench=. slows CI and can skew timing-sensitive checks. Reserve benches for perf workflows.
Apply:
- run: go test -v -bench=. -race ./... + run: go test -v -race ./...
47-51: Use npm ci for reproducible installs in CInpm ci is faster and honors the lockfile strictly.
Apply:
- npm install + npm ci npm run lint
15-17: Pin Go version from api/go.modUpdating your workflow to read the Go version directly from
api/go.modeliminates manual CI edits on version bumps:- uses: actions/setup-go@v5 - with: - go-version: 1.24 + uses: actions/setup-go@v5 + with: + go-version-file: api/go.mod• Verified
api/go.modon line 3 declaresgo 1.24.4.
•actions/setup-go@v5supports thego-version-fileinput to read the version from your module file.This ensures your CI always uses the exact version declared in
go.modwithout hardcoding.ui/src/middleware.ts (1)
3-5: Remove no-op middleware to avoid per-request edge overheadSince it always returns NextResponse.next(), keeping this file incurs runtime overhead on /app routes. Prefer deleting ui/src/middleware.ts entirely.
ui/src/app/layout.tsx (1)
1-1: RootLayout can stay a Server Component; confirm metadata removal is intentional.No client-only APIs are used here. Keeping RootLayout server-side avoids shipping extra JS and re-enables app-wide Metadata later if needed.
Apply this to revert client-ization:
-'use client' + export default function RootLayout({ children }: LayoutProps) {Also applies to: 8-15
ui/src/components/app/survey/SurveyLayout.tsx (1)
8-9: Avoid duplicating API base URL; centralize config.API_BASE_URL is also defined in lib/api.ts. Prefer a single source (e.g., export it from lib/api or a new lib/config.ts) to prevent drift across files.
ui/src/lib/api.ts (3)
1-1: Export API_BASE_URL for reuse.This enables consumers like SurveyLayout to import the same base URL without duplicating logic.
-const API_BASE_URL = process.env.NEXT_PUBLIC_API_ADDR || 'http://localhost:9900' +export const API_BASE_URL = + process.env.NEXT_PUBLIC_API_ADDR || 'http://localhost:9900'
3-8: Normalize fetch init and avoid mutating the caller’s object; quiet logs in production.-export async function call(path: string, init?: RequestInit) { +export async function call(path: string, init: RequestInit = {}) { try { - if (init) { - init['cache'] = 'no-store' - } + init = { cache: 'no-store', ...init } - console.log('calling', `${API_BASE_URL}${path}`) + if (process.env.NODE_ENV !== 'production') { + console.debug('calling', `${API_BASE_URL}${path}`) + } const res = await fetch(`${API_BASE_URL}${path}`, init) const data = await res.json()Also applies to: 9-12
180-190: Handle failed downloads gracefully.If the API returns an error status, this currently downloads an error payload. Check res.ok before creating a Blob.
export async function download(surveyUUID: string, fileName: string) { const path = `/app/surveys/${surveyUUID}/download/${fileName}` - const res = await fetch(`${API_BASE_URL}${path}`) + const res = await fetch(`${API_BASE_URL}${path}`, { cache: 'no-store' }) + if (!res.ok) { + console.error('download failed', res.status) + return + } const blob = await res.blob() const fileUrl = URL.createObjectURL(blob) const a = document.createElement('a') a.href = fileUrl a.download = fileName a.click() URL.revokeObjectURL(fileUrl) }ui/src/components/app/survey/SurveyQuestions.tsx (4)
355-357: Specify radix for parseInt.Avoids unexpected parsing on some inputs.
- payload = { - value: parseInt(selectedStringValue as string), - } + payload = { + value: parseInt(selectedStringValue as string, 10), + }
331-337: Prevent submit when no value is selected (Enter key on form).The form onSubmit path ignores isSubmitDisabled. Short-circuit submitAnswer.
async function submitAnswer() { if (currentQuestion === undefined) { return } + if ( + selectedStringValue === undefined && + selectedArrayValue.length === 0 && + sortableItems.length === 0 && + selectedFile === undefined + ) { + return + }Also applies to: 448-453
261-271: Avoid setState during render for Ranking initialization.Initialize sortableItems in a useEffect when currentQuestion changes to Ranking.
// Add near imports: import { useEffect } from 'react' // Add effect: useEffect(() => { if ( currentQuestion?.type === SurveyQuestionType.Ranking && sortableItems.length === 0 ) { setSortableItems( currentQuestion.options.map((option, index) => ({ id: index, name: option })) ) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentQuestion])
318-326: FileInput defaultValue is ignored by browsers.File inputs don’t support preset values; drop defaultValue to avoid implying it works.
- <FileInput - defaultValue={selectedFile?.name || ''} + <FileInput placeholder="Upload Your File here..." requiredui/src/components/app/SurveysPage.tsx (1)
27-31: Add an empty-state row for zero surveysA brief placeholder improves UX when the list is empty.
Apply:
- <Table.Body className="divide-y"> - {surveys.map((survey) => { - return <SurveyRow key={survey.uuid} survey={survey} /> - })} - </Table.Body> + <Table.Body className="divide-y"> + {surveys.length === 0 ? ( + <Table.Row className="bg-gray-800"> + <Table.Cell colSpan={6} className="text-center"> + No surveys yet. + </Table.Cell> + </Table.Row> + ) : ( + surveys.map((survey) => ( + <SurveyRow key={survey.uuid} survey={survey} /> + )) + )} + </Table.Body>ui/src/components/app/SurveyResponsesPage.tsx (5)
29-33: Remove redundant cast and reassignmentcurrentSurvey is already typed; reassigning the prop is unnecessary and confusing.
- currentSurvey = currentSurvey as Survey
50-55: Guard filename extraction in downloadFileAvoids off-by-one when no slash is present.
- const downloadFile = async (path: string) => { - await download( - currentSurvey.uuid, - path.substring(path.lastIndexOf('/') + 1) - ) - } + const downloadFile = async (path: string) => { + const idx = path.lastIndexOf('/') + const fileName = idx >= 0 ? path.substring(idx + 1) : path + await download(currentSurvey.uuid, fileName) + }
150-160: Reset pagination state on sort changeKeep UI state consistent when sorting resets to page 1.
onClick={() => { setSortBy(col.key) let newOrder = 'asc' if (sortBy === col.key) { newOrder = order === 'asc' ? 'desc' : 'asc' } setOrder(newOrder) + setCurrentPage(1) fetchResponses(1, col.key, newOrder) }}
64-76: Harden API error handling in fetchResponsesProtect against thrown network errors and ensure loading/error states are reliable.
const fetchResponses = async ( page: number, sortBy: string, order: string ) => { setErrorMsg('') const limit = SurveySessionsLimit const offset = (page - 1) * limit - const surveySessionsResp = await getSurveySessions( - currentSurvey.uuid, - `limit=${limit}&offset=${offset}&sort_by=${sortBy}&order=${order}` - ) - - if (surveySessionsResp.error) { - setErrorMsg('Unable to load survey sessions') - } else { - setSessions(surveySessionsResp.data.data.sessions) - } + try { + const surveySessionsResp = await getSurveySessions( + currentSurvey.uuid, + `limit=${limit}&offset=${offset}&sort_by=${sortBy}&order=${order}` + ) + if (surveySessionsResp.error) { + setErrorMsg('Unable to load survey sessions') + } else { + setSessions(surveySessionsResp.data.data.sessions) + } + } catch { + setErrorMsg('Unable to load survey sessions') + } }
115-121: Avoid loading 1,000,000 items into memory for exportThis can freeze the UI and blow memory. Prefer a backend export endpoint or streaming download.
Proposed approach:
- Add an API: GET /app/surveys/:uuid/sessions/export?format=json
- On success, stream the file or return a pre-signed URL; here just trigger download of that URL.
UI change:
- const allSessionsResp = await getSurveySessions( - currentSurvey.uuid, - `limit=1000000&offset=0&sort_by=created_at&order=desc` - ) + // TODO: replace with real export endpoint once available + const allSessionsResp = await getSurveySessions( + currentSurvey.uuid, + `limit=1000&offset=0&sort_by=created_at&order=desc` + )I can draft the server + client export flow if helpful.
ui/src/components/app/SurveyRow.tsx (3)
25-35: Wrap updateSurveyStatus in try/catchPrevents unhandled promise rejections and surfaces unexpected errors.
async function updateSurveyStatus(surveyUUID: string, status: string) { - const res = await updateSurvey(surveyUUID, { - delivery_status: status, - }) - - if (res.error) { - setErrorMsg(res.error) - } else { - window.location.href = `/app` - } + try { + const res = await updateSurvey(surveyUUID, { + delivery_status: status, + }) + if (res.error) { + setErrorMsg(res.error) + return + } + window.location.href = `/app` + } catch { + setErrorMsg('Unexpected error updating survey status') + } }
43-44: Fix typo: canSartSurvey → canStartSurveyImproves readability and consistency.
- const canSartSurvey = + const canStartSurvey = survey.parse_status === SurveyParseStatus.Success && !isLaunched ... - {(isLaunched || canSartSurvey) && ( + {(isLaunched || canStartSurvey) && (Also applies to: 89-90
61-66: Provide fallback color for unknown parse_statusPrevents undefined from reaching Badge color.
- color={parseStatusColors.get(survey.parse_status)} + color={parseStatusColors.get(survey.parse_status) ?? 'info'}ui/src/components/app/survey/SurveyIntro.tsx (1)
15-16: Consistent setter casing and disable double-clicks on StartRename the setter and add a transient loading flag to prevent duplicate session creation; also add try/catch.
- const [errMessage, seterrMessage] = useState<string | undefined>(undefined) + const [errMessage, setErrMessage] = useState<string | undefined>(undefined) + const [starting, setStarting] = useState(false) ... - <Button - onClick={async () => { - seterrMessage(undefined) - const sessionRes = await createSurveySession(survey.url_slug) - if (sessionRes.error) { - seterrMessage(sessionRes.error) - return - } + <Button + disabled={starting} + onClick={async () => { + setErrMessage(undefined) + setStarting(true) + try { + const sessionRes = await createSurveySession(survey.url_slug) + if (sessionRes.error) { + setErrMessage(sessionRes.error) + return + } localStorage.setItem( `survey_session_id:${survey.url_slug}`, sessionRes.data.data.uuid ) setSurveySession(sessionRes.data.data) + } catch { + setErrMessage('Failed to start survey session') + } finally { + setStarting(false) + } }} > Start </Button> ... - <ErrCode message={errMessage} /> + <ErrCode message={errMessage} />Also applies to: 41-47, 60-62
ui/src/app/app/page.tsx (1)
16-27: Protect against unmounted updates and thrown network errorsAdd a mounted flag and try/catch/finally to avoid state updates after unmount and to handle exceptions.
- useEffect(() => { - const fetchSurveys = async () => { - const surveysResp = await getSurveys() - if (surveysResp.error) { - setErrMsg('Failed to fetch surveys') - } else { - setSurveys(surveysResp.data.data) - } - setLoading(false) - } - fetchSurveys() - }, []) + useEffect(() => { + let mounted = true + const fetchSurveys = async () => { + try { + const surveysResp = await getSurveys() + if (!mounted) return + if (surveysResp.error) { + setErrMsg('Failed to fetch surveys') + } else { + setSurveys(surveysResp.data.data) + } + } catch { + if (mounted) setErrMsg('Failed to fetch surveys') + } finally { + if (mounted) setLoading(false) + } + } + fetchSurveys() + return () => { + mounted = false + } + }, [])ui/src/components/app/survey/SurveyForm.tsx (1)
19-45: Streamline effect: single loading path, fewer repeats, safer access.Consolidate early returns with a try/finally so loading state is always cleared, and avoid repeating the LS key.
Apply:
useEffect(() => { - ;(async () => { - if (typeof window !== 'undefined') { - const lsValue = localStorage.getItem( - `survey_session_id:${survey.url_slug}` - ) - - if (!lsValue) { - setIsNewSession(true) - setIsLoading(false) - return - } - - const sessionRes = await getSurveySession(survey.url_slug, lsValue) - if (sessionRes.error || !sessionRes.data.data) { - localStorage.removeItem(`survey_session_id:${survey.url_slug}`) - setIsNewSession(true) - setIsLoading(false) - return - } - - setSurveySession(sessionRes.data.data) - setIsNewSession(false) - setIsLoading(false) - } - })() + ;(async () => { + setIsLoading(true) + if (typeof window === 'undefined') { + setIsNewSession(true) + setIsLoading(false) + return + } + const sessionKey = `survey_session_id:${survey.url_slug}` + try { + const lsValue = localStorage.getItem(sessionKey) + if (!lsValue) { + setIsNewSession(true) + return + } + const sessionRes = await getSurveySession(survey.url_slug, lsValue) + if (sessionRes.error || !sessionRes.data?.data) { + localStorage.removeItem(sessionKey) + setIsNewSession(true) + return + } + setSurveySession(sessionRes.data.data) + setIsNewSession(false) + } finally { + setIsLoading(false) + } + })() }, [survey])
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (3)
screenshots/app.pngis excluded by!**/*.pngscreenshots/survey.pngis excluded by!**/*.pngui/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (21)
.github/workflows/test.yaml(2 hunks)README.md(1 hunks)compose.yaml(0 hunks)ui/.env.example(1 hunks)ui/Dockerfile(0 hunks)ui/package.json(0 hunks)ui/src/app/app/page.tsx(2 hunks)ui/src/app/app/surveys/[survey_uuid]/responses/page.tsx(2 hunks)ui/src/app/layout.tsx(1 hunks)ui/src/app/survey/[url_slug]/page.tsx(1 hunks)ui/src/components/app/SurveyResponsesPage.tsx(4 hunks)ui/src/components/app/SurveyRow.tsx(4 hunks)ui/src/components/app/SurveysPage.tsx(2 hunks)ui/src/components/app/survey/SurveyForm.tsx(4 hunks)ui/src/components/app/survey/SurveyIntro.tsx(2 hunks)ui/src/components/app/survey/SurveyLayout.tsx(1 hunks)ui/src/components/app/survey/SurveyQuestions.tsx(3 hunks)ui/src/components/ui/BgPattern.tsx(1 hunks)ui/src/components/ui/LogoIcon.tsx(1 hunks)ui/src/lib/api.ts(1 hunks)ui/src/middleware.ts(1 hunks)
💤 Files with no reviewable changes (3)
- ui/package.json
- compose.yaml
- ui/Dockerfile
🧰 Additional context used
🧬 Code graph analysis (9)
ui/src/components/app/survey/SurveyLayout.tsx (1)
ui/src/lib/types.ts (1)
SurveyThemeCustom(35-35)
ui/src/app/app/surveys/[survey_uuid]/responses/page.tsx (3)
ui/src/lib/api.ts (2)
getSurveys(118-120)getSurveySessions(146-148)ui/src/lib/types.ts (1)
SurveySessionsLimit(27-27)ui/src/components/app/SurveyResponsesPage.tsx (1)
SurveyResponsesPage(29-323)
ui/src/components/app/survey/SurveyQuestions.tsx (1)
ui/src/lib/api.ts (1)
submitQuestionAnswer(161-178)
ui/src/app/survey/[url_slug]/page.tsx (4)
ui/src/lib/types.ts (1)
Survey(12-25)ui/src/lib/api.ts (1)
getSurvey(107-116)ui/src/components/app/survey/SurveyLayout.tsx (1)
SurveyLayout(16-33)ui/src/components/app/survey/SurveyForm.tsx (1)
SurveyForm(13-79)
ui/src/components/app/survey/SurveyIntro.tsx (1)
ui/src/lib/api.ts (1)
createSurveySession(122-133)
ui/src/components/app/SurveysPage.tsx (1)
ui/src/components/app/SurveyRow.tsx (1)
SurveyRow(21-140)
ui/src/components/app/SurveyRow.tsx (1)
ui/src/lib/api.ts (1)
updateSurvey(150-152)
ui/src/components/app/survey/SurveyForm.tsx (2)
ui/src/lib/api.ts (1)
getSurveySession(135-144)ui/src/components/app/survey/SurveyIntro.tsx (1)
SurveyIntro(14-66)
ui/src/app/app/page.tsx (2)
ui/src/lib/api.ts (1)
getSurveys(118-120)ui/src/components/app/SurveysPage.tsx (1)
SurveysPage(11-37)
🪛 LanguageTool
README.md
[grammar] ~284-~284: There might be a mistake here.
Context: ...e.g. /root/surveys. It's suggested to use mounted volume for this directory. - `U...
(QB_NEW_EN)
🪛 markdownlint-cli2 (0.17.2)
README.md
275-275: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
289-289: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
299-299: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🔇 Additional comments (9)
ui/.env.example (1)
1-5: Drop server-only secrets from UI env exampleThe ripgrep scan confirms that
IRON_SESSION_SECRETandHTTP_BASIC_AUTHappear only inui/.env.exampleand nowhere else in the UI code. It’s safe to remove these lines to avoid exposing server-only secrets and keep onlyNEXT_PUBLIC_variables.Please apply this diff:
ui/.env.example ──────────────────────────────────── NEXT_PUBLIC_API_ADDR=http://localhost:9900 -IRON_SESSION_SECRET=not_very_secret_replace_me -HTTP_BASIC_AUTH=user:pass +# UI consumes only NEXT_PUBLIC_* vars. Do not put secrets here..github/workflows/test.yaml (1)
10-12: Consolidating lint and tests improves CI throughputMerging into a single Go job is a good simplification.
ui/src/middleware.ts (1)
7-9: Auth removed from middleware — verify client API calls & server protections• No server-session or iron-session usage remains in the UI; all occurrences of
sessionrefer to yourSurveySessiondomain model.
• API requests are made viaui/src/lib/api.ts(incall()anddownload()) and one style fetch inui/src/components/app/survey/SurveyLayout.tsx.
• By default,fetch()does not send cross-origin cookies—ensure you passcredentials: 'include'or attach anAuthorizationheader in each call’sinitobject.
• On the server, confirm that every/app/*route still enforces auth (e.g. via JWT validation or session checks) since middleware no longer handles it.
• Make sure no UI route relies on Next.js middleware for access control—implement client-side guards or redirects where needed.ui/src/components/app/SurveysPage.tsx (1)
11-29: Prop simplification looks goodRemoving apiURL and narrowing props to surveys is consistent with the API centralization.
ui/src/components/app/SurveyRow.tsx (2)
1-2: Client conversion acknowledgedAppropriate for interactive controls in this row.
90-110: Button UI/UX changes look goodFlex layout and explicit icon sizing improve alignment and readability.
ui/src/components/app/survey/SurveyIntro.tsx (1)
14-14: Prop simplification LGTMDropping apiURL aligns with centralized API layer.
ui/src/app/app/page.tsx (2)
1-4: Client-only page is appropriate hereHooks and client-side fetching justify the conversion.
47-48: SurveysPage usage matches new propsNo apiURL prop required anymore.
| ``` | ||
| docker-compose up -d --build | ||
| ``` | ||
|
|
||
| And you should be able to access the UI on [localhost:3000](http://localhost:3000) (default basic auth: `user:pass`). | ||
|
|
||
| You can deploy individual services to any cloud provider or self host them. | ||
|
|
||
| - Go backend. | ||
| - Next.js frontend. | ||
| - Postgres database. | ||
|
|
||
| ### Environment Variables | ||
| Environment variables: |
There was a problem hiding this comment.
Fix fenced code block languages and a grammar nit.
Add explicit languages to fenced blocks and adjust wording (“a mounted volume”).
-```
+```bash
docker-compose up -d --build...
- +bash
cd ui
npm install
npm run dev
...
-```
+```bash
cd api
export DATABASE_URL="postgres://user:pass@localhost:5432/formulosity?sslmode=disable"
export SURVEYS_DIR="./surveys"
go run main.go
-... It's suggested to use mounted volume for this directory.
+... It's suggested to use a mounted volume for this directory.
Also applies to: 289-293, 299-305, 284-284
<details>
<summary>🧰 Tools</summary>
<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>
275-275: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
</details>
</details>
<details>
<summary>🤖 Prompt for AI Agents</summary>
In README.md around lines 275-279 (also apply same changes to 284, 289-293, and
299-305), update the fenced code blocks to include explicit languages (e.g.,
bash) for each triple-backtick block and change the grammar nit by replacing
“It's suggested to use mounted volume for this directory.” with “It's suggested
to use a mounted volume for this directory.”; ensure each code block opening
fence has the correct language tag and the corrected sentence appears where
referenced.
</details>
<!-- fingerprinting:phantom:medusa:chinchilla -->
<!-- This is an auto-generated comment by CodeRabbit -->
| @@ -1,23 +1,37 @@ | |||
| import { Metadata } from 'next' | |||
| 'use client' | |||
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Repo-wide check: ensure apiURL usage is fully removed and no hard-coded base URLs
Run to verify the migration is complete.
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) No leftover apiURL props/usages
rg -n --type=ts --type=tsx -S '\bapiURL\b' ui || echo "OK: no apiURL references"
# 2) API base derived from env
rg -n --type=ts --type=tsx -S 'NEXT_PUBLIC_API_ADDR' ui/src/lib/api.ts || {
echo "Missing NEXT_PUBLIC_API_ADDR usage in lib/api.ts"; exit 1; }
# 3) Flag any hard-coded http(s) URLs in UI (manual review may be needed)
rg -n --type=ts --type=tsx -S 'https\?://[^"'\'' ]+' ui || echo "OK: no hard-coded URLs found"Length of output: 369
Critical: lib/api.ts is missing usage of NEXT_PUBLIC_API_ADDR
Running the verification uncovered that ui/src/lib/api.ts does not reference the NEXT_PUBLIC_API_ADDR environment variable, leaving the API base URL hard-coded or undefined. This is a blocking issue—please update lib/api.ts to derive its base URL from process.env.NEXT_PUBLIC_API_ADDR.
• Location: ui/src/lib/api.ts
• Action: Replace any hard-coded URL or fallback with something like:
// ui/src/lib/api.ts
const API_BASE = process.env.NEXT_PUBLIC_API_ADDR
?? /* optional fallback or throw error */;
// then use API_BASE when constructing fetch calls🤖 Prompt for AI Agents
In ui/src/lib/api.ts (entire file), the API base URL is currently hard-coded or
missing; change the module to derive the base URL from
process.env.NEXT_PUBLIC_API_ADDR by assigning it to a single API_BASE constant
(and provide an explicit fallback or throw if undefined), then update all
fetch/construct-url sites to use API_BASE so every request uses
NEXT_PUBLIC_API_ADDR.
| export default function ResponsesPage() { | ||
| const params = useParams() | ||
| const [currentSurvey, setCurrentSurvey] = useState<Survey | undefined>( | ||
| undefined | ||
| ) | ||
| const [errMsg, setErrMsg] = useState('') | ||
| const [loading, setLoading] = useState(true) | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Reset state on uuid changes, handle missing params, avoid object mutation, and type params.
- loading/errMsg/currentSurvey aren’t reset on param change → stale UI and flashes.
- If params.survey_uuid is absent, loading can hang.
- Mutating API object before setState is unnecessary; prefer immutable update.
- Make useParams typed and remove casts.
Apply:
-export default function ResponsesPage() {
- const params = useParams()
+export default function ResponsesPage() {
+ const { survey_uuid } = useParams<{ survey_uuid: string }>()
@@
- useEffect(() => {
- const fetchData = async () => {
- if (!params.survey_uuid) return
-
- const surveysResp = await getSurveys()
- if (surveysResp.error) {
- setErrMsg('Unable to fetch surveys')
- setLoading(false)
- return
- }
-
- const surveys = surveysResp.data.data
- const survey = surveys.find(
- (survey: Survey) => survey.uuid === params.survey_uuid
- )
-
- if (!survey) {
- setErrMsg('Survey not found')
- setLoading(false)
- return
- }
-
- const surveySessionsResp = await getSurveySessions(
- survey.uuid,
- `limit=${SurveySessionsLimit}&offset=0&sort_by=created_at&order=desc`
- )
-
- if (surveySessionsResp.error) {
- setErrMsg('Unable to fetch survey sessions')
- } else {
- const updatedSurvey = surveySessionsResp.data.data.survey
- updatedSurvey.sessions = surveySessionsResp.data.data.sessions
- updatedSurvey.pages_count = surveySessionsResp.data.data.pages_count
- setCurrentSurvey(updatedSurvey)
- }
-
- setLoading(false)
- }
-
- fetchData()
- }, [params.survey_uuid])
+ useEffect(() => {
+ const fetchData = async () => {
+ setLoading(true)
+ setErrMsg('')
+ setCurrentSurvey(undefined)
+
+ if (!survey_uuid || typeof survey_uuid !== 'string') {
+ setErrMsg('Survey not found')
+ setLoading(false)
+ return
+ }
+
+ try {
+ const surveysResp = await getSurveys()
+ if (surveysResp.error) {
+ setErrMsg('Unable to fetch surveys')
+ return
+ }
+
+ const surveys = surveysResp.data.data
+ const survey = surveys.find((s: Survey) => s.uuid === survey_uuid)
+ if (!survey) {
+ setErrMsg('Survey not found')
+ return
+ }
+
+ const surveySessionsResp = await getSurveySessions(
+ survey.uuid,
+ `limit=${SurveySessionsLimit}&offset=0&sort_by=created_at&order=desc`
+ )
+
+ if (surveySessionsResp.error) {
+ setErrMsg('Unable to fetch survey sessions')
+ } else {
+ const apiData = surveySessionsResp.data.data
+ setCurrentSurvey({
+ ...apiData.survey,
+ sessions: apiData.sessions,
+ pages_count: apiData.pages_count,
+ })
+ }
+ } catch {
+ setErrMsg('Unexpected error')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ fetchData()
+ }, [survey_uuid])Also applies to: 20-61, 78-81
| const params = useParams() | ||
| const [survey, setSurvey] = useState<Survey | null>(null) | ||
| const [loading, setLoading] = useState(true) | ||
| const [notFound, setNotFound] = useState(false) | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
Fix sticky notFound and stale data on route changes; make params type-safe.
- notFound is never reset on subsequent navigations; once set, valid slugs will still render SurveyNotFound.
- loading isn’t re-enabled on slug changes, so prior content can flash until fetch completes.
- Guard missing/invalid slug to avoid indefinite loading.
- Prefer typed useParams to drop casts.
Apply:
- const params = useParams()
+ const { url_slug } = useParams<{ url_slug: string }>()
@@
- useEffect(() => {
- const fetchSurvey = async () => {
- if (!params.url_slug) return
-
- const surveyResp = await getSurvey(params.url_slug as string)
-
- if (
- surveyResp.error ||
- !surveyResp.data.data ||
- !surveyResp.data.data.config
- ) {
- setNotFound(true)
- } else {
- setSurvey(surveyResp.data.data as Survey)
- }
- setLoading(false)
- }
-
- fetchSurvey()
- }, [params.url_slug])
+ useEffect(() => {
+ const fetchSurvey = async () => {
+ setLoading(true)
+ setNotFound(false)
+ setSurvey(null)
+
+ if (!url_slug || typeof url_slug !== 'string') {
+ setNotFound(true)
+ setLoading(false)
+ return
+ }
+
+ try {
+ const surveyResp = await getSurvey(url_slug)
+ if (!surveyResp || surveyResp.error || !surveyResp.data?.data?.config) {
+ setNotFound(true)
+ } else {
+ setSurvey(surveyResp.data.data as Survey)
+ }
+ } catch {
+ setNotFound(true)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ fetchSurvey()
+ }, [url_slug])Also applies to: 17-36
🤖 Prompt for AI Agents
In ui/src/app/survey/[url_slug]/page.tsx around lines 12-16 (also applies to
lines 17-36), the component doesn't reset notFound/loading on route changes and
uses an untyped useParams which causes sticky "not found" and stale content;
update useParams to a typed form (e.g. useParams<{ url_slug?: string }>()),
validate the slug early and return or set notFound when missing/invalid, and in
the effect that fetches the survey reset setNotFound(false) and setLoading(true)
whenever the slug changes (and clear/setSurvey appropriately) so each navigation
triggers a fresh load and prevents indefinite loading; also ensure the fetch
logic handles cancellation/errors by setting loading to false on error and
setting notFound when a 404 is returned.
| if (loading) { | ||
| return ( | ||
| <SurveyLayout> | ||
| <div>Loading...</div> | ||
| </SurveyLayout> | ||
| ) |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Confirm SurveyLayout props are optional when omitted.
You render SurveyLayout without surveyTheme/urlSlug during loading and notFound states. Ensure its prop types mark them optional; otherwise TS should complain.
Run:
Expected: SurveyLayoutProps shows surveyTheme? and urlSlug?.
Also applies to: 46-51
🏁 Script executed:
#!/bin/bash
rg -n -C2 'SurveyLayoutProps|export default function SurveyLayout' ui/src/components/app/survey/SurveyLayout.tsxLength of output: 469
Add optional surveyTheme prop to SurveyLayoutProps
The SurveyLayout component is rendered without any props in the loading and notFound states, but its props type currently only defines children? and urlSlug?. You need to declare surveyTheme as an optional prop to avoid TypeScript errors.
• File: ui/src/components/app/survey/SurveyLayout.tsx
– Update the SurveyLayoutProps type to include surveyTheme?: SurveyTheme
Suggested diff:
type SurveyLayoutProps = {
children?: ReactNode
urlSlug?: string
+ surveyTheme?: SurveyTheme
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (loading) { | |
| return ( | |
| <SurveyLayout> | |
| <div>Loading...</div> | |
| </SurveyLayout> | |
| ) | |
| // File: ui/src/components/app/survey/SurveyLayout.tsx | |
| type SurveyLayoutProps = { | |
| children?: ReactNode | |
| urlSlug?: string | |
| surveyTheme?: SurveyTheme | |
| } |
🤖 Prompt for AI Agents
In ui/src/components/app/survey/SurveyLayout.tsx (update the props type
referenced by ui/src/app/survey/[url_slug]/page.tsx lines 38-43), the
SurveyLayoutProps currently lacks a surveyTheme field causing TS errors when
rendering SurveyLayout without props; add surveyTheme?: SurveyTheme to the
SurveyLayoutProps type, import or reference the SurveyTheme type where required,
and ensure the prop is optional so loading and notFound states can render
SurveyLayout with no props.
| <link | ||
| rel="stylesheet" | ||
| href={API_BASE_URL + '/surveys/' + urlSlug + '/css'} | ||
| /> |
There was a problem hiding this comment.
Guard undefined slug when emitting custom theme CSS link.
If surveyTheme is custom but urlSlug is undefined, the link will point to /surveys/undefined/css.
- {surveyTheme == SurveyThemeCustom && (
+ {surveyTheme == SurveyThemeCustom && urlSlug && (
<link
rel="stylesheet"
href={API_BASE_URL + '/surveys/' + urlSlug + '/css'}
/>
)}Optionally, consider injecting this into via a route-level head.tsx for better loading behavior.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <link | |
| rel="stylesheet" | |
| href={API_BASE_URL + '/surveys/' + urlSlug + '/css'} | |
| /> | |
| {/* Only render custom-theme CSS link when we have a defined slug */} | |
| {surveyTheme == SurveyThemeCustom && urlSlug && ( | |
| <link | |
| rel="stylesheet" | |
| href={API_BASE_URL + '/surveys/' + urlSlug + '/css'} | |
| /> | |
| )} |
🤖 Prompt for AI Agents
In ui/src/components/app/survey/SurveyLayout.tsx around lines 24 to 27, the
component currently emits a stylesheet link using API_BASE_URL + '/surveys/' +
urlSlug + '/css' which will produce a /surveys/undefined/css URL when urlSlug is
not set; update the component to only render the <link> when surveyTheme ===
'custom' (or equivalent flag) AND urlSlug is a non-empty string (guard against
undefined/null), constructing the href only after that check so no undefined
appears; optionally move injection into a route-level head.tsx for better
loading behavior as suggested.
| export async function getSurvey(urlSlug: string) { | ||
| const headers = { | ||
| Referer: typeof window !== 'undefined' ? window.location.host : '', | ||
| } | ||
|
|
||
| return await call(`/surveys/${urlSlug}`, { | ||
| method: 'GET', | ||
| headers: headers, | ||
| }) | ||
| } |
There was a problem hiding this comment.
Do not set the Referer header in browser fetch; it’s a forbidden header.
Browsers control Referer and will ignore or block manual setting. Rely on the default behavior or set referrerPolicy in fetch init if needed.
export async function getSurvey(urlSlug: string) {
- const headers = {
- Referer: typeof window !== 'undefined' ? window.location.host : '',
- }
-
- return await call(`/surveys/${urlSlug}`, {
- method: 'GET',
- headers: headers,
- })
+ return await call(`/surveys/${urlSlug}`, { method: 'GET' })
}
export async function createSurveySession(urlSlug: string) {
- const headers = {
- 'Content-Type': 'application/json',
- Referer: typeof window !== 'undefined' ? window.location.host : '',
- }
-
- return await call(`/surveys/${urlSlug}/sessions`, {
- method: 'PUT',
- body: JSON.stringify({}),
- headers: headers,
- })
+ return await call(`/surveys/${urlSlug}/sessions`, {
+ method: 'PUT',
+ body: JSON.stringify({}),
+ headers: { 'Content-Type': 'application/json' },
+ })
}
export async function getSurveySession(urlSlug: string, sessionId: string) {
- const headers = {
- Referer: typeof window !== 'undefined' ? window.location.host : '',
- }
-
- return await call(`/surveys/${urlSlug}/sessions/${sessionId}`, {
- method: 'GET',
- headers: headers,
- })
+ return await call(`/surveys/${urlSlug}/sessions/${sessionId}`, {
+ method: 'GET',
+ })
}Also applies to: 122-133, 135-144
🤖 Prompt for AI Agents
In ui/src/lib/api.ts around lines 107-116 (and similarly at 122-133 and
135-144), the code attempts to set the Referer header manually which is a
forbidden header in browsers; remove the manual Referer header from the headers
object and let the browser manage it, or if you need to control referrer
behavior set the fetch init option referrerPolicy instead (e.g., include or
no-referrer) and adjust callers to pass referrerPolicy when calling call().
client-only next.js
Summary by CodeRabbit