Feat(csv): adding csv exports that are server side#313
Conversation
… line in one of the project GET
…t present in the database.. :(
📝 WalkthroughWalkthroughAdds server-side workspace/project analytics and CSV export endpoints, wires authenticated routes, and updates the analytics page to consume aggregated data and download exports. Issue-list loading now tolerates secondary request failures, while web tooling versions are updated. ChangesAnalytics feature
Issue list loading behavior
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant AnalyticsWorkItemsPage
participant GinRouter
participant AnalyticsHandler
participant Database
AnalyticsWorkItemsPage->>GinRouter: Request analytics or CSV
GinRouter->>AnalyticsHandler: Dispatch authenticated request
AnalyticsHandler->>Database: Query grouped counts or issues
Database-->>AnalyticsHandler: Return query results
AnalyticsHandler-->>AnalyticsWorkItemsPage: Return JSON analytics or CSV download
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✅ Passed checks (2 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" 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. Comment Warning |
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (1)
apps/web/src/pages/IssueListPage.tsx (1)
589-590: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove leftover debugging statements.
These
console.logstatements appear to be debugging artifacts and should be removed to keep the production console clean.🧹 Proposed fix
- console.log(project) - console.log(workspace)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/pages/IssueListPage.tsx` around lines 589 - 590, Remove the leftover console.log statements for project and workspace from the surrounding IssueListPage component, leaving the production logic unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/internal/handler/analytics.go`:
- Around line 254-266: Update the CSV export handler around csv.Writer creation
to check errors from both the header and per-issue writer.Write calls, and
ensure writer.Flush is executed before inspecting writer.Error(). Propagate or
return an appropriate error when any write or flush error occurs, including the
analogous export block around the second issue loop.
- Around line 14-16: Refactor AnalyticsHandler and all four analytics/export
handlers so they resolve the authenticated user and delegate authorization-aware
workspace/project access checks to a service, including validating the project
slug. Move every GORM query out of the handlers into a store called by that
service, preserving the existing response behavior only after access is
authorized.
- Around line 19-24: The analytics response contract lacks populated
created-versus-resolved trend data. In
apps/api/internal/handler/analytics.go:19-24, extend AnalyticsResponse with a
typed time-series field and populate it through the analytics handler’s existing
data flow; in apps/web/src/pages/AnalyticsWorkItemsPage.tsx:246, consume that
response field with the corresponding concrete type instead of any[] so the
chart receives real backend data.
- Around line 257-265: Update the issue row export in the analytics CSV handler
to neutralize spreadsheet formulas in user-controlled fields before passing them
to csv.Writer. Apply the same sanitization to issue.Name, issue.State, and
issue.Priority, and also to the corresponding cells in the referenced export
block around lines 300-307; preserve IDs and headers unless they are subject to
the same unsafe-value requirement.
- Around line 90-120: Update the analytics handler’s aggregate-query error
handling, including the assignee and label flows and the secondary project-query
section, so any database failure is returned or surfaced through an explicit
partial-result status instead of leaving empty maps that imply zero counts.
Preserve successful aggregation behavior and ensure the response clearly
distinguishes valid empty metrics from failed queries.
In `@apps/api/internal/router/router.go`:
- Around line 436-437: Preserve trailing slashes for both project analytics
routes in apps/api/internal/router/router.go at lines 436-437 by registering
slash-terminated paths. Update the export request in
apps/web/src/pages/AnalyticsWorkItemsPage.tsx at lines 206-208 to use the
corresponding slash-terminated URL.
In `@apps/web/src/pages/AnalyticsWorkItemsPage.tsx`:
- Line 190: Update both catch clauses in AnalyticsWorkItemsPage, including the
blocks around the existing catch handlers, to remove the unused err bindings and
use bindingless catch syntax while preserving their current error-handling
bodies.
- Line 267: Replace the fixed English user-facing labels in
AnalyticsWorkItemsPage, including the KPI, chart-axis, table, action, and export
labels near the referenced sections, with the existing t(...) translation flow.
Add or reuse appropriate translation keys for each label while preserving the
current rendered text and behavior.
- Around line 141-174: Update the workspace-loading effect around getBySlug so
the analytics and project requests are awaited before setLoading(false) runs,
rather than being launched in nested promise chains. Add and maintain an
analytics error state when the analytics fetch fails, and render that error
instead of silently treating missing analytics data as zero; preserve
cancellation checks before state updates.
- Around line 235-239: Update the KPI count logic in AnalyticsWorkItemsPage
around backlogCount, startedCount, unstartedCount, completedCount, and
totalIssues to derive counts from the configured stable state groups rather than
hard-coded display-name keys. Ensure totalIssues sums every value in
analytics.by_state, including Cancelled and custom states, while preserving the
existing KPI bucket semantics through the group definitions.
In `@apps/web/src/pages/IssueListPage.tsx`:
- Line 185: Update the safeFetch helper to use a generic type parameter, such as
the TSX-safe T, so its promise and fallback share the same type and the returned
Promise is typed accordingly. Preserve the existing fallback behavior and ensure
downstream Promise.all results are inferred without any.
---
Nitpick comments:
In `@apps/web/src/pages/IssueListPage.tsx`:
- Around line 589-590: Remove the leftover console.log statements for project
and workspace from the surrounding IssueListPage component, leaving the
production logic unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: bb305e98-fd9a-42a8-be2f-354597bce129
⛔ Files ignored due to path filters (1)
apps/web/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (5)
apps/api/internal/handler/analytics.goapps/api/internal/router/router.goapps/web/package.jsonapps/web/src/pages/AnalyticsWorkItemsPage.tsxapps/web/src/pages/IssueListPage.tsx
| type AnalyticsHandler struct { | ||
| DB *gorm.DB | ||
| Log *slog.Logger |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift
Move these queries behind an authorization-aware service.
All four handlers query using route parameters without checking that the authenticated user belongs to the workspace/project; the project handlers also ignore slug. Any authenticated user can therefore access another tenant’s analytics or exports.
Resolve the current user in the handler, enforce workspace/project access in a service, and place GORM queries in a store.
As per coding guidelines, “handlers call services, services call stores; handlers must never touch GORM directly.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/internal/handler/analytics.go` around lines 14 - 16, Refactor
AnalyticsHandler and all four analytics/export handlers so they resolve the
authenticated user and delegate authorization-aware workspace/project access
checks to a service, including validating the project slug. Move every GORM
query out of the handlers into a store called by that service, preserving the
existing response behavior only after access is authorized.
Source: Coding guidelines
| type AnalyticsResponse struct { | ||
| ByState map[string]int64 `json:"by_state"` | ||
| ByPriority map[string]int64 `json:"by_priority"` | ||
| ByAssignee map[string]int64 `json:"by_assignee"` | ||
| ByLabel map[string]int64 `json:"by_label"` | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Implement the created-versus-resolved analytics contract end to end.
The backend exposes no trend series, while the frontend permanently supplies an empty array, so the requested chart can never display data.
apps/api/internal/handler/analytics.go#L19-L24: add typed created/resolved time-series data to the response and populate it.apps/web/src/pages/AnalyticsWorkItemsPage.tsx#L246-L246: consume that field using a concrete type instead ofany[].
📍 Affects 2 files
apps/api/internal/handler/analytics.go#L19-L24(this comment)apps/web/src/pages/AnalyticsWorkItemsPage.tsx#L246-L246
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/internal/handler/analytics.go` around lines 19 - 24, The analytics
response contract lacks populated created-versus-resolved trend data. In
apps/api/internal/handler/analytics.go:19-24, extend AnalyticsResponse with a
typed time-series field and populate it through the analytics handler’s existing
data flow; in apps/web/src/pages/AnalyticsWorkItemsPage.tsx:246, consume that
response field with the corresponding concrete type instead of any[] so the
chart receives real backend data.
Source: Linters/SAST tools
| byAssignee := make(map[string]int64) | ||
| if err == nil { | ||
| for _, r := range assigneeResults { | ||
| byAssignee[r.Email] = r.Count | ||
| } | ||
| } else { | ||
| h.Log.Warn("failed to fetch workspace assignee analytics", "error", err) | ||
| } | ||
|
|
||
| // 4. Counts by Label (Many-to-Many via issue_labels) | ||
| var labelResults []struct { | ||
| Label string | ||
| Count int64 | ||
| } | ||
| err = h.DB.Table("issue_labels"). | ||
| Select("labels.name as label, count(issue_labels.issue_id) as count"). | ||
| Joins("INNER JOIN labels ON issue_labels.label_id = labels.id"). | ||
| Joins("INNER JOIN issues ON issue_labels.issue_id = issues.id"). | ||
| Joins("INNER JOIN workspaces ON issues.workspace_id = workspaces.id"). | ||
| Where("workspaces.slug = ? AND issues.deleted_at IS NULL AND workspaces.deleted_at IS NULL AND labels.deleted_at IS NULL", slug). | ||
| Group("labels.name"). | ||
| Scan(&labelResults).Error | ||
|
|
||
| byLabel := make(map[string]int64) | ||
| if err == nil { | ||
| for _, r := range labelResults { | ||
| byLabel[r.Label] = r.Count | ||
| } | ||
| } else { | ||
| h.Log.Warn("failed to fetch workspace label analytics", "error", err) | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Do not report failed aggregate queries as zero counts.
Assignee/label failures—and all secondary project-query failures—currently produce a successful response containing empty maps. Return an error or expose an explicit partial-result status so database failures cannot be mistaken for valid zero metrics.
Also applies to: 170-215
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/internal/handler/analytics.go` around lines 90 - 120, Update the
analytics handler’s aggregate-query error handling, including the assignee and
label flows and the secondary project-query section, so any database failure is
returned or surfaced through an explicit partial-result status instead of
leaving empty maps that imply zero counts. Preserve successful aggregation
behavior and ensure the response clearly distinguishes valid empty metrics from
failed queries.
| writer := csv.NewWriter(c.Writer) | ||
| defer writer.Flush() | ||
|
|
||
| _ = writer.Write([]string{"Issue ID", "Title", "State", "Priority"}) | ||
|
|
||
| for _, issue := range issues { | ||
| _ = writer.Write([]string{ | ||
| issue.ID, | ||
| issue.Name, | ||
| issue.State, | ||
| issue.Priority, | ||
| }) | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Check CSV write and flush errors.
Every Write error is discarded, and deferred Flush prevents checking writer.Error(). A disconnected client can receive a truncated file presented as a successful export.
Proposed pattern
- defer writer.Flush()
+ defer writer.Flush()
- _ = writer.Write(headers)
+ if err := writer.Write(headers); err != nil {
+ h.Log.Error("failed to write CSV header", "error", err)
+ return
+ }
+ writer.Flush()
+ if err := writer.Error(); err != nil {
+ h.Log.Error("failed to flush CSV export", "error", err)
+ }Also applies to: 297-307
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/internal/handler/analytics.go` around lines 254 - 266, Update the
CSV export handler around csv.Writer creation to check errors from both the
header and per-issue writer.Write calls, and ensure writer.Flush is executed
before inspecting writer.Error(). Propagate or return an appropriate error when
any write or flush error occurs, including the analogous export block around the
second issue loop.
| _ = writer.Write([]string{"Issue ID", "Title", "State", "Priority"}) | ||
|
|
||
| for _, issue := range issues { | ||
| _ = writer.Write([]string{ | ||
| issue.ID, | ||
| issue.Name, | ||
| issue.State, | ||
| issue.Priority, | ||
| }) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Neutralize spreadsheet formulas in exported cells.
Issue titles, state names, and priorities can begin with formula markers such as =, +, -, or @. Prefix unsafe values before passing them to csv.Writer; CSV quoting alone does not prevent spreadsheet execution.
Also applies to: 300-307
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/internal/handler/analytics.go` around lines 257 - 265, Update the
issue row export in the analytics CSV handler to neutralize spreadsheet formulas
in user-controlled fields before passing them to csv.Writer. Apply the same
sanitization to issue.Name, issue.State, and issue.Priority, and also to the
corresponding cells in the referenced export block around lines 300-307;
preserve IDs and headers unless they are subject to the same unsafe-value
requirement.
| workspaceService | ||
| .getBySlug(workspaceSlug) | ||
| .then((w) => { | ||
| if (cancelled) return; | ||
| setWorkspace(w); | ||
| return projectService.list(workspaceSlug); | ||
| }) | ||
| .then((projs) => { | ||
| if (!cancelled && projs?.length) setProjects(projs); | ||
| if (!cancelled && projs?.length) { | ||
| return Promise.all([ | ||
| ...projs.map((p) => issueService.list(workspaceSlug!, p.id, { limit: 200 })), | ||
| ...projs.map((p) => stateService.list(workspaceSlug!, p.id)), | ||
| ]); | ||
| } | ||
| return []; | ||
| }) | ||
| .then((results) => { | ||
| if (cancelled || !results?.length) return; | ||
| const half = results.length / 2; | ||
| const issueArrays = results.slice(0, half) as IssueApiResponse[][]; | ||
| const stateArrays = results.slice(half) as StateApiResponse[][]; | ||
| setIssues(issueArrays.flat()); | ||
| setStates(stateArrays.flat()); | ||
| }) | ||
| .catch(() => { | ||
| if (!cancelled) setWorkspace(null); | ||
| setProjects([]); | ||
| setIssues([]); | ||
| setStates([]); | ||
| }) | ||
| .finally(() => { | ||
| if (!cancelled) setLoading(false); | ||
| }); | ||
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| }, [workspaceSlug]); | ||
| .getBySlug(workspaceSlug) | ||
| .then((w) => { | ||
| if (cancelled) return; | ||
| setWorkspace(w); | ||
|
|
||
| const getStateName = (stateId: string | null | undefined) => | ||
| stateId ? (states.find((s) => s.id === stateId)?.name ?? stateId) : '—'; | ||
| projectService.list(workspaceSlug) | ||
| .then((projs) => { | ||
| if (!cancelled && projs) setProjects(projs); | ||
| }) | ||
| .catch((err) => console.error("Erreur projets:", err)); | ||
|
|
||
| const backlogCount = issues.filter((i) => getStateName(i.state_id) === 'Backlog').length; | ||
| const startedCount = issues.filter((i) => getStateName(i.state_id) === 'In Progress').length; | ||
| const unstartedCount = issues.filter((i) => getStateName(i.state_id) === 'Todo').length; | ||
| const completedCount = issues.filter((i) => getStateName(i.state_id) === 'Done').length; | ||
| // Add the trailing slash back to the URL | ||
| fetch(`${API_BASE}/api/workspaces/${workspaceSlug}/analytics/`, { credentials: 'include' }) | ||
| .then((res) => { | ||
| if (!res.ok) { | ||
| throw new Error(`Code erreur serveur Go : ${res.status}`); | ||
| } | ||
| return res.json(); | ||
| }) | ||
| .then((analyticsData) => { | ||
| if (!cancelled && analyticsData) setAnalytics(analyticsData); | ||
| }) | ||
| .catch((err) => { | ||
| console.error("Erreur API Analytics Go:", err); | ||
| }); | ||
| }) | ||
| .catch((err) => { | ||
| console.error("Erreur Workspace:", err); | ||
| if (!cancelled) setWorkspace(null); | ||
| }) | ||
| .finally(() => { | ||
| if (!cancelled) setLoading(false); | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Await analytics before clearing the loading state.
The outer finally only waits for getBySlug; the nested analytics request remains pending. If it fails, the page silently renders every metric as zero. Await all required requests and expose an analytics error state.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/pages/AnalyticsWorkItemsPage.tsx` around lines 141 - 174, Update
the workspace-loading effect around getBySlug so the analytics and project
requests are awaited before setLoading(false) runs, rather than being launched
in nested promise chains. Add and maintain an analytics error state when the
analytics fetch fails, and render that error instead of silently treating
missing analytics data as zero; preserve cancellation checks before state
updates.
| .toISOString() | ||
| .slice(0, 10)}.csv`; | ||
| await downloadCsv(`${API_BASE}/api/workspaces/${workspaceSlug}/analytics/export/`, fallback); | ||
| } catch (err) { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Remove the unused catch bindings to restore lint.
Both err bindings are unused; use catch { ... }.
Also applies to: 210-210
🧰 Tools
🪛 GitHub Check: lint-and-format
[failure] 190-190:
'err' is defined but never used
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/pages/AnalyticsWorkItemsPage.tsx` at line 190, Update both catch
clauses in AnalyticsWorkItemsPage, including the blocks around the existing
catch handlers, to remove the unused err bindings and use bindingless catch
syntax while preserving their current error-handling bodies.
Source: Linters/SAST tools
| const backlogCount = analytics?.by_state["Backlog"] ?? 0; | ||
| const startedCount = analytics?.by_state["In Progress"] ?? 0; | ||
| const unstartedCount = analytics?.by_state["Todo"] ?? 0; | ||
| const completedCount = analytics?.by_state["Done"] ?? 0; | ||
| const totalIssues = backlogCount + startedCount + unstartedCount + completedCount; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
ast-grep outline apps/web/src/pages/AnalyticsWorkItemsPage.tsx --view expanded
rg -n "by_state|Backlog|In Progress|Todo|Done|AnalyticsWorkItemsPage" apps/web/src/pages -SRepository: Devlaner/devlane
Length of output: 2766
🏁 Script executed:
sed -n '210,290p' apps/web/src/pages/AnalyticsWorkItemsPage.tsx
rg -n "AnalyticsResponse|by_state|analytics.*state|work items analytics|analytics work items" apps -SRepository: Devlaner/devlane
Length of output: 4749
🏁 Script executed:
sed -n '1,260p' apps/api/internal/handler/analytics.goRepository: Devlaner/devlane
Length of output: 8058
🏁 Script executed:
rg -n "type State|states\.name|custom state|work item state|Backlog|Todo|In Progress|Done|state.*category|state.*group|state_id" apps -SRepository: Devlaner/devlane
Length of output: 45229
Use stable state groups for the KPI counts. by_state is keyed by state names, so this hard-codes the default labels and leaves Cancelled and any custom states out of totalIssues. Sum all by_state values for the total, and derive the buckets from state groups instead of display names.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/pages/AnalyticsWorkItemsPage.tsx` around lines 235 - 239, Update
the KPI count logic in AnalyticsWorkItemsPage around backlogCount, startedCount,
unstartedCount, completedCount, and totalIssues to derive counts from the
configured stable state groups rather than hard-coded display-name keys. Ensure
totalIssues sums every value in analytics.by_state, including Cancelled and
custom states, while preserving the existing KPI bucket semantics through the
group definitions.
| {t('analytics.totalWorkItems', 'Total Work items')} | ||
| </p> | ||
| <p className="mt-1 text-2xl font-semibold text-(--txt-primary)">{issues.length}</p> | ||
| <p className="text-xs font-medium text-(--txt-tertiary)">Total Work items</p> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Keep new user-facing labels localized.
These fixed English strings bypass the existing t(...) translation flow. Restore translation keys for KPI, chart-axis, table, action, and export labels.
Also applies to: 319-331, 419-431, 455-455, 497-497, 504-527
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/pages/AnalyticsWorkItemsPage.tsx` at line 267, Replace the fixed
English user-facing labels in AnalyticsWorkItemsPage, including the KPI,
chart-axis, table, action, and export labels near the referenced sections, with
the existing t(...) translation flow. Add or reuse appropriate translation keys
for each label while preserving the current rendered text and behavior.
| let cancelled = false; | ||
| setLoading(true); | ||
|
|
||
| const safeFetch = (promise: Promise<any>, fallback: any): Promise<any> => { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use a generic type instead of any for type safety.
The use of any breaks type safety and triggers a static analysis linter failure. Using a generic type parameter ensures that the fallback value and the returned promise match the expected type of the provided promise, properly typing the variables in the downstream Promise.all resolution.
(Note: <T,> syntax is used to prevent the TypeScript compiler from mistaking the generic parameter for a JSX tag in .tsx files.)
♻️ Proposed fix
- const safeFetch = (promise: Promise<any>, fallback: any): Promise<any> => {
+ const safeFetch = <T,>(promise: Promise<T>, fallback: T): Promise<T> => {📝 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.
| const safeFetch = (promise: Promise<any>, fallback: any): Promise<any> => { | |
| const safeFetch = <T,>(promise: Promise<T>, fallback: T): Promise<T> => { |
🧰 Tools
🪛 GitHub Check: lint-and-format
[failure] 185-185:
Unexpected any. Specify a different type
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/src/pages/IssueListPage.tsx` at line 185, Update the safeFetch
helper to use a generic type parameter, such as the TSX-safe T, so its promise
and fallback share the same type and the returned Promise is typed accordingly.
Preserve the existing fallback behavior and ensure downstream Promise.all
results are inferred without any.
Source: Linters/SAST tools
Summary
This PR implements the server-side analytics endpoints and the CSV export capability for workspaces and projects, resolving the client-side scalability issues and wiring up the "Export as CSV" button in the UI.
Linked issues
Closes #204
Type of change
feat:) — user-visible new capabilitySurface
apps/api/)apps/web/)What changed
API Backend (
apps/api/)Frontend UI (
apps/web/)AnalyticsOverviewPageandAnalyticsWorkItemsPageto fetch metrics from the new backend endpoints instead of doing expensive client-side aggregation.Why this approach
Processing aggregations on the server ensures the platform scales efficiently as workspaces grow to thousands of issues. Using optimized database joins prevents the "N+1 query" performance pitfall.
Database / migrations
No schema migrations required.
Breaking changes
Test plan
npm run validatefrom the repository root passes cleanly (TypeScript check + golang test suites).go run ./cmd/api seed.Screenshots / recordings (UI changes)
AI assistance
Gemini— and AI-assisted commits include aCo-Authored-By:trailerChecklist
feat(api/ui): implement server-side analytics and working CSV export #204)--no-verifybypass).envvalues committedReview Notes & Future Improvements
While testing locally, I noticed a few behaviors and structural details to highlight:
Percentage Calculations & Charts:
doneandbacklogare visible in the data, they do not render properly inside the main chart. This might be an existing issue with how state types/cycles are mapped, which we can refine later.Database Schema Details (Assignees & Labels):
assigneeorlabelcolumns on theissuestable, and because setting up the many-to-many joins (issue_assigneesandissue_labels) was causing SQL errors, I skipped adding assignees and labels to the CSV columns for now to keep the code stable. We can add these relations in a follow-up PR once we iron out the database join logicSummary by CodeRabbit
New Features
Bug Fixes