feat(#28): sparkline chart for campaign funding momentum#29
feat(#28): sparkline chart for campaign funding momentum#29Jing-yilin merged 3 commits intodevelopfrom
Conversation
- Backend: add snapshot_date + backers_count to CampaignSnapshot, unique index on (campaign_pid, snapshot_date) for one-row-per-day dedup - Backend: fix storeSnapshots() to upsert (ON CONFLICT) instead of blind insert - Backend: add GET /api/campaigns/:pid/history endpoint - iOS: add CampaignSnapshotDTO + APIClient.fetchCampaignHistory() - iOS: SparklineView using Swift Charts (line + area, green/orange by trend) - iOS: integrate SparklineView into CampaignRowView (right column above heart)
Jing-yilin
left a comment
There was a problem hiding this comment.
-
backend/internal/model/model.go:35-37+backend/internal/db/db.go:38-46
This migration is not deploy-safe for an existingcampaign_snapshotstable.AutoMigratenow has to addsnapshot_dateasNOT NULLand create a unique index on(campaign_pid, snapshot_date), but the table already contains rows created before this field existed and the old code explicitly inserted duplicate same-day snapshots. On Postgres that means startup migration can fail before the API comes up. This needs an explicit data migration/backfill path that populatessnapshot_date, collapses same-day duplicates, and only then adds the unique constraint. -
ios/KickWatch/Sources/Views/SparklineView.swift:27-30
This turns every visibleCampaignRowViewinto its own history request, and the row is used inside scrollingLists in discover/search/alerts. Opening a feed now fans out into N extra/historycalls, and scrolling can refetch the same campaigns again because the fetch is tied to view appearance with no shared cache. That is a significant client/backend load regression. The history data needs to be cached or loaded at a higher level instead of per-row on demand.
…dex review issues - [High] Add pre-AutoMigrate DO block that adds snapshot_date as nullable, backfills DATE(snapshot_at), deduplicates existing rows, then sets NOT NULL — prevents ADD COLUMN NOT NULL failure on existing prod table - [Medium] Add 5-min in-memory history cache in APIClient (actor-isolated) so SparklineView scroll reuse no longer fires N duplicate /history requests
Resolved conflicts: - backend/internal/db/db.go: Combined pre-migration logic for snapshot_date with develop's column rename migrations - backend/internal/model/model.go: Merged CampaignSnapshot to include both snapshot_date unique index and column:campaign_pid - ios/KickWatch/Sources/Services/APIClient.swift: Kept develop's investor-friendly HistoryDataPoint/CampaignHistoryResponse over sparkline's CampaignSnapshotDTO - ios/KickWatch/Sources/Views/SparklineView.swift: Kept develop's enhanced investor UI version Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Critical fixes discovered by Codex review: 1. [P0] Remove duplicate GetCampaignHistory function (lines 177-193) - Merge left two function definitions (178 and 217) - Kept the correct version with days parameter support 2. [P0] Remove sparkline from CampaignRowView - Line 18 called SparklineView(pid:) which no longer exists - Investor-friendly UI (develop) already has sparkline in CampaignDetailView - Loading history for every row would cause N+1 query performance issues Refs: backend/internal/handler/campaigns.go:178, ios/KickWatch/Sources/Views/CampaignRowView.swift:18 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Jing-yilin
left a comment
There was a problem hiding this comment.
Codex Review
[High] Merged handler conflict leaves the backend unbuildable and changes the /history contract
backend/internal/handler/campaigns.go now defines GetCampaignHistory twice. go test ./... fails with a redeclaration error, so the API server no longer builds. The newly added copy also returns a bare []CampaignSnapshot and drops the existing days filtering, while the iOS client still decodes { "history": [...] }.
Refs: backend/internal/handler/campaigns.go:178, backend/internal/handler/campaigns.go:217, ios/KickWatch/Sources/Services/APIClient.swift:67, ios/KickWatch/Sources/Services/APIClient.swift:194
[High] CampaignRowView now calls a non-existent SparklineView(pid:) initializer
The row change introduces SparklineView(pid: campaign.pid), but SparklineView only accepts dataPoints: [HistoryDataPoint]. xcodebuild -project KickWatch.xcodeproj -scheme KickWatch -sdk iphonesimulator build fails with incorrect argument label in call and cannot convert value of type 'String' to expected argument type '[HistoryDataPoint]'.
Refs: ios/KickWatch/Sources/Views/CampaignRowView.swift:18, ios/KickWatch/Sources/Views/SparklineView.swift:4
Tests run:
cd backend && go test ./...❌cd ios && xcodegen generate✅cd ios && xcodebuild -project KickWatch.xcodeproj -scheme KickWatch -sdk iphonesimulator build❌
Fixes AppliedBoth [High] issues identified by Codex have been resolved in commit 34b8c93: ✅ Fixed: Duplicate
|
Closes #28
Changes
Backend
CampaignSnapshotmodel: addedsnapshot_date(typedate) +backers_count; unique index on(campaign_pid, snapshot_date)ensures exactly one row per campaign per calendar daystoreSnapshots(): fixed blindINSERT→ON CONFLICT DO UPDATEupsert — no more duplicate rows per crawlGET /api/campaigns/:pid/history: new endpoint returning daily snapshots ordered oldest→newestiOS
CampaignSnapshotDTO+APIClient.fetchCampaignHistory(pid:)SparklineView: Swift Charts line + area chart (64×28), loaded lazily with.task(id: pid); green when trending up, orange when trending down; shows nothing until ≥2 data pointsCampaignRowView: sparkline appears in a right column above the heart buttonBehaviour