Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
ee84871
feat(backend): init Go module and main.go entry point
Jing-yilin Feb 27, 2026
a21fda9
feat(backend): add config, GORM models, and DB connection
Jing-yilin Feb 27, 2026
22e07cf
feat(backend): add CORS and request logger middleware
Jing-yilin Feb 27, 2026
0ed4c5b
feat(backend): add Kickstarter REST discover client
Jing-yilin Feb 27, 2026
b10a9a9
feat(backend): add Kickstarter GraphQL client with session bootstrap
Jing-yilin Feb 27, 2026
ac518a0
feat(backend): add APNs HTTP/2 push notification service
Jing-yilin Feb 27, 2026
c285225
feat(backend): add nightly cron crawler and alert matching
Jing-yilin Feb 27, 2026
2b873dc
feat(backend): add campaigns, devices, and alerts HTTP handlers
Jing-yilin Feb 27, 2026
7d400bc
feat(backend): add Dockerfile
Jing-yilin Feb 27, 2026
83a672a
chore: fix gitignore to allow .env.example; add backend env example
Jing-yilin Feb 27, 2026
120eba4
feat(ios): add project.yml, Package.swift, and asset catalog
Jing-yilin Feb 27, 2026
519ce5c
feat(ios): add SwiftData models (Campaign, WatchlistAlert, RecentSearch)
Jing-yilin Feb 27, 2026
f32b5b0
feat(ios): add APIClient, KeychainHelper, NotificationService, ImageC…
Jing-yilin Feb 27, 2026
10a825c
feat(ios): add DiscoverViewModel and AlertsViewModel
Jing-yilin Feb 27, 2026
4530eb8
feat(ios): add app entry point, AppDelegate, and ContentView tab stru…
Jing-yilin Feb 27, 2026
14c8388
feat(ios): add DiscoverView, CampaignRowView, CategoryChip, SearchView
Jing-yilin Feb 27, 2026
702f3f2
feat(ios): add CampaignDetailView with funding ring and back link
Jing-yilin Feb 27, 2026
bcf78a1
feat(ios): add WatchlistView with swipe-to-remove and status badges
Jing-yilin Feb 27, 2026
972d9f9
feat(ios): add AlertsView, NewAlertSheet, AlertMatchesView
Jing-yilin Feb 27, 2026
9097d99
feat(ios): add SettingsView with notification opt-in and app version
Jing-yilin Feb 27, 2026
c5bb875
feat(ci): add backend test and ECS deploy workflows
Jing-yilin Feb 27, 2026
ddb1202
fix(backend): remove duplicate json tag in restProject struct
Jing-yilin Feb 27, 2026
21f2661
feat(ios): add app icon in all required sizes from final logo
Jing-yilin Feb 27, 2026
a956228
feat(ci): add Dockerfile.ci for CI pre-built binary deploy
Jing-yilin Feb 27, 2026
6ee54ae
feat(ci): rewrite deploy workflow with OIDC, dev/prod branching, inli…
Jing-yilin Feb 27, 2026
efd37c7
feat(ci): update test workflow to use go.mod version file and verify …
Jing-yilin Feb 27, 2026
bf70741
fix(ci): set AWS_REGION to us-east-2
Jing-yilin Feb 27, 2026
b5fb48d
docs: archive MVP implementation, app icon creation, and AWS infra setup
Jing-yilin Feb 27, 2026
76c2dcc
feat(ios): set DEVELOPMENT_TEAM, add APNs entitlements
Jing-yilin Feb 27, 2026
fce4574
fix(ios): update bundle ID to com.rescience.kickwatch
Jing-yilin Feb 27, 2026
5d28903
feat(backend): read APNs key from APNS_KEY env var (Secrets Manager) …
Jing-yilin Feb 27, 2026
6914ea0
feat(ci): inject APNS_KEY from Secrets Manager into ECS task
Jing-yilin Feb 27, 2026
39893cf
docs: archive APNs setup (key GUFRSCY8ZV, bundle com.rescience.kickwa…
Jing-yilin Feb 27, 2026
ebc5ec9
Merge pull request #2 from ReScienceLab/feature/ci-oidc
Jing-yilin Feb 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .archive/2026-02-27/apns-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
date: 2026-02-27
title: APNs Key Setup and CI/CD Integration
category: infrastructure
tags: [apns, ios, push-notifications, secrets-manager, ecs, github-actions]
related: [2026-02-27/aws-infra-setup.md]
---

# APNs Key Setup and CI/CD Integration

## Apple Developer Portal

- **Key Name**: KickWatch APNs
- **Key ID**: `GUFRSCY8ZV`
- **Team ID**: `7Q28CBP3S5` (same as SnapAction)
- **Bundle ID**: `com.rescience.kickwatch`
- **Environment**: Sandbox & Production (covers both dev and prod with one key)
- **Key Restriction**: Team Scoped (All Topics)
- **File**: `AuthKey_GUFRSCY8ZV.p8` — downloaded to `/Users/yilin/Downloads/`

## Secrets Manager (us-east-2)

All 4 APNs secrets set for both dev and prod prefixes:

| Secret | Value |
|--------|-------|
| `kickwatch-dev/apns-key-id` | `GUFRSCY8ZV` |
| `kickwatch-dev/apns-team-id` | `7Q28CBP3S5` |
| `kickwatch-dev/apns-bundle-id` | `com.rescience.kickwatch` |
| `kickwatch-dev/apns-key` | Full `.p8` PEM content |
| `kickwatch/apns-key-id` | `GUFRSCY8ZV` |
| `kickwatch/apns-team-id` | `7Q28CBP3S5` |
| `kickwatch/apns-bundle-id` | `com.rescience.kickwatch` |
| `kickwatch/apns-key` | Full `.p8` PEM content |

## Commands Used

```bash
KEY_ID="GUFRSCY8ZV"
REGION=us-east-2

# Key ID
aws secretsmanager put-secret-value \
--secret-id kickwatch-dev/apns-key-id --region $REGION --secret-string "$KEY_ID"

# .p8 content
aws secretsmanager put-secret-value \
--secret-id kickwatch-dev/apns-key --region $REGION \
--secret-string "$(cat ~/Downloads/AuthKey_GUFRSCY8ZV.p8)"
```

## Backend Change: File Path → Env Var

`internal/service/apns.go` updated to read key from `APNS_KEY` env var first, falling back to `APNS_KEY_PATH` file. Avoids need to mount `.p8` file into ECS container.

`internal/config/config.go` added `APNSKey string` field reading `APNS_KEY`.

## CI Workflow Change

`deploy-backend.yml` — removed `APNS_KEY_PATH` env var, added `APNS_KEY` secret injected from Secrets Manager ARN.

## iOS Changes

- `project.yml`: `DEVELOPMENT_TEAM: 7Q28CBP3S5`, `PRODUCT_BUNDLE_IDENTIFIER: com.rescience.kickwatch`
- `KickWatch.entitlements`: `aps-environment = development`

## Gotchas

- APNs key environment set to **Sandbox & Production** — one key works for both; do NOT create separate keys
- Bundle ID must match exactly what's registered in Apple Developer Portal
- `APNS_KEY` env var content is the raw PEM string including `-----BEGIN PRIVATE KEY-----` header/footer
- ECS task execution role needs `secretsmanager:GetSecretValue` for `kickwatch*` ARNs (already added)
62 changes: 62 additions & 0 deletions .archive/2026-02-27/app-icon-creation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
date: 2026-02-27
title: KickWatch App Icon Creation
category: design
tags: [app-icon, logo, svg, nanobanana, rsvg-convert, xcode, appiconset]
related: [2026-02-27/mvp-implementation.md]
---

# App Icon Creation

## Concept

Kickstarter K shape (rounded bubbly white K on green #05CE78) + newspaper/daily digest metaphor = daily monitor app identity. Notion-style: flat, no gradients, clean.

## Process

1. Generated 7 variations via nanobanana skill (Gemini image gen)
2. Selected `logo-07.png` — but user preferred a manually provided `o.png`
3. Pipeline on `o.png`:
- `crop_logo.py` → `final-cropped.png` (719×719 from 776×776)
- `remove_bg.py` → `final-nobg.png` (170KB, transparent bg via remove.bg API)
- `vectorize.py` → `final.svg` (10KB via Recraft API)

## SVG Centering Fix

The vectorized SVG had content off-center (bbox x:106–1983, y:285–1887 in 2000×2000 viewBox). Solution — wrap in centered group with white background:

```svg
<rect width="2000" height="2000" fill="white"/>
<g transform="translate(1000,1000) scale(0.82) translate(-1044.5,-1086)">
<!-- original paths -->
</g>
```

Content center: (1044.5, 1086). Scale 0.82 gives ~10% padding on all sides.

## PNG Generation

Used `rsvg-convert` (available via homebrew at `/opt/homebrew/bin/rsvg-convert`):

```bash
for SIZE in 20 29 40 58 60 76 80 87 120 152 167 180 1024; do
rsvg-convert -w $SIZE -h $SIZE final-centered.svg -o AppIcon-${SIZE}x${SIZE}.png
done
cp AppIcon-1024x1024.png AppIcon.png
```

**cairosvg NOT usable** on this machine — cairo native library missing. Use `rsvg-convert` instead.

## Contents.json

Mirrors SnapAction's `AppIcon.appiconset/Contents.json` exactly (18 image entries for iPhone + iPad + ios-marketing).

## Files

All source assets in:
`.skill-archive/logo-creator/2026-02-27-kickwatch-logo/`
- `final-centered.svg` ← source of truth for icon
- `final-nobg.png`, `final-cropped.png`, `final.svg`

Final PNGs committed to:
`ios/KickWatch/Assets.xcassets/AppIcon.appiconset/`
112 changes: 112 additions & 0 deletions .archive/2026-02-27/aws-infra-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
date: 2026-02-27
title: KickWatch AWS Infrastructure Setup
category: infrastructure
tags: [aws, ecs, ecr, rds, iam, oidc, secrets-manager, github-actions]
related: [2026-02-27/mvp-implementation.md]
---

# KickWatch AWS Infrastructure Setup

## Account & Region
- Account ID: `739654145647`
- Region: `us-east-2`
- IAM user: `snapaction-admin` (shared with SnapAction)

## Resources Created

### ECR Repositories
- `kickwatch-api` — prod
- `kickwatch-api-dev` — dev

### IAM Roles
- `kickwatch-deploy-role` — GitHub Actions OIDC deploy role
- Trust: `repo:ReScienceLab/KickWatch:*`
- Policy: `kickwatch-deploy-policy` (ECR push, ECS deploy, iam:PassRole, secrets read)
- `kickwatch-task-role` — ECS container role (no extra permissions in v1)
- `ecsTaskExecutionRole` — existing shared role, added `kickwatch-secrets-access` inline policy

### OIDC Provider
- Reused existing: `arn:aws:iam::739654145647:oidc-provider/token.actions.githubusercontent.com`

### ECS Clusters
- `kickwatch-cluster` (prod, containerInsights=enabled)
- `kickwatch-cluster-dev` (dev, containerInsights=enabled)

### ECS Services
- `kickwatch-cluster-dev/kickwatch-api-dev-service` (desired=0, task def :2)
- `kickwatch-cluster/kickwatch-api-service` (desired=0, task def :1)

### ECS Task Definitions
- `kickwatch-api-dev:2` — dev, GIN_MODE=debug, APNS_ENV=sandbox
- `kickwatch-api:1` — prod, GIN_MODE=release, APNS_ENV=production
- Networking: awsvpc, subnets: `subnet-03c3f58cea867dac7`, `subnet-0eaf3dc3284bf18d9`, `subnet-0d6addfa05326637e`
- SG: `sg-09a8956d7d1e3274e` (default VPC SG)
- assignPublicIp: ENABLED

### RDS
- `kickwatch-db-dev` — postgres 16.8, db.t3.micro, 20GB
- Endpoint: `kickwatch-db-dev.c164w44w2oh3.us-east-2.rds.amazonaws.com`
- DB name: `kickwatch_dev`
- User: `kickwatch`
- Password: stored in `/tmp/kw_dbpw_dev.txt` locally → set in Secrets Manager
- Publicly accessible: YES (needed for ECS task; protected by SG)
- SG: `sg-0f27ad8fd043ce974` (snapaction-rds-sg, allows 5432 from default SG)
- Prod RDS: **not yet created** — create when ready to deploy prod

### CloudWatch Log Groups
- `/ecs/kickwatch-api` (30 day retention)
- `/ecs/kickwatch-api-dev` (14 day retention)

### Secrets Manager
All in `us-east-2`:
| Secret | Value |
|--------|-------|
| `kickwatch-dev/database-url` | Real URL pointing to kickwatch-db-dev |
| `kickwatch-dev/apns-key-id` | `FILL_IN_APNS_KEY_ID` ← needs real value |
| `kickwatch-dev/apns-team-id` | `FILL_IN_APNS_TEAM_ID` ← needs real value |
| `kickwatch-dev/apns-bundle-id` | `com.kickwatch.app` |
| `kickwatch/database-url` | `PLACEHOLDER` ← fill when prod RDS created |
| `kickwatch/apns-key-id` | `FILL_IN_APNS_KEY_ID` ← needs real value |
| `kickwatch/apns-team-id` | `FILL_IN_APNS_TEAM_ID` ← needs real value |
| `kickwatch/apns-bundle-id` | `com.kickwatch.app` |

### GitHub Secrets (ReScienceLab/KickWatch)
- `AWS_DEPLOY_ROLE_ARN` = `arn:aws:iam::739654145647:role/kickwatch-deploy-role`
- `AWS_ACCOUNT_ID` = `739654145647`

## GitHub Actions Workflow
- `test-backend.yml` — triggers on `backend/**` changes, go vet + test + build
- `deploy-backend.yml` — OIDC auth, `develop`→dev deploy, `main`→prod deploy, Dockerfile.ci
- PR #2 open: `feature/ci-oidc → develop`

## What Needs Manual Action
1. Fill APNs secrets: `APNS_KEY_ID`, `APNS_TEAM_ID` — from Apple Developer Portal
2. Upload `.p8` APNs key file — needs ECS secrets mount or embed in Secrets Manager
3. Create prod RDS (`kickwatch-db`) when ready to go to production
4. Set `DEVELOPMENT_TEAM` in `ios/project.yml` for Xcode builds
5. Set ECS service desired_count to 1 once first image is pushed to ECR

## Gotchas
- VPN (Quantumult X) causes RDS DNS to resolve to 198.18.x.x — same issue as SnapAction
- Can't connect to RDS via psql from local machine when VPN active
- Fix: disable VPN, OR use ECS Exec from a running container
- `kickwatch-db-dev` shares SG `sg-0f27ad8fd043ce974` with SnapAction RDS
- SG already had port 5432 intra-SG rule (default SG → default SG)
- snapaction-db-dev was temporarily set to `publicly-accessible` during setup → reverted
- ECS services start at `desired_count=0` — CI deploy sets count to 1 on first deploy

## Key Commands
```bash
# Update a secret
aws secretsmanager put-secret-value \
--secret-id kickwatch-dev/apns-key-id \
--region us-east-2 --secret-string "YOUR_KEY_ID"

# Tail ECS logs
aws logs tail /ecs/kickwatch-api-dev --region us-east-2 --follow

# Force new deployment
aws ecs update-service --cluster kickwatch-cluster-dev \
--service kickwatch-api-dev-service --force-new-deployment --region us-east-2
```
135 changes: 135 additions & 0 deletions .archive/2026-02-27/mvp-implementation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
---
date: 2026-02-27
title: KickWatch MVP Implementation
category: feature
tags: [go, gin, gorm, swiftui, swiftdata, kickstarter, graphql, apns, cron, xcodegen]
---

# KickWatch MVP Implementation

## What Was Built

Full MVP from scratch across 22+ atomic commits on `develop` branch.

### Backend (Go/Gin) — `backend/`

**Module**: `github.com/kickwatch/backend`

**Key packages added**:
```
github.com/gin-gonic/gin
github.com/joho/godotenv
github.com/google/uuid
github.com/golang-jwt/jwt/v5
github.com/robfig/cron/v3
gorm.io/gorm
gorm.io/driver/postgres
```

**Structure**:
- `internal/config/` — env var loader
- `internal/model/` — GORM models: Campaign, Category, Device, Alert, AlertMatch
- `internal/db/` — AutoMigrate on startup
- `internal/middleware/` — CORS, Logger
- `internal/service/kickstarter_rest.go` — REST /discover/advanced.json (no auth, nightly crawl)
- `internal/service/kickstarter_graph.go` — GraphQL /graph with session bootstrap (CSRF + _ksr_session cookie), 12h refresh, 403 retry
- `internal/service/apns.go` — APNs HTTP/2 with JWT signing (golang-jwt ES256)
- `internal/service/cron.go` — nightly 02:00 UTC crawl, 15 categories × 10 pages, upsert + alert matching + APNs push
- `internal/handler/` — campaigns, search, categories, devices, alerts CRUD

**API routes**:
```
GET /api/health
GET /api/campaigns?sort=trending|newest|ending&category_id=&cursor=&limit=
GET /api/campaigns/search?q=&category_id=&cursor=
GET /api/campaigns/:pid
GET /api/categories
POST /api/devices/register
POST /api/alerts
GET /api/alerts?device_id=
PATCH /api/alerts/:id
DELETE /api/alerts/:id
GET /api/alerts/:id/matches
```

**Gotcha**: `restProject` struct had duplicate json tag `"urls"` on two fields — caused `go vet` failure. Fixed by removing the unused `URL string` field.

### iOS (SwiftUI/SwiftData) — `ios/`

**project.yml** key settings:
```yaml
bundleIdPrefix: com.kickwatch
deploymentTarget: iOS: "17.0"
PRODUCT_BUNDLE_IDENTIFIER: com.kickwatch.app
DEVELOPMENT_TEAM: "" # fill in before building
```

**SwiftData models**: Campaign, WatchlistAlert, RecentSearch

**Services**:
- `APIClient` — actor, base URL switches DEBUG/Release, supports GET/POST/PATCH/DELETE
- `KeychainHelper` — identical pattern to SnapAction
- `NotificationService` — @MainActor ObservableObject, registers APNs token via APIClient, stores device_id in Keychain
- `ImageCache` — actor-based URL→Image cache with RemoteImage SwiftUI view

**ViewModels**: `@Observable` (iOS 17 pattern, not ObservableObject)
- `DiscoverViewModel` — sort + category filter + cursor pagination
- `AlertsViewModel` — full CRUD

**Views**: DiscoverView, CampaignRowView, CampaignDetailView (funding ring), WatchlistView, AlertsView, AlertMatchesView, SearchView, SettingsView, CategoryChip

**App entry**: `KickWatchApp` with `@UIApplicationDelegateAdaptor(AppDelegate.self)` for APNs token registration

### CI/CD
- `.github/workflows/test-backend.yml` — triggered on `backend/**` changes, runs `go build`, `go test`, `go vet`
- `.github/workflows/deploy-backend.yml` — triggered on `main` push, builds Docker image, pushes to ECR, deploys to ECS

## Git Workflow

- Worktree created at `.worktrees/develop` for `develop` branch
- `.worktrees/` added to `.gitignore` before creation
- All work on `develop`, never touched `main` directly
- Published repo: https://github.com/ReScienceLab/KickWatch

## .gitignore Fix

Original pattern `*.env` blocked `.env.example`. Changed to:
```
.env
.env.local
.env.production
!.env.example
```

## App Icon

- Generated via Gemini (nanobanana skill): Kickstarter K + newspaper/daily digest metaphor, Notion-style flat design, green (#05CE78)
- Best result: `logo-07.png` → user provided `o.png` as final source
- Processed: crop → remove_bg (remove.bg API) → vectorize (Recraft API) → SVG
- Centered in white background SVG: `final-centered.svg`
- Transform: `translate(1000,1000) scale(0.82) translate(-1044.5,-1086)` (content bbox: x 106–1983, y 285–1887)
- All 14 PNG sizes generated with `rsvg-convert` (homebrew):
```bash
rsvg-convert -w $SIZE -h $SIZE input.svg -o AppIcon-${SIZE}x${SIZE}.png
```
Sizes: 20, 29, 40, 58, 60, 76, 80, 87, 120, 152, 167, 180, 1024

## Commands Reference

```bash
# Backend
cd backend && go run ./cmd/api
cd backend && go test ./...
cd backend && go build ./... && go vet ./...

# iOS
cd ios && xcodegen generate
xcodebuild -project ios/KickWatch.xcodeproj -scheme KickWatch build

# Worktree
git worktree add .worktrees/develop -b develop
cd .worktrees/develop

# Icon generation
rsvg-convert -w 1024 -h 1024 final-centered.svg -o AppIcon-1024x1024.png
```
Loading
Loading