From 77b787c0d9815f669a1ccdbf50d90b522c695d3d Mon Sep 17 00:00:00 2001 From: Jeremy Eder Date: Mon, 8 Dec 2025 23:54:16 -0500 Subject: [PATCH 1/5] feat: add RFE creation and notifications, reorganize scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI Features: - Set Inbox as initial screen instead of Sessions - Add 'Create RFE' option to FAB menu with source attachment support - Replace 'Agent' with 'Deep Research' in FAB - Update 'GitHub Notifications' to 'Notifications' with multi-source filtering - Add RFE creation screen with Google Docs, URLs, and Jira source inputs - Add source filtering UI for GitHub, Google Workspace, Jira, GitLab, Miro Repository Organization: - Move dev.sh to scripts/ (new development environment manager) - Move start-offline.sh to scripts/ - Remove obsolete phase implementation scripts - Remove outdated security audit summary - Remove empty implementation log 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- SECURITY_AUDIT_SUMMARY.md | 187 ------ app/(tabs)/_layout.tsx | 13 +- app/(tabs)/index.tsx | 2 +- app/notifications/index.tsx | 57 +- app/rfe/create.tsx | 379 ++++++++++++ components/layout/CreateFAB.tsx | 3 +- implement-phases-auto.sh | 161 ------ implement-phases.sh | 578 ------------------- implementation.log | 0 scripts/dev.sh | 448 ++++++++++++++ start-offline.sh => scripts/start-offline.sh | 0 11 files changed, 892 insertions(+), 936 deletions(-) delete mode 100644 SECURITY_AUDIT_SUMMARY.md create mode 100644 app/rfe/create.tsx delete mode 100755 implement-phases-auto.sh delete mode 100755 implement-phases.sh delete mode 100644 implementation.log create mode 100755 scripts/dev.sh rename start-offline.sh => scripts/start-offline.sh (100%) diff --git a/SECURITY_AUDIT_SUMMARY.md b/SECURITY_AUDIT_SUMMARY.md deleted file mode 100644 index c382ec7..0000000 --- a/SECURITY_AUDIT_SUMMARY.md +++ /dev/null @@ -1,187 +0,0 @@ -# ACP Mobile Security Audit - Executive Summary - -**Date:** 2025-11-26 -**Overall Security Assessment:** MEDIUM -**App Store Approval Risk:** HIGH ⚠️ - ---- - -## Critical Findings Summary - -### 7 CRITICAL Issues Found - -1. **Missing Privacy Manifest Data Collection Declarations** - - **Risk:** Immediate App Store rejection - - **File:** `ios/ACPMobile/PrivacyInfo.xcprivacy` - - **Issue:** Empty `NSPrivacyCollectedDataTypes` array while app collects email, user ID, and session data - - **Fix Time:** 4 hours - -2. **No JWT Token Expiration Validation** - - **Risk:** Authentication bypass with expired tokens - - **File:** `services/auth/token-manager.ts` (line 48-51) - - **Issue:** `isAuthenticated()` only checks if token exists, never validates expiration - - **Fix Time:** 1 day - -3. **Excessive Debug Logging in Production** - - **Risk:** Credential exposure in crash reports - - **Files:** `services/api/client.ts`, `hooks/useAuth.tsx`, `services/api/realtime.ts` - - **Issue:** console.log() calls without **DEV** guards expose sensitive data - - **Fix Time:** 1 day - -4. **Missing SSL Certificate Pinning** - - **Risk:** Man-in-the-Middle attacks, OAuth token theft - - **File:** `services/api/client.ts` - - **Issue:** No certificate pinning allows network interception - - **Fix Time:** 2 days - -5. **Insecure Deep Link Handling** - - **Risk:** OAuth callback interception, authentication bypass - - **Files:** `app.json`, `services/auth/oauth.ts` - - **Issue:** Custom URL scheme `acp://` can be hijacked by malicious apps - - **Fix Time:** 1 day - -6. **No API Response Validation** - - **Risk:** App crashes, XSS, data corruption - - **Files:** `services/api/sessions.ts`, `services/api/realtime.ts` - - **Issue:** No runtime validation of API responses - - **Fix Time:** 2 days - -7. **Code Verifier Stored in Memory** - - **Risk:** PKCE security violation, authorization code theft - - **File:** `services/auth/oauth.ts` (line 14) - - **Issue:** OAuth code verifier stored as class variable instead of SecureStore - - **Fix Time:** 4 hours - ---- - -## High Priority Findings (5 Issues) - -- Missing jailbreak/root detection -- No request rate limiting -- Missing certificate expiration monitoring -- Insufficient error information disclosure -- No timeout configuration for SSE connections - ---- - -## Medium Priority Findings (4 Issues) - -- AsyncStorage used for potentially sensitive repository data -- No biometric authentication option -- Missing network connectivity validation -- No request deduplication - ---- - -## App Store Compliance Status - -| Requirement | Status | Action Required | -| ---------------- | ------------- | -------------------------------- | -| Privacy Manifest | ❌ INCOMPLETE | Add data collection declarations | -| SSL/TLS Security | ❌ MISSING | Implement certificate pinning | -| OAuth Deep Links | ❌ INSECURE | Migrate to Universal Links | -| Input Validation | ❌ MISSING | Add Zod validation schemas | -| Secure Storage | ✅ GOOD | Tokens in SecureStore | -| Debug Logging | ⚠️ PARTIAL | Remove production logging | - ---- - -## OWASP Mobile Top 10 Compliance - -- **M2 - Insecure Data Storage:** ⚠️ PARTIAL (code verifier issue) -- **M3 - Insecure Communication:** ❌ FAIL (no SSL pinning) -- **M4 - Insecure Authentication:** ❌ FAIL (token validation, deep links) -- **M7 - Client Code Quality:** ❌ FAIL (input validation, logging) -- **M8 - Code Tampering:** ❌ FAIL (no jailbreak detection) - ---- - -## Remediation Roadmap - -### Phase 1: Critical Blockers (Required Before App Store Submission) - -**Timeline:** 2 weeks - -1. Update Privacy Manifest - 4 hours -2. Implement SSL Certificate Pinning - 2 days -3. Fix Deep Link Security (Universal Links) - 1 day -4. Add Input Validation (Zod) - 2 days -5. Implement Token Expiration Checks - 1 day -6. Secure Code Verifier Storage - 4 hours -7. Fix Debug Logging - 1 day - -**Total:** 8-10 days of focused development - -### Phase 2: High Priority Security (Post-Launch) - -**Timeline:** 1 week after App Store approval - -- Jailbreak detection -- Rate limiting -- Error sanitization -- SSE timeouts - -### Phase 3: Security Hardening (Ongoing) - -- Biometric authentication -- Advanced monitoring -- Code obfuscation - ---- - -## Key Recommendations - -### DO NOT SUBMIT TO APP STORE UNTIL: - -1. ✅ Privacy manifest updated with data collection declarations -2. ✅ SSL certificate pinning implemented -3. ✅ Universal Links replace custom URL scheme -4. ✅ JWT token expiration validation added -5. ✅ Input validation on all API responses -6. ✅ Debug logging removed from production -7. ✅ Code verifier moved to SecureStore - -### Immediate Actions: - -```bash -# Install required security dependencies -npm install zod jwt-decode react-native-ssl-pinning jail-monkey - -# Create environment configuration -cp .env.example .env -# (Configure production API URLs) -``` - ---- - -## Testing Checklist Before Submission - -- [ ] Test OAuth flow with Universal Links -- [ ] Verify SSL pinning blocks mitmproxy -- [ ] Test with expired JWT token -- [ ] Test with malformed API responses -- [ ] Verify no sensitive data in logs (production build) -- [ ] Test on jailbroken device (should block authentication) -- [ ] Verify Privacy Manifest accepted by App Store Connect - ---- - -## Security Audit Artifacts - -1. **Full Audit Report:** `SECURITY_AUDIT_REPORT.md` (detailed findings) -2. **This Summary:** `SECURITY_AUDIT_SUMMARY.md` -3. **Next Review:** After critical fixes implemented - ---- - -**App Store Submission Recommendation:** ❌ **NOT READY** - -**Estimated Time to Production-Ready:** 2-3 weeks - -**Primary Concerns:** - -- Guaranteed App Store rejection due to incomplete Privacy Manifest -- OAuth security vulnerabilities expose user accounts -- No SSL pinning allows credential theft on public WiFi - -**Auditor:** Secure Software Braintrust (AI Security Team) diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 77349ba..34445dc 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -33,19 +33,20 @@ export default function TabLayout() { headerShown: false, tabBarButton: HapticTab, }} + initialRouteName="inbox" > , + title: 'Inbox', + tabBarIcon: ({ color }) => , }} /> , + title: 'Sessions', + tabBarIcon: ({ color }) => , }} /> 0 ? unreadCount : undefined, onPress: () => router.push('/notifications'), }, diff --git a/app/notifications/index.tsx b/app/notifications/index.tsx index 4319ce5..048c269 100644 --- a/app/notifications/index.tsx +++ b/app/notifications/index.tsx @@ -10,11 +10,13 @@ import { OfflineBanner } from '@/components/ui/OfflineBanner' import type { GitHubNotification } from '@/types/notification' type FilterType = 'all' | 'unread' +type SourceFilter = 'all' | 'github' | 'google' | 'jira' | 'gitlab' | 'miro' export default function NotificationsScreen() { const { colors } = useTheme() const { isOffline } = useOffline() const [filter, setFilter] = useState('all') + const [sourceFilter, setSourceFilter] = useState('all') const { notifications, unreadCount, isLoading, refetch } = useNotifications(filter === 'unread') const { showActions } = useNotificationActions() const markAllAsRead = useMarkAllAsRead() @@ -24,6 +26,15 @@ export default function NotificationsScreen() { { label: 'Unread', value: 'unread', badge: unreadCount }, ] + const sourceFilters: { label: string; value: SourceFilter; icon: string }[] = [ + { label: 'All Sources', value: 'all', icon: 'filter' }, + { label: 'GitHub', value: 'github', icon: 'github' }, + { label: 'Google Workspace', value: 'google', icon: 'mail' }, + { label: 'Jira', value: 'jira', icon: 'trello' }, + { label: 'GitLab', value: 'gitlab', icon: 'gitlab' }, + { label: 'Miro', value: 'miro', icon: 'grid' }, + ] + const handleNotificationPress = useCallback( (notification: GitHubNotification) => { showActions(notification, () => { @@ -68,7 +79,7 @@ export default function NotificationsScreen() { {/* Header with Mark All Read button */} - GitHub Notifications + Notifications {unreadCount > 0 && ( - {/* Filter Chips */} + {/* Filter Chips - Read/Unread */} + {/* Source Filter Chips */} + + {sourceFilters.map((f) => { + const isActive = sourceFilter === f.value + return ( + setSourceFilter(f.value)} + activeOpacity={0.7} + accessibilityRole="button" + accessibilityLabel={`Filter by ${f.label}`} + accessibilityState={{ selected: isActive }} + > + + + {f.label} + + + ) + })} + + {/* Notifications List */} ([]) + const [newSourceType, setNewSourceType] = useState<'gdoc' | 'url' | 'jira'>('gdoc') + const [newSourceValue, setNewSourceValue] = useState('') + + const addSource = () => { + if (newSourceValue.trim()) { + const newSource: Source = { + id: Date.now().toString(), + type: newSourceType, + value: newSourceValue.trim(), + } + setSources([...sources, newSource]) + setNewSourceValue('') + } + } + + const removeSource = (id: string) => { + setSources(sources.filter((s) => s.id !== id)) + } + + const handleSubmit = () => { + // TODO: Implement batch mode submission with attached gdoc design document + console.log('Creating RFE in batch mode:', { title, description, sources }) + // For now, just navigate back + router.back() + } + + const getSourceIcon = (type: 'gdoc' | 'url' | 'jira') => { + switch (type) { + case 'gdoc': + return 'file-text' + case 'url': + return 'link' + case 'jira': + return 'package' + default: + return 'file' + } + } + + return ( + + {/* Header */} + + router.back()} + style={styles.backButton} + accessibilityRole="button" + accessibilityLabel="Go back" + > + + + Create RFE + + + + + {/* Title Input */} + + Title + + + + {/* Description Input */} + + Description + + + + {/* Sources Section */} + + Sources & Context + + Add Google Docs, URLs, or Jira tickets as context for your RFE + + + {/* Source Type Selector */} + + {(['gdoc', 'url', 'jira'] as const).map((type) => ( + setNewSourceType(type)} + accessibilityRole="button" + accessibilityLabel={`Select ${type} type`} + > + + + {type.toUpperCase()} + + + ))} + + + {/* Source Input */} + + + + + + + + {/* Sources List */} + {sources.length > 0 && ( + + {sources.map((source) => ( + + + + + {source.type.toUpperCase()} + + + {source.value} + + + removeSource(source.id)} + style={styles.removeButton} + accessibilityRole="button" + accessibilityLabel="Remove source" + > + + + + ))} + + )} + + + {/* Batch Mode Info */} + + + + This RFE will be created in batch mode. Attach your Google Doc design document to the + session for processing. + + + + {/* Submit Button */} + + Create RFE + + + + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingTop: Platform.OS === 'ios' ? 60 : 20, + paddingBottom: 16, + paddingHorizontal: 16, + borderBottomWidth: 1, + }, + backButton: { + padding: 4, + }, + headerTitle: { + fontSize: 18, + fontWeight: '700', + }, + content: { + flex: 1, + paddingHorizontal: 16, + }, + section: { + marginTop: 24, + }, + label: { + fontSize: 16, + fontWeight: '600', + marginBottom: 8, + }, + helperText: { + fontSize: 14, + marginBottom: 12, + }, + input: { + borderWidth: 1, + borderRadius: 8, + padding: 12, + fontSize: 16, + }, + textArea: { + borderWidth: 1, + borderRadius: 8, + padding: 12, + fontSize: 16, + minHeight: 100, + }, + sourceTypeContainer: { + flexDirection: 'row', + gap: 8, + marginBottom: 12, + }, + typeButton: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 8, + borderWidth: 1, + }, + typeButtonText: { + fontSize: 12, + fontWeight: '600', + }, + sourceInputContainer: { + flexDirection: 'row', + gap: 8, + marginBottom: 16, + }, + sourceInput: { + flex: 1, + borderWidth: 1, + borderRadius: 8, + padding: 12, + fontSize: 16, + }, + addButton: { + width: 44, + height: 44, + borderRadius: 8, + justifyContent: 'center', + alignItems: 'center', + }, + sourcesList: { + gap: 8, + }, + sourceItem: { + flexDirection: 'row', + alignItems: 'center', + padding: 12, + borderRadius: 8, + borderWidth: 1, + gap: 12, + }, + sourceContent: { + flex: 1, + }, + sourceType: { + fontSize: 11, + fontWeight: '600', + marginBottom: 2, + }, + sourceValue: { + fontSize: 14, + }, + removeButton: { + padding: 4, + }, + infoBanner: { + flexDirection: 'row', + gap: 12, + padding: 16, + borderRadius: 8, + marginTop: 24, + }, + infoText: { + flex: 1, + fontSize: 14, + lineHeight: 20, + }, + submitButton: { + marginTop: 24, + padding: 16, + borderRadius: 12, + alignItems: 'center', + }, + submitButtonText: { + color: '#fff', + fontSize: 16, + fontWeight: '700', + }, +}) diff --git a/components/layout/CreateFAB.tsx b/components/layout/CreateFAB.tsx index 7278df4..e947811 100644 --- a/components/layout/CreateFAB.tsx +++ b/components/layout/CreateFAB.tsx @@ -23,7 +23,8 @@ export function CreateFAB() { // @ts-expect-error lucide-react-native icon name type complexity const createOptions: CreateOption[] = [ - { id: 'agent', label: 'Agent', icon: 'user', soon: false }, + { id: 'deep-research', label: 'Deep Research', icon: 'search', soon: false }, + { id: 'rfe', label: 'Create RFE', icon: 'file-plus', route: '/rfe/create' }, { id: 'scheduled-task', label: 'Scheduled Task', icon: 'clock', soon: false }, { id: 'session', label: 'Session', icon: 'zap', route: '/sessions/new' }, { id: 'skill', label: 'Skill', icon: 'target', soon: false }, diff --git a/implement-phases-auto.sh b/implement-phases-auto.sh deleted file mode 100755 index cbc8fcf..0000000 --- a/implement-phases-auto.sh +++ /dev/null @@ -1,161 +0,0 @@ -#!/usr/bin/env bash - -# ACP Mobile - Fully Automated Phase Implementation Script -# Uses Claude Code to implement all phases automatically - -set -e - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -# Configuration -MAIN_BRANCH="main" -REPO_ROOT="/Users/jeder/repos/acp-mobile" -WORKTREES_DIR="${REPO_ROOT}/../acp-mobile-worktrees" -PROMPTS_DIR="${REPO_ROOT}/.claude/prompts/phases" - -print_header() { echo -e "\n${GREEN}▶ $1${NC}\n"; } -print_success() { echo -e "${GREEN}✓ $1${NC}"; } -print_warning() { echo -e "${YELLOW}⚠ $1${NC}"; } -print_error() { echo -e "${RED}✗ Error: $1${NC}"; } - -cd "$REPO_ROOT" -mkdir -p "$PROMPTS_DIR" "$WORKTREES_DIR" - -echo -e "${BLUE}============================================${NC}" -echo -e "${BLUE}ACP Mobile - Automated Implementation${NC}" -echo -e "${BLUE}============================================${NC}" -echo "" - -# Phase definitions -declare -A PHASES -PHASES["phase-11"]="Phase 11: Verify Phase 7 Status|feature/phase-11-verify-session-creation" -PHASES["phase-12a"]="Phase 12A: Verify API Status Indicator (FR-018)|feature/phase-12a-api-status-indicator" -PHASES["phase-12b"]="Phase 12B: Decision on Announcements Feature (FR-024)|feature/phase-12b-announcements-feature" -PHASES["phase-13a"]="Phase 13A: Quiet Hours Feature|feature/phase-13a-quiet-hours" -PHASES["phase-13b"]="Phase 13B: Deep Linking Optimization|feature/phase-13b-deep-linking" -PHASES["phase-13c"]="Phase 13C: Bundle Size Optimization|feature/phase-13c-bundle-size" -PHASES["phase-13d"]="Phase 13D: App Telemetry Integration|feature/phase-13d-telemetry" - -all_phases=("phase-11" "phase-12a" "phase-12b" "phase-13a" "phase-13b" "phase-13c" "phase-13d") - -# Update main -print_header "Updating Main Branch" -git checkout "$MAIN_BRANCH" -git fetch origin -git rebase "origin/${MAIN_BRANCH}" - -# Create all worktrees -print_header "Creating Worktrees" -for phase_id in "${all_phases[@]}"; do - IFS='|' read -r phase_name branch_name <<< "${PHASES[$phase_id]}" - worktree_path="${WORKTREES_DIR}/${phase_id}" - - # Cleanup - [ -d "$worktree_path" ] && git worktree remove --force "$worktree_path" 2>/dev/null || true - git rev-parse --verify "$branch_name" >/dev/null 2>&1 && git branch -D "$branch_name" - - # Create worktree - git worktree add -b "$branch_name" "$worktree_path" "$MAIN_BRANCH" - print_success "Created worktree: ${phase_name}" -done - -# Implement all phases in parallel using Claude Code -print_header "Implementing All Phases in Parallel" - -pids=() -for phase_id in "${all_phases[@]}"; do - IFS='|' read -r phase_name branch_name <<< "${PHASES[$phase_id]}" - worktree_path="${WORKTREES_DIR}/${phase_id}" - prompt_file="${PROMPTS_DIR}/${phase_id}.md" - - ( - cd "$worktree_path" - echo "Implementing ${phase_name}..." - - # Read the prompt and execute with claude - if [ -f "$prompt_file" ]; then - # Use claude command to execute the prompt - cat "$prompt_file" | claude --dangerously-skip-permissions 2>&1 | tee "${worktree_path}/implementation.log" - else - echo "Warning: Prompt file not found for ${phase_id}" - fi - ) & - pids+=($!) - print_success "Started implementation: ${phase_name} (PID: ${pids[-1]})" -done - -# Wait for all implementations to complete -print_header "Waiting for All Implementations to Complete" -for pid in "${pids[@]}"; do - wait $pid - echo "Process $pid completed" -done -print_success "All implementations completed!" - -# Create and merge PRs for all phases -print_header "Creating and Merging PRs" - -for phase_id in "${all_phases[@]}"; do - IFS='|' read -r phase_name branch_name <<< "${PHASES[$phase_id]}" - worktree_path="${WORKTREES_DIR}/${phase_id}" - - cd "$worktree_path" - - # Check for commits - if [ "$(git rev-list --count ${MAIN_BRANCH}..HEAD)" -eq 0 ]; then - print_warning "No commits found for ${phase_name}. Skipping." - continue - fi - - # Push branch - print_header "Pushing ${phase_name}" - git push -u origin "$branch_name" - - # Create PR - pr_url=$(gh pr create \ - --title "${phase_name}" \ - --body "## Phase: ${phase_id} - -### Implementation Summary -Implements ${phase_name} as defined in the phase implementation plan. - -### Prompt Reference -See \`.claude/prompts/phases/${phase_id}.md\` - -### Testing -- [x] Automated implementation -- [x] All checks pass - -🤖 Generated with [Claude Code](https://claude.com/claude-code) - -Co-Authored-By: Claude " | tail -n 1) - - print_success "PR created: ${pr_url}" - - # Merge PR - pr_number=$(echo "$pr_url" | grep -oE '[0-9]+$') - gh pr merge "$pr_number" --squash --delete-branch --auto - print_success "PR #${pr_number} merged!" -done - -# Cleanup -cd "$REPO_ROOT" -print_header "Cleaning Up" -for phase_id in "${all_phases[@]}"; do - git worktree remove --force "${WORKTREES_DIR}/${phase_id}" 2>/dev/null || true -done -git worktree prune - -# Update main -git checkout "$MAIN_BRANCH" -git fetch origin -git rebase "origin/${MAIN_BRANCH}" - -print_header "✅ All Phases Complete!" -echo "All 7 phases implemented, committed, and merged to main!" -echo "" diff --git a/implement-phases.sh b/implement-phases.sh deleted file mode 100755 index 28d0395..0000000 --- a/implement-phases.sh +++ /dev/null @@ -1,578 +0,0 @@ -#!/bin/bash - -# ACP Mobile - Automated Phase Implementation Script -# Implements phases 11, 12A, 12B, and 13A-D with PR creation - -set -e # Exit on error - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Configuration -MAIN_BRANCH="main" -REPO_ROOT="/Users/jeder/repos/acp-mobile" -PROMPTS_DIR="${REPO_ROOT}/.claude/prompts/phases" - -# Ensure we're in the repo root -cd "$REPO_ROOT" - -# Create prompts directory if it doesn't exist -mkdir -p "$PROMPTS_DIR" - -echo -e "${BLUE}============================================${NC}" -echo -e "${BLUE}ACP Mobile - Phase Implementation Workflow${NC}" -echo -e "${BLUE}============================================${NC}" -echo "" - -# Function to print section headers -print_header() { - echo -e "\n${GREEN}▶ $1${NC}\n" -} - -# Function to print errors -print_error() { - echo -e "${RED}✗ Error: $1${NC}" -} - -# Function to print success -print_success() { - echo -e "${GREEN}✓ $1${NC}" -} - -# Function to print warnings -print_warning() { - echo -e "${YELLOW}⚠ $1${NC}" -} - -# Function to create phase prompt file -create_prompt_file() { - local phase_id="$1" - local phase_name="$2" - local prompt_content="$3" - - local prompt_file="${PROMPTS_DIR}/${phase_id}.md" - - echo "$prompt_content" > "$prompt_file" - print_success "Created prompt file: $prompt_file" -} - -# Function to implement a phase -implement_phase() { - local phase_id="$1" - local phase_name="$2" - local branch_name="$3" - local prompt_file="${PROMPTS_DIR}/${phase_id}.md" - - print_header "Implementing ${phase_name} (${phase_id})" - - # Ensure we're starting from main and rebase from upstream - echo "Switching to ${MAIN_BRANCH}..." - git checkout "$MAIN_BRANCH" - - echo "Fetching from upstream..." - git fetch origin - - echo "Rebasing ${MAIN_BRANCH} from origin/${MAIN_BRANCH}..." - git rebase "origin/${MAIN_BRANCH}" - - # Create and checkout feature branch - echo "Creating branch: ${branch_name}..." - if git rev-parse --verify "$branch_name" >/dev/null 2>&1; then - print_warning "Branch ${branch_name} already exists. Deleting and recreating..." - git branch -D "$branch_name" - fi - git checkout -b "$branch_name" - - # Display the prompt for Claude - print_header "Phase Prompt" - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - cat "$prompt_file" - echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo "" - - # Pause for Claude to implement - print_warning "Ready to implement ${phase_name}" - echo "" - echo "Next steps:" - echo " 1. Review the prompt above" - echo " 2. Use Claude Code to implement the requirements" - echo " 3. Claude will commit changes automatically" - echo " 4. Press ENTER when implementation is complete" - echo "" - read -p "Press ENTER to continue to PR creation, or Ctrl+C to abort..." - - # Check if there are commits on this branch - if [ "$(git rev-list --count ${MAIN_BRANCH}..HEAD)" -eq 0 ]; then - print_error "No commits found on ${branch_name}. Skipping PR creation." - return 1 - fi - - # Push branch - print_header "Pushing branch to remote" - git push -u origin "$branch_name" - - # Create PR - print_header "Creating Pull Request" - local pr_url=$(gh pr create \ - --title "${phase_name}" \ - --body "$(cat </dev/null 2>&1 +} + +# Kill processes on a specific port +kill_port() { + local port=$1 + if check_port $port; then + log_warn "Killing process on port $port" + lsof -ti:$port | xargs kill -9 2>/dev/null || true + sleep 1 + fi +} + +# Stop all development processes +stop_dev() { + log_info "Stopping all development processes..." + + # Kill Metro bundler (port 8081) + kill_port 8081 + + # Kill Expo dev tools (port 19000, 19001, 19002) + kill_port 19000 + kill_port 19001 + kill_port 19002 + + # Kill any Expo/Metro/React Native processes + pkill -f "expo start" 2>/dev/null || true + pkill -f "expo run:ios" 2>/dev/null || true + pkill -f "metro" 2>/dev/null || true + pkill -f "react-native" 2>/dev/null || true + + # Kill any node processes related to this project + pgrep -f "node.*$PROJECT_DIR" | xargs kill -9 2>/dev/null || true + + log_success "All development processes stopped" +} + +# Clean caches +clean_caches() { + log_info "Cleaning development caches..." + + # Clean Watchman + if command -v watchman &> /dev/null; then + log_info "Cleaning Watchman watches..." + watchman watch-del-all 2>/dev/null || true + log_success "Watchman cleaned" + fi + + # Clean Metro cache + log_info "Cleaning Metro bundler cache..." + rm -rf $HOME/.metro 2>/dev/null || true + rm -rf /tmp/metro-* 2>/dev/null || true + rm -rf /tmp/haste-* 2>/dev/null || true + rm -rf /tmp/react-* 2>/dev/null || true + + # Clean node_modules cache + log_info "Cleaning node_modules cache..." + rm -rf node_modules/.cache 2>/dev/null || true + + # Clean Expo cache + log_info "Cleaning Expo cache..." + rm -rf .expo 2>/dev/null || true + + # Clean iOS build artifacts (optional - can be slow) + if [ "$1" == "deep" ]; then + log_info "Deep clean: Removing iOS build artifacts..." + rm -rf ios/build 2>/dev/null || true + rm -rf ~/Library/Developer/Xcode/DerivedData/ACPMobile-* 2>/dev/null || true + fi + + log_success "Caches cleaned" +} + +# Full rebuild (reinstall dependencies) +rebuild() { + log_info "Starting full rebuild..." + + stop_dev + clean_caches deep + + log_info "Removing node_modules and package-lock.json..." + rm -rf node_modules + rm -f package-lock.json + + log_info "Reinstalling dependencies..." + npm install + + log_success "Rebuild complete" +} + +# Start development environment +start_dev() { + log_info "Starting development environment..." + + # Make sure no stray processes are running + stop_dev + sleep 2 + + # Start Expo in a new terminal window + log_info "Starting Metro bundler..." + osascript < /dev/null; then + log_success "iOS Simulator: RUNNING" + # Show which device + xcrun simctl list devices | grep Booted || true + else + log_warn "iOS Simulator: NOT RUNNING" + fi + + # Check for running processes + echo "" + log_info "Related processes:" + ps aux | grep -E "expo|metro|node.*$PROJECT_DIR" | grep -v grep || echo " None found" + + # Check ports + echo "" + log_info "Port status:" + echo " 8081 (Metro): $(lsof -i :8081 -sTCP:LISTEN -t >/dev/null 2>&1 && echo '✓ In use' || echo '✗ Free')" + echo " 19000 (Expo): $(lsof -i :19000 -sTCP:LISTEN -t >/dev/null 2>&1 && echo '✓ In use' || echo '✗ Free')" + echo " 19001 (Expo): $(lsof -i :19001 -sTCP:LISTEN -t >/dev/null 2>&1 && echo '✓ In use' || echo '✗ Free')" +} + +# Show help +show_help() { + cat << EOF +${BLUE}Expo React Native Development Environment Manager${NC} + +${GREEN}Usage:${NC} + ./dev.sh [command] + +${GREEN}Commands:${NC} + ${YELLOW}start${NC} Start Metro bundler and iOS simulator in new terminal windows + ${YELLOW}stop${NC} Stop all development processes (Metro, Expo, iOS builds) + ${YELLOW}restart${NC} Stop and restart the development environment + ${YELLOW}clean${NC} Clean all caches (Metro, Watchman, Expo, node_modules/.cache) + ${YELLOW}deep-clean${NC} Clean caches + iOS build artifacts (slower) + ${YELLOW}rebuild${NC} Full rebuild (stop, deep-clean, reinstall dependencies) + ${YELLOW}status${NC} Show status of development processes and ports + ${YELLOW}help${NC} Show this help message + +${GREEN}Examples:${NC} + ./dev.sh start # Start development environment + ./dev.sh stop # Stop everything + ./dev.sh clean # Clean caches when things are acting weird + ./dev.sh rebuild # Nuclear option - full reinstall + +${GREEN}Common Issues:${NC} + "Port already in use" → Run: ./dev.sh stop + "Module not found" → Run: ./dev.sh rebuild + "Bundler acting weird" → Run: ./dev.sh clean && ./dev.sh restart + "Nothing works" → Run: ./dev.sh rebuild + +${GREEN}Quick Reference:${NC} + Metro bundler runs on port 8081 + Expo dev tools run on ports 19000-19002 + Press 'r' in Metro terminal to reload + Press 'i' in Metro terminal to open iOS simulator + +EOF +} + +# Show component relationships +show_architecture() { + cat << 'EOF' +╔════════════════════════════════════════════════════════════════════════════╗ +║ EXPO REACT NATIVE ARCHITECTURE ║ +╚════════════════════════════════════════════════════════════════════════════╝ + +┌─────────────────────────────────────────────────────────────────────────┐ +│ YOUR DEVELOPMENT MACHINE │ +│ │ +│ ┌──────────────────────────────────────────────────────────────┐ │ +│ │ 1. METRO BUNDLER (JavaScript Bundler) │ │ +│ │ Port: 8081 │ │ +│ │ • Takes your React Native code (JS/TSX) │ │ +│ │ • Bundles it into a single JavaScript file │ │ +│ │ • Serves it to the iOS/Android app │ │ +│ │ • Watches for file changes (Fast Refresh) │ │ +│ │ • Command: npx expo start │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌──────────────────────────────────────────────────────────────┐ │ +│ │ 2. EXPO CLI (Development Orchestrator) │ │ +│ │ Ports: 19000-19002 │ │ +│ │ • Manages Metro bundler │ │ +│ │ • Provides dev tools UI │ │ +│ │ • Handles platform-specific builds │ │ +│ │ • Manages native dependencies │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌──────────────────────────────────────────────────────────────┐ │ +│ │ 3. XCODE BUILD TOOLS (Native iOS Build) │ │ +│ │ • Compiles native iOS code (Objective-C/Swift) │ │ +│ │ • Links native modules (Expo modules, etc.) │ │ +│ │ • Creates .app bundle for simulator/device │ │ +│ │ • Command: npx expo run:ios (or npm run ios) │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +│ ↓ │ +└─────────────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────────────┐ +│ iOS SIMULATOR │ +│ ┌──────────────────────────────────────────────────────────────┐ │ +│ │ 4. SIMULATOR APP (Virtual iPhone/iPad) │ │ +│ │ • Runs your compiled .app bundle │ │ +│ │ • Connects to Metro on localhost:8081 │ │ +│ │ • Downloads JavaScript bundle │ │ +│ │ • Executes React Native JavaScript │ │ +│ │ • Renders UI using native iOS components │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + +╔════════════════════════════════════════════════════════════════════════════╗ +║ DATA FLOW EXPLAINED ║ +╚════════════════════════════════════════════════════════════════════════════╝ + +1. You write code in src/ (TypeScript/React) + ↓ +2. Watchman detects file changes (macOS file watching service) + ↓ +3. Metro Bundler re-bundles your JavaScript + ↓ +4. Fast Refresh sends update to simulator via WebSocket + ↓ +5. React Native runtime updates UI without full reload + +╔════════════════════════════════════════════════════════════════════════════╗ +║ COMMON PORT USAGE ║ +╚════════════════════════════════════════════════════════════════════════════╝ + +8081 Metro Bundler (JavaScript served here) +19000 Expo Dev Server (main) +19001 Expo Dev Server (alternative) +19002 Expo Dev Tools UI + +╔════════════════════════════════════════════════════════════════════════════╗ +║ WHAT EACH TOOL DOES ║ +╚════════════════════════════════════════════════════════════════════════════╝ + +METRO BUNDLER +• JavaScript bundler (like webpack for React Native) +• Converts your ES6/TypeScript → ES5 JavaScript +• Handles module resolution (imports/requires) +• Serves bundle to app over HTTP +• Watches files for changes + +EXPO CLI +• Wrapper around Metro with extra features +• Manages Expo SDK modules (camera, location, etc.) +• Provides QR code for Expo Go app +• Handles iOS/Android builds +• Manages native dependencies automatically + +XCODE +• Apple's native iOS development tools +• Compiles native iOS code (C/Objective-C/Swift) +• Links native libraries +• Signs the app bundle +• Required for iOS simulator/device builds + +WATCHMAN +• Facebook's file watching service +• Detects when you save files +• Triggers Metro to rebuild +• More efficient than Node's fs.watch + +REACT NATIVE +• JavaScript framework for native mobile apps +• Your code runs in JavaScript engine (JSC/Hermes) +• Communicates with native iOS via "bridge" +• Native components render iOS UI (not WebView!) + +╔════════════════════════════════════════════════════════════════════════════╗ +║ TROUBLESHOOTING GUIDE ║ +╚════════════════════════════════════════════════════════════════════════════╝ + +"Port 8081 already in use" + → Metro is already running or crashed + → Fix: ./dev.sh stop + +"Unable to load script from assets" + → Metro not running or app can't connect + → Fix: ./dev.sh restart + +"Invariant Violation: Module AppRegistry is not registered" + → Metro cache corrupted + → Fix: ./dev.sh clean + +"Build failed" / "Xcode errors" + → iOS dependencies out of sync + → Fix: cd ios && pod install && cd .. (if using pods) + → Fix: ./dev.sh rebuild + +"Watchman warning about recrawl" + → Watchman database corrupted + → Fix: watchman watch-del-all + +"Changes not reflecting" + → Metro not detecting changes + → Fix: Press 'r' in Metro terminal to reload + → Fix: ./dev.sh clean && ./dev.sh restart + +EOF +} + +# Main command handler +case "${1:-help}" in + start) + start_dev + ;; + stop) + stop_dev + ;; + restart) + restart_dev + ;; + clean) + stop_dev + clean_caches + log_success "Ready to start fresh with: ./dev.sh start" + ;; + deep-clean) + stop_dev + clean_caches deep + log_success "Deep clean complete. Ready to start: ./dev.sh start" + ;; + rebuild) + rebuild + log_success "Rebuild complete. Start with: ./dev.sh start" + ;; + status) + show_status + ;; + arch|architecture) + show_architecture + ;; + help|--help|-h) + show_help + show_architecture + ;; + *) + log_error "Unknown command: $1" + echo "" + show_help + exit 1 + ;; +esac diff --git a/start-offline.sh b/scripts/start-offline.sh similarity index 100% rename from start-offline.sh rename to scripts/start-offline.sh From 06af426451ee73e2bedba5b05652564dbd64dc55 Mon Sep 17 00:00:00 2001 From: Jeremy Eder Date: Mon, 8 Dec 2025 23:56:51 -0500 Subject: [PATCH 2/5] chore: remove obsolete API contract verification doc The API contract verification document is no longer needed. Co-Authored-By: Claude Sonnet 4.5 --- API_CONTRACT_VERIFICATION.md | 265 ----------------------------------- 1 file changed, 265 deletions(-) delete mode 100644 API_CONTRACT_VERIFICATION.md diff --git a/API_CONTRACT_VERIFICATION.md b/API_CONTRACT_VERIFICATION.md deleted file mode 100644 index f943ccd..0000000 --- a/API_CONTRACT_VERIFICATION.md +++ /dev/null @@ -1,265 +0,0 @@ -# API Contract Verification - Phase 7 Session Creation - -## Issue Summary - -**Location**: `services/api/sessions.ts:85` -**Status**: ⚠️ POTENTIAL MISMATCH -**Severity**: Medium - May cause runtime failures in production - -## The Problem - -The `createSessionFromRepo()` helper function contains a suspicious API contract mapping: - -```typescript -// services/api/sessions.ts:71-89 -export async function createSessionFromRepo(params: { - name: string - repositoryId: string // ← Input parameter - workflowType: string - model: string - description?: string -}): Promise { - const request: CreateSessionRequest = { - name: params.name, - workflowType: params.workflowType, - model: params.model as ModelType, - repositoryUrl: params.repositoryId, // ← Passed as repositoryUrl (LINE 85) - } - - return SessionsAPI.createSession(request) -} -``` - -### The Code Comment - -Line 85 includes the comment: `// Backend should handle this` - -This suggests uncertainty about whether: - -1. The backend API actually expects `repositoryUrl` but the frontend has `repositoryId` -2. The backend will accept a repository ID in the `repositoryUrl` field -3. This is a temporary workaround pending backend changes - -## Type Definitions - -### Frontend Type (types/session.ts:41-47) - -```typescript -export interface CreateSessionRequest { - name?: string - workflowType: string - model: ModelType - repositoryUrl: string // ← API expects URL, but receives ID - branch?: string -} -``` - -### Repository Type (types/api.ts) - -```typescript -export interface Repository { - id: string // ← This is what we have - name: string - url: string // ← This is what API expects? - branch: string - isConnected: boolean -} -``` - -## Potential Issues - -### 1. Runtime Validation Failure - -If the backend validates `repositoryUrl` as a URL (e.g., must start with `https://`), passing a repository ID like `"repo-123"` will fail. - -**Expected format**: `https://github.com/user/repo` -**Actual value sent**: `repo-123` - -### 2. Backend Lookup Failure - -If the backend uses `repositoryUrl` to look up the repository in its database, it won't find a match for the ID. - -### 3. Inconsistent Data Model - -The type system says `repositoryUrl: string`, but the implementation passes `repositoryId`. This creates a lie in the type system. - -## Test Coverage - -The unit tests in `services/api/__tests__/sessions-helper.test.ts` explicitly verify this behavior: - -```typescript -// Line 129-146: API contract verification test -it('passes repositoryId as repositoryUrl to backend', async () => { - let capturedPayload: any - - mock.onPost('/sessions').reply((config) => { - capturedPayload = JSON.parse(config.data) - // ... - }) - - await createSessionFromRepo({ - name: 'test session', - repositoryId: 'repo-123', - workflowType: 'review', - model: 'sonnet-4.5', - }) - - // Verify the API contract quirk - expect(capturedPayload.repositoryUrl).toBe('repo-123') - expect(capturedPayload.repositoryId).toBeUndefined() -}) -``` - -## Verification Steps Required - -### Step 1: Check Backend API Documentation - -Find the backend API specification for `POST /sessions`: - -- What field name does it expect? (`repositoryId` or `repositoryUrl`) -- What format does it expect? (ID string or full GitHub URL) -- Does it validate the format? - -### Step 2: Test with Real Backend - -If backend is available: - -```typescript -// Test Case 1: Send repository ID -POST /sessions -{ - "name": "Test Session", - "workflowType": "review", - "model": "sonnet-4.5", - "repositoryUrl": "repo-123" // ID instead of URL -} - -// Expected: Does this succeed or fail? -``` - -```typescript -// Test Case 2: Send repository URL -POST /sessions -{ - "name": "Test Session", - "workflowType": "review", - "model": "sonnet-4.5", - "repositoryUrl": "https://github.com/user/my-app" -} - -// Expected: Does this succeed? -``` - -### Step 3: Check Backend Code - -If you have access to backend code, search for: - -- Session creation endpoint handler -- Parameter validation logic -- Repository lookup logic - -## Recommended Solutions - -### Option 1: Fix the API Contract (Preferred) - -Change the frontend to send the actual repository URL: - -```typescript -export async function createSessionFromRepo(params: { - name: string - repository: Repository // ← Pass full repository object - workflowType: string - model: string - description?: string -}): Promise { - const request: CreateSessionRequest = { - name: params.name, - workflowType: params.workflowType, - model: params.model as ModelType, - repositoryUrl: params.repository.url, // ← Send the actual URL - } - - return SessionsAPI.createSession(request) -} -``` - -**Impact**: Requires updating `app/sessions/new.tsx:63-68` to pass the full repository object. - -### Option 2: Update Type Definition - -If backend actually accepts repository ID, update the type: - -```typescript -export interface CreateSessionRequest { - name?: string - workflowType: string - model: ModelType - repositoryId: string // ← Rename to match reality - branch?: string -} -``` - -**Impact**: Changes the API contract to match actual implementation. - -### Option 3: Backend Lookup - -If backend needs URL but only has ID, do a repository lookup first: - -```typescript -export async function createSessionFromRepo(params: { - name: string - repositoryId: string - workflowType: string - model: string -}): Promise { - // Fetch the repository to get the URL - const repos = await fetchRepos() - const repo = repos.find((r) => r.id === params.repositoryId) - - if (!repo) { - throw new Error(`Repository not found: ${params.repositoryId}`) - } - - const request: CreateSessionRequest = { - name: params.name, - workflowType: params.workflowType, - model: params.model as ModelType, - repositoryUrl: repo.url, // ← Use the actual URL - } - - return SessionsAPI.createSession(request) -} -``` - -**Impact**: Adds extra API call, but maintains type safety. - -## Files Affected - -If we implement Option 1 (recommended): - -1. `services/api/sessions.ts` - Update `createSessionFromRepo()` signature -2. `app/sessions/new.tsx` - Pass full repository object instead of just ID -3. `services/api/__tests__/sessions-helper.test.ts` - Update test expectations -4. `app/sessions/__tests__/new.test.tsx` - Update integration test expectations - -## Next Steps - -1. **Immediate**: Document this issue (✅ Done) -2. **Short-term**: Contact backend team to verify API contract -3. **Before production**: Test with real backend API -4. **If mismatch confirmed**: Implement Option 1 or 3 above - -## Related Files - -- `services/api/sessions.ts:71-89` - The problematic function -- `types/session.ts:41-47` - CreateSessionRequest interface -- `types/api.ts` - Repository interface -- `services/api/__tests__/sessions-helper.test.ts:129-146` - Test verifying current behavior -- `app/sessions/new.tsx:55-83` - Usage of createSessionFromRepo - ---- - -**Created**: 2025-11-27 (Phase 11 verification) -**Last Updated**: 2025-11-27 -**Owner**: Jeremy Eder -**Status**: NEEDS BACKEND VERIFICATION From 819dd56e237348232ac7d8146bcecfd9e5109b10 Mon Sep 17 00:00:00 2001 From: Jeremy Eder Date: Tue, 9 Dec 2025 00:03:48 -0500 Subject: [PATCH 3/5] fix: remove unused Quick Action callbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed unused renderQuickAction and quickActionKeyExtractor callbacks that were created for FlatList but not used after refactoring to use direct .map() instead. Fixes ESLint warnings introduced in PR #33. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- app/(tabs)/index.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 433a484..764bb49 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -167,14 +167,6 @@ export default function DashboardScreen() { [runningSessions.length, unreadCount, router] ) - // Render callback for Quick Action buttons - const renderQuickAction = useCallback( - ({ item }: { item: QuickAction }) => , - [colors] - ) - - const quickActionKeyExtractor = useCallback((item: QuickAction) => item.id, []) - if (authLoading) { return ( From 1fc1fae8a94af325965d53b1a884a4333b614070 Mon Sep 17 00:00:00 2001 From: Jeremy Eder Date: Tue, 9 Dec 2025 00:14:48 -0500 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20resolve=20all=20ESLint=20warnings=20?= =?UTF-8?q?(26=20=E2=86=92=200)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed pre-existing lint issues in codebase to allow PR to pass CI: Performance & Imports: - app/_layout.tsx: Converted require() to ES6 imports - components/PerformanceMonitor.tsx: Converted require() to ES6 imports, removed unused getMemoryMonitor Unused Variables: - app/admin/*.tsx: Removed unused setPeriod setters (3 files) - app/login.tsx: Removed unused Image import - components/inbox/StuckAgentBanner.tsx: Removed unused router import - app/decisions/[id].tsx: Removed unused error variable - app/sessions/[id]/review.tsx: Removed unused error variables (2 instances) - app/settings/appearance.tsx: Fixed ThemeOption name collision, removed unused Ionicons React Hooks Dependencies: - app/announcements/index.tsx: Wrapped markAsRead in useCallback - app/auth/callback.tsx: Added eslint-disable for intentional one-time effect, removed unused OAUTH_CONFIG - app/settings/index.tsx: Added eslint-disable for intentional one-time effect - components/layout/Header.tsx: Reordered functions to avoid forward reference, added handleSignOut to showUserMenu dependencies All changes are fixes to pre-existing code quality issues, not regressions from PR #33. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .gitignore | 1 + app/_layout.tsx | 7 ++--- app/admin/platforms.tsx | 2 +- app/admin/signals.tsx | 2 +- app/admin/users.tsx | 2 +- app/announcements/index.tsx | 23 ++++++++------- app/auth/callback.tsx | 2 +- app/decisions/[id].tsx | 4 +-- app/login.tsx | 1 - app/sessions/[id]/review.tsx | 4 +-- app/settings/appearance.tsx | 9 +++--- app/settings/index.tsx | 1 + components/PerformanceMonitor.tsx | 14 ++------- components/inbox/StuckAgentBanner.tsx | 1 - components/layout/Header.tsx | 42 +++++++++++++-------------- 15 files changed, 54 insertions(+), 61 deletions(-) diff --git a/.gitignore b/.gitignore index fb34255..21918de 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,4 @@ __pycache__/ # Implementation scripts implement-phases.sh +.claude-learnings.md diff --git a/app/_layout.tsx b/app/_layout.tsx index 5a9ce5a..262f338 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -11,6 +11,9 @@ import { errorHandler } from '@/utils/errorHandler' import { initializeSentry } from '@/services/monitoring/sentry' import { useEffect, useState } from 'react' import { View, Text, TouchableOpacity, StyleSheet } from 'react-native' +import { startMemoryMonitoring } from '@/utils/performanceMonitor' +import { startFPSMonitoring } from '@/utils/fpsMonitor' +import { getRenderTracker } from '@/utils/renderTracker' // Singleton QueryClient instance (prevents memory leaks and cache loss) let queryClient: QueryClient | null = null @@ -129,9 +132,6 @@ export default function RootLayout() { // Initialize performance monitoring in development useEffect(() => { if (__DEV__) { - const { startMemoryMonitoring } = require('@/utils/performanceMonitor') - const { startFPSMonitoring } = require('@/utils/fpsMonitor') - // Start monitoring const memoryMonitor = startMemoryMonitoring({ checkIntervalMs: 15000, // Check every 15 seconds @@ -155,7 +155,6 @@ export default function RootLayout() { report: () => { memoryMonitor.printReport() fpsMonitor.printReport() - const { getRenderTracker } = require('@/utils/renderTracker') getRenderTracker().printReport() }, } diff --git a/app/admin/platforms.tsx b/app/admin/platforms.tsx index 969ed88..159e0fe 100644 --- a/app/admin/platforms.tsx +++ b/app/admin/platforms.tsx @@ -8,7 +8,7 @@ import { ADMIN_METRICS } from '@/constants/AdminMetrics' import type { PlatformPeriod, OSVersionBreakdown } from '@/services/analytics/types' export default function PlatformComparisonDashboard() { - const [period, setPeriod] = useState('30d') + const [period] = useState('30d') const { data, isLoading, error, refetch, isRefetching } = usePlatforms(period) const onRefresh = () => { diff --git a/app/admin/signals.tsx b/app/admin/signals.tsx index 75f9b25..01517a2 100644 --- a/app/admin/signals.tsx +++ b/app/admin/signals.tsx @@ -12,7 +12,7 @@ import { ADMIN_METRICS } from '@/constants/AdminMetrics' import type { GoldenSignalsPeriod } from '@/services/analytics/types' export default function GoldenSignalsDashboard() { - const [period, setPeriod] = useState('7d') + const [period] = useState('7d') const { data, isLoading, error, refetch, isRefetching } = useGoldenSignals(period) const onRefresh = () => { diff --git a/app/admin/users.tsx b/app/admin/users.tsx index 888eea0..57a9ee2 100644 --- a/app/admin/users.tsx +++ b/app/admin/users.tsx @@ -9,7 +9,7 @@ import { ADMIN_METRICS } from '@/constants/AdminMetrics' import type { EngagementPeriod } from '@/services/analytics/types' export default function EngagementDashboard() { - const [period, setPeriod] = useState('24h') + const [period] = useState('24h') const { data, isLoading, error, refetch, isRefetching } = useEngagement(period) const onRefresh = () => { diff --git a/app/announcements/index.tsx b/app/announcements/index.tsx index 2350014..0134186 100644 --- a/app/announcements/index.tsx +++ b/app/announcements/index.tsx @@ -39,15 +39,18 @@ export default function AnnouncementsScreen() { } } - const markAsRead = async (announcementId: string) => { - try { - const newReadIds = [...readIds, announcementId] - setReadIds(newReadIds) - await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(newReadIds)) - } catch (error) { - console.error('Failed to mark announcement as read:', error) - } - } + const markAsRead = useCallback( + async (announcementId: string) => { + try { + const newReadIds = [...readIds, announcementId] + setReadIds(newReadIds) + await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(newReadIds)) + } catch (error) { + console.error('Failed to mark announcement as read:', error) + } + }, + [readIds] + ) const handleAnnouncementPress = useCallback( (announcement: Announcement) => { @@ -55,7 +58,7 @@ export default function AnnouncementsScreen() { markAsRead(announcement.id) } }, - [readIds] + [readIds, markAsRead] ) const isUnread = useCallback( diff --git a/app/auth/callback.tsx b/app/auth/callback.tsx index ed2cbd8..822a892 100644 --- a/app/auth/callback.tsx +++ b/app/auth/callback.tsx @@ -3,7 +3,6 @@ import { View, Text, ActivityIndicator, StyleSheet } from 'react-native' import { useRouter, useLocalSearchParams } from 'expo-router' import { AuthAPI } from '@/services/api/auth' import { OAuthService } from '@/services/auth/oauth' -import { OAUTH_CONFIG } from '@/utils/constants' /** * OAuth Callback Handler @@ -22,6 +21,7 @@ export default function AuthCallbackScreen() { useEffect(() => { handleCallback() + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const handleCallback = async () => { diff --git a/app/decisions/[id].tsx b/app/decisions/[id].tsx index 67acc46..69c82e1 100644 --- a/app/decisions/[id].tsx +++ b/app/decisions/[id].tsx @@ -53,7 +53,7 @@ export default function ReviewDecisionScreen() { const handleComplete = async () => { try { - const result = await completeDecisionReview(decision.id, { + await completeDecisionReview(decision.id, { comment, quickResponse: quickResponse || undefined, viewedSections: Array.from(viewedSections), @@ -65,7 +65,7 @@ export default function ReviewDecisionScreen() { 'Your review has been submitted. (Undo functionality will be in full implementation)', [{ text: 'OK', onPress: () => router.back() }] ) - } catch (error) { + } catch { Alert.alert('Error', 'Failed to complete review') } } diff --git a/app/login.tsx b/app/login.tsx index c3466c3..c9a98f8 100644 --- a/app/login.tsx +++ b/app/login.tsx @@ -3,7 +3,6 @@ import { View, Text, StyleSheet, - Image, TouchableOpacity, ActivityIndicator, Alert, diff --git a/app/sessions/[id]/review.tsx b/app/sessions/[id]/review.tsx index 1438fa1..85d2a3d 100644 --- a/app/sessions/[id]/review.tsx +++ b/app/sessions/[id]/review.tsx @@ -56,7 +56,7 @@ export default function SessionReviewScreen() { }) router.back() - } catch (error) { + } catch { Alert.alert('Error', 'Failed to submit review. Please try again.') } finally { setIsSubmitting(false) @@ -92,7 +92,7 @@ export default function SessionReviewScreen() { }) router.back() - } catch (error) { + } catch { Alert.alert('Error', 'Failed to approve session. Please try again.') } finally { setIsSubmitting(false) diff --git a/app/settings/appearance.tsx b/app/settings/appearance.tsx index 648d7f5..0f82373 100644 --- a/app/settings/appearance.tsx +++ b/app/settings/appearance.tsx @@ -1,23 +1,22 @@ import React, { useState, useEffect } from 'react' import { View, Text, TouchableOpacity, StyleSheet } from 'react-native' -import { Ionicons } from '@expo/vector-icons' import { useTheme } from '../../hooks/useTheme' import { useOffline } from '../../hooks/useOffline' import { OfflineBanner } from '../../components/ui/OfflineBanner' import { PreferencesService } from '../../services/storage/preferences' -type ThemeOption = 'light' | 'dark' | 'system' +type ThemeMode = 'light' | 'dark' | 'system' export default function AppearanceSettingsScreen() { const { theme, setThemeMode } = useTheme() const { isOffline } = useOffline() - const [selectedTheme, setSelectedTheme] = useState(theme) + const [selectedTheme, setSelectedTheme] = useState(theme) useEffect(() => { setSelectedTheme(theme) }, [theme]) - async function handleThemeChange(newTheme: ThemeOption) { + async function handleThemeChange(newTheme: ThemeMode) { setSelectedTheme(newTheme) setThemeMode(newTheme) @@ -62,7 +61,7 @@ export default function AppearanceSettingsScreen() { interface ThemeOptionProps { label: string description: string - value: ThemeOption + value: ThemeMode selected: boolean onSelect: () => void } diff --git a/app/settings/index.tsx b/app/settings/index.tsx index 3b951a3..3b92f4c 100644 --- a/app/settings/index.tsx +++ b/app/settings/index.tsx @@ -21,6 +21,7 @@ export default function SettingsScreen() { useEffect(() => { loadProfile() + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) async function loadProfile() { diff --git a/components/PerformanceMonitor.tsx b/components/PerformanceMonitor.tsx index fc173aa..64bb2b0 100644 --- a/components/PerformanceMonitor.tsx +++ b/components/PerformanceMonitor.tsx @@ -8,9 +8,9 @@ import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from 'react-native' import { useState, useEffect } from 'react' import { useTheme } from '@/hooks/useTheme' -import type { MemoryStats } from '@/utils/performanceMonitor' -import type { FPSStats } from '@/utils/fpsMonitor' -import type { RenderStats } from '@/utils/renderTracker' +import { getMemoryMonitor, type MemoryStats } from '@/utils/performanceMonitor' +import { getFPSMonitor, type FPSStats } from '@/utils/fpsMonitor' +import { getRenderTracker, type RenderStats } from '@/utils/renderTracker' interface PerformanceMetrics { memory: MemoryStats | null @@ -32,10 +32,6 @@ export function PerformanceMonitor() { // Update metrics every second const interval = setInterval(() => { - const { getMemoryMonitor } = require('@/utils/performanceMonitor') - const { getFPSMonitor } = require('@/utils/fpsMonitor') - const { getRenderTracker } = require('@/utils/renderTracker') - setMetrics({ memory: getMemoryMonitor().getCurrentStats(), fps: getFPSMonitor().getStats(), @@ -213,10 +209,6 @@ export function PerformanceMonitor() { { - const { getRenderTracker } = require('@/utils/renderTracker') - const { getMemoryMonitor } = require('@/utils/performanceMonitor') - const { getFPSMonitor } = require('@/utils/fpsMonitor') - getRenderTracker().reset() getFPSMonitor().reset() console.log('🔄 Performance metrics reset') diff --git a/components/inbox/StuckAgentBanner.tsx b/components/inbox/StuckAgentBanner.tsx index 6f8eb25..2da40cd 100644 --- a/components/inbox/StuckAgentBanner.tsx +++ b/components/inbox/StuckAgentBanner.tsx @@ -3,7 +3,6 @@ import { View, Text, StyleSheet, TouchableOpacity } from 'react-native' import { StuckAgent } from '@/types/inbox' import { useTheme } from '@/hooks/useTheme' import { AgentAvatar } from '../ui/AgentAvatar' -import { router } from 'expo-router' interface StuckAgentBannerProps { agents: StuckAgent[] diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx index 53bff3d..3f4b555 100644 --- a/components/layout/Header.tsx +++ b/components/layout/Header.tsx @@ -87,6 +87,26 @@ export const Header = memo(({ isRefetching = false }: HeaderProps) => { const openNotifications = useCallback(() => setNotificationsVisible(true), []) const closeNotifications = useCallback(() => setNotificationsVisible(false), []) + // Sign out handler (defined before showUserMenu to avoid forward reference) + const handleSignOut = useCallback(async () => { + Alert.alert('Sign Out', 'Are you sure?', [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Sign Out', + style: 'destructive', + onPress: async () => { + try { + await PreferencesService.clearAll() + // logout() will clear auth context and tokens + router.replace('/login') + } catch (error) { + console.error('Sign out failed:', error) + } + }, + }, + ]) + }, []) + // User menu handler const showUserMenu = useCallback(() => { if (Platform.OS === 'ios') { @@ -116,27 +136,7 @@ export const Header = memo(({ isRefetching = false }: HeaderProps) => { { text: 'Cancel', style: 'cancel' }, ]) } - }, []) - - // Sign out handler - const handleSignOut = useCallback(async () => { - Alert.alert('Sign Out', 'Are you sure?', [ - { text: 'Cancel', style: 'cancel' }, - { - text: 'Sign Out', - style: 'destructive', - onPress: async () => { - try { - await PreferencesService.clearAll() - // logout() will clear auth context and tokens - router.replace('/login') - } catch (error) { - console.error('Sign out failed:', error) - } - }, - }, - ]) - }, []) + }, [handleSignOut]) return ( From d98e7f6b162c74b1eb01dbad1623de672fd271dd Mon Sep 17 00:00:00 2001 From: Jeremy Eder Date: Tue, 9 Dec 2025 00:27:54 -0500 Subject: [PATCH 5/5] fix: address code review feedback from PR #33 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented all critical and high-impact improvements identified by code-reviewer and code-simplifier agents. **Critical Issues Fixed:** - Implemented source filtering logic in notifications screen - Filters notifications by source (GitHub, Google, Jira, etc.) - Shows empty state for unsupported sources (temporary until multi-source support) - Removed console.log from RFE create screen (production code) - Added input validation for RFE source URLs/IDs - Google Docs URLs validated - URLs validated with protocol check - Jira ticket IDs validated with format check - User-friendly error alerts for invalid inputs **Code Simplifications:** - Replaced getSourceIcon switch statement with SOURCE_ICONS const map - Removed over-memoization in Header component - Moved getInitials to pure function outside component - Removed unnecessary useMemo from firstName and userInitials - Removed useCallback from simple setter functions - Pre-sorted CreateFAB options array to eliminate runtime sorting - Removed additional console.log statements from CreateFAB **Configuration:** - Added .venv/ to .prettierignore to prevent Python venv formatting errors All changes pass TypeScript type-check, ESLint, and Prettier validation. Pre-existing test failures in unrelated components (WorkflowTypeGrid, RepositoryPicker) are not caused by these changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .prettierignore | 1 + app/notifications/index.tsx | 13 +++++- app/rfe/create.tsx | 71 ++++++++++++++++++++++----------- components/layout/CreateFAB.tsx | 11 ++--- components/layout/Header.tsx | 34 ++++++++-------- 5 files changed, 80 insertions(+), 50 deletions(-) diff --git a/.prettierignore b/.prettierignore index f5065e3..7c8ae32 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,6 +2,7 @@ node_modules/ package-lock.json yarn.lock +.venv/ # Build outputs dist/ diff --git a/app/notifications/index.tsx b/app/notifications/index.tsx index 048c269..bf0c8f7 100644 --- a/app/notifications/index.tsx +++ b/app/notifications/index.tsx @@ -35,6 +35,17 @@ export default function NotificationsScreen() { { label: 'Miro', value: 'miro', icon: 'grid' }, ] + // Filter notifications by source + // Note: Currently only GitHub notifications are supported in the data model. + // When multi-source support is added, this will filter based on notification.source + const filteredNotifications = React.useMemo(() => { + if (sourceFilter === 'all' || sourceFilter === 'github') { + return notifications + } + // Other sources not yet supported - return empty array + return [] + }, [notifications, sourceFilter]) + const handleNotificationPress = useCallback( (notification: GitHubNotification) => { showActions(notification, () => { @@ -178,7 +189,7 @@ export default function NotificationsScreen() { {/* Notifications List */} ( diff --git a/app/rfe/create.tsx b/app/rfe/create.tsx index 902dcd7..147928b 100644 --- a/app/rfe/create.tsx +++ b/app/rfe/create.tsx @@ -7,6 +7,7 @@ import { ScrollView, StyleSheet, Platform, + Alert, } from 'react-native' import { useRouter } from 'expo-router' import { Feather } from '@expo/vector-icons' @@ -18,6 +19,38 @@ interface Source { value: string } +// Icon mapping for source types +const SOURCE_ICONS = { + gdoc: 'file-text', + url: 'link', + jira: 'package', +} as const + +// Input validation for different source types +const validateSource = (type: 'gdoc' | 'url' | 'jira', value: string): string | null => { + const trimmed = value.trim() + if (!trimmed) return 'Source cannot be empty' + + switch (type) { + case 'gdoc': + if (!trimmed.includes('docs.google.com/document')) { + return 'Please enter a valid Google Docs URL' + } + break + case 'url': + if (!/^https?:\/\/.+\..+/.test(trimmed)) { + return 'Please enter a valid URL (must start with http:// or https://)' + } + break + case 'jira': + if (!/^[A-Z]+-\d+$/.test(trimmed)) { + return 'Please enter a valid Jira ticket ID (e.g., PROJ-123)' + } + break + } + return null +} + export default function CreateRFEScreen() { const { colors } = useTheme() const router = useRouter() @@ -28,15 +61,19 @@ export default function CreateRFEScreen() { const [newSourceValue, setNewSourceValue] = useState('') const addSource = () => { - if (newSourceValue.trim()) { - const newSource: Source = { - id: Date.now().toString(), - type: newSourceType, - value: newSourceValue.trim(), - } - setSources([...sources, newSource]) - setNewSourceValue('') + const validationError = validateSource(newSourceType, newSourceValue) + if (validationError) { + Alert.alert('Invalid Source', validationError) + return + } + + const newSource: Source = { + id: Date.now().toString(), + type: newSourceType, + value: newSourceValue.trim(), } + setSources([...sources, newSource]) + setNewSourceValue('') } const removeSource = (id: string) => { @@ -45,24 +82,10 @@ export default function CreateRFEScreen() { const handleSubmit = () => { // TODO: Implement batch mode submission with attached gdoc design document - console.log('Creating RFE in batch mode:', { title, description, sources }) // For now, just navigate back router.back() } - const getSourceIcon = (type: 'gdoc' | 'url' | 'jira') => { - switch (type) { - case 'gdoc': - return 'file-text' - case 'url': - return 'link' - case 'jira': - return 'package' - default: - return 'file' - } - } - return ( {/* Header */} @@ -139,7 +162,7 @@ export default function CreateRFEScreen() { accessibilityLabel={`Select ${type} type`} > @@ -190,7 +213,7 @@ export default function CreateRFEScreen() { { backgroundColor: colors.card, borderColor: colors.border }, ]} > - + {source.type.toUpperCase()} diff --git a/components/layout/CreateFAB.tsx b/components/layout/CreateFAB.tsx index e947811..252c44c 100644 --- a/components/layout/CreateFAB.tsx +++ b/components/layout/CreateFAB.tsx @@ -21,31 +21,28 @@ export function CreateFAB() { const router = useRouter() const [modalVisible, setModalVisible] = useState(false) - // @ts-expect-error lucide-react-native icon name type complexity + // Options are pre-sorted alphabetically to avoid runtime sorting const createOptions: CreateOption[] = [ - { id: 'deep-research', label: 'Deep Research', icon: 'search', soon: false }, { id: 'rfe', label: 'Create RFE', icon: 'file-plus', route: '/rfe/create' }, + { id: 'deep-research', label: 'Deep Research', icon: 'search', soon: false }, { id: 'scheduled-task', label: 'Scheduled Task', icon: 'clock', soon: false }, { id: 'session', label: 'Session', icon: 'zap', route: '/sessions/new' }, { id: 'skill', label: 'Skill', icon: 'target', soon: false }, { id: 'workflow', label: 'Workflow', icon: 'git-branch', soon: true }, - ].sort((a, b) => a.label.localeCompare(b.label)) + ] const handleOptionPress = (option: CreateOption) => { setModalVisible(false) if (option.soon) { // TODO: Show "Coming Soon" toast - console.log(`${option.label} coming soon!`) return } if (option.route) { router.push(option.route as any) - } else { - // TODO: Navigate to specific creation screens when implemented - console.log(`Create ${option.label} - Not implemented yet`) } + // TODO: Navigate to specific creation screens when implemented } return ( diff --git a/components/layout/Header.tsx b/components/layout/Header.tsx index 3f4b555..68b45d7 100644 --- a/components/layout/Header.tsx +++ b/components/layout/Header.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo, useCallback, memo } from 'react' +import React, { useState, useEffect, useCallback, memo } from 'react' import { View, Text, @@ -25,6 +25,15 @@ interface HeaderProps { const STORAGE_KEY = '@acp_read_announcements' +// Pure function - no need for memoization +function getInitials(name: string): string { + const parts = name.split(' ') + if (parts.length >= 2) { + return parts[0][0] + parts[1][0] + } + return name[0] +} + // Memoized Header component to prevent unnecessary re-renders export const Header = memo(({ isRefetching = false }: HeaderProps) => { const { colors } = useTheme() @@ -68,24 +77,13 @@ export const Header = memo(({ isRefetching = false }: HeaderProps) => { return () => clearInterval(interval) }, []) - // Memoize getInitials function - const getInitials = useCallback((name: string) => { - const parts = name.split(' ') - if (parts.length >= 2) { - return parts[0][0] + parts[1][0] - } - return name[0] - }, []) - - // Memoize firstName to prevent recalculation - const firstName = useMemo(() => (user?.name ? user.name.split(' ')[0] : 'there'), [user?.name]) - - // Memoize user initials - const userInitials = useMemo(() => (user ? getInitials(user.name) : 'U'), [user, getInitials]) + // Simple derived values - no need for memoization + const firstName = user?.name ? user.name.split(' ')[0] : 'there' + const userInitials = user ? getInitials(user.name) : 'U' - // Memoize notifications toggle callback - const openNotifications = useCallback(() => setNotificationsVisible(true), []) - const closeNotifications = useCallback(() => setNotificationsVisible(false), []) + // Simple setters - no need for useCallback + const openNotifications = () => setNotificationsVisible(true) + const closeNotifications = () => setNotificationsVisible(false) // Sign out handler (defined before showUserMenu to avoid forward reference) const handleSignOut = useCallback(async () => {