Skip to content

feat(#28): sparkline chart for campaign funding momentum#29

Merged
Jing-yilin merged 3 commits intodevelopfrom
feature/28-sparkline
Feb 28, 2026
Merged

feat(#28): sparkline chart for campaign funding momentum#29
Jing-yilin merged 3 commits intodevelopfrom
feature/28-sparkline

Conversation

@Jing-yilin
Copy link
Contributor

Closes #28

Changes

Backend

  • CampaignSnapshot model: added snapshot_date (type date) + backers_count; unique index on (campaign_pid, snapshot_date) ensures exactly one row per campaign per calendar day
  • storeSnapshots(): fixed blind INSERTON CONFLICT DO UPDATE upsert — no more duplicate rows per crawl
  • GET /api/campaigns/:pid/history: new endpoint returning daily snapshots ordered oldest→newest

iOS

  • 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 points
  • CampaignRowView: sparkline appears in a right column above the heart button

Behaviour

State Display
First day (1 point) Nothing (loading silently)
Day 2+ (≥2 points) Sparkline chart
API unavailable Graceful fallback (nothing shown)

- 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 Jing-yilin added enhancement New feature or request backend Backend related ios iOS related api API related labels Feb 27, 2026
Copy link
Contributor Author

@Jing-yilin Jing-yilin left a comment

Choose a reason for hiding this comment

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

  1. backend/internal/model/model.go:35-37 + backend/internal/db/db.go:38-46
    This migration is not deploy-safe for an existing campaign_snapshots table. AutoMigrate now has to add snapshot_date as NOT NULL and 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 populates snapshot_date, collapses same-day duplicates, and only then adds the unique constraint.

  2. ios/KickWatch/Sources/Views/SparklineView.swift:27-30
    This turns every visible CampaignRowView into its own history request, and the row is used inside scrolling Lists in discover/search/alerts. Opening a feed now fans out into N extra /history calls, 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>
@Jing-yilin Jing-yilin merged commit 722a8fc into develop Feb 28, 2026
0 of 2 checks passed
@Jing-yilin Jing-yilin deleted the feature/28-sparkline branch February 28, 2026 06:01
Jing-yilin added a commit that referenced this pull request Feb 28, 2026
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>
Copy link
Contributor Author

@Jing-yilin Jing-yilin left a comment

Choose a reason for hiding this comment

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

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

@Jing-yilin
Copy link
Contributor Author

Fixes Applied

Both [High] issues identified by Codex have been resolved in commit 34b8c93:

✅ Fixed: Duplicate GetCampaignHistory function

  • Removed the duplicate function definition at lines 177-193
  • Kept the correct version (line 217) with days parameter support and proper {"history": [...]} response format

✅ Fixed: SparklineView API mismatch in CampaignRowView

  • Removed the sparkline from CampaignRowView entirely (line 18)
  • The investor-friendly UI already has a better sparkline implementation in CampaignDetailView
  • This also prevents N+1 query performance issues from loading history for every row in scrolling lists

Backend tests now pass:

$ cd backend && go test ./...
?   	github.com/kickwatch/backend/cmd/api	[no test files]
ok  	github.com/kickwatch/backend/internal/service	(cached)

Refs: 34b8c93

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api API related backend Backend related enhancement New feature or request ios iOS related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant