Skip to content

client-only#49

Merged
plutov merged 2 commits intomainfrom
vuejs-ui
Aug 27, 2025
Merged

client-only#49
plutov merged 2 commits intomainfrom
vuejs-ui

Conversation

@plutov
Copy link
Owner

@plutov plutov commented Aug 27, 2025

client-only next.js

Summary by CodeRabbit

  • Refactor
    • UI pages/components now fetch on the client with improved loading/error states; API base URL comes from NEXT_PUBLIC_API_ADDR.
    • Removed Basic Auth middleware in UI.
    • Removed UI container from Docker Compose and deleted UI Dockerfile.
  • Bug Fixes
    • More reliable file uploads and downloads in the UI.
  • Documentation
    • README updated: API/Postgres via Docker Compose, clearer local run steps for API and UI, consolidated env vars.
  • Chores
    • CI: merged lint and test for Go; upgraded UI matrix to Node 22.
    • UI .env example updated.

@plutov plutov self-assigned this Aug 27, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 27, 2025

Walkthrough

Consolidates 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

Cohort / File(s) Summary
CI workflow updates
.github/workflows/test.yaml
Merge test steps into lint job (renamed to lint-test-go), run go test with race/bench, keep Go 1.24 and golangci-lint v7, update UI matrix Node to 22.
Docs refocus to API/Postgres
README.md
Remove screenshots and UI-centric deploy notes; emphasize Docker Compose for API+Postgres; add local run instructions for API and UI; streamline env var docs.
Compose/Docker pruning
compose.yaml, ui/Dockerfile
Delete UI service from compose and remove UI Dockerfile; API and Postgres unchanged.
UI environment and deps
ui/.env.example, ui/package.json
Replace CONSOLE_API_ADDR* with NEXT_PUBLIC_API_ADDR; remove iron-session dependency.
Client-side page conversions
ui/src/app/app/page.tsx, ui/src/app/app/surveys/[survey_uuid]/responses/page.tsx, ui/src/app/layout.tsx, ui/src/app/survey/[url_slug]/page.tsx
Add "use client", drop server-side metadata and async exports, fetch data via useEffect, manage loading/error state, remove apiURL props.
Survey UI prop surface cleanup
ui/src/components/app/SurveyResponsesPage.tsx, ui/src/components/app/SurveyRow.tsx, ui/src/components/app/SurveysPage.tsx
Remove apiURL from props and calls; adjust signatures and internal API helper usage accordingly.
Survey form flow updates
ui/src/components/app/survey/SurveyForm.tsx, ui/src/components/app/survey/SurveyIntro.tsx, ui/src/components/app/survey/SurveyLayout.tsx, ui/src/components/app/survey/SurveyQuestions.tsx
Centralize API base URL via env in layout; remove apiURL props; update session creation/fetch and answer submission signatures; simplify useEffect deps.
API client centralization
ui/src/lib/api.ts
Introduce API_BASE_URL from NEXT_PUBLIC_API_ADDR; remove host/apiURL params; unify helpers; adjust Referer header from window; update download; refine FormData handling.
UI client-only utilities
ui/src/components/ui/BgPattern.tsx, ui/src/components/ui/LogoIcon.tsx
Add "use client" directive; no logic changes.
Middleware simplification
ui/src/middleware.ts
Remove Basic Auth; middleware now always returns NextResponse.next(); drop NextRequest param.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A bunny taps deploy with glee,
Trimmed the sails of UI at sea.
One URL to call them all,
Client pages fetch on install.
Lints and tests now hop as one—
Compose is light, the chores are done.
Carrots cached; refactor won! 🥕✨

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 Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch vuejs-ui

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.json
README.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 crash

Accessing 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 SVG

This 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 default

No 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 default

Running 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 CI

npm ci is faster and honors the lockfile strictly.

Apply:

-          npm install
+          npm ci
           npm run lint

15-17: Pin Go version from api/go.mod

Updating your workflow to read the Go version directly from api/go.mod eliminates 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.mod on line 3 declares go 1.24.4.
actions/setup-go@v5 supports the go-version-file input to read the version from your module file.

This ensures your CI always uses the exact version declared in go.mod without hardcoding.

ui/src/middleware.ts (1)

3-5: Remove no-op middleware to avoid per-request edge overhead

Since 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..."
           required
ui/src/components/app/SurveysPage.tsx (1)

27-31: Add an empty-state row for zero surveys

A 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 reassignment

currentSurvey is already typed; reassigning the prop is unnecessary and confusing.

-  currentSurvey = currentSurvey as Survey

50-55: Guard filename extraction in downloadFile

Avoids 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 change

Keep 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 fetchResponses

Protect 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 export

This 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/catch

Prevents 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 → canStartSurvey

Improves 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_status

Prevents 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 Start

Rename 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 errors

Add 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.

📥 Commits

Reviewing files that changed from the base of the PR and between d55c0b0 and 467c0e4.

⛔ Files ignored due to path filters (3)
  • screenshots/app.png is excluded by !**/*.png
  • screenshots/survey.png is excluded by !**/*.png
  • ui/package-lock.json is 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 example

The ripgrep scan confirms that IRON_SESSION_SECRET and HTTP_BASIC_AUTH appear only in ui/.env.example and nowhere else in the UI code. It’s safe to remove these lines to avoid exposing server-only secrets and keep only NEXT_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 throughput

Merging 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 session refer to your SurveySession domain model.
• API requests are made via ui/src/lib/api.ts (in call() and download()) and one style fetch in ui/src/components/app/survey/SurveyLayout.tsx.
• By default, fetch() does not send cross-origin cookies—ensure you pass credentials: 'include' or attach an Authorization header in each call’s init object.
• 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 good

Removing apiURL and narrowing props to surveys is consistent with the API centralization.

ui/src/components/app/SurveyRow.tsx (2)

1-2: Client conversion acknowledged

Appropriate for interactive controls in this row.


90-110: Button UI/UX changes look good

Flex layout and explicit icon sizing improve alignment and readability.

ui/src/components/app/survey/SurveyIntro.tsx (1)

14-14: Prop simplification LGTM

Dropping apiURL aligns with centralized API layer.

ui/src/app/app/page.tsx (2)

1-4: Client-only page is appropriate here

Hooks and client-side fetching justify the conversion.


47-48: SurveysPage usage matches new props

No apiURL prop required anymore.

Comment on lines 275 to +279
```
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:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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.

Comment on lines +12 to 19
export default function ResponsesPage() {
const params = useParams()
const [currentSurvey, setCurrentSurvey] = useState<Survey | undefined>(
undefined
)
const [errMsg, setErrMsg] = useState('')
const [loading, setLoading] = useState(true)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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

Comment on lines +12 to +16
const params = useParams()
const [survey, setSurvey] = useState<Survey | null>(null)
const [loading, setLoading] = useState(true)
const [notFound, setNotFound] = useState(false)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment on lines +38 to +43
if (loading) {
return (
<SurveyLayout>
<div>Loading...</div>
</SurveyLayout>
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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.tsx

Length 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.

Suggested change
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.

Comment on lines +24 to +27
<link
rel="stylesheet"
href={API_BASE_URL + '/surveys/' + urlSlug + '/css'}
/>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
<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.

Comment on lines +107 to 116
export async function getSurvey(urlSlug: string) {
const headers = {
Referer: typeof window !== 'undefined' ? window.location.host : '',
}

return await call(`/surveys/${urlSlug}`, {
method: 'GET',
headers: headers,
})
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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().

@plutov plutov merged commit 8c2e3d7 into main Aug 27, 2025
3 checks passed
@plutov plutov deleted the vuejs-ui branch August 27, 2025 13:07
@coderabbitai coderabbitai bot mentioned this pull request Aug 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant